diff --git a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs index 5975229b3..074e4e55e 100644 --- a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs +++ b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs @@ -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 diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs index fe4c7c1c9..6c5a05d81 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs @@ -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(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); } } @@ -399,7 +422,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(); @@ -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) @@ -462,4 +488,4 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop }); } } -} \ No newline at end of file +} 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..acbfe6980 --- /dev/null +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs @@ -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 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; + } + } +} diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs index 1d0347c5f..c4d04539b 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; @@ -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(serialPort, SerialPortData.EMeterArg.AllParams); _currentOp = CurrentPoseidonOp.ReadDatastream_Running; @@ -449,6 +463,8 @@ 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."; } 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; + } + /// Stop this operation public void Stop() diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs index 123273e24..9684009f6 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/SerialPortData.cs @@ -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,13 +15,17 @@ 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; } public int MeterType { get; set; } @@ -56,4 +62,4 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop MeterType = meterType; } } -} \ No newline at end of file +} diff --git a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/SmartReaderSelection.cs b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/SmartReaderSelection.cs new file mode 100644 index 000000000..a2ea883c3 --- /dev/null +++ b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/SmartReaderSelection.cs @@ -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 +{ + /// + /// 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. + /// + public static class SmartReaderSelection + { + private const string LegacyIPerlFamilyKey = "iperl"; + + public static void EnsureConfiguredReaders(LocalSettings settings, IEnumerable readers) + { + if (settings == null || readers == null) + return; + + List 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 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 readers) + { + List 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 readers, long state) + { + List 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(); + 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(); + 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 ToReaderList(IEnumerable readers) + { + return readers == null ? new List() : readers.Where(reader => reader != null).ToList(); + } + } +} diff --git a/TBF/SmartReaderSelectionSettings.cs b/TBF/SmartReaderSelectionSettings.cs new file mode 100644 index 000000000..b8ad30855 --- /dev/null +++ b/TBF/SmartReaderSelectionSettings.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace TBF +{ + /// + /// User selection of smart register readers. The selection is deliberately + /// kept per reader family because reader positions are only meaningful inside + /// one configured family. + /// + public sealed class SmartReaderSelectionSettings + { + public bool LegacyOptoHeadsEnabledMigrated; + public List Families = new List(); + } + + public sealed class SmartReaderFamilySelection + { + public string FamilyKey; + public List Readers = new List(); + } + + public sealed class SmartReaderSelectionItem + { + /// Stable configured component name; not a visual row index. + public string ReaderKey; + public bool Enabled; + } +} diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 693750689..7a71a773d 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -1758,6 +1758,7 @@ + diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs index 66a0a303a..788948ee1 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs @@ -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( + 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"); + } } -} \ 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..dc181cf97 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs @@ -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(); + 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 { 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)); + } + } +} diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs index 26abb99fa..dfc0b75a9 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReaderTest.cs @@ -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); + } } -} \ 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..24f1ec31b --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonSingleMeterIntegrationTest.cs @@ -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."); + } + } +} diff --git a/TBFTests/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrlTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrlTest.cs index cf988c93b..3ffbc6dd3 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrlTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonReader/UniHeadTestCtrlTest.cs @@ -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().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().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().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().Select(i => i.ToString())); - Console.WriteLine(allText); - Assert.IsTrue(allText.Contains("Status: OptoHeadC7")); - Assert.IsTrue(allText.Contains("C7Data: C7")); } } -} \ No newline at end of file +} diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index 987c2d75f..8c2f17db1 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -135,6 +135,8 @@ + +