From 2f1ada3f5bf5f9ca40afc738b0f2756186f4dc26 Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Mon, 19 Jan 2026 08:10:36 +0100 Subject: [PATCH] Add DiagnosticLedState classes for States #4-#7, enums for PipeStatus and SpikeDetectionStatus, and implement TouchReadProtocol commands, frames, and builders. --- .../communication/C4/TouchReadCommand.cs | 134 +++++++++++++ .../C4/TouchReadDeviceSubCommand.cs | 105 ++++++++++ .../communication/C4/TouchReadFrame.cs | 27 +++ .../communication/C4/TouchReadFrameBuilder.cs | 115 +++++++++++ .../communication/C4/TouchReadFrameParser.cs | 63 ++++++ .../communication/C4/TouchReadProtocol.cs | 10 + .../communication/C4/TouchReadResponse.cs | 34 ++++ .../C4/diagnosticLed/DiagnosticLedParser.cs | 75 +++++++ .../C4/diagnosticLed/DiagnosticLedState.cs | 13 ++ .../parserer/DiagnosticLedData.cs | 86 ++++++++ .../parserer/DiagnosticLedState1Data.cs | 40 ++++ .../parserer/DiagnosticLedState2Data.cs | 72 +++++++ .../parserer/DiagnosticLedState3Data.cs | 71 +++++++ .../parserer/DiagnosticLedState4Data.cs | 73 +++++++ .../parserer/DiagnosticLedState5Data.cs | 93 +++++++++ .../parserer/DiagnosticLedState6Data.cs | 134 +++++++++++++ .../parserer/DiagnosticLedState7Data.cs | 137 +++++++++++++ .../C4/diagnosticLed/parserer/PipeStatus.cs | 11 + .../parserer/SpikeDetectionStatus.cs | 11 + .../diagnosticLed/utils/DiagnosticChecksum.cs | 13 ++ .../C4/diagnosticLed/utils/DiagnosticHex.cs | 40 ++++ .../C4/hexLogger/HexFormatter.cs | 59 ++++++ .../C4/hexLogger/TouchReadControlDecoder.cs | 16 ++ .../C4/hexLogger/TouchReadLogger.cs | 56 ++++++ .../C4/led/ITouchReadLedParser.cs | 7 + .../C4/led/ShortVariableLedParser.cs | 17 ++ .../communication/C4/led/TouchReadLedData.cs | 77 +++++++ .../C4/led/TouchReadLedMessage.cs | 21 ++ TBF/TBF.csproj | 28 +++ .../C4/TouchReadBaudRateDetectionTests.cs | 113 +++++++++++ .../C4/TouchReadFrameBuilderTest.cs | 109 ++++++++++ .../C4/TouchReadSerialIntegrationTests.cs | 123 ++++++++++++ .../diagnosticLed/DiagnosticLedParserTest.cs | 189 ++++++++++++++++++ .../C4/led/ShortVariableLedParserTest.cs | 44 ++++ .../C4/led/TouchReadLedMessageTest.cs | 45 +++++ TBFTests/TBFTests.csproj | 6 + 36 files changed, 2267 insertions(+) create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadCommand.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadDeviceSubCommand.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrame.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilder.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameParser.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadProtocol.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadResponse.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParser.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedState.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedData.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState1Data.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState2Data.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState3Data.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState4Data.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState5Data.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState6Data.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState7Data.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/PipeStatus.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/SpikeDetectionStatus.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticChecksum.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticHex.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/HexFormatter.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadControlDecoder.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadLogger.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ITouchReadLedParser.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParser.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedData.cs create mode 100644 TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessage.cs create mode 100644 TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadBaudRateDetectionTests.cs create mode 100644 TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilderTest.cs create mode 100644 TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadSerialIntegrationTests.cs create mode 100644 TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParserTest.cs create mode 100644 TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParserTest.cs create mode 100644 TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessageTest.cs diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadCommand.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadCommand.cs new file mode 100644 index 000000000..6ea8862fe --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadCommand.cs @@ -0,0 +1,134 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + /// + /// Common iPERL TouchRead bidirectional commands. + /// These commands consist of a single-byte command code + /// placed in the Information field. + /// + public enum TouchReadCommand : byte + { + /// + /// Simple (legacy) commands (e.g. View Factory ID = 0x01) + /// + Simple = 0x00, + + /// + /// View Factory ID (ex-works serial number). + /// Returns a 0–12 byte ASCII string terminated by NULL. + /// Response only if RF flag is set. + /// + ViewFactoryId = 0x01, + + /// + /// Set Factory ID (0–12 ASCII characters, NULL terminated). + /// Protected by meter seal. + /// + SetFactoryId = 0x02, + + /// + /// View Customer Programmable ID (1–12 ASCII characters). + /// + ViewProgrammableId = 0x03, + + /// + /// Set Customer Programmable ID (1–12 ASCII characters, NULL terminated). + /// + SetProgrammableId = 0x04, + + /// + /// View Version and Type string. + /// Example: B1.22,SMW002,B0.02 + /// + ViewVersionAndType = 0x05, + + /// + /// View Customer Programmable Text (0–20 ASCII characters). + /// + ViewProgrammableText = 0x07, + + /// + /// Set Customer Programmable Text (0–20 ASCII characters, NULL terminated). + /// + SetProgrammableText = 0x08, + + /// + /// View number of reading digits and decimal shift. + /// Payload: uint8 digits, int8 decimal shift. + /// + ViewNumberOfReadingDigits = 0x09, + + /// + /// Set number of reading digits and decimal shift. + /// Digits range: 4–8, Decimal shift: -5..0. + /// + SetNumberOfReadingDigits = 0x0A, + + /// + /// View reading units. + /// Returns numeric unit code (m3, ft3, gallons). + /// + ViewReadingUnits = 0x0B, + + /// + /// Set reading units. + /// Valid values: 0x00=m3, 0x01=ft3, 0x04=US gallons, 0xFF=off. + /// + SetReadingUnits = 0x0C, + + /// + /// View reading multiplier (resolution). + /// Range: -7..+5 or 0x80 (disabled). + /// + ViewReadingMultiplier = 0x0F, + + /// + /// Set reading multiplier (resolution). + /// + SetReadingMultiplier = 0x10, + + /// + /// View preset total (volume accumulator). + /// Returns 8 ASCII digits + NULL. + /// + ViewPresetTotal = 0x13, + + /// + /// Set preset total (0–8 ASCII digits, NULL terminated). + /// Protected by meter seal. + /// + SetPresetTotal = 0x14, + + /// + /// View reading mode (unidirectional TouchRead format). + /// + ViewReadingMode = 0x15, + + /// + /// Set reading mode. + /// Values: Short Variable, Extended, Fixed, Smart Meter. + /// + SetReadingMode = 0x16, + + /// + /// View build information (firmware details). + /// + ViewBuildInformation = 0x17, + + /// + /// View meter state. + /// + ViewState = 0x19, + + /// + /// Set meter state (operating mode). + /// Protected by meter seal. + /// + SetState = 0x1A, + + /// + /// Device-specific command prefix. + /// Must be followed by a device sub-command byte. + /// + DeviceSpecific = 0xFD + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadDeviceSubCommand.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadDeviceSubCommand.cs new file mode 100644 index 000000000..422bdce5c --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadDeviceSubCommand.cs @@ -0,0 +1,105 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + /// + /// Device-specific TouchRead sub-commands. + /// These commands are used with the DeviceSpecific (0xFD) command. + /// + public enum TouchReadDeviceSubCommand : byte + { + // ---- Alarm / Mask ----------------------------------------- + /// + /// View Alarm Mask (lower 16 bits). + /// Returns a 2-byte little-endian alarm bit field. + /// + ViewAlarmMask = 0x31, + + /// + /// Set Alarm Mask (lower 16 bits). + /// Payload: 2-byte little-endian alarm bit field. + /// + SetAlarmMask = 0x32, + + /// + /// View alarm persistence period (days). + /// Range: 8–90 days. + /// + ViewPersistence = 0x33, + + /// + /// Set alarm persistence period (days). + /// Range: 8–90 days. + /// + SetPersistence = 0x34, + + /// + /// View leak duration (hours). + /// Range: 24–180 hours. + /// + ViewLeakDuration = 0x35, + + /// + /// Set leak duration (hours). + /// Range: 24–180 hours. + /// + SetLeakDuration = 0x36, + + ViewAlarms = 0x37, + SetAlarms = 0x38, + + /// + /// View system time. + /// Returns uint32 seconds since 2000-01-01. + /// + ViewSystemTime = 0x10, + + /// + /// Set system time. + /// Payload: uint32 seconds since 2000-01-01. + /// If zero, device will reset and erase data. + /// Protected by meter seal. + /// + SetSystemTime = 0x11, + + + // ---- Manufacture / Time ----------------------------------- + ViewManufactureDate = 0x39, + SetManufactureDate = 0x3A, + + ViewSecondsIdle = 0x3B, + ViewSecondsActive = 0x3D, + ViewSecondsUsed = 0x3F, + + // ---- Snapshot / Logging ----------------------------------- + ViewSnapshotData = 0x41, + ViewDatalogDuration = 0x43, + SetDatalogDuration = 0x44, + ReadDatalog = 0x45, + ClearDatalog = 0x46, + + // ---- History ---------------------------------------------- + ViewHistoryMask = 0x47, + SetHistoryMask = 0x48, + ReadHistory = 0x49, + ClearHistory = 0x4A, + + // ---- Diagnostics ------------------------------------------ + ViewDiagnostics = 0x4B, + ResetDiagnostics = 0x4C, + + // ---- Calibration / Build ---------------------------------- + ViewCalibration = 0x53, + SetCalibration = 0x54, + + ViewIPerlBuild = 0x65, + SetIPerlBuild = 0x66, + + // ---- Bootloader (dangerous!) ------------------------------ + EnterBootloader = 0x81, + ReadFlash = 0x82, + EraseAll = 0x83, + EraseSegment = 0x84, + UpdateCode = 0x85, + ExitBootloader = 0x86 + } + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrame.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrame.cs new file mode 100644 index 000000000..1b9fb04f9 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrame.cs @@ -0,0 +1,27 @@ +using System; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + public sealed class TouchReadFrame + { + public byte Start { get; } + public byte Length { get; } + public byte Control { get; } + public byte[] Information { get; } + public ushort Checksum { get; } + + public TouchReadFrame( + byte start, + byte length, + byte control, + byte[] information, + ushort checksum) + { + Start = start; + Length = length; + Control = control; + Information = information ?? Array.Empty(); + Checksum = checksum; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilder.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilder.cs new file mode 100644 index 000000000..dbe7ebb47 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilder.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + public sealed class TouchReadFrameBuilder + { + private const byte START = 0x0D; + private byte _control; + private readonly List _information = new List(); + + public TouchReadFrameBuilder RequestResponse(bool enabled) + { + _control = enabled ? (byte)0x08 : (byte)0x00; + return this; + } + + public TouchReadFrameBuilder AddCommand(TouchReadCommand command) + { + _information.Add((byte)command); + return this; + } + + public TouchReadFrameBuilder AddSubCommand(TouchReadDeviceSubCommand subCommand) + { + if (_information.Count == 0 || + _information[0] != (byte)TouchReadCommand.DeviceSpecific) + throw new InvalidOperationException( + "Sub-command is only valid for DeviceSpecific (0xFD) commands."); + + _information.Add((byte)subCommand); + return this; + } + + public TouchReadFrameBuilder AddDeviceCommand( + TouchReadDeviceSubCommand subCommand) + { + _information.Add((byte)TouchReadCommand.DeviceSpecific); + _information.Add((byte)subCommand); + return this; + } + + public TouchReadFrameBuilder AddPayload(byte[] payload) + { + if (payload != null) + _information.AddRange(payload); + + return this; + } + + public TouchReadFrameBuilder AddNullTerminatedAscii(string text) + { + if (!string.IsNullOrEmpty(text)) + _information.AddRange( + System.Text.Encoding.ASCII.GetBytes(text)); + + _information.Add(0x00); + return this; + } + + public TouchReadFrame BuildFrame() + { + if (_information.Count == 0) + throw new InvalidOperationException("No command specified."); + + byte length = (byte)(1 + _information.Count + 2); + + var raw = new List + { + START, + length, + _control + }; + + raw.AddRange(_information); + + ushort checksum = CalculateChecksum(raw); + raw.Add((byte)(checksum >> 8)); + raw.Add((byte)(checksum & 0xFF)); + + return new TouchReadFrame( + START, + length, + _control, + _information.ToArray(), + checksum); + } + + public byte[] BuildBytes() + { + TouchReadFrame frame = BuildFrame(); + + var bytes = new List + { + frame.Start, + frame.Length, + frame.Control + }; + + bytes.AddRange(frame.Information); + bytes.Add((byte)(frame.Checksum >> 8)); + bytes.Add((byte)(frame.Checksum & 0xFF)); + + return bytes.ToArray(); + } + + private static ushort CalculateChecksum(IEnumerable data) + { + ushort sum = 0; + foreach (var b in data) + sum += b; + return sum; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameParser.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameParser.cs new file mode 100644 index 000000000..0755a2f22 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameParser.cs @@ -0,0 +1,63 @@ +using System; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + public sealed class TouchReadFrameParser + { + private const byte START = 0x0D; + + public TouchReadResponse Parse(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + if (data.Length < 6) + throw new FormatException("Frame too short."); + + if (data[0] != START) + throw new FormatException("Invalid START byte."); + + byte length = data[1]; + if (length + 2 != data.Length) + throw new FormatException("Length mismatch."); + + ushort receivedChecksum = + (ushort)((data[data.Length - 2] << 8) | + data[data.Length - 1]); + + ushort calculatedChecksum = CalculateChecksum(data, data.Length - 2); + if (receivedChecksum != calculatedChecksum) + throw new FormatException("Checksum error."); + + byte control = data[2]; + byte status = data[3]; + + byte[] payload = ExtractPayload(data); + + return new TouchReadResponse(control, status, payload); + } + + private static ushort CalculateChecksum(byte[] data, int count) + { + ushort sum = 0; + for (int i = 0; i < count; i++) + sum += data[i]; + return sum; + } + + private static byte[] ExtractPayload(byte[] data) + { + // payload exists only if frame longer than: + // START + LEN + CTRL + STATUS + CHK_HI + CHK_LO = 6 bytes + if (data.Length <= 6) + return Array.Empty(); + + int payloadLength = data.Length - 6; + byte[] payload = new byte[payloadLength]; + Buffer.BlockCopy(data, 4, payload, 0, payloadLength); + return payload; + } + } + + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadProtocol.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadProtocol.cs new file mode 100644 index 000000000..9200decb9 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadProtocol.cs @@ -0,0 +1,10 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + public static class TouchReadProtocol + { + public const byte START = 0x0D; + + // Control bits (CNTRL1) + public const byte RESPONSE_FLAG = 0x08; // RF + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadResponse.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadResponse.cs new file mode 100644 index 000000000..c58d08dc7 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadResponse.cs @@ -0,0 +1,34 @@ +using System; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + public sealed class TouchReadResponse + { + public byte Control { get; } + public byte Status { get; } + public byte[] Payload { get; } + + public bool IsOk => Status == 0x01; + + public TouchReadResponse(byte control, byte status, byte[] payload) + { + Control = control; + Status = status; + Payload = payload ?? Array.Empty(); + } + + public string GetAsciiPayload() + { + if (Payload.Length == 0) + return null; + + int length = Array.IndexOf(Payload, (byte)0x00); + if (length < 0) + length = Payload.Length; + + return System.Text.Encoding.ASCII.GetString(Payload, 0, length); + } + } + + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParser.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParser.cs new file mode 100644 index 000000000..993697110 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParser.cs @@ -0,0 +1,75 @@ +using System; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed +{ + public sealed class DiagnosticLedParser +{ + private readonly DiagnosticLedState _state; + + public DiagnosticLedParser(DiagnosticLedState state) + { + _state = state; + } + + public DiagnosticLedData ParseLine(string line) + { + if (string.IsNullOrEmpty(line)) + throw new ArgumentNullException(nameof(line)); + + if (!line.EndsWith("\r\n")) + throw new FormatException("Invalid diagnostic LED line termination"); + + string trimmed = line.TrimEnd('\r', '\n'); + string[] parts = trimmed.Split('\t'); + + if (parts.Length < 2) + throw new FormatException("Too few diagnostic LED fields"); + + // ---- Checksum ---- + string checksumHex = parts[parts.Length - 1]; + + int lastTab = trimmed.LastIndexOf('\t'); + if (lastTab < 0) + throw new FormatException("Checksum separator not found"); + + string beforeChecksum = trimmed.Substring(0, lastTab + 1); + + byte expected = DiagnosticChecksum.Compute(beforeChecksum); + byte actual = DiagnosticHex.ParseByte(checksumHex); + + if (expected != actual) + throw new FormatException("Diagnostic LED checksum mismatch"); + + // ---- Dispatch ---- + switch (_state) + { + case DiagnosticLedState.State1: + return new DiagnosticLedState1Data(line, parts); + + case DiagnosticLedState.State2: + return new DiagnosticLedState2Data(line, parts); + + case DiagnosticLedState.State3: + return new DiagnosticLedState3Data(line, parts); + + case DiagnosticLedState.State4: + return new DiagnosticLedState4Data(line, parts); + + case DiagnosticLedState.State5: + return new DiagnosticLedState5Data(line, parts); + + case DiagnosticLedState.State6: + return new DiagnosticLedState6Data(line, parts); + + case DiagnosticLedState.State7: + return new DiagnosticLedState7Data(line, parts); + + default: + throw new NotSupportedException("Unknown diagnostic LED state"); + } + } +} + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedState.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedState.cs new file mode 100644 index 000000000..746eb27ef --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedState.cs @@ -0,0 +1,13 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed +{ + public enum DiagnosticLedState : byte + { + State1 = 0x01, + State2 = 0x02, + State3 = 0x03, + State4 = 0x04, + State5 = 0x05, + State6 = 0x06, + State7 = 0x07 + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedData.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedData.cs new file mode 100644 index 000000000..b53ad0f71 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedData.cs @@ -0,0 +1,86 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// + /// Base class for all Diagnostic LED data frames. + /// + /// + /// The iPERL meter emits diagnostic LED frames when the + /// Diagnostic LED is enabled using the + /// Set Diagnostic LED State (0xFD 0x60) command. + /// + /// + /// + /// All diagnostic LED states (State #1 – State #7) share a common + /// set of leading fields, followed by state-specific extensions. + /// This class represents those common fields. + /// + /// + /// + /// + /// Pos + /// Common field description + /// + /// 0 – xxxxxxSigned 24-bit ADC value (two’s complement) + /// 1 – aaaaUnsigned 16-bit field strength (internal units) + /// 2 – yyyySigned 16-bit raw flow rate (¼ ml per bit) + /// 3 – vvvvvvUnsigned 24-bit raw volume accumulation (¼ ml per bit) + /// 4 – ccccUnsigned 16-bit millivolt delta on the field drive capacitor + /// + /// + /// + /// Each derived state class parses additional fields starting at + /// position 5, according to the selected diagnostic LED state. + /// + /// + /// + /// The raw ASCII line (including checksum and CRLF) is preserved + /// for logging, debugging, and offline analysis. + /// + /// + public abstract class DiagnosticLedData + { + /// + /// Raw diagnostic LED line exactly as received from the meter, + /// including checksum and CRLF. + /// + public string RawLine { get; } + + // ----- Common fields (present in all LED states) ----- + + /// + /// Signed 24-bit ADC value (two’s complement). + /// + public int Adc24 { get; protected set; } + + /// + /// Unsigned 16-bit field strength in internal (non-legacy) units. + /// + public ushort FieldStrength { get; protected set; } + + /// + /// Signed 16-bit raw flow rate in units of ¼ milliliter per bit. + /// + public short RawFlow { get; protected set; } + + /// + /// Unsigned 24-bit raw volume accumulation in units of ¼ milliliter per bit. + /// + public uint RawVolume { get; protected set; } + + /// + /// Unsigned 16-bit millivolt delta measured on the field drive capacitor. + /// + public ushort CapacitorMv { get; protected set; } + + /// + /// Initializes the base diagnostic LED data with the raw input line. + /// + /// + /// Raw ASCII line received from the diagnostic LED output. + /// + protected DiagnosticLedData(string raw) + { + RawLine = raw; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState1Data.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState1Data.cs new file mode 100644 index 000000000..e905a4e8c --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState1Data.cs @@ -0,0 +1,40 @@ +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// + /// Diagnostic LED State #1 data frame. + /// + /// + /// Frame format: TAB-separated ASCII hexadecimal fields, terminated by CRLF. + /// The checksum is an 8-bit sum of all previous ASCII bytes including + /// the TAB character before the checksum field. + /// + /// + /// + /// + /// Pos + /// Field description + /// + /// 0 – xxxxxxSigned 24-bit ADC value (two’s complement) + /// 1 – aaaaUnsigned 16-bit field strength (internal units) + /// 2 – yyyySigned 16-bit raw flow rate (¼ ml per bit) + /// 3 – vvvvvvUnsigned 24-bit raw volume accumulation (¼ ml per bit) + /// 4 – ccccUnsigned 16-bit millivolt delta on the field drive capacitor + /// 5 – ssUnsigned 8-bit checksum (sum of all previous ASCII bytes + /// including the TAB before the checksum field) + /// + /// + public class DiagnosticLedState1Data : DiagnosticLedData + { + public DiagnosticLedState1Data(string raw, string[] f) + : base(raw) + { + Adc24 = DiagnosticHex.ParseInt24(f[0]); + FieldStrength = DiagnosticHex.ParseUInt16(f[1]); + RawFlow = DiagnosticHex.ParseInt16(f[2]); + RawVolume = DiagnosticHex.ParseUInt24(f[3]); + CapacitorMv = DiagnosticHex.ParseUInt16(f[4]); + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState2Data.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState2Data.cs new file mode 100644 index 000000000..d4eac87a8 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState2Data.cs @@ -0,0 +1,72 @@ +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// + /// Diagnostic LED State #2 data frame. + /// + /// + /// State #2 extends the common diagnostic LED fields with information + /// about the LCD-displayed volume, the current meter operating state, + /// and whether the meter is in low-flow cutoff mode. + /// + /// + /// + /// Frame format: TAB-separated ASCII hexadecimal fields, terminated by CRLF. + /// The checksum is an 8-bit sum of all previous ASCII bytes including + /// the TAB character before the checksum field. + /// + /// + /// + /// + /// Pos + /// Field description + /// + /// 0 – xxxxxxSigned 24-bit ADC value (two’s complement) + /// 1 – aaaaUnsigned 16-bit field strength (internal units) + /// 2 – yyyySigned 16-bit raw flow rate (¼ ml per bit) + /// 3 – vvvvvvUnsigned 24-bit raw volume accumulation (¼ ml per bit) + /// 4 – ccccUnsigned 16-bit millivolt delta on the field drive capacitor + /// 5 – ggggggggUnsigned 32-bit volume displayed on the LCD + /// 6 – mmUnsigned 8-bit meter state (see Table 17-23 in protocol documentation) + /// 7 – ffUnsigned 8-bit boolean flag indicating low-flow cutoff + /// state (0 = false, 1 = true) + /// 8 – ssUnsigned 8-bit checksum (sum of all previous ASCII bytes including + /// the TAB before the checksum field) + /// + /// + public sealed class DiagnosticLedState2Data : DiagnosticLedData + { + /// + /// Volume displayed on LCD (raw units). + /// + public uint LcdVolume { get; } + + /// + /// Meter state (see Table 17-23). + /// + public byte MeterState { get; } + + /// + /// True if meter is in low-flow cutoff. + /// + public bool IsLowFlowCutoff { get; } + + public DiagnosticLedState2Data(string rawLine, string[] fields) + : base(rawLine) + { + // ---- Common fields (0–4) ---- + Adc24 = DiagnosticHex.ParseInt24(fields[0]); + FieldStrength = DiagnosticHex.ParseUInt16(fields[1]); + RawFlow = DiagnosticHex.ParseInt16(fields[2]); + RawVolume = DiagnosticHex.ParseUInt24(fields[3]); + CapacitorMv = DiagnosticHex.ParseUInt16(fields[4]); + + // ---- State #2 specific ---- + LcdVolume = DiagnosticHex.ParseUInt32(fields[5]); + MeterState = DiagnosticHex.ParseByte(fields[6]); + IsLowFlowCutoff = DiagnosticHex.ParseByte(fields[7]) != 0; + } + } + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState3Data.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState3Data.cs new file mode 100644 index 000000000..070c100f9 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState3Data.cs @@ -0,0 +1,71 @@ +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// + /// Diagnostic LED State #3 data frame. + /// + /// + /// State #3 extends the common diagnostic LED fields with calibration + /// and timing information related to the field drive and ASIC operation. + /// + /// + /// + /// Frame format: TAB-separated ASCII hexadecimal fields, terminated by CRLF. + /// The checksum is an 8-bit sum of all previous ASCII bytes including + /// the TAB character before the checksum field. + /// + /// + /// + /// + /// Pos + /// Field description + /// + /// 0 – xxxxxxSigned 24-bit ADC value (two’s complement) + /// 1 – aaaaUnsigned 16-bit field strength (internal units) + /// 2 – yyyySigned 16-bit raw flow rate (¼ ml per bit) + /// 3 – vvvvvvUnsigned 24-bit raw volume accumulation (¼ ml per bit) + /// 4 – ccccUnsigned 16-bit millivolt delta on the field drive capacitor + /// 5 – ttttUnsigned 16-bit field calibration value + /// 6 – bbbbbbbbUnsigned 32-bit ASIC timestamp (8192 ticks per second, + /// rolls over at 2^32) + /// 7 – ffUnsigned 8-bit field drive time in microseconds + /// 8 – ssUnsigned 8-bit checksum (sum of all previous ASCII bytes including + /// the TAB before the checksum field) + /// + /// + public sealed class DiagnosticLedState3Data : DiagnosticLedData + { + /// + /// Unsigned 16-bit field calibration value. + /// + public ushort FieldCalibration { get; } + + /// + /// ASIC timestamp in units of 1 / 8192 seconds. + /// Rolls over at 2^32. + /// + public uint AsicTimestamp { get; } + + /// + /// Field drive time in microseconds. + /// + public byte FieldDriveTimeUs { get; } + + public DiagnosticLedState3Data(string rawLine, string[] fields) + : base(rawLine) + { + // ---- Common fields (0–4) ---- + Adc24 = DiagnosticHex.ParseInt24(fields[0]); + FieldStrength = DiagnosticHex.ParseUInt16(fields[1]); + RawFlow = DiagnosticHex.ParseInt16(fields[2]); + RawVolume = DiagnosticHex.ParseUInt24(fields[3]); + CapacitorMv = DiagnosticHex.ParseUInt16(fields[4]); + + // ---- State #3 specific fields ---- + FieldCalibration = DiagnosticHex.ParseUInt16(fields[5]); + AsicTimestamp = DiagnosticHex.ParseUInt32(fields[6]); + FieldDriveTimeUs = DiagnosticHex.ParseByte(fields[7]); + } + } +} diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState4Data.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState4Data.cs new file mode 100644 index 000000000..46c737059 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState4Data.cs @@ -0,0 +1,73 @@ +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// + /// Diagnostic LED State #4 data frame. + /// Frame format (TAB-separated ASCII HEX fields, CRLF terminated). + /// + /// + /// # / Field + /// Description + /// + /// 0 – xxxxxxsigned 24-bit ADC value + /// 1 – aaaaunsigned 16-bit Field strength + /// 2 – yyyysigned 16-bit Raw flow rate (1/4 ml per bit) + /// 3 – vvvvvvunsigned 24-bit Raw volume accumulation + /// 4 – ccccunsigned 16-bit Capacitor mV delta + /// 5 – ttttunsigned 16-bit Field calibration + /// 6 – bbbbbbbbunsigned 32-bit ASIC timestamp + /// 7 – ffunsigned 8-bit Field drive time (µs) + /// 8 – mmmmmmmmsigned 32-bit Mean flow rate + /// 9 – ggggunsigned 16-bit Field 1 measurement + /// 10 – hhhhunsigned 16-bit Field 2 measurement + /// 11 – ccccunsigned 16-bit Integrator calibration positive + /// 12 – nnnnunsigned 16-bit Integrator calibration negative + /// 13 – qqunsigned 8-bit ASIC state + /// 14 – ssunsigned 8-bit Checksum + /// + /// + public sealed class DiagnosticLedState4Data : DiagnosticLedData + { + public ushort FieldCalibration { get; } + public uint AsicTimestamp { get; } + public byte FieldDriveTimeUs { get; } + + public int MeanFlowRate { get; } + + public ushort Field1Measurement { get; } + public ushort Field2Measurement { get; } + + public ushort IntegratorCalibrationPositive { get; } + public ushort IntegratorCalibrationNegative { get; } + + public byte AsicState { get; } + + public DiagnosticLedState4Data(string rawLine, string[] fields) + : base(rawLine) + { + // ---- Common fields (0–4) ---- + Adc24 = DiagnosticHex.ParseInt24(fields[0]); + FieldStrength = DiagnosticHex.ParseUInt16(fields[1]); + RawFlow = DiagnosticHex.ParseInt16(fields[2]); + RawVolume = DiagnosticHex.ParseUInt24(fields[3]); + CapacitorMv = DiagnosticHex.ParseUInt16(fields[4]); + + // ---- State #4 specific ---- + FieldCalibration = DiagnosticHex.ParseUInt16(fields[5]); + AsicTimestamp = DiagnosticHex.ParseUInt32(fields[6]); + FieldDriveTimeUs = DiagnosticHex.ParseByte(fields[7]); + + MeanFlowRate = unchecked((int)DiagnosticHex.ParseUInt32(fields[8])); + + Field1Measurement = DiagnosticHex.ParseUInt16(fields[9]); + Field2Measurement = DiagnosticHex.ParseUInt16(fields[10]); + + IntegratorCalibrationPositive = DiagnosticHex.ParseUInt16(fields[11]); + IntegratorCalibrationNegative = DiagnosticHex.ParseUInt16(fields[12]); + + AsicState = DiagnosticHex.ParseByte(fields[13]); + } + } + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState5Data.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState5Data.cs new file mode 100644 index 000000000..2835d04fc --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState5Data.cs @@ -0,0 +1,93 @@ +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// +/// Diagnostic LED State #5 data frame. +/// +/// Frame format: TAB-separated ASCII HEX fields, CRLF terminated. +/// +/// +/// +/// # / Field +/// Description +/// +/// 0 – xxxxxxsigned 24-bit ADC value +/// 1 – aaaaunsigned 16-bit Field strength (internal units) +/// 2 – yyyysigned 16-bit Raw flow rate (1/4 ml per bit) +/// 3 – vvvvvvunsigned 24-bit Raw volume accumulation (1/4 ml per bit) +/// 4 – ccccunsigned 16-bit Millivolts delta on field drive capacitor +/// 5 – ttttunsigned 16-bit Field calibration value +/// 6 – bbbbbbbbunsigned 32-bit ASIC timestamp (8192 ticks/sec, rolls over at 2^32) +/// 7 – ffunsigned 8-bit Field drive time in microseconds +/// 8 – mmmmmmmmsigned 32-bit Mean flow rate (rolls over at 2^32) +/// 9 – ggggunsigned 16-bit Field 1 measurement +/// 10 – hhhhunsigned 16-bit Field 2 measurement +/// 11 – ccccunsigned 16-bit Integrator calibration positive +/// 12 – nnnnunsigned 16-bit Integrator calibration negative +/// 13 – qqunsigned 8-bit ASIC state +/// 14 – iiiisigned 16-bit Water impedance measurement +/// 15 – ssunsigned 8-bit Checksum +/// +/// +public sealed class DiagnosticLedState5Data : DiagnosticLedData +{ + /// Field calibration value (tttt). + public ushort FieldCalibration { get; } + + /// ASIC timestamp (bbbbbbbb), 8192 ticks per second. + public uint AsicTimestamp { get; } + + /// Field drive time in microseconds (ff). + public byte FieldDriveTimeUs { get; } + + /// Mean flow rate (mmmmmmmm), signed 32-bit. + public int MeanFlowRate { get; } + + /// Field 1 measurement (gggg). + public ushort Field1Measurement { get; } + + /// Field 2 measurement (hhhh). + public ushort Field2Measurement { get; } + + /// Integrator calibration positive (cccc). + public ushort IntegratorCalibrationPositive { get; } + + /// Integrator calibration negative (nnnn). + public ushort IntegratorCalibrationNegative { get; } + + /// ASIC state (qq). + public byte AsicState { get; } + + /// Water impedance measurement (iiii), signed 16-bit. + public short WaterImpedance { get; } + + public DiagnosticLedState5Data(string rawLine, string[] fields) + : base(rawLine) + { + // ---- Common fields ---- + Adc24 = DiagnosticHex.ParseInt24(fields[0]); + FieldStrength = DiagnosticHex.ParseUInt16(fields[1]); + RawFlow = DiagnosticHex.ParseInt16(fields[2]); + RawVolume = DiagnosticHex.ParseUInt24(fields[3]); + CapacitorMv = DiagnosticHex.ParseUInt16(fields[4]); + + // ---- State #5 specific ---- + FieldCalibration = DiagnosticHex.ParseUInt16(fields[5]); + AsicTimestamp = DiagnosticHex.ParseUInt32(fields[6]); + FieldDriveTimeUs = DiagnosticHex.ParseByte(fields[7]); + + MeanFlowRate = unchecked((int)DiagnosticHex.ParseUInt32(fields[8])); + + Field1Measurement = DiagnosticHex.ParseUInt16(fields[9]); + Field2Measurement = DiagnosticHex.ParseUInt16(fields[10]); + + IntegratorCalibrationPositive = DiagnosticHex.ParseUInt16(fields[11]); + IntegratorCalibrationNegative = DiagnosticHex.ParseUInt16(fields[12]); + + AsicState = DiagnosticHex.ParseByte(fields[13]); + WaterImpedance = DiagnosticHex.ParseInt16(fields[14]); + } +} + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState6Data.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState6Data.cs new file mode 100644 index 000000000..46a853867 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState6Data.cs @@ -0,0 +1,134 @@ +using System; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// +/// Diagnostic LED State #6 data frame. +/// +/// Frame format: TAB-separated ASCII HEX fields, CRLF terminated. +/// +/// +/// +/// # / Field +/// Description +/// +/// 0 – xxxxxxsigned 24-bit ADC value +/// 1 – aaaaunsigned 16-bit Field strength (internal units) +/// 2 – yyyysigned 16-bit Raw flow rate (1/4 ml per bit) +/// 3 – vvvvvvunsigned 24-bit Raw volume accumulation (1/4 ml per bit) +/// 4 – ccccunsigned 16-bit Millivolts delta on field drive capacitor +/// 5 – ttttunsigned 16-bit Field calibration value +/// 6 – bbbbbbbbunsigned 32-bit ASIC timestamp (8192 ticks/sec, rolls over at 2^32) +/// 7 – ffunsigned 8-bit Field drive time in microseconds +/// 8 – mmmmmmmmsigned 32-bit Mean flow rate (rolls over at 2^32) +/// 9 – ggggunsigned 16-bit Field 1 measurement +/// 10 – hhhhunsigned 16-bit Field 2 measurement +/// 11 – ccccunsigned 16-bit Integrator calibration positive +/// 12 – nnnnunsigned 16-bit Integrator calibration negative +/// 13 – qqunsigned 8-bit ASIC state 0 +/// 14 – iiiisigned 16-bit Water impedance measurement +/// 15 – rrrrsigned 16-bit Electrode delta (mV) +/// 16 – ppunsigned 8-bit Spike detection diagnostic +/// 17 – llunsigned 8-bit Pipe status +/// 18 – ddddddddunsigned 32-bit LCD volume +/// 19 – oounsigned 8-bit ASIC state 1 +/// 20 – ssunsigned 8-bit Checksum +/// +/// +public sealed class DiagnosticLedState6Data : DiagnosticLedData +{ + public ushort FieldCalibration { get; } + public uint AsicTimestamp { get; } + public byte FieldDriveTimeUs { get; } + + public int MeanFlowRate { get; } + + public ushort Field1Measurement { get; } + public ushort Field2Measurement { get; } + + public ushort IntegratorCalibrationPositive { get; } + public ushort IntegratorCalibrationNegative { get; } + + public byte AsicState0 { get; } + + public short WaterImpedance { get; } + public short ElectrodeDeltaMv { get; } + + public byte SpikeDetection { get; } + public byte PipeStatus { get; } + + public uint LcdVolume { get; } + + public byte AsicState1 { get; } + + public DiagnosticLedState6Data(string rawLine, string[] fields) + : base(rawLine) + { + // ---- Common fields (0–4) ---- + Adc24 = DiagnosticHex.ParseInt24(fields[0]); + FieldStrength = DiagnosticHex.ParseUInt16(fields[1]); + RawFlow = DiagnosticHex.ParseInt16(fields[2]); + RawVolume = DiagnosticHex.ParseUInt24(fields[3]); + CapacitorMv = DiagnosticHex.ParseUInt16(fields[4]); + + // ---- State #6 specific ---- + FieldCalibration = DiagnosticHex.ParseUInt16(fields[5]); + AsicTimestamp = DiagnosticHex.ParseUInt32(fields[6]); + FieldDriveTimeUs = DiagnosticHex.ParseByte(fields[7]); + + MeanFlowRate = unchecked((int)DiagnosticHex.ParseUInt32(fields[8])); + + Field1Measurement = DiagnosticHex.ParseUInt16(fields[9]); + Field2Measurement = DiagnosticHex.ParseUInt16(fields[10]); + + IntegratorCalibrationPositive = DiagnosticHex.ParseUInt16(fields[11]); + IntegratorCalibrationNegative = DiagnosticHex.ParseUInt16(fields[12]); + + AsicState0 = DiagnosticHex.ParseByte(fields[13]); + + WaterImpedance = DiagnosticHex.ParseInt16(fields[14]); + ElectrodeDeltaMv = DiagnosticHex.ParseInt16(fields[15]); + + SpikeDetection = DiagnosticHex.ParseByte(fields[16]); + PipeStatus = DiagnosticHex.ParseByte(fields[17]); + + LcdVolume = DiagnosticHex.ParseUInt32(fields[18]); + AsicState1 = DiagnosticHex.ParseByte(fields[19]); + } + + + /// + /// Pipe status interpreted as . + /// If the value is outside the defined range, returns null. + /// + public PipeStatus PipeStatusEnumValue + { + get + { + if (!Enum.IsDefined(typeof(PipeStatus), PipeStatus)) + throw new InvalidOperationException( + "Unknown pipe status value: 0x" + PipeStatus.ToString("X2")); + + return (PipeStatus)PipeStatus; + } + } + + /// + /// Spike Detection interpreted as . + /// If the value is outside the defined range, returns null. + /// + public SpikeDetectionStatus SpikeDetectionEnumValue + { + get + { + if (!Enum.IsDefined(typeof(SpikeDetectionStatus), SpikeDetection)) + throw new InvalidOperationException( + "Unknown Spike Detection value: 0x" + SpikeDetection.ToString("X2")); + + return (SpikeDetectionStatus)SpikeDetection; + } + } +} + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState7Data.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState7Data.cs new file mode 100644 index 000000000..e2ea42dff --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/DiagnosticLedState7Data.cs @@ -0,0 +1,137 @@ +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + /// +/// Diagnostic LED State #7 data frame. +/// +/// Frame format: TAB-separated ASCII HEX fields, CRLF terminated. +/// This state extends State #6 with additional ADC and learning diagnostics. +/// +/// +/// +/// # / Field +/// Description +/// +/// 0 – xxxxxxsigned 24-bit ADC value +/// 1 – aaaaunsigned 16-bit Field strength (internal units) +/// 2 – yyyysigned 16-bit Raw flow rate (1/4 ml per bit) +/// 3 – vvvvvvunsigned 24-bit Raw volume accumulation (1/4 ml per bit) +/// 4 – ccccunsigned 16-bit Millivolts delta on field drive capacitor +/// 5 – ttttunsigned 16-bit Field calibration value +/// 6 – bbbbbbbbunsigned 32-bit ASIC timestamp (8192 ticks/sec) +/// 7 – ffunsigned 8-bit Field drive time (µs) +/// 8 – mmmmmmmmsigned 32-bit Mean flow rate +/// 9 – ggggunsigned 16-bit Field 1 measurement +/// 10 – hhhhunsigned 16-bit Field 2 measurement +/// 11 – ccccunsigned 16-bit Integrator calibration positive +/// 12 – nnnnunsigned 16-bit Integrator calibration negative +/// 13 – qqunsigned 8-bit ASIC state 0 +/// 14 – iiiisigned 16-bit Water impedance measurement +/// 15 – rrrrsigned 16-bit Electrode delta (mV) +/// 16 – ppunsigned 8-bit Spike detection diagnostic +/// 17 – llunsigned 8-bit Pipe status +/// 18 – ddddddddunsigned 32-bit LCD volume +/// 19 – oounsigned 8-bit ASIC state 1 +/// 20 – xxxxxxsigned 24-bit Raw ADC value (before offset correction) +/// 21 – yyyyyysigned 24-bit Detrended ADC value +/// 22 – iiiisigned 16-bit Imaginary water impedance +/// 23 – nnnnunsigned 16-bit Electrode voltage noise level +/// 24 – aaunsigned 8-bit ADC offset learning status +/// 25 – ssunsigned 8-bit Checksum +/// +/// +public sealed class DiagnosticLedState7Data : DiagnosticLedData +{ + // ----- State #6 fields ----- + + public ushort FieldCalibration { get; } + public uint AsicTimestamp { get; } + public byte FieldDriveTimeUs { get; } + + public int MeanFlowRate { get; } + + public ushort Field1Measurement { get; } + public ushort Field2Measurement { get; } + + public ushort IntegratorCalibrationPositive { get; } + public ushort IntegratorCalibrationNegative { get; } + + public byte AsicState0 { get; } + + public short WaterImpedance { get; } + public short ElectrodeDeltaMv { get; } + + public byte SpikeDetection { get; } + public byte PipeStatus { get; } + + public uint LcdVolume { get; } + + public byte AsicState1 { get; } + + // ----- State #7 extensions ----- + + /// Raw ADC value before offset correction (signed 24-bit). + public int RawAdcBeforeOffset { get; } + + /// Detrended ADC value (signed 24-bit). + public int DetrendedAdc { get; } + + /// Imaginary water impedance (signed 16-bit). + public short ImaginaryWaterImpedance { get; } + + /// Electrode voltage noise level (unsigned 16-bit). + public ushort ElectrodeVoltageNoise { get; } + + /// + /// ADC offset learning status bitfield. + /// Bit 0: currently learning + /// Bit 1: completed first learning cycle + /// Other bits reserved. + /// + public byte AdcOffsetLearningStatus { get; } + + public DiagnosticLedState7Data(string rawLine, string[] fields) + : base(rawLine) + { + // ---- Common fields (0–4) ---- + Adc24 = DiagnosticHex.ParseInt24(fields[0]); + FieldStrength = DiagnosticHex.ParseUInt16(fields[1]); + RawFlow = DiagnosticHex.ParseInt16(fields[2]); + RawVolume = DiagnosticHex.ParseUInt24(fields[3]); + CapacitorMv = DiagnosticHex.ParseUInt16(fields[4]); + + // ---- State #6 fields ---- + FieldCalibration = DiagnosticHex.ParseUInt16(fields[5]); + AsicTimestamp = DiagnosticHex.ParseUInt32(fields[6]); + FieldDriveTimeUs = DiagnosticHex.ParseByte(fields[7]); + + MeanFlowRate = unchecked((int)DiagnosticHex.ParseUInt32(fields[8])); + + Field1Measurement = DiagnosticHex.ParseUInt16(fields[9]); + Field2Measurement = DiagnosticHex.ParseUInt16(fields[10]); + + IntegratorCalibrationPositive = DiagnosticHex.ParseUInt16(fields[11]); + IntegratorCalibrationNegative = DiagnosticHex.ParseUInt16(fields[12]); + + AsicState0 = DiagnosticHex.ParseByte(fields[13]); + + WaterImpedance = DiagnosticHex.ParseInt16(fields[14]); + ElectrodeDeltaMv = DiagnosticHex.ParseInt16(fields[15]); + + SpikeDetection = DiagnosticHex.ParseByte(fields[16]); + PipeStatus = DiagnosticHex.ParseByte(fields[17]); + + LcdVolume = DiagnosticHex.ParseUInt32(fields[18]); + AsicState1 = DiagnosticHex.ParseByte(fields[19]); + + // ---- State #7 extensions ---- + RawAdcBeforeOffset = DiagnosticHex.ParseInt24(fields[20]); + DetrendedAdc = DiagnosticHex.ParseInt24(fields[21]); + ImaginaryWaterImpedance = DiagnosticHex.ParseInt16(fields[22]); + ElectrodeVoltageNoise = DiagnosticHex.ParseUInt16(fields[23]); + AdcOffsetLearningStatus = DiagnosticHex.ParseByte(fields[24]); + } +} + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/PipeStatus.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/PipeStatus.cs new file mode 100644 index 000000000..8a64ac23c --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/PipeStatus.cs @@ -0,0 +1,11 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + public enum PipeStatus : byte + { + MetroLowFlowCut = 0, + MetroFlowReverse = 1, + MetroFlowForward = 2, + MetroEmptyPipe = 3 + } + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/SpikeDetectionStatus.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/SpikeDetectionStatus.cs new file mode 100644 index 000000000..5a5a700b4 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/parserer/SpikeDetectionStatus.cs @@ -0,0 +1,11 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer +{ + public enum SpikeDetectionStatus : byte + { + NoSpike = 0, + AdcSpike = 1, + SpikeHoldOff = 2, + SpikeHighFlow = 5 + } + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticChecksum.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticChecksum.cs new file mode 100644 index 000000000..3d9b5e4cd --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticChecksum.cs @@ -0,0 +1,13 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils +{ + internal static class DiagnosticChecksum + { + public static byte Compute(string lineWithoutChecksum) + { + byte sum = 0; + foreach (char c in lineWithoutChecksum) + sum += (byte)c; + return sum; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticHex.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticHex.cs new file mode 100644 index 000000000..b08549f7a --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/utils/DiagnosticHex.cs @@ -0,0 +1,40 @@ +using System; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils +{ + internal static class DiagnosticHex + { + public static int ParseInt24(string hex) + { + int value = Convert.ToInt32(hex, 16); + if ((value & 0x800000) != 0) + value |= unchecked((int)0xFF000000); // sign extend + return value; + } + + public static uint ParseUInt24(string hex) + { + return Convert.ToUInt32(hex, 16); + } + + public static short ParseInt16(string hex) + { + return unchecked((short)Convert.ToUInt16(hex, 16)); + } + + public static ushort ParseUInt16(string hex) + { + return Convert.ToUInt16(hex, 16); + } + + public static uint ParseUInt32(string hex) + { + return Convert.ToUInt32(hex, 16); + } + + public static byte ParseByte(string hex) + { + return Convert.ToByte(hex, 16); + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/HexFormatter.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/HexFormatter.cs new file mode 100644 index 000000000..2091fd2c8 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/HexFormatter.cs @@ -0,0 +1,59 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger +{ + public static class HexFormatter + { + /// + /// Formats a single byte as 0xNN. + /// Example: 0x0D + /// + public static string ToHex(byte value) + { + return "0x" + value.ToString("X2"); + } + + /// + /// Formats a byte array as 0xNN 0xNN ... + /// + public static string ToHex(byte[] data) + { + if (data == null || data.Length == 0) + return ""; + + var sb = new System.Text.StringBuilder(); + + for (int i = 0; i < data.Length; i++) + { + if (i > 0) + sb.Append(' '); + + sb.Append("0x"); + sb.Append(data[i].ToString("X2")); + } + + return sb.ToString(); + } + + /// + /// Formats a byte array exactly as shown in serial terminals. + /// Example: "0D 04 08 01 00 1A" + /// + public static string ToSerialHex(byte[] data) + { + if (data == null || data.Length == 0) + return string.Empty; + + var sb = new System.Text.StringBuilder(); + + for (int i = 0; i < data.Length; i++) + { + if (i > 0) + sb.Append(' '); + + sb.Append(data[i].ToString("X2")); + } + + return sb.ToString(); + } + } + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadControlDecoder.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadControlDecoder.cs new file mode 100644 index 000000000..c747e5a1b --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadControlDecoder.cs @@ -0,0 +1,16 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger +{ + public static class TouchReadControlDecoder + { + public static string Describe(byte control) + { + if (control == 0x00) + return "RF=0 (No response expected)"; + + if (control == 0x08) + return "RF=1 (Response expected)"; + + return "INVALID CONTROL BITS (unsupported pattern)"; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadLogger.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadLogger.cs new file mode 100644 index 000000000..11fa0e017 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/hexLogger/TouchReadLogger.cs @@ -0,0 +1,56 @@ +using System; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger +{ + public static class TouchReadLogger + { + public static string DescribeTx(byte[] frame) + { + if (frame == null || frame.Length < 6) + return "Invalid frame"; + + return + "TX Frame\n" + + $" START : {HexFormatter.ToHex(frame[0])}\n" + + $" LEN : {HexFormatter.ToHex(frame[1])} ({frame[1]})\n" + + $" CONTROL : {HexFormatter.ToHex(frame[2])} - {TouchReadControlDecoder.Describe(frame[2])}\n" + + $" INFO : {HexFormatter.ToHex(GetInformation(frame))}\n" + + $" CHECKSUM: {HexFormatter.ToHex(frame[frame.Length - 2])} {HexFormatter.ToHex(frame[frame.Length - 1])}\n" + + $" RAW : {HexFormatter.ToHex(frame)}"; + } + + private static byte[] GetInformation(byte[] frame) + { + int infoLength = frame.Length - 5; // CTRL + INFO + CHK(2) + if (infoLength <= 0) + return Array.Empty(); + + var info = new byte[infoLength]; + Buffer.BlockCopy(frame, 3, info, 0, infoLength); + return info; + } + + public static string DescribeRx(byte[] frame, TouchReadResponse response) + { + return + "RX Frame\n" + + $" START : {HexFormatter.ToHex(frame[0])}\n" + + $" LEN : {HexFormatter.ToHex(frame[1])} ({frame[1]})\n" + + $" CONTROL : {HexFormatter.ToHex(response.Control)}\n" + + $" STATUS : {HexFormatter.ToHex(response.Status)} ({DescribeStatus(response.Status)})\n" + + $" PAYLOAD : {HexFormatter.ToHex(response.Payload)}\n" + + $" RAW : {HexFormatter.ToHex(frame)}"; + } + + private static string DescribeStatus(byte status) + { + switch (status) + { + case 0x01: return "Command complete, no errors"; + case 0x02: return "Unable to execute"; + case 0x04: return "Unsupported control bits"; + default: return "Unknown status"; + } + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ITouchReadLedParser.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ITouchReadLedParser.cs new file mode 100644 index 000000000..fc8907e67 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ITouchReadLedParser.cs @@ -0,0 +1,7 @@ +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led +{ + public interface ITouchReadLedParser + { + TouchReadLedData Parse(TouchReadLedMessage message); + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParser.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParser.cs new file mode 100644 index 000000000..0ba81be12 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParser.cs @@ -0,0 +1,17 @@ +using System.Globalization; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led +{ + public class ShortVariableLedParser : ITouchReadLedParser + { + public TouchReadLedData Parse(TouchReadLedMessage msg) + { + return new TouchReadLedData(msg.Raw) + { + MeterId = msg.Fields[0], + Reading = decimal.Parse(msg.Fields[1], + CultureInfo.InvariantCulture) + }; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedData.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedData.cs new file mode 100644 index 000000000..8064ae507 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedData.cs @@ -0,0 +1,77 @@ +using System; +using System.Globalization; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led +{ + /// + /// Parsed data from a unidirectional TouchRead LED message. + /// The exact populated fields depend on the configured reading mode. + /// + public sealed class TouchReadLedData + { + /// + /// Raw LED message including delimiters. + /// Example: ";12345678,00012345.67,m3;" + /// + public string Raw { get; } + + /// + /// Meter factory ID or serial number (if present). + /// + public string MeterId { get; set; } + + /// + /// Customer programmable ID (if present). + /// + public string CustomerId { get; set; } + + /// + /// Parsed meter reading value. + /// + public decimal? Reading { get; set; } + + /// + /// Engineering units (e.g. "m3", "ft3", "gal"). + /// + public string Units { get; set; } + + /// + /// Optional alarm/status field (bitfield or text). + /// + public string AlarmStatus { get; set; } + + /// + /// Timestamp when the LED data was received. + /// + public DateTime Timestamp { get; } + + public TouchReadLedData(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) + throw new ArgumentException("Raw LED data must not be null or empty.", nameof(raw)); + + Raw = raw; + Timestamp = DateTime.UtcNow; + } + + /// + /// Helper to safely parse a decimal value using invariant culture. + /// + public static decimal? ParseDecimal(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + if (decimal.TryParse( + value, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out var result)) + { + return result; + } + + return null; + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessage.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessage.cs new file mode 100644 index 000000000..3d15579f6 --- /dev/null +++ b/TBF/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessage.cs @@ -0,0 +1,21 @@ +using System; + +namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led +{ + public class TouchReadLedMessage + { + public string Raw { get; } + public string[] Fields { get; } + + public TouchReadLedMessage(string raw) + { + Raw = raw ?? throw new ArgumentNullException(nameof(raw)); + + if (!raw.StartsWith(";") || !raw.EndsWith(";")) + throw new FormatException("Invalid LED message framing"); + + string content = raw.Substring(1, raw.Length - 2); + Fields = content.Split(','); + } + } +} \ No newline at end of file diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 69c66bf8e..cb345f578 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -1251,6 +1251,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadBaudRateDetectionTests.cs b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadBaudRateDetectionTests.cs new file mode 100644 index 000000000..a62a2990f --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadBaudRateDetectionTests.cs @@ -0,0 +1,113 @@ +using System; +using System.IO.Ports; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger; + +namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + [TestClass] + public class TouchReadBaudRateDetectionTests + { + private const string ComPort = "COM3"; // COM PORT OF THE ASIC + private const int ReadTimeoutMs = 1500; + + private static readonly int[] StandardBaudRates = + { + 300, 600, 7812, 1200, 18432, 2400, 4800, + 9600, 10400, 15625, 19200, 31250, 36864, + 38400, 50000, 57600, 62500, 76800, 115200 + }; + + [TestMethod] + [TestCategory("Hardware")] + [TestCategory("Serial")] + public void Detect_BaudRate_By_ViewFactoryId() + { + byte[] request = new TouchReadFrameBuilder() + .RequestResponse(true) + .AddCommand(TouchReadCommand.ViewFactoryId) + .BuildBytes(); + + var parser = new TouchReadFrameParser(); + + foreach (int baud in StandardBaudRates) + { + Console.WriteLine($"--- Testing baud rate: {baud} ---"); + + try + { + using (var port = new SerialPort(ComPort, baud, Parity.None, 7, StopBits.One)) + { + port.ReadTimeout = ReadTimeoutMs; + port.WriteTimeout = 500; + port.Open(); + + port.DiscardInBuffer(); + port.DiscardOutBuffer(); + + Console.WriteLine("TX → " + HexFormatter.ToSerialHex(request)); + port.Write(request, 0, request.Length); + + byte[] response = ReadFullFrame(port); + + Console.WriteLine("RX ← " + HexFormatter.ToSerialHex(response)); + + TouchReadResponse decoded = parser.Parse(response); + + if (decoded.IsOk) + { + string factoryId = decoded.GetAsciiPayload(); + + Console.WriteLine(); + Console.WriteLine("VALID RESPONSE"); + Console.WriteLine("Baud rate : " + baud); + Console.WriteLine("Factory ID : " + factoryId); + Console.WriteLine(); + + Assert.IsFalse(string.IsNullOrEmpty(factoryId), + "Factory ID is empty"); + + return; // SUCCESS → stop scanning + } + } + } + catch (TimeoutException) + { + Console.WriteLine("Timeout"); + } + catch (Exception ex) + { + Console.WriteLine("Error: " + ex.Message); + } + } + + Assert.Fail("No valid baud rate detected."); + } + + private static byte[] ReadFullFrame(SerialPort port) + { + byte start = (byte)port.ReadByte(); + if (start != 0x0D) + throw new InvalidOperationException("Invalid START byte"); + + byte length = (byte)port.ReadByte(); + + int remaining = length; + byte[] buffer = new byte[2 + remaining]; + + buffer[0] = start; + buffer[1] = length; + + int offset = 2; + while (remaining > 0) + { + int read = port.Read(buffer, offset, remaining); + offset += read; + remaining -= read; + } + + return buffer; + } + } +} diff --git a/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilderTest.cs b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilderTest.cs new file mode 100644 index 000000000..3f9f4a284 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadFrameBuilderTest.cs @@ -0,0 +1,109 @@ +using System; +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger; + +namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + [TestClass] + [TestSubject(typeof(TouchReadFrameBuilder))] + public class TouchReadFrameBuilderTest + { + + [TestMethod] + public void Encode_ViewFactoryId_Command() + { + byte[] frame = new TouchReadFrameBuilder() + .RequestResponse(true) + .AddCommand(TouchReadCommand.ViewFactoryId) + .BuildBytes(); + + byte[] expected = + { + 0x0D, // START + 0x04, // LEN + 0x08, // CONTROL (RF) + 0x01, // COMMAND + 0x00, // CHECKSUM HI + 0x1A // CHECKSUM LO + }; + + CollectionAssert.AreEqual(expected, frame); + + string log = TouchReadLogger.DescribeTx(frame); + Console.WriteLine(log); + Console.WriteLine(@"Raw: < {0} >", HexFormatter.ToSerialHex(frame)); + } + + [TestMethod] + public void Encode_ViewProgrammableId_Command() + { + byte[] frame = new TouchReadFrameBuilder() + .RequestResponse(true) + .AddCommand(TouchReadCommand.ViewProgrammableId) + .BuildBytes(); + + byte[] expected = + { + 0x0D, // START + 0x04, // LEN + 0x08, // CONTROL (RF) + 0x03, // COMMAND + 0x00, // CHECKSUM HI + 0x1C // CHECKSUM LO + }; + + CollectionAssert.AreEqual(expected, frame); + + string log = TouchReadLogger.DescribeTx(frame); + Console.WriteLine(log); + Console.WriteLine(@"Raw: <{0}>", HexFormatter.ToSerialHex(frame)); + } + + [TestMethod] + public void Encode_SetState_Idle() + { + // Arrange + byte[] frame = new TouchReadFrameBuilder() + .RequestResponse(true) + .AddCommand(TouchReadCommand.SetState) + .AddPayload(new byte[] { 0x01 }) // Idle + .BuildBytes(); + + byte[] expected = + { + 0x0D, // START + 0x05, // LEN + 0x08, // CONTROL (RF) + 0x1A, // COMMAND (Set State) + 0x01, // PAYLOAD (Idle) + 0x00, // CHECKSUM HI + 0x35 // CHECKSUM LO + }; + + // Assert + CollectionAssert.AreEqual(expected, frame, + $"Encoded frame mismatch.\nExpected: {HexFormatter.ToSerialHex(expected)}\nActual: {HexFormatter.ToSerialHex(frame)}"); + } + + [TestMethod] + [ExpectedException(typeof(FormatException))] + public void Decode_InvalidStart_Throws() + { + byte[] response = + { + 0x00, // invalid START + 0x04, + 0x00, + 0x01, + 0x00, + 0x12 + }; + + var parser = new TouchReadFrameParser(); + + parser.Parse(response); + } + } +} \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadSerialIntegrationTests.cs b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadSerialIntegrationTests.cs new file mode 100644 index 000000000..554652324 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/TouchReadSerialIntegrationTests.cs @@ -0,0 +1,123 @@ +using System; +using System.IO.Ports; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger; + +namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4 +{ + [TestClass] + public class TouchReadSerialIntegrationTests + { + private const string ComPort = "COM3"; // CHANGE THIS + private const int BaudRate = 9600;//38400;//115200;//9600; // VERIFY FROM METER DOC + private const int ReadTimeoutMs = 2000; + + [TestMethod] + [TestCategory("Hardware")] + [TestCategory("Serial")] + public void Serial_ViewFactoryId_ReadSerialNumber() + { + // -------- Arrange -------- + byte[] request = new TouchReadFrameBuilder() + .RequestResponse(true) + .AddCommand(TouchReadCommand.ViewFactoryId) + .BuildBytes(); + + var parser = new TouchReadFrameParser(); + + using (var port = new SerialPort(ComPort, BaudRate, Parity.None, 8, StopBits.One)) + { + port.ReadTimeout = ReadTimeoutMs; + port.WriteTimeout = 500; + port.Open(); + + // Flush buffers + port.DiscardInBuffer(); + port.DiscardOutBuffer(); + + // -------- Act -------- + port.Write(request, 0, request.Length); + + Console.WriteLine("TX → " + HexFormatter.ToSerialHex(request)); + + byte[] response = ReadFullFrame(port); + + Console.WriteLine("RX ← " + HexFormatter.ToSerialHex(response)); + + TouchReadResponse decoded = parser.Parse(response); + + // -------- Assert -------- + Assert.AreEqual(0x01, decoded.Status, "Meter returned error status"); + + string serialNumber = decoded.GetAsciiPayload(); + + Assert.IsFalse(string.IsNullOrEmpty(serialNumber), + "Factory ID (serial number) is empty"); + + Console.WriteLine("Meter Factory ID: " + serialNumber); + } + } + + /// + /// Reads a full TouchRead frame from the serial port. + /// Blocks until complete frame or timeout. + /// + private static byte[] ReadFullFrame(SerialPort port) + { + // Read START + LEN first + byte start = (byte)port.ReadByte(); + if (start != 0x0D) + throw new InvalidOperationException("Invalid START byte from meter"); + + byte length = (byte)port.ReadByte(); + + // LEN counts from CONTROL to CHECKSUM + int remaining = length; + + byte[] buffer = new byte[2 + remaining]; + buffer[0] = start; + buffer[1] = length; + + int offset = 2; + while (remaining > 0) + { + int read = port.Read(buffer, offset, remaining); + offset += read; + remaining -= read; + } + + return buffer; + } + + + [TestMethod] + [TestCategory("Hardware")] + public void Serial_RawSniff() + { + using (var port = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One)) + { + port.ReadTimeout = 500; + port.Open(); + + Console.WriteLine("Listening for 5 seconds..."); + DateTime end = DateTime.Now.AddSeconds(5); + + while (DateTime.Now < end) + { + try + { + int b = port.ReadByte(); + Console.Write($"{b:X2} "); + } + catch (TimeoutException) + { + } + } + + Console.WriteLine("\nDone."); + } + } + + } +} \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParserTest.cs b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParserTest.cs new file mode 100644 index 000000000..89a70f117 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/diagnosticLed/DiagnosticLedParserTest.cs @@ -0,0 +1,189 @@ +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer; + +namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed +{ + [TestClass] + [TestSubject(typeof(DiagnosticLedParser))] + public class DiagnosticLedParserTest + { + private static string WithChecksum(string bodyWithoutChecksum) + { + byte sum = 0; + foreach (char c in bodyWithoutChecksum) + sum += (byte)c; + + return bodyWithoutChecksum + sum.ToString("X2") + "\r\n"; + } + + [TestMethod] + public void Parse_DiagnosticLed_State1() + { + string body = + "FFFF9C\t" + // signed 24-bit ADC = -100 + "2020\t" + // field strength + "FFFA\t" + // raw flow (-6) + "0050FC\t" + // raw volume + "0054\t"; // capacitor mV + + string line = WithChecksum(body); + + var parser = new DiagnosticLedParser(DiagnosticLedState.State1); + var data = (DiagnosticLedState1Data)parser.ParseLine(line); + + Assert.AreEqual(-100, data.Adc24); + Assert.AreEqual((ushort)0x2020, data.FieldStrength); + Assert.AreEqual((short)-6, data.RawFlow); + Assert.AreEqual((uint)0x0050FC, data.RawVolume); + Assert.AreEqual((ushort)0x0054, data.CapacitorMv); + } + + + [TestMethod] + public void Parse_DiagnosticLed_State2() + { + string line = + "00004F\t029A\t0000\tFFD3B1\t005C\t3B9AC9B1\t02\t01\t0D\r\n"; + + var parser = new DiagnosticLedParser(DiagnosticLedState.State2); + var data = (DiagnosticLedState2Data)parser.ParseLine(line); + + Assert.AreEqual(79, data.Adc24); + Assert.AreEqual((ushort)666, data.FieldStrength); + Assert.AreEqual((short)0, data.RawFlow); + Assert.AreEqual(0xFFD3B1u, data.RawVolume); + Assert.AreEqual((ushort)92, data.CapacitorMv); + Assert.AreEqual(0x3B9AC9B1u, data.LcdVolume); + Assert.AreEqual((byte)0x02, data.MeterState); + Assert.IsTrue(data.IsLowFlowCutoff); + } + + + [TestMethod] + public void Parse_DiagnosticLed_State3() + { + string body = + "FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t"; + + string line = WithChecksum(body); + + var parser = new DiagnosticLedParser(DiagnosticLedState.State3); + var data = (DiagnosticLedState3Data)parser.ParseLine(line); + + Assert.AreEqual(-13303, data.Adc24); + Assert.AreEqual((ushort)0x2020, data.FieldStrength); + Assert.AreEqual((short)-6, data.RawFlow); + Assert.AreEqual((uint)0x0050FC, data.RawVolume); + Assert.AreEqual((ushort)0x0054, data.CapacitorMv); + Assert.AreEqual((ushort)0x0B01, data.FieldCalibration); + Assert.AreEqual((uint)0x048000, data.AsicTimestamp); + Assert.AreEqual((byte)0xA7, data.FieldDriveTimeUs); + } + + + [TestMethod] + public void Parse_DiagnosticLed_State4() + { + string body = + "000ABC\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" + + "00001234\t00F0\t00F1\t0100\t0200\t03\t"; + + string line = WithChecksum(body); + + var parser = new DiagnosticLedParser(DiagnosticLedState.State4); + var data = (DiagnosticLedState4Data)parser.ParseLine(line); + + Assert.AreEqual(2748, data.Adc24); + Assert.AreEqual((ushort)0x2020, data.FieldStrength); + Assert.AreEqual((short)-6, data.RawFlow); + Assert.AreEqual((uint)0x0050FC, data.RawVolume); + Assert.AreEqual((ushort)0x0054, data.CapacitorMv); + + Assert.AreEqual((ushort)0x0B01, data.FieldCalibration); + Assert.AreEqual((uint)0x048000, data.AsicTimestamp); + Assert.AreEqual((byte)0xA7, data.FieldDriveTimeUs); + + Assert.AreEqual(0x00001234, data.MeanFlowRate); + Assert.AreEqual((ushort)0x00F0, data.Field1Measurement); + Assert.AreEqual((ushort)0x00F1, data.Field2Measurement); + Assert.AreEqual((ushort)0x0100, data.IntegratorCalibrationPositive); + Assert.AreEqual((ushort)0x0200, data.IntegratorCalibrationNegative); + Assert.AreEqual((byte)0x03, data.AsicState); + } + + [TestMethod] + public void Parse_DiagnosticLed_State5() + { + string body = + "FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" + + "00001234\t00F0\t00F1\t0100\t0200\t03\tFFEC\t"; + + string line = WithChecksum(body); + + var parser = new DiagnosticLedParser(DiagnosticLedState.State5); + var data = (DiagnosticLedState5Data)parser.ParseLine(line); + + Assert.AreEqual((short)-20, data.WaterImpedance); + Assert.AreEqual((byte)0x03, data.AsicState); + } + + [TestMethod] + public void Parse_DiagnosticLed_State6() + { + string body = + "FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" + + "00001234\t00F0\t00F1\t0100\t0200\t03\tFFEC\t0010\t" + + "02\t" + // pp spike detection + "02\t" + // ll pipe status + "00000099\t" + // LCD volume + "01\t"; // ASIC state1 + + + string line = WithChecksum(body); + + var parser = new DiagnosticLedParser(DiagnosticLedState.State6); + var data = (DiagnosticLedState6Data)parser.ParseLine(line); + + Assert.AreEqual((short)-20, data.WaterImpedance); + Assert.AreEqual((short)0x0010, data.ElectrodeDeltaMv); + Assert.AreEqual((byte)0x02, data.SpikeDetection); + Assert.AreEqual((byte)0x02, data.PipeStatus); + Assert.AreEqual((uint)0x99, data.LcdVolume); + Assert.AreEqual((byte)0x01, data.AsicState1); + } + + [TestMethod] + public void Parse_DiagnosticLed_State7() + { + string body = + "FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" + + "00001234\t00F0\t00F1\t0100\t0200\t03\tFFEC\t0010\t" + + "02\t" + // pp spike detection + "02\t" + // ll pipe status + "00000099\t" + // LCD volume + "01\t" + // ASIC state1 + "FFAA10\t" + // raw ADC before offset + "000123\t" + // detrended ADC + "FFEE\t" + // imaginary water impedance + "0011\t" + // electrode voltage noise + "03\t"; // ADC offset learning status + + string line = WithChecksum(body); + + var parser = new DiagnosticLedParser(DiagnosticLedState.State7); + var data = (DiagnosticLedState7Data)parser.ParseLine(line); + + Assert.AreEqual(-22000, data.RawAdcBeforeOffset); + Assert.AreEqual(0x000123, data.DetrendedAdc); + Assert.AreEqual((short)-18, data.ImaginaryWaterImpedance); + Assert.AreEqual((ushort)0x0011, data.ElectrodeVoltageNoise); + Assert.AreEqual((byte)0x03, data.AdcOffsetLearningStatus); + } + + + + + } +} \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParserTest.cs b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParserTest.cs new file mode 100644 index 000000000..b113ec2b6 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/ShortVariableLedParserTest.cs @@ -0,0 +1,44 @@ +using System; +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led; + +namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.led +{ + [TestClass] + [TestSubject(typeof(ShortVariableLedParser))] + public class ShortVariableLedParserTest + { + + [TestMethod] + public void Parse_ValidShortVariableMessage() + { + // Arrange + string raw = ";12345678,00012345.67;"; + var message = new TouchReadLedMessage(raw); + var parser = new ShortVariableLedParser(); + + // Act + TouchReadLedData data = parser.Parse(message); + + // Assert + Assert.IsNotNull(data); + Assert.AreEqual(raw, data.Raw); + Assert.AreEqual("12345678", data.MeterId); + Assert.AreEqual(12345.67m, data.Reading); + } + + [TestMethod] + [ExpectedException(typeof(FormatException))] + public void Parse_InvalidDecimal_Throws() + { + // Arrange + string raw = ";12345678,ABCDEF;"; + var message = new TouchReadLedMessage(raw); + var parser = new ShortVariableLedParser(); + + // Act + parser.Parse(message); + } + } +} \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessageTest.cs b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessageTest.cs new file mode 100644 index 000000000..5eb084082 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/iPerlASICReader/communication/C4/led/TouchReadLedMessageTest.cs @@ -0,0 +1,45 @@ +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led; + +namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.led +{ + [TestClass] + [TestSubject(typeof(TouchReadLedMessage))] + public class TouchReadLedMessageTest + { + + [TestMethod] + public void Parse_LedMessage_Basic() + { + string raw = ";12345678,00012345.67;"; + + var msg = new TouchReadLedMessage(raw); + + Assert.AreEqual(2, msg.Fields.Length); + Assert.AreEqual("12345678", msg.Fields[0]); + Assert.AreEqual("00012345.67", msg.Fields[1]); + } + + [TestMethod] + public void TouchReadLedData_Parse_Extended() + { + string raw = ";12345678,ABC123,00012345.67,m3;"; + + var msg = new TouchReadLedMessage(raw); + + var data = new TouchReadLedData(raw) + { + MeterId = msg.Fields[0], + CustomerId = msg.Fields[1], + Reading = TouchReadLedData.ParseDecimal(msg.Fields[2]), + Units = msg.Fields[3] + }; + + Assert.AreEqual("12345678", data.MeterId); + Assert.AreEqual("ABC123", data.CustomerId); + Assert.AreEqual(12345.67m, data.Reading); + Assert.AreEqual("m3", data.Units); + } + } +} \ No newline at end of file diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index cd6a043f3..0226c233e 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -103,6 +103,12 @@ + + + + + +