Files
laatzen/Common/Hardware/WaterMeter/Genesis/Protocols/StreamingProtocol/StreamingDecoder.cs
T

482 lines
20 KiB
C#

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
{
/// <summary>
/// Data fields and definitions for GENESIS streaming protocol
/// </summary>
public class StreamingDecoder
{
private const Double MilliLitersToQmFactor = 1.0E-6;
private const Double DefaultVolumeScaleRawPerMl = 1024.0;
private const Double DefaultVolumeFactorRawToQm = MilliLitersToQmFactor /
DefaultVolumeScaleRawPerMl;
private const Double MaxAccuVolumeRaw = 0x100000000; //2^32
private const Double DefaultAccuDutOverflowVolumeCm = MaxAccuVolumeRaw *
DefaultVolumeFactorRawToQm;
private const Double CpuTimeToSecondsFactor = 1.0 / 0x10000;
private const Double CpuTimeOverflowS = 0x100000000 * CpuTimeToSecondsFactor;
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
private const Double PulseWidthToRelFactor = 1.0 / 0x100; // 2^8
private const UInt16 IsValidProtG = 1;
private const UInt16 IsValidProtH = 0;
private CalibrationRecord _dataCalibRec;
/// <summary>
/// Default data for
/// </summary>
private readonly FlowTestRecord _dataDefault = new FlowTestRecord
{
VolumeCm = 0.0,
OverflowVolumeCm = DisplayMlSetupDutOverflowVolumeCm,
TimeS = 0.0,
OverflowTimeS = CpuTimeOverflowS,
Crc = 0xFFFF,
IsValid = false
};
private readonly CalibrationRecord _rawDataDefault = new CalibrationRecord
{
Channel = 0,
Validation = 0xFFFF,
TotalTimeOfFlightS = 0.0,
DeltaTimeOfFlightS = 0.0,
VolumeScaleRawPerMl = DefaultVolumeScaleRawPerMl,
VolumeFactorRawToQm = DefaultVolumeFactorRawToQm,
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 readonly FlowTestRecord _dataFlowTestRec;
private String _rawMsgForCrc;
/// <summary>
/// Calibration data
/// </summary>
public CalibrationRecord DataCalib = new CalibrationRecord();
private readonly Boolean _ignoreCorruptedData;
/// <summary>
/// Constructor initializes all decoded members with default values
/// </summary>
public StreamingDecoder(Boolean ignoreCorruptedData = true)
{
_dataFlowTestRec = _dataDefault;
_dataCalibRec = _rawDataDefault;
_ignoreCorruptedData = ignoreCorruptedData;
}
/// <summary>
/// Flow test data
/// </summary>
public FlowTestRecord DataFlowTest { get; private set; }
/// <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>
public Boolean DecodeMsg(String rawMsg)
{
var isSuccessfulDecoded = false;
try
{
// save the raw message for CRC calculation before separation to fields
_rawMsgForCrc = rawMsg;
// extract message and split it to fields
//Dn 50
//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 ";
rawMsg = rawMsg.Replace('\n', ' ');
var field = rawMsg.Split(' ');
switch (field[0])
{
case "@f":
isSuccessfulDecoded = DecodeProtocolF(field);
if (isSuccessfulDecoded || !_ignoreCorruptedData)
{
DataFlowTest = _dataFlowTestRec;
}
break;
case "@g":
{
isSuccessfulDecoded = DecodeProtocolG(ref _dataCalibRec, field);
if (isSuccessfulDecoded || !_ignoreCorruptedData)
{
DataCalib = _dataCalibRec;
}
break;
}
case "@h":
{
isSuccessfulDecoded = DecodeProtocolH(ref _dataCalibRec, field);
if (isSuccessfulDecoded || !_ignoreCorruptedData)
{
DataCalib = _dataCalibRec;
}
break;
}
}
}
catch (Exception)
{
// ignored
}
return isSuccessfulDecoded;
}
/// <summary>
/// Extracting message from string fields for protocol 'f'
/// </summary>
/// <param name="field">Separated fields containing the measurement as string</param>
/// <returns></returns>
private Boolean DecodeProtocolF(IList<String> field)
{
_dataFlowTestRec.DecodedTime = DateTimeOffset.UtcNow;
_dataFlowTestRec.IsValid = true;
//extract bytes of raw message for CRC calculation each character removing CRC (4 char)
var byteArraySize = _rawMsgForCrc.Length - 4;
var byteArray = new Byte[byteArraySize];
for (var i = 0; i < byteArraySize; i++)
{
byteArray[i] = (Byte)_rawMsgForCrc[i];
}
_rawMsgForCrc = null;
var calculatedCrc = Crc16Ccitt.CalculateMsb1021(byteArray);
//extract received CRC
_dataFlowTestRec.Crc = ushort.Parse(field[(Int32)ProtFsubString.Crc],
NumberStyles.HexNumber);
//compare received with calculated CRC
if (calculatedCrc != _dataFlowTestRec.Crc)
{
_dataFlowTestRec.IsValid = false;
return false;
}
_dataFlowTestRec.IsValid = true;
//build result values, the display volume can be positive or negative!
_dataFlowTestRec.VolumeCm = int.Parse(field[(Int32)ProtFsubString.DisplayVolume],
NumberStyles.AllowHexSpecifier) * MilliLitersToQmFactor;
_dataFlowTestRec.TimeS = uint.Parse(field[(Int32)ProtFsubString.CpuTime],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
return _dataFlowTestRec.IsValid;
}
/// <summary>
/// Extracting message from string fields to individual raw channel for protocol 'g'
/// </summary>
/// <param name="dataProtGRec">Reference to result structure for raw data for one channel</param>
/// <param name="field">Separated fields containing the measurement as string</param>
/// <returns>true if protocol is valid</returns>
/// <remarks date="2018-Mar-22" author="T.Wiedebusch">
/// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm
/// </remarks>
private Boolean DecodeProtocolG(ref CalibrationRecord dataProtGRec, IList<String> field)
{
dataProtGRec.DecodedTime = DateTimeOffset.UtcNow;
dataProtGRec.IsValid = false;
//extract bytes of raw message for CRC calculation each character removing CRC (4 char)
var byteArraySize = _rawMsgForCrc.Length - 4;
var byteArray = new Byte[byteArraySize];
for (var i = 0; i < byteArraySize; i++)
{
byteArray[i] = (Byte)_rawMsgForCrc[i];
}
_rawMsgForCrc = null;
var calculatedCrc = Crc16Ccitt.CalculateMsb1021(byteArray);
//extract received CRC
dataProtGRec.Crc = ushort.Parse(field[(Int32)ProtGsubString.Crc],
NumberStyles.HexNumber);
dataProtGRec.Channel = ushort.Parse(field[(Int32)ProtGsubString.ChanNo],
NumberStyles.HexNumber);
dataProtGRec.Validation = ushort.Parse(field[(Int32)ProtGsubString.Validation],
NumberStyles.HexNumber);
//compare received with calculated CRC
if (calculatedCrc != dataProtGRec.Crc || IsValidProtG != dataProtGRec.Validation)
{
return false;
}
//decode rest of message only if CRC and mode are valid
dataProtGRec.IsValid = true;
// Delta time of flight
dataProtGRec.DeltaTimeOfFlightS = int.Parse(field[(Int32)ProtGsubString.Dtof],
NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit;
// Delta raw volume between last sample
dataProtGRec.DeltaVolumeRaw = uint.Parse(field[(Int32)ProtGsubString.RawDVolume],
NumberStyles.AllowHexSpecifier);
//volume scaling
var volumeRawScale = uint.Parse(field[(Int32)ProtGsubString.VolumeScale],
NumberStyles.AllowHexSpecifier);
dataProtGRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl;
dataProtGRec.VolumeFactorRawToQm = MilliLitersToQmFactor / dataProtGRec.VolumeScaleRawPerMl;
dataProtGRec.OverflowVolumeCm = MaxAccuVolumeRaw * 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(field[(Int32)ProtGsubString.AccuVolume],
NumberStyles.AllowHexSpecifier);
dataProtGRec.VolumeCm = dataProtGRec.AccuVolumeRaw * dataProtGRec.VolumeFactorRawToQm;
//Sample interval
dataProtGRec.SampleIntervalS = uint.Parse(field[(Int32)ProtGsubString.SampleInterval],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
//amplitude for high threshold in V
dataProtGRec.AmplitudeUpV = uint.Parse(field[(Int32)ProtGsubString.AmplitudeUp],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//amplitude for low threshold in V
dataProtGRec.AmplitudeDownV = uint.Parse(field[(Int32)ProtGsubString.AmplitudeDown],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//pulse width ratio high threshold
dataProtGRec.PulseWidthRatioUp = uint.Parse(field[(Int32)ProtGsubString.PulseWidthRatioUp],
NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor;
//pulse width ratio low threshold
dataProtGRec.PulseWidthRatioDown = uint.Parse(field[(Int32)ProtGsubString.PulseWidthRatioDown],
NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor;
//raw temperature
dataProtGRec.TemperatureRaw = int.Parse(field[(Int32)ProtGsubString.RawTemperature],
NumberStyles.AllowHexSpecifier);
//temperature scaling
dataProtGRec.TemperaturePowFactor = uint.Parse(field[(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(field[(Int32)ProtGsubString.CpuTime],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
return true;
}
/// <summary>
/// Extracting message from string fields to individual raw channel for protocol 'g'
/// </summary>
/// <param name="dataProtHRec">Reference to result structure for raw data for one channel</param>
/// <param name="field">Separated fields containing the measurement as string</param>
/// <returns>true if protocol is valid</returns>
/// <remarks date="2018-Mar-22" author="T.Wiedebusch">
/// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm
/// </remarks>
private Boolean DecodeProtocolH(ref CalibrationRecord dataProtHRec, IList<String> field)
{
dataProtHRec.DecodedTime = DateTimeOffset.UtcNow;
dataProtHRec.IsValid = false;
//extract bytes of raw message for CRC calculation each character removing CRC (4 char)
var byteArraySize = _rawMsgForCrc.Length - 4;
var byteArray = new Byte[byteArraySize];
for (var i = 0; i < byteArraySize; i++)
{
byteArray[i] = (Byte)_rawMsgForCrc[i];
}
_rawMsgForCrc = null;
var calculatedCrc = Crc16Ccitt.CalculateMsb1021(byteArray);
//extract received CRC
dataProtHRec.Crc = ushort.Parse(field[(Int32)ProtHsubString.Crc],
NumberStyles.HexNumber);
dataProtHRec.Channel = ushort.Parse(field[(Int32)ProtHsubString.ChanNo],
NumberStyles.HexNumber);
dataProtHRec.Validation = ushort.Parse(field[(Int32)ProtHsubString.Validation],
NumberStyles.HexNumber);
//compare received with calculated CRC
if (_ignoreCorruptedData &&
(calculatedCrc != dataProtHRec.Crc || IsValidProtH != dataProtHRec.Validation))
{
return false;
}
// decode rest of message only if CRC and mode are valid
dataProtHRec.IsValid = true;
// Total time of flight
dataProtHRec.TotalTimeOfFlightS = int.Parse(field[(Int32)ProtHsubString.Ttof],
NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit;
//Delta time of flight
dataProtHRec.DeltaTimeOfFlightS = int.Parse(field[(Int32)ProtHsubString.Dtof],
NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit;
dataProtHRec.RAWTotalTimeOfFlight =
int.Parse(field[(Int32)ProtHsubString.Ttof], NumberStyles.AllowHexSpecifier);
dataProtHRec.RAWDeltaTimeOfFlight = int.Parse(field[(Int32)ProtHsubString.Dtof], NumberStyles.AllowHexSpecifier);
//Delta raw volume between two samples
dataProtHRec.DeltaVolumeRaw = uint.Parse(field[(Int32)ProtHsubString.RawDVolume],
NumberStyles.AllowHexSpecifier);
//volume scaling
var volumeRawScale = uint.Parse(field[(Int32)ProtHsubString.VolumeScale],
NumberStyles.AllowHexSpecifier);
dataProtHRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl;
dataProtHRec.VolumeFactorRawToQm = MilliLitersToQmFactor / dataProtHRec.VolumeScaleRawPerMl;
dataProtHRec.OverflowVolumeCm = MaxAccuVolumeRaw * 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(field[(Int32)ProtHsubString.AccuVolume],
NumberStyles.AllowHexSpecifier);
dataProtHRec.VolumeCm = dataProtHRec.AccuVolumeRaw * dataProtHRec.VolumeFactorRawToQm;
//Sample interval
dataProtHRec.SampleIntervalS = uint.Parse(field[(Int32)ProtHsubString.SampleInterval],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
//amplitude for high threshold in V
dataProtHRec.AmplitudeUpV = uint.Parse(field[(Int32)ProtHsubString.AmplitudeUp],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//amplitude for low threshold in V
dataProtHRec.AmplitudeDownV = uint.Parse(field[(Int32)ProtHsubString.AmplitudeDown],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//raw temperature
dataProtHRec.TemperatureRaw = int.Parse(field[(Int32)ProtHsubString.RawTemperature],
NumberStyles.AllowHexSpecifier);
//temperature scaling
dataProtHRec.TemperaturePowFactor = uint.Parse(field[(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(field[(Int32)ProtHsubString.CpuTime],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
return true;
}
/// 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 '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
}
}
}