Add DiagnosticLedState classes for States #4-#7, enums for PipeStatus and SpikeDetectionStatus, and implement TouchReadProtocol commands, frames, and builders.

This commit is contained in:
Michal Buzik 2026-01-19 08:10:36 +01:00
parent 988a8c6642
commit 2f1ada3f5b
36 changed files with 2267 additions and 0 deletions

View File

@ -0,0 +1,134 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
/// <summary>
/// Common iPERL TouchRead bidirectional commands.
/// These commands consist of a single-byte command code
/// placed in the Information field.
/// </summary>
public enum TouchReadCommand : byte
{
/// <summary>
/// Simple (legacy) commands (e.g. View Factory ID = 0x01)
/// </summary>
Simple = 0x00,
/// <summary>
/// View Factory ID (ex-works serial number).
/// Returns a 012 byte ASCII string terminated by NULL.
/// Response only if RF flag is set.
/// </summary>
ViewFactoryId = 0x01,
/// <summary>
/// Set Factory ID (012 ASCII characters, NULL terminated).
/// Protected by meter seal.
/// </summary>
SetFactoryId = 0x02,
/// <summary>
/// View Customer Programmable ID (112 ASCII characters).
/// </summary>
ViewProgrammableId = 0x03,
/// <summary>
/// Set Customer Programmable ID (112 ASCII characters, NULL terminated).
/// </summary>
SetProgrammableId = 0x04,
/// <summary>
/// View Version and Type string.
/// Example: B1.22,SMW002,B0.02
/// </summary>
ViewVersionAndType = 0x05,
/// <summary>
/// View Customer Programmable Text (020 ASCII characters).
/// </summary>
ViewProgrammableText = 0x07,
/// <summary>
/// Set Customer Programmable Text (020 ASCII characters, NULL terminated).
/// </summary>
SetProgrammableText = 0x08,
/// <summary>
/// View number of reading digits and decimal shift.
/// Payload: uint8 digits, int8 decimal shift.
/// </summary>
ViewNumberOfReadingDigits = 0x09,
/// <summary>
/// Set number of reading digits and decimal shift.
/// Digits range: 48, Decimal shift: -5..0.
/// </summary>
SetNumberOfReadingDigits = 0x0A,
/// <summary>
/// View reading units.
/// Returns numeric unit code (m3, ft3, gallons).
/// </summary>
ViewReadingUnits = 0x0B,
/// <summary>
/// Set reading units.
/// Valid values: 0x00=m3, 0x01=ft3, 0x04=US gallons, 0xFF=off.
/// </summary>
SetReadingUnits = 0x0C,
/// <summary>
/// View reading multiplier (resolution).
/// Range: -7..+5 or 0x80 (disabled).
/// </summary>
ViewReadingMultiplier = 0x0F,
/// <summary>
/// Set reading multiplier (resolution).
/// </summary>
SetReadingMultiplier = 0x10,
/// <summary>
/// View preset total (volume accumulator).
/// Returns 8 ASCII digits + NULL.
/// </summary>
ViewPresetTotal = 0x13,
/// <summary>
/// Set preset total (08 ASCII digits, NULL terminated).
/// Protected by meter seal.
/// </summary>
SetPresetTotal = 0x14,
/// <summary>
/// View reading mode (unidirectional TouchRead format).
/// </summary>
ViewReadingMode = 0x15,
/// <summary>
/// Set reading mode.
/// Values: Short Variable, Extended, Fixed, Smart Meter.
/// </summary>
SetReadingMode = 0x16,
/// <summary>
/// View build information (firmware details).
/// </summary>
ViewBuildInformation = 0x17,
/// <summary>
/// View meter state.
/// </summary>
ViewState = 0x19,
/// <summary>
/// Set meter state (operating mode).
/// Protected by meter seal.
/// </summary>
SetState = 0x1A,
/// <summary>
/// Device-specific command prefix.
/// Must be followed by a device sub-command byte.
/// </summary>
DeviceSpecific = 0xFD
}
}

View File

@ -0,0 +1,105 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
/// <summary>
/// Device-specific TouchRead sub-commands.
/// These commands are used with the DeviceSpecific (0xFD) command.
/// </summary>
public enum TouchReadDeviceSubCommand : byte
{
// ---- Alarm / Mask -----------------------------------------
/// <summary>
/// View Alarm Mask (lower 16 bits).
/// Returns a 2-byte little-endian alarm bit field.
/// </summary>
ViewAlarmMask = 0x31,
/// <summary>
/// Set Alarm Mask (lower 16 bits).
/// Payload: 2-byte little-endian alarm bit field.
/// </summary>
SetAlarmMask = 0x32,
/// <summary>
/// View alarm persistence period (days).
/// Range: 890 days.
/// </summary>
ViewPersistence = 0x33,
/// <summary>
/// Set alarm persistence period (days).
/// Range: 890 days.
/// </summary>
SetPersistence = 0x34,
/// <summary>
/// View leak duration (hours).
/// Range: 24180 hours.
/// </summary>
ViewLeakDuration = 0x35,
/// <summary>
/// Set leak duration (hours).
/// Range: 24180 hours.
/// </summary>
SetLeakDuration = 0x36,
ViewAlarms = 0x37,
SetAlarms = 0x38,
/// <summary>
/// View system time.
/// Returns uint32 seconds since 2000-01-01.
/// </summary>
ViewSystemTime = 0x10,
/// <summary>
/// Set system time.
/// Payload: uint32 seconds since 2000-01-01.
/// If zero, device will reset and erase data.
/// Protected by meter seal.
/// </summary>
SetSystemTime = 0x11,
// ---- Manufacture / Time -----------------------------------
ViewManufactureDate = 0x39,
SetManufactureDate = 0x3A,
ViewSecondsIdle = 0x3B,
ViewSecondsActive = 0x3D,
ViewSecondsUsed = 0x3F,
// ---- Snapshot / Logging -----------------------------------
ViewSnapshotData = 0x41,
ViewDatalogDuration = 0x43,
SetDatalogDuration = 0x44,
ReadDatalog = 0x45,
ClearDatalog = 0x46,
// ---- History ----------------------------------------------
ViewHistoryMask = 0x47,
SetHistoryMask = 0x48,
ReadHistory = 0x49,
ClearHistory = 0x4A,
// ---- Diagnostics ------------------------------------------
ViewDiagnostics = 0x4B,
ResetDiagnostics = 0x4C,
// ---- Calibration / Build ----------------------------------
ViewCalibration = 0x53,
SetCalibration = 0x54,
ViewIPerlBuild = 0x65,
SetIPerlBuild = 0x66,
// ---- Bootloader (dangerous!) ------------------------------
EnterBootloader = 0x81,
ReadFlash = 0x82,
EraseAll = 0x83,
EraseSegment = 0x84,
UpdateCode = 0x85,
ExitBootloader = 0x86
}
}

View File

