using System; using System.Collections.Generic; using System.Globalization; using Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.MeasurementRecords; using Xylem.Common.Utils.Crc16Ccitt; // ReSharper disable UnusedMember.Local namespace Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol { /// /// Data fields and definitions for GENESIS streaming protocol /// 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 /// /// Default data for bend detection tests of Genesis /// private readonly BendDetectionRecord _bendDetectionDefault = new BendDetectionRecord { StatusBendU0 = BendDetectionRecord.StatusBendU0Enum.OKAY, InstallationType = BendDetectionRecord.InstallationTypeEnum.INSTALLATION_UNDISTURBED, CorrectionFactor_percent = 0.0, PreCorrectionVolumeRaw = 0.0, PostCorrectionVolumeRaw = 0.0, TimeS = 0.0, OverflowTimeS = CpuTimeOverflowS, Crc = 0xFFFF, IsValid = false }; /// /// Default data for flow tests of Genesis /// private readonly FlowTestRecord _dataDefault = new FlowTestRecord { VolumeCm = 0.0, OverflowVolumeCm = DisplayMlSetupDutOverflowVolumeCm, TimeS = 0.0, OverflowTimeS = CpuTimeOverflowS, Crc = 0xFFFF, IsValid = false }; /// /// Default data for calibration of Genesis /// private readonly CalibrationRecord _rawDataDefault = new CalibrationRecord { Channel = 0, Validation = 0xFFFF, TotalTimeOfFlightS = 0.0, DeltaTimeOfFlightS = 0.0, VolumeScaleRawPerMl = DefaultVolumeScaleRawPerMl, VolumeFactorRawToQm = DefaultVolumeFactorRawToCm, DeltaVolumeRaw = 0.0, DeltaVolumeQm = 0.0, AccuVolumeRaw = 0.0, VolumeCm = 0.0, OverflowVolumeCm = DefaultAccuDutOverflowVolumeCm, SampleIntervalS = 0.0, AmplitudeUpV = 0.0, AmplitudeDownV = 0.0, PulseWidthRatioUp = 0.0, PulseWidthRatioDown = 0.0, TemperatureRaw = 20.0, TemperaturePowFactor = 1.0, TemperatureDegC = 20.0, TimeS = 0.0, OverflowTimeS = CpuTimeOverflowS, Crc = 0xFFFF, IsValid = false }; private CalibrationRecord _dataCalibRec; private FlowTestRecord _dataFlowTestRec; private BendDetectionRecord _dataBendDetectRec; private readonly Boolean _ignoreCorruptedData; /// /// Constructor initializes all decoded members with default values /// public StreamingDecoder(Boolean ignoreCorruptedData = true) { _dataFlowTestRec = _dataDefault; _dataCalibRec = _rawDataDefault; _dataBendDetectRec = _bendDetectionDefault; _ignoreCorruptedData = ignoreCorruptedData; } /// /// Calibration data /// public CalibrationRecord DataCalib { get; private set; } /// /// Flow test data /// public FlowTestRecord DataFlowTest { get; private set; } /// /// Bend detection test data /// public BendDetectionRecord DataBendDetectTest { get; private set; } /// /// Decoding the raw message /// /// message received as one line delimited with LF /// true if decoding was successful and data has been validated /// /// - Modified using common CRC check before branching to the protocol specific decoder. /// /// /// - Introduced protocol 'm' for bending detection. /// public Boolean DecodeMsg(String rawMsg) { var rawRecordIsValid = false; try { // save the raw message for CRC calculation before separation to fields //_rawMsgForCrc = rawMsg; // extract message and split it to fields //DN50 //2022-07-21 07:22:06.9871 | @f 8497D 062E4216 9B2A //2022-07-21 07:22:06.9871 | @h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331 //2022-07-21 07:22:07.0171 | @h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD //rawMsg = "@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E"; //DN80 //2022-04-28 15:19:54.9167 | @f AA754B 4D0CEE78 5D89 //2022-04-28 15:19:54.9337 | @h 1 0 0EF4A130 0002AAB8 000DAC8C 02B98FBD 00000200 00000FFC 643BCF30 63C9CFF6 00015096 0C 4D0CF3CC 3E87 //2022-04-28 15:19:54.9497 | @h 2 0 0EFA3000 000283E7 000CE580 E3D5EFFA 00000200 00001000 6DC0A84E 6CD462C2 00015096 0C 4D0CF922 1646 //2022-04-28 15:19:54.9627 | @h 3 0 0EFF7F91 0002AC39 000DB43D FE1C0254 00000200 00000FFE 6C932DE6 6D20005D 00015096 0C 4D0CFE76 868A //2022-04-28 15:19:54.9787 | @f AA7C01 4D0CFE76 B08F // rawMsg = "@h 3 0 0EFF7F91 0002AC39 000DB43D FE1C0254 00000200 00000FFE 6C932DE6 6D20005D 00015096 0C 4D0CFE76 868A "; // 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 "@m": _dataBendDetectRec.IsValid = rawRecordIsValid; if (rawRecordIsValid || !_ignoreCorruptedData) { DecodeProtocolM(ref _dataBendDetectRec, rawMsgFields); DataBendDetectTest = _dataBendDetectRec; } break; case "@f": _dataFlowTestRec.IsValid = rawRecordIsValid; if (rawRecordIsValid || !_ignoreCorruptedData) { DecodeProtocolF(ref _dataFlowTestRec, rawMsgFields); DataFlowTest = _dataFlowTestRec; } break; case "@g": _dataCalibRec.IsValid = rawRecordIsValid; if (rawRecordIsValid || !_ignoreCorruptedData) { DecodeProtocolG(ref _dataCalibRec, rawMsgFields); DataCalib = _dataCalibRec; } break; case "@h": _dataCalibRec.IsValid = rawRecordIsValid; if (rawRecordIsValid || !_ignoreCorruptedData) { DecodeProtocolH(ref _dataCalibRec, rawMsgFields); DataCalib = _dataCalibRec; } break; } } catch (Exception) { // ignored } return rawRecordIsValid; } /// /// Extracting message from string fields for protocol 'm' /// /// reference to bend detection test record /// Separated fields containing the measurement as string /// /// /// - Modified using common CRC check in advance. /// private static void DecodeProtocolM(ref BendDetectionRecord dataBendTestRec, IList fields) { // time stamp attachment dataBendTestRec.DecodedTime = DateTimeOffset.UtcNow; // extract received CRC dataBendTestRec.Crc = ushort.Parse(fields[(Int32)ProtMsubString.Crc], NumberStyles.HexNumber); // extract the status dataBendTestRec.StatusBendU0 = (BendDetectionRecord.StatusBendU0Enum)UInt32.Parse(fields[(Int32)ProtMsubString.Status], NumberStyles.AllowHexSpecifier); // extract the installation type dataBendTestRec.InstallationType = (BendDetectionRecord.InstallationTypeEnum)UInt32.Parse(fields[(Int32)ProtMsubString.Installation], NumberStyles.AllowHexSpecifier); // extract the correction factor in percent dataBendTestRec.CorrectionFactor_percent = UInt32.Parse(fields[(Int32)ProtMsubString.Factor], NumberStyles.AllowHexSpecifier) * BendDetectionRecord.CorrectionFactorScale; // build result values, the volume before and after correction can be positive or negative! dataBendTestRec.PreCorrectionVolumeRaw = Int32.Parse(fields[(Int32)ProtMsubString.PreVolume], NumberStyles.AllowHexSpecifier); // build result values, the volume before and after correction can be positive or negative! dataBendTestRec.PostCorrectionVolumeRaw = Int32.Parse(fields[(Int32)ProtMsubString.PostVolume], NumberStyles.AllowHexSpecifier); } /// /// Extracting message from string fields for protocol 'f' /// /// reference to flow test record /// Separated fields containing the measurement as string /// /// /// - Modified using common CRC check in advance. /// private static void DecodeProtocolF(ref FlowTestRecord dataFlowTestRec, IList fields) { // time stamp attachment dataFlowTestRec.DecodedTime = DateTimeOffset.UtcNow; // extract received CRC dataFlowTestRec.Crc = ushort.Parse(fields[(Int32)ProtFsubString.Crc], NumberStyles.HexNumber); // build result values, the display volume can be positive or negative! dataFlowTestRec.VolumeCm = int.Parse(fields[(Int32)ProtFsubString.DisplayVolume], NumberStyles.AllowHexSpecifier) * MilliLitersToCmFactor; dataFlowTestRec.TimeS = uint.Parse(fields[(Int32)ProtFsubString.CpuTime], NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; } /// /// Extracting message from string fields to individual raw channel for protocol 'g' /// /// Reference to result structure for raw data for one channel /// Separated fields containing the measurement as string /// true if protocol is valid /// /// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm /// /// /// - Modified using common CRC check in advance. /// private static void DecodeProtocolG(ref CalibrationRecord dataProtGRec, IList fields) { // time stamp attachment dataProtGRec.DecodedTime = DateTimeOffset.UtcNow; // extract received CRC dataProtGRec.Crc = ushort.Parse(fields[(Int32)ProtGsubString.Crc], NumberStyles.HexNumber); dataProtGRec.Channel = ushort.Parse(fields[(Int32)ProtGsubString.ChanNo], NumberStyles.HexNumber); dataProtGRec.Validation = ushort.Parse(fields[(Int32)ProtGsubString.Validation], NumberStyles.HexNumber); // Delta time of flight dataProtGRec.DeltaTimeOfFlightS = int.Parse(fields[(Int32)ProtGsubString.Dtof], NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit; // Delta raw volume between last sample dataProtGRec.DeltaVolumeRaw = uint.Parse(fields[(Int32)ProtGsubString.RawDVolume], NumberStyles.AllowHexSpecifier); // volume scaling var volumeRawScale = uint.Parse(fields[(Int32)ProtGsubString.VolumeScale], NumberStyles.AllowHexSpecifier); dataProtGRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl; dataProtGRec.VolumeFactorRawToQm = MilliLitersToCmFactor / dataProtGRec.VolumeScaleRawPerMl; dataProtGRec.OverflowVolumeCm = MaxGenesisAccuVolumeRaw * dataProtGRec.VolumeFactorRawToQm; // Calculate volume in cubic meters out of the raw volume dataProtGRec.DeltaVolumeQm = dataProtGRec.DeltaVolumeRaw * dataProtGRec.VolumeFactorRawToQm; // accumulated volume for each channel received from water meter scaled with volumeScale // the volume can just be positive dataProtGRec.AccuVolumeRaw = uint.Parse(fields[(Int32)ProtGsubString.AccuVolume], NumberStyles.AllowHexSpecifier); dataProtGRec.VolumeCm = dataProtGRec.AccuVolumeRaw * dataProtGRec.VolumeFactorRawToQm; // Sample interval dataProtGRec.SampleIntervalS = uint.Parse(fields[(Int32)ProtGsubString.SampleInterval], NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; // amplitude for high threshold in V dataProtGRec.AmplitudeUpV = uint.Parse(fields[(Int32)ProtGsubString.AmplitudeUp], NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; // amplitude for low threshold in V dataProtGRec.AmplitudeDownV = uint.Parse(fields[(Int32)ProtGsubString.AmplitudeDown], NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; // pulse width ratio high threshold dataProtGRec.PulseWidthRatioUp = uint.Parse(fields[(Int32)ProtGsubString.PulseWidthRatioUp], NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor; // pulse width ratio low threshold dataProtGRec.PulseWidthRatioDown = uint.Parse(fields[(Int32)ProtGsubString.PulseWidthRatioDown], NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor; // raw temperature dataProtGRec.TemperatureRaw = int.Parse(fields[(Int32)ProtGsubString.RawTemperature], NumberStyles.AllowHexSpecifier); // temperature scaling dataProtGRec.TemperaturePowFactor = uint.Parse(fields[(Int32)ProtGsubString.TemperatureScale], NumberStyles.AllowHexSpecifier); // Calculate temperature dataProtGRec.TemperatureDegC = dataProtGRec.TemperatureRaw / Math.Pow(2.0, dataProtGRec.TemperaturePowFactor); // absolute CPU time, started at LED mode 3 activation dataProtGRec.TimeS = uint.Parse(fields[(Int32)ProtGsubString.CpuTime], NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; } /// /// Extracting message from string fields to individual raw channel for protocol 'g' /// /// Reference to result structure for raw data for one channel /// Separated fields containing the measurement as string /// true if protocol is valid /// /// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm /// /// /// - Modified using common CRC check in advance. /// private static void DecodeProtocolH(ref CalibrationRecord dataProtHRec, IList fields) { // time stamp attachment dataProtHRec.DecodedTime = DateTimeOffset.UtcNow; // extract received CRC dataProtHRec.Crc = ushort.Parse(fields[(Int32)ProtHsubString.Crc], NumberStyles.HexNumber); dataProtHRec.Channel = ushort.Parse(fields[(Int32)ProtHsubString.ChanNo], NumberStyles.HexNumber); dataProtHRec.Validation = ushort.Parse(fields[(Int32)ProtHsubString.Validation], NumberStyles.HexNumber); // Total time of flight dataProtHRec.TotalTimeOfFlightS = int.Parse(fields[(Int32)ProtHsubString.Ttof], NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit; // Delta time of flight dataProtHRec.DeltaTimeOfFlightS = int.Parse(fields[(Int32)ProtHsubString.Dtof], NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit; dataProtHRec.RawTotalTimeOfFlight = int.Parse(fields[(Int32)ProtHsubString.Ttof], NumberStyles.AllowHexSpecifier); dataProtHRec.RawDeltaTimeOfFlight = int.Parse(fields[(Int32)ProtHsubString.Dtof], NumberStyles.AllowHexSpecifier); // Delta raw volume between two samples dataProtHRec.DeltaVolumeRaw = uint.Parse(fields[(Int32)ProtHsubString.RawDVolume], NumberStyles.AllowHexSpecifier); // volume scaling var volumeRawScale = uint.Parse(fields[(Int32)ProtHsubString.VolumeScale], NumberStyles.AllowHexSpecifier); dataProtHRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl; dataProtHRec.VolumeFactorRawToQm = MilliLitersToCmFactor / dataProtHRec.VolumeScaleRawPerMl; dataProtHRec.OverflowVolumeCm = MaxGenesisAccuVolumeRaw * dataProtHRec.VolumeFactorRawToQm; // Calculate volume in cubic meters out of the raw volume dataProtHRec.DeltaVolumeQm = dataProtHRec.DeltaVolumeRaw * dataProtHRec.VolumeFactorRawToQm; // accumulated volume for each channel received from water meter scaled with volumeScale // the volume can just be positive dataProtHRec.AccuVolumeRaw = uint.Parse(fields[(Int32)ProtHsubString.AccuVolume], NumberStyles.AllowHexSpecifier); dataProtHRec.VolumeCm = dataProtHRec.AccuVolumeRaw * dataProtHRec.VolumeFactorRawToQm; // Sample interval dataProtHRec.SampleIntervalS = uint.Parse(fields[(Int32)ProtHsubString.SampleInterval], NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; // amplitude for high threshold in V dataProtHRec.AmplitudeUpV = uint.Parse(fields[(Int32)ProtHsubString.AmplitudeUp], NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; // amplitude for low threshold in V dataProtHRec.AmplitudeDownV = uint.Parse(fields[(Int32)ProtHsubString.AmplitudeDown], NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; // raw temperature dataProtHRec.TemperatureRaw = int.Parse(fields[(Int32)ProtHsubString.RawTemperature], NumberStyles.AllowHexSpecifier); // temperature scaling dataProtHRec.TemperaturePowFactor = uint.Parse(fields[(Int32)ProtHsubString.TemperatureScale], NumberStyles.AllowHexSpecifier); // Calculate temperature dataProtHRec.TemperatureDegC = dataProtHRec.TemperatureRaw / ( Math.Pow(2.0, dataProtHRec.TemperaturePowFactor)); // absolute CPU time, started at LED mode 3 activation dataProtHRec.TimeS = uint.Parse(fields[(Int32)ProtHsubString.CpuTime], NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; } /// field position in protocol 'f' private enum ProtFsubString { //do not remove this needed for position in record ProtType, DisplayVolume, CpuTime, Crc } /// field position in protocol 'm' private enum ProtMsubString { //do not remove this needed for position in record ProtType, Status, Installation, Factor, PreVolume, PostVolume, Crc } /// field position in protocol 'g' private enum ProtGsubString { //do not remove this needed for position in record ProtType, ChanNo, Validation, Dtof, RawDVolume, AccuVolume, VolumeScale, SampleInterval, AmplitudeUp, AmplitudeDown, PulseWidthRatioUp, PulseWidthRatioDown, RawTemperature, TemperatureScale, CpuTime, Crc } /// field position in protocol 'h' private enum ProtHsubString { //do not remove this needed for position in record ProtType, ChanNo, Validation, Ttof, Dtof, RawDVolume, AccuVolume, VolumeScale, SampleInterval, AmplitudeUp, AmplitudeDown, RawTemperature, TemperatureScale, CpuTime, Crc } } }