laatzen/Common/Hardware/WaterMeter/Genesis/GenesisCore/GenesisMeter.cs

2408 lines
90 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Logic.ProductionToProductMapper.Cordonel;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Timers;
using NLog;
using Xylem.Common.CommonCore.Configuration;
using Xylem.Common.CommonCore.Consts;
using Xylem.Common.CommonCore.ThreadWatcher;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore.EventArguments;
using Xylem.Common.Hardware.Interfaces.Ports.SerialPorts;
using Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore;
using Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.EventArguments;
using Xylem.Common.Hardware.Interfaces.Protocols.TransmitProtocol;
using Xylem.Common.Hardware.WaterMeter.Genesis.Applications;
using Xylem.Common.Hardware.WaterMeter.Genesis.Applications.Const;
using Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages;
using Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.EventArguments;
using Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.MeasurementRecords;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.Consts;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd;
using Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol;
using Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.Consts;
using Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.EventArguments;
using Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol;
using Xylem.Common.Hardware.WaterMeter.Genesis.Registers;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore.Consts;
using Xylem.Common.Hardware.WaterMeter.WaterMeterRegisters;
using Xylem.Common.Logic.ProductionOrderCore.File;
using Xylem.Common.Logic.ProductionOrderCore.OrderData;
using Xylem.Common.Logic.RelatePcb;
using Xylem.Common.Logic.SoftwareAccessHelper;
using Xylem.Common.Metrology.Measurements;
using Xylem.Common.Metrology.Measurements.Consts;
using Xylem.Common.Utils.Logging;
using Timer = System.Timers.Timer;
namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
{
/// <inheritdoc cref="IMeter" />
/// <summary>
/// Genesis meter, main class for all action that occurs on production life of meter
/// one meter always have a linked port for UART communication and one for LED (even if you don't need them)
/// it´s has to be open for vb6 COM so don't use any record types or objects that vb6 didn't understand or
/// interprets differently than dotNet
/// </summary>
[Guid("767AEE0F-7321-4ABF-9196-05CC249009B2"),
ComVisible(true),
ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(IMeterEvents))]
public partial class GenesisMeter : IMeter, IMeterEvents, IWaterMeterWithRegisters
{
#region ctor
/// <summary> Default constructor. </summary>
///
/// <remarks> R.Drabesch, 2018-Feb-16. </remarks>
public GenesisMeter()
{
SetLogger();
}
/// <summary>
/// Process configuration
/// </summary>
public ProcessConfig Configuration = new ProcessConfig();
/// <summary>
/// Transmit protocol access for underlie objects to change response timeout
/// </summary>
public ITransmitProtocol TransmitProtocol;
//initially do not signal event
private readonly AutoResetEvent _onSyncKeepSessionThread = new AutoResetEvent(false);
private Thread _keepSessionThread;
private Timer _keepSessionTimer;
private readonly CancellationTokenSource _keepSessionToken = new CancellationTokenSource();
private ILogger _logger;
private ILogger _loggerRawData;
/// <summary>
/// </summary>
/// <param name="slot">Slot number for test-bench </param>
/// <param name="requestPort"><see cref="PortConfig" /> for request Port</param>
/// <param name="streamingPort"><see cref="PortConfig" /> for streaming Port</param>
/// <param name="ignoreCorruptedData">don´t send CRC error or telegrams with error flag to caller</param>
/// <param name="password">
/// If you have a password for highest access level you need, if you don't want to access the meter
/// (just read led record) than you can leave it null
/// </param>
public GenesisMeter(Int32 slot, PortConfig? requestPort, PortConfig? streamingPort,
Boolean ignoreCorruptedData = true, String password = null)
{
_logger = NLogHelper.CreateOrGetMultiLogger($"Slot:{slot}", "Slot", "Meter", "MeterBase", "MeterBase");
SetupGenesisMeter(slot, requestPort, streamingPort, ignoreCorruptedData, password);
}
#endregion
#region Const
/// <summary>
/// Keep up the highest access level for all function in <see cref="GenesisMeter" /> where used
/// </summary>
private const Int32 LoginLvl = 8;
/// <summary>
/// Logout with level 0
/// </summary>
private const Int32 LogoutLvl = 0;
#endregion
#region Properties
private String _serialNumber = "";
/// <inheritdoc />
/// <summary>
/// Property is redundant because is also stored in
/// <see cref="T:Xylem.Sensus.WaterMeter.Genesis.Logic.Domain.Genesis.DataPackages.MeterRegister" /> but never less is
/// is the identification of the meter it has a separate Property
/// </summary>
public String SerialNumber
{
get => _serialNumber;
set
{
_serialNumber = value;
SetLogger();
}
}
/// <inheritdoc />
/// <summary>
/// Save Slot Number (position at test-bench) for logging purposes
/// </summary>
public Int32 Slot
{
get; set;
}
/// <summary>
/// Represent <see cref="Register.Configexchange.PcbSerialNumber" />
/// proposed for login
/// </summary>
public String PcbId
{
get; private set;
}
/// <inheritdoc />
public Int32 IntermediateUpdateTimeS { get; set; } = CommunicationConfig.MeasurementUpdateTimeS;
/// <summary>
/// Add on ctor a password and it will be used for login in if no other password is set
/// </summary>
private String _password;
/// <summary>
/// default false only enabled by <see cref="EnableAutoLogon"/>
/// when it is true, the meter will enforce the login when session is gone or access is denied
/// </summary>
private Boolean _autoLogon;
/// <summary>
/// Enable raw record logging, if set every incoming package will be logged
/// </summary>
private Boolean _enableRawDataLogging;
/// <summary>
/// Enable use of registers that are not valid (last version not match the current app)
/// </summary>
public Boolean IsDevelopmentUsage
{
get; set;
}
/// <summary>
/// All parameter store in this Genesis is up in here
/// if you want to read them from meter use <see cref="ReadRegister" />
/// if you want to set them to meter use <see cref="WriteRegister{T}" />
/// </summary>
private readonly MeterRegisters _configRegister = new MeterRegisters();
/// <summary>
/// All applications defined by configuration.json
/// </summary>
private readonly List<ApplicationDefinition> _configApplications = new List<ApplicationDefinition>();
/// <summary>
/// request port assignment/info
/// </summary>
public IPort RequestPort
{
get; private set;
}
/// <summary>
/// request protocol assignment/info
/// </summary>
public RequestProtocol RequestProtocol;
/// <summary>
/// streaming port assignment/info
/// </summary>
public IPort StreamingPort
{
get; private set;
}
/// <summary>
/// streaming protocol assignment/info
/// </summary>
public StreamingProtocol StreamingProtocol;
/// <summary>
/// to reduce the IrdA communication in test bench, preparation will be done once (when SkipPreparationForTestBench is true)
/// </summary>
public Boolean SkipPreparationForTestBench
{
get; set;
}
/// <summary>
/// Response received after request for record
/// </summary>
public event EventHandler<RequestResponseDataEventArgs> OnRequestRecordReceived;
/// <inheritdoc />
public void SetConfigRegisterDefinitions(List<IRegister> allRegisters)
{
_configRegister.AddRegistersDefinitions(allRegisters);
}
private void SetConfigApplicationDefinitions(List<ApplicationDefinition> allApplications)
{
foreach (var add in allApplications)
{
try
{
var addGenesisApplication = add;
if (addGenesisApplication != null)
{
_configApplications.Add(addGenesisApplication);
}
}
catch (Exception)
{
// ignored
}
}
}
/// <summary>
/// Returns meter registers
/// </summary>
/// <returns></returns>
public MeterRegisters GetConfigRegistersDefinitions()
{
return _configRegister;
}
/// <inheritdoc />
void IWaterMeterWithRegisters.LoadConfiguration(String location)
{
var configFile = ProgramConfig.RegisterDefinitionFileName;
var configFilePath = Path.Combine(location, configFile);
if (!new FileInfo(configFilePath).Exists)
{
configFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
nameof(Genesis), configFile);
if (!new FileInfo(configFilePath).Exists)
{
_logger.Fatal($"Slot:{Slot} - Missing register definition file (configuration.json). Search location: {configFilePath}");
throw new ApplicationException("Missing register definition file (configuration.json). Setup is invalid!");
}
}
try
{
_logger.Trace($"Slot:{Slot} - Registers definitions loaded from {configFilePath}");
if (Configuration.AutoUpdateFiles)
{
_logger.Trace($"Slot:{Slot} - Check for updates of {configFilePath}");
var fi = new FileInfo(configFilePath);
try
{
var str = LocalWebRequest.GetRequest($"{ServiceUrls.FileContentControllerUrl()}GetUpdateFileContent?FileName={fi.Name}&ClientFileDate={fi.CreationTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")}&isDeveloper=0", 5000);
if (!string.IsNullOrEmpty(str.Trim('\"')))
{
var baseFile = JsonConvert.DeserializeObject<BaseFile>(str);
_logger.Trace($"Slot:{Slot} - Updated register definitions File");
if (baseFile != null)
{
File.WriteAllText(configFilePath, baseFile.FileContent);
if (baseFile.UploadDate != null)
File.SetCreationTime(configFilePath, baseFile.UploadDate.Value.UtcDateTime);
}
}
else
{
_logger.Trace($"Slot:{Slot} - Update not needed. Register definition file is up to date.");
}
}
catch (Exception ex)
{
_logger.Warn(ex, $"Slot:{Slot} - Register definition file update went wrong.");
}
}
_logger.Trace($"Slot:{Slot} - Registers definitions loaded from {configFilePath}");
}
catch (Exception ex)
{
_logger.Warn(ex, $"Slot:{Slot} - Register definition file update went wrong.");
}
//builds a configuration file reader from configuration.json and reads all registers definitions
//and application definitions
var registerReader = new GenesisConfigurationReader(configFilePath);
SetConfigRegisterDefinitions(registerReader.ConfigRegistersDefinitions);
SetConfigApplicationDefinitions(registerReader.ConfigApplicationDefinitions);
}
event EventHandler IMeter.OnDisposeCompleted
{
add
{
// throw new NotImplementedException();
}
remove
{
// throw new NotImplementedException();
}
}
#endregion
#region state machine and states
/// <summary>
/// do not use it to set ProcessStatus
/// if you want to change ProcessStatus of this genesis use <see cref="ProcessStatus" />
/// <see cref="_myProcessStatus" /> is just store for some routines
/// </summary>
private ProcessState _myProcessStatus;
/// <inheritdoc />
/// if state is change
/// <see cref="OnProcessStatusChanged" />
/// will be invoked.
/// on some states other events will be invoke as well
public ProcessState ProcessStatus
{
get
{
return _myProcessStatus;
}
set
{
if (_myProcessStatus == value)
{
return;
}
_myProcessStatus = value;
Invoker(OnProcessStatusChanged);
switch (value)
{
case ProcessState.InitIsActive:
Invoker(OnInitCompleted);
break;
case ProcessState.MeasurementIsActive:
Invoker(OnMeasurementInitCompleted);
break;
case ProcessState.MeasurementIsDone:
Invoker(OnMeasurementCompleted);
break;
case ProcessState.CalibrationIsActive:
Invoker(OnCalibInitCompleted);
break;
case ProcessState.CalibrationIsDone:
Invoker(OnCalibCompleted);
break;
case ProcessState.QRefDone:
case ProcessState.WaitForQRef:
case ProcessState.WaitForCalibration:
case ProcessState.WaitForMeasurement:
case ProcessState.IsNotInit:
//no extra event needed
break;
default:
throw new OverflowException("Genesis measurement unknown process status");
}
}
}
/// <summary>
/// do not use it to set ErrorStatus
/// if you want to change ErrorStatus of this genesis use <see cref="ErrorStatus" />
/// <see cref="_myErrorState" /> is just store for some routines
/// </summary>
private ErrorState _myErrorState;
/// fires up
/// <see cref="OnErrorStatusChanged" />
public ErrorState ErrorStatus
{
get
{
return _myErrorState;
}
set
{
_myErrorState = value;
Invoker(OnErrorStatusChanged);
}
}
/// <summary>
/// Logged in to device
/// </summary>
public Boolean IsLoggedOn
{
get; private set;
}
private String _currentActionText = "";
/// <summary>
/// Hold the current process name e.g. FlowTest, Preadjustement for logging
/// </summary>
public String CurrentActionText
{
get
{
return _currentActionText;
}
set
{
_currentActionText = value;
SetLogger();
}
}
/// <summary>
/// Setup water meter from configuration file:
/// -slot,
/// -streaming protocol,
/// -streaming port
/// -request port
/// </summary>
/// <param name="slot"></param>
/// <param name="ignoreCorruptedData"></param>
/// <exception cref="ApplicationException"></exception>
public void SetupFromConfigFile(Int32 slot, Boolean ignoreCorruptedData = true)
{
SetupFromConfigFile(slot, ignoreCorruptedData, true, true);
}
/// <inheritdoc />
public void SetupFromConfigFile(Int32 slot, Boolean ignoreCorruptedData, Boolean useRequest, Boolean useStreaming)
{
Slot = slot;
SetLogger();
var configFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(Genesis), ProgramConfig.SerialConfigFileName);
if (!File.Exists(configFile))
{
configFile = Path.Combine(ProgramConfig.SerialConfigFileName);
if (!File.Exists(configFile))
{
throw new ApplicationException($"Configuration file {configFile} not found ");
}
}
var tr = new StreamReader(configFile);
var slotConfigList = JsonConvert.DeserializeObject<SlotConfig[]>(tr.ReadToEnd());
if (slotConfigList != null)
{
var slotConfig = slotConfigList.FirstOrDefault(t => t.Slot == slot);
if (slotConfig == null)
{
throw new ApplicationException($"Slot:{slot} - File " +
$"{ProgramConfig.SerialConfigFileName} does not contain valid configuration");
}
_logger.Debug($"Slot:{slot} - Configuration loaded");
if (!useRequest)
{
SetupGenesisMeter(slot, null, slotConfig.Streaming, ignoreCorruptedData);
}
else if (!useStreaming)
{
SetupGenesisMeter(slot, slotConfig.Request, null, ignoreCorruptedData);
}
else
{
SetupGenesisMeter(slot, slotConfig.Request, slotConfig.Streaming, ignoreCorruptedData);
}
}
}
private void SetLogger()
{
if (_logger != null)
{
LogManager.Flush();
}
if (string.IsNullOrEmpty(CurrentActionText))
{
_logger = NLogHelper.CreateOrGetMultiLogger($"Slot:{Slot}", $"", "Meter", "MeterBase", "MeterBase");
}
else
{
var id = !string.IsNullOrEmpty(SerialNumber) ? $"SN{SerialNumber}" : $"PCB{PcbId}";
_logger = NLogHelper.CreateOrGetMultiLogger($"Slot:{Slot}_{id}_{CurrentActionText}", $"Slot", "Meter", "MeterBase", "MeterBase");
if (StreamingPort != null && StreamingPort is LedSerialPort)
{
var streamingIdent = $"LedRawData_Slot_{Slot}_{id}_{CurrentActionText}";
((LedSerialPort)StreamingPort).Ident = streamingIdent;
((LedSerialPort)StreamingPort).RefreshLogger();
if (!string.IsNullOrEmpty(SerialNumber) && CurrentActionText.Contains("PG"))
{
var pathMain = NLogHelper.GetPath(_logger);
var pathRaw = NLogHelper.GetPath(((LedSerialPort)StreamingPort).GetRawLogger());
var basePath = Path.GetDirectoryName(pathMain);
//
var testRunString = CurrentActionText.Replace("PG", "");
var lastChat = pathMain.LastIndexOf('\\');
pathMain = pathMain.Substring(lastChat + 1);
pathRaw = Path.GetFileName(pathRaw);
var tmpUrl = $"http://10.49.40.25/MeterProcessState/api/TestBench/SetMeterLogPathInfo?SerialNumber={SerialNumber}&TestRunId={testRunString}&BasePath={basePath}&MainLogName={pathMain}&LEDName={pathRaw}";
_logger.Debug($"Request Url: { tmpUrl}");
//ToDO: Update to service URl
LocalWebRequest.GetRequest(tmpUrl, 5000);
}
}
}
}
/// <summary>
/// just internal setup. called on ctor or from vb6 setup function
/// </summary>
/// <param name="slot">Slot Number for test-bench </param>
/// <param name="requestPort"><see cref="PortConfig" /> for RFID/UART/IrDA Port</param>
/// <param name="streamingPort"><see cref="PortConfig" /> for LED Port</param>
/// <param name="ignoreCorruptedData">don´t send CRC error or telegrams with error flag to caller</param>
/// <param name="password">
/// If you have a password for highest access level you need, if you don't want to access the meter
/// (just read led record) than you can leave it nullS
/// </param>
public void SetupGenesisMeter(Int32 slot, PortConfig? requestPort, PortConfig? streamingPort,
Boolean ignoreCorruptedData = true, String password = "")
{
Slot = slot;
SetLogger();
_password = password;
if (requestPort != null)
{
String requestIdent;
if (requestPort.Value.Type == "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.RfidSerialPort")
{
requestIdent = $"Slot:{Slot}, Port:{requestPort.Value.PortName}, Protocol:Request, Type:RFID -";
//CRC calculation does NOT include the CRC itself
RequestProtocol = new RequestProtocol(requestIdent);
TransmitProtocol = new RfidTransmitProtocol(requestPort.Value.PortName);
RequestProtocol.SetTransmitProtocol(TransmitProtocol);
RequestPort = new RfidSerialPort(requestIdent, requestPort.Value.GetSerialPort(),
TransmitProtocol.GetTransmitPortSettings());
}
else if (requestPort.Value.Type == "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.UartSerialPort")
{
requestIdent = $"Slot:{Slot}, Port:{requestPort.Value.PortName}, Protocol:Request, Type:UART -";
//CRC calculation does include the CRC itself filled with 0x0000 in advance
RequestProtocol = new RequestProtocol(requestIdent);
TransmitProtocol = new UartTransmitProtocol(requestPort.Value.PortName);
RequestProtocol.SetTransmitProtocol(TransmitProtocol);
RequestPort = new UartSerialPort(requestIdent, requestPort.Value.GetSerialPort(),
TransmitProtocol.GetTransmitPortSettings());
}
else if (requestPort.Value.Type == "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.IrdaSerialPort")
{
requestIdent = $"Slot:{Slot}, Port:{requestPort.Value.PortName}, Protocol:Request, Type:IrDA -";
RequestProtocol = new RequestProtocol(requestIdent);
TransmitProtocol = new IrdaTransmitProtocol(requestPort.Value.PortName);
RequestProtocol.SetTransmitProtocol(TransmitProtocol);
RequestPort = new IrdaSerialPort(requestIdent, requestPort.Value.GetSerialPort(),
TransmitProtocol.GetTransmitPortSettings());
}
}
if (streamingPort != null)
{
var ledTransmit = new LedTransmitProtocol(streamingPort.Value.PortName);
var streamingIdent = $"Slot:{Slot}, Port:{streamingPort.Value.PortName}, Protocol:LED";
StreamingPort = new LedSerialPort(streamingIdent, streamingPort.Value.GetSerialPort(),
ledTransmit.GetTransmitPortSettings());
StreamingProtocol = new StreamingProtocol(streamingIdent, ignoreCorruptedData);
}
Configuration = new ProcessConfig(ProgramConfig.ConfigFileName);
_logger.Debug($"Slot:{Slot} - Setup done");
}
/// <inheritdoc />
/// <summary>
/// Start Event listening, has to be called after <see cref="SetupGenesisMeter" />
/// </summary>
public void ConnectMeter()
{
if (RequestPort != null && RequestPort.GetPortName() != BaseSerialPort.PortNotAssigned)
{
LinkPortToProtocol(RequestPort, RequestProtocol);
_logger.Trace($"Slot:{Slot} - RequestPort and RequestProtocol linked");
RequestProtocol.OnMeterRegisterUpdated += RequestProtocol_MeterRegisterUpdated;
RequestProtocol.OnRecordIsDecoded += RequestProtocol_ApplyDecodedRecord;
RequestProtocol.OnAuthorizationGrant += RequestProtocol_AuthorizationGrant;
}
if (StreamingPort != null && StreamingPort.GetPortName() != BaseSerialPort.PortNotAssigned)
{
LinkPortToProtocol(StreamingPort, StreamingProtocol);
_logger.Trace($"Slot:{Slot} - StreamingPort and StreamingProtocol linked");
StreamingProtocol.OnRecordIsDecoded += StreamingProtocol_ApplyDecodedRecord;
}
_logger.Debug($"Slot:{Slot} - Connected and communication started");
}
/// <summary>
/// Add a port to working queue
/// </summary>
/// <param name="port">On <see cref="IPort" /> should one once added in runtime</param>
/// <param name="protocol"></param>
public void LinkPortToProtocol(IPort port, IProtocol protocol)
{
//for streaming mode only
if (port == null)
{
return;
}
port.OnRawRecordReceived += delegate (Object o, BasePortDataEventArgs rawMsg)
{
protocol.FillDecodingBuffer(rawMsg);
};
try
{
if (!port.IsOpen())
{
port.Open();
}
}
catch (Exception ex)
{
throw new ApplicationException($"Cannot Open Comport. Check TaskManager for other Genesis relevant programs and close them. Also check if the comport is valid! { Environment.NewLine }{ ex.Message}");
}
protocol.OnRecordReadyToSend += delegate (Object o, BasePortDataEventArgs dataToSend)
{
var data = (List<Byte>)dataToSend.GetData();
port.PortWrite(data.ToArray());
};
}
/// <summary>
/// Synchronization of the record for the streaming interface
/// </summary>
/// <param name="markRecord"></param>
/// <exception cref="ApplicationException"></exception>
public void SyncStreamingBuffer(SyncMarkRecord markRecord)
{
if (StreamingPort == null)
{
throw new ApplicationException("Synchronization not possible due to undefined streaming port");
}
StreamingPort.SynchronizeReceiveBuffer(markRecord);
}
private void RequestProtocol_AuthorizationGrant(Object sender, EventArgs e)
{
_logger.Debug($"Slot:{Slot} - Authorization succeeded");
}
#endregion
#region Events
//public event EventHandler<StringEventArgs> HasInformationForCaller;
/// <inheritdoc cref="IMeter" />
public event EventHandler OnCalibCompleted;
/// <inheritdoc cref="IMeter" />
public event EventHandler OnErrorStatusChanged;
/// <inheritdoc cref="IMeter" />
public event EventHandler OnProcessStatusChanged;
/// <inheritdoc cref="IMeter" />
public event EventHandler OnCalibInitCompleted;
/// <inheritdoc cref="IMeter" />
public event EventHandler OnInitCompleted;
/// <inheritdoc cref="IMeter" />
public event EventHandler OnMeasurementInitCompleted;
/// <inheritdoc cref="IMeter" />
public event EventHandler OnMeasurementCompleted;
/// <summary>
/// call this if session is expired and you need an re-authorization
/// </summary>
public EventHandler<AuthorizationRequiredEventArgs> AuthorizationRequired;
/// <summary>
/// A helper to invoke events only if someone is listening, otherwise nothing will happen
/// </summary>
/// <param name="handler">Event handler to call</param>
/// <param name="sender">Sender (can be null)</param>
/// <param name="e">Event arguments (can be null)</param>
private void Invoker(EventHandler handler, Object sender = null, EventArgs e = null)
{
if (handler != null)
{
if (sender == null)
{
sender = this;
}
if (e == null)
{
e = new EventArgs();
}
handler.Invoke(sender, e);
}
// ReSharper disable once RedundantIfElseBlock
else
{
//nobody is listening
}
}
/// <summary>
/// Track all incoming led record packages (Calibration and Flow)
/// </summary>
/// <param name="sender"></param>
/// <param name="e">
/// I should be <see cref="FlowTestRecord" /> or <see cref="CalibDataEventArgs" />
/// otherwise this method does nothing
/// </param>
private void StreamingProtocol_ApplyDecodedRecord(Object sender, BaseDataEventArgs e)
{
var result = e.GetEventData();
if (result is IMeasurementRecord)
{
if (result is FlowTestRecord)
{
if (!((FlowTestRecord)result).IsValid)
{
return;
}
}
if (result is CalibrationRecord)
{
if (!((CalibrationRecord)result).IsValid)
{
return;
}
}
AddData(((IMeasurementRecord)result));
}
}
/// <summary>
/// tracking request record processing
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void RequestProtocol_ApplyDecodedRecord(Object sender, BaseDataEventArgs e)
{
if (e is RequestResponseDataEventArgs)
{
OnRequestRecordReceived?.Invoke(sender, (RequestResponseDataEventArgs)e);
}
}
#endregion
#region Commands
/// <summary>
/// QueryCaps can send before login
/// arrange record for communication (e.g. Baud rate)
/// </summary>
/// <returns>
/// Running outgoing command
/// </returns>
public RequestRecord QueryCaps()
{
_logger.Debug($"Slot:{Slot} - Request capabilities");
return RequestProtocol.CommandToMeter(Commands.QueryCaps);
}
/// <summary>
/// The backup login level is needed for re-authorization
/// </summary>
private Int32 _backupLoginLvl;
/// <summary>
/// SetRegister <see cref="Register.Configexchange.Privilege" /> to loginLvl and wait for response
/// </summary>
/// <param name="loginLvl"></param>
/// <param name="runImmediately"> true = process command; false = add command to list. call
/// <see cref="RequestProtocolProcess()"></see> to process login command </param>
/// <returns> if is completed and no error occurred it is true </returns>
public Boolean PrivilegeCommandToMeter(Int32 loginLvl, Boolean runImmediately = true)
{
_logger.Debug($"Slot:{Slot} - Set privilege login level({loginLvl})");
_backupLoginLvl = loginLvl;
var result = WriteRegister(Register.Configexchange.Privilege, loginLvl);
if (!runImmediately)
{
return true;
}
RequestProtocolProcess();
return result;
}
/// <summary>
/// Set <see cref="Register.Configexchange.Password" />
/// </summary>
/// <param name="password"> if null , use password from initialization</param>
/// <param name="runImmediately"> true = process command; false = add command to list. call
/// <see cref="RequestProtocolProcess()"></see> to process login command </param>
/// <returns>
/// </returns>
private RequestRecord LoginCommandToMeter(String password, Boolean runImmediately = true)
{
try
{
var regDef = _configRegister.GetRegisterDefinitionByName(Register.Configexchange.Password);
StopKeepSession();
//restore password if new password is zero
if (string.IsNullOrEmpty(password))
{
password = _password;
}
if (string.IsNullOrEmpty(password))
{
throw new AuthenticationException("Password is empty");
}
//set new password
_password = password;
_logger.Debug($"Slot:{Slot} - Login with password");
//Due to password is defined as uint96_t this direct communication will be used instead of
//WriteRegister. The data shouldn't be resorted MSB<->LSB like other unit data types!
var responseRecord = RequestProtocol.CommandToMeter(Commands.MultipleWriteData, regDef,
Encoding.ASCII.GetBytes(password), hideDataInLog: true);
if (runImmediately)
{
RequestProtocolProcess();
}
StartKeepSession();
return responseRecord;
}
catch (Exception e)
{
_logger.Error(e);
}
return null;
}
/// <summary>
/// Clear password to force new password reading
/// </summary>
/// <remarks date="2023JAN21" author="R.Drabesch">
/// - Initial.
/// </remarks>
public void ClearPassword()
{
_password = string.Empty;
}
/// <summary>
/// Acquires the password from WEB-API.
/// </summary>
/// <returns>password</returns>
/// <remarks date="2018JAN08" author="R.Drabesch">
/// - Initial.
/// </remarks>
/// <remarks date="2022-Nov-02" author="T.Wiedebusch">
/// - PcbId handling improved.
/// </remarks>
/// <remarks date="2022-Dec-12" author="T.Wiedebusch">
/// - PcbId unknown returns immediately.
/// </remarks>
/// <remarks date="2023-Jan-19" author="T.Wiedebusch">
/// - Password had been reset to empty string if read from file, corrected!
/// </remarks>
/// <remarks date="2023-Feb-28" author="T.Wiedebusch">
/// - Used <see cref="MeterPwdHandlerDb.GetPasswordFromDb"/>
/// </remarks>
private String GetPassword()
{
// without PCB ID it is not possible to login,
// but if it is already assigned to this Genesis, don't read it again
if (string.IsNullOrEmpty(PcbId))
{
//GetPcbId fills the PsbId property
if (string.IsNullOrEmpty(GetPcbId()) || string.IsNullOrEmpty(PcbId) || PcbId.Length < 8)
{
if (string.IsNullOrEmpty(GetPcbId()))
{
return "";
}
}
}
// on unknown password try to get it from DB
if (MeterPwdHandlerDb.GetPasswordFromDb(PcbId, out var password))
{
_logger.Info($"Slot:{Slot} - Got password from GenesisPasswordService, " +
$"URL({ServiceUrls.GenesisGetPasswordServiceUrl() + PcbId})");
}
else
{
_logger.Info($"Slot:{Slot} - Cannot get password from GenesisPasswordService, " +
$"URL({ServiceUrls.GenesisGetPasswordServiceUrl() + PcbId}))");
// try to read the password from an offline password file
if (!File.Exists(ProgramConfig.OfflineInfoFile))
return password;
var listOfOfflinePasswords = JsonConvert.DeserializeObject<List<OfflinePasswordItem>>(
File.ReadAllText(ProgramConfig.OfflineInfoFile));
if (listOfOfflinePasswords != null)
password = listOfOfflinePasswords.First(x => x.PcbId == PcbId).Password;
if (!string.IsNullOrEmpty(password))
{
_logger.Info($"Slot:{Slot} - Got password for PcbId:{PcbId} from password file!");
}
}
return password;
}
/// <inheritdoc />
/// <remarks date="2018JAN08" author="drabesch">
/// Get password from server
/// </remarks>
public Boolean Login()
{
if (string.IsNullOrEmpty(_password) || string.IsNullOrEmpty(PcbId))
{
var password = GetPassword();
if (string.IsNullOrEmpty(password))
{
return false;
}
return Login(password);
}
return Login(_password);
}
/// <summary>
/// Login with identical password as last time
/// </summary>
/// <returns></returns>
/// <remarks date="2019-Jun-23" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2022-Jul-22" author="Thomas Wiedebusch">
/// - Immediately returns if already logged in.
/// </remarks>
public Boolean ReLogin()
{
if (IsLoggedOn)
return true;
return string.IsNullOrEmpty(_password) ? Login() : Login(_password);
}
private class OfflinePasswordItem
{
public OfflinePasswordItem(String pcbId, String password)
{
PcbId = pcbId;
Password = password;
}
public String PcbId
{
get;
}
public String Password
{
get;
}
}
/// <summary>
/// Starting a timer to keep session active,
/// starting the <see cref="KeepSessionThreadLoop"/>.
/// </summary>
/// <param name="password">password string to login</param>
/// <param name="runImmediately"> true = process command; false = add command to list. call
/// <see cref="RequestProtocolProcess()"></see> to process login command </param>
/// <param name="skipReadMeterFwAndAssignRegisters"></param>
/// <returns></returns>
/// <remarks date="2019JAN01" author="drabesch">
/// pass runImmediately for auto login
/// </remarks>
/// <remarks date="2021-Feb-02" author="Thomas Wiedebusch">
/// - Avoid retries during login, a retry will lock the Genesis for 2s, 4s, 8s, 16s and so on.
/// </remarks>
/// <remarks date="2023-Feb-20" author="Thomas Wiedebusch">
/// - Quick login removed.
/// </remarks>
/// <remarks date="2023-Mar-06" author="Thomas Wiedebusch">
/// - Pulse module deactivated at login.
/// </remarks>
/// <remarks date="2023-Mar-07" author="Thomas Wiedebusch">
/// - REMOVED: Pulse module deactivated at login.
/// - Skip FW readout to quick connect being able to immediately switch off the pulse mode before App detection.
/// </remarks>
public Boolean Login(String password, Boolean runImmediately = true, Boolean skipReadMeterFwAndAssignRegisters = false)
{
var backupRetries = CommunicationConfig.RequestRetries;
CommunicationConfig.RequestRetries = 0;
PrivilegeCommandToMeter(LoginLvl, runImmediately);
var responseRecord = LoginCommandToMeter(password, runImmediately);
if (!runImmediately)
{
RequestProtocol.ReorderRecordList(Register.Configexchange.Privilege);
}
RequestProtocol.ProcessRecordList();
IsLoggedOn = responseRecord.Acknowledge == RequestAcknowledgeState.Ok
&& responseRecord.ResponseErrorBase == 0
&& responseRecord.ResponseErrorReason == 0;
_password = password;
if (IsLoggedOn)
{
_logger.Info($"Slot:{Slot} - Successful logged in to meter");
StartKeepSession();
if (Configuration != null && !Configuration.ProductionMode)
{
IsDevelopmentUsage = true;
}
if (!skipReadMeterFwAndAssignRegisters)
ReadMeterFirmwareAndAssignRegisters();
}
else
{
_logger.Warn($"Slot:{Slot} - Login to meter failed!");
}
CommunicationConfig.RequestRetries = backupRetries;
return IsLoggedOn;
}
/// <summary>
/// Building the FW Version string out of 2 bytes of data.
/// REASON: FLEXNETVERSION is one application which does not follow the same rule of
/// decimal numbers. Instead it uses hexadecimal digits. After the conversion of this
/// number to a pure Uint32, needed for comparison of file versions in configuration.json,
/// the hexadecimal outline will be lost and the comparison with the rowproduct.txt fails.
/// FW-update needs this information to validate a tested package!
/// </summary>
/// <param name="msb">most significant byte</param>
/// <param name="lsb">last significant byte</param>
/// <returns>converted "msb.lsb" as string e.g. "12.34" or "2.0C" </returns>
public static String BuildFwVersionString(Byte msb, Byte lsb)
{
//skip leading 0
var msbPartString = (msb & 0xF0) > 0 ? $"{(msb & 0xF0) >> 4:X0}" : "";
return msbPartString + $"{msb & 0x0F:X0}" + "." + $"{(lsb & 0xF0) >> 4:X1}" + $"{lsb & 0x0F:X1}";
}
/// <summary>
/// Building a version string with 2 decimals e.g. " 12.34"
/// </summary>
/// <remarks date="2022-Dec-02" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
/// <param name="version">the version</param>
/// <returns>converted version to string </returns>
public static String BuildFwVersionString(Int32? version)
{
return version == null ? "0.00" : $@"{version / 100}.{version % 100:D2}";
}
/// <summary>
/// List of all present meter applications
/// </summary>
public List<MeterApplications> MeterAppListVersion = new List<MeterApplications>();
/// <summary>
/// Core revision of boot code.
/// </summary>
//public String CoreRevision;
public Int32? CoreRevision;
/// <summary>
/// This is the FLEXNETVERSION version which describes the entire packet.
/// </summary>
public String FwVersion;
/// <summary>
/// This is the Metrology Lookup Table CRC.
/// </summary>
public String LutCrc;
/// <summary>
/// This is the meter size.
/// </summary>
public String MeterSize;
/// <summary>
/// Region (EMEA, NA or China).
/// </summary>
public String Region;
/// <summary>
/// Radio frequency in MHz (433 or 868 or null).
/// </summary>
public Int32? RadioFrequencyMhz;
/// <summary>
/// Metrology upgrade permission.
/// </summary>
public Byte MetrologyUpgradePermission;
/// <summary>
/// Check region and size.
/// After reading the version, all valid registers are going to be selected.
/// </summary>
/// <remarks date="2023-Jan-19" author="Thomas Wiedebusch">
/// - Initial, extracted from <see cref="ReadMeterFirmwareAndAssignRegisters"/>
/// </remarks>
public void CheckRegionSizeLutCrc()
{
//read metrology lookup table CRC
try
{
var lutCrcRaw = ReadRegister(Register.Genesisflow.LookupFileCrc);
if (lutCrcRaw != null)
{
var lutCrc = RegisterConverter.ConvertTo<UInt32>(lutCrcRaw);
LutCrc = $@"0x{lutCrc:X4}";
}
}
catch (Exception)// exception will be thrown if register is not present on versions without LUT
{
LutCrc = Constants.StrUnknown;
}
//read meter size
MeterSize = MeterSizeConverter.ConvertMeterSizeEnumToSizeName((MeterSize)RegisterConverter.ConvertTo<Byte>(
ReadRegister(Register.Genesisflow.MeterSize)));
//read radio frequency
Region = Constants.StrUnknown;
RadioFrequencyMhz = null;
try
{
var radioFrequencyRaw = ReadRegister(Register.Sensusradio.FrequencyIndicator);
if (radioFrequencyRaw != null)
{
Region = "EMEA";
RadioFrequencyMhz = RegisterConverter.ConvertTo<Int32>(radioFrequencyRaw);
}
else
{
Region = "NA";
}
}
catch (Exception) // exception will be thrown on uninstalled SENSUSRADIO
{
Region = "NA";
}
// check meter size string contains "DN", because if "US" it cannot be an EMEA version
if ((Region.Contains("NA") && MeterSize.Contains("DN")) ||
(Region.Contains("EMEA") && MeterSize.Contains("US")))
{
Region = Constants.StrUnknown;
MeterSize = Constants.StrUnknown;
LutCrc = Constants.StrUnknown;
}
}
/// <summary>
/// Reading all applications which can be found in the configuration.json and have been stored
/// to the meter register dictionary in advance.
/// The read process covers the FW version and the CRC.
/// After reading the version, all valid registers are going to be selected.
/// </summary>
/// <remarks date="2021-Mar-10" author="Thomas Wiedebusch">
/// - Removed "Cordonel " from CoreRevision to have the e.g. "1.64" remaining
/// </remarks>
/// <remarks date="2021-Apr-15" author="Thomas Wiedebusch">
/// - Read radio frequency
/// </remarks>
/// <remarks date="2022-Mar-30" author="Thomas Wiedebusch">
/// - Read lockup table CRC,
/// - Read meter size.
/// </remarks>
/// <remarks date="2022-Apr-27" author="Thomas Wiedebusch">
/// - Read lockup table CRC bugfix with try, catch for legacy versions, where this register does not exists.
/// </remarks>
/// <remarks date="2022-Dec-02" author="Thomas Wiedebusch">
/// - CoreRevision from String to Int32?,
/// - MeterSize from String to MeterSize,
/// - Region separated from RadioRegion as String,
/// - RadioFrequencyMhz introduced as Int32? (null if "NA" region or not installed),
/// - LutCrc from String to Int32?.
/// </remarks>
/// <remarks date="2023-Jan-16" author="Thomas Wiedebusch">
/// - Checked meter size string for "DN", than it cannot be an EMEA version.
/// </remarks>
/// <remarks date="2023-Feb-13" author="Thomas Wiedebusch">
/// - Removed retry as this will be handled by the <see cref="RequestProtocol"/>.
/// </remarks>
private void ReadMeterFirmwareAndAssignRegisters()
{
//avoid overwriting of list if this already exits
if (MeterAppListVersion.Any())
{
_logger.Warn($"Slot:{Slot} - Trying to override meter dictionary. Skip read out FW");
return;
}
//Get all existing applications from meter register dictionary
foreach (var appVersionToRead in _configApplications.OrderBy(o => o.AppId))
{
UInt32 readFwVersion = 0;
UInt16 readFwCrc = 0;
var isInstalled = false;
var versionString = "";
if (WriteRegister(Register.System.CheckFwPresence, appVersionToRead.AppId))
{
//read version, skip retries on SYSTEM 0x04-> FW nor present is a valid feedback
var readFwVersionBytes = ReadRegister(Register.System.CheckFwPresence, 4);
//the FW version as Uint32 will be used for comparison with the configuration.json
if (readFwVersionBytes != null && readFwVersionBytes.Length > 0)
{
readFwVersion = (UInt32)(((readFwVersionBytes[1] & 0xF0) >> 4) * 10 +
(readFwVersionBytes[1] & 0x0F)) * 100;
readFwVersion += (UInt32)(((readFwVersionBytes[0] & 0xF0) >> 4) * 10 +
(readFwVersionBytes[0] & 0x0F));
//build the version string, because e.g. FLEXNETVERSION is one application which does not
//follow the same rule. After the upper conversion, the hexadecimal outline will be lost
versionString = BuildFwVersionString(readFwVersionBytes[1], readFwVersionBytes[0]);
if (appVersionToRead.AppName == "FLEXNETVERSION")
FwVersion = versionString;
if (readFwVersion != 0)
{
isInstalled = true;
//write application Id to CRC register
if (WriteRegister(Register.System.CheckFwCrc, appVersionToRead.AppId))
{
readFwCrc = RegisterConverter.ConvertTo<UInt16>(ReadRegister(Register.System.CheckFwCrc));
}
}
}
}
//add application always, even if this is not installed. This will be used for FW updates
MeterAppListVersion.Add(new MeterApplications()
{
AppId = appVersionToRead.AppId,
Version = readFwVersion,
StrVersion = versionString,
AppName = appVersionToRead.AppName,
Crc = readFwCrc,
Status = isInstalled ? MeterAppState.Unknown : MeterAppState.MeterAppNotInstalled,
IsInstalled = isInstalled,
});
}
CheckRegionSizeLutCrc();
//read metrology upgrade permission
MetrologyUpgradePermission = RegisterConverter.ConvertTo<Byte>(
ReadRegister(Register.System.MetrologyUpgradePermission));
//read core revision
var coreRegisterRead = RegisterConverter.ConvertTo<String>(ReadRegister(Register.System.CoreRevision, 16));
CoreRevision = null;
if (!string.IsNullOrEmpty(coreRegisterRead))
{
var coreRevisionStrings = coreRegisterRead.Split('\0');
//remove Cordonel sub-string and the blanks
var coreRevisionSubString = $"{coreRevisionStrings[0].ToLower().Replace("cordonel ", "")}";
var coreRevisionSubStrings = coreRevisionSubString.Split('.');
if (int.TryParse(coreRevisionSubStrings[0], out var coreRevisionMsb))
{
if (int.TryParse(coreRevisionSubStrings[1], out var coreRevisionLsb))
{
CoreRevision = coreRevisionMsb * 100 + coreRevisionLsb;
}
}
}
//log application information to file
_logger.Info($"Slot:{Slot} - PCB ID: {PcbId}");
_logger.Info($"Slot:{Slot} - Date time (UTC): {DateTimeOffset.UtcNow}");
if (CoreRevision != null)
{
_logger.Info($"Slot:{Slot} - System Core Revision: {BuildFwVersionString(CoreRevision)}");
}
_logger.Info($"Slot:{Slot} - Region: {Region}");
if (RadioFrequencyMhz != null)
{
_logger.Info($"Slot:{Slot} - Radio frequency [MHz]: {RadioFrequencyMhz}");
}
foreach (var fm in MeterAppListVersion.OrderBy(o => o.AppId))
{
var versionString = fm.IsInstalled
? $"V: {fm.StrVersion} - CRC: 0x{fm.Crc:X4}" : "Not installed";
_logger.Info($"Slot:{Slot} - AppId: 0x{fm.AppId:X2} - {versionString} - AppName: {fm.AppName}");
}
_logger.Info($"Slot:{Slot} - Meter size: {MeterSize}");
_logger.Info($"Slot:{Slot} - Upgrade permission: {MetrologyUpgradePermission:X2}");
//remove unused registers with invalid version
var tempConfigRegisters = _configRegister.MeterRegisterDic.ToList();
_configRegister.MeterRegisterDic.Clear();
foreach (var registerToCheck in tempConfigRegisters)
{
var currentGroup = MeterAppListVersion.First(f => f.AppId == registerToCheck.Key.AppAddress);
if (!registerToCheck.Key.RegisterDetail.Version.First.HasValue &&
!registerToCheck.Key.RegisterDetail.Version.Last.HasValue)
{
//TODO legacy to collect not clear specified registers
registerToCheck.Key.IsAvailable = true;
_configRegister.MeterRegisterDic.TryAdd(registerToCheck.Key, null);
continue;
}
if (registerToCheck.Key.RegisterDetail.Version.Last != null &&
registerToCheck.Key.RegisterDetail.Version.First != null &&
registerToCheck.Key.RegisterDetail.Version.First.Value <= currentGroup.Version &&
registerToCheck.Key.RegisterDetail.Version.Last.Value >= currentGroup.Version)
{
registerToCheck.Key.IsAvailable = true;
_configRegister.MeterRegisterDic.TryAdd(registerToCheck.Key, null);
}
}
foreach (var checkMinRegister in Register.GetMinRequiredRegisters())
{
if (_configRegister.MeterRegisterDic.All(a => a.Key.GetIdent() != checkMinRegister))
{
// These registers ar required for essential operation, they have to be added even if the
// version in the configuration.json is outdated or they are not defined!
try
{
var allVersion = tempConfigRegisters.Where(a => a.Key.GetIdent() == checkMinRegister)?.OrderByDescending(
s => s.Key.RegisterDetail.Version.Last)?.First();
if (allVersion.HasValue)
{
allVersion.Value.Key.RegisterDetail.Version.Last = int.MaxValue;
_configRegister.MeterRegisterDic.TryAdd(allVersion.Value.Key, null);
}
}
catch (Exception ex)
{
_logger.Warn(ex, $"Slot:{Slot} - Not able to set minimum list of registers. check json file for {checkMinRegister} ");
}
}
}
if (IsDevelopmentUsage)
{
var registerToPotentialAdd = new List<RegisterDefinition>();
foreach (var allRegisterItem in tempConfigRegisters)
{
if (_configRegister.MeterRegisterDic.All(a => a.Key.GetIdent() != allRegisterItem.Key.GetIdent()))
{
registerToPotentialAdd.Add(allRegisterItem.Key);
}
}
if (registerToPotentialAdd.Any())
{
var registerAddForOldConfigs = registerToPotentialAdd.GroupBy(item => item.GetIdent())
.Select(grp => grp.Aggregate((max, cur) =>
(max == null || cur.RegisterDetail.Version.Last > max.RegisterDetail.Version.Last) ?
cur : max));
foreach (var registerAddOldConfig in registerAddForOldConfigs)
{
registerAddOldConfig.IsAvailable = false;
_configRegister.MeterRegisterDic.TryAdd(registerAddOldConfig, null);
}
}
}
}
/// <summary>
/// Automatic login to meter enabled if logged out by lost authentication
/// </summary>
public void EnableAutoLogon()
{
_autoLogon = true;
StartKeepSession();
}
private void StartKeepSession()
{
if (!_autoLogon || string.IsNullOrEmpty(PcbId))
{
return;
}
if (RequestProtocol == null)
{
return;
}
if (_keepSessionThread == null)
{
_keepSessionThread = new Thread(KeepSessionThreadLoop) { Name = $"Slot:{Slot} Keep session thread" };
ThreadWatcher.Instance.Start(_keepSessionThread);
_keepSessionTimer = new Timer(CommunicationConfig.KeepSessionTimeS * 1000);
_keepSessionTimer.Elapsed += KeepSessionTimerElapsed_SyncKeepSessionThread;
_keepSessionTimer.Start();
}
}
private void StopKeepSession()
{
//if (!_autoLogon) return;
_autoLogon = false;
if (_keepSessionTimer != null)
{
_keepSessionTimer.Stop();
_keepSessionTimer.Dispose();
}
if (_keepSessionThread != null)
{
_onSyncKeepSessionThread.Dispose();
_keepSessionThread = null;
}
if (RequestProtocol != null)
{
}
}
/// <summary>
/// If the timeout timer elapsed to keep the session active, this routine will be called
/// awaking the <see cref="KeepSessionThreadLoop"/>.
/// </summary>
/// <param name="sender"></param>
/// <param name="eventARgs"></param>
private void KeepSessionTimerElapsed_SyncKeepSessionThread(Object sender, ElapsedEventArgs eventARgs)
{
if (_autoLogon)
{
//put KeepSessionThread state from WaitSleepJoin to Running
_onSyncKeepSessionThread.Set();
}
}
/// <summary>
/// Thread to keep the connection to the meter open and the session alive,
/// because on missing communication the meter is going to logout automatically.
/// This thread will be started at <see cref="Login()"/> and aborted on
/// <see cref="Logout"/>
/// </summary>
private void KeepSessionThreadLoop()
{
try
{
while (_keepSessionThread != null && _keepSessionThread.IsAlive && !_keepSessionToken.IsCancellationRequested)
{
//put KeepSessionThread to WaitSleepJoin until next timer elapsed
_onSyncKeepSessionThread.WaitOne();
if (_autoLogon)
{
//check the communication time
_logger.Info($"Slot:{Slot} - Check last communication time");
if (RequestProtocol.LastCommTime.HasValue &&
RequestProtocol.LastCommTime.Value.AddSeconds(CommunicationConfig.KeepSessionTimeS)
< DateTimeOffset.UtcNow)
{
_logger.Info($"Slot:{Slot} - Force communication to keep session");
ReadRegister(Register.Genesisflow.LedMode);
}
}
}
}
catch (ThreadAbortException)
{
}
}
/// <inheritdoc />
public Boolean Logout()
{
//check if not logged in
if (!IsLoggedOn)
{
return true;
}
StopKeepSession();
_autoLogon = false;
if (PrivilegeCommandToMeter(LogoutLvl))
{
_logger.Info($"Slot:{Slot} - Successful logged out from meter");
IsLoggedOn = false;
return true;
}
_logger.Warn($"Slot:{Slot} - Logout from meter failed");
return false;
}
/// <summary>
/// Check if EMPTY_PIPE or REBOOT is set
/// </summary>
/// <returns></returns>
public Boolean CheckStateAfterMeasurement()
{
return HasAlarms(Alarm.EMPTY_PIPE | Alarm.REBOOT);
}
/// <summary>
/// Check if alarmToCheck is set on register
/// </summary>
/// <param name="alarmToCheck">is Flags so you can use more than one alarms (like Alarm.EMPTY_PIPE | Alarm.REBOOT)</param>
/// <returns></returns>
public Boolean HasAlarms(Alarm alarmToCheck)
{
var currentAlarms = GetAlarms();
return currentAlarms.HasFlag(alarmToCheck);
}
/// <summary>
/// Read out all AlarmStatus register and combine them to one <see cref="Alarm"/>
/// </summary>
/// <returns></returns>
public Alarm GetAlarms()
{
List<String> alarms = new List<String>()
{
Register.Customer.AlarmStatus0,
Register.Customer.AlarmStatus1,
Register.Customer.AlarmStatus2,
Register.Customer.AlarmStatus3,
Register.Customer.AlarmStatus4,
Register.Customer.AlarmStatus5,
Register.Customer.AlarmStatus6,
Register.Customer.AlarmStatus7
};
List<Byte> responseAllAlarms = new List<Byte>();
foreach (var regRead in alarms)
{
var a = ReadRegister(regRead);
if (a != null)
{
foreach (var resp in a)
{
responseAllAlarms.Add(resp);
}
}
}
var i = 0;
var cReg = 0;
foreach (var val in responseAllAlarms)
{
if (val > 0)
{
i += (1 << cReg);
}
cReg += 1;
}
var fullAlarm = (Alarm)i;
return fullAlarm;
//ReadRegister()
}
/// <summary>
/// Reset the empty pipe alarm
/// </summary>
public void ResetAlarm(Alarm clear)
{
WriteRegister(Register.Customer.TriggerAlarmCancel, clear.GetHashCode());
}
/// <summary>
/// Alarm messages
/// </summary>
[Flags]
public enum Alarm
{
// ReSharper disable InconsistentNaming
#pragma warning disable CS1591
REBOOT = 1 << 0,
LOW_BATTERY = 1 << 1,
EMPTY_PIPE = 1 << 4,
REVERSE_FLOW = 1 << 6,
SUSPECT_LEAK = 1 << 7,
BROKEN_PIPE = 1 << 8,
LOW_PRESSURE = 1 << 9,
HIGH_PRESSURE = 1 << 10,
LOW_TEMPERATURE = 1 << 11,
HIGH_TEMPERATURE = 1 << 12,
RADIO_ERROR = 1 << 13,
METROLOGY_PARAMS = 1 << 14,
METROLOGY_MEASURE = 1 << 15,
ALL = 0xFFFFFF,
#pragma warning restore CS1591
// ReSharper restore InconsistentNaming
}
/// <summary>
/// Reads the PCB Identification, can be accessed at all login levels,
/// Read Privilege is always needed to BUGFIX read access to PCB ID
/// </summary>
/// <returns>PCB ID</returns>
/// <remarks date="2018JAN08" author="R.Drabesch">
/// - PcbId handling improved.
/// </remarks>
/// <remarks date="2022-Nov-02" author="T.Wiedebusch">
/// - PcbId handling improved,
/// - Removed retries as these are handled by the protocol.
/// </remarks>
public String GetPcbId()
{
if (RequestPort == null)
{
return "Request port not assigned";
}
// Read Privilege is always needed to BUGFIX read access to PCB ID
_logger.Info($"Slot:{Slot} - Get privilege level");
ReadRegister(Register.Configexchange.Privilege);
_logger.Info($"Slot:{Slot} - Old PCB identification was {PcbId}");
var pcbIdRaw = ReadRegister(Register.Configexchange.PcbSerialNumber);
if (pcbIdRaw != null)
{
PcbId = Encoding.ASCII.GetString(pcbIdRaw).Split('\0')[0];
_logger.Info($"Slot:{Slot} - Get PCB identification {PcbId}");
return PcbId;
}
return string.Empty;
}
/// <summary>
/// Get the actual register dictionary
/// </summary>
/// <returns></returns>
public ConcurrentDictionary<RegisterDefinition, Byte[]> GetRegistersDic()
{
if (_configRegister?.MeterRegisterDic == null || !_configRegister.MeterRegisterDic.Any())
{
return new ConcurrentDictionary<RegisterDefinition, Byte[]>();
}
return _configRegister.MeterRegisterDic;
}
/// <summary>
/// Serial number of meter married with PcbId
/// </summary>
/// <returns></returns>
public Int32 GetSerialNumber()
{
return RelatePcb.GetSerialNumber(ServiceUrls.MarriageServiceUrl(), GetPcbId());
}
/// <inheritdoc />
public void SetLcdText(Boolean release, Byte[] textInHex)
{
if (release)
{
_logger.Info($"Slot:{Slot} - Set LCD into active mode");
WriteRegister(Register.Genesisflow.TriggerIdle, new Byte[] { 0x00, 0x00, 0x00, 0x00 });
}
else
{
if (!textInHex.Any() || textInHex.Length != 2)
{
throw new ApplicationException("Text cannot be set, please use 2 byte array for 4 hexadecimal digits");
}
//Trigger write is allowed only during logged in with level 7 or 8
if (IsLoggedOn)
{
_logger.Info($"Slot:{Slot} - Set LCD text to {textInHex[0]} {textInHex[1]}");
WriteRegister(Register.Genesisflow.TriggerIdle,
new Byte[] { textInHex[1], textInHex[0], 0x00, 0x00 }, true, true);
}
}
}
#endregion
#region Register
/// <summary>
/// Event to Sync Register on MeterSide and <see cref="_configRegister" />
/// Fired on Write or read register
/// </summary>
/// <param name="sender"></param>
/// <param name="e">Register to update with value to update</param>
private void RequestProtocol_MeterRegisterUpdated(Object sender, RegisterUpdatedEventArgs e)
{
_configRegister.Set(e.Register, e.Value);
}
/// <summary>
/// Write Register, optional: wait for result and validate,
/// write register will ALWAYS log the data DON'T use for password write
/// </summary>
/// <typeparam name="T">Data type of Register</typeparam>
/// <param name="reg">Register to change</param>
/// <param name="value">Value to save</param>
/// <param name="waitForResult">wait until result is ready</param>
/// <param name="checkRegister">check the content of the register by read back</param>
/// <param name="skipRetryErrorCode">skip retries on this error code return</param>
/// <returns>true on successful operation</returns>
public Boolean WriteRegister<T>(String reg, T value, Boolean waitForResult = true,
Boolean checkRegister = false, UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode)
{
try
{
var regDef = _configRegister.GetRegisterDefinitionByName(reg);
var data = RegisterConverter.ConvertFrom(value);
var hideDataInLog = regDef.RegisterName.Contains("EncryptionKey") || regDef.RegisterName.Contains("Password");
var rawMsg = hideDataInLog ? "(*****)" : $"({BitConverter.ToString(data.ToArray())})";
_logger.Info($"Slot:{Slot} - Write register({regDef.GetIdent()}), record({rawMsg})");
PreRegisterWrite(regDef, data);
var command = data.Length > RegisterDefinition.ChunkSize ?
Commands.MultipleWriteData : Commands.WriteData;
var responseRecord = RequestProtocol.CommandToMeter(command, regDef, data,
hideDataInLog: hideDataInLog, skipRetryErrorCode: skipRetryErrorCode);
if (!waitForResult && !checkRegister)
{
return true;
}
RequestProtocolProcess();
//if register check by read back is not required we return the acknowledge state
if (!checkRegister || regDef.RegisterDetail.Privilege.Lvl8 == Registers.Access.WO)
{
return responseRecord.Acknowledge == RequestAcknowledgeState.Ok;
}
var registerData = ReadRegister(reg, data.Length, skipRetryErrorCode);
//check if register read was okay and read and written data are EQUAL
if (registerData == null || registerData.Length < data.Length ||
data.Where((t, i) => registerData[i] != t).Any())
{
return false;
}
return responseRecord.Acknowledge == RequestAcknowledgeState.Ok;
}
catch (Exception e)
{
_logger.Error($"Slot:{Slot} - {e}");
}
return false;
}
/// <summary>
/// Read Register and return byte array
/// </summary>
/// <param name="reg"></param>
/// <param name="expectedLength">expected length for response</param>
/// <param name="skipRetryErrorCode">skip retries on this error code return</param>
/// <returns></returns>
public Byte[] ReadRegister(String reg, Int32? expectedLength = null,
UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode)
{
try
{
var regDef = _configRegister.GetRegisterDefinitionByName(reg);
_configRegister.Set(regDef, null);
_logger.Info($"Slot:{Slot} - Read register({regDef.GetIdent()})");
var hideDataInLog = regDef.RegisterName.Contains("EncryptionKey");
// setup length based on data type
if (!expectedLength.HasValue)
{
expectedLength = RegisterConverter.SizeOf(regDef);
}
var dataLengthPreset = expectedLength.Value;
if (expectedLength.Value <= RegisterDefinition.ChunkSize)
{
RequestProtocol?.CommandToMeter(Commands.ReadData, regDef, hideDataInLog: hideDataInLog,
skipRetryErrorCode: skipRetryErrorCode);
}
else
{
RequestProtocol?.CommandToMeter(Commands.MultipleReadData, regDef, expectedLength: dataLengthPreset,
hideDataInLog: hideDataInLog, skipRetryErrorCode: skipRetryErrorCode);
}
RequestProtocolProcess();
var result = _configRegister.Get(reg);
PostRegisterRead(regDef, result);
return result;
}
catch (Exception e)
{
_logger.Error($"Slot:{Slot} - {e}");
}
return null;
}
/// <remarks date="2020-Dec-19" author="Thomas Wiedebusch">
/// - Removed throw because the throw always kicks in to
/// the FW-Update process causing an unpredictable stop!
/// </remarks>
private void RequestProtocolProcess()
{
switch (RequestProtocol.ProcessRecordList())
{
case RequestAcknowledgeState.AuthorizationRequired:
//access only if IsLoggedOn marker is set and authorization has been lost
if (IsLoggedOn)
{
_logger.Debug($"Slot:{Slot} - Authorization is required");
if (!string.IsNullOrEmpty(_password))
{
IsLoggedOn = false;
if (Login(_password, false))
{
}
}
}
break;
case RequestAcknowledgeState.Ok:
break;
//try out this
case RequestAcknowledgeState.CommandError:
case RequestAcknowledgeState.MeterError:
Thread.Sleep(200);
break;
}
}
/// <summary>
/// Send UI1236 command
/// </summary>
/// <remarks date="2022-Aug-24" author="R.Drabesch">
/// - Initial
/// </remarks>
// ReSharper disable once InconsistentNaming UI1236 is a naming forced by the caller
public Byte[] SendUI1236Command(Byte[] ui1236Frame, Boolean waitForResult = true, Boolean checkRegister = false,
UInt16 skipRetryErrorCode = 4)
{
var requestIdent = $"Slot:{Slot} - Sending Ui-1236 frame";
_logger.Info(requestIdent);
var sendFifoUi1236 = RequestProtocol.AddRecordToSendFifoUI1236(requestIdent, ui1236Frame);
RequestProtocolProcess();
return sendFifoUi1236.ResponsePayload.ToArray();
}
#endregion
#region Class handling
/// <summary>
/// Clear Ports
/// </summary>
public void Clear()
{
StreamingPort?.Clear();
RequestPort?.Clear();
}
/// <inheritdoc />
public void DisposeMeter()
{
Dispose();
}
/// <inheritdoc />
public void Dispose()
{
_logger.Trace($"Slot:{Slot} - Start to dispose");
LogRawData(false);
LogManager.Flush();
//isPart of Logout
//StopKeepSession();
//Cancel receive tokens
_keepSessionToken?.Cancel();
try
{
Logout();
}
catch (Exception)
{
// ignored
}
//Run thread again to notice CancellationToken has changed
_onSyncKeepSessionThread?.Set();
//this timeout counter is being used for dispose only
var timeoutCounter = 100;
if (_keepSessionThread != null)
{
while (_keepSessionThread.ThreadState != ThreadState.Stopped && timeoutCounter > 0)
{
Thread.Sleep(1);
timeoutCounter -= 1;
}
if (_keepSessionThread.ThreadState != ThreadState.Stopped)
{
//if thread is still running try to abort
// try to avoid abort (takes age to run and is not safe)
_keepSessionThread.Abort();
}
}
_keepSessionThread = null;
RequestPort?.Dispose();
StreamingPort?.Dispose();
RequestProtocol?.Dispose();
StreamingProtocol?.Dispose();
_logger.Trace($"Slot:{Slot} - Dispose is finished");
}
#endregion
#region Logging
/// <inheritdoc />
public void WriteLog(String text)
{
_logger.Info($"Slot:{Slot} - {text}");
}
/// <inheritdoc />
public void LogRawData(Boolean on)
{
_enableRawDataLogging = on;
if (StreamingPort is LedSerialPort port)
{
port.RecordStreamingRawData = _enableRawDataLogging;
}
}
/// <summary>
/// Set the process state to display and to the production data base
/// </summary>
/// <param name="newState"></param>
public void SetProcessState(DisplayCodes newState)
{
SetProcessState(newState, true);
}
/// <inheritdoc />
public void SetProcessState(DisplayCodes newState, Boolean webRequest)
{
if (webRequest)
{
var url = ServiceUrls.MeterProcessStateUrl();
url += $"setState?PcbID={PcbId}&code={newState.GetHashCode()}&version={Assembly.GetExecutingAssembly().GetName().Version}";
try
{
LocalWebRequest.PostRequestAsync(url);
}
catch (Exception ex)
{
_logger.Info(
$"Slot:{Slot} - Cannot store process state to service, URL({url}), Error({ex.Message})");
}
}
SetLcdText(false, BitConverter.GetBytes((Int64)newState).Take(2).ToArray());
}
/// <inheritdoc />
public DisplayCodes GetLastProcessState()
{
var url = ServiceUrls.MeterProcessStateUrl();
url += $"GetLastState?PcbID={PcbId}";
try
{
Int32 i;
if (int.TryParse(LocalWebRequest.GetRequest(url), out i))
{
return ((DisplayCodes)i);
}
}
catch (Exception ex)
{
_logger.Info($"Slot:{Slot} - Cannot store process state to service, URL({url}), Error({ex.Message})");
}
return DisplayCodes.None;
}
private void PreRegisterWrite(RegisterDefinition reg, Byte[] value)
{
//if (_configuration.UseMinMaxCheck)
//{
// var result = RegisterCheck.CheckWrite(reg, value);
// if (!string.IsNullOrEmpty(result))
// {
// if (reg.GetIdent().ToUpper() != "GENESISFLOW_DisplayPow10".ToUpper())
// {
// throw new Exception(result);
// }
// }
//}
}
/// <summary>
/// Executes all StoreConfiguration and StoreCalibration for each application.
/// </summary>
/// <returns>true if all configurations are stored</returns>
/// <remarks date="2019-Apr-06" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2022-Apr-21" author="Thomas Wiedebusch">
/// - Corrected logic to enter write and read loop,
/// - Removed SENSUSRADIO from read check as it returns NULL on read of StoreConfiguration.
/// </remarks>
/// <remarks date="2022-Apr-26" author="Thomas Wiedebusch">
/// - After write store configuration extended sleep.
/// </remarks>
/// <remarks date="2022-Jun-23" author="Thomas Wiedebusch">
/// - Removed comparison of allConfigRegisters less than 8 because the NA version has less registers and new
/// apps will have a new register.
/// </remarks>
/// <remarks date="2022-Jul-22" author="Thomas Wiedebusch">
/// - Removed temporary NA2ALARMS read back as this has no handler in the FW.
/// </remarks>
/// <remarks date="2023-Nov-22" author="Thomas Wiedebusch">
/// - Stored first all configurations for ever app, then read it back.
/// </remarks>
public Boolean StoreAllConfigurations()
{
var retVal = true;
try
{
foreach (var a in _configRegister.MeterRegisterDic.Where(a =>
a.Key.RegisterName.ToLower() == "storeconfiguration" ||
a.Key.RegisterName.ToLower() == "storecalibration"))
{
if (!MeterAppListVersion.Where(v => v.AppId == a.Key.AppAddress).Any() ||
!MeterAppListVersion.Where(v => v.AppId == a.Key.AppAddress).First().IsInstalled)
{
}
else
{
// retries for each register access
var retriesLeft = 2;
Boolean result;
do
{
Thread.Sleep(150);
result = WriteRegister(a.Key.GetIdent(), 1);
} while (!result && retriesLeft-- > 0);
if (!result)
{
retVal = false;
}
}
}
}
catch (Exception)
{
retVal = false;
}
// check loop
try
{
foreach (var a in _configRegister.MeterRegisterDic.Where(a =>
a.Key.RegisterName.ToLower() == "storeconfiguration" ||
a.Key.RegisterName.ToLower() == "storecalibration"))
{
if (!MeterAppListVersion.Where(v => v.AppId == a.Key.AppAddress).Any() ||
!MeterAppListVersion.Where(v => v.AppId == a.Key.AppAddress).First().IsInstalled)
{
}
else
{
//TODO enable this for R1.2.x take the version of FW
if (a.Key.AppName == "SENSUSRADIO" || a.Key.AppName == "NA2WALARMS")
{
continue;
}
// retries for each register access
var retriesLeft = 2;
Boolean result;
do
{
var readResult = ReadRegister(a.Key.GetIdent());
result = readResult != null && readResult.ToList().All(array => array == 0x00);
} while (!result && retriesLeft-- > 0);
if (!result)
{
retVal = false;
}
}
}
}
catch (Exception)
{
retVal = false;
}
return retVal;
}
/// <summary>
/// Upload app list to data base
/// </summary>
/// <param name="progressName"></param>
/// <param name="version"></param>
public void PushProgress(String progressName, String version)
{
var apps = new List<CordonelAppVersion>();
foreach (var item in MeterAppListVersion)
{
apps.Add(new CordonelAppVersion(item.AppId, item.StrVersion, item.AppId != 15));
}
apps.Add(new CordonelAppVersion(-1, BuildFwVersionString(CoreRevision), false));
var url = $"{ServiceUrls.MeterProcessStateUrl()}SetCordonelProgress?PcbId={PcbId}" +
$"&ProgressName={progressName}&version={version}";
LocalWebRequest.PostRequestAsync(url, json: apps);
}
/// <summary>
/// Backup current register dictionary to data base
/// </summary>
/// <exception cref="Exception"></exception>
public void StoreCurrentRegisterDic()
{
try
{
var url = ServiceUrls.RegisterWatchServiceUrl();
if (!string.IsNullOrEmpty(Configuration?.RegisterWatchServiceUrl))
{
url = Configuration.RegisterWatchServiceUrl;
}
var d = new List<RegisterToDb>();
foreach (var a in _configRegister.MeterRegisterDic)
{
try
{
d.Add(new RegisterToDb(a.Key.RegisterAddress, a.Value));
}
catch (Exception)
{
// ignored
}
}
var r = LocalWebRequest.PostRequestAsync($"{url}FullRegisters?PcbID={PcbId}", 2000, d);
if (!r)
{
throw new Exception("Could not store CalibrationResults on server");
}
}
catch (Exception ex)
{
_logger.Error($"Slot:{Slot} - {ex} - Can't store at RegisterWatchService");
}
}
/// <summary>
/// Logging of register write processes to meter
/// ATTENTION: DON'T USE FOR PASSWORDS!
/// </summary>
/// <param name="reg"></param>
/// <param name="value"></param>
private void PostRegisterRead(RegisterDefinition reg, Byte[] value)
{
if (Configuration.UseRegisterWatchService)
{
try
{
var valuestring = "NULL";
if (value != null)
{
valuestring = BitConverter.ToString(value);
}
var url = $"{Configuration.RegisterWatchServiceUrl}RegisterWrite?PcbID={PcbId}&Address={BitConverter.ToString(reg.RegisterAddress)}&value={valuestring}";
var result = LocalWebRequest.PostRequestAsync(url, 1500, string.Empty);
if (!result)
{
_logger.Error(new Exception($"Slot:{Slot} - HTTP status not OK"),
"Can't store at RegisterWatchService");
}
}
catch (Exception ex)
{
_logger.Error($"Slot:{Slot} - {ex} - Can't store at RegisterWatchService");
}
}
}
/// <summary>
/// Storage of calibration values
/// </summary>
/// <param name="calibrationFactor1"></param>
/// <param name="calibrationFactor2"></param>
/// <param name="calibrationFactor3"></param>
public void LogCalibrationData(UInt16 calibrationFactor1, UInt16 calibrationFactor2, UInt16 calibrationFactor3)
{
if (Configuration.UseCalibrationLogger)
{
try
{
var request = (HttpWebRequest)WebRequest.Create(
$"{Configuration.CalibrationLoggerServiceUrl}PostCalibrationResult?PcbID={PcbId}" +
$"&flowCalibrationPath1={calibrationFactor1}&flowCalibrationPath2={calibrationFactor2}" +
$"&flowCalibrationPath3={calibrationFactor3}");
var data = Encoding.ASCII.GetBytes(string.Empty);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = data.Length;
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
//var responseStream = response.GetResponseStream();
//if (responseStream == null) return;
//var responseString = new StreamReader(responseStream).ReadToEnd();
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Accepted)
{
_logger.Error(new Exception($"Slot:{Slot} - HTTP status not OK({response.StatusCode})"), "can not store at RegisterWatchService");
}
}
catch (Exception ex)
{
_logger.Error($"Slot:{Slot} - {ex} - can not store at RegisterWatchService");
}
}
}
#endregion
}
}