@ -0,0 +1,27 @@
using System;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
public sealed class TouchReadFrame
{
public byte Start { get; }
public byte Length { get; }
public byte Control { get; }
public byte[] Information { get; }
public ushort Checksum { get; }
public TouchReadFrame(
byte start,
byte length,
byte control,
byte[] information,
ushort checksum)
{
Start = start;
Length = length;
Control = control;
Information = information ?? Array.Empty<byte>();
Checksum = checksum;
}
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
public sealed class TouchReadFrameBuilder
{
private const byte START = 0x0D;
private byte _control;
private readonly List<byte> _information = new List<byte>();
public TouchReadFrameBuilder RequestResponse(bool enabled)
{
_control = enabled ? (byte)0x08 : (byte)0x00;
return this;
}
public TouchReadFrameBuilder AddCommand(TouchReadCommand command)
{
_information.Add((byte)command);
return this;
}
public TouchReadFrameBuilder AddSubCommand(TouchReadDeviceSubCommand subCommand)
{
if (_information.Count == 0 ||
_information[0] != (byte)TouchReadCommand.DeviceSpecific)
throw new InvalidOperationException(
"Sub-command is only valid for DeviceSpecific (0xFD) commands.");
_information.Add((byte)subCommand);
return this;
}
public TouchReadFrameBuilder AddDeviceCommand(
TouchReadDeviceSubCommand subCommand)
{
_information.Add((byte)TouchReadCommand.DeviceSpecific);
_information.Add((byte)subCommand);
return this;
}
public TouchReadFrameBuilder AddPayload(byte[] payload)
{
if (payload != null)
_information.AddRange(payload);
return this;
}
public TouchReadFrameBuilder AddNullTerminatedAscii(string text)
{
if (!string.IsNullOrEmpty(text))
_information.AddRange(
System.Text.Encoding.ASCII.GetBytes(text));
_information.Add(0x00);
return this;
}
public TouchReadFrame BuildFrame()
{
if (_information.Count == 0)
throw new InvalidOperationException("No command specified.");
byte length = (byte)(1 + _information.Count + 2);
var raw = new List<byte>
{
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<byte>
{
frame.Start,
frame.Length,
frame.Control
};
bytes.AddRange(frame.Information);
bytes.Add((byte)(frame.Checksum >> 8));
bytes.Add((byte)(frame.Checksum & 0xFF));
return bytes.ToArray();
}
private static ushort CalculateChecksum(IEnumerable<byte> data)
{
ushort sum = 0;
foreach (var b in data)
sum += b;
return sum;
}
}
}

View File

@ -0,0 +1,63 @@
using System;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
public sealed class TouchReadFrameParser
{
private const byte START = 0x0D;
public TouchReadResponse Parse(byte[] data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
if (data.Length < 6)
throw new FormatException("Frame too short.");
if (data[0] != START)
throw new FormatException("Invalid START byte.");
byte length = data[1];
if (length + 2 != data.Length)
throw new FormatException("Length mismatch.");
ushort receivedChecksum =
(ushort)((data[data.Length - 2] << 8) |
data[data.Length - 1]);
ushort calculatedChecksum = CalculateChecksum(data, data.Length - 2);
if (receivedChecksum != calculatedChecksum)
throw new FormatException("Checksum error.");
byte control = data[2];
byte status = data[3];
byte[] payload = ExtractPayload(data);
return new TouchReadResponse(control, status, payload);
}
private static ushort CalculateChecksum(byte[] data, int count)
{
ushort sum = 0;
for (int i = 0; i < count; i++)
sum += data[i];
return sum;
}
private static byte[] ExtractPayload(byte[] data)
{
// payload exists only if frame longer than:
// START + LEN + CTRL + STATUS + CHK_HI + CHK_LO = 6 bytes
if (data.Length <= 6)
return Array.Empty<byte>();
int payloadLength = data.Length - 6;
byte[] payload = new byte[payloadLength];
Buffer.BlockCopy(data, 4, payload, 0, payloadLength);
return payload;
}
}
}

View File

@ -0,0 +1,10 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
public static class TouchReadProtocol
{
public const byte START = 0x0D;
// Control bits (CNTRL1)
public const byte RESPONSE_FLAG = 0x08; // RF
}
}

View File

@ -0,0 +1,34 @@
using System;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
public sealed class TouchReadResponse
{
public byte Control { get; }
public byte Status { get; }
public byte[] Payload { get; }
public bool IsOk => Status == 0x01;
public TouchReadResponse(byte control, byte status, byte[] payload)
{
Control = control;
Status = status;
Payload = payload ?? Array.Empty<byte>();
}
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);
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed
{
public sealed class DiagnosticLedParser
{
private readonly DiagnosticLedState _state;
public DiagnosticLedParser(DiagnosticLedState state)
{
_state = state;
}
public DiagnosticLedData ParseLine(string line)
{
if (string.IsNullOrEmpty(line))
throw new ArgumentNullException(nameof(line));
if (!line.EndsWith("\r\n"))
throw new FormatException("Invalid diagnostic LED line termination");
string trimmed = line.TrimEnd('\r', '\n');
string[] parts = trimmed.Split('\t');
if (parts.Length < 2)
throw new FormatException("Too few diagnostic LED fields");
// ---- Checksum ----
string checksumHex = parts[parts.Length - 1];
int lastTab = trimmed.LastIndexOf('\t');
if (lastTab < 0)
throw new FormatException("Checksum separator not found");
string beforeChecksum = trimmed.Substring(0, lastTab + 1);
byte expected = DiagnosticChecksum.Compute(beforeChecksum);
byte actual = DiagnosticHex.ParseByte(checksumHex);
if (expected != actual)
throw new FormatException("Diagnostic LED checksum mismatch");
// ---- Dispatch ----
switch (_state)
{
case DiagnosticLedState.State1:
return new DiagnosticLedState1Data(line, parts);
case DiagnosticLedState.State2:
return new DiagnosticLedState2Data(line, parts);
case DiagnosticLedState.State3:
return new DiagnosticLedState3Data(line, parts);
case DiagnosticLedState.State4:
return new DiagnosticLedState4Data(line, parts);
case DiagnosticLedState.State5:
return new DiagnosticLedState5Data(line, parts);
case DiagnosticLedState.State6:
return new DiagnosticLedState6Data(line, parts);
case DiagnosticLedState.State7:
return new DiagnosticLedState7Data(line, parts);
default:
throw new NotSupportedException("Unknown diagnostic LED state");
}
}
}
}

View File

@ -0,0 +1,13 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed
{
public enum DiagnosticLedState : byte
{
State1 = 0x01,
State2 = 0x02,
State3 = 0x03,
State4 = 0x04,
State5 = 0x05,
State6 = 0x06,
State7 = 0x07
}
}

View File

