From 59cd5f9dde0930b9d56deaba14a6866e76e1c0f7 Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Mon, 31 Aug 2026 13:02:09 +0200 Subject: [PATCH] fix(morrisville): release 3.9.2205.1 Poseidon CLI diagnostics and results DB migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Results/DB.cs | 7 +- Results/Results.csproj | 6 +- TBF/Properties/AssemblyInfo.cs | 4 +- TBF/ReadMe_BugFix.md | 9 + TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs | 60 ++--- .../PoseidonCmdStartStop/CliRunner.cs | 46 +++- .../PoseidonCmdStartStop/CliTaskInfo.cs | 6 +- .../PoseidonCmdStartStop/PoseidonReadCycle.cs | 81 +++++++ .../PoseidonCmdStartStop/PoseidonReader.cs | 95 +++++++- .../PoseidonReader/UniHeadTestCtrl.cs | 8 + .../communication/OpticalHeadTest.cs | 62 ++--- .../PoseidonImplHeadTestCtrl.cs | 24 +- TBF/TBF.csproj | 1 + .../CliRunnerTimeoutTest.cs | 29 ++- .../PoseidonReadCycleTest.cs | 213 ++++++++++++++++++ .../PoseidonReaderTest.cs | 31 ++- .../PoseidonSingleMeterIntegrationTest.cs | 89 ++++++++ .../IPerlCommunicationFormIntegrationTest.cs | 98 ++++++++ TBFTests/TBFTests.csproj | 9 +- 19 files changed, 781 insertions(+), 97 deletions(-) create mode 100644 TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs create mode 100644 TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs create mode 100644 TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonSingleMeterIntegrationTest.cs create mode 100644 TBFTests/Rig/TestMethods/iPerlCommunication/IPerlCommunicationFormIntegrationTest.cs diff --git a/Results/DB.cs b/Results/DB.cs index 1bc7a5d0f..23f06a0a2 100644 --- a/Results/DB.cs +++ b/Results/DB.cs @@ -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(); } diff --git a/Results/Results.csproj b/Results/Results.csproj index 06cf05a71..484ea4d1f 100644 --- a/Results/Results.csproj +++ b/Results/Results.csproj @@ -53,6 +53,9 @@ ..\packages\NHibernate.4.0.4.4000\lib\net40\NHibernate.dll + + ..\packages\System.Data.SQLite.2.0.3\lib\net471\System.Data.SQLite.dll + @@ -69,6 +72,7 @@ + @@ -265,4 +269,4 @@ --> - \ No newline at end of file + diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs index be61ee32a..4405d1a97 100644 --- a/TBF/Properties/AssemblyInfo.cs +++ b/TBF/Properties/AssemblyInfo.cs @@ -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")] diff --git a/TBF/ReadMe_BugFix.md b/TBF/ReadMe_BugFix.md index 7a8010bc9..40648787a 100644 --- a/TBF/ReadMe_BugFix.md +++ b/TBF/ReadMe_BugFix.md @@ -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. | diff --git a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs index 95901aeb2..9dc95d57d 100644 --- a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs +++ b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs @@ -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 poseidonReaders = new List(); + 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 operationsBefore = + new Dictionary(); + 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) diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs index 31473d2ba..6511300e9 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs @@ -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(string json, out T value) where T : new() { - if (json != null) + if (!string.IsNullOrWhiteSpace(json)) { try { value = JsonConvert.DeserializeObject(json); - return true; + return value != null; } catch (Exception ex) { log?.Debug(ex.Message); - value = TryConvert(json); - return true; + return TryConvert(json, out value); } } @@ -410,7 +431,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop return false; } - private static T TryConvert(string json) where T : new() + private static bool TryConvert(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) diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs index aabb6ec79..aeac9961e 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs @@ -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 } } } -} \ No newline at end of file +} diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs new file mode 100644 index 000000000..0c07a1fe6 --- /dev/null +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using TBF.Rig; + +namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop +{ + /// + /// 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. + /// + 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 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; + } + } +} diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs index 90318b0cb..99aab7f55 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs @@ -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; } } /// Run this operation /// eventDone @@ -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)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); + 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 } + /// + /// CLI values must not depend on the Windows UI culture. Both dot and comma + /// are accepted as a decimal separator and normalized before parsing. + /// + 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; + } + /// Stop this operation public void Stop() diff --git a/TBF/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrl.cs b/TBF/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrl.cs index 3c8d3d5b6..7ee475cd3 100644 --- a/TBF/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrl.cs +++ b/TBF/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrl.cs @@ -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; diff --git a/TBF/Rig/RegisterReaders/PoseidonReader/communication/OpticalHeadTest.cs b/TBF/Rig/RegisterReaders/PoseidonReader/communication/OpticalHeadTest.cs index 35f5e234a..700687d27 100644 --- a/TBF/Rig/RegisterReaders/PoseidonReader/communication/OpticalHeadTest.cs +++ b/TBF/Rig/RegisterReaders/PoseidonReader/communication/OpticalHeadTest.cs @@ -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; diff --git a/TBF/Rig/RegisterReaders/PoseidonReader/implementations/PoseidonImplHeadTestCtrl.cs b/TBF/Rig/RegisterReaders/PoseidonReader/implementations/PoseidonImplHeadTestCtrl.cs index 65761e1b2..b6c95eb12 100644 --- a/TBF/Rig/RegisterReaders/PoseidonReader/implementations/PoseidonImplHeadTestCtrl.cs +++ b/TBF/Rig/RegisterReaders/PoseidonReader/implementations/PoseidonImplHeadTestCtrl.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index b8a4bffa5..b8afb07f5 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -1331,6 +1331,7 @@ + diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs index c144e22d2..4d312fb37 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs @@ -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( + 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"); } } -} \ No newline at end of file +} diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs new file mode 100644 index 000000000..9aa1ae19b --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs @@ -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(); + 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 + { + 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(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; + } + } +} diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs index 26abb99fa..7d2f2e48b 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs @@ -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; + } + } } -} \ No newline at end of file +} diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonSingleMeterIntegrationTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonSingleMeterIntegrationTest.cs new file mode 100644 index 000000000..994bfbaa1 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonSingleMeterIntegrationTest.cs @@ -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 +{ + /// + /// 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. + /// + [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().FirstOrDefault(); + Assert.IsNotNull(startTextBox, "The start-state text box must exist."); + Assert.AreEqual(dialog.WMStartStateStr[0], startTextBox.Text); + } + } + } +} diff --git a/TBFTests/Rig/TestMethods/iPerlCommunication/IPerlCommunicationFormIntegrationTest.cs b/TBFTests/Rig/TestMethods/iPerlCommunication/IPerlCommunicationFormIntegrationTest.cs new file mode 100644 index 000000000..8b4533d49 --- /dev/null +++ b/TBFTests/Rig/TestMethods/iPerlCommunication/IPerlCommunicationFormIntegrationTest.cs @@ -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 +{ + /// + /// Manual, read-only integration test through the legacy iPerl communication form. + /// It reads Configuration only; it never writes to the connected meter. + /// + [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())); + } + } + } +} diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index 2c59a2925..b2d12071d 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -107,6 +107,9 @@ + + + @@ -115,6 +118,10 @@ + + {5954d496-caab-4f7a-bde2-bdc8f47dab19} + NfcS5_DLL + {c8939821-ba5c-4988-a3d0-bf53b74865c7} Common @@ -171,4 +178,4 @@ --> - \ No newline at end of file +