diff --git a/TBF/Rig/TestMethods/iPerlCommunication/common/OptoTelegramRaw.cs b/TBF/Rig/TestMethods/iPerlCommunication/common/OptoTelegramRaw.cs
new file mode 100644
index 000000000..119931a71
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/common/OptoTelegramRaw.cs
@@ -0,0 +1,327 @@
+///
+/// Copyright (c) 2015-2021 Sensus Metering Systems
+///
+
+using System;
+using System.Globalization;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.parserer;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.common
+{
+ public enum OptoTelegramFlags : byte
+ {
+ OK = 0,
+ OK_TestStart,
+ OK_TestEnd,
+ InvalidTelegram, /// Wrong telegram format of checksum error
+ SyncError,
+ }
+
+ public class OptoTelegramRaw
+ {
+ public static readonly int Length = 42;
+ private static CultureInfo culture;
+
+
+ ///
+ /// Strobed value
+ ///
+ public static decimal TestStartTimestampDec;
+
+ ///
+ /// Stored values
+ ///
+ public OptoTelegramFlags Flags;
+
+ public DateTime DateTime; /// From PC
+ public float RefFlow; /// [m3/h]
+ public int Counter;
+
+ public Int32 EmfRaw; /// Signed EMF from iPerl opto data
+ public Int16 MagneticFieldRaw;
+ public Int16 FlowRaw;
+ public UInt32 VolumeRaw;
+ public Int64 VolumeRawExt;
+ public Int16 Impedance;
+ public UInt32 Timestamp;
+ public Int64 TimestampExt;
+ public byte CheckSum;
+
+ ///
+ /// Calculated values
+ ///
+ public double EMF()
+ {
+ return 0.000000333 * (double)EmfRaw;
+ }
+ public double MagneticField() { return (double)MagneticFieldRaw; }
+ public double Flow(double scalingFactor) { return 0.225 * scalingFactor * (double)FlowRaw; }
+ public double Volume(double scalingFactor) { return 0.0000625 * scalingFactor * (double)VolumeRawExt; }
+ public Int32 FlipTime() { return Impedance; }
+ public decimal TimestampDec() { return (decimal)TimestampExt / (decimal)8192; }
+ public double VolumeDelta(double scalingFactor, OptoTelegramRaw previous) { return (previous == null) ? 0 : Volume(scalingFactor) - previous.Volume(scalingFactor); }
+ public decimal TimeDelta() { return TimestampDec() - TestStartTimestampDec; }
+ public string Label()
+ {
+ if (Flags == OptoTelegramFlags.OK_TestStart) return "#### start test ####";
+ else if (Flags == OptoTelegramFlags.OK_TestEnd) return "#### end of test ####";
+ else return string.Empty;
+ }
+
+
+ static OptoTelegramRaw()
+ {
+ culture = CultureInfo.CreateSpecificCulture("DE"); /// This is to use comma as decimal number separator
+ }
+
+ public OptoTelegramRaw()
+ {
+ }
+
+ ///
+ /// Parses optical telegram and returns OptoTelegramRaw object
+ ///
+ ///
+ /// Create a configuration structure from a complete byte array
+ ///
+ /// Telegram description:
+ ///
+ /// AAAAAA[tab]BBBB[tab]CCCC[tab]DDDDDD[tab]EEEE[tab]FFFFFFFF[tab]GG[cr][lf] (42 bytes)
+ ///
+ /// Data Comment Type Calculate to decimal
+ /// ----------------------------------------------------------------
+ /// AAAAAA EMF Int24 Value * 0.000000333
+ /// BBBB Magnetic field Int16 Value
+ /// CCCC Flow Int16 Value * 0.225 * Scalig factor
+ /// DDDDDD Volume Int24 Value / 16000 * Scaling factor
+ /// EEEE Impedance Int16 Value
+ /// FFFFFFFF Timestamp Uint32 Value / 8192
+ /// GG Checksum Byte
+ /// ----------------------------------------------------------------
+ ///
+ /// Example:
+ /// FFFFFE 51EA 0000 65324E 0087 F6319DFF 86
+ /// FFDD3A 51F9 0000 65324E 0088 F631A60B 45
+ /// ...
+ ///
+ /// A complete byte array data
+ /// true = telegram OK, false = telegram NOK
+ public bool UpdateFromString(string telegram, int counter, float refFlow, ref Int64 volumeRawExtLast, ref Int64 timestampExtLast, bool isLog = false)
+ {
+ DateTime = DateTime.Now;
+ Counter = counter;
+ RefFlow = refFlow;
+
+ if ((telegram == null) || (telegram.Length < Length) ||
+ (telegram[6] != '\t') || (telegram[11] != '\t') || (telegram[16] != '\t') ||
+ (telegram[23] != '\t') || (telegram[28] != '\t') || (telegram[37] != '\t') ||
+ (!isLog && (telegram[40] != '\r' || telegram[41] != '\n')))
+ {
+ Flags = OptoTelegramFlags.InvalidTelegram;
+ return false;
+ }
+
+ UInt32 uEmfRaw;
+ bool f1 = UInt32.TryParse(telegram.Substring(0, 6), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out uEmfRaw);
+ EmfRaw = (uEmfRaw > 0x7FFFFF) ? ((int)uEmfRaw - 0x1000000) : (int)uEmfRaw;
+
+ bool f2 = Int16.TryParse(telegram.Substring(7, 4), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out MagneticFieldRaw);
+ bool f3 = Int16.TryParse(telegram.Substring(12, 4), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out FlowRaw);
+ bool f4 = UInt32.TryParse(telegram.Substring(17, 6), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out VolumeRaw);
+ bool f5 = Int16.TryParse(telegram.Substring(24, 4), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out Impedance);
+ bool f6 = UInt32.TryParse(telegram.Substring(29, 8), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out Timestamp);
+ bool f7 = byte.TryParse(telegram.Substring(38, 2), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out CheckSum);
+
+ byte calculatedCheckSum = 0;
+ for (int i = 0; i < Length - 4; i++)
+ {
+ calculatedCheckSum += (byte)telegram[i];
+ }
+
+ bool allOk = f1 && f2 && f3 && f4 && f5 && f6 && f7 && (calculatedCheckSum == CheckSum);
+
+ if (allOk)
+ {
+ ///
+ /// Cope with 'VolumeRaw' overflow
+ ///
+ Int64 uncorrected = (Int64)(((UInt64)volumeRawExtLast & 0xFFFFFFFFFF000000UL) | VolumeRaw);
+ if (Math.Abs(uncorrected - volumeRawExtLast) <= 0x800000L)
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected;
+ }
+ else if (Math.Abs(uncorrected + 0x1000000L - volumeRawExtLast) <= 0x800000L)
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected + 0x1000000L;
+ }
+ else if (Math.Abs(uncorrected - 0x1000000L - volumeRawExtLast) <= 0x800000L)
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected - 0x1000000L;
+ }
+ else
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected;
+ }
+
+ ///
+ /// Cope with 'Timestamp' overflow
+ ///
+ uncorrected = (Int64)(((UInt64)timestampExtLast & 0xFFFFFFFF00000000UL) | Timestamp);
+ if (Math.Abs(uncorrected - timestampExtLast) <= 0x80000000L)
+ {
+ TimestampExt = timestampExtLast = uncorrected;
+ }
+ else if (Math.Abs(uncorrected + 0x100000000L - timestampExtLast) <= 0x80000000L)
+ {
+ TimestampExt = timestampExtLast = uncorrected + 0x100000000L;
+ }
+ else if (Math.Abs(uncorrected - 0x100000000L - timestampExtLast) <= 0x80000000L)
+ {
+ TimestampExt = timestampExtLast = uncorrected - 0x100000000L;
+ }
+ else
+ {
+ TimestampExt = timestampExtLast = uncorrected;
+ }
+ }
+
+ Flags = allOk ? OptoTelegramFlags.OK : OptoTelegramFlags.InvalidTelegram;
+
+ return allOk;
+ }
+
+ //New IPERL ASIC
+ public void UpdateFromSmart(DiagnosticLedState4Data data, int counter, float refFlow,
+ ref Int64 volumeRawExtLast, ref Int64 timestampExtLast)
+ {
+ DateTime = DateTime.Now;
+ Counter = counter;
+ RefFlow = refFlow;
+
+ FlowRaw = data.RawFlow;
+ VolumeRaw = data.RawVolume;
+ Timestamp = data.AsicTimestamp;
+
+
+ ///
+ /// Cope with 'VolumeRaw' overflow
+ ///
+ Int64 uncorrected = (Int64)(((UInt64)volumeRawExtLast & 0xFFFFFFFFFF000000UL) | VolumeRaw);
+ if (Math.Abs(uncorrected - volumeRawExtLast) <= 0x800000L)
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected;
+ }
+ else if (Math.Abs(uncorrected + 0x1000000L - volumeRawExtLast) <= 0x800000L)
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected + 0x1000000L;
+ }
+ else if (Math.Abs(uncorrected - 0x1000000L - volumeRawExtLast) <= 0x800000L)
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected - 0x1000000L;
+ }
+ else
+ {
+ VolumeRawExt = volumeRawExtLast = uncorrected;
+ }
+
+ ///
+ /// Cope with 'Timestamp' overflow
+ ///
+ uncorrected = (Int64)(((UInt64)timestampExtLast & 0xFFFFFFFF00000000UL) | Timestamp);
+ if (Math.Abs(uncorrected - timestampExtLast) <= 0x80000000L)
+ {
+ TimestampExt = timestampExtLast = uncorrected;
+ }
+ else if (Math.Abs(uncorrected + 0x100000000L - timestampExtLast) <= 0x80000000L)
+ {
+ TimestampExt = timestampExtLast = uncorrected + 0x100000000L;
+ }
+ else if (Math.Abs(uncorrected - 0x100000000L - timestampExtLast) <= 0x80000000L)
+ {
+ TimestampExt = timestampExtLast = uncorrected - 0x100000000L;
+ }
+ else
+ {
+ TimestampExt = timestampExtLast = uncorrected;
+ }
+ }
+
+ ///
+ /// Alternative to UpdateFromString(...) when data are flushed
+ ///
+ public bool UpdateFromStringDummy(string telegram)
+ {
+ DateTime = DateTime.Now;
+ RefFlow = 0;
+
+ if ((telegram == null) || (telegram.Length < Length) ||
+ (telegram[6] != '\t') || (telegram[11] != '\t') || (telegram[16] != '\t') ||
+ (telegram[23] != '\t') || (telegram[28] != '\t') || (telegram[37] != '\t') ||
+ (telegram[40] != '\r') || (telegram[41] != '\n'))
+ {
+ Flags = OptoTelegramFlags.InvalidTelegram;
+ return false;
+ }
+
+ bool f7 = byte.TryParse(telegram.Substring(38, 2), NumberStyles.HexNumber, CultureInfo.CurrentCulture, out CheckSum);
+
+ byte calculatedCheckSum = 0;
+ for (int i = 0; i < Length - 4; i++)
+ {
+ calculatedCheckSum += (byte)telegram[i];
+ }
+
+ bool allOk = f7 && (calculatedCheckSum == CheckSum);
+
+ Flags = allOk ? OptoTelegramFlags.OK : OptoTelegramFlags.InvalidTelegram;
+
+ return allOk;
+ }
+
+
+ public void SetFlags(OptoTelegramFlags flags)
+ {
+ this.Flags = flags;
+ }
+
+
+ public string ToString(double scalingFactor, OptoTelegramRaw previous)
+ {
+ if (Flags == OptoTelegramFlags.SyncError)
+ {
+ return "Sychronization error";
+ }
+ else if (Flags == OptoTelegramFlags.InvalidTelegram)
+ {
+ return "Invalid telegram";
+ }
+ else /// if (flags == OptoTelegramFlags.OK / OptoTelegramFlags.OK_TestStart / OptoTelegramFlags.OK_TestEnd)
+ {
+ return string.Format("{0}:{1}:{2}.{3}\t{4} :\t{5}\t{6}\t{7}\t{8}\t{9}\t{10}\t{11}\t{12}\t{13}\t{14}\t{15}\t{16}\t{17}\t{18}\t{19}\t{20}\t{21}\t{22}",
+ DateTime.Hour.ToString("D2"),
+ DateTime.Minute.ToString("D2"),
+ DateTime.Second.ToString("D2"),
+ DateTime.Millisecond.ToString("D4"),
+ Counter,
+ (EmfRaw & 0x00FFFFFF).ToString("X6"),
+ MagneticFieldRaw.ToString("X4"),
+ FlowRaw.ToString("X4"),
+ VolumeRaw.ToString("X6"),
+ Impedance.ToString("X4"),
+ Timestamp.ToString("X8"),
+ CheckSum.ToString("X2"),
+ EMF().ToString("F4", culture),
+ MagneticField().ToString("F0", culture),
+ Flow(scalingFactor).ToString("F2", culture),
+ Volume(scalingFactor).ToString("F4", culture),
+ FlipTime().ToString("F0", culture),
+ TimestampDec().ToString("F4", culture),
+ (RefFlow * 1000).ToString("F2", culture),
+ VolumeDelta(scalingFactor, previous).ToString("F4", culture),
+ TimeDelta().ToString("F3", culture),
+ scalingFactor.ToString("F1", culture),
+ Label());
+ }
+ }
+ }
+}
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/Constants.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/Constants.cs
new file mode 100644
index 000000000..3d06977ea
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/Constants.cs
@@ -0,0 +1,15 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol
+{
+ public static class Constants
+ {
+ public const byte Start = 0x53; //'S'
+ public const byte Write = 0x57; // 'W'
+ public const byte Read = 0x52; // 'R'
+ public const byte End = 0x0D; //'.'
+ public const byte Question = (byte)0x3F; // '?'
+ public static readonly byte[] Version = {0x76, 0x65, 0x72, 0x73 }; // 'v' 'e' 'r' 's'
+
+ public const byte StatusOk = 0x01;
+ public const byte StatusNok = 0x00;
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrame.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrame.cs
new file mode 100644
index 000000000..7730399e9
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrame.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol
+{
+ public sealed class IperlHatFrame
+ {
+ public byte Start { get; }
+ public byte Direction { get; }
+ public byte End { get; }
+ public byte Length { get; }
+
+ public byte[] CommandInformation { get; }
+ public byte[] Payload { get; }
+
+ public IperlHatFrame(byte start, byte direction, byte length, byte[] commandBytes, byte[] payload, byte end)
+ {
+ Start = start;
+ Direction = direction;
+ Length = length;
+ CommandInformation = commandBytes ?? Array.Empty();
+ Payload = payload ?? Array.Empty();
+ End = end;
+ }
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrameBuilder.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrameBuilder.cs
new file mode 100644
index 000000000..b7535b9a7
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrameBuilder.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol
+{
+ public sealed class IperlHatFrameBuilder
+ {
+
+ private byte _direction;
+ private readonly List _commandBytes = new List();
+ private readonly List _payload = new List();
+
+ public IperlHatFrameBuilder RequestResponse(bool enabled)
+ {
+ _direction = enabled ? TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Write : TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Read;
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddCommand(ProtocolCommand command)
+ {
+ _commandBytes.Add((byte)command);
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddSubCommand(ProtocolCommand subCommand)
+ {
+ if (_commandBytes.Count == 0 ||
+ _commandBytes[0] != (byte)ProtocolCommand.DeviceSpecific)
+ throw new InvalidOperationException(
+ "Sub-command is only valid for DeviceSpecific (0xFD) commands.");
+
+ _commandBytes.Add((byte)subCommand);
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddSubCommand(ProtocolStatuses subCommand)
+ {
+ if (_commandBytes.Count == 0 ||
+ _commandBytes[0] != (byte)ProtocolCommand.SetState)
+ throw new InvalidOperationException(
+ "Sub-command is only valid for SetState (0xA1) commands.");
+
+ _commandBytes.Add((byte)subCommand);
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddDeviceCommand(
+ ProtocolDeviceSubCommand subCommand)
+ {
+ _commandBytes.Add((byte)ProtocolCommand.DeviceSpecific);
+ _commandBytes.Add((byte)subCommand);
+ return this;
+ }
+
+ public IperlHatFrameBuilder SetVersionCommand()
+ {
+ _commandBytes.Add((byte)ProtocolCommand.Question);
+ _payload.AddRange(TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Version);
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddPayload(byte[] payload)
+ {
+ if (payload != null)
+ _payload.AddRange(payload);
+
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddPayload(DiagnosticLedState state)
+ {
+ _payload.Add((byte)state);
+
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddPayload(byte payload)
+ {
+ _payload.Add(payload);
+
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddDiagnosticLedState(DiagnosticLedState state)
+ {
+ RequestResponse(true);
+ AddDeviceCommand(ProtocolDeviceSubCommand.SetDiagnosticLEDState);
+ AddPayload((byte)state);
+ return this;
+ }
+
+ public IperlHatFrameBuilder AddNullTerminatedAscii(string text)
+ {
+ if (!string.IsNullOrEmpty(text))
+ _commandBytes.AddRange(
+ System.Text.Encoding.ASCII.GetBytes(text));
+
+ _commandBytes.Add(0x00);
+ return this;
+ }
+
+ public IperlHatFrame BuildFrame()
+ {
+ if (_commandBytes.Count == 0)
+ throw new InvalidOperationException("No command specified.");
+
+ byte length = (byte)(4 + _commandBytes.Count + _payload.Count); // 4 = START + dirrection + LEN + END
+
+
+ return new IperlHatFrame(
+ TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Start,
+ _direction,
+ length,
+ _commandBytes.ToArray(),
+ _payload.ToArray(),
+ TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.End);
+ }
+
+ public byte[] BuildBytes()
+ {
+ IperlHatFrame frame = BuildFrame();
+
+ if (frame.CommandInformation.Length > 0 && frame.CommandInformation[0] == TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Question)
+ {
+ var bytes = new List
+ {
+ frame.Start,
+ frame.Direction,
+ };
+
+ bytes.AddRange(frame.CommandInformation);
+ bytes.AddRange(frame.Payload);
+ bytes.Add(frame.End);
+
+ return bytes.ToArray();
+ }
+ else
+ {
+ var bytes = new List
+ {
+ frame.Start,
+ frame.Direction,
+ frame.Length,
+ };
+
+ bytes.AddRange(frame.CommandInformation);
+ bytes.AddRange(frame.Payload);
+ bytes.Add(frame.End);
+
+ return bytes.ToArray();
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrameParser.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrameParser.cs
new file mode 100644
index 000000000..59d2ef724
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatFrameParser.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol
+{
+ public sealed class IperlHatFrameParser
+ {
+
+ public IperlHatResponse Parse(byte[] data)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ if (data.Length < 5)
+ throw new FormatException("Frame too short.");
+
+
+
+ if (data[0] != TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Start)
+ {
+ //if version parse version
+ if (data[0] == TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Question)
+ {
+ //Define Question answer
+ var prefix = new List{ TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Question };
+ var end = new List{ TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.End };
+
+ if (IsPrefixValid(data, prefix, end))
+ {
+ //whole payload may be like "vers: Harry T:B800, V:06.06.01, FW:190215, 7ECE, B1.6.01, HW:4, Serial:0"
+ prefix = new List{ TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Question };
+ byte[] payloadVersion = ExtractPayloadUsePrefix(data, prefix, end);
+ return new IperlHatResponse(TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Question, payloadVersion.Length > 0 ? TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.StatusOk : TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.StatusNok, payloadVersion);
+ }
+ }
+
+ throw new FormatException("Invalid START byte.");
+ }
+
+ if (data[1] != TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Read)
+ throw new FormatException("Frame is no Response.");
+
+ byte length = data[2];
+ if (length != data.Length)
+ throw new FormatException("Length mismatch.");
+
+ byte direction = data[1];
+ byte status = data[3];
+
+ var prefixCommand = new List{ TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Start,direction,length,status };
+ var endCommand = new List{ TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.End };
+
+ byte[] payload = ExtractPayloadUsePrefix(data,prefixCommand,endCommand);
+
+ return new IperlHatResponse(0x00, status, payload);
+ }
+
+
+ private static byte[] ExtractPayloadUsePrefix(byte[] data, List prefix, List end)
+ {
+ // payload exists only if frame longer than:
+ // START + DIRECTION + LEN + CTRL + END = 5 bytes
+ // OR VERSION_START + VERSION = 5 bytes
+ if (data.Length <= 5)
+ return Array.Empty();
+
+ //check prefix is equal
+ int prefixLength = prefix.Count;
+ byte[] commandPrefix = new byte[prefixLength];
+ Buffer.BlockCopy(data, 0, commandPrefix, 0, prefixLength);
+
+ if (StartsWithPrefix(end, commandPrefix))
+ {
+ return Array.Empty();
+ }
+
+ int payloadLength = data.Length - (prefix.Count + end.Count);
+ byte[] payload = new byte[payloadLength];
+ Buffer.BlockCopy(data, prefix.Count, payload, 0, payloadLength);
+ return payload;
+ }
+
+ private static bool IsPrefixValid(byte[] data, List prefix, List end)
+ {
+ int prefixLength = prefix.Count;
+ // payload exists only if frame longer than:
+ // OR VERSION_START + VERSION = 5 bytes - "?VERS" version implemented
+ if (data.Length <= prefixLength) // need be and on END
+ return false;
+
+ //check prefix is equal
+ byte[] commandPrefix = new byte[prefixLength];
+ Buffer.BlockCopy(data, 0, commandPrefix, 0, prefixLength);
+
+ if (StartsWithPrefix(end, commandPrefix))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool StartsWithPrefix(List data, byte[] prefix)
+ {
+ if (data.Count < prefix.Length)
+ return false;
+
+ for (int i = 0; i < prefix.Length; i++)
+ {
+ if (data[i] != prefix[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ private static byte[] ExtractVersionPayload(byte[] data)
+ {
+ // payload exists only if frame longer than:
+ // START + LEN + CTRL + STATUS + CHK_HI + CHK_LO = 6 bytes
+ if (data.Length <= 5)
+ return Array.Empty();
+
+ int payloadLength = data.Length - 4;
+ byte[] payload = new byte[payloadLength];
+ Buffer.BlockCopy(data, 5, payload, 0, payloadLength);
+ return payload;
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatProtocol.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatProtocol.cs
new file mode 100644
index 000000000..2cc4b1c4e
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatProtocol.cs
@@ -0,0 +1,10 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol
+{
+ public static class IperlHatProtocol
+ {
+ 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/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatResponse.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatResponse.cs
new file mode 100644
index 000000000..d60214966
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/IperlHatProtocol/IperlHatResponse.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol
+{
+ public sealed class IperlHatResponse
+ {
+ public byte Control { get; } //classic control byte - valid for question now
+ private byte Status { get; }
+ public byte[] Payload { get; }
+
+ public bool IsOk => Status == TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.StatusOk;
+
+ public IperlHatResponse(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/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/DiagnosticLedParser.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/DiagnosticLedParser.cs
new file mode 100644
index 000000000..2c7e07b94
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/DiagnosticLedParser.cs
@@ -0,0 +1,75 @@
+using System;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.parserer;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed
+{
+ public sealed class DiagnosticLedParser
+{
+ private readonly DiagnosticLedState _state;
+
+ public DiagnosticLedParser(DiagnosticLedState state)
+ {
+ _state = state;
+ }
+
+ public DiagnosticLedData ParseLine(string line, bool checkLineTermination = true)
+ {
+ if (string.IsNullOrEmpty(line))
+ throw new ArgumentNullException(nameof(line));
+
+ if (checkLineTermination && !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/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/DiagnosticLedState.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/DiagnosticLedState.cs
new file mode 100644
index 000000000..cc958fac0
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/DiagnosticLedState.cs
@@ -0,0 +1,95 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed
+{
+ ///
+ /// Diagnostic LED output mode.
+ ///
+ /// Determines the format and content of high-speed serial diagnostic data
+ /// emitted by the meter when the diagnostic LED is enabled.
+ ///
+ ///
+ /// Each state corresponds to a specific TAB-separated ASCII HEX frame layout
+ /// as defined in the iPERL TouchRead protocol documentation.
+ ///
+ ///
+ /// See
+ /// diagnostic LED States.
+ ///
+ ///
+ public enum DiagnosticLedState : byte
+ {
+ ///
+ /// Diagnostic LED OFF - State #0.
+ ///
+ /// Basic diagnostic output containing raw ADC, field strength,
+ /// flow rate, volume accumulator, and capacitor voltage.
+ ///
+ ///
+ StateOFF = 0x00,
+
+ ///
+ /// Diagnostic LED State #1.
+ ///
+ /// Basic diagnostic output containing raw ADC, field strength,
+ /// flow rate, volume accumulator, and capacitor voltage.
+ ///
+ ///
+ State1 = 0x01,
+
+ ///
+ /// Diagnostic LED State #2.
+ ///
+ /// Extends State #1 with LCD volume, meter state,
+ /// and low-flow cutoff indication.
+ ///
+ ///
+ State2 = 0x02,
+
+ ///
+ /// Diagnostic LED State #3.
+ ///
+ /// Extends State #1 with field calibration value,
+ /// ASIC timestamp, and field drive time.
+ ///
+ ///
+ State3 = 0x03,
+
+ ///
+ /// Diagnostic LED State #4.
+ ///
+ /// Extended diagnostic output including mean flow rate,
+ /// field measurements, integrator calibration values,
+ /// and ASIC state.
+ ///
+ ///
+ State4 = 0x04,
+
+ ///
+ /// Diagnostic LED State #5.
+ ///
+ /// Extends State #4 with water impedance measurement.
+ ///
+ ///
+ State5 = 0x05,
+
+ ///
+ /// Diagnostic LED State #6.
+ ///
+ /// Extends State #5 with electrode delta, spike detection data,
+ /// pipe status, LCD volume, and additional ASIC state.
+ ///
+ ///
+ State6 = 0x06,
+
+ ///
+ /// Diagnostic LED State #7.
+ ///
+ /// Extends State #6 with raw ADC before offset correction,
+ /// detrended ADC value, imaginary water impedance,
+ /// electrode voltage noise, and ADC offset learning status.
+ ///
+ ///
+ State7 = 0x07
+ }
+}
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedData.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedData.cs
new file mode 100644
index 000000000..1ca1709db
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedData.cs
@@ -0,0 +1,89 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.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
+ {
+
+ public abstract int GetByteCount();
+
+ ///
+ /// 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/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedFrameSpec.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedFrameSpec.cs
new file mode 100644
index 000000000..d034dcaef
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedFrameSpec.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.parserer
+{
+ public static class DiagnosticLedFrameSpec
+ {
+ public static int GetExpectedAsciiLength(DiagnosticLedState state)
+ {
+ switch (state)
+ {
+ case DiagnosticLedState.State1: return 33;
+ case DiagnosticLedState.State2: return 48;
+ case DiagnosticLedState.State3: return 50;
+ case DiagnosticLedState.State4: return 84;
+ case DiagnosticLedState.State5: return 89;
+ case DiagnosticLedState.State6: return 112;
+ case DiagnosticLedState.State7: return 139;
+ default: throw new ArgumentOutOfRangeException(nameof(state));
+ }
+ }
+
+ public static int GetExpectedFieldCount(DiagnosticLedState state)
+ {
+ switch (state)
+ {
+ case DiagnosticLedState.State1: return 6;
+ case DiagnosticLedState.State2: return 9;
+ case DiagnosticLedState.State3: return 9;
+ case DiagnosticLedState.State4: return 15;
+ case DiagnosticLedState.State5: return 16;
+ case DiagnosticLedState.State6: return 21;
+ case DiagnosticLedState.State7: return 26;
+ default: throw new ArgumentOutOfRangeException(nameof(state));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState1Data.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState1Data.cs
new file mode 100644
index 000000000..6d9edeee0
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState1Data.cs
@@ -0,0 +1,58 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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]);
+ }
+
+ public override string ToString()
+ {
+ return $"DiagnosticLedState1Data: Adc24={Adc24}, FieldStrength={FieldStrength}, RawFlow={RawFlow}, RawVolume={RawVolume}, CapacitorMv={CapacitorMv}";
+ }
+
+ ///
+ /// Format: xxxxxx aaaa yyyy vvvvvv cccc ss
+ /// Chars total = 26
+ /// Tabs = 5
+ /// CRLF = 2
+ /// Total bytes = 33
+ ///
+ /// Total bytes
+ public override int GetByteCount()
+ {
+ return 33;
+ }
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState2Data.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState2Data.cs
new file mode 100644
index 000000000..cda442695
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState2Data.cs
@@ -0,0 +1,90 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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;
+ }
+
+ public override string ToString()
+ {
+ return $"DiagnosticLedState2Data: Adc24={Adc24}, FieldStrength={FieldStrength}, RawFlow={RawFlow}, RawVolume={RawVolume}, CapacitorMv={CapacitorMv}, LcdVolume={LcdVolume}, MeterState={MeterState}, IsLowFlowCutoff={IsLowFlowCutoff}";
+ }
+
+ ///
+ /// Format: xxxxxx aaaa yyyy vvvvvv cccc gggggggg mm ff ss
+ /// Chars total = 38
+ /// Tabs = 8
+ /// CRLF = 2
+ /// Total bytes = 48
+ ///
+ /// Total bytes
+ public override int GetByteCount()
+ {
+ return 48;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState3Data.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState3Data.cs
new file mode 100644
index 000000000..09a9f22bf
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState3Data.cs
@@ -0,0 +1,89 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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]);
+ }
+
+ public override string ToString()
+ {
+ return $"DiagnosticLedState3Data: Adc24={Adc24}, FieldStrength={FieldStrength}, RawFlow={RawFlow}, RawVolume={RawVolume}, CapacitorMv={CapacitorMv}, FieldCalibration={FieldCalibration}, AsicTimestamp={AsicTimestamp}, FieldDriveTimeUs={FieldDriveTimeUs}";
+ }
+
+ ///
+ /// Format: xxxxxx aaaa yyyy vvvvvv cccc tttt bbbbbbbb ff ss
+ /// Chars total = 40
+ /// Tabs = 8
+ /// CRLF = 2
+ /// Total bytes = 50
+ ///
+ /// Total bytes
+ public override int GetByteCount()
+ {
+ return 50;
+ }
+ }
+}
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState4Data.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState4Data.cs
new file mode 100644
index 000000000..76af9df4c
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState4Data.cs
@@ -0,0 +1,91 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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]);
+ }
+
+ public override string ToString()
+ {
+ return $"DiagnosticLedState4Data: Adc24={Adc24}, FieldStrength={FieldStrength}, RawFlow={RawFlow}, RawVolume={RawVolume}, CapacitorMv={CapacitorMv}, FieldCalibration={FieldCalibration}, AsicTimestamp={AsicTimestamp}, FieldDriveTimeUs={FieldDriveTimeUs}, MeanFlowRate={MeanFlowRate}, Field1Measurement={Field1Measurement}, Field2Measurement={Field2Measurement}, IntegratorCalibrationPositive={IntegratorCalibrationPositive}, IntegratorCalibrationNegative={IntegratorCalibrationNegative}, AsicState={AsicState}";
+ }
+
+ ///
+ /// Format: xxxxxx aaaa yyyy vvvvvv cccc tttt bbbbbbbb mmmmmmmm ff gggg hhhh cccc nnnn qq ss
+ /// Chars total = 68
+ /// Tabs = 14
+ /// CRLF = 2
+ /// Total bytes = 84
+ ///
+ /// Total bytes
+ public override int GetByteCount()
+ {
+ return 84;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState5Data.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState5Data.cs
new file mode 100644
index 000000000..c541fa74a
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState5Data.cs
@@ -0,0 +1,111 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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]);
+ }
+
+ public override string ToString()
+ {
+ return $"DiagnosticLedState5Data: Adc24={Adc24}, FieldStrength={FieldStrength}, RawFlow={RawFlow}, RawVolume={RawVolume}, CapacitorMv={CapacitorMv}, FieldCalibration={FieldCalibration}, AsicTimestamp={AsicTimestamp}, FieldDriveTimeUs={FieldDriveTimeUs}, MeanFlowRate={MeanFlowRate}, Field1Measurement={Field1Measurement}, Field2Measurement={Field2Measurement}, IntegratorCalibrationPositive={IntegratorCalibrationPositive}, IntegratorCalibrationNegative={IntegratorCalibrationNegative}, AsicState={AsicState}, WaterImpedance={WaterImpedance}";
+ }
+
+ ///
+ /// Format: xxxxxx aaaa yyyy vvvvvv cccc tttt bbbbbbbb mmmmmmmm ff iiii ss
+ /// Chars total = 72
+ /// Tabs = 15
+ /// CRLF = 2
+ /// Total bytes = 89
+ ///
+ /// Total bytes
+ public override int GetByteCount()
+ {
+ return 89;
+ }
+}
+
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState6Data.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState6Data.cs
new file mode 100644
index 000000000..d74df8ed8
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState6Data.cs
@@ -0,0 +1,152 @@
+using System;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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;
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"DiagnosticLedState6Data: Adc24={Adc24}, FieldStrength={FieldStrength}, RawFlow={RawFlow}, RawVolume={RawVolume}, CapacitorMv={CapacitorMv}, FieldCalibration={FieldCalibration}, AsicTimestamp={AsicTimestamp}, FieldDriveTimeUs={FieldDriveTimeUs}, MeanFlowRate={MeanFlowRate}, Field1Measurement={Field1Measurement}, Field2Measurement={Field2Measurement}, IntegratorCalibrationPositive={IntegratorCalibrationPositive}, IntegratorCalibrationNegative={IntegratorCalibrationNegative}, AsicState0={AsicState0}, WaterImpedance={WaterImpedance}, SpikeDetection={SpikeDetection}, PipeStatus={PipeStatus}, LcdVolume={LcdVolume}, AsicState1={AsicState1}";
+ }
+
+ ///
+ /// Format: xxxxxx aaaa yyyy vvvvvv cccc tttt bbbbbbbb mmmmmmmm ff iiii rrrr pp ll dddddddd oo ss
+ /// Chars total = 90
+ /// Tabs = 20
+ /// CRLF = 2
+ /// Total bytes = 112
+ ///
+ /// Total bytes
+ public override int GetByteCount()
+ {
+ return 112;
+ }
+}
+
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState7Data.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState7Data.cs
new file mode 100644
index 000000000..7e203e254
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/DiagnosticLedState7Data.cs
@@ -0,0 +1,155 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.utils;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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]);
+ }
+
+ public override string ToString()
+ {
+ return $"DiagnosticLedState7Data: Adc24={Adc24}, FieldStrength={FieldStrength}, RawFlow={RawFlow}, RawVolume={RawVolume}, CapacitorMv={CapacitorMv}, FieldCalibration={FieldCalibration}, AsicTimestamp={AsicTimestamp}, FieldDriveTimeUs={FieldDriveTimeUs}, MeanFlowRate={MeanFlowRate}, Field1Measurement={Field1Measurement}, Field2Measurement={Field2Measurement}, IntegratorCalibrationPositive={IntegratorCalibrationPositive}, IntegratorCalibrationNegative={IntegratorCalibrationNegative}, AsicState0={AsicState0}, WaterImpedance={WaterImpedance}, ElectrodeDeltaMv={ElectrodeDeltaMv}, SpikeDetection={SpikeDetection}, PipeStatus={PipeStatus}, LcdVolume={LcdVolume}, AsicState1={AsicState1}, RawAdcBeforeOffset={RawAdcBeforeOffset}, DetrendedAdc={DetrendedAdc}, ImaginaryWaterImpedance={ImaginaryWaterImpedance}, ElectrodeVoltageNoise={ElectrodeVoltageNoise}, AdcOffsetLearningStatus={AdcOffsetLearningStatus}";
+ }
+
+ ///
+ /// Format:
+ /// Chars total = 112
+ /// Tabs = 25
+ /// CRLF = 2
+ /// Total bytes = 139
+ ///
+ /// Total bytes
+ public override int GetByteCount()
+ {
+ return 139;
+ }
+}
+
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/PipeStatus.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/PipeStatus.cs
new file mode 100644
index 000000000..ce229feb9
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/PipeStatus.cs
@@ -0,0 +1,11 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/SpikeDetectionStatus.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/SpikeDetectionStatus.cs
new file mode 100644
index 000000000..ba1d8d324
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/parserer/SpikeDetectionStatus.cs
@@ -0,0 +1,11 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/utils/DiagnosticChecksum.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/utils/DiagnosticChecksum.cs
new file mode 100644
index 000000000..34abc6571
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/utils/DiagnosticChecksum.cs
@@ -0,0 +1,13 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/utils/DiagnosticHex.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/utils/DiagnosticHex.cs
new file mode 100644
index 000000000..fefd02486
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/diagnosticLed/utils/DiagnosticHex.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/hexLogger/HexFormatter.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/HexFormatter.cs
new file mode 100644
index 000000000..8a9121671
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/HexFormatter.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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");
+ }
+
+ ///
+ /// int to byte securely
+ ///
+ ///
+ ///
+ ///
+ public static byte ToHexByte(int value)
+ {
+ if (value < 0 || value > 255)
+ throw new ArgumentOutOfRangeException(nameof(value),
+ "Value must be between 0 and 255.");
+
+ return (byte)value;
+ }
+
+ ///
+ /// 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();
+ }
+
+
+ public static string ToHexWithAscii(byte value)
+ {
+ char c = (value >= 32 && value <= 126) ? (char)value : '.';
+ return $"0x{value:X2} ('{c}')";
+ }
+
+ public static string ToSerialHexWithAscii(byte[] data)
+ {
+ if (data == null || data.Length == 0)
+ return string.Empty;
+
+ var hex = new StringBuilder(data.Length * 3);
+ var ascii = new StringBuilder(data.Length);
+
+ foreach (byte b in data)
+ {
+ hex.Append(b.ToString("X2")).Append(' ');
+
+ // Printable ASCII range
+ if (b >= 32 && b <= 126)
+ {
+ ascii.Append((char)b);
+ }
+ // Binary numbers 0–9 -> show digit
+ else if (b <= 9)
+ {
+ ascii.Append((char)('0' + b));
+ }
+ else
+ {
+ ascii.Append('.');
+ }
+ }
+
+ // remove last trailing space in hex
+ if (hex.Length > 0)
+ hex.Length--;
+
+ return $"{hex} | {ascii}";
+ }
+
+
+
+ public static string ToHex(int value)
+ {
+ return $"0x{(byte)value:X2}";
+ }
+
+ public static byte[] IntToBytesBE(int value, int byteCount)
+ {
+ var result = new byte[byteCount];
+
+ for (int i = 0; i < byteCount; i++)
+ result[byteCount - 1 - i] = (byte)(value >> (8 * i));
+
+ return result;
+ }
+
+ public static byte[] IntToBytesLE(int value, int byteCount)
+ {
+ var result = new byte[byteCount];
+
+ for (int i = 0; i < byteCount; i++)
+ result[i] = (byte)(value >> (8 * i));
+
+ return result;
+ }
+
+ public static byte[] AsciiToBytes(string text)
+ {
+ return string.IsNullOrEmpty(text)
+ ? Array.Empty()
+ : System.Text.Encoding.ASCII.GetBytes(text);
+ }
+
+ ///
+ /// Converts a hex string to a byte array.
+ /// Like: string hex = "3F 76 65 72 73 3A 20 48 61 72 72 79 20 54 3A 42 38 30 30 2C 20";
+ ///
+ ///
+ ///
+ ///
+ public static byte[] HexStringToByteArray(string hex)
+ {
+ if (hex == null)
+ throw new ArgumentNullException(nameof(hex));
+
+ return hex
+ .Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(b => byte.Parse(b, NumberStyles.HexNumber, CultureInfo.InvariantCulture))
+ .ToArray();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/IpelHatCommandDecoder.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/IpelHatCommandDecoder.cs
new file mode 100644
index 000000000..2a7545fb0
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/IpelHatCommandDecoder.cs
@@ -0,0 +1,21 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.hexLogger
+{
+ public class IpelHatCommandDecoder
+ {
+ public static string DescribeCommand(byte command)
+ {
+ return "";
+ }
+
+ public static string DescribeDirection(byte direction)
+ {
+ if (direction == IperlHatProtocol.Constants.Write)
+ return "(WRITE - OUTGOING)";
+
+ if (direction == IperlHatProtocol.Constants.Read)
+ return "(READ - INCOMING)";
+
+ return "INVALID CONTROL BITS (unsupported pattern)";
+ }
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/IperlHatLogger.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/IperlHatLogger.cs
new file mode 100644
index 000000000..bfc1a7a99
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/IperlHatLogger.cs
@@ -0,0 +1,85 @@
+using System;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.wiredProtocol;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.hexLogger
+{
+ public static class IperlHatLogger
+ {
+ public static string DescribeTx(byte[] frame)
+ {
+ if (frame == null || frame.Length < 5)
+ return "Invalid frame";
+
+ if (frame[2] == IperlHatProtocol.Constants.Question)
+ {
+ return
+ "TX Frame\n" +
+ $" START : {HexFormatter.ToHex(frame[0])}\n" +
+ $" DIRECTION : {HexFormatter.ToHex(frame[1])} ({IpelHatCommandDecoder.DescribeDirection(frame[1])})\n" +
+ $" COMMAND : {HexFormatter.ToHexWithAscii(frame[2])}\n" +
+ $" INFO : {HexFormatter.ToSerialHexWithAscii(GetInformatioQuestion(frame))}\n" +
+ $" END : {HexFormatter.ToHex(frame[frame.Length - 1])}\n" +
+ $" RAW : {HexFormatter.ToHex(frame)}";
+ }
+ else
+ {
+ return
+ "TX Frame\n" +
+ $" START : {HexFormatter.ToHex(frame[0])}\n" +
+ $" DIRECTION : {HexFormatter.ToHex(frame[1])} ({HexFormatter.ToHexWithAscii(frame[1])}) {IpelHatCommandDecoder.DescribeDirection(frame[1])}\n" +
+ $" LEN : {HexFormatter.ToHex(frame[2])} - {(int)frame[2]}\n" +
+ $" COMMAND : {HexFormatter.ToHexWithAscii(frame[3])}\n" +
+ $" INFO : {HexFormatter.ToSerialHexWithAscii(GetInformation(frame))}\n" +
+ $" END : {HexFormatter.ToHex(frame[frame.Length - 1])}\n" +
+ $" RAW : {HexFormatter.ToHex(frame)}";
+ }
+ }
+
+ //payload
+ private static byte[] GetInformation(byte[] frame)
+ {
+ int infoLength = frame.Length - 5; // START + DIRECTION + LEN + COMMAND + END
+ if (infoLength <= 0)
+ return Array.Empty();
+
+ var info = new byte[infoLength];
+ Buffer.BlockCopy(frame, 4, info, 0, infoLength);
+ return info;
+ }
+
+ //payload for question
+ private static byte[] GetInformatioQuestion(byte[] frame)
+ {
+ int infoLength = frame.Length - 4; // START + DIRECTION + COMMAND + END
+ 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/TestMethods/iPerlCommunication/communication/C4/hexLogger/TouchReadControlDecoder.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/TouchReadControlDecoder.cs
new file mode 100644
index 000000000..2d2a3d0dd
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/TouchReadControlDecoder.cs
@@ -0,0 +1,16 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/hexLogger/TouchReadLogger.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/TouchReadLogger.cs
new file mode 100644
index 000000000..372adfe55
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/hexLogger/TouchReadLogger.cs
@@ -0,0 +1,57 @@
+using System;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.wiredProtocol;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/led/ITouchReadLedParser.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/ITouchReadLedParser.cs
new file mode 100644
index 000000000..b3cac3b6e
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/ITouchReadLedParser.cs
@@ -0,0 +1,7 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.led
+{
+ public interface ITouchReadLedParser
+ {
+ TouchReadLedData Parse(TouchReadLedMessage message);
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/ShortVariableLedParser.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/ShortVariableLedParser.cs
new file mode 100644
index 000000000..7947029cc
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/ShortVariableLedParser.cs
@@ -0,0 +1,17 @@
+using System.Globalization;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/led/TouchReadLedData.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/TouchReadLedData.cs
new file mode 100644
index 000000000..427ccf1a1
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/TouchReadLedData.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Globalization;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/TestMethods/iPerlCommunication/communication/C4/led/TouchReadLedMessage.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/TouchReadLedMessage.cs
new file mode 100644
index 000000000..abb0ef647
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/led/TouchReadLedMessage.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.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/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolCommand.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolCommand.cs
new file mode 100644
index 000000000..5b6758817
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolCommand.cs
@@ -0,0 +1,140 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons
+{
+ ///
+ /// Common iPERL TouchRead bidirectional commands.
+ /// These commands consist of a single-byte command code
+ /// placed in the Information field.
+ ///
+ public enum ProtocolCommand : 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,
+
+ ///
+ /// Question - specific switch to add additional payload request like "vers"
+ /// Mandatory add payload
+ ///
+ Question = 0x3F,
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolDeviceSubCommand.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolDeviceSubCommand.cs
new file mode 100644
index 000000000..3e83e13b0
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolDeviceSubCommand.cs
@@ -0,0 +1,203 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons
+{
+ ///
+ /// Device-specific TouchRead sub-commands.
+ /// These sub-commands are used together with the
+ /// (0xFD) command.
+ ///
+ public enum ProtocolDeviceSubCommand : byte
+ {
+ // ==========================================================
+ // System / Time
+ // ==========================================================
+
+ ///
+ /// View system time.
+ /// Returns uint32 seconds since 2000-01-01 00:00:00.
+ ///
+ ViewSystemTime = 0x10,
+
+ ///
+ /// Set system time.
+ /// Payload: uint32 seconds since 2000-01-01.
+ /// If set to zero, the meter resets and erases data.
+ /// Protected by meter seal.
+ ///
+ SetSystemTime = 0x11,
+
+ // ==========================================================
+ // Alarm Mask / Alarm Configuration
+ // ==========================================================
+
+ /// View alarm mask (lower 16 bits).
+ ViewAlarmMask = 0x31,
+
+ /// Set alarm mask (lower 16 bits).
+ SetAlarmMask = 0x32,
+
+ /// View alarm persistence period (days).
+ ViewPersistence = 0x33,
+
+ /// Set alarm persistence period (days).
+ SetPersistence = 0x34,
+
+ /// View leak duration (hours).
+ ViewLeakDuration = 0x35,
+
+ /// Set leak duration (hours).
+ SetLeakDuration = 0x36,
+
+ /// View current alarm states.
+ ViewAlarms = 0x37,
+
+ /// Set alarm states (protected by meter seal).
+ SetAlarms = 0x38,
+
+ // ==========================================================
+ // Manufacture / Counters
+ // ==========================================================
+
+ /// View manufacture date.
+ ViewManufactureDate = 0x39,
+
+ /// Set manufacture date (protected by meter seal).
+ SetManufactureDate = 0x3A,
+
+ /// View seconds idle.
+ ViewSecondsIdle = 0x3B,
+
+ /// View seconds active.
+ ViewSecondsActive = 0x3D,
+
+ /// View seconds used.
+ ViewSecondsUsed = 0x3F,
+
+ // ==========================================================
+ // Snapshot / Datalog
+ // ==========================================================
+
+ /// View snapshot data.
+ ViewSnapshotData = 0x41,
+
+ /// View datalog duration.
+ ViewDatalogDuration = 0x43,
+
+ /// Set datalog duration.
+ SetDatalogDuration = 0x44,
+
+ /// Read datalog.
+ ReadDatalog = 0x45,
+
+ /// Clear datalog.
+ ClearDatalog = 0x46,
+
+ // ==========================================================
+ // History
+ // ==========================================================
+
+ /// View history mask.
+ ViewHistoryMask = 0x47,
+
+ /// Set history mask.
+ SetHistoryMask = 0x48,
+
+ /// Read history.
+ ReadHistory = 0x49,
+
+ /// Clear history.
+ ClearHistory = 0x4A,
+
+ // ==========================================================
+ // Diagnostics / Status
+ // ==========================================================
+
+ /// View diagnostics.
+ ViewDiagnostics = 0x4B,
+
+ /// Reset diagnostics.
+ ResetDiagnostics = 0x4C,
+
+ /// View status file.
+ ViewStatusFile = 0x4F,
+
+ /// Set status file (protected by meter seal).
+ SetStatusFile = 0x50,
+
+ // ==========================================================
+ // Calibration / Configuration
+ // ==========================================================
+
+ /// View calibration structure.
+ ViewCalibrationStructure = 0x51,
+
+ /// Set calibration structure (protected by meter seal).
+ SetCalibrationStructure = 0x52,
+
+ /// View calibration.
+ ViewCalibration = 0x53,
+
+ /// Set calibration (protected by meter seal).
+ SetCalibration = 0x54,
+
+ /// View reboot count.
+ ViewRebootCount = 0x55,
+
+ /// Set reboot count (protected by meter seal).
+ SetRebootCount = 0x56,
+
+ /// View temperature.
+ ViewTemperature = 0x57,
+
+ /// Set temperature (protected by meter seal).
+ SetTemperature = 0x58,
+
+ // ==========================================================
+ // Diagnostic LED / Hardware
+ // ==========================================================
+
+ ///
+ /// Set diagnostic LED state.
+ /// Enables or disables high-speed LED serial output.
+ ///
+ /// See
+ /// diagnostic LED output modes.
+ ///
+ ///
+ SetDiagnosticLEDState = 0x60,
+
+
+ // ==========================================================
+ // Build / Firmware Info
+ // ==========================================================
+
+ /// View iPERL build information.
+ ViewIPerlBuild = 0x65,
+
+ /// Set iPERL build (protected by meter seal).
+ SetIPerlBuild = 0x66,
+
+ // ==========================================================
+ // Bootloader (DANGEROUS – use with care)
+ // ==========================================================
+
+ /// Enter bootloader mode.
+ EnterBootloader = 0x81,
+
+ /// Read FLASH memory.
+ ReadFlash = 0x82,
+
+ /// Erase all FLASH memory.
+ EraseAll = 0x83,
+
+ /// Erase FLASH segment.
+ EraseSegment = 0x84,
+
+ /// Update firmware code.
+ UpdateCode = 0x85,
+
+ /// Exit bootloader mode.
+ ExitBootloader = 0x86
+ }
+}
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolStatuses.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolStatuses.cs
new file mode 100644
index 000000000..d6c4aa334
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/protocolCommons/ProtocolStatuses.cs
@@ -0,0 +1,10 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons
+{
+ public enum ProtocolStatuses : byte
+ {
+ Idle = 0x01,
+ Active = 0x02,
+ Inactive = 0x03,
+
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrame.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrame.cs
new file mode 100644
index 000000000..00c1afaf8
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrame.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.wiredProtocol
+{
+ 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/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrameBuilder.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrameBuilder.cs
new file mode 100644
index 000000000..ed3827970
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrameBuilder.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.wiredProtocol
+{
+ 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(ProtocolCommand command)
+ {
+ _information.Add((byte)command);
+ return this;
+ }
+
+ public TouchReadFrameBuilder AddSubCommand(ProtocolDeviceSubCommand subCommand)
+ {
+ if (_information.Count == 0 ||
+ _information[0] != (byte)ProtocolCommand.DeviceSpecific)
+ throw new InvalidOperationException(
+ "Sub-command is only valid for DeviceSpecific (0xFD) commands.");
+
+ _information.Add((byte)subCommand);
+ return this;
+ }
+
+ public TouchReadFrameBuilder AddDeviceCommand(
+ ProtocolDeviceSubCommand subCommand)
+ {
+ _information.Add((byte)ProtocolCommand.DeviceSpecific);
+ _information.Add((byte)subCommand);
+ return this;
+ }
+
+ public TouchReadFrameBuilder AddPayload(byte[] payload)
+ {
+ if (payload != null)
+ _information.AddRange(payload);
+
+ return this;
+ }
+
+ public TouchReadFrameBuilder AddDiagnosticLedState(DiagnosticLedState state)
+ {
+ _information.Add((byte)ProtocolCommand.DeviceSpecific);
+ _information.Add((byte)ProtocolDeviceSubCommand.SetDiagnosticLEDState);
+ _information.Add((byte)state);
+ 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();
+ }
+
+ public 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/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrameParser.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrameParser.cs
new file mode 100644
index 000000000..e11ec1d6d
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadFrameParser.cs
@@ -0,0 +1,63 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.wiredProtocol
+{
+ 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/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadProtocol.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadProtocol.cs
new file mode 100644
index 000000000..2c44f4a09
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadProtocol.cs
@@ -0,0 +1,10 @@
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.wiredProtocol
+{
+ 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/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadResponse.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadResponse.cs
new file mode 100644
index 000000000..20e12f26a
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/C4/wiredProtocol/TouchReadResponse.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.C4.wiredProtocol
+{
+ 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/TestMethods/iPerlCommunication/communication/OpthoHeadService.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/OpthoHeadService.cs
new file mode 100644
index 000000000..55859155f
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/OpthoHeadService.cs
@@ -0,0 +1,10 @@
+using log4net;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication
+{
+ public class OpthoHeadService
+ {
+ protected static readonly ILog rfidDataLogger = LogManager.GetLogger("RfidData");
+
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/OptoHeadTest.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/OptoHeadTest.cs
new file mode 100644
index 000000000..c44b51354
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/OptoHeadTest.cs
@@ -0,0 +1,123 @@
+using System;
+using System.IO.Ports;
+using Common;
+using log4net;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.Utils;
+using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication
+{
+ public class OptoHeadTest : IDisposable
+ {
+ protected static readonly ILog rfidDataLogger = LogManager.GetLogger("RfidData");
+
+ private static SerialDriver serialDriver;
+
+ public static SerialDriver BuildConnection(IperlHead iHead)
+ {
+ return new SerialDriverBuilder()
+ .WithPort($"COM{iHead.RfidComPortNr}")
+ .WithBaudRate(2400)
+ .WithDataBits(8)
+ .WithParity(Parity.None)
+ .WithStopBits(StopBits.One)
+ .WithTimeouts(4000, 2000)
+ .BuildAndConnect();
+
+ }
+
+ public void CloseConnection()
+ {
+ if (serialDriver != null)
+ serialDriver.CloseConnection();
+ }
+
+
+ public static string ReadRequest_PCB(IperlHead iHead)
+ {
+ if (iHead.DebugLevel == DebugMode.Simulate)
+ {
+ return "-OK Simulated response-";
+ }
+
+ try
+ {
+ if (iHead != null)
+ {
+ if (serialDriver == null)
+ serialDriver = BuildConnection(iHead);
+
+ RadioService headService = new RadioService(serialDriver);
+ string serialNo = headService.ReadRequest_PCB(iHead);
+ return serialNo;
+ }
+ }
+ catch (Exception ex)
+ {
+ return (ex.Message.ToString());
+ }
+
+ return "";
+ }
+
+ public static string SetTestMode(IperlHead iHead)
+ {
+ if (iHead.DebugLevel == DebugMode.Simulate)
+ {
+ return "-OK Simulated response-";
+ }
+
+
+ try
+ {
+ if (iHead != null)
+ {
+ if (serialDriver == null)
+ serialDriver = BuildConnection(iHead);
+
+ RadioService headService = new RadioService(serialDriver);
+ string answer = headService.SetTestMode(iHead);
+ return answer;
+ }
+ }
+ catch (Exception ex)
+ {
+ return (ex.Message.ToString());
+ }
+
+ return "";
+ }
+
+ public static string SetActiveMode(IperlHead iHead)
+ {
+ if (iHead.DebugLevel == DebugMode.Simulate)
+ {
+ return "-OK Simulated response-";
+ }
+
+ try
+ {
+ if (iHead != null)
+ {
+ if (serialDriver == null)
+ serialDriver = BuildConnection(iHead);
+
+ RadioService headService = new RadioService(serialDriver);
+ string answer = headService.SetActiveMode(iHead);
+ return answer;
+ }
+ }
+ catch (Exception ex)
+ {
+ return (ex.Message.ToString());
+ }
+
+ return "";
+ }
+
+ public void Dispose()
+ {
+ CloseConnection();
+ }
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/RadioService.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/RadioService.cs
new file mode 100644
index 000000000..219ebb6ce
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/RadioService.cs
@@ -0,0 +1,117 @@
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons;
+using TBF.Rig.TestMethods.iPerlCommunication.communication.Utils;
+using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication
+{
+ public class RadioService
+ {
+
+ static string okResponse = "Command complete, no errors";
+ static string errorResponse = "Unable to execute";
+
+ private SerialDriver serialDriver;
+ public RadioService(SerialDriver serialDriver)
+ {
+ this.serialDriver = serialDriver;
+ }
+
+ public string ReadRequest_PCB(IperlHead iHead)
+ {
+ if (!serialDriver.IsOpen())
+ {
+ serialDriver.Open();
+ }
+
+ var request = new IperlHatFrameBuilder()
+ .RequestResponse(true)
+ .AddCommand(ProtocolCommand.ViewFactoryId)
+ .BuildBytes();
+
+
+ byte[] rawData = serialDriver.SendAndWait(request, 10000);
+ if (rawData == null)
+ return null;
+
+ // parse rawData
+ var parser = new IperlHatFrameParser();
+ IperlHatResponse decoded = parser.Parse(rawData);
+ if (decoded.IsOk)
+ {
+ return decoded.GetAsciiPayload();
+ }
+
+ return null;
+ }
+
+ public string SetTestMode(IperlHead iHead)
+ {
+ if (!serialDriver.IsOpen())
+ {
+ serialDriver.Open();
+ }
+
+ //Set LED to state 4
+ byte[] request = new IperlHatFrameBuilder()
+ .AddDiagnosticLedState(DiagnosticLedState.State4)
+ .BuildBytes();
+
+ byte[] rawData = serialDriver.SendAndWait(request, 1000);
+ if (rawData == null)
+ return null;
+
+
+ // parse rawData
+ var parser = new IperlHatFrameParser();
+ IperlHatResponse decoded = parser.Parse(rawData);
+ if (decoded.IsOk)
+ {
+
+
+ //correct or incorrect response
+ //okResponse, errorResponse
+
+ return "Set Test Mode - OK";
+ }
+
+ return "Set Test Mode - FAILED";
+
+ }
+
+ ///
+ /// stop data streaming by LED
+ ///
+ ///
+ ///
+ public string SetActiveMode(IperlHead iHead)
+ {
+ if (!serialDriver.IsOpen())
+ {
+ serialDriver.Open();
+ }
+
+ //Set LED to state 1
+ byte[] request = new IperlHatFrameBuilder()
+ .AddDiagnosticLedState(DiagnosticLedState.StateOFF)
+ .BuildBytes();
+
+ byte[] rawData = serialDriver.SendAndWait(request, 1000);
+ if (rawData == null)
+ return null;
+
+
+ // parse rawData
+ var parser = new IperlHatFrameParser();
+ IperlHatResponse decoded = parser.Parse(rawData);
+ if (decoded.IsOk)
+ {
+
+ return "Set Active Mode - OK";
+ }
+
+ return "Set Active Mode - FAILED";
+ }
+ }
+}
\ No newline at end of file
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/Utils/SerialDriver.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/Utils/SerialDriver.cs
new file mode 100644
index 000000000..cd0e5bb1d
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/Utils/SerialDriver.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO.Ports;
+using System.Threading;
+using FluentNHibernate.Conventions;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.Utils
+{
+ public class SerialDriver : IDisposable
+ {
+ public string ErrorMessage { get; private set; }
+ private List SerialPortReadBuffer = new List();
+
+ private SerialPort _serialPort;
+ private readonly List _binMessages = new List();
+ private bool _isReading;
+
+ // Stored configuration (used by Builder)
+ private readonly string _portName;
+ private readonly int _baudRate;
+ private readonly int _dataBits;
+ private readonly Parity _parity;
+ private readonly StopBits _stopBits;
+ private readonly int _readTimeout;
+ private readonly int _writeTimeout;
+
+ private readonly ManualResetEvent _responseReceived = new ManualResetEvent(false);
+
+ #region Constructors
+
+ // Default constructor (legacy support)
+ public SerialDriver()
+ {
+ _serialPort = new SerialPort();
+ }
+
+ // Builder constructor
+ internal SerialDriver(
+ string portName,
+ int baudRate,
+ int dataBits,
+ Parity parity,
+ StopBits stopBits,
+ int readTimeout,
+ int writeTimeout)
+ {
+ _portName = portName;
+ _baudRate = baudRate;
+ _dataBits = dataBits;
+ _parity = parity;
+ _stopBits = stopBits;
+ _readTimeout = readTimeout;
+ _writeTimeout = writeTimeout;
+ }
+
+ #endregion
+
+ #region Open / Close
+
+ // Builder-based open
+ public bool Open()
+ {
+ return OpenConnection(
+ _portName,
+ _baudRate,
+ _dataBits,
+ _parity,
+ _stopBits,
+ _readTimeout,
+ _writeTimeout
+ );
+ }
+
+ // Legacy API (unchanged)
+ public bool OpenConnection(
+ string comPort,
+ int baudrate,
+ int dataBits,
+ Parity parity,
+ StopBits stopbits,
+ int readTimeout = 1000,
+ int writeTimeout = 1000)
+ {
+ lock (this)
+ {
+ CloseConnection();
+
+ try
+ {
+ ErrorMessage = string.Empty;
+
+ _serialPort = new SerialPort(comPort, baudrate, parity, dataBits, stopbits)
+ {
+ ReadTimeout = readTimeout,
+ WriteTimeout = writeTimeout
+ };
+
+ _serialPort.DataReceived += DataReceivedHandler;
+ _serialPort.Open();
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = $"COM error: Open failed {comPort}. {ex.Message}";
+ return false;
+ }
+
+ if (!_serialPort.IsOpen)
+ {
+ ErrorMessage = $"COM error: Can't open {comPort}.";
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void CloseConnection()
+ {
+ if (_serialPort != null)
+ {
+ _serialPort.DataReceived -= DataReceivedHandler;
+ if (_serialPort.IsOpen)
+ _serialPort.Close();
+
+ _serialPort.Dispose();
+ _serialPort = null;
+ }
+ }
+
+ public bool IsOpen() => _serialPort?.IsOpen == true;
+
+ #endregion
+
+ #region Send / Receive
+
+ public bool SendMessage(byte[] sendDataBytes, int length, int readTimeout = 1000, int writeTimeout = 1000)
+ {
+ if (!IsOpen()) return false;
+ if (sendDataBytes.Length == 0) return true;
+
+ try
+ {
+ PrepareReading();
+
+ _serialPort.WriteTimeout = writeTimeout;
+ _serialPort.ReadTimeout = readTimeout;
+ _serialPort.Write(sendDataBytes, 0, length);
+
+ _isReading = true;
+
+ var stopwatch = Stopwatch.StartNew();
+ while (_isReading)
+ {
+ if (stopwatch.ElapsedMilliseconds > readTimeout)
+ {
+ ErrorMessage = "COM error: Receive timeout";
+ return false;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage = $"COM error: Transmit failed {_serialPort.PortName}. {ex.Message}";
+ return false;
+ }
+
+ return true;
+ }
+
+ private void PrepareReading()
+ {
+ _serialPort.DiscardInBuffer();
+ _binMessages.Clear();
+ _responseReceived.Reset();
+ _isReading = true;
+ }
+
+ public byte[] GetRawData()
+ {
+ return _binMessages.ToArray();
+ }
+
+ private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
+ {
+ lock (this)
+ {
+ if (_serialPort == null || !_serialPort.IsOpen) return;
+
+ try
+ {
+ Thread.Sleep(5);
+
+ if (!SerialPortReadBuffer.IsEmpty())
+ {
+ SerialPortReadBuffer.Clear();
+ }
+
+ int iWordCounter = 0;
+ bool isStart = false;
+ bool isQuestion = false;
+ int iLength = 0;
+ while (true)//_serialPort.BytesToRead > 0
+ {
+ byte readByte = (byte)_serialPort.ReadByte();
+
+ //I have START
+ if (readByte == TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Start)
+ {
+ iWordCounter++;
+ isStart = true;
+ }
+ // I have QUESTION
+ if (readByte == TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.Question)
+ {
+ iWordCounter++;
+ isQuestion = true;
+ }
+ //I count length from start
+ if (iWordCounter > 0)
+ iWordCounter++;
+
+ if (iWordCounter > 0)
+ {
+ //Store byte to data
+ SerialPortReadBuffer.Add(readByte);
+ }
+ // we have length
+ if (iLength == 0 && isStart && SerialPortReadBuffer.Count > 2 )
+ {
+ iLength = (int)SerialPortReadBuffer[2];
+ }
+
+ //If we have enough bytes
+ if (isStart && iLength > 0 && SerialPortReadBuffer.Count >= iLength)
+ {
+ break;
+ }
+ //if we read END
+ if (isQuestion && readByte == TestMethods.iPerlCommunication.communication.C4.IperlHatProtocol.Constants.End)
+ {
+ break;
+ }
+ }
+
+ if (SerialPortReadBuffer.Count > 0)
+ {
+ _binMessages.AddRange(SerialPortReadBuffer.ToArray());
+ _responseReceived.Set();
+ }
+ }
+ catch (TimeoutException te)
+ {
+ // Ignore shutdown race conditions
+ }
+ finally
+ {
+ _isReading = false;
+ }
+ }
+ }
+
+ public byte[] SendAndWait(byte[] data, int timeoutMs)
+ {
+ if (!IsOpen())
+ throw new InvalidOperationException("Serial port not open");
+
+ PrepareReading();
+ _serialPort.Write(data, 0, data.Length);
+
+ if (!_responseReceived.WaitOne(timeoutMs))
+ {
+ ErrorMessage = "COM error: response timeout";
+ return null;
+ }
+
+ return GetRawData();
+ }
+
+
+ #endregion
+
+ public void Dispose()
+ {
+ CloseConnection();
+ }
+ }
+}
diff --git a/TBF/Rig/TestMethods/iPerlCommunication/communication/Utils/SerialDriverBuilder.cs b/TBF/Rig/TestMethods/iPerlCommunication/communication/Utils/SerialDriverBuilder.cs
new file mode 100644
index 000000000..3e9c9aa00
--- /dev/null
+++ b/TBF/Rig/TestMethods/iPerlCommunication/communication/Utils/SerialDriverBuilder.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO.Ports;
+
+namespace TBF.Rig.TestMethods.iPerlCommunication.communication.Utils
+{
+ public class SerialDriverBuilder
+ {
+ private string _portName;
+ private int _baudRate = 9600;
+ private int _dataBits = 8;
+ private Parity _parity = Parity.None;
+ private StopBits _stopBits = StopBits.One;
+ private int _readTimeout = 1000;
+ private int _writeTimeout = 1000;
+
+ public SerialDriverBuilder WithPort(string portName)
+ {
+ _portName = portName;
+ return this;
+ }
+
+ public SerialDriverBuilder WithBaudRate(int baudRate)
+ {
+ _baudRate = baudRate;
+ return this;
+ }
+
+ public SerialDriverBuilder WithDataBits(int dataBits)
+ {
+ _dataBits = dataBits;
+ return this;
+ }
+
+ public SerialDriverBuilder WithParity(Parity parity)
+ {
+ _parity = parity;
+ return this;
+ }
+
+ public SerialDriverBuilder WithStopBits(StopBits stopBits)
+ {
+ _stopBits = stopBits;
+ return this;
+ }
+
+ public SerialDriverBuilder WithTimeouts(int readTimeout, int writeTimeout)
+ {
+ _readTimeout = readTimeout;
+ _writeTimeout = writeTimeout;
+ return this;
+ }
+
+ ///
+ /// Build driver WITHOUT opening connection
+ ///
+ public SerialDriver Build()
+ {
+ return new SerialDriver(
+ _portName,
+ _baudRate,
+ _dataBits,
+ _parity,
+ _stopBits,
+ _readTimeout,
+ _writeTimeout
+ );
+ }
+
+ ///
+ /// Build driver AND open connection
+ ///
+ public SerialDriver BuildAndConnect()
+ {
+ var driver = Build();
+ if (!driver.Open())
+ {
+ throw new InvalidOperationException(driver.ErrorMessage);
+ }
+ return driver;
+ }
+ }
+}
\ No newline at end of file
diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj
index 8296097c5..05496974d 100644
--- a/TBF/TBF.csproj
+++ b/TBF/TBF.csproj
@@ -1444,6 +1444,50 @@
Component
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Form