@ -0,0 +1,86 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Base class for all Diagnostic LED data frames.
///
/// <para>
/// The iPERL meter emits diagnostic LED frames when the
/// Diagnostic LED is enabled using the
/// <c>Set Diagnostic LED State (0xFD 0x60)</c> command.
/// </para>
///
/// <para>
/// 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.
/// </para>
///
/// <list type="table">
/// <listheader>
/// <term>Pos</term>
/// <description>Common field description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>Signed 24-bit ADC value (twos complement)</description></item>
/// <item><term>1 aaaa</term><description>Unsigned 16-bit field strength (internal units)</description></item>
/// <item><term>2 yyyy</term><description>Signed 16-bit raw flow rate (¼ ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>Unsigned 24-bit raw volume accumulation (¼ ml per bit)</description></item>
/// <item><term>4 cccc</term><description>Unsigned 16-bit millivolt delta on the field drive capacitor</description></item>
/// </list>
///
/// <para>
/// Each derived state class parses additional fields starting at
/// position 5, according to the selected diagnostic LED state.
/// </para>
///
/// <para>
/// The raw ASCII line (including checksum and CRLF) is preserved
/// for logging, debugging, and offline analysis.
/// </para>
/// </summary>
public abstract class DiagnosticLedData
{
/// <summary>
/// Raw diagnostic LED line exactly as received from the meter,
/// including checksum and CRLF.
/// </summary>
public string RawLine { get; }
// ----- Common fields (present in all LED states) -----
/// <summary>
/// Signed 24-bit ADC value (twos complement).
/// </summary>
public int Adc24 { get; protected set; }
/// <summary>
/// Unsigned 16-bit field strength in internal (non-legacy) units.
/// </summary>
public ushort FieldStrength { get; protected set; }
/// <summary>
/// Signed 16-bit raw flow rate in units of ¼ milliliter per bit.
/// </summary>
public short RawFlow { get; protected set; }
/// <summary>
/// Unsigned 24-bit raw volume accumulation in units of ¼ milliliter per bit.
/// </summary>
public uint RawVolume { get; protected set; }
/// <summary>
/// Unsigned 16-bit millivolt delta measured on the field drive capacitor.
/// </summary>
public ushort CapacitorMv { get; protected set; }
/// <summary>
/// Initializes the base diagnostic LED data with the raw input line.
/// </summary>
/// <param name="raw">
/// Raw ASCII line received from the diagnostic LED output.
/// </param>
protected DiagnosticLedData(string raw)
{
RawLine = raw;
}
}
}

View File

@ -0,0 +1,40 @@
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Diagnostic LED State #1 data frame.
///
/// <para>
/// 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.
/// </para>
///
/// <list type="table">
/// <listheader>
/// <term>Pos</term>
/// <description>Field description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>Signed 24-bit ADC value (twos complement)</description></item>
/// <item><term>1 aaaa</term><description>Unsigned 16-bit field strength (internal units)</description></item>
/// <item><term>2 yyyy</term><description>Signed 16-bit raw flow rate (¼ ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>Unsigned 24-bit raw volume accumulation (¼ ml per bit)</description></item>
/// <item><term>4 cccc</term><description>Unsigned 16-bit millivolt delta on the field drive capacitor</description></item>
/// <item><term>5 ss</term><description>Unsigned 8-bit checksum (sum of all previous ASCII bytes
/// including the TAB before the checksum field)</description></item>
/// </list>
/// </summary>
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]);
}
}
}

View File

@ -0,0 +1,72 @@
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Diagnostic LED State #2 data frame.
///
/// <para>
/// 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.
/// </para>
///
/// <para>
/// 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.
/// </para>
///
/// <list type="table">
/// <listheader>
/// <term>Pos</term>
/// <description>Field description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>Signed 24-bit ADC value (twos complement)</description></item>
/// <item><term>1 aaaa</term><description>Unsigned 16-bit field strength (internal units)</description></item>
/// <item><term>2 yyyy</term><description>Signed 16-bit raw flow rate (¼ ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>Unsigned 24-bit raw volume accumulation (¼ ml per bit)</description></item>
/// <item><term>4 cccc</term><description>Unsigned 16-bit millivolt delta on the field drive capacitor</description></item>
/// <item><term>5 gggggggg</term><description>Unsigned 32-bit volume displayed on the LCD</description></item>
/// <item><term>6 mm</term><description>Unsigned 8-bit meter state (see Table 17-23 in protocol documentation)</description></item>
/// <item><term>7 ff</term><description>Unsigned 8-bit boolean flag indicating low-flow cutoff
/// state (0 = false, 1 = true)</description></item>
/// <item><term>8 ss</term><description>Unsigned 8-bit checksum (sum of all previous ASCII bytes including
/// the TAB before the checksum field)</description></item>
/// </list>
/// </summary>
public sealed class DiagnosticLedState2Data : DiagnosticLedData
{
/// <summary>
/// Volume displayed on LCD (raw units).
/// </summary>
public uint LcdVolume { get; }
/// <summary>
/// Meter state (see Table 17-23).
/// </summary>
public byte MeterState { get; }
/// <summary>
/// True if meter is in low-flow cutoff.
/// </summary>
public bool IsLowFlowCutoff { get; }
public DiagnosticLedState2Data(string rawLine, string[] fields)
: base(rawLine)
{
// ---- Common fields (04) ----
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;
}
}
}

View File

@ -0,0 +1,71 @@
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Diagnostic LED State #3 data frame.
///
/// <para>
/// State #3 extends the common diagnostic LED fields with calibration
/// and timing information related to the field drive and ASIC operation.
/// </para>
///
/// <para>
/// 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.
/// </para>
///
/// <list type="table">
/// <listheader>
/// <term>Pos</term>
/// <description>Field description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>Signed 24-bit ADC value (twos complement)</description></item>
/// <item><term>1 aaaa</term><description>Unsigned 16-bit field strength (internal units)</description></item>
/// <item><term>2 yyyy</term><description>Signed 16-bit raw flow rate (¼ ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>Unsigned 24-bit raw volume accumulation (¼ ml per bit)</description></item>
/// <item><term>4 cccc</term><description>Unsigned 16-bit millivolt delta on the field drive capacitor</description></item>
/// <item><term>5 tttt</term><description>Unsigned 16-bit field calibration value</description></item>
/// <item><term>6 bbbbbbbb</term><description>Unsigned 32-bit ASIC timestamp (8192 ticks per second,
/// rolls over at 2^32)</description></item>
/// <item><term>7 ff</term><description>Unsigned 8-bit field drive time in microseconds</description></item>
/// <item><term>8 ss</term><description>Unsigned 8-bit checksum (sum of all previous ASCII bytes including
/// the TAB before the checksum field)</description></item>
/// </list>
/// </summary>
public sealed class DiagnosticLedState3Data : DiagnosticLedData
{
/// <summary>
/// Unsigned 16-bit field calibration value.
/// </summary>
public ushort FieldCalibration { get; }
/// <summary>
/// ASIC timestamp in units of 1 / 8192 seconds.
/// Rolls over at 2^32.
/// </summary>
public uint AsicTimestamp { get; }
/// <summary>
/// Field drive time in microseconds.
/// </summary>
public byte FieldDriveTimeUs { get; }
public DiagnosticLedState3Data(string rawLine, string[] fields)
: base(rawLine)
{
// ---- Common fields (04) ----
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]);
}
}
}

View File

@ -0,0 +1,73 @@
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Diagnostic LED State #4 data frame.
/// <para>Frame format (TAB-separated ASCII HEX fields, CRLF terminated).</para>
/// <list type="table">
/// <listheader>
/// <term># / Field</term>
/// <description>Description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>signed 24-bit ADC value</description></item>
/// <item><term>1 aaaa</term><description>unsigned 16-bit Field strength</description></item>
/// <item><term>2 yyyy</term><description>signed 16-bit Raw flow rate (1/4 ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>unsigned 24-bit Raw volume accumulation</description></item>
/// <item><term>4 cccc</term><description>unsigned 16-bit Capacitor mV delta</description></item>
/// <item><term>5 tttt</term><description>unsigned 16-bit Field calibration</description></item>
/// <item><term>6 bbbbbbbb</term><description>unsigned 32-bit ASIC timestamp</description></item>
/// <item><term>7 ff</term><description>unsigned 8-bit Field drive time (µs)</description></item>
/// <item><term>8 mmmmmmmm</term><description>signed 32-bit Mean flow rate</description></item>
/// <item><term>9 gggg</term><description>unsigned 16-bit Field 1 measurement</description></item>
/// <item><term>10 hhhh</term><description>unsigned 16-bit Field 2 measurement</description></item>
/// <item><term>11 cccc</term><description>unsigned 16-bit Integrator calibration positive</description></item>
/// <item><term>12 nnnn</term><description>unsigned 16-bit Integrator calibration negative</description></item>
/// <item><term>13 qq</term><description>unsigned 8-bit ASIC state</description></item>
/// <item><term>14 ss</term><description>unsigned 8-bit Checksum</description></item>
/// </list>
/// </summary>
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 (04) ----
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]);
}
}
}

