MagFlux: - Initial streaming protocol with Crc16Ccitt.ModbudRtuLsbA001 implementation

This commit is contained in:
Thomas Wiedebusch
2023-03-09 15:05:04 +01:00
parent b19e0fb462
commit b8f0b97a2f
6 changed files with 342 additions and 313 deletions
+1
View File
@@ -49,6 +49,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=METROLOGYASST/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=METROLOGYASST/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mettbl/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=mettbl/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Milli/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Milli/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Modbus/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCBID/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=PCBID/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=perc/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=perc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Preadjustment/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Preadjustment/@EntryIndexedValue">True</s:Boolean>
@@ -221,6 +221,7 @@
<Compile Include="EventArguments\RegisterUpdateEventArgs.cs" /> <Compile Include="EventArguments\RegisterUpdateEventArgs.cs" />
<Compile Include="EventArguments\RequestResponseDataEventArgs.cs" /> <Compile Include="EventArguments\RequestResponseDataEventArgs.cs" />
<Compile Include="MeasurementRecords\CalibrationRecord.cs" /> <Compile Include="MeasurementRecords\CalibrationRecord.cs" />
<Compile Include="MeasurementRecords\MagFluxRecord.cs" />
<Compile Include="MeasurementRecords\FlowTestRecord.cs" /> <Compile Include="MeasurementRecords\FlowTestRecord.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RadioConfigurationParams.cs" /> <Compile Include="RadioConfigurationParams.cs" />
@@ -1,130 +1,4 @@
//using System; using System;
//using System.Text;
//using Xylem.Common.CommonCore.Consts;
//using Xylem.Common.Metrology.Measurements;
//namespace Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.MeasurementRecords
//{
// /// <inheritdoc />
// /// <summary>
// /// struct to hold Led record for protocol f (contains measurement record)
// /// </summary>
// public class FlowTestRecord : IMeasurementRecord
// {
// /// <summary>
// /// actual volume in cubic meters
// /// </summary>
// public Double DisplayVolumeQm;
// /// <summary>
// /// Overflow volume at Display
// /// </summary>
// public Double DisplayDutOverflowVolumeCm;
// /// <summary>
// /// time converted to seconds
// /// </summary>
// public Double TimeS;
// /// <summary>
// /// Overflow time
// /// </summary>
// public Double OverflowTimeS;
// /// <summary>
// /// CRC16 CCITT
// /// </summary>
// public UInt16 Crc;
// /// <summary>
// /// record is valid
// /// </summary>
// public Boolean IsValid;
// /// <summary>
// /// Mark package as start, end, intermediate or not sync
// /// </summary>
// public SyncMarkRecord SyncMarkRecord;
// /// <summary>
// /// time when package was decoded
// /// </summary>
// public DateTimeOffset DecodedTime;
// /// <summary>
// /// time when package was received
// /// </summary>
// public DateTimeOffset ReceivedTime;
// public Double VolumeCm { get; set; }
// public Double OverflowVolumeCm { get; set; }
// /// <summary>
// /// Get result as string
// /// </summary>
// /// <returns></returns>
// public override String ToString()
// {
// var sb = new StringBuilder();
// sb = sb.Append($"DisplayVolumeQm={DisplayVolumeQm}").Append(",")
// .Append($"DisplayDutOverflowVolumeCm={DisplayDutOverflowVolumeCm}").Append(",")
// .Append($"TimeS={TimeS}").Append(",")
// .Append($"OverflowTimeS={OverflowTimeS}").Append(",")
// .Append($"CRC={Crc}").Append(",")
// .Append($"IsValid={IsValid}").Append(",")
// .Append($"ReceivedTimeUtc={ReceivedTime}").Append(",")
// .Append($"DecodedTimeUtc={DecodedTime}").Append(",")
// .Append($"SyncMarkRecord={SyncMarkRecord}");
// return sb.ToString();
// }
// /// <inheritdoc />
// public Double GetVolumeCm()
// {
// return DisplayVolumeQm;
// }
// /// <inheritdoc />
// public Double GetTimeS()
// {
// return TimeS;
// }
// /// <inheritdoc />
// public SyncMarkRecord GetDataSyncMark()
// {
// return SyncMarkRecord;
// }
// /// <inheritdoc />
// public Int32 GetChannel()
// {
// //channel 0 is for the entire device without a physical channel assignment
// return 0;
// }
// /// <inheritdoc />
// public Double GetOverflowVolumeCm()
// {
// return DisplayDutOverflowVolumeCm;
// }
// /// <inheritdoc />
// public Double GetOverflowTimeS()
// {
// return OverflowTimeS;
// }
// /// <inheritdoc />
// public Object Clone()
// {
// return MemberwiseClone();
// }
// }
//}
using System;
using System.Text; using System.Text;
using Xylem.Common.Metrology.Measurements; using Xylem.Common.Metrology.Measurements;
@@ -0,0 +1,58 @@
using System;
using System.Text;
using Xylem.Common.Metrology.Measurements;
namespace Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.MeasurementRecords
{
/// <inheritdoc />
/// <summary>
/// struct to hold Led record for protocol L (contains measurement record)
/// </summary>
public class MagFluxRecord : MeasurementRecord
{
/// <summary>
/// Flow rate in cubic meters per hour
/// </summary>
public Double FlowRateCmPh;
/// <summary>
/// Sensor Id of MagFlux
/// </summary>
public String SensorId;
/// <summary>
/// Serial number of MagFlux
/// </summary>
public String UniqueId;
/// <summary>
/// Status of sensor and electronics
/// </summary>
public UInt32 StatusBits;
private const String StringDelimiter = "; \n";
/// <summary>
/// Get result as string
/// </summary>
/// <returns></returns>
public override String ToString()
{
var sb = new StringBuilder();
sb = sb.Append($"Volume [m³]: {VolumeCm}").Append(StringDelimiter)
.Append($"CPU Time [s]: {TimeS}").Append(StringDelimiter)
.Append($"FlowRate [m³/h]: {FlowRateCmPh}").Append(StringDelimiter)
.Append($"Status: {StatusBits:X8}").Append(StringDelimiter)
.Append($"UniqueId: {UniqueId}").Append(StringDelimiter)
.Append($"SensorSN: {SensorId}").Append(StringDelimiter)
.Append($"CRC: {Crc:X4}").Append(StringDelimiter)
.Append($"OverflowVolume [m³]: {OverflowVolumeCm}").Append(StringDelimiter)
.Append($"OverflowTime [s]: {OverflowTimeS}").Append(StringDelimiter)
.Append($"IsValid: {IsValid}").Append(StringDelimiter)
.Append($"ReceivedTime [UTC]: {ReceivedTime}").Append(StringDelimiter)
.Append($"DecodedTime [UTC]: {DecodedTime}").Append(StringDelimiter)
.Append($"SyncMarkRecord: {SyncMarkRecord}");
return sb.ToString();
}
}
}
@@ -16,21 +16,33 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol
{ {
private const Double MilliLitersToQmFactor = 1.0E-6; private const Double MilliLitersToQmFactor = 1.0E-6;
private const Double DefaultVolumeScaleRawPerMl = 1024.0; private const Double DefaultVolumeScaleRawPerMl = 1024.0;
private const Double DefaultVolumeFactorRawToQm = MilliLitersToQmFactor / private const Double DefaultVolumeFactorRawToQm = MilliLitersToQmFactor / DefaultVolumeScaleRawPerMl;
DefaultVolumeScaleRawPerMl;
private const Double MaxAccuVolumeRaw = 0x100000000; //2^32 private const Double MaxAccuVolumeRaw = 0x100000000; //2^32
private const Double DefaultAccuDutOverflowVolumeCm = MaxAccuVolumeRaw * private const Double DefaultAccuDutOverflowVolumeCm = MaxAccuVolumeRaw * DefaultVolumeFactorRawToQm;
DefaultVolumeFactorRawToQm;
private const Double CpuTimeToSecondsFactor = 1.0 / 0x10000; private const Double CpuTimeToSecondsFactor = 1.0 / 0x10000;
private const Double CpuTimeOverflowS = 0x100000000 * CpuTimeToSecondsFactor; private const Double CpuTimeOverflowS = 0x100000000 * CpuTimeToSecondsFactor;
private const Double DisplayMlSetupDutOverflowVolumeCm = 1000.0; //overflow of LCD if set to ml 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 TofToSecondsFactor38Bit = 1.0 / 0x4000000000; // 2^38
private const Double AmplitudeToVoltFactor = 1.0 / 0x400000 / 1000.0; // 2^22 private const Double AmplitudeToVoltFactor = 1.0 / 0x400000 / 1000.0; // 2^22
private const Double PulseWidthToRelFactor = 1.0 / 0x100; // 2^8 private const Double PulseWidthToRelFactor = 1.0 / 0x100; // 2^8
private const UInt16 IsValidProtG = 1; private const Double LitersPerSecondToCmPerHourFactor = 3600.0 / 1000.0;
private const UInt16 IsValidProtH = 0;
private CalibrationRecord _dataCalibRec; /// <summary>
/// Default data for
/// </summary>
private readonly MagFluxRecord _dataMagFluxDefault = new MagFluxRecord
{
VolumeCm = 0.0,
TimeS = 0.0,
FlowRateCmPh = 0.0,
StatusBits = 0,
UniqueId = "?",
SensorId = "?",
Crc = 0xFFFF,
OverflowVolumeCm = DisplayMlSetupDutOverflowVolumeCm, //TODO THW it is a UInt64 overflow, please change
OverflowTimeS = CpuTimeOverflowS,
IsValid = false
};
/// <summary> /// <summary>
/// Default data for /// Default data for
@@ -74,13 +86,9 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol
IsValid = false IsValid = false
}; };
private readonly FlowTestRecord _dataFlowTestRec; private CalibrationRecord _dataCalibRec;
private FlowTestRecord _dataFlowTestRec;
private String _rawMsgForCrc; private MagFluxRecord _dataMagFluxRec;
/// <summary>
/// Calibration data
/// </summary>
public CalibrationRecord DataCalib = new CalibrationRecord();
private readonly Boolean _ignoreCorruptedData; private readonly Boolean _ignoreCorruptedData;
/// <summary> /// <summary>
@@ -88,344 +96,397 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol
/// </summary> /// </summary>
public StreamingDecoder(Boolean ignoreCorruptedData = true) public StreamingDecoder(Boolean ignoreCorruptedData = true)
{ {
_dataMagFluxRec = _dataMagFluxDefault;
_dataFlowTestRec = _dataDefault; _dataFlowTestRec = _dataDefault;
_dataCalibRec = _rawDataDefault; _dataCalibRec = _rawDataDefault;
_ignoreCorruptedData = ignoreCorruptedData; _ignoreCorruptedData = ignoreCorruptedData;
} }
/// <summary>
/// Calibration data
/// </summary>
public CalibrationRecord DataCalib
{
get; private set;
}
/// <summary>
/// Data of MagFlux
/// </summary>
public MagFluxRecord DataMagFlux
{
get; private set;
}
/// <summary> /// <summary>
/// Flow test data /// Flow test data
/// </summary> /// </summary>
public FlowTestRecord DataFlowTest { get; private set; } public FlowTestRecord DataFlowTest
{
get; private set;
}
/// <summary> /// <summary>
/// Decoding the raw message /// Decoding the raw message
/// </summary> /// </summary>
/// <param name="rawMsg">message received as one line delimited with LF</param> /// <param name="rawMsg">message received as one line delimited with LF</param>
/// <returns>true if decoding was successful and data has been validated</returns> /// <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>
public Boolean DecodeMsg(String rawMsg) public Boolean DecodeMsg(String rawMsg)
{ {
var isSuccessfulDecoded = false; var rawRecordIsValid = false;
try try
{ {
// save the raw message for CRC calculation before separation to fields // save the raw message for CRC calculation before separation to fields
_rawMsgForCrc = rawMsg; //_rawMsgForCrc = rawMsg;
// extract message and split it to fields // extract message and split it to fields
//MagFlux test string from Jan Bennett 2023-03-09
//rawMsg = "@l 34c21 e6e56 3c40f883 0 3c005d5232500120373235 0 5790"
//Dn 50
//DN50
//2022-07-21 07:22:06.9871 | @f 8497D 062E4216 9B2A //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: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 //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"; //rawMsg = "@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E";
//DN80 //DN80
// 2022-04-28 15:19:54.9167 | @f AA754B 4D0CEE78 5D89 //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.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.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.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 //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 = "@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', ' '); rawMsg = rawMsg.Replace('\n', ' ');
var field = rawMsg.Split(' ');
switch (field[0]) // 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
for (var x = 0; x < rawMsgFields.Length - 1; x++)
{ {
case "@f": rawRecordForCrc += rawMsgFields[x];
isSuccessfulDecoded = DecodeProtocolF(field); // add the field delimiter from raw data
if (isSuccessfulDecoded || !_ignoreCorruptedData) 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];
}
var calculatedCrc = rawMsgFields[0] == "@l" ? // for MagFlux protocol "l"
Crc16Ccitt.ModbusRtuLsbA001(byteArray) :
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":
_dataMagFluxRec.IsValid = rawRecordIsValid;
if (rawRecordIsValid || !_ignoreCorruptedData)
{ {
DecodeProtocolL(ref _dataMagFluxRec, rawMsgFields);
DataMagFlux = _dataMagFluxRec;
}
break;
case "@f":
_dataFlowTestRec.IsValid = rawRecordIsValid;
if (rawRecordIsValid || !_ignoreCorruptedData)
{
DecodeProtocolF(ref _dataFlowTestRec, rawMsgFields);
DataFlowTest = _dataFlowTestRec; DataFlowTest = _dataFlowTestRec;
} }
break; break;
case "@g": case "@g":
_dataCalibRec.IsValid = rawRecordIsValid;
if (rawRecordIsValid || !_ignoreCorruptedData)
{ {
isSuccessfulDecoded = DecodeProtocolG(ref _dataCalibRec, field); DecodeProtocolG(ref _dataCalibRec, rawMsgFields);
if (isSuccessfulDecoded || !_ignoreCorruptedData) DataCalib = _dataCalibRec;
{
DataCalib = _dataCalibRec;
}
break;
} }
break;
case "@h": case "@h":
_dataCalibRec.IsValid = rawRecordIsValid;
if (rawRecordIsValid || !_ignoreCorruptedData)
{ {
DecodeProtocolH(ref _dataCalibRec, rawMsgFields);
DataCalib = _dataCalibRec;
isSuccessfulDecoded = DecodeProtocolH(ref _dataCalibRec, field);
if (isSuccessfulDecoded || !_ignoreCorruptedData)
{
DataCalib = _dataCalibRec;
}
break;
} }
break;
} }
} }
catch (Exception) catch (Exception)
{ {
// ignored // ignored
} }
return isSuccessfulDecoded; 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>
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[(Int32)ProtLsubString.Crc], NumberStyles.HexNumber);
// build result values, the display volume can be positive or negative!
dataMagFluxRec.VolumeCm = UInt64.Parse(fields[(Int32)ProtLsubString.DisplayVolume],
NumberStyles.AllowHexSpecifier) * MilliLitersToQmFactor;
dataMagFluxRec.TimeS = UInt32.Parse(fields[(Int32)ProtLsubString.CpuTime],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
// 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);
dataMagFluxRec.UniqueId = fields[(Int32)ProtLsubString.UniqueId];
dataMagFluxRec.SensorId = fields[(Int32)ProtLsubString.SensorId];
} }
/// <summary> /// <summary>
/// Extracting message from string fields for protocol 'f' /// Extracting message from string fields for protocol 'f'
/// </summary> /// </summary>
/// <param name="field">Separated fields containing the measurement as string</param> /// <param name="dataFlowTestRec">reference to flow test record</param>
/// <param name="fields">Separated fields containing the measurement as string</param>
/// <returns></returns> /// <returns></returns>
private Boolean DecodeProtocolF(IList<String> field) /// <remarks date="2023-Mar-09" author="T.Wiedebusch">
/// - Modified using common CRC check in advance.
/// </remarks>
private static void DecodeProtocolF(ref FlowTestRecord dataFlowTestRec, IList<String> fields)
{ {
_dataFlowTestRec.DecodedTime = DateTimeOffset.UtcNow; // time stamp attachment
_dataFlowTestRec.IsValid = true; dataFlowTestRec.DecodedTime = DateTimeOffset.UtcNow;
//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; // extract received CRC
var calculatedCrc = Crc16Ccitt.CalculateMsb1021(byteArray); dataFlowTestRec.Crc = ushort.Parse(fields[(Int32)ProtFsubString.Crc],
//extract received CRC
_dataFlowTestRec.Crc = ushort.Parse(field[(Int32)ProtFsubString.Crc],
NumberStyles.HexNumber); NumberStyles.HexNumber);
//compare received with calculated CRC // build result values, the display volume can be positive or negative!
if (calculatedCrc != _dataFlowTestRec.Crc) dataFlowTestRec.VolumeCm = int.Parse(fields[(Int32)ProtFsubString.DisplayVolume],
{ NumberStyles.AllowHexSpecifier) * MilliLitersToQmFactor;
_dataFlowTestRec.IsValid = false; dataFlowTestRec.TimeS = uint.Parse(fields[(Int32)ProtFsubString.CpuTime],
return false; NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
}
_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> /// <summary>
/// Extracting message from string fields to individual raw channel for protocol 'g' /// Extracting message from string fields to individual raw channel for protocol 'g'
/// </summary> /// </summary>
/// <param name="dataProtGRec">Reference to result structure for raw data for one channel</param> /// <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> /// <param name="fields">Separated fields containing the measurement as string</param>
/// <returns>true if protocol is valid</returns> /// <returns>true if protocol is valid</returns>
/// <remarks date="2018-Mar-22" author="T.Wiedebusch"> /// <remarks date="2018-Mar-22" author="T.Wiedebusch">
/// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm /// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm
/// </remarks> /// </remarks>
private Boolean DecodeProtocolG(ref CalibrationRecord dataProtGRec, IList<String> field) /// <remarks date="2023-Mar-09" author="T.Wiedebusch">
/// - Modified using common CRC check in advance.
/// </remarks>
private static void DecodeProtocolG(ref CalibrationRecord dataProtGRec, IList<String> fields)
{ {
// time stamp attachment
dataProtGRec.DecodedTime = DateTimeOffset.UtcNow; dataProtGRec.DecodedTime = DateTimeOffset.UtcNow;
dataProtGRec.IsValid = false;
//extract bytes of raw message for CRC calculation each character removing CRC (4 char) // extract received CRC
var byteArraySize = _rawMsgForCrc.Length - 4; dataProtGRec.Crc = ushort.Parse(fields[(Int32)ProtGsubString.Crc],
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); NumberStyles.HexNumber);
dataProtGRec.Channel = ushort.Parse(field[(Int32)ProtGsubString.ChanNo], dataProtGRec.Channel = ushort.Parse(fields[(Int32)ProtGsubString.ChanNo],
NumberStyles.HexNumber); NumberStyles.HexNumber);
dataProtGRec.Validation = ushort.Parse(field[(Int32)ProtGsubString.Validation], dataProtGRec.Validation = ushort.Parse(fields[(Int32)ProtGsubString.Validation],
NumberStyles.HexNumber); 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 // Delta time of flight
dataProtGRec.DeltaTimeOfFlightS = int.Parse(field[(Int32)ProtGsubString.Dtof], dataProtGRec.DeltaTimeOfFlightS = int.Parse(fields[(Int32)ProtGsubString.Dtof],
NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit; NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit;
// Delta raw volume between last sample // Delta raw volume between last sample
dataProtGRec.DeltaVolumeRaw = uint.Parse(field[(Int32)ProtGsubString.RawDVolume], dataProtGRec.DeltaVolumeRaw = uint.Parse(fields[(Int32)ProtGsubString.RawDVolume],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
//volume scaling // volume scaling
var volumeRawScale = uint.Parse(field[(Int32)ProtGsubString.VolumeScale], var volumeRawScale = uint.Parse(fields[(Int32)ProtGsubString.VolumeScale],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
dataProtGRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl; dataProtGRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl;
dataProtGRec.VolumeFactorRawToQm = MilliLitersToQmFactor / dataProtGRec.VolumeScaleRawPerMl; dataProtGRec.VolumeFactorRawToQm = MilliLitersToQmFactor / dataProtGRec.VolumeScaleRawPerMl;
dataProtGRec.OverflowVolumeCm = MaxAccuVolumeRaw * dataProtGRec.VolumeFactorRawToQm; dataProtGRec.OverflowVolumeCm = MaxAccuVolumeRaw * dataProtGRec.VolumeFactorRawToQm;
//Calculate volume in cubic meters out of the raw volume // Calculate volume in cubic meters out of the raw volume
dataProtGRec.DeltaVolumeQm = dataProtGRec.DeltaVolumeRaw * dataProtGRec.VolumeFactorRawToQm; dataProtGRec.DeltaVolumeQm = dataProtGRec.DeltaVolumeRaw * dataProtGRec.VolumeFactorRawToQm;
//accumulated volume for each channel received from water meter scaled with volumeScale // accumulated volume for each channel received from water meter scaled with volumeScale
//the volume can just be positive // the volume can just be positive
dataProtGRec.AccuVolumeRaw = uint.Parse(field[(Int32)ProtGsubString.AccuVolume], dataProtGRec.AccuVolumeRaw = uint.Parse(fields[(Int32)ProtGsubString.AccuVolume],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
dataProtGRec.VolumeCm = dataProtGRec.AccuVolumeRaw * dataProtGRec.VolumeFactorRawToQm; dataProtGRec.VolumeCm = dataProtGRec.AccuVolumeRaw * dataProtGRec.VolumeFactorRawToQm;
//Sample interval // Sample interval
dataProtGRec.SampleIntervalS = uint.Parse(field[(Int32)ProtGsubString.SampleInterval], dataProtGRec.SampleIntervalS = uint.Parse(fields[(Int32)ProtGsubString.SampleInterval],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
//amplitude for high threshold in V // amplitude for high threshold in V
dataProtGRec.AmplitudeUpV = uint.Parse(field[(Int32)ProtGsubString.AmplitudeUp], dataProtGRec.AmplitudeUpV = uint.Parse(fields[(Int32)ProtGsubString.AmplitudeUp],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//amplitude for low threshold in V // amplitude for low threshold in V
dataProtGRec.AmplitudeDownV = uint.Parse(field[(Int32)ProtGsubString.AmplitudeDown], dataProtGRec.AmplitudeDownV = uint.Parse(fields[(Int32)ProtGsubString.AmplitudeDown],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//pulse width ratio high threshold // pulse width ratio high threshold
dataProtGRec.PulseWidthRatioUp = uint.Parse(field[(Int32)ProtGsubString.PulseWidthRatioUp], dataProtGRec.PulseWidthRatioUp = uint.Parse(fields[(Int32)ProtGsubString.PulseWidthRatioUp],
NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor; NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor;
//pulse width ratio low threshold // pulse width ratio low threshold
dataProtGRec.PulseWidthRatioDown = uint.Parse(field[(Int32)ProtGsubString.PulseWidthRatioDown], dataProtGRec.PulseWidthRatioDown = uint.Parse(fields[(Int32)ProtGsubString.PulseWidthRatioDown],
NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor; NumberStyles.AllowHexSpecifier) * PulseWidthToRelFactor;
//raw temperature // raw temperature
dataProtGRec.TemperatureRaw = int.Parse(field[(Int32)ProtGsubString.RawTemperature], dataProtGRec.TemperatureRaw = int.Parse(fields[(Int32)ProtGsubString.RawTemperature],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
//temperature scaling // temperature scaling
dataProtGRec.TemperaturePowFactor = uint.Parse(field[(Int32)ProtGsubString.TemperatureScale], dataProtGRec.TemperaturePowFactor = uint.Parse(fields[(Int32)ProtGsubString.TemperatureScale],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
//Calculate temperature // Calculate temperature
dataProtGRec.TemperatureDegC = dataProtGRec.TemperatureRaw / dataProtGRec.TemperatureDegC = dataProtGRec.TemperatureRaw /
Math.Pow(2.0, dataProtGRec.TemperaturePowFactor); Math.Pow(2.0, dataProtGRec.TemperaturePowFactor);
//absolute CPU time, started at LED mode 3 activation // absolute CPU time, started at LED mode 3 activation
dataProtGRec.TimeS = uint.Parse(field[(Int32)ProtGsubString.CpuTime], dataProtGRec.TimeS = uint.Parse(fields[(Int32)ProtGsubString.CpuTime],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
return true;
} }
/// <summary> /// <summary>
/// Extracting message from string fields to individual raw channel for protocol 'g' /// Extracting message from string fields to individual raw channel for protocol 'g'
/// </summary> /// </summary>
/// <param name="dataProtHRec">Reference to result structure for raw data for one channel</param> /// <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> /// <param name="fields">Separated fields containing the measurement as string</param>
/// <returns>true if protocol is valid</returns> /// <returns>true if protocol is valid</returns>
/// <remarks date="2018-Mar-22" author="T.Wiedebusch"> /// <remarks date="2018-Mar-22" author="T.Wiedebusch">
/// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm /// - Usage of VolumeFactorRawToQm and calculation of AccuDutOverflowVolumeCm
/// </remarks> /// </remarks>
private Boolean DecodeProtocolH(ref CalibrationRecord dataProtHRec, IList<String> field) /// <remarks date="2023-Mar-09" author="T.Wiedebusch">
/// - Modified using common CRC check in advance.
/// </remarks>
private static void DecodeProtocolH(ref CalibrationRecord dataProtHRec, IList<String> fields)
{ {
// time stamp attachment
dataProtHRec.DecodedTime = DateTimeOffset.UtcNow; dataProtHRec.DecodedTime = DateTimeOffset.UtcNow;
dataProtHRec.IsValid = false;
//extract bytes of raw message for CRC calculation each character removing CRC (4 char) // extract received CRC
var byteArraySize = _rawMsgForCrc.Length - 4; dataProtHRec.Crc = ushort.Parse(fields[(Int32)ProtHsubString.Crc],
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); NumberStyles.HexNumber);
dataProtHRec.Channel = ushort.Parse(field[(Int32)ProtHsubString.ChanNo], dataProtHRec.Channel = ushort.Parse(fields[(Int32)ProtHsubString.ChanNo],
NumberStyles.HexNumber); NumberStyles.HexNumber);
dataProtHRec.Validation = ushort.Parse(field[(Int32)ProtHsubString.Validation], dataProtHRec.Validation = ushort.Parse(fields[(Int32)ProtHsubString.Validation],
NumberStyles.HexNumber); 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 // Total time of flight
dataProtHRec.TotalTimeOfFlightS = int.Parse(field[(Int32)ProtHsubString.Ttof], dataProtHRec.TotalTimeOfFlightS = int.Parse(fields[(Int32)ProtHsubString.Ttof],
NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit; NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit;
//Delta time of flight // Delta time of flight
dataProtHRec.DeltaTimeOfFlightS = int.Parse(field[(Int32)ProtHsubString.Dtof], dataProtHRec.DeltaTimeOfFlightS = int.Parse(fields[(Int32)ProtHsubString.Dtof],
NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit; NumberStyles.AllowHexSpecifier) * TofToSecondsFactor38Bit;
dataProtHRec.RawTotalTimeOfFlight = dataProtHRec.RawTotalTimeOfFlight =
int.Parse(field[(Int32)ProtHsubString.Ttof], NumberStyles.AllowHexSpecifier); int.Parse(fields[(Int32)ProtHsubString.Ttof], NumberStyles.AllowHexSpecifier);
dataProtHRec.RawDeltaTimeOfFlight = int.Parse(field[(Int32)ProtHsubString.Dtof], NumberStyles.AllowHexSpecifier); dataProtHRec.RawDeltaTimeOfFlight = int.Parse(fields[(Int32)ProtHsubString.Dtof], NumberStyles.AllowHexSpecifier);
//Delta raw volume between two samples // Delta raw volume between two samples
dataProtHRec.DeltaVolumeRaw = uint.Parse(field[(Int32)ProtHsubString.RawDVolume], dataProtHRec.DeltaVolumeRaw = uint.Parse(fields[(Int32)ProtHsubString.RawDVolume],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
//volume scaling // volume scaling
var volumeRawScale = uint.Parse(field[(Int32)ProtHsubString.VolumeScale], var volumeRawScale = uint.Parse(fields[(Int32)ProtHsubString.VolumeScale],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
dataProtHRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl; dataProtHRec.VolumeScaleRawPerMl = volumeRawScale != 0 ? volumeRawScale : DefaultVolumeScaleRawPerMl;
dataProtHRec.VolumeFactorRawToQm = MilliLitersToQmFactor / dataProtHRec.VolumeScaleRawPerMl; dataProtHRec.VolumeFactorRawToQm = MilliLitersToQmFactor / dataProtHRec.VolumeScaleRawPerMl;
dataProtHRec.OverflowVolumeCm = MaxAccuVolumeRaw * dataProtHRec.VolumeFactorRawToQm; dataProtHRec.OverflowVolumeCm = MaxAccuVolumeRaw * dataProtHRec.VolumeFactorRawToQm;
//Calculate volume in cubic meters out of the raw volume // Calculate volume in cubic meters out of the raw volume
dataProtHRec.DeltaVolumeQm = dataProtHRec.DeltaVolumeRaw * dataProtHRec.VolumeFactorRawToQm; dataProtHRec.DeltaVolumeQm = dataProtHRec.DeltaVolumeRaw * dataProtHRec.VolumeFactorRawToQm;
//accumulated volume for each channel received from water meter scaled with volumeScale // accumulated volume for each channel received from water meter scaled with volumeScale
//the volume can just be positive // the volume can just be positive
dataProtHRec.AccuVolumeRaw = uint.Parse(field[(Int32)ProtHsubString.AccuVolume], dataProtHRec.AccuVolumeRaw = uint.Parse(fields[(Int32)ProtHsubString.AccuVolume],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
dataProtHRec.VolumeCm = dataProtHRec.AccuVolumeRaw * dataProtHRec.VolumeFactorRawToQm; dataProtHRec.VolumeCm = dataProtHRec.AccuVolumeRaw * dataProtHRec.VolumeFactorRawToQm;
//Sample interval // Sample interval
dataProtHRec.SampleIntervalS = uint.Parse(field[(Int32)ProtHsubString.SampleInterval], dataProtHRec.SampleIntervalS = uint.Parse(fields[(Int32)ProtHsubString.SampleInterval],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
//amplitude for high threshold in V // amplitude for high threshold in V
dataProtHRec.AmplitudeUpV = uint.Parse(field[(Int32)ProtHsubString.AmplitudeUp], dataProtHRec.AmplitudeUpV = uint.Parse(fields[(Int32)ProtHsubString.AmplitudeUp],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//amplitude for low threshold in V // amplitude for low threshold in V
dataProtHRec.AmplitudeDownV = uint.Parse(field[(Int32)ProtHsubString.AmplitudeDown], dataProtHRec.AmplitudeDownV = uint.Parse(fields[(Int32)ProtHsubString.AmplitudeDown],
NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor; NumberStyles.AllowHexSpecifier) * AmplitudeToVoltFactor;
//raw temperature // raw temperature
dataProtHRec.TemperatureRaw = int.Parse(field[(Int32)ProtHsubString.RawTemperature], dataProtHRec.TemperatureRaw = int.Parse(fields[(Int32)ProtHsubString.RawTemperature],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
//temperature scaling // temperature scaling
dataProtHRec.TemperaturePowFactor = uint.Parse(field[(Int32)ProtHsubString.TemperatureScale], dataProtHRec.TemperaturePowFactor = uint.Parse(fields[(Int32)ProtHsubString.TemperatureScale],
NumberStyles.AllowHexSpecifier); NumberStyles.AllowHexSpecifier);
//Calculate temperature // Calculate temperature
dataProtHRec.TemperatureDegC = dataProtHRec.TemperatureRaw / ( dataProtHRec.TemperatureDegC = dataProtHRec.TemperatureRaw / (
Math.Pow(2.0, dataProtHRec.TemperaturePowFactor)); Math.Pow(2.0, dataProtHRec.TemperaturePowFactor));
//absolute CPU time, started at LED mode 3 activation // absolute CPU time, started at LED mode 3 activation
dataProtHRec.TimeS = uint.Parse(field[(Int32)ProtHsubString.CpuTime], dataProtHRec.TimeS = uint.Parse(fields[(Int32)ProtHsubString.CpuTime],
NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor; NumberStyles.AllowHexSpecifier) * CpuTimeToSecondsFactor;
return true;
} }
/// field position in protocol 'l'
private enum ProtLsubString
{
//do not remove this needed for position in record
ProtType,
DisplayVolume,
CpuTime,
FlowRate,
StatusBits,
UniqueId,
SensorId,
Crc
}
/// field position in protocol 'f' /// field position in protocol 'f'
private enum ProtFsubString private enum ProtFsubString
{ {
+34
View File
@@ -16,6 +16,10 @@ namespace Xylem.Common.Utils.Crc16Ccitt
/// </summary> /// </summary>
private const UInt16 CrcSeed3791 = 0x3791; private const UInt16 CrcSeed3791 = 0x3791;
/// <summary>
/// Generator polynomial MagFlux - Modbus RTU
/// </summary>
private const UInt16 CrcGpA001 = 0xA001;
/// <summary> /// <summary>
/// Generator polynomial GENESIS /// Generator polynomial GENESIS
/// </summary> /// </summary>
@@ -73,6 +77,36 @@ namespace Xylem.Common.Utils.Crc16Ccitt
return (UInt16)~CalculateMsb1021(data); return (UInt16)~CalculateMsb1021(data);
} }
/// <summary>
/// Calculates the Modbus RTU CRC (LSB first, CRCCCITT 0xA001)
/// from a data array of bytes.
/// </summary>
/// <param name="data">data to be processed</param>
/// <returns>Calculated CRC</returns>
public static UInt16 ModbusRtuLsbA001(Byte[] data)
{
var crc = CrcSeedFfff;
for (var t = 0; t < data.Length; t++)
{
crc ^= data[t]; // XOR byte into least sig. byte of crc
for (var i = 8; i != 0; i--)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= CrcGpA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
/// <summary> /// <summary>
/// Calculates the CRC (MSB first, CRCCCITT 0x1021) from a data array of bytes. /// Calculates the CRC (MSB first, CRCCCITT 0x1021) from a data array of bytes.
/// </summary> /// </summary>