common/Hardware/WaterMeter/MagFlux/Protocols/StreamingProtocol/StreamingDecoder.cs
2026-04-23 17:50:07 +02:00

245 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using Xylem.Common.Hardware.WaterMeter.MagFlux.DataPackages.MeasurementRecords;
using Xylem.Common.Utils.Crc16Ccitt;
namespace Xylem.Common.Hardware.WaterMeter.MagFlux.Protocols.StreamingProtocol
{
/// <summary>
/// Data fields and definitions for GENESIS streaming protocol
/// </summary>
public class StreamingDecoder
{
private const Double MilliLitersToCmFactor = 1.0E-6;
private const Double CpuTimeToSecondsFactor = 1.0 / 0x10000;
private const Double CpuTimeOverflowS = 0x100000000 * CpuTimeToSecondsFactor;
private const Double LitersPerSecondToCmPerHourFactor = 3600.0 / 1000.0;
private const Double DefaultVolumeScaleRawPerMl = 1024.0;
private const Double DefaultVolumeFactorRawToCm = MilliLitersToCmFactor / DefaultVolumeScaleRawPerMl;
private const Double MaxGenesisAccuVolumeRaw = UInt32.MaxValue; //0x100000000; //2^32
private const Double DefaultAccuDutOverflowVolumeCm = MaxGenesisAccuVolumeRaw * DefaultVolumeFactorRawToCm;
private const Double DisplayMlSetupDutOverflowVolumeCm = 1000.0; //overflow of LCD if set to ml
private const Double TofToSecondsFactor38Bit = 1.0 / 0x4000000000; // 2^38
private const Double AmplitudeToVoltFactor = 1.0 / 0x400000 / 1000.0; // 2^22 100 0000 0000 0000 0000 0000b
private const Double PulseWidthToRelFactor = 1.0 / 0x100; // 2^8
// protocol 'l' MagFlux
private const Double MaxMagFluxVolumeRaw = UInt64.MaxValue;//0x10000000000000000; //2^64
private const Double DefaultMagFluxOverflowVolumeCm = MaxMagFluxVolumeRaw / MilliLitersToCmFactor;
/// <summary>
/// Default data for
/// </summary>
private readonly MagFluxRecord _dataMagFluxDefault = new MagFluxRecord
{
VolumeCm = 0.0,
ForwardVolumeCm = 0.0,
NegativeReverseVolumeCm = 0.0,
TimeS = 0.0,
FlowRateCmPh = 0.0,
StatusBits = 0,
UniqueId = "?",
SensorId = "?",
Crc = 0xFFFF,
OverflowVolumeCm = DefaultMagFluxOverflowVolumeCm,
OverflowTimeS = CpuTimeOverflowS,
IsValid = false
};
private MagFluxRecord _dataMagFluxRec;
private readonly Boolean _ignoreCorruptedData;
private String _sensorIdMask;
private String _uniqueIdMask;
/// <summary>
/// Constructor initializes all decoded members with default values
/// </summary>
public StreamingDecoder(Boolean ignoreCorruptedData = true)
{
_dataMagFluxRec = _dataMagFluxDefault;
_ignoreCorruptedData = ignoreCorruptedData;
}
/// <summary>
/// Data of MagFlux
/// </summary>
public MagFluxRecord DataMagFlux
{
get; private set;
}
/// <summary>
/// Set decoding mask for streaming protocol, wipes out all not matching records:
/// - SensorIdMask,
/// - UniqueIdMask
/// </summary>
/// <param name="sensorIdMask"></param>
/// <param name="uniqueIdMask"></param>
/// <returns>true if decoding was successful and data has been validated</returns>
/// <remarks date="2023-Mai-30" author="T.Wiedebusch">
/// - Initial.
/// </remarks>
/// <remarks date="2024-Aug-19" author="T.Wiedebusch">
/// - Changed from PcbId to SensorId.
/// </remarks>
public void SetDecodingMask(String sensorIdMask, String uniqueIdMask)
{
_sensorIdMask = sensorIdMask;
_uniqueIdMask = uniqueIdMask;
}
/// <summary>
/// Decoding the raw message
/// </summary>
/// <param name="rawMsg">message received as one line delimited with LF</param>
/// <returns>true if decoding was successful and data has been validated</returns>
/// <remarks date="2023-Mar-09" author="T.Wiedebusch">
/// - Modified using common CRC check before branching to the protocol specific decoder.
/// </remarks>
/// <remarks date="2023-May-25" author="T.Wiedebusch">
/// - Introduced reverse volume.
/// </remarks>
/// <remarks date="2024-Aug-19" author="T.Wiedebusch">
/// - Changed from PcbId to SensorId.
/// </remarks>
public Boolean DecodeMsg(String rawMsg)
{
var rawRecordIsValid = false;
try
{
//MagFlux test string:
//rawMsg = "@l 1bd911f 0 95b8d4 40cf9b5c 0 26004f5232500120373235 0 5863"
//Protocol version, DUT:Vol FWD [mL], DUT: Vol REV [ml], DUT:Time [s], DUT:Flow_LPS, DUT:StatusBitsHex,
//l, 29200671, 0, 149.7219849, 6.487714767, 0,
//DUT:UniqueId, DUT:SensorId, DUT:CRC
//4.59406E+25, 0, 22627 (0x5863)
// The received message is a string with a line delimiter.
rawMsg = rawMsg.Replace('\n', ' ');
// The raw message fields are the separated values from the received string with a blank as field separator
var rawMsgFields = rawMsg.Split(' ');
// The last element is the CRC, the CRC can be separated, calculated and validated before trying to decode the content
var rawRecordForCrc = "";
// get all fields excluding the CRC (length - 1)
for (var x = 0; x < rawMsgFields.Length - 1; x++)
{
rawRecordForCrc += rawMsgFields[x];
// add the field delimiter from raw data
rawRecordForCrc += " ";
}
// extract bytes of raw message for CRC calculation each character, CRC field is already removed
var byteArraySize = rawRecordForCrc.Length;
var byteArray = new Byte[byteArraySize];
for (var i = 0; i < byteArraySize; i++)
{
byteArray[i] = (Byte)rawRecordForCrc[i];
}
// calculate the CRC from the received data
var calculatedCrc = Crc16Ccitt.CalculateMsb1021(byteArray);
// extract received CRC
var receivedCrc = UInt16.Parse(rawMsgFields[rawMsgFields.Length - 1], NumberStyles.HexNumber);
// compare received with calculated CRC and remind valid decoding
rawRecordIsValid = calculatedCrc == receivedCrc;
switch (rawMsgFields[0])
{
case "@l":
//protocol @m starts with all contents of protocol @l and contains additional fields
//which needn't be decoded as those are used for DEBUG
case "@n":
if (rawRecordIsValid || !_ignoreCorruptedData)
{
DecodeProtocolL(ref _dataMagFluxRec, rawMsgFields);
if (string.Equals(_uniqueIdMask, _dataMagFluxRec.UniqueId) &&
string.Equals(_sensorIdMask, _dataMagFluxRec.SensorId))
{
DataMagFlux = _dataMagFluxRec;
_dataMagFluxRec.IsValid = rawRecordIsValid;
}
}
break;
}
}
catch (Exception)
{
// ignored
}
return rawRecordIsValid;
}
/// <summary>
/// Extracting message from string fields for protocol 'l'
/// </summary>
/// <param name="dataMagFluxRec">reference to mag flux record</param>
/// <param name="fields">fields holding the separated values for decoding</param>
/// <remarks date="2023-Mar-09" author="T.Wiedebusch">
/// - Initial.
/// </remarks>
/// <remarks date="2023-Jun-06" author="T.Wiedebusch">
/// - Calculated sum volume from forward- minus reverse-volume.
/// </remarks>
/// <remarks date="2023-Jun-07" author="T.Wiedebusch">
/// - Reverse volume multiplied with -1.0 as the test bench expects a negative value for reverse volume.
/// </remarks>
/// <remarks date="2024-Oct-28" author="T.Wiedebusch">
/// - CRC is the last field of the data stream as new protocol "@n" is dynamic of size while the beginning
/// is identical to "@l".
/// </remarks>
private static void DecodeProtocolL(ref MagFluxRecord dataMagFluxRec, IList<String> fields)
{
// time stamp attachment
dataMagFluxRec.DecodedTime = DateTimeOffset.UtcNow;
// extract received CRC
dataMagFluxRec.Crc = UInt16.Parse(fields[fields.Count - 1], NumberStyles.HexNumber);
// build result values
dataMagFluxRec.ForwardVolumeCm = UInt64.Parse(fields[(Int32)ProtLsubString.FwdVolume],
NumberStyles.AllowHexSpecifier) * MilliLitersToCmFactor;
dataMagFluxRec.NegativeReverseVolumeCm = UInt64.Parse(fields[(Int32)ProtLsubString.RevVolume],
NumberStyles.AllowHexSpecifier) * MilliLitersToCmFactor * -1.0;
dataMagFluxRec.TimeS = UInt32.Parse(fields[(Int32)ProtLsubString.CpuTime],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
dataMagFluxRec.VolumeCm = dataMagFluxRec.ForwardVolumeCm + dataMagFluxRec.NegativeReverseVolumeCm;
// parse the floating point value of the flow rate which is a string containing the hex value as 32 bit
var hex = UInt32.Parse(fields[(Int32)ProtLsubString.FlowRate], NumberStyles.AllowHexSpecifier);
var floatValues = BitConverter.GetBytes(hex);
dataMagFluxRec.FlowRateCmPh = BitConverter.ToSingle(floatValues, 0) *
LitersPerSecondToCmPerHourFactor;
// get status and identifiers
dataMagFluxRec.StatusBits = UInt32.Parse(fields[(Int32)ProtLsubString.StatusBits],
NumberStyles.AllowHexSpecifier);
// adding the hex identifier, as this is not part of the string
dataMagFluxRec.UniqueId = $"0x{fields[(Int32)ProtLsubString.UniqueId]}";
var decimalSensorId = UInt32.Parse(fields[(Int32)ProtLsubString.SensorId], NumberStyles.AllowHexSpecifier);
dataMagFluxRec.SensorId = decimalSensorId.ToString();
}
/// field position in protocol 'l' (small 'L')
private enum ProtLsubString
{
// Sorted to "@l" field positions.
ProtType,
FwdVolume,
RevVolume,
CpuTime,
FlowRate,
StatusBits,
UniqueId,
SensorId
}
}
}