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