diff --git a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs index 074e4e55e..6f4d39027 100644 --- a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs +++ b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs @@ -41,6 +41,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd System.Windows.Forms.Form modelessDlg; + bool modelessDialogOpening; + bool modelessDialogWaitLogged; public bool Completed { get { return (modelessDlg is IHasCompleted) ? (modelessDlg as IHasCompleted).Completed : true; } } double refVolume; @@ -70,6 +72,15 @@ namespace TBF.Rig.DataEntry.PoseidonCmd } ReadDataOp readDataOp; + // Keep the CLI phase between state-machine ticks. Blocking here prevents + // the state machine from processing a UI STOP request. + PoseidonReadPhaseRunner poseidonPhaseRunner; + List poseidonPhaseReaders; + DateTime poseidonPhaseStartedAt; + DateTime poseidonNextProgressLogAt; + bool sendStartPhaseInitialized; + int sendStartPhaseIterations; + public EntryForm() { } @@ -126,6 +137,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd //entryFormCfg.Direction = Direction.S640; //currentOp = CurrentOp.EnterTestStartStates; this.regReaders = regReaders; + log.InfoFormat("Poseidon data-entry requested START; operation={0}, batchWMs={1}, readers=[{2}]", + currentOp, waterMeters == null ? -1 : waterMeters.Count, DescribeReaders(regReaders)); int iterator = 0; foreach (IRegReader reader in regReaders) { @@ -167,35 +180,105 @@ namespace TBF.Rig.DataEntry.PoseidonCmd this.refVolume = refVolume; this.errLimLo = errLimLo; this.errLimHi = errLimHi; + log.InfoFormat("Poseidon data-entry requested END; operation={0}, batchWMs={1}, refVolume={2}, errLow={3}, errHigh={4}, readers=[{5}]", + currentOp, waterMeters == null ? -1 : waterMeters.Count, refVolume, errLimLo, errLimHi, + DescribeReaders(regReaders)); return this; } delegate void EntryFormDlgt(EntryForm myRef); + + string DescribeReaders(IRegReader[] readers) + { + if (readers == null) return ""; + + var descriptions = new List(); + for (int index = 0; index < readers.Length; index++) + { + IRegReader reader = readers[index]; + if (reader == null) + { + descriptions.Add(index + ":"); + continue; + } + + PoseidonReader poseidonReader = reader as PoseidonReader; + descriptions.Add(poseidonReader == null + ? index + ":" + reader.GetType().Name + : string.Format("{0}:{1}(COM{2},state={3})", index, poseidonReader.Name, + poseidonReader.ComPortNr, poseidonReader.CurrentOp)); + } + return string.Join("; ", descriptions); + } + + int EnabledWaterMetersCount() + { + if (disabled == null) return TBF.Data.WMsCount; + int count = 0; + for (int index = 0; index < disabled.Length; index++) + if (!disabled[index]) count++; + return count; + } /// void OpenBeginningDlg(EntryForm myRef) { if (ShowForm == 0) return; + log.DebugFormat("Poseidon data-entry: creating CycleBeginningForm; configuredWMs={0}, batchWMs={1}", + TBF.Data.WMsCount, waterMeters == null ? -1 : waterMeters.Count); myRef.modelessDlg = new CycleBeginningForm(TBF.Data.WMsCount, myRef.entryFormCfg, ProcessData.SelectedProcedure.OrderInfo != null ? ProcessData.SelectedProcedure.OrderInfo.POName : string.Empty); (myRef.modelessDlg as CycleBeginningForm)?.AutoClickOkAfterDelay(); modelessDlg.Show(); } + + // The state machine runs on a worker thread. A synchronous Invoke can + // deadlock it while the UI is waiting for the next state-machine tick. + void BeginOpenDialog(string dialogName, EntryFormDlgt openDialog) + { + modelessDialogOpening = true; + modelessDialogWaitLogged = false; + log.InfoFormat("Poseidon data-entry: queueing dialog={0}, operation={1}, showForm={2}, readers=[{3}]", + dialogName, currentOp, ShowForm, DescribeReaders(regReaders)); + Program.MainWnd.BeginInvoke(new Action(() => + { + try + { + log.DebugFormat("Poseidon data-entry: opening dialog={0}, operation={1}", dialogName, currentOp); + openDialog(this); + log.InfoFormat("Poseidon data-entry: dialog opened={0}, operation={1}, formType={2}", + dialogName, currentOp, modelessDlg == null ? "" : modelessDlg.GetType().Name); + } + finally + { + modelessDialogOpening = false; + } + })); + } /// void OpenTestStartStatesDlg(EntryForm myRef) { if (ShowForm == 0) return; + log.DebugFormat("Poseidon data-entry: creating TestStartEndForm START; configuredWMs={0}, enabledWMs={1}, batchWMs={2}, readers=[{3}]", + TBF.Data.WMsCount, EnabledWaterMetersCount(), waterMeters == null ? -1 : waterMeters.Count, DescribeReaders(regReaders)); myRef.modelessDlg = new TestStartEndForm(myRef.waterMeters.Count, myRef.regReaders, disabled); modelessDlg.Show(); + log.DebugFormat("Poseidon data-entry: TestStartEndForm START visible={0}, handleCreated={1}", + modelessDlg.Visible, modelessDlg.IsHandleCreated); } /// void OpenTestEndStatesDlg(EntryForm myRef) { if (ShowForm == 0) return; + log.DebugFormat("Poseidon data-entry: creating TestStartEndForm END; configuredWMs={0}, enabledWMs={1}, batchWMs={2}, refVolume={3}, errLow={4}, errHigh={5}, readers=[{6}]", + TBF.Data.WMsCount, EnabledWaterMetersCount(), waterMeters == null ? -1 : waterMeters.Count, + refVolume, errLimLo, errLimHi, DescribeReaders(regReaders)); myRef.modelessDlg = new TestStartEndForm(TBF.Data.WMsCount, myRef.regReaders, wmStartStateStr, disabled, refVolume, errLimLo, errLimHi); modelessDlg.Show(); + log.DebugFormat("Poseidon data-entry: TestStartEndForm END visible={0}, handleCreated={1}", + modelessDlg.Visible, modelessDlg.IsHandleCreated); } /// Start this operation @@ -204,6 +287,12 @@ namespace TBF.Rig.DataEntry.PoseidonCmd readDataOp = ReadDataOp.None; readAndSetDataToMeters = false; filedDataToMeters = false; + poseidonPhaseRunner = null; + poseidonPhaseReaders = null; + sendStartPhaseInitialized = false; + sendStartPhaseIterations = 0; + modelessDialogOpening = false; + modelessDialogWaitLogged = false; if (ShowForm == 0) return ; @@ -215,7 +304,7 @@ namespace TBF.Rig.DataEntry.PoseidonCmd //ReadAndSetDataToMeters(); if (ProcessData.SelectedProcedure.OrderInfo == null) { - Program.MainWnd.Invoke(new EntryFormDlgt(OpenBeginningDlg), this); + BeginOpenDialog("CycleBeginningForm", OpenBeginningDlg); } else { @@ -225,10 +314,10 @@ namespace TBF.Rig.DataEntry.PoseidonCmd } break; case CurrentOp.ReadDatastream_StartStates: - Program.MainWnd.Invoke(new EntryFormDlgt(OpenTestStartStatesDlg), this); + BeginOpenDialog("TestStartEndForm.START", OpenTestStartStatesDlg); break; case CurrentOp.ReadDatastream_EndStates: - Program.MainWnd.Invoke(new EntryFormDlgt(OpenTestEndStatesDlg), this); + BeginOpenDialog("TestStartEndForm.END", OpenTestEndStatesDlg); break; } } @@ -240,6 +329,17 @@ namespace TBF.Rig.DataEntry.PoseidonCmd /// Event.ResultsPrinted public Event Run() { + if (modelessDialogOpening) + { + if (!modelessDialogWaitLogged) + { + log.WarnFormat("Poseidon data-entry: state machine waiting for dialog open; operation={0}, readers=[{1}]", + currentOp, DescribeReaders(regReaders)); + modelessDialogWaitLogged = true; + } + return Event.ModelessFormIsOpen; + } + if (!readAndSetDataToMeters) // run until not finished readAndSetDataToMeters = ReadAndSetDataToMeters(); @@ -352,7 +452,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd } } resultSaved = true; - modelessDlg = null; + modelessDlg = null; + modelessDialogOpening = false; } return Event.ModelessFormClosed; /// Form closed @@ -369,6 +470,7 @@ namespace TBF.Rig.DataEntry.PoseidonCmd currentOp = CurrentOp.None; readAndSetDataToMeters = false; filedDataToMeters = false; + sendStartPhaseInitialized = false; } public int ShowForm @@ -389,93 +491,108 @@ namespace TBF.Rig.DataEntry.PoseidonCmd bool bOperationSuccess = false; if (currentOp == CurrentOp.SendStartDataStream) { - bool finishedReading = regReaders == null; // we can work only with register readers - while (!finishedReading) //TODO BUMI lock - fuck ? + if (!sendStartPhaseInitialized) { - bool bAllReadersFinished = true; - foreach (var iRegReader in regReaders ) + sendStartPhaseInitialized = true; + sendStartPhaseIterations = 0; + poseidonPhaseStartedAt = DateTime.UtcNow; + poseidonNextProgressLogAt = poseidonPhaseStartedAt.AddSeconds(5); + log.InfoFormat("Poseidon send-start phase started; readers=[{0}]", DescribeReaders(regReaders)); + if (regReaders != null) { - if (iRegReader is PoseidonReader) + foreach (IRegReader regReader in regReaders) { - PoseidonReader poseidonReader = (iRegReader as PoseidonReader); + PoseidonReader poseidonReader = regReader as PoseidonReader; + if (poseidonReader == null) continue; poseidonReader.SetCliLogging(CliLogging); - - if (poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.SendStartDataStream_Done - || poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.SendStartDataStream_Runing) - { - poseidonReader.SetCurrentOp(PoseidonReader.CurrentPoseidonOp.SendStartDataStream); - } - /// Send start data stream - poseidonReader.Run(); - readDataOp = ReadDataOp.Start; - if (!(poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done - || poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error)) - { - bAllReadersFinished = false; - } + poseidonReader.SetCurrentOp(PoseidonReader.CurrentPoseidonOp.SendStartDataStream); + log.DebugFormat("Poseidon send-start: armed reader={0}, COM={1}, state={2}", + poseidonReader.Name, poseidonReader.ComPortNr, poseidonReader.CurrentOp); } } - /// Wait for all readers to finish - if (bAllReadersFinished) - { - finishedReading = true; - readDataOp = ReadDataOp.Done; - } - else - { - System.Threading.Thread.Sleep(10); - } } - bOperationSuccess = true; + sendStartPhaseIterations++; + bool allReadersFinished = true; + if (regReaders != null) + { + foreach (IRegReader regReader in regReaders) + { + PoseidonReader poseidonReader = regReader as PoseidonReader; + if (poseidonReader == null) continue; + poseidonReader.Run(); + readDataOp = ReadDataOp.Start; + if (poseidonReader.CurrentOp != PoseidonReader.CurrentPoseidonOp.Done + && poseidonReader.CurrentOp != PoseidonReader.CurrentPoseidonOp.Error) + allReadersFinished = false; + } + } + + if (allReadersFinished) + { + readDataOp = ReadDataOp.Done; + log.InfoFormat("Poseidon send-start phase completed; iterations={0}, elapsedMs={1}", + sendStartPhaseIterations, (long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds); + sendStartPhaseInitialized = false; + bOperationSuccess = true; + } + else if (DateTime.UtcNow >= poseidonNextProgressLogAt) + { + log.WarnFormat("Poseidon send-start phase waiting; iterations={0}, elapsedMs={1}, readers=[{2}]", + sendStartPhaseIterations, (long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds, + DescribeReaders(regReaders)); + poseidonNextProgressLogAt = DateTime.UtcNow.AddSeconds(5); + } } else if ((currentOp == CurrentOp.ReadDatastream_StartStates) || (currentOp == CurrentOp.ReadDatastream_EndStates)) { - bool finishedReading = regReaders == null; // we can work only with register readers - while (!finishedReading) //TODO BUMI lock - fuck ? + if (poseidonPhaseRunner == null) { - bool bAllReadersFinished = true; - foreach (var iRegReader in regReaders ) + poseidonPhaseReaders = new List(); + if (regReaders != null) { - if (iRegReader is PoseidonReader) + foreach (IRegReader regReader in regReaders) { - PoseidonReader poseidonReader = (iRegReader as PoseidonReader); - if(poseidonReader == null) - continue; + PoseidonReader poseidonReader = regReader as PoseidonReader; + if (poseidonReader == null) continue; poseidonReader.SetCliLogging(CliLogging); - // A terminal reader must not be armed again. Re-starting Error/Done - // launched the CLI on every polling iteration and kept the dialog loading. - if (poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.None) - { - log.DebugFormat("Poseidon read: starting {0} for {1}", currentOp, poseidonReader.Name); - poseidonReader.SetCurrentOp((currentOp == CurrentOp.ReadDatastream_StartStates)? - PoseidonReader.CurrentPoseidonOp.ReadDataStream_Start : - PoseidonReader.CurrentPoseidonOp.ReadDataStream_End); - } - /// Send start data stream - poseidonReader.Run(); - if (poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error) - log.ErrorFormat("Poseidon read: {0} completed with Error during {1}", poseidonReader.Name, currentOp); - if (!(poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done - || poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error)) - { - bAllReadersFinished = false; - } + poseidonPhaseReaders.Add(new PoseidonReaderOperation(poseidonReader)); } } - if (bAllReadersFinished) - { - log.DebugFormat("Poseidon read: all readers finished for {0}", currentOp); - finishedReading = true; - } - else - { - System.Threading.Thread.Sleep(10); - } + poseidonPhaseRunner = new PoseidonReadPhaseRunner( + currentOp == CurrentOp.ReadDatastream_StartStates); + poseidonPhaseStartedAt = DateTime.UtcNow; + poseidonNextProgressLogAt = poseidonPhaseStartedAt.AddSeconds(5); + log.InfoFormat("Poseidon phase started: phase={0}, readers={1}, detail=[{2}]", + currentOp, poseidonPhaseReaders.Count, DescribeReaders(regReaders)); } - bOperationSuccess = true; + bool allReadersFinished = poseidonPhaseRunner.RunIteration(poseidonPhaseReaders); + foreach (IPoseidonReadOperation poseidonReader in poseidonPhaseReaders) + if (poseidonReader.HasError) + log.ErrorFormat("Poseidon read: {0} completed with Error during {1}", poseidonReader.Name, currentOp); + + if (allReadersFinished) + { + log.InfoFormat("Poseidon phase completed: phase={0}, iterations={1}, elapsedMs={2}", + currentOp, poseidonPhaseRunner.IterationCount, + (long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds); + poseidonPhaseRunner = null; + poseidonPhaseReaders = null; + bOperationSuccess = true; + } + else if (DateTime.UtcNow >= poseidonNextProgressLogAt) + { + var pendingReaders = new List(); + foreach (IPoseidonReadOperation poseidonReader in poseidonPhaseReaders) + if (!poseidonReader.IsFinished) pendingReaders.Add(poseidonReader.Name); + log.WarnFormat("Poseidon phase waiting: phase={0}, iterations={1}, elapsedMs={2}, pending={3}", + currentOp, poseidonPhaseRunner.IterationCount, + (long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds, + string.Join(",", pendingReaders)); + poseidonNextProgressLogAt = DateTime.UtcNow.AddSeconds(5); + } } return bOperationSuccess; diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs index 6c5a05d81..03037f0ab 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs @@ -300,6 +300,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop public void AddRunAndCaptureJsonAsync(string fileName, string args) where T : new() { + log?.Debug($"CLI queued: file='{fileName}', args='{args}'"); ResetStartTime(); var cts = new CancellationTokenSource(); @@ -334,6 +335,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop try { + log?.Debug($"CLI starting: file='{psi.FileName}', args='{psi.Arguments}', workingDirectory='{psi.WorkingDirectory}', exists={System.IO.File.Exists(psi.FileName)}"); process.Start(); var stdoutTask = process.StandardOutput.ReadToEndAsync(); @@ -363,6 +365,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop info.StandardError = stderrTask.Result ?? ""; string allOutput = info.StandardOutput + info.StandardError; log?.Debug(allOutput); + log?.Debug($"CLI completed: name='{info.Name}', exitCode={info.ExitCode}, stdoutLength={info.StandardOutput.Length}, stderrLength={info.StandardError.Length}"); if (info.ExitCode != 0) { diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs index acbfe6980..5ef03773e 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs @@ -46,4 +46,39 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop return allReadersFinished; } } + + /// + /// Owns one dialog phase (START or STOP). A reader is armed once per phase, + /// independently of its terminal state from a preceding phase. + /// + public sealed class PoseidonReadPhaseRunner + { + private readonly bool readStart; + private readonly HashSet startedReaders = + new HashSet(); + + public int IterationCount { get; private set; } + + public PoseidonReadPhaseRunner(bool readStart) + { + this.readStart = readStart; + } + + public bool RunIteration(IEnumerable readers) + { + if (readers == null) return true; + + IterationCount++; + + bool allReadersFinished = true; + foreach (IPoseidonReadOperation reader in readers) + { + if (reader == null) continue; + if (startedReaders.Add(reader)) reader.Start(readStart); + reader.Run(); + if (!reader.IsFinished) allReadersFinished = false; + } + return allReadersFinished; + } + } } diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs index c4d04539b..0e3e34a84 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs @@ -294,9 +294,17 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop if ((DebugLevel == DebugMode.Normal)||(DebugLevel == DebugMode.Simulate)) { /// Prepare serial port - serialPort = new SerialPortData(string.Format("COM{0}", registerReaderCfg.ComPortNr), - registerReaderCfg.CliFileName, - registerReaderCfg.MeterType); + if (registerReaderCfg.MeterType <= 0) + { + log.WarnFormat( + "{0}: configured Poseidon MeterType={1}; using CLI default MeterType={2}.", + Name, + registerReaderCfg.MeterType, + SerialPortData.DefaultPoseidonMeterType); + } + serialPort = new SerialPortData(string.Format("COM{0}", registerReaderCfg.ComPortNr), + registerReaderCfg.CliFileName, + registerReaderCfg.MeterType); //TODO BUMI prepare serial port - for us do nothing @@ -438,6 +446,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop { _currentOp = CurrentPoseidonOp.ReadDatastream_Done; incommingTime = CliRunner.IncommingTime; + log.DebugFormat("{0}: CLI task completed for {1}; incomingTime={2}", Name, + _isReadingStart ? "START" : "STOP", incommingTime); } else if (CliRunner.TimeOutReceived(SafetyTimeOut)) { @@ -445,6 +455,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop CliRunner.CancelUndoneTasksAsTimedOut(); _currentOp = CurrentPoseidonOp.ReadDatastream_Done; incommingTime = CliRunner.IncommingTime; + log.ErrorFormat("{0}: CLI timeout for {1}; timeoutMs={2}", Name, + _isReadingStart ? "START" : "STOP", SafetyTimeOut); } return Event.Busy; @@ -468,6 +480,11 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop } else { + lastCliReadFailureReason = _lastOpTimedOut + ? "CLI read timed out." + : "No completed JsonDataFromPoseidon task was available."; + log.ErrorFormat("{0}: Poseidon {1} read failed: {2}", Name, + _isReadingStart ? "START" : "STOP", lastCliReadFailureReason); if (_lastOpTimedOut) { log.Warn($"PoseidonReader {Name}: ReadDatastream timed out, no completed result available."); diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs index 9684009f6..93fe2cc05 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs @@ -6,6 +6,9 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop public class SerialPortData { public const string CliDirectory = @"C:\TBF\Cli"; + // HatCliDemo identifies Poseidon with the numeric meter type 74. + // Older persisted configurations can contain the uninitialized value 0. + public const int DefaultPoseidonMeterType = 74; private Boolean? _cliExists; public bool CliExists { get { @@ -30,6 +33,11 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop public string PortName { get; set; } public int MeterType { get; set; } + public static int NormalizeMeterType(int meterType) + { + return meterType > 0 ? meterType : DefaultPoseidonMeterType; + } + public enum EMeterArg { Calibration = 0, AllParams = 2, @@ -59,7 +67,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop { PortName = portName; CmdClientName = cmdClientName; - MeterType = meterType; + MeterType = NormalizeMeterType(meterType); } } } diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs index 788948ee1..84f68399b 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs @@ -246,6 +246,17 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop } } + [TestMethod] + public void SerialPortData_ZeroMeterType_UsesPoseidonCliDefault() + { + var serialPort = new SerialPortData("COM3", "HatCliDemo.exe", 0); + + Assert.AreEqual(SerialPortData.DefaultPoseidonMeterType, serialPort.MeterType); + Assert.AreEqual( + "-p COM3 -m 74 --operation readall", + serialPort.DefaultArgSettings(SerialPortData.EMeterArg.AllParams)); + } + [TestMethod] public async Task CliProcess_ExitErrorWithoutJson_RecordsDiagnosticsAndReturnsNoReading() { diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs index dc181cf97..c228200ee 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs @@ -50,6 +50,27 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop foreach (FakeReader reader in readers) Assert.AreEqual(1, reader.StartCount, reader.Name); } + [TestMethod] + public void StartThenStop_PhasesArmTheSameReaderOncePerPhase() + { + var reader = new FakeReader("Pos1", 1); + var readers = new List { reader }; + var startPhase = new PoseidonReadPhaseRunner(true); + + // START phase completes. + startPhase.RunIteration(readers); + startPhase.RunIteration(readers); + Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp); + + // A new STOP phase re-arms the terminal reader exactly once. + var stopPhase = new PoseidonReadPhaseRunner(false); + stopPhase.RunIteration(readers); + stopPhase.RunIteration(readers); + + Assert.AreEqual(2, reader.StartCount); + Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp); + } + [TestMethod] public void InputJson_ZeroReading_WithDecimalCommaOrDot_IsValid() {