View File

@ -0,0 +1,93 @@
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Diagnostic LED State #5 data frame.
/// <para>
/// Frame format: TAB-separated ASCII HEX fields, CRLF terminated.
/// </para>
/// <list type="table">
/// <listheader>
/// <term># / Field</term>
/// <description>Description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>signed 24-bit ADC value</description></item>
/// <item><term>1 aaaa</term><description>unsigned 16-bit Field strength (internal units)</description></item>
/// <item><term>2 yyyy</term><description>signed 16-bit Raw flow rate (1/4 ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>unsigned 24-bit Raw volume accumulation (1/4 ml per bit)</description></item>
/// <item><term>4 cccc</term><description>unsigned 16-bit Millivolts delta on field drive capacitor</description></item>
/// <item><term>5 tttt</term><description>unsigned 16-bit Field calibration value</description></item>
/// <item><term>6 bbbbbbbb</term><description>unsigned 32-bit ASIC timestamp (8192 ticks/sec, rolls over at 2^32)</description></item>
/// <item><term>7 ff</term><description>unsigned 8-bit Field drive time in microseconds</description></item>
/// <item><term>8 mmmmmmmm</term><description>signed 32-bit Mean flow rate (rolls over at 2^32)</description></item>
/// <item><term>9 gggg</term><description>unsigned 16-bit Field 1 measurement</description></item>
/// <item><term>10 hhhh</term><description>unsigned 16-bit Field 2 measurement</description></item>
/// <item><term>11 cccc</term><description>unsigned 16-bit Integrator calibration positive</description></item>
/// <item><term>12 nnnn</term><description>unsigned 16-bit Integrator calibration negative</description></item>
/// <item><term>13 qq</term><description>unsigned 8-bit ASIC state</description></item>
/// <item><term>14 iiii</term><description>signed 16-bit Water impedance measurement</description></item>
/// <item><term>15 ss</term><description>unsigned 8-bit Checksum</description></item>
/// </list>
/// </summary>
public sealed class DiagnosticLedState5Data : DiagnosticLedData
{
/// <summary>Field calibration value (tttt).</summary>
public ushort FieldCalibration { get; }
/// <summary>ASIC timestamp (bbbbbbbb), 8192 ticks per second.</summary>
public uint AsicTimestamp { get; }
/// <summary>Field drive time in microseconds (ff).</summary>
public byte FieldDriveTimeUs { get; }
/// <summary>Mean flow rate (mmmmmmmm), signed 32-bit.</summary>
public int MeanFlowRate { get; }
/// <summary>Field 1 measurement (gggg).</summary>
public ushort Field1Measurement { get; }
/// <summary>Field 2 measurement (hhhh).</summary>
public ushort Field2Measurement { get; }
/// <summary>Integrator calibration positive (cccc).</summary>
public ushort IntegratorCalibrationPositive { get; }
/// <summary>Integrator calibration negative (nnnn).</summary>
public ushort IntegratorCalibrationNegative { get; }
/// <summary>ASIC state (qq).</summary>
public byte AsicState { get; }
/// <summary>Water impedance measurement (iiii), signed 16-bit.</summary>
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]);
}
}
}

View File

@ -0,0 +1,134 @@
using System;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Diagnostic LED State #6 data frame.
/// <para>
/// Frame format: TAB-separated ASCII HEX fields, CRLF terminated.
/// </para>
/// <list type="table">
/// <listheader>
/// <term># / Field</term>
/// <description>Description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>signed 24-bit ADC value</description></item>
/// <item><term>1 aaaa</term><description>unsigned 16-bit Field strength (internal units)</description></item>
/// <item><term>2 yyyy</term><description>signed 16-bit Raw flow rate (1/4 ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>unsigned 24-bit Raw volume accumulation (1/4 ml per bit)</description></item>
/// <item><term>4 cccc</term><description>unsigned 16-bit Millivolts delta on field drive capacitor</description></item>
/// <item><term>5 tttt</term><description>unsigned 16-bit Field calibration value</description></item>
/// <item><term>6 bbbbbbbb</term><description>unsigned 32-bit ASIC timestamp (8192 ticks/sec, rolls over at 2^32)</description></item>
/// <item><term>7 ff</term><description>unsigned 8-bit Field drive time in microseconds</description></item>
/// <item><term>8 mmmmmmmm</term><description>signed 32-bit Mean flow rate (rolls over at 2^32)</description></item>
/// <item><term>9 gggg</term><description>unsigned 16-bit Field 1 measurement</description></item>
/// <item><term>10 hhhh</term><description>unsigned 16-bit Field 2 measurement</description></item>
/// <item><term>11 cccc</term><description>unsigned 16-bit Integrator calibration positive</description></item>
/// <item><term>12 nnnn</term><description>unsigned 16-bit Integrator calibration negative</description></item>
/// <item><term>13 qq</term><description>unsigned 8-bit ASIC state 0</description></item>
/// <item><term>14 iiii</term><description>signed 16-bit Water impedance measurement</description></item>
/// <item><term>15 rrrr</term><description>signed 16-bit Electrode delta (mV)</description></item>
/// <item><term>16 pp</term><description>unsigned 8-bit Spike detection diagnostic</description></item>
/// <item><term>17 ll</term><description>unsigned 8-bit Pipe status</description></item>
/// <item><term>18 dddddddd</term><description>unsigned 32-bit LCD volume</description></item>
/// <item><term>19 oo</term><description>unsigned 8-bit ASIC state 1</description></item>
/// <item><term>20 ss</term><description>unsigned 8-bit Checksum</description></item>
/// </list>
/// </summary>
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 (04) ----
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]);
}
/// <summary>
/// Pipe status interpreted as <see cref="PipeStatus"/>.
/// If the value is outside the defined range, returns null.
/// </summary>
public PipeStatus PipeStatusEnumValue
{
get
{
if (!Enum.IsDefined(typeof(PipeStatus), PipeStatus))
throw new InvalidOperationException(
"Unknown pipe status value: 0x" + PipeStatus.ToString("X2"));
return (PipeStatus)PipeStatus;
}
}
/// <summary>
/// Spike Detection interpreted as <see cref="SpikeDetectionStatus"/>.
/// If the value is outside the defined range, returns null.
/// </summary>
public SpikeDetectionStatus SpikeDetectionEnumValue
{
get
{
if (!Enum.IsDefined(typeof(SpikeDetectionStatus), SpikeDetection))
throw new InvalidOperationException(
"Unknown Spike Detection value: 0x" + SpikeDetection.ToString("X2"));
return (SpikeDetectionStatus)SpikeDetection;
}
}
}
}

View File

@ -0,0 +1,137 @@
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
/// <summary>
/// Diagnostic LED State #7 data frame.
/// <para>
/// Frame format: TAB-separated ASCII HEX fields, CRLF terminated.
/// This state extends State #6 with additional ADC and learning diagnostics.
/// </para>
/// <list type="table">
/// <listheader>
/// <term># / Field</term>
/// <description>Description</description>
/// </listheader>
/// <item><term>0 xxxxxx</term><description>signed 24-bit ADC value</description></item>
/// <item><term>1 aaaa</term><description>unsigned 16-bit Field strength (internal units)</description></item>
/// <item><term>2 yyyy</term><description>signed 16-bit Raw flow rate (1/4 ml per bit)</description></item>
/// <item><term>3 vvvvvv</term><description>unsigned 24-bit Raw volume accumulation (1/4 ml per bit)</description></item>
/// <item><term>4 cccc</term><description>unsigned 16-bit Millivolts delta on field drive capacitor</description></item>
/// <item><term>5 tttt</term><description>unsigned 16-bit Field calibration value</description></item>
/// <item><term>6 bbbbbbbb</term><description>unsigned 32-bit ASIC timestamp (8192 ticks/sec)</description></item>
/// <item><term>7 ff</term><description>unsigned 8-bit Field drive time (µs)</description></item>
/// <item><term>8 mmmmmmmm</term><description>signed 32-bit Mean flow rate</description></item>
/// <item><term>9 gggg</term><description>unsigned 16-bit Field 1 measurement</description></item>
/// <item><term>10 hhhh</term><description>unsigned 16-bit Field 2 measurement</description></item>
/// <item><term>11 cccc</term><description>unsigned 16-bit Integrator calibration positive</description></item>
/// <item><term>12 nnnn</term><description>unsigned 16-bit Integrator calibration negative</description></item>
/// <item><term>13 qq</term><description>unsigned 8-bit ASIC state 0</description></item>
/// <item><term>14 iiii</term><description>signed 16-bit Water impedance measurement</description></item>
/// <item><term>15 rrrr</term><description>signed 16-bit Electrode delta (mV)</description></item>
/// <item><term>16 pp</term><description>unsigned 8-bit Spike detection diagnostic</description></item>
/// <item><term>17 ll</term><description>unsigned 8-bit Pipe status</description></item>
/// <item><term>18 dddddddd</term><description>unsigned 32-bit LCD volume</description></item>
/// <item><term>19 oo</term><description>unsigned 8-bit ASIC state 1</description></item>
/// <item><term>20 xxxxxx</term><description>signed 24-bit Raw ADC value (before offset correction)</description></item>
/// <item><term>21 yyyyyy</term><description>signed 24-bit Detrended ADC value</description></item>
/// <item><term>22 iiii</term><description>signed 16-bit Imaginary water impedance</description></item>
/// <item><term>23 nnnn</term><description>unsigned 16-bit Electrode voltage noise level</description></item>
/// <item><term>24 aa</term><description>unsigned 8-bit ADC offset learning status</description></item>
/// <item><term>25 ss</term><description>unsigned 8-bit Checksum</description></item>
/// </list>
/// </summary>
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 -----
/// <summary>Raw ADC value before offset correction (signed 24-bit).</summary>
public int RawAdcBeforeOffset { get; }
/// <summary>Detrended ADC value (signed 24-bit).</summary>
public int DetrendedAdc { get; }
/// <summary>Imaginary water impedance (signed 16-bit).</summary>
public short ImaginaryWaterImpedance { get; }
/// <summary>Electrode voltage noise level (unsigned 16-bit).</summary>
public ushort ElectrodeVoltageNoise { get; }
/// <summary>
/// ADC offset learning status bitfield.
/// Bit 0: currently learning
/// Bit 1: completed first learning cycle
/// Other bits reserved.
/// </summary>
public byte AdcOffsetLearningStatus { get; }
public DiagnosticLedState7Data(string rawLine, string[] fields)
: base(rawLine)
{
// ---- Common fields (04) ----
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]);
}
}
}

View File

@ -0,0 +1,11 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
public enum PipeStatus : byte
{
MetroLowFlowCut = 0,
MetroFlowReverse = 1,
MetroFlowForward = 2,
MetroEmptyPipe = 3
}
}

View File

@ -0,0 +1,11 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer
{
public enum SpikeDetectionStatus : byte
{
NoSpike = 0,
AdcSpike = 1,
SpikeHoldOff = 2,
SpikeHighFlow = 5
}
}

View File

@ -0,0 +1,13 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils
{
internal static class DiagnosticChecksum
{
public static byte Compute(string lineWithoutChecksum)
{
byte sum = 0;
foreach (char c in lineWithoutChecksum)
sum += (byte)c;
return sum;
}
}
}

View File

@ -0,0 +1,40 @@
using System;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.utils
{
internal static class DiagnosticHex
{
public static int ParseInt24(string hex)
{
int value = Convert.ToInt32(hex, 16);
if ((value & 0x800000) != 0)
value |= unchecked((int)0xFF000000); // sign extend
return value;
}
public static uint ParseUInt24(string hex)
{
return Convert.ToUInt32(hex, 16);
}
public static short ParseInt16(string hex)
{
return unchecked((short)Convert.ToUInt16(hex, 16));
}
public static ushort ParseUInt16(string hex)
{
return Convert.ToUInt16(hex, 16);
}
public static uint ParseUInt32(string hex)
{
return Convert.ToUInt32(hex, 16);
}
public static byte ParseByte(string hex)
{
return Convert.ToByte(hex, 16);
}
}
}

View File

@ -0,0 +1,59 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger
{
public static class HexFormatter
{
/// <summary>
/// Formats a single byte as 0xNN.
/// Example: 0x0D
/// </summary>
public static string ToHex(byte value)
{
return "0x" + value.ToString("X2");
}
/// <summary>
/// Formats a byte array as 0xNN 0xNN ...
/// </summary>
public static string ToHex(byte[] data)
{
if (data == null || data.Length == 0)
return "<empty>";
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();
}
/// <summary>
/// Formats a byte array exactly as shown in serial terminals.
/// Example: "0D 04 08 01 00 1A"
/// </summary>
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();
}
}
}

View File

@ -0,0 +1,16 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger
{
public static class TouchReadControlDecoder
{
public static string Describe(byte control)
{
if (control == 0x00)
return "RF=0 (No response expected)";
if (control == 0x08)
return "RF=1 (Response expected)";
return "INVALID CONTROL BITS (unsupported pattern)";
}
}
}

View File

@ -0,0 +1,56 @@
using System;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger
{
public static class TouchReadLogger
{
public static string DescribeTx(byte[] frame)
{
if (frame == null || frame.Length < 6)
return "Invalid frame";
return
"TX Frame\n" +
$" START : {HexFormatter.ToHex(frame[0])}\n" +
$" LEN : {HexFormatter.ToHex(frame[1])} ({frame[1]})\n" +
$" CONTROL : {HexFormatter.ToHex(frame[2])} - {TouchReadControlDecoder.Describe(frame[2])}\n" +
$" INFO : {HexFormatter.ToHex(GetInformation(frame))}\n" +
$" CHECKSUM: {HexFormatter.ToHex(frame[frame.Length - 2])} {HexFormatter.ToHex(frame[frame.Length - 1])}\n" +
$" RAW : {HexFormatter.ToHex(frame)}";
}
private static byte[] GetInformation(byte[] frame)
{
int infoLength = frame.Length - 5; // CTRL + INFO + CHK(2)
if (infoLength <= 0)
return Array.Empty<byte>();
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";
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led
{
public interface ITouchReadLedParser
{
TouchReadLedData Parse(TouchReadLedMessage message);
}
}

View File

@ -0,0 +1,17 @@
using System.Globalization;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led
{
public class ShortVariableLedParser : ITouchReadLedParser
{
public TouchReadLedData Parse(TouchReadLedMessage msg)
{
return new TouchReadLedData(msg.Raw)
{
MeterId = msg.Fields[0],
Reading = decimal.Parse(msg.Fields[1],
CultureInfo.InvariantCulture)
};
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Globalization;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led
{
/// <summary>
/// Parsed data from a unidirectional TouchRead LED message.
/// The exact populated fields depend on the configured reading mode.
/// </summary>
public sealed class TouchReadLedData
{
/// <summary>
/// Raw LED message including delimiters.
/// Example: ";12345678,00012345.67,m3;"
/// </summary>
public string Raw { get; }
/// <summary>
/// Meter factory ID or serial number (if present).
/// </summary>
public string MeterId { get; set; }
/// <summary>
/// Customer programmable ID (if present).
/// </summary>
public string CustomerId { get; set; }
/// <summary>
/// Parsed meter reading value.
/// </summary>
public decimal? Reading { get; set; }
/// <summary>
/// Engineering units (e.g. "m3", "ft3", "gal").
/// </summary>
public string Units { get; set; }
/// <summary>
/// Optional alarm/status field (bitfield or text).
/// </summary>
public string AlarmStatus { get; set; }
/// <summary>
/// Timestamp when the LED data was received.
/// </summary>
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;
}
/// <summary>
/// Helper to safely parse a decimal value using invariant culture.
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led
{
public class TouchReadLedMessage
{
public string Raw { get; }
public string[] Fields { get; }
public TouchReadLedMessage(string raw)
{
Raw = raw ?? throw new ArgumentNullException(nameof(raw));
if (!raw.StartsWith(";") || !raw.EndsWith(";"))
throw new FormatException("Invalid LED message framing");
string content = raw.Substring(1, raw.Length - 2);
Fields = content.Split(',');
}
}
}

View File

@ -1251,6 +1251,34 @@
<Compile Include="Rig\RegisterReaders\DataStream\Reader\ProcParams.cs" />
<Compile Include="Rig\RegisterReaders\DataStream\Reader\Reader.cs" />
<Compile Include="Rig\RegisterReaders\DataStream\Reader\ReaderCfg.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\DiagnosticLedParser.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\DiagnosticLedState.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedData.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedState1Data.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedState2Data.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedState3Data.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedState4Data.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedState5Data.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedState6Data.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\DiagnosticLedState7Data.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\PipeStatus.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\parserer\SpikeDetectionStatus.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\utils\DiagnosticChecksum.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\utils\DiagnosticHex.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\hexLogger\HexFormatter.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\hexLogger\TouchReadControlDecoder.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\hexLogger\TouchReadLogger.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\led\ITouchReadLedParser.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\led\ShortVariableLedParser.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\led\TouchReadLedData.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\led\TouchReadLedMessage.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadCommand.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadDeviceSubCommand.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadFrame.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadFrameBuilder.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadFrameParser.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadProtocol.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadResponse.cs" />
<Compile Include="Rig\RegisterReaders\iPerlReaderUNI\common\IUniHeadTestCtrl.cs" />
<Compile Include="Rig\RegisterReaders\iPerlReaderUNI\Factory.cs" />
<Compile Include="Rig\RegisterReaders\iPerlReaderUNI\IPerlUniCfgCtrl.cs">

View File

@ -0,0 +1,113 @@
using System;
using System.IO.Ports;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger;
namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
[TestClass]
public class TouchReadBaudRateDetectionTests
{
private const string ComPort = "COM3"; // COM PORT OF THE ASIC
private const int ReadTimeoutMs = 1500;
private static readonly int[] StandardBaudRates =
{
300, 600, 7812, 1200, 18432, 2400, 4800,
9600, 10400, 15625, 19200, 31250, 36864,
38400, 50000, 57600, 62500, 76800, 115200
};
[TestMethod]
[TestCategory("Hardware")]
[TestCategory("Serial")]
public void Detect_BaudRate_By_ViewFactoryId()
{
byte[] request = new TouchReadFrameBuilder()
.RequestResponse(true)
.AddCommand(TouchReadCommand.ViewFactoryId)
.BuildBytes();
var parser = new TouchReadFrameParser();
foreach (int baud in StandardBaudRates)
{
Console.WriteLine($"--- Testing baud rate: {baud} ---");
try
{
using (var port = new SerialPort(ComPort, baud, Parity.None, 7, StopBits.One))
{
port.ReadTimeout = ReadTimeoutMs;
port.WriteTimeout = 500;
port.Open();
port.DiscardInBuffer();
port.DiscardOutBuffer();
Console.WriteLine("TX → " + HexFormatter.ToSerialHex(request));
port.Write(request, 0, request.Length);
byte[] response = ReadFullFrame(port);
Console.WriteLine("RX ← " + HexFormatter.ToSerialHex(response));
TouchReadResponse decoded = parser.Parse(response);
if (decoded.IsOk)
{
string factoryId = decoded.GetAsciiPayload();
Console.WriteLine();
Console.WriteLine("VALID RESPONSE");
Console.WriteLine("Baud rate : " + baud);
Console.WriteLine("Factory ID : " + factoryId);
Console.WriteLine();
Assert.IsFalse(string.IsNullOrEmpty(factoryId),
"Factory ID is empty");
return; // SUCCESS → stop scanning
}
}
}
catch (TimeoutException)
{
Console.WriteLine("Timeout");
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
}
Assert.Fail("No valid baud rate detected.");
}
private static byte[] ReadFullFrame(SerialPort port)
{
byte start = (byte)port.ReadByte();
if (start != 0x0D)
throw new InvalidOperationException("Invalid START byte");
byte length = (byte)port.ReadByte();
int remaining = length;
byte[] buffer = new byte[2 + remaining];
buffer[0] = start;
buffer[1] = length;
int offset = 2;
while (remaining > 0)
{
int read = port.Read(buffer, offset, remaining);
offset += read;
remaining -= read;
}
return buffer;
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger;
namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
[TestClass]
[TestSubject(typeof(TouchReadFrameBuilder))]
public class TouchReadFrameBuilderTest
{
[TestMethod]
public void Encode_ViewFactoryId_Command()
{
byte[] frame = new TouchReadFrameBuilder()
.RequestResponse(true)
.AddCommand(TouchReadCommand.ViewFactoryId)
.BuildBytes();
byte[] expected =
{
0x0D, // START
0x04, // LEN
0x08, // CONTROL (RF)
0x01, // COMMAND
0x00, // CHECKSUM HI
0x1A // CHECKSUM LO
};
CollectionAssert.AreEqual(expected, frame);
string log = TouchReadLogger.DescribeTx(frame);
Console.WriteLine(log);
Console.WriteLine(@"Raw: < {0} >", HexFormatter.ToSerialHex(frame));
}
[TestMethod]
public void Encode_ViewProgrammableId_Command()
{
byte[] frame = new TouchReadFrameBuilder()
.RequestResponse(true)
.AddCommand(TouchReadCommand.ViewProgrammableId)
.BuildBytes();
byte[] expected =
{
0x0D, // START
0x04, // LEN
0x08, // CONTROL (RF)
0x03, // COMMAND
0x00, // CHECKSUM HI
0x1C // CHECKSUM LO
};
CollectionAssert.AreEqual(expected, frame);
string log = TouchReadLogger.DescribeTx(frame);
Console.WriteLine(log);
Console.WriteLine(@"Raw: <{0}>", HexFormatter.ToSerialHex(frame));
}
[TestMethod]
public void Encode_SetState_Idle()
{
// Arrange
byte[] frame = new TouchReadFrameBuilder()
.RequestResponse(true)
.AddCommand(TouchReadCommand.SetState)
.AddPayload(new byte[] { 0x01 }) // Idle
.BuildBytes();
byte[] expected =
{
0x0D, // START
0x05, // LEN
0x08, // CONTROL (RF)
0x1A, // COMMAND (Set State)
0x01, // PAYLOAD (Idle)
0x00, // CHECKSUM HI
0x35 // CHECKSUM LO
};
// Assert
CollectionAssert.AreEqual(expected, frame,
$"Encoded frame mismatch.\nExpected: {HexFormatter.ToSerialHex(expected)}\nActual: {HexFormatter.ToSerialHex(frame)}");
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void Decode_InvalidStart_Throws()
{
byte[] response =
{
0x00, // invalid START
0x04,
0x00,
0x01,
0x00,
0x12
};
var parser = new TouchReadFrameParser();
parser.Parse(response);
}
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.IO.Ports;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.hexLogger;
namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4
{
[TestClass]
public class TouchReadSerialIntegrationTests
{
private const string ComPort = "COM3"; // CHANGE THIS
private const int BaudRate = 9600;//38400;//115200;//9600; // VERIFY FROM METER DOC
private const int ReadTimeoutMs = 2000;
[TestMethod]
[TestCategory("Hardware")]
[TestCategory("Serial")]
public void Serial_ViewFactoryId_ReadSerialNumber()
{
// -------- Arrange --------
byte[] request = new TouchReadFrameBuilder()
.RequestResponse(true)
.AddCommand(TouchReadCommand.ViewFactoryId)
.BuildBytes();
var parser = new TouchReadFrameParser();
using (var port = new SerialPort(ComPort, BaudRate, Parity.None, 8, StopBits.One))
{
port.ReadTimeout = ReadTimeoutMs;
port.WriteTimeout = 500;
port.Open();
// Flush buffers
port.DiscardInBuffer();
port.DiscardOutBuffer();
// -------- Act --------
port.Write(request, 0, request.Length);
Console.WriteLine("TX → " + HexFormatter.ToSerialHex(request));
byte[] response = ReadFullFrame(port);
Console.WriteLine("RX ← " + HexFormatter.ToSerialHex(response));
TouchReadResponse decoded = parser.Parse(response);
// -------- Assert --------
Assert.AreEqual(0x01, decoded.Status, "Meter returned error status");
string serialNumber = decoded.GetAsciiPayload();
Assert.IsFalse(string.IsNullOrEmpty(serialNumber),
"Factory ID (serial number) is empty");
Console.WriteLine("Meter Factory ID: " + serialNumber);
}
}
/// <summary>
/// Reads a full TouchRead frame from the serial port.
/// Blocks until complete frame or timeout.
/// </summary>
private static byte[] ReadFullFrame(SerialPort port)
{
// Read START + LEN first
byte start = (byte)port.ReadByte();
if (start != 0x0D)
throw new InvalidOperationException("Invalid START byte from meter");
byte length = (byte)port.ReadByte();
// LEN counts from CONTROL to CHECKSUM
int remaining = length;
byte[] buffer = new byte[2 + remaining];
buffer[0] = start;
buffer[1] = length;
int offset = 2;
while (remaining > 0)
{
int read = port.Read(buffer, offset, remaining);
offset += read;
remaining -= read;
}
return buffer;
}
[TestMethod]
[TestCategory("Hardware")]
public void Serial_RawSniff()
{
using (var port = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
port.ReadTimeout = 500;
port.Open();
Console.WriteLine("Listening for 5 seconds...");
DateTime end = DateTime.Now.AddSeconds(5);
while (DateTime.Now < end)
{
try
{
int b = port.ReadByte();
Console.Write($"{b:X2} ");
}
catch (TimeoutException)
{
}
}
Console.WriteLine("\nDone.");
}
}
}
}

View File

@ -0,0 +1,189 @@
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed.parserer;
namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.diagnosticLed
{
[TestClass]
[TestSubject(typeof(DiagnosticLedParser))]
public class DiagnosticLedParserTest
{
private static string WithChecksum(string bodyWithoutChecksum)
{
byte sum = 0;
foreach (char c in bodyWithoutChecksum)
sum += (byte)c;
return bodyWithoutChecksum + sum.ToString("X2") + "\r\n";
}
[TestMethod]
public void Parse_DiagnosticLed_State1()
{
string body =
"FFFF9C\t" + // signed 24-bit ADC = -100
"2020\t" + // field strength
"FFFA\t" + // raw flow (-6)
"0050FC\t" + // raw volume
"0054\t"; // capacitor mV
string line = WithChecksum(body);
var parser = new DiagnosticLedParser(DiagnosticLedState.State1);
var data = (DiagnosticLedState1Data)parser.ParseLine(line);
Assert.AreEqual(-100, data.Adc24);
Assert.AreEqual((ushort)0x2020, data.FieldStrength);
Assert.AreEqual((short)-6, data.RawFlow);
Assert.AreEqual((uint)0x0050FC, data.RawVolume);
Assert.AreEqual((ushort)0x0054, data.CapacitorMv);
}
[TestMethod]
public void Parse_DiagnosticLed_State2()
{
string line =
"00004F\t029A\t0000\tFFD3B1\t005C\t3B9AC9B1\t02\t01\t0D\r\n";
var parser = new DiagnosticLedParser(DiagnosticLedState.State2);
var data = (DiagnosticLedState2Data)parser.ParseLine(line);
Assert.AreEqual(79, data.Adc24);
Assert.AreEqual((ushort)666, data.FieldStrength);
Assert.AreEqual((short)0, data.RawFlow);
Assert.AreEqual(0xFFD3B1u, data.RawVolume);
Assert.AreEqual((ushort)92, data.CapacitorMv);
Assert.AreEqual(0x3B9AC9B1u, data.LcdVolume);
Assert.AreEqual((byte)0x02, data.MeterState);
Assert.IsTrue(data.IsLowFlowCutoff);
}
[TestMethod]
public void Parse_DiagnosticLed_State3()
{
string body =
"FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t";
string line = WithChecksum(body);
var parser = new DiagnosticLedParser(DiagnosticLedState.State3);
var data = (DiagnosticLedState3Data)parser.ParseLine(line);
Assert.AreEqual(-13303, data.Adc24);
Assert.AreEqual((ushort)0x2020, data.FieldStrength);
Assert.AreEqual((short)-6, data.RawFlow);
Assert.AreEqual((uint)0x0050FC, data.RawVolume);
Assert.AreEqual((ushort)0x0054, data.CapacitorMv);
Assert.AreEqual((ushort)0x0B01, data.FieldCalibration);
Assert.AreEqual((uint)0x048000, data.AsicTimestamp);
Assert.AreEqual((byte)0xA7, data.FieldDriveTimeUs);
}
[TestMethod]
public void Parse_DiagnosticLed_State4()
{
string body =
"000ABC\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" +
"00001234\t00F0\t00F1\t0100\t0200\t03\t";
string line = WithChecksum(body);
var parser = new DiagnosticLedParser(DiagnosticLedState.State4);
var data = (DiagnosticLedState4Data)parser.ParseLine(line);
Assert.AreEqual(2748, data.Adc24);
Assert.AreEqual((ushort)0x2020, data.FieldStrength);
Assert.AreEqual((short)-6, data.RawFlow);
Assert.AreEqual((uint)0x0050FC, data.RawVolume);
Assert.AreEqual((ushort)0x0054, data.CapacitorMv);
Assert.AreEqual((ushort)0x0B01, data.FieldCalibration);
Assert.AreEqual((uint)0x048000, data.AsicTimestamp);
Assert.AreEqual((byte)0xA7, data.FieldDriveTimeUs);
Assert.AreEqual(0x00001234, data.MeanFlowRate);
Assert.AreEqual((ushort)0x00F0, data.Field1Measurement);
Assert.AreEqual((ushort)0x00F1, data.Field2Measurement);
Assert.AreEqual((ushort)0x0100, data.IntegratorCalibrationPositive);
Assert.AreEqual((ushort)0x0200, data.IntegratorCalibrationNegative);
Assert.AreEqual((byte)0x03, data.AsicState);
}
[TestMethod]
public void Parse_DiagnosticLed_State5()
{
string body =
"FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" +
"00001234\t00F0\t00F1\t0100\t0200\t03\tFFEC\t";
string line = WithChecksum(body);
var parser = new DiagnosticLedParser(DiagnosticLedState.State5);
var data = (DiagnosticLedState5Data)parser.ParseLine(line);
Assert.AreEqual((short)-20, data.WaterImpedance);
Assert.AreEqual((byte)0x03, data.AsicState);
}
[TestMethod]
public void Parse_DiagnosticLed_State6()
{
string body =
"FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" +
"00001234\t00F0\t00F1\t0100\t0200\t03\tFFEC\t0010\t" +
"02\t" + // pp spike detection
"02\t" + // ll pipe status
"00000099\t" + // LCD volume
"01\t"; // ASIC state1
string line = WithChecksum(body);
var parser = new DiagnosticLedParser(DiagnosticLedState.State6);
var data = (DiagnosticLedState6Data)parser.ParseLine(line);
Assert.AreEqual((short)-20, data.WaterImpedance);
Assert.AreEqual((short)0x0010, data.ElectrodeDeltaMv);
Assert.AreEqual((byte)0x02, data.SpikeDetection);
Assert.AreEqual((byte)0x02, data.PipeStatus);
Assert.AreEqual((uint)0x99, data.LcdVolume);
Assert.AreEqual((byte)0x01, data.AsicState1);
}
[TestMethod]
public void Parse_DiagnosticLed_State7()
{
string body =
"FFCC09\t2020\tFFFA\t0050FC\t0054\t0B01\t048000\tA7\t" +
"00001234\t00F0\t00F1\t0100\t0200\t03\tFFEC\t0010\t" +
"02\t" + // pp spike detection
"02\t" + // ll pipe status
"00000099\t" + // LCD volume
"01\t" + // ASIC state1
"FFAA10\t" + // raw ADC before offset
"000123\t" + // detrended ADC
"FFEE\t" + // imaginary water impedance
"0011\t" + // electrode voltage noise
"03\t"; // ADC offset learning status
string line = WithChecksum(body);
var parser = new DiagnosticLedParser(DiagnosticLedState.State7);
var data = (DiagnosticLedState7Data)parser.ParseLine(line);
Assert.AreEqual(-22000, data.RawAdcBeforeOffset);
Assert.AreEqual(0x000123, data.DetrendedAdc);
Assert.AreEqual((short)-18, data.ImaginaryWaterImpedance);
Assert.AreEqual((ushort)0x0011, data.ElectrodeVoltageNoise);
Assert.AreEqual((byte)0x03, data.AdcOffsetLearningStatus);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led;
namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.led
{
[TestClass]
[TestSubject(typeof(ShortVariableLedParser))]
public class ShortVariableLedParserTest
{
[TestMethod]
public void Parse_ValidShortVariableMessage()
{
// Arrange
string raw = ";12345678,00012345.67;";
var message = new TouchReadLedMessage(raw);
var parser = new ShortVariableLedParser();
// Act
TouchReadLedData data = parser.Parse(message);
// Assert
Assert.IsNotNull(data);
Assert.AreEqual(raw, data.Raw);
Assert.AreEqual("12345678", data.MeterId);
Assert.AreEqual(12345.67m, data.Reading);
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void Parse_InvalidDecimal_Throws()
{
// Arrange
string raw = ";12345678,ABCDEF;";
var message = new TouchReadLedMessage(raw);
var parser = new ShortVariableLedParser();
// Act
parser.Parse(message);
}
}
}

View File

@ -0,0 +1,45 @@
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.iPerlASICReader.communication.C4.led;
namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.led
{
[TestClass]
[TestSubject(typeof(TouchReadLedMessage))]
public class TouchReadLedMessageTest
{
[TestMethod]
public void Parse_LedMessage_Basic()
{
string raw = ";12345678,00012345.67;";
var msg = new TouchReadLedMessage(raw);
Assert.AreEqual(2, msg.Fields.Length);
Assert.AreEqual("12345678", msg.Fields[0]);
Assert.AreEqual("00012345.67", msg.Fields[1]);
}
[TestMethod]
public void TouchReadLedData_Parse_Extended()
{
string raw = ";12345678,ABC123,00012345.67,m3;";
var msg = new TouchReadLedMessage(raw);
var data = new TouchReadLedData(raw)
{
MeterId = msg.Fields[0],
CustomerId = msg.Fields[1],
Reading = TouchReadLedData.ParseDecimal(msg.Fields[2]),
Units = msg.Fields[3]
};
Assert.AreEqual("12345678", data.MeterId);
Assert.AreEqual("ABC123", data.CustomerId);
Assert.AreEqual(12345.67m, data.Reading);
Assert.AreEqual("m3", data.Units);
}
}
}

View File

@ -103,6 +103,12 @@
<Compile Include="Rig\Network\Camera\KeyenceIV3G120\CameraTest.cs" />
<Compile Include="Rig\Network\Camera\RoiForFixedStartKeyence\RoiTest.cs" />
<Compile Include="Rig\Output\FileWriters\Enhanced\WriterTest.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\DiagnosticLedParserTest.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\led\ShortVariableLedParserTest.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\led\TouchReadLedMessageTest.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadBaudRateDetectionTests.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadFrameBuilderTest.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\TouchReadSerialIntegrationTests.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\CliRunnerTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonReader\UniHeadTestCtrlTest.cs" />