Poseidon move from feature/morisville/fix_cli_timeout

This commit is contained in:
Michal Buzik 2026-09-01 10:27:04 +02:00
parent e4d4d75642
commit 8d42232589
15 changed files with 534 additions and 48 deletions

View File

@ -444,15 +444,19 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
if(poseidonReader == null)
continue;
poseidonReader.SetCliLogging(CliLogging);
if (!(poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.ReadDatastream_Done
|| poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.ReadDatastream_Running))
// 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))
{
@ -462,6 +466,7 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
}
if (bAllReadersFinished)
{
log.DebugFormat("Poseidon read: all readers finished for {0}", currentOp);
finishedReading = true;
}
else

View File

@ -222,6 +222,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
FileName = fileName,
Arguments = args,
WorkingDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(fileName)),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
@ -258,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 exit code {info.ExitCode}.";
log?.Error($"{info.Name}: {info.FailureReason} stderr='{info.StandardError}'");
}
info.State = CliTaskState.Completed;
return allOutput;
}
@ -311,6 +321,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
FileName = fileName,
Arguments = args,
WorkingDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(fileName)),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
@ -347,9 +358,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 exit 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))
@ -358,6 +380,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);
}
@ -380,18 +404,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);
}
}
@ -399,7 +422,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();
@ -417,21 +440,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
{

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using TBF.Rig;
namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
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;
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;
@ -62,12 +63,19 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
CurrentPoseidonOp _currentOp;
private bool _isReadingStart = true;
private bool? _isCliLogging;
private bool lastCliReadingParsed;
private string lastCliReadFailureReason;
private bool lastCliReadSucceeded;
public CurrentPoseidonOp CurrentOp
{
get { return _currentOp; }
}
public bool LastCliReadingParsed { get { return lastCliReadingParsed; } }
public string LastCliReadFailureReason { get { return lastCliReadFailureReason; } }
public bool LastCliReadSucceeded { get { return lastCliReadSucceeded; } }
public void SetCurrentOp(CurrentPoseidonOp operation = CurrentPoseidonOp.None)
{
_currentOp = operation ;
@ -407,12 +415,18 @@ 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);
CliRunner.Clear();
_lastOpTimedOut = false;
log.DebugFormat("{0}: starting CLI read, direction={1}, path='{2}', args='{3}'",
Name, _isReadingStart ? "start" : "end", serialPort.SerialPortCmdClientPath,
serialPort.DefaultArgSettings(SerialPortData.EMeterArg.AllParams));
CliRunner.AddRunAndCaptureJsonAsync<JsonDataFromPoseidon>(serialPort,
SerialPortData.EMeterArg.AllParams);
_currentOp = CurrentPoseidonOp.ReadDatastream_Running;
@ -449,6 +463,8 @@ 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.";
}
else
{
@ -464,6 +480,15 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
if (data != null)
{
string validationError;
if (!TryValidateCliReadResponse(data, out validationError))
{
lastCliReadFailureReason = validationError;
log.ErrorFormat("{0}: Poseidon {1} read rejected. {2}", Name,
_isReadingStart ? "Begin" : "End", validationError);
}
else
{
if (string.IsNullOrEmpty(wmSerialNr))
{
try
@ -477,14 +502,21 @@ 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 + "'.";
}
}
@ -497,6 +529,24 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
public static bool TryParseCliReading(string reading, out double value)
{
value = 0;
if (String.IsNullOrWhiteSpace(reading)) return false;
return Double.TryParse(reading.Trim().Replace(',', '.'), 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 != true) { failureReason = "NfcTagDetected is false or missing."; return false; }
if (data.ReadingComplete != true) { failureReason = "ReadingComplete is false or missing."; 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

@ -5,6 +5,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
public class SerialPortData
{
public const string CliDirectory = @"C:\TBF\Cli";
private Boolean? _cliExists;
public bool CliExists { get {
if (_cliExists == null || !_cliExists.HasValue)
@ -13,12 +15,16 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
return _cliExists.Value;
} }
public string SerialPortCmdClientPath {
#if DEBUG
get { return Path.Combine("C:\\","TBF","Cli", CmdClientName);}
#else
get { return Path.Combine("..","Cli", CmdClientName);}
#endif
public string SerialPortCmdClientPath
{
get
{
string cliFileName = Path.GetFileName(CmdClientName);
if (String.IsNullOrWhiteSpace(cliFileName))
cliFileName = "HalCli.exe";
return Path.Combine(CliDirectory, cliFileName);
}
}
public string CmdClientName { get; set; } = "HalCli.exe";
public string PortName { get; set; }

View File

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TBF.Rig.GenericDevices;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
{
/// <summary>
/// Resolves persisted smart-reader selections. The visual dialog may have a
/// fixed number of controls, but the persisted model contains only configured
/// readers in each family.
/// </summary>
public static class SmartReaderSelection
{
private const string LegacyIPerlFamilyKey = "iperl";
public static void EnsureConfiguredReaders(LocalSettings settings, IEnumerable<ISmartReader> readers)
{
if (settings == null || readers == null)
return;
List<ISmartReader> configuredReaders = readers.Where(reader => reader != null).ToList();
if (configuredReaders.Count == 0)
return;
SmartReaderSelectionSettings selections = GetOrCreateSettings(settings);
bool migrateLegacyMask = !selections.LegacyOptoHeadsEnabledMigrated;
string legacyFamily = configuredReaders.Any(reader => GetFamilyKey(reader) == LegacyIPerlFamilyKey)
? LegacyIPerlFamilyKey
: GetFamilyKey(configuredReaders[0]);
foreach (IGrouping<string, ISmartReader> family in configuredReaders.GroupBy(GetFamilyKey))
{
SmartReaderFamilySelection storedFamily = GetOrCreateFamily(selections, family.Key);
int ordinal = 0;
foreach (ISmartReader reader in family)
{
SmartReaderSelectionItem item = storedFamily.Readers
.FirstOrDefault(candidate => candidate.ReaderKey == GetReaderKey(reader));
if (item == null)
{
item = new SmartReaderSelectionItem
{
ReaderKey = GetReaderKey(reader),
Enabled = !migrateLegacyMask || family.Key != legacyFamily ||
IsLegacyMaskEnabled(settings.OptoHeadsEnabled, reader, ordinal)
};
storedFamily.Readers.Add(item);
}
ordinal++;
}
}
if (migrateLegacyMask)
selections.LegacyOptoHeadsEnabledMigrated = true;
}
public static long GetMask(LocalSettings settings, IEnumerable<ISmartReader> readers)
{
List<ISmartReader> familyReaders = ToReaderList(readers);
if (familyReaders.Count == 0)
return 0L;
EnsureConfiguredReaders(settings, familyReaders);
long result = 0L;
for (int index = 0; index < familyReaders.Count && index < 63; index++)
{
if (IsEnabled(settings, familyReaders[index]))
result |= 1L << index;
}
return result;
}
public static void SetMask(LocalSettings settings, IEnumerable<ISmartReader> readers, long state)
{
List<ISmartReader> familyReaders = ToReaderList(readers);
if (settings == null || familyReaders.Count == 0)
return;
EnsureConfiguredReaders(settings, familyReaders);
for (int index = 0; index < familyReaders.Count; index++)
SetEnabled(settings, familyReaders[index], index < 63 && (state & (1L << index)) != 0L);
}
public static bool IsEnabled(LocalSettings settings, IRegReader reader)
{
if (settings == null || reader == null)
return true;
SmartReaderSelectionSettings selections = settings.SmartReaderSelections;
if (selections == null)
return true;
SmartReaderFamilySelection family = selections.Families == null
? null
: selections.Families.FirstOrDefault(item => item.FamilyKey == GetFamilyKey(reader));
SmartReaderSelectionItem item = family == null || family.Readers == null
? null
: family.Readers.FirstOrDefault(candidate => candidate.ReaderKey == GetReaderKey(reader));
// A newly configured reader is enabled until the user explicitly
// changes it in SmartCommunicationForm.
return item == null || item.Enabled;
}
public static string GetFamilyKey(IRegReader reader)
{
string typeName = reader == null ? string.Empty : reader.GetType().FullName ?? string.Empty;
if (typeName.IndexOf(".AllyReader.", StringComparison.OrdinalIgnoreCase) >= 0)
return "ally";
if (typeName.IndexOf(".GenesisRegReader.", StringComparison.OrdinalIgnoreCase) >= 0)
return "genesis";
if (typeName.IndexOf(".iPerlASICReader.", StringComparison.OrdinalIgnoreCase) >= 0)
return "iperl-asic";
if (typeName.IndexOf(".Poseidon", StringComparison.OrdinalIgnoreCase) >= 0)
return "poseidon";
return LegacyIPerlFamilyKey;
}
public static string GetReaderKey(IRegReader reader)
{
if (reader == null)
return string.Empty;
if (!string.IsNullOrEmpty(reader.Name))
return reader.Name;
return string.Format("{0}#{1}", reader.GetType().FullName, reader.Position);
}
private static void SetEnabled(LocalSettings settings, IRegReader reader, bool enabled)
{
SmartReaderFamilySelection family = GetOrCreateFamily(GetOrCreateSettings(settings), GetFamilyKey(reader));
SmartReaderSelectionItem item = family.Readers.FirstOrDefault(candidate => candidate.ReaderKey == GetReaderKey(reader));
if (item == null)
{
item = new SmartReaderSelectionItem { ReaderKey = GetReaderKey(reader) };
family.Readers.Add(item);
}
item.Enabled = enabled;
}
private static SmartReaderSelectionSettings GetOrCreateSettings(LocalSettings settings)
{
if (settings.SmartReaderSelections == null)
settings.SmartReaderSelections = new SmartReaderSelectionSettings();
if (settings.SmartReaderSelections.Families == null)
settings.SmartReaderSelections.Families = new List<SmartReaderFamilySelection>();
return settings.SmartReaderSelections;
}
private static SmartReaderFamilySelection GetOrCreateFamily(SmartReaderSelectionSettings settings, string familyKey)
{
SmartReaderFamilySelection family = settings.Families.FirstOrDefault(item => item.FamilyKey == familyKey);
if (family == null)
{
family = new SmartReaderFamilySelection { FamilyKey = familyKey };
settings.Families.Add(family);
}
if (family.Readers == null)
family.Readers = new List<SmartReaderSelectionItem>();
return family;
}
private static bool IsLegacyMaskEnabled(long legacyMask, IRegReader reader, int ordinal)
{
int index = reader.Position > 0 && reader.Position <= 63 ? reader.Position - 1 : ordinal;
return index >= 0 && index < 63 && (legacyMask & (1L << index)) != 0L;
}
private static List<ISmartReader> ToReaderList(IEnumerable<ISmartReader> readers)
{
return readers == null ? new List<ISmartReader>() : readers.Where(reader => reader != null).ToList();
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace TBF
{
/// <summary>
/// User selection of smart register readers. The selection is deliberately
/// kept per reader family because reader positions are only meaningful inside
/// one configured family.
/// </summary>
public sealed class SmartReaderSelectionSettings
{
public bool LegacyOptoHeadsEnabledMigrated;
public List<SmartReaderFamilySelection> Families = new List<SmartReaderFamilySelection>();
}
public sealed class SmartReaderFamilySelection
{
public string FamilyKey;
public List<SmartReaderSelectionItem> Readers = new List<SmartReaderSelectionItem>();
}
public sealed class SmartReaderSelectionItem
{
/// <summary>Stable configured component name; not a visual row index.</summary>
public string ReaderKey;
public bool Enabled;
}
}

View File

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

View File

@ -245,5 +245,23 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
"Invalid JSON should not produce a valid DeviceId.");
}
}
[TestMethod]
public async Task CliProcess_ExitErrorWithoutJson_RecordsDiagnosticsAndReturnsNoReading()
{
string cmdExe = 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);
Assert.AreEqual(17, info.ExitCode);
StringAssert.Contains(info.FailureReason, "exit code 17");
StringAssert.Contains(info.StandardError, "log4net");
}
}
}

View File

@ -0,0 +1,74 @@
using System.Collections.Generic;
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 FakeReader : IPoseidonReadOperation
{
private readonly int iterationsToComplete;
private int iterations;
public FakeReader(string name, int iterationsToComplete) { Name = name; this.iterationsToComplete = iterationsToComplete; }
public string Name { get; private set; }
public int StartCount { get; private set; }
public CmdPoseidonReader.CurrentPoseidonOp CurrentOp { get; private set; }
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()
{
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 && ++iterations >= iterationsToComplete)
{ CurrentOp = CmdPoseidonReader.CurrentPoseidonOp.Done; return Event.Done; }
return Event.Busy;
}
}
[TestMethod]
public void RunIteration_FullBench_ArmsEachReaderOnce()
{
var readers = new List<IPoseidonReadOperation>();
for (int i = 1; i <= 48; i++) readers.Add(new FakeReader("PoseidonPos" + i, 1 + i % 3));
for (int i = 0; i < 5; i++) PoseidonReadCycle.RunIteration(readers, true);
foreach (FakeReader reader in readers) Assert.AreEqual(1, reader.StartCount, reader.Name);
}
[TestMethod]
public void RunIteration_ReducedBench_DoesNotRestartCompletedReader()
{
var readers = new List<IPoseidonReadOperation> { new FakeReader("Pos1", 1), new FakeReader("Pos2", 3) };
for (int i = 0; i < 5; i++) PoseidonReadCycle.RunIteration(readers, false);
foreach (FakeReader reader in readers) Assert.AreEqual(1, reader.StartCount, reader.Name);
}
[TestMethod]
public void InputJson_ZeroReading_WithDecimalCommaOrDot_IsValid()
{
foreach (string reading in new[] { "00000,0", "00000.0" })
{
double value;
Assert.IsTrue(CmdPoseidonReader.TryParseCliReading(reading, out value));
Assert.AreEqual(0d, value);
}
}
[TestMethod]
public void CliReadResponse_RequiresNfcAndCompletedReading()
{
string reason;
var data = new JsonDataFromPoseidon { NfcTagDetected = true, ReadingComplete = true, Reading = "00000.0" };
Assert.IsTrue(CmdPoseidonReader.TryValidateCliReadResponse(data, out reason));
data.ReadingComplete = false;
Assert.IsFalse(CmdPoseidonReader.TryValidateCliReadResponse(data, out reason));
}
}
}

View File

@ -37,5 +37,20 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
Assert.IsTrue(tryGetDeviceId);
Assert.AreEqual("1000000267", strOut);
}
[TestMethod]
public void TryParseCliReading_ShouldAcceptZeroWithDotAndComma()
{
double dotValue;
double commaValue;
Assert.IsTrue(TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader
.TryParseCliReading("00000.0", out dotValue));
Assert.IsTrue(TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader
.TryParseCliReading("00000,0", out commaValue));
Assert.AreEqual(0d, dotValue);
Assert.AreEqual(0d, commaValue);
}
}
}

View File

@ -0,0 +1,51 @@
using System.IO;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
[TestClass]
[TestCategory("Integration")]
public class PoseidonSingleMeterIntegrationTest
{
// Set this only when a real Poseidon meter is connected to the selected hat.
private const int RealPoseidonComPort = 0;
private const string RealPoseidonCliFile = "HatCliDemo.exe";
[TestMethod]
[TestCategory("Manual")]
public void RealMeter_ReadStartValueThroughCli()
{
if (RealPoseidonComPort <= 0)
Assert.Inconclusive("Set RealPoseidonComPort before running this manual Poseidon integration test.");
string cliPath = Path.Combine(SerialPortData.CliDirectory, RealPoseidonCliFile);
if (!File.Exists(cliPath))
Assert.Inconclusive("Poseidon CLI was not found: " + cliPath);
var cfg = new PoseidonCfg("PoseidonIntegration", new Factory())
{
ComPortNr = RealPoseidonComPort,
CliFileName = RealPoseidonCliFile
};
var reader = new CmdPoseidonReader(cfg, null);
reader.DebugLevel = Common.DebugMode.Normal;
reader.Initialize();
reader.SetCurrentOp(CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start);
for (int iteration = 0; iteration < 3500 &&
reader.CurrentOp != CmdPoseidonReader.CurrentPoseidonOp.Done &&
reader.CurrentOp != CmdPoseidonReader.CurrentPoseidonOp.Error; iteration++)
{
reader.Run();
Thread.Sleep(10);
}
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
Assert.IsTrue(reader.LastCliReadingParsed,
"CLI returned no parseable reading. Inspect the Poseidon CLI and TBF logs.");
}
}
}

View File

@ -119,6 +119,7 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
Name = "PoseidonCfg",
OptoComPortNr = 6,
RfidComPortNr = 5,
DebugLevel = Common.DebugMode.Simulate,
};
config = poseidonCfg;
@ -156,7 +157,8 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
string allText = string.Join(", ", listBox.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("ReadSerialNo: 1000000267"));
Assert.IsTrue(allText.Contains("ReadSerialNo: 1111"),
"The UI unit test must use the deterministic simulated serial number, not physical hardware.");
}
@ -215,10 +217,6 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
string allText = string.Join(", ", listBox.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("SetTestMode: OK"));
//some input from the meter
Thread.Sleep(50000);
//SET OPTOTEST MODE OFF
comboBox.SelectedIndex = (int)PoseidonImplHeadTestCtrl.Operations.SetTestModeOff;
uniHeadTestCtrl.CommandTestButtonClick(commandTestButton, eventArgs);
@ -226,22 +224,6 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
allText = string.Join(", ", listBox.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("SetActiveMode: OK"));
/////////////// results of OptoHead
// Get the private ListBox field via reflection
var fieldOpto = uniHeadTestCtrl.GetType()
.GetField("optoListBox", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
// Read its value from the control instance (not Text!)
var listBoxOpto = fieldOpto?.GetValue(uniHeadTestCtrl) as System.Windows.Forms.ListBox;
// Verify we found it
Assert.IsNotNull(listBoxOpto, "optoListBox not found in UniHeadTestCtrl");
//check if the test mode is set OFF
allText = string.Join(", ", listBoxOpto.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("Status: OptoHeadC7"));
Assert.IsTrue(allText.Contains("C7Data: C7"));
}
}
}

View File

@ -135,6 +135,8 @@
<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\RegisterReaders\PoseidonReader\UniHeadTestCtrlTest.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ReadStableMassOpTest.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\GenesisHead\GenesisHeadBatchIntegrationTest.cs" />