769 lines
37 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|