Poseidon - merge project

This commit is contained in:
Michal Buzik 2026-09-02 16:18:21 +02:00
parent 8d42232589
commit b8e54e1fc9
7 changed files with 287 additions and 75 deletions

View File

@ -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<IPoseidonReadOperation> 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 "<null>";
var descriptions = new List<string>();
for (int index = 0; index < readers.Length; index++)
{
IRegReader reader = readers[index];
if (reader == null)
{
descriptions.Add(index + ":<null>");
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 ? "<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);
}
/// <summary>Start this operation</summary>
@ -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
/// <returns>Event.ResultsPrinted</returns>
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<IPoseidonReadOperation>();
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<string>();
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;

View File

@ -300,6 +300,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public void AddRunAndCaptureJsonAsync<T>(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)
{

View File

@ -46,4 +46,39 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return allReadersFinished;
}
}
/// <summary>
/// Owns one dialog phase (START or STOP). A reader is armed once per phase,
/// independently of its terminal state from a preceding phase.
/// </summary>
public sealed class PoseidonReadPhaseRunner
{
private readonly bool readStart;
private readonly HashSet<IPoseidonReadOperation> startedReaders =
new HashSet<IPoseidonReadOperation>();
public int IterationCount { get; private set; }
public PoseidonReadPhaseRunner(bool readStart)
{
this.readStart = readStart;
}
public bool RunIteration(IEnumerable<IPoseidonReadOperation> 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;
}
}
}

View File

@ -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.");

View File

@ -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);
}
}
}

View File

@ -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()
{

View File

@ -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<IPoseidonReadOperation> { 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()
{