common/Hardware/WaterMeter/Genesis/Protocols/RequestProtocol/RequestProtocol.cs
2026-04-23 17:50:07 +02:00

769 lines
37 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Xylem.Common.Utils.Logging;
using NLog;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore.EventArguments;
using Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore;
using Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.EventArguments;
using Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.EventArguments;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig;
using Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.Consts;
using Xylem.Common.Hardware.WaterMeter.Genesis.Registers;
namespace Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol
{
/// <summary>
/// a bidirectional protocol support read and write genesis meter registers
/// needed for
/// </summary>
public class RequestProtocol : BaseProtocol
{
//All read and write data sets (payload size) are organized in 4 byte chunks
//Format of RequestProtocol read data
//Request CMD | Register LSB | Register MSB | 0x00 | 0x00 | 0x00 | 0x00
//Response CMD | ErrCode Reason | ErrCode Base | DATA0 | DATA1 | DATA2 | DATA3
//Format of RequestProtocol write data
//Request CMD | Register LSB | Register MSB | DATA0 | DATA1 | DATA2 | DATA3
//Response CMD | ErrCode Reason | ErrCode Base | UNDEF0 | UNDEF1 | UNDEF2 | UNDEF3
//Format of RequestProtocol multiple read data
//Request CMD | Register LSB | Register MSB | RequestChunks LSB | RequestChunks MSB
//Response CMD | ErrCode Reason | ErrCode Base | ErrPosition LSB | ErrPosition MSB |
//DATA0 | DATA1 | DATA2 | DATA3.... (repeat for number of chunks)
//Format of RequestProtocol multiple read data
//Request CMD | Register LSB | Register MSB | WriteChunks LSB | WriteChunks MSB |
//DATA0 | DATA1 | DATA2 | DATA3.... (repeat for number of chunks)
//Response CMD | ErrCode Reason | ErrCode Base | ErrPosition LSB | ErrPosition MSB
//Indexes of the request protocol read/write data
//private const Int32 RequestCommandIndex = 0;
//private const Int32 RequestRegisterIndex = 1;
//private const Int32 RequestDataIndex = 3;
//Indexes for response protocol read/write data
private const Int32 ResponseCommandIndex = 0;
private const Int32 ResponseErrorCodeIndex = 1;
private const Int32 ResponseDataIndex = 3;
//Indexes of the request protocol multiple read/write data
//private const Int32 RequestChunksIndex = 3;
//private const Int32 RequestMultiDataIndex = 5;
//Indexes of the response protocol multiple read/write data
private const Int32 ResponseMultiErrorPosition = 3;
private const Int32 ResponseMultiDataIndex = 5;
//Indexes of error code
private const Int32 ErrorCodeReasonIndex = 0;
private const Int32 ErrorCodeBaseIndex = 1;
//Multiple response header size is 1 byte command 2 bytes error code and 2 bytes error position
private const Int32 ResponseMultiHeaderSize = 5;
//fill byte
private const Byte FillByte = 0x00;
private readonly ILogger _logger;
// wakeup from register (water meter) to adapter
private static readonly Byte[] WakeupMessage = { 0x00, 0xFF, 0xFF };
/// <summary>
/// FIFO of records to be send next
/// </summary>
private readonly ConcurrentQueue<RequestRecord> _recordsSendFifo = new ConcurrentQueue<RequestRecord>();
/// <summary>
/// This is the actual record in the send loop
/// </summary>
private RequestRecord _recordInProcess;
/// <summary>
/// Last communication time for session refresh
/// </summary>
public DateTimeOffset? LastCommTime;
/// <inheritdoc />
public override event EventHandler<BasePortDataEventArgs> OnRecordReadyToSend;
/// <inheritdoc />
public override event EventHandler<BaseDataEventArgs> OnRecordIsDecoded;
/// <summary>
/// Occurs when a meter Response for write password is good
/// </summary>
public event EventHandler OnAuthorizationGrant;
/// <summary>
/// Event after a register entries changed
/// </summary>
public event EventHandler<RegisterUpdatedEventArgs> OnMeterRegisterUpdated;
/// <summary>
/// User adjustable additional retry timeout. This is 0 ms for standard operation.
/// </summary>
public Int32 AdditionalRetryTimeoutMs;
private Int32 _maxTimeOutMs;
private readonly String _ident;
/// <inheritdoc />
public RequestProtocol(String ident) : base(ident)
{
_ident = ident;
_logger = NLogHelper.CreateOrGetMultiLogger(ident, "", "RequestProtocol", "MeterBase", "MeterBase");
}
/// <summary>
/// Process all records needed to be sent, this routine has to be called
/// to kick-off the communication of all RequestRecords saved to the send
/// FIFO <see cref="AddRecordToSendFifo"/>
/// </summary>
/// <remarks date="2018-Dec-07" author="T.Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Dec-09" author="T.Wiedebusch">
/// - Logging of raw RequestProtocol at time of sending,
/// - Logging of retries.
/// </remarks>
/// <remarks date="2018-Dec-12" author="T.Wiedebusch">
/// - Added timeout from transmit protocol.
/// </remarks>
/// <remarks date="2019-Jan-02" author="Drabesch">
/// - Added reorder FIFO to bring login at first position
/// </remarks>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
/// <remarks date="2019-Jun-06" author="T.Wiedebusch">
/// - Retry counter reset if authorization required and this record will be put back into FIFO,
/// - Retry delay corrected for all errors.
/// </remarks>
/// <remarks date="2019-Jun-12" author="T.Wiedebusch">
/// - Dynamic retry delay: ResponseTimeoutMs * Retry counter.
/// </remarks>
/// <remarks date="2019-Jun-28" author="T.Wiedebusch">
/// - Skip retries on specific error mask to speed up communication on functional errors
/// or informational feed backs (e.g FW not installed 0x0004)
/// <see cref="RequestRecord.ResponseErrorCode"/>
/// </remarks>
/// <remarks date="2019-Aug-29" author="T.Wiedebusch">
/// - User adjustable additional retry timeout.
/// </remarks>
/// <remarks date="2020-Jan-17/18" author="T.Wiedebusch">
/// - Response timeout message output,
/// - Initialize acknowledge code before communication to NoResponse.
/// </remarks>
/// <remarks date="2020-Jan-22" author="T.Wiedebusch">
/// - Response timeout deviated from system time.
/// </remarks>
/// <remarks date="2021-Feb-02" author="T.Wiedebusch">
/// - Avoid enqueue of _recordInProcess if retry counter is 0.
/// </remarks>
/// <remarks date="2022-Nov-04" author="T.Wiedebusch">
/// - Additional DEBUG information included about FIFO and loops.
/// </remarks>
/// <remarks date="2022-Nov-07" author="T.Wiedebusch">
/// - Command not assigned response for recordInProcess == null.
/// </remarks>
/// <remarks date="2024-Apr-22" author="T.Wiedebusch">
/// - Initial request acknowledge state changed from NotDecoded to NoResponse.
/// </remarks>
public RequestAcknowledgeState ProcessRecordList()
{
_logger.Trace($"{_ident} Entry to ProcessRecordList, FIFO contains ({_recordsSendFifo.Count}) records");
//The inter record send delay has to be hold before trying to communicate again
const Int32 interRecordSendDelayMs = CommunicationConfig.InterRecordSendDelayMs;
//remind counter for FIFO and therefore execution loops
var loopCounter = 0;
//check if FIFO is empty and get the actual record to process out of it
while (_recordsSendFifo.TryDequeue(out var internalRecord))
{
// Starting with record number 1
loopCounter++;
_logger.Trace($"{_ident} Record({loopCounter}) - Dequeued from FIFO");
//pointer to new record
_recordInProcess = internalRecord;
if (_recordInProcess == null)
return RequestAcknowledgeState.CommandNotAssigned;
do
{
//mark record as answer outstanding
_recordInProcess.Acknowledge = RequestAcknowledgeState.NoResponse;
_logger.Trace($"{_ident} Record({loopCounter}) - Processing");
//log retries
if (_recordInProcess.RetryCtr > 0)
{
_logger.Debug($"{_ident} Retry({_recordInProcess.RetryCtr})");
_logger.Trace($"{_ident} Record({loopCounter}) - Retry({_recordInProcess.RetryCtr})");
}
//log request protocol content
_logger.Debug(_recordInProcess.HideDataInLog
? $"{_ident} SentData(*****)"
: $"{_ident} SentData({BitConverter.ToString(_recordInProcess.RequestProtocolData.ToArray())})");
//remind time for keep-session-active test to deny automatic logout of meter
LastCommTime = DateTimeOffset.UtcNow;
//set timeout for one communication trial adding the transmit protocol specific timeout and
//increase the timeout with each retry
_maxTimeOutMs = _recordInProcess.ResponseTimeoutMs + AdditionalRetryTimeoutMs +
CommunicationConfig.ResponseTimeoutMs * (_recordInProcess.RetryCtr + 1);
//start initial communication or retry
OnRecordReadyToSend?.Invoke(this, new ListBytePortDataEventArgs(_recordInProcess.EncodedRequestData));
//time reminder of request record
var requestTimeUtc = DateTimeOffset.UtcNow;
Int32 actualResponseWaitTimeMs;
//wait for communication acknowledge or until timeout, this also handles the inter record send delay
do
{
//minimum delay is the inter record send delay
Thread.Sleep(interRecordSendDelayMs);
var actualTimeUtc = DateTimeOffset.UtcNow;
//actual time difference from request to now
var timeSpan = actualTimeUtc - requestTimeUtc;
//avoid total milliseconds below zero at time overflow
if (timeSpan.TotalMilliseconds < 0)
{
requestTimeUtc = DateTimeOffset.UtcNow;
}
actualResponseWaitTimeMs = (Int32)timeSpan.TotalMilliseconds;
} while (_recordInProcess.Acknowledge != RequestAcknowledgeState.Ok &&
_recordInProcess.SkipRetryErrorCode != _recordInProcess.ResponseErrorCode &&
_maxTimeOutMs > actualResponseWaitTimeMs);
//if timeout value reaches zero, the response hasn't been received or the delay until
//next communication needed to be hold
if (_maxTimeOutMs <= actualResponseWaitTimeMs)
{
_logger.Error(_recordInProcess.Acknowledge == RequestAcknowledgeState.NoResponse
? $"{_ident} Response timeout({actualResponseWaitTimeMs}ms)"
: $"{_ident} Communication delay({actualResponseWaitTimeMs}ms)");
}
//skip loop to handle re-authorization
if (_recordInProcess.Acknowledge != RequestAcknowledgeState.AuthorizationRequired)
continue;
//reset retry counter for this record, it will be dispatched after re-authorization
_recordInProcess.RetryCtr = 0;
if (CommunicationConfig.RequestRetries > 0)
{
//add the recordInProcess to the top of the FIFO
_recordsSendFifo.Enqueue(_recordInProcess);
}
//call login
return _recordInProcess.Acknowledge;
} while (_recordInProcess.Acknowledge != RequestAcknowledgeState.Ok &&
_recordInProcess.RetryCtr++ < CommunicationConfig.RequestRetries &&
_recordInProcess.SkipRetryErrorCode != _recordInProcess.ResponseErrorCode);
}
return _recordInProcess.Acknowledge;
}
/// <summary>
/// Assemble record with transmit protocol and put it to record-send-FIFO,
/// backup the ready-to-send, which is the dataEncodedWithTransmitProtocol,
/// to the RequestRecord object for sending including the retry capability.
/// </summary>
/// <param name="payload">Data package with all details like CRC, etc</param>
/// <param name="cmd">Command identifier <see cref="Commands" /></param>
/// <param name="register"><see cref="RegisterDefinition" /> which is intend</param>
/// <param name="hideDataInLog">hiding data in log file to avoid spying of passwords</param>
/// <param name="skipRetryErrorCode">error mask to skip retries</param>
/// <returns>the assembled record for the send FIFO</returns>
/// <remarks date="2018-Dec-08" author="T.Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Dec-09" author="T.Wiedebusch">
/// - Logging of raw data (RequestProtocol) moved to ProcessRecordList.
/// </remarks>
/// <remarks date="2018-Dec-12" author="T.Wiedebusch">
/// - Added timeout from transmit protocol.
/// </remarks>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
/// <remarks date="2019-Jun-28" author="T.Wiedebusch">
/// - Skip retries on specific error mask to speed up communication on functional errors
/// or informational feed backs (e.g FW not installed 0x0004)
/// <see cref="RequestRecord.ResponseErrorCode"/>
/// </remarks>
/// <remarks date="2020-Jan-18" author="T.Wiedebusch">
/// - Initial skip retry error code set to 0x0004 (e.g FW not installed 0x0004).
/// </remarks>
/// <remarks date="2023-Mai-16" author="T.Wiedebusch">
/// - Hide data in log forwarded to DecodeDateForPhysicalLayer to hide passwords in log files.
/// </remarks>
private RequestRecord AddRecordToSendFifo(Byte cmd, Byte[] payload, RegisterDefinition register = null,
Boolean hideDataInLog = false, UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode)
{
var requestProtocolData = new List<Byte> { cmd };
requestProtocolData.AddRange(payload);
//encode request protocol data with transmit protocol
var transmit = GetTransmitProtocol();
var dataEncodedWithTransmitProtocol = transmit.DecodeDataForPhysicalLayer(_ident, cmd, payload, hideDataInLog);
//remind port specific response timeout
var transmitPortSettings = transmit.GetTransmitPortSettings();
var recordForSendFifo = new RequestRecord(cmd, requestProtocolData, dataEncodedWithTransmitProtocol,
transmitPortSettings.ResponseTimeoutMs, register, hideDataInLog, skipRetryErrorCode);
_recordsSendFifo.Enqueue(recordForSendFifo);
return recordForSendFifo;
}
/// <summary>
/// Record dispatcher to send FIFO of UI1236 command
/// </summary>
/// <remarks date="2022-Aug-24" author="R.Drabesch">
/// - Initial
/// </remarks>
/// <param name="ident">string for logging of slot</param>
/// <param name="payload">Data package with all details like CRC, etc</param>
/// <param name="register"><see cref="RegisterDefinition" /> which is intend</param>
/// <param name="hideDataInLog">hiding data in log file to avoid spying of passwords</param>
/// <param name="skipRetryErrorCode">error mask to skip retries</param>
/// <returns>the assembled record for the send FIFO</returns>
// ReSharper disable once InconsistentNaming UI1236 is a naming forced by the caller
public RequestRecord AddRecordToSendFifoUI1236(String ident, Byte[] payload, RegisterDefinition register = null,
Boolean hideDataInLog = false, UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode)
{
const Byte maxValue = byte.MaxValue;
var requestProtocolData = new List<Byte>
{
maxValue
};
requestProtocolData.AddRange(payload);
var transmitProtocol = GetTransmitProtocol();
var encodedRequestData = transmitProtocol.DecodeDataForPhysicalLayerUI1236(ident, payload, hideDataInLog);
var transmitPortSettings = transmitProtocol.GetTransmitPortSettings();
var sendFifoUi1236 = new RequestRecord(maxValue, requestProtocolData, encodedRequestData,
transmitPortSettings.ResponseTimeoutMs, register, hideDataInLog, skipRetryErrorCode);
_recordsSendFifo.Enqueue(sendFifoUi1236);
return sendFifoUi1236;
}
/// <summary>
/// Called after successful response.
/// Decode and check record, handle Errors and dispatch result.
/// Invokes <see cref="OnRecordIsDecoded" />if somebody is listening
/// </summary>
/// <remarks date="2018-Mar-15" author="R.Drahbesch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Dec-08" author="T.Wiedebusch">
/// - First part reworked to extract the response protocol information
/// </remarks>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
/// <remarks date="2019-Jun-28" author="T.Wiedebusch">
/// - Error code extraction changed for <see cref="RequestRecord.SkipRetryErrorCode"/>
/// </remarks>
/// <remarks date="2019-Dec-12/16" author="T.Wiedebusch">
/// - Wakeup-message will avoid further timeout (during FW update this is in the range
/// of 15000ms).
/// - Allow one additional retry on decoding error, which is often the wakeup-message.
/// </remarks>
/// <remarks date="2019-Jan-17" author="T.Wiedebusch">
/// - Send time reminder for timeout time calculation as output in log-file,
/// - OnRecordIsDecoded?.Invoke moved before return to assure that the Acknowledge status is set.
/// </remarks>
/// <remarks date="2019-Jan-22" author="T.Wiedebusch">
/// - Avoid activation of wakeup retry if record is meanwhile acknowledged.
/// </remarks>
/// <remarks date="2022-Jul-19" author="T.Wiedebusch">
/// - Wakeup message handling changed,
/// - Multiple replies on wakeup message allowed.
/// </remarks>
/// <remarks date="2022-Oct-02" author="T.Wiedebusch">
/// - On wakeup message 5 retires are allowed to avoid an infinite loop.
/// </remarks>
/// <remarks date="2022-Oct-04" author="T.Wiedebusch">
/// - On wakeup message exit this routine.
/// </remarks>
/// <remarks date="2023-Feb-13" author="T.Wiedebusch">
/// - Early exit on _recordInProcess == null,
/// - Wakeup message retries from 5 to 2,
/// - Removed error base from error code decision as error base is only the AppId
/// </remarks>
/// <remarks date="2023-Sep-01" author="T.Wiedebusch">
/// - HideDataInLog.
/// </remarks>
/// <remarks date="2024-Jan-24" author="T.Wiedebusch">
/// - Ignore wakeup if message already acknowledged (avoid to set
/// "_recordInProcess.Acknowledge = RequestAcknowledgeState.NotDecoded"),
/// - Avoid to overwrite "_recordInProcess.Acknowledge = RequestAcknowledgeState.Acknowledge" with
/// "RequestAcknowledgeState.WakeupMessage".
/// </remarks>
protected override void DecodeRecord(IPortDataEventArgs data)
{
if (_recordInProcess == null)
return;
//the request response record covers the entire request protocol
var responseRecord = GetTransmitProtocol().DecodeDataForLogicLayer(_ident, (List<Byte>)data.GetData(),
_recordInProcess.HideDataInLog);
//on protocol decoding failure
if (responseRecord.Count == 0)
{
_recordInProcess.Acknowledge = RequestAcknowledgeState.DecodingError;
_logger.Debug($"{_ident} Message decoding error.");
//reset timeout to a normal value if timeout is extremely high but response received
if (_maxTimeOutMs > CommunicationConfig.BusyTimeoutMs)
_maxTimeOutMs = CommunicationConfig.BusyTimeoutMs;
return;
}
if (responseRecord.Count == WakeupMessage.Length)
{
// check for wakeup message
var wakeUp = true;
for (var i = 0; i < WakeupMessage.Length; i++)
{
if (responseRecord[i] != WakeupMessage[i])
wakeUp = false;
}
if (wakeUp)
{
//avoid wakeup retry on acknowledged record
if (RequestAcknowledgeState.Ok != _recordInProcess.Acknowledge)
_recordInProcess.Acknowledge = RequestAcknowledgeState.WakeupMessage;
//initiate a single retry on wakeup message response
if (_recordInProcess.WakeupMessageRetryCtr < 2)
{
_recordInProcess.WakeupMessageRetryCtr++;
_logger.Debug(
$"{_ident} Wakeup message({_recordInProcess.WakeupMessageRetryCtr}) received");
}
}
else
{
//if it is not identified as valid wakeup message it is something unknown
_recordInProcess.Acknowledge = RequestAcknowledgeState.DecodingError;
_logger.Debug($"{_ident} Message decoding error.");
}
// reset retry counter to get all required retries
_recordInProcess.RetryCtr = -1;
//reset timeout to a normal value if timeout is extremely high but response received
if (_maxTimeOutMs > CommunicationConfig.BusyTimeoutMs)
_maxTimeOutMs = CommunicationConfig.BusyTimeoutMs;
return;
}
//log request protocol content
_logger.Debug(_recordInProcess.HideDataInLog
? $"{_ident} DecodedRecord(*****)"
: $"{_ident} DecodedRecord({BitConverter.ToString(responseRecord.ToArray())})");
//extract information command
var replyCmd = responseRecord[ResponseCommandIndex];
//extract position of error for multiple access
if (replyCmd == Commands.MultipleReadDataReply || replyCmd == Commands.MultipleWriteDataReply)
{
_recordInProcess.ResponseErrorPosition =
(UInt16)((responseRecord[ResponseMultiErrorPosition] & 0x00FF) |
(UInt16)((responseRecord[ResponseMultiErrorPosition + 1] << 8) & 0xFF00));
}
//extract the error codes from response record to _record in process
_recordInProcess.ResponseErrorBase = responseRecord[ResponseErrorCodeIndex + ErrorCodeBaseIndex];
_recordInProcess.ResponseErrorReason = responseRecord[ResponseErrorCodeIndex + ErrorCodeReasonIndex];
//combine error base and error reason
_recordInProcess.ResponseErrorCode = (UInt16)(_recordInProcess.ResponseErrorReason |
(_recordInProcess.ResponseErrorBase << 8));
_recordInProcess.ResponsePayload = new List<Byte>();
switch (replyCmd)
{
case Commands.MultipleReadDataReply:
_recordInProcess.ResponsePayload.AddRange(
responseRecord.GetRange(ResponseMultiDataIndex,
responseRecord.Count - ResponseMultiHeaderSize));
break;
case Commands.ReadDataReply:
_recordInProcess.ResponsePayload.AddRange(responseRecord.GetRange(ResponseDataIndex,
RegisterDefinition.ChunkSize));
break;
}
//Deny access if reply does not match
if (_recordInProcess.ResponseCommand != replyCmd)
{
_logger.Fatal($"{_ident} Wrong command received({replyCmd:X2})," +
$" expected command({_recordInProcess.ResponseCommand:X2})");
_recordInProcess.Acknowledge = RequestAcknowledgeState.CommandError;
OnRecordIsDecoded?.Invoke(this,
new RequestResponseDataEventArgs { RequestResponseData = responseRecord });
return;
}
//examine error code, if base (AppId) is not 0 the reason 0 may be an error!!!!
if (/*_recordInProcess.ResponseErrorBase != 0 ||*/ _recordInProcess.ResponseErrorReason != 0)
{
_logger.Warn($"{_ident} Error code received(0x{_recordInProcess.ResponseErrorBase:X2}" +
$"{_recordInProcess.ResponseErrorReason:X2})");
_recordInProcess.Acknowledge = RequestAcknowledgeState.MeterError;
CheckErrorCode();
OnRecordIsDecoded?.Invoke(this,
new RequestResponseDataEventArgs { RequestResponseData = responseRecord });
return;
}
//register password acknowledge
if (_recordInProcess.Register.GetIdent() == Register.Configexchange.Password)
{
OnAuthorizationGrant?.Invoke(null, null);
}
//dispatch data
if (_recordInProcess.ResponsePayload != null)
{
OnMeterRegisterUpdated?.Invoke(this, new RegisterUpdatedEventArgs(_recordInProcess.Register,
_recordInProcess.ResponsePayload.ToArray()));
}
_recordInProcess.Acknowledge = RequestAcknowledgeState.Ok;
OnRecordIsDecoded?.Invoke(this,
new RequestResponseDataEventArgs { RequestResponseData = responseRecord });
}
/// <summary>
/// Method to send basic Commands to Meter. Creates an integer of 4 bytes for the payload.
/// Supported Commands are: <see cref="Commands.ReadData" />,<see cref="Commands.MultipleReadData" />,
/// <see cref="Commands.WriteData" />, <see cref="Commands.MultipleWriteData" /> and <see cref="Commands.QueryCaps" />.
/// Calculate CRCs and Command length and check if command is valid.
/// Use <see cref="AddRecordToSendFifo" /> to push data to Port/Meter.
/// </summary>
/// <param name="command">
/// Supported <see cref="Commands.ReadData" />,<see cref="Commands.MultipleReadData" />,
/// <see cref="Commands.WriteData" />, <see cref="Commands.MultipleWriteData" /> and <see cref="Commands.QueryCaps" />.
/// </param>
/// <param name="meterRegister">Register to Read or Write, null for <see cref="Commands.QueryCaps" /></param>
/// <param name="payload">
/// Data to push into the <see cref="RegisterDefinition" />.
/// must be null on Read Commands ( <see cref="Commands.ReadData" />,<see cref="Commands.MultipleReadData" />)
/// and not null on WriteData (<see cref="Commands.WriteData" /> and <see cref="Commands.MultipleWriteData" />)
/// </param>
/// <param name="expectedLength"></param>
/// <param name="hideDataInLog">hiding data in log file to avoid spying of passwords</param>
/// <param name="skipRetryErrorCode">mask to skip reties on error</param>
/// <returns>an new command just send to port/meter </returns>
/// <remarks date="2018-Mar-15" author="R.Drahbesch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Dec-08" author="T.Wiedebusch">
/// - Reworked to zero pad payload with chunks of 4 bytes, the caller needn't take care of the size
/// </remarks>
/// <remarks date="2018-Dec-09" author="T.Wiedebusch">
/// - Corrected payload content in request protocol
/// </remarks>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
/// <remarks date="2019-Jun-28" author="T.Wiedebusch">
/// - Skip retries on specific error mask to speed up communication on functional errors
/// or informational feed backs (e.g FW not installed 0x0004)
/// <see cref="RequestRecord.ResponseErrorCode"/>
/// </remarks>
/// <remarks date="2019-Aug-28" author="R.Drabesch">
/// - Multiple read for string split to simple reads.
/// </remarks>
/// <remarks date="2019-Jan-18" author="T.Wiedebusch">
/// - Default <see cref="CommunicationConfig.SkipRetryErrorCode"/> set.
/// </remarks>
/// <remarks date="2023-Sep-06" author="T.Wiedebusch">
/// - MultipleReadData based on data size.
/// </remarks>
/// <remarks date="2023-Nov-24" author="T.Wiedebusch">
/// - Exit multiple read date if retries exceeded.
/// </remarks>
/// <remarks date="2024-Apr-11" author="T.Wiedebusch">
/// - Exit multiple read date if retries exceeded increased to <see cref="CommunicationConfig.MaxRequestRetries"/>.
/// </remarks>
/// <remarks date="2024-Apr-11 Version 2" author="T.Wiedebusch">
/// - Rewound to version from 06.09.2023 14:44:33 before Commit 8c9d7704.
/// </remarks>
/// <remarks date="2024-Apr-15" author="T.Wiedebusch">
/// - Removed useless too short comments as every string is going to be delimited by a 0 and usually won't match
/// to a 4 byte chunk.
/// </remarks>
/// <remarks date="2026-Jan-06" author="T.Wiedebusch">
/// - Removed redundant 'return cRecord'.
/// </remarks>
public RequestRecord CommandToMeter(Byte command, RegisterDefinition meterRegister = null,
Byte[] payload = null, Int32? expectedLength = null, Boolean hideDataInLog = false,
UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode)
{
if (command == Commands.MultipleReadData && meterRegister != null && meterRegister.DataType == typeof(String))
{
var completeResponse = new List<Byte>();
while (true)
{
var cRecord = CommandToMeter(Commands.ReadData, meterRegister, payload, expectedLength,
hideDataInLog, skipRetryErrorCode);
var rest = ProcessRecordList();
if (rest == RequestAcknowledgeState.Ok && cRecord?.ResponsePayload != null)
{
completeResponse.AddRange(cRecord.ResponsePayload);
if (!cRecord.ResponsePayload.Contains(0)) continue;
cRecord.ResponsePayload = completeResponse;
OnMeterRegisterUpdated?.Invoke(this,
new RegisterUpdatedEventArgs(meterRegister, completeResponse.ToArray()));
}
return cRecord;
}
}
//the minimum payload is one 4 byte chunk
var chunkSize = RegisterDefinition.ChunkSize;
if (payload == null)
{
payload = new Byte[chunkSize];
for (var i = 0; i < chunkSize; i++)
{
payload[i] = FillByte;
}
}
//real payload based on 4 byte chunks
var requestPayload = new List<Byte>();
requestPayload.AddRange(payload);
//fill-byte padding
if (payload.Length % chunkSize != 0)
{
for (var i = 0; i < chunkSize - payload.Length % chunkSize; i++)
{
requestPayload.Add(FillByte);
}
}
//request protocol excluding the command
var requestProtocol = new List<Byte>();
if (meterRegister != null)
{
requestProtocol.Add(meterRegister.RegisterAddress[1]);
requestProtocol.Add(meterRegister.RegisterAddress[0]);
if (command == Commands.MultipleWriteData)
{
//set counter to inform meter writes are expected
//the payload size is already padded to 4 byte chunks
var numberOfChunks = (UInt16)(requestPayload.Count / chunkSize);
requestProtocol.Add((Byte)(numberOfChunks & 0xFF));
requestProtocol.Add((Byte)((numberOfChunks >> 8) & 0xFF));
}
if (command == Commands.MultipleReadData)
{
//TODO THW create expected size out of meterRegister.DataType
if (!expectedLength.HasValue)
{
if ((meterRegister.DataType == typeof(UInt64) || meterRegister.DataType == typeof(Int64)))
{
expectedLength = 8;
}
else
{
expectedLength = 24;
}
}
var numberOfChunks = (UInt16)(expectedLength.Value / chunkSize);
if (expectedLength % chunkSize != 0)
numberOfChunks++;
requestProtocol.Add((Byte)(numberOfChunks & 0xFF));
requestProtocol.Add((Byte)((numberOfChunks >> 8) & 0xFF));
}
//add always the payload to the request protocol
requestProtocol.AddRange(requestPayload);
}
return AddRecordToSendFifo(command, requestProtocol.ToArray(), meterRegister,
hideDataInLog, skipRetryErrorCode);
}
/// <summary>
/// Analyzes the error code
/// </summary>
private void CheckErrorCode()
{
if (_recordInProcess.ResponseErrorBase == (Byte)ConfigExErrors.Base)
{
if (_recordInProcess.ResponseErrorReason == (Byte)ConfigExErrors.AuthenticationFail ||
_recordInProcess.ResponseErrorReason == (Byte)ConfigExErrors.AccessDenied ||
_recordInProcess.ResponseErrorReason == (Byte)ConfigExErrors.LockedOut)
{
_recordInProcess.Acknowledge = RequestAcknowledgeState.AuthorizationRequired;
}
}
}
/// <summary>
/// Reorders the record list so that first entry is the defined topRegister
/// </summary>
/// <param name="topRegister"></param>
public void ReorderRecordList(String topRegister)
{
_recordsSendFifo.Enqueue(_recordInProcess);
//reorder FIFO to bring login at first position
for (var i = 0; i < _recordsSendFifo.Count; i++)
{
_recordsSendFifo.TryPeek(out var tmpRecord);
if (tmpRecord == null || tmpRecord.Register.GetIdent() == topRegister)
{
break;
}
_recordsSendFifo.TryDequeue(out tmpRecord);
_recordsSendFifo.Enqueue(tmpRecord);
}
}
/// <summary>
/// Checks the active register to access
/// </summary>
/// <param name="registerName"></param>
/// <returns></returns>
public Boolean ContainsRegisterIdent(String registerName)
{
return _recordsSendFifo.Any(a => string.Equals(a.Register.GetIdent(),
registerName, StringComparison.CurrentCultureIgnoreCase));
}
}
}