fix(morrisville): release 3.9.2205.1 Poseidon CLI diagnostics and results DB migration

- improve Poseidon CLI diagnostics with working-directory, exit-code, stdout/stderr, JSON and NFC/reading validation
- accept decimal dot and comma readings, including valid zero readings
- add reader-cycle, fake CLI, timeout and Poseidon parsing test coverage
- prevent legacy optical-head mode errors from crashing the UI
- add automatic MySQL/SQLite Results DB migrations for MeterTestRslt:
  FlipMode, ExtraDataPath and X1–X9
- add SQLite reference required by the migration helper
- update ReadMe_BugFix version history for releases 3.9.2149–3.9.2205
This commit is contained in:
Michal Buzik 2026-08-31 13:02:09 +02:00
parent 2fd2697e3b
commit 59cd5f9dde
19 changed files with 781 additions and 97 deletions

View File

@ -11,6 +11,7 @@ using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
using Common;
using Results.Entities;
using Results.Entities.helpers;
namespace Results
{
@ -107,7 +108,11 @@ namespace Results
throw new Exception("Connection string was not specified");
}
if (SessionFactory == null) SessionFactory = CreateSessionFactory();
if (SessionFactory == null)
{
DatabaseMigrationHelper.EnsureSchema(dbType, connectionString);
SessionFactory = CreateSessionFactory();
}
return SessionFactory.OpenSession();
}

View File

@ -53,6 +53,9 @@
<Reference Include="NHibernate">
<HintPath>..\packages\NHibernate.4.0.4.4000\lib\net40\NHibernate.dll</HintPath>
</Reference>
<Reference Include="System.Data.SQLite, Version=2.0.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\System.Data.SQLite.2.0.3\lib\net471\System.Data.SQLite.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
@ -69,6 +72,7 @@
<Compile Include="DBase.cs" />
<Compile Include="Entities\Batch.cs" />
<Compile Include="Entities\Components.cs" />
<Compile Include="Entities\helpers\DatabaseMigrationHelper.cs" />
<Compile Include="Entities\MeterTestRslt.cs" />
<Compile Include="Entities\PurchaseBox.cs" />
<Compile Include="Entities\PurchaseOrder.cs" />
@ -265,4 +269,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

View File

@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// Build Number
// Revision
//
[assembly: AssemblyVersion("3.9.2204.1")]
[assembly: AssemblyFileVersion("3.9.2204.1")]
[assembly: AssemblyVersion("3.9.2205.1")]
[assembly: AssemblyFileVersion("3.9.2205.1")]

View File

@ -3,3 +3,12 @@
| Version | Target Environment | Title | Description |
|------------|--------------------------------------------------------------|-----------------------------------------------------|------------------------------------|
| 3.9.2149.0 | HeatMeters, Heat meter sensors, Procedure Dilog, Tab Process | Excanged columns value 'Sensor' and 'Heat meter sensor' | Fix in code ProcedureDlg, row 1851 |
| 3.9.2149.1 | General release | Version iteration | Assembly and file version increment. |
| 3.9.2149.2 | Poseidon register reader | Poseidon pulse and timing handling | Added reference-pulse reading in `Run()` and improved Poseidon timing/task tracking. |
| 3.9.2149.4 | Mass collection / Poseidon start-stop | Mass collection update | Refactored standing-start mass collection, extended Poseidon start/end data-entry configuration and improved dialog/task handling. |
| 3.9.2200.1 | Test infrastructure | TBF test assembly access | Added `InternalsVisibleTo` support for `TBFTests`. |
| 3.9.2201.1 | Poseidon CLI | Poseidon CLI configuration | Added macro descriptions, refined serial-port/CLI argument configuration and extended CLI test coverage. |
| 3.9.2202.1 | Poseidon CLI | CLI release iteration | Assembly and file version increment for the Poseidon CLI workstream. |
| 3.9.2203.1 | Poseidon CLI / smart-meter sequence | CLI test and configuration update | Updated CLI executable test setup, serial-port default responses and smart-meter component-name handling. |
| 3.9.2204.1 | Morrisville / Poseidon CLI | Morrisville CLI diagnostics | Restored lost 2204 versioning, added detailed CLI command/response logging and used a fixed CLI directory for deployment. |
| 3.9.2205.1 | Morrisville / Poseidon CLI / Results DB | Poseidon read diagnostics and results DB migration | Improved Poseidon CLI execution and diagnostics (fixed CLI working directory, exit code/stdout/stderr capture, JSON/NFC/reading validation and culture-independent decimal parsing). Added reader-cycle and fake-CLI test coverage. Prevented a device/CLI error while changing the legacy optical-head mode from crashing the UI. Results DB now creates missing `MeterTestRslt` compatibility columns (`FlipMode`, `ExtraDataPath`, `X1`-`X9`) automatically for MySQL and SQLite before results are saved. |

View File

@ -433,42 +433,42 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
|| (currentOp == CurrentOp.ReadDatastream_EndStates))
{
bool finishedReading = regReaders == null; // we can work only with register readers
List<IPoseidonReadOperation> poseidonReaders = new List<IPoseidonReadOperation>();
if (regReaders != null)
{
foreach (var iRegReader in regReaders)
{
PoseidonReader poseidonReader = iRegReader as PoseidonReader;
if (poseidonReader != null)
{
poseidonReader.SetCliLogging(CliLogging);
poseidonReaders.Add(new PoseidonReaderOperation(poseidonReader));
}
}
}
while (!finishedReading) //TODO BUMI lock - fuck ?
{
bool bAllReadersFinished = true;
foreach (var iRegReader in regReaders )
Dictionary<IPoseidonReadOperation, bool> operationsBefore =
new Dictionary<IPoseidonReadOperation, bool>();
foreach (IPoseidonReadOperation poseidonReader in poseidonReaders)
{
if (iRegReader is PoseidonReader)
operationsBefore.Add(poseidonReader, poseidonReader.IsFinished);
if (poseidonReader.IsNotStarted)
log.DebugFormat("Poseidon read: starting {0} for {1}", currentOp, poseidonReader.Name);
}
bool bAllReadersFinished = PoseidonReadCycle.RunIteration(
poseidonReaders,
currentOp == CurrentOp.ReadDatastream_StartStates);
foreach (IPoseidonReadOperation poseidonReader in poseidonReaders)
{
if (poseidonReader.IsFinished && !operationsBefore[poseidonReader])
{
PoseidonReader poseidonReader = (iRegReader as PoseidonReader);
if(poseidonReader == null)
continue;
poseidonReader.SetCliLogging(CliLogging);
// Start a read only once. The previous condition restarted a
// reader after it reached Done/Error, which kept the dialog in
// Loading... and repeatedly launched the CLI for every meter.
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)
{
if (poseidonReader.HasError)
log.ErrorFormat("Poseidon read: {0} completed with Error during {1}", poseidonReader.Name, currentOp);
}
else if (poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done)
{
else
log.DebugFormat("Poseidon read: {0} completed during {1}", poseidonReader.Name, currentOp);
}
if (!(poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done
|| poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error))
{
bAllReadersFinished = false;
}
}
}
if (bAllReadersFinished)

View File

@ -259,9 +259,18 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? "");
info.ExitCode = process.ExitCode;
info.StandardOutput = stdOutTask.Result ?? "";
info.StandardError = stdErrTask.Result ?? "";
string allOutput = info.StandardOutput + info.StandardError;
log?.Debug(allOutput);
if (info.ExitCode != 0)
{
info.FailureReason = $"CLI exited with code {info.ExitCode}.";
log?.Error($"{info.Name}: {info.FailureReason} stderr='{info.StandardError}'");
}
info.State = CliTaskState.Completed;
return allOutput;
}
@ -358,9 +367,20 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? "");
info.ExitCode = process.ExitCode;
info.StandardOutput = stdoutTask.Result ?? "";
info.StandardError = stderrTask.Result ?? "";
string allOutput = info.StandardOutput + info.StandardError;
log?.Debug(allOutput);
if (info.ExitCode != 0)
{
info.FailureReason = $"CLI exited with code {info.ExitCode}.";
log?.Error($"{info.Name}: {info.FailureReason} stderr='{info.StandardError}'");
info.State = CliTaskState.Completed;
return default(T);
}
string json = ExtractJson(allOutput);
T result;
if (TryJsonStringDeserialize(json, out result))
@ -369,6 +389,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return result;
}
info.FailureReason = "CLI completed without a valid JSON response.";
log?.Error($"{info.Name}: {info.FailureReason} stdout='{info.StandardOutput}' stderr='{info.StandardError}'");
info.State = CliTaskState.Completed;
return default(T);
}
@ -391,18 +413,17 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public bool TryJsonStringDeserialize<T>(string json, out T value) where T : new()
{
if (json != null)
if (!string.IsNullOrWhiteSpace(json))
{
try
{
value = JsonConvert.DeserializeObject<T>(json);
return true;
return value != null;
}
catch (Exception ex)
{
log?.Debug(ex.Message);
value = TryConvert<T>(json);
return true;
return TryConvert(json, out value);
}
}
@ -410,7 +431,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return false;
}
private static T TryConvert<T>(string json) where T : new()
private static bool TryConvert<T>(string json, out T value) where T : new()
{
T obj = new T();
@ -428,21 +449,24 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
try
{
object value = token.ToObject(prop.PropertyType);
prop.SetValue(obj, value);
object propertyValue = token.ToObject(prop.PropertyType);
prop.SetValue(obj, propertyValue);
}
catch
{
}
}
}
value = obj;
return true;
}
catch (Exception ex)
{
Console.WriteLine($"TryConvert failed: {ex.Message}");
value = default(T);
return false;
}
return obj;
}
public string ExtractJson(string text)

View File

@ -11,6 +11,10 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public Process Process { get; set; }
public CliTaskState State { get; set; } = CliTaskState.Running;
public string Name { get; set; }
public int? ExitCode { get; set; }
public string StandardOutput { get; set; }
public string StandardError { get; set; }
public string FailureReason { get; set; }
public bool UseResult
{
@ -22,4 +26,4 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
}
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using TBF.Rig;
namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
/// <summary>
/// A single polling step for the Poseidon start/end read dialog.
///
/// A reader is armed only from None. In particular, a terminal reader
/// must not be armed again: doing so starts a new CLI process on every
/// polling iteration and leaves the dialog in Loading.
/// </summary>
public interface IPoseidonReadOperation
{
string Name { get; }
bool IsNotStarted { get; }
bool IsFinished { get; }
bool HasError { get; }
void Start(bool readStart);
Event Run();
}
public sealed class PoseidonReaderOperation : IPoseidonReadOperation
{
private readonly PoseidonReader reader;
public PoseidonReaderOperation(PoseidonReader reader)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
this.reader = reader;
}
public string Name { get { return reader.Name; } }
public bool IsNotStarted { get { return reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.None; } }
public bool IsFinished
{
get
{
return reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done ||
reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error;
}
}
public bool HasError { get { return reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error; } }
public void Start(bool readStart)
{
reader.SetCurrentOp(readStart
? PoseidonReader.CurrentPoseidonOp.ReadDataStream_Start
: PoseidonReader.CurrentPoseidonOp.ReadDataStream_End);
}
public Event Run() { return reader.Run(); }
}
public static class PoseidonReadCycle
{
public static bool RunIteration(IEnumerable<IPoseidonReadOperation> readers, bool readStart)
{
if (readers == null)
return true;
bool allReadersFinished = true;
foreach (IPoseidonReadOperation reader in readers)
{
if (reader == null)
continue;
// None means that this dialog operation has not been started yet.
// Done/Error are terminal and deliberately remain terminal.
if (reader.IsNotStarted)
reader.Start(readStart);
reader.Run();
if (!reader.IsFinished)
allReadersFinished = false;
}
return allReadersFinished;
}
}
}

View File

@ -3,6 +3,7 @@
///
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Ports;
using System.Text.RegularExpressions;
@ -338,11 +339,17 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
private static long SafetyTimeOut = 30 * 1000;
private long incommingTime = -1;
private bool _lastOpTimedOut;
private bool lastCliReadingParsed;
private string lastCliReadFailureReason;
private bool lastCliReadSucceeded;
public long DeltaTime { get{return fullTimeInMilis;}}
public long IncommingTime { get{return incommingTime;}}
public bool LastCliReadingParsed { get { return lastCliReadingParsed; } }
public string LastCliReadFailureReason { get { return lastCliReadFailureReason; } }
public bool LastCliReadSucceeded { get { return lastCliReadSucceeded; } }
/// <summary>Run this operation</summary>
/// <returns>eventDone</returns>
@ -405,6 +412,9 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
else if (_currentOp == CurrentPoseidonOp.ReadDataStream_Start
|| _currentOp == CurrentPoseidonOp.ReadDataStream_End)
{
lastCliReadingParsed = false;
lastCliReadFailureReason = null;
lastCliReadSucceeded = false;
startTimeInMilis = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
incommingTime = -1;
_isReadingStart = (_currentOp == CurrentPoseidonOp.ReadDataStream_Start);
@ -454,16 +464,30 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
var task = (Task<JsonDataFromPoseidon>)firstTaskInfo.Task;
data = task.Result;
if (data == null)
{
lastCliReadFailureReason = firstTaskInfo.FailureReason ?? "CLI returned no Poseidon JSON data.";
log.ErrorFormat("{0}: Poseidon {1} read failed. reason='{2}', exitCode={3}, stderr='{4}'",
Name,
_isReadingStart ? "Begin" : "End",
lastCliReadFailureReason,
firstTaskInfo.ExitCode,
firstTaskInfo.StandardError);
}
}
else
{
var lastTaskInfo = CliRunner.TaskPool.FindLast(t => t.Task is Task<JsonDataFromPoseidon>);
lastCliReadFailureReason = lastTaskInfo == null
? "No CLI task was created."
: lastTaskInfo.FailureReason ?? "CLI task was not usable, state=" + lastTaskInfo.State + ".";
if (_lastOpTimedOut)
{
log.Warn($"PoseidonReader {Name}: ReadDatastream timed out, no completed result available.");
log.Warn($"PoseidonReader {Name}: ReadDatastream timed out, no completed result available. {lastCliReadFailureReason}");
}
else
{
log.Warn("No completed JsonDataFromPoseidon task available.");
log.Error($"PoseidonReader {Name}: no usable JsonDataFromPoseidon task. {lastCliReadFailureReason}");
}
}
@ -471,6 +495,15 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
log.DebugFormat("{0}: CLI read result deviceId={1}, reading={2}, readingComplete={3}, nfcTagDetected={4}",
Name, data.DeviceId, data.Reading, data.ReadingComplete, data.NfcTagDetected);
string validationError;
if (!TryValidateCliReadResponse(data, out validationError))
{
lastCliReadFailureReason = validationError;
log.ErrorFormat("{0}: Poseidon {1} read rejected. {2}. Begin={3}, End={4}",
Name, _isReadingStart ? "Begin" : "End", validationError, beginWMState, endWMState);
}
else
{
if (string.IsNullOrEmpty(wmSerialNr))
{
try
@ -484,14 +517,26 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
double volume;
if (Double.TryParse(data.Reading, out volume))
if (TryParseCliReading(data.Reading, out volume))
{
lastCliReadingParsed = true;
lastCliReadSucceeded = true;
double volumeLi = Units.ConvertFrom(Unit.USgal, volume);
if (_isReadingStart)
beginWMState = volumeLi;
else
endWMState = volumeLi;
log.InfoFormat("{0}: Poseidon {1} value stored. deviceId={2}, rawReading='{3}', gallons={4}, litres={5}, Begin={6}, End={7}",
Name, _isReadingStart ? "Begin" : "End", data.DeviceId, data.Reading, volume, volumeLi, beginWMState, endWMState);
}
else
{
lastCliReadFailureReason = "Reading could not be parsed: '" + data.Reading + "'.";
log.ErrorFormat("{0}: cannot parse CLI reading '{1}' using invariant or current culture.",
Name, data.Reading);
}
}
}
@ -504,6 +549,50 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
/// <summary>
/// CLI values must not depend on the Windows UI culture. Both dot and comma
/// are accepted as a decimal separator and normalized before parsing.
/// </summary>
public static bool TryParseCliReading(string reading, out double value)
{
value = 0;
if (String.IsNullOrWhiteSpace(reading))
return false;
string normalizedReading = reading.Trim().Replace(',', '.');
return Double.TryParse(normalizedReading, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
public static bool TryValidateCliReadResponse(JsonDataFromPoseidon data, out string failureReason)
{
if (data == null)
{
failureReason = "CLI returned no JSON data.";
return false;
}
if (!data.NfcTagDetected)
{
failureReason = "NfcTagDetected=false.";
return false;
}
if (!data.ReadingComplete)
{
failureReason = "ReadingComplete=false.";
return false;
}
if (String.IsNullOrWhiteSpace(data.Reading))
{
failureReason = "Reading is empty.";
return false;
}
failureReason = null;
return true;
}
/// <summary>Stop this operation</summary>
public void Stop()

View File

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Windows.Forms;
using log4net;
using TBF.Rig.RegisterReaders.CommonRR.IPerl.communication;
using TBF.Rig.RegisterReaders.iPerlReaderUNI.common;
using TBF.Rig.RegisterReaders.PoseidonReader.implementations;
@ -11,6 +12,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonReader
{
public partial class UniHeadTestCtrl : UserControl
{
private static readonly ILog log = LogManager.GetLogger(typeof(UniHeadTestCtrl));
private IUniHeadTestCtrl _ctrl;
private IUniHeadTestCtrl Ctrl { get => _ctrl; }
@ -77,6 +79,12 @@ namespace TBF.Rig.RegisterReaders.PoseidonReader
RfidOutputListBox = rfidOutputListBox
});
}
catch (Exception exception)
{
// A device/CLI communication failure must not terminate Application.Run.
log.Error("Poseidon head test command failed.", exception);
rfidOutputListBox.Items.Add("Error: " + exception.Message);
}
finally
{
commandTestButton.Enabled = true;

View File

@ -77,21 +77,26 @@ namespace TBF.Rig.RegisterReaders.PoseidonReader.communication
internal static string SetActiveMode(PoseidonCfg iHeadCfg)
{
SerialPortData serialPortData = new SerialPortData(
$"COM{iHeadCfg.RfidComPortNr}",
iHeadCfg.CliProgramName,
iHeadCfg.CliMeterType);
NfcHeadServiceOld headService = new NfcHeadServiceOld(serialPortData);
// Wait synchronously
OptoHeadStatus optoHeadStatus = headService.SetTestingMode(OptoHeadStatus.OptoHeadDisabled);
if (optoHeadStatus == OptoHeadStatus.OptoHeadDisabled)
try
{
_lastOptoHeadStatus = optoHeadStatus;
return "OK";
SerialPortData serialPortData = new SerialPortData(
$"COM{iHeadCfg.RfidComPortNr}",
iHeadCfg.CliProgramName,
iHeadCfg.CliMeterType);
NfcHeadServiceOld headService = new NfcHeadServiceOld(serialPortData);
OptoHeadStatus optoHeadStatus = headService.SetTestingMode(OptoHeadStatus.OptoHeadDisabled);
if (optoHeadStatus == OptoHeadStatus.OptoHeadDisabled)
{
_lastOptoHeadStatus = optoHeadStatus;
return "OK";
}
return "Error Set Active Mode";
}
else
catch (Exception exception)
{
return "Error Set Test Mode";
rfidDataLogger.Error("Poseidon Set Active Mode failed.", exception);
return "Error Set Active Mode: " + exception.Message;
}
}
@ -100,23 +105,28 @@ namespace TBF.Rig.RegisterReaders.PoseidonReader.communication
internal static string SetTestMode(PoseidonCfg iHeadCfg)
{
SerialPortData serialPortData = new SerialPortData(
$"COM{iHeadCfg.RfidComPortNr}",
iHeadCfg.CliProgramName,
iHeadCfg.CliMeterType);
NfcHeadServiceOld headService = new NfcHeadServiceOld(serialPortData);
// Wait synchronously
OptoHeadStatus optoHeadStatus = headService.SetTestingMode(OptoHeadStatus.OptoHeadC7);
if (optoHeadStatus == OptoHeadStatus.OptoHeadC7)
{
_lastOptoHeadStatus = optoHeadStatus;
_lastIHeadCfg = iHeadCfg;
return "OK";
}
else
try
{
SerialPortData serialPortData = new SerialPortData(
$"COM{iHeadCfg.RfidComPortNr}",
iHeadCfg.CliProgramName,
iHeadCfg.CliMeterType);
NfcHeadServiceOld headService = new NfcHeadServiceOld(serialPortData);
OptoHeadStatus optoHeadStatus = headService.SetTestingMode(OptoHeadStatus.OptoHeadC7);
if (optoHeadStatus == OptoHeadStatus.OptoHeadC7)
{
_lastOptoHeadStatus = optoHeadStatus;
_lastIHeadCfg = iHeadCfg;
return "OK";
}
return "Error Set Test Mode";
}
catch (Exception exception)
{
rfidDataLogger.Error("Poseidon Set Test Mode failed.", exception);
return "Error Set Test Mode: " + exception.Message;
}
}
private static OptoHeadService optoHeadService;

View File

@ -85,18 +85,20 @@ namespace TBF.Rig.RegisterReaders.PoseidonReader.implementations
rfidListItem.Text = $"ReadSerialNo: {OpticalHeadTest.ReadRequest_SerialNo(poseidonCfg)}";
break;
case Operations.SetTestModeOn:
rfidListItem.Text = $"SetTestMode: {OpticalHeadTest.SetTestMode(poseidonCfg)}";
//Do start thread
a.OptoListBox.Items.Clear();
stopWorkerThread = false;
optoThread = new Thread(OptoWorker);
if (!optoThread.IsAlive)
string setTestModeResult = OpticalHeadTest.SetTestMode(poseidonCfg);
rfidListItem.Text = $"SetTestMode: {setTestModeResult}";
// Start the opto worker only after a successful mode change.
if (setTestModeResult == "OK")
{
OpticalHeadTest.StartOptotestInputLoop(poseidonCfg,OptoReceivedHandler); // open opto port
optoThread.Start();
a.OptoListBox.Items.Clear();
stopWorkerThread = false;
optoThread = new Thread(OptoWorker);
if (!optoThread.IsAlive)
{
OpticalHeadTest.StartOptotestInputLoop(poseidonCfg, OptoReceivedHandler);
optoThread.Start();
}
}
break;
case Operations.SetTestModeOff:
//Do stop thread
@ -133,4 +135,4 @@ namespace TBF.Rig.RegisterReaders.PoseidonReader.implementations
this.stopWorkerThread = true;
}
}
}
}

View File

@ -1331,6 +1331,7 @@
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonCfg.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonProcParams.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReader.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReadCycle.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\ProcessExtensions.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\ScopedLoggerFactory.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\SerialPortData.cs" />

View File

@ -285,14 +285,25 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
Assert.IsNotNull(task);
Assert.AreEqual(TaskStatus.RanToCompletion, task.Status);
// Depending on TryJsonStringDeserialize fallback,
// result may be null or an empty/default object.
if (task.Result != null)
{
Assert.IsTrue(
string.IsNullOrEmpty(task.Result.DeviceId),
"Invalid JSON should not produce a valid DeviceId.");
}
Assert.IsNull(task.Result, "Invalid JSON must not create a default reading object.");
}
[TestMethod]
public async Task CliProcess_ExitErrorWithoutJson_RecordsDiagnosticsAndReturnsNoReading()
{
string cmdExe = System.Environment.GetEnvironmentVariable("ComSpec") ?? @"C:\\Windows\\System32\\cmd.exe";
var cliRunner = new CliRunner(false);
var info = new CliTaskInfo { Name = "missing-dependency-simulation" };
JsonDataFromPoseidon result = await cliRunner.RunAndCaptureJsonAsync<JsonDataFromPoseidon>(
cmdExe,
"/d /c \"echo Could not load file or assembly 'log4net' 1>&2 & exit /b 17\"",
info);
Assert.IsNull(result, "A failed CLI process must not create a default zero reading.");
Assert.AreEqual(17, info.ExitCode);
StringAssert.Contains(info.FailureReason, "exit code 17");
StringAssert.Contains(info.StandardError, "log4net");
}
}
}
}

View File

@ -0,0 +1,213 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
[TestClass]
[TestSubject(typeof(PoseidonReadCycle))]
public class PoseidonReadCycleTest
{
private sealed class FakePoseidonReader : IPoseidonReadOperation
{
public string Name { get; private set; }
public CmdPoseidonReader.CurrentPoseidonOp CurrentOp { get; private set; }
public int StartCount { get; private set; }
public int RunCount { get; private set; }
private readonly int runningIterationsBeforeDone;
private int runningIterations;
public FakePoseidonReader(string name, int runningIterationsBeforeDone = 1)
{
Name = name;
this.runningIterationsBeforeDone = runningIterationsBeforeDone;
}
public bool IsNotStarted { get { return CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.None; } }
public bool IsFinished
{
get
{
return CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Done ||
CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Error;
}
}
public bool HasError { get { return CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Error; } }
public void Start(bool readStart)
{
StartCount++;
CurrentOp = readStart
? CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start
: CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_End;
}
public Event Run()
{
RunCount++;
if (CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start ||
CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_End)
{
CurrentOp = CmdPoseidonReader.CurrentPoseidonOp.ReadDatastream_Running;
return Event.Busy;
}
if (CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.ReadDatastream_Running)
{
runningIterations++;
if (runningIterations >= runningIterationsBeforeDone)
{
CurrentOp = CmdPoseidonReader.CurrentPoseidonOp.Done;
return Event.Done;
}
return Event.Busy;
}
return Event.None;
}
}
[TestMethod]
public void RunIteration_FullBench_DoesNotRestartCompletedReaders()
{
var readers = new List<IPoseidonReadOperation>();
for (int i = 1; i <= 48; i++)
readers.Add(new FakePoseidonReader("PoseidonPos" + i, 1 + (i % 3)));
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, true));
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, true));
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, true));
Assert.IsTrue(PoseidonReadCycle.RunIteration(readers, true));
foreach (FakePoseidonReader reader in readers)
{
Assert.AreEqual(1, reader.StartCount, reader.Name + " must be armed exactly once.");
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
}
}
[TestMethod]
public void RunIteration_ReducedBench_DoesNotRestartCompletedReaders()
{
var readers = new List<IPoseidonReadOperation>
{
new FakePoseidonReader("PoseidonPos1", 1),
new FakePoseidonReader("PoseidonPos2", 3)
};
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, false));
// Pos1 is now Done while Pos2 is still running. This is the state
// which used to relaunch Pos1's CLI process over and over.
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, false));
Assert.AreEqual(1, ((FakePoseidonReader)readers[0]).StartCount);
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, false));
Assert.IsTrue(PoseidonReadCycle.RunIteration(readers, false));
foreach (FakePoseidonReader reader in readers)
{
Assert.AreEqual(1, reader.StartCount, reader.Name + " must be armed exactly once.");
}
}
[TestMethod]
public void InputJson_ZeroReading_WithDecimalCommaOrDot_IsDeserializedAndParsedCorrectly()
{
AssertInputJsonReading("00000,0", 0.0);
AssertInputJsonReading("00000.0", 0.0);
}
[TestMethod]
public void CliReadResponse_ZeroReading_IsAcceptedOnlyWhenNfcAndReadingAreComplete()
{
var validZero = new JsonDataFromPoseidon
{
NfcTagDetected = true,
ReadingComplete = true,
Reading = "00000.0"
};
string failureReason;
Assert.IsTrue(CmdPoseidonReader.TryValidateCliReadResponse(validZero, out failureReason));
Assert.IsNull(failureReason);
validZero.ReadingComplete = false;
Assert.IsFalse(CmdPoseidonReader.TryValidateCliReadResponse(validZero, out failureReason));
Assert.AreEqual("ReadingComplete=false.", failureReason);
validZero.ReadingComplete = true;
validZero.NfcTagDetected = false;
Assert.IsFalse(CmdPoseidonReader.TryValidateCliReadResponse(validZero, out failureReason));
Assert.AreEqual("NfcTagDetected=false.", failureReason);
}
private static void AssertInputJsonReading(string reading, double expectedValue)
{
string json = "{\"NfcTagDetected\":true,\"ReadingComplete\":true," +
"\"DeviceId\":\"1000000322\",\"Reading\":\"" + reading + "\"}";
var cliRunner = new CliRunner(false);
JsonDataFromPoseidon data;
bool deserialized = cliRunner.TryJsonStringDeserialize<JsonDataFromPoseidon>(json, out data);
Assert.IsTrue(deserialized, "CLI JSON must be deserialized for Reading='" + reading + "'.");
Assert.IsNotNull(data);
Assert.IsTrue(data.NfcTagDetected);
Assert.IsTrue(data.ReadingComplete);
Assert.AreEqual("1000000322", data.DeviceId);
Assert.AreEqual(reading, data.Reading);
double parsedReading;
bool parsed = CmdPoseidonReader.TryParseCliReading(data.Reading, out parsedReading);
Assert.IsTrue(parsed, "CLI Reading must be parsed for '" + reading + "'.");
Assert.AreEqual(expectedValue, parsedReading, 0.000001);
}
[TestMethod]
[TestCategory("Integration")]
public void CmdSleepTest_OneReader_CompletesAndStoresStartValue()
{
const string cliFile = "cmdSleepTest.exe";
string cliPath = Path.Combine(SerialPortData.CliDirectory, cliFile);
if (!File.Exists(cliPath))
Assert.Inconclusive("cmdSleepTest.exe is not installed in " + SerialPortData.CliDirectory);
CmdPoseidonReader reader = CreateReader("PoseidonPos1", 3, cliFile);
reader.SetCurrentOp(CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start);
for (int iteration = 0; iteration < 300 && !IsTerminal(reader); iteration++)
{
reader.Run();
Thread.Sleep(10);
}
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
Assert.IsFalse(string.IsNullOrEmpty(reader.SerialNr));
Assert.IsTrue(reader.BeginWMState > 0, "The JSON reading from cmdSleepTest must be transferred to BeginWMState.");
}
internal static CmdPoseidonReader CreateReader(string name, int comPort, string cliFile)
{
var cfg = new PoseidonCfg(name, new Factory())
{
ComPortNr = comPort,
CliFileName = cliFile,
TimeOut = 10
};
var reader = new CmdPoseidonReader(cfg, null);
reader.DebugLevel = Common.DebugMode.Normal;
reader.Initialize();
return reader;
}
internal static bool IsTerminal(CmdPoseidonReader reader)
{
return reader.CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Done ||
reader.CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Error;
}
}
}

View File

@ -1,5 +1,9 @@
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Globalization;
using System.Threading;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
@ -37,5 +41,30 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
Assert.IsTrue(tryGetDeviceId);
Assert.AreEqual("1000000267", strOut);
}
[TestMethod]
public void TryParseCliReading_DecimalDot_IsIndependentOfWindowsCulture()
{
CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture;
try
{
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("sk-SK");
double value;
bool parsed = CmdPoseidonReader.TryParseCliReading("0014383.3", out value);
Assert.IsTrue(parsed);
Assert.AreEqual(14383.3, value, 0.000001);
parsed = CmdPoseidonReader.TryParseCliReading("0014383,3", out value);
Assert.IsTrue(parsed);
Assert.AreEqual(14383.3, value, 0.000001);
}
finally
{
Thread.CurrentThread.CurrentCulture = originalCulture;
}
}
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.DataEntry.PoseidonCmd;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
/// <summary>
/// Manual, opt-in test for a real Poseidon meter/test-hat. It is deliberately
/// inconclusive unless TBF_POSEIDON_COM_PORT_X or _Y is configured. Do not
/// use it while an ASIC meter is connected to the selected test hat.
/// </summary>
[TestClass]
[TestCategory("Integration")]
public class PoseidonSingleMeterIntegrationTest
{
// Set exactly one of these to the real COM number after a Poseidon meter
// is connected. Keep both values 0 while an ASIC meter is connected.
private const int RealPoseidonComPortHat = 3;
private const int RealPoseidonComPortOptho = 4;
private const string RealPoseidonCliFile = "HatCliDemo.exe";
[TestMethod]
[TestCategory("Manual")]
public void RealMeter_ReadAndFillStartDialog()
{
int comPort = RealPoseidonComPortHat > 0
? RealPoseidonComPortHat
: RealPoseidonComPortOptho;
if (comPort <= 0)
{
Assert.Inconclusive(
"Set RealPoseidonComPortHat or RealPoseidonComPortOptho in this test class before running the manual integration test.");
}
Exception failure = null;
Thread testThread = new Thread(() =>
{
try
{
RunRealMeterScenario(comPort, RealPoseidonCliFile);
}
catch (Exception exception)
{
failure = exception;
}
});
testThread.SetApartmentState(ApartmentState.STA);
testThread.IsBackground = true;
testThread.Start();
testThread.Join(TimeSpan.FromSeconds(75));
Assert.IsFalse(testThread.IsAlive, "The real-meter integration test did not finish within 75 seconds.");
if (failure != null) throw failure;
}
private static void RunRealMeterScenario(int comPort, string cliFile)
{
var reader = PoseidonReadCycleTest.CreateReader("PoseidonPos1", comPort, cliFile);
reader.SetCurrentOp(CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start);
for (int iteration = 0; iteration < 3500 && !PoseidonReadCycleTest.IsTerminal(reader); iteration++)
{
reader.Run();
Thread.Sleep(10);
}
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
Assert.IsTrue(reader.LastCliReadingParsed,
"CLI returned no parseable Reading. Check the Poseidon reader and CLI logs for the raw JSON response.");
TBF.Data.SetData(1, 1, 1);
using (var dialog = new TestStartEndForm(1, new TBF.Rig.GenericDevices.IRegReader[] { reader }, new bool[1]))
{
dialog.CreateControl();
dialog.WMStartState[0] = reader.BeginWMState;
dialog.WMStartStateStr[0] = reader.BeginWMState.ToString();
dialog.UpdateValues(true, true, reader.DeltaTime);
TextBox startTextBox = dialog.Controls.Find("startTextBox1", true).OfType<TextBox>().FirstOrDefault();
Assert.IsNotNull(startTextBox, "The start-state text box must exist.");
Assert.AreEqual(dialog.WMStartStateStr[0], startTextBox.Text);
}
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Threading;
using Common;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
using IPerlCommunicationForm = TBF.Rig.TestMethods.iPerlCommunication.iPerlCommunicationFormTestMethod;
using IPerlConfigStruct = TBF.Rig.TestMethods.iPerlCommunication.iPerlHead.ConfigStruct;
using IPerlHead = TBF.Rig.TestMethods.iPerlCommunication.iPerlHead.IperlHead;
using IPerlHeadCfg = TBF.Rig.TestMethods.iPerlCommunication.iPerlHead.IperlHeadCfg;
using IPerlHeadFactory = TBF.Rig.TestMethods.iPerlCommunication.iPerlHead.Factory;
using static Sensus.iPerl.NfcHandler.MCI_Protocol;
namespace TBFTests.Rig.TestMethods.iPerlCommunication
{
/// <summary>
/// Manual, read-only integration test through the legacy iPerl communication form.
/// It reads Configuration only; it never writes to the connected meter.
/// </summary>
[TestClass]
[TestCategory("Integration")]
public class IPerlCommunicationFormIntegrationTest
{
// Set the COM port of the currently connected iPerl/ASIC test head.
// Keep 0 to disable this manual test.
private const int RealRfidComPort = 3;
private const int RealOptoComPort = 0;
private const CommunicationInterface RealCommunicationInterface = CommunicationInterface.RFID;
[TestMethod]
[TestCategory("Manual")]
public void ReadConfiguration_ThroughIPerlCommunicationForm()
{
if (RealRfidComPort <= 0)
{
Assert.Inconclusive(
"Set RealRfidComPort in IPerlCommunicationFormIntegrationTest before running this manual test.");
}
Exception failure = null;
Thread testThread = new Thread(() =>
{
try
{
ReadConfiguration();
}
catch (Exception exception)
{
failure = exception;
}
});
testThread.SetApartmentState(ApartmentState.STA);
testThread.IsBackground = true;
testThread.Start();
testThread.Join(TimeSpan.FromSeconds(30));
Assert.IsFalse(testThread.IsAlive, "iPerl configuration read did not finish within 30 seconds.");
if (failure != null) throw failure;
}
private static void ReadConfiguration()
{
var headCfg = new IPerlHeadCfg(new IPerlHeadFactory())
{
Name = "iPerl1",
RfidComPortNr = RealRfidComPort,
OptoComPortNr = RealOptoComPort,
CommunicationInterface = RealCommunicationInterface
};
var head = new IPerlHead(headCfg)
{
DebugLevel = DebugMode.Normal
};
head.StartSession();
// The form initializes the same CfgIPerl and request pipeline that
// production iPerlCommunication uses. No form is shown to the user.
using (var form = new IPerlCommunicationForm(true))
{
byte[] buffer;
int result = IPerlCommunicationForm.ReadRequestPort(
head,
MessageID.Configuration,
StructName.Configuration,
0,
IPerlConfigStruct.Length,
out buffer);
Assert.AreEqual(0, result, "iPerlCommunicationForm could not read the meter Configuration.");
Assert.IsNotNull(buffer);
Assert.AreEqual(IPerlConfigStruct.Length, buffer.Length);
IPerlConfigStruct configuration = IPerlConfigStruct.FromByteArray(buffer);
Assert.IsNotNull(configuration);
Assert.IsFalse(string.IsNullOrWhiteSpace(configuration.GetPcbNrString()));
}
}
}
}

View File

@ -107,6 +107,9 @@
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\CliRunnerTimeoutTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\CliRunnerTimeoutUnitTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReadCycleTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonSingleMeterIntegrationTest.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\IPerlCommunicationFormIntegrationTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonReader\UniHeadTestCtrlTest.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ReadStableMassOpTest.cs" />
</ItemGroup>
@ -115,6 +118,10 @@
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\NfcS5_DLL\NfcS5_DLL.csproj">
<Project>{5954d496-caab-4f7a-bde2-bdc8f47dab19}</Project>
<Name>NfcS5_DLL</Name>
</ProjectReference>
<ProjectReference Include="..\Common\Common.csproj">
<Project>{c8939821-ba5c-4988-a3d0-bf53b74865c7}</Project>
<Name>Common</Name>
@ -171,4 +178,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>