laatzen/Common/Hardware/WaterMeter/Genesis/GenesisCore/GenesisMeter.cs
Thomas Wiedebusch 933d18d534 GenesisMeter: - avoid registers of uninstalled apps,
ProductionUiCordonel: - temporary removed 'reboot counter check',
RegisterRestorer: -modified _excludedFromReadParameters
2025-12-16 11:51:02 +01:00

3048 lines
118 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 NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
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.Text.RegularExpressions;
using System.Threading;
using System.Timers;
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.GenesisCore.Properties;
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 Access = Xylem.Common.Hardware.WaterMeter.Genesis.Registers.Access;
using Register = Xylem.Common.Hardware.WaterMeter.Genesis.Registers.Register;
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 : IGenesisMeter, IMeterEvents, IWaterMeterWithRegisters
{
#region ctor
/// <summary> Default constructor. </summary>
///
/// <remarks> R.Drabesch, 2018-Feb-16. </remarks>
public GenesisMeter()
{
SetLogger();
}
//initially do not signal event
private readonly AutoResetEvent _onSyncKeepSessionThread = new AutoResetEvent(false);
private Thread _keepSessionThread;
private Timer _keepSessionTimer;
private readonly CancellationTokenSource _keepSessionToken = new CancellationTokenSource();
/// <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;
/// <summary>
/// Minimal length for PcbId
/// </summary>
public const Int32 MinPcbIdLength = 9;
/// <summary>
/// The quiescent current threshold is the absolut minimal threshold for current consumption´in micro-Ampere.
/// This will be used to decide that the current isn't below for operational calculations of power consumption.
/// </summary>
public const Int32 QuiescentCurrent_uA = 50;
#endregion
#region Properties
/// <summary>
/// Logger for NLog
/// </summary>
protected ILogger Logger
{
set; get;
}
private ILogger _loggerRawData;
/// <summary>
/// Optional request to use the offline passwords
/// </summary>
public Boolean UseOfflinePasswords { set; get; }
/// <summary>
/// Remind the offline password for comparison if this is actively used
/// </summary>
public String OfflinePassword { protected set; get; } = "";
/// <summary>
/// Process configuration
/// </summary>
public ProcessConfig Configuration { internal set; get; } //= new ProcessConfig();
/// <summary>
/// Transmit protocol access for underlie objects to change response timeout
/// </summary>
public ITransmitProtocol TransmitProtocol
{
internal set; get;
}
private String _serialNumber = "";
/// <inheritdoc />
public String SerialNumber
{
get => _serialNumber;
set
{
_serialNumber = value;
SetLogger();
}
}
/// <summary>
/// Customer serial number for informal issues
/// </summary>
public String CustomerSerialNumber { get; set; } = "";
/// <inheritdoc />
/// <summary>
/// Save Slot Number (position at test-bench) for logging purposes
/// </summary>
public Int32 Slot
{
get; set;
}
/// <inheritdoc />
public String PcbId
{
get; protected set;
}
/// <inheritdoc />
public List<MeterApplications> MeterAppListVersion
{
protected set; get;
} = new List<MeterApplications>();
/// <inheritdoc />
public Int32? CoreRevision
{
protected set; get;
}
/// <inheritdoc />
public InterfaceInfo InterfaceInfo
{
get; internal set;
}
/// <inheritdoc />
public String StrCoreRevision
{
protected set; get;
}
/// <inheritdoc />
public String FwVersion
{
protected set; get;
}
/// <inheritdoc />
public UInt32? InstalledFwVersion
{
protected set; get;
}
/// <inheritdoc />
public String LutCrc {
protected set;
get;
} = "?";
/// <inheritdoc />
public String MeterSize {
protected set;
get;
} = "?";
/// <inheritdoc />
public Boolean InterfaceSupportsFwVersion
{
protected set;
get;
}
/// <inheritdoc />
public String Region {
protected set;
get;
} = "?";
/// <inheritdoc />
public String MeterLength
{
set;
get;
} = "?";
/// <inheritdoc />
public Boolean PressureSensorAssembled
{
protected set; get;
}
/// <summary>
/// Radio frequency in MHz (433 or 868 or null).
/// </summary>
public Int32? RadioFrequencyMhz
{
protected set; get;
}
/// <summary>
/// Metrology upgrade permission.
/// </summary>
public Byte MetrologyUpgradePermission
{
set; get;
}
/// <inheritdoc />
public Int32 IntermediateUpdateTimeS { get; set; } = CommunicationConfig.MeasurementUpdateTimeS;
/// <inheritdoc />
public Int32 OrderNumber
{
set; get;
}
/// <inheritdoc />
public Int64 RadioAddress
{
set; get;
}
/// <summary>
/// Add on ctor a password, it will be used for login in if no other password is set
/// </summary>
protected String Password
{
set; get;
}
/// <summary>
/// reminder for last communication acknowledge code to detect communication errors
/// </summary>
private RequestAcknowledgeState _acknowledgeCode;
/// <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>
/// Detected pulse adapter communication on request port
/// </summary>
public Boolean PulseAdapterAutoDetected
{
private set;
get;
}
/// <summary>
/// Enable use of registers that are not valid (last version not match the current app)
/// </summary>
public Boolean IsDevelopmentUsage
{
get; private set;
}
/// <summary>
/// All parameters stored in the SOFTWARE of GenesisMeter: NOT IN THE METER
/// 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>
protected readonly MeterRegisters ConfigRegister = new MeterRegisters();
/// <summary>
/// All applications defined by configuration.json
/// </summary>
protected 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
{
private set; get;
}
/// <summary>
/// streaming port assignment/info
/// </summary>
public IPort StreamingPort
{
get; private set;
}
/// <summary>
/// streaming protocol assignment/info
/// </summary>
public StreamingProtocol StreamingProtocol
{
private set; get;
}
/// <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;
/// <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 invoked as well
public ProcessState ProcessStatus
{
get => _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 => _myErrorState;
set
{
_myErrorState = value;
Invoker(OnErrorStatusChanged);
}
}
/// <summary>
/// Logged in to device
/// </summary>
public Boolean IsLoggedOn
{
get; protected set;
}
private String _currentActionText = "";
/// <summary>
/// Hold the current process name e.g. FlowTest, Preadjustment for logging
/// </summary>
public String CurrentActionText
{
get => _currentActionText;
set
{
_currentActionText = value;
SetLogger();
}
}
/// <summary>
/// Unlocked the property change ability for special sequences.
/// </summary>
public Boolean UnlockEssentialProperties
{
get;
protected set;
}
#endregion
#region Setup
/// <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>
/// Unlocked the property change ability for special sequences.
/// </summary>
/// <param name="unlockPropertyChange">true if unlock required</param>
/// <returns>true if possible</returns>
public Boolean EnablePropertyChangeAbility(Boolean unlockPropertyChange)
{
UnlockEssentialProperties = unlockPropertyChange;
return true;
}
/// <summary>
/// Set the meter size if previously unlocked
/// </summary>
/// <param name="meterSize">true if setting is allowed</param>
/// <returns>true if possible</returns>
public virtual Boolean SetMeterSize<T>(T meterSize)
{
if (!UnlockEssentialProperties)
return false;
if (meterSize is String)
{
var ms = meterSize.ToString();
if (Logic.ProductionOrderCore.OrderData.MeterSize.NA == MeterSizeConverter.ConvertMeterSizeNameToEnum(ms))
return false;
MeterSize = ms;
}
else if (meterSize.GetType() == typeof(MeterSize))
{
var ms = Convert.ToInt32(meterSize);
MeterSize = MeterSizeConverter.ConvertMeterSizeEnumToSizeName((MeterSize)ms);
}
else // Type is unknown
{
return false;
}
return true;
}
/// <summary>
/// Set the pressure sensor if previously unlocked
/// </summary>
/// <param name="pressureSensorAssembled">true if setting is allow</param>
/// <returns>true if possible</returns>
public Boolean SetPressureSensorAssembled(Boolean pressureSensorAssembled)
{
if (!UnlockEssentialProperties)
return false;
PressureSensorAssembled = pressureSensorAssembled;
return true;
}
/// <summary>
/// Returns meter registers
/// </summary>
/// <returns></returns>
public MeterRegisters GetConfigRegistersDefinitions()
{
return ConfigRegister;
}
/// <inheritdoc />
public void 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)
{
var msg = $"Slot:{Slot} - {Resources.StrErrorMissingConfiguration} {location}";
Logger.Fatal($"Slot:{Slot} - Missing register definition file (configuration.json). Search location: {location}");
throw new ApplicationException(msg);
}
}
try
{
Logger.Trace($"Slot:{Slot} - Registers definitions loaded from {location}");
if (Configuration != null && 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:yyyy-MM-ddTHH:mm:ss.fffZ}" +
"&isDeveloper=0", 30000);
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.");
}
try
{
//builds a configuration file reader from configuration.json and reads all registers definitions
//and application definitions
var registerReader = new GenesisConfigurationReader(configFilePath);
InterfaceInfo = new InterfaceInfo
{
SupportedFwVersions = new List<String>()
};
InterfaceInfo.SupportedFwVersions.AddRange(registerReader.InterfaceInfo.SupportedFwVersions);
InterfaceInfo.InterfaceVersion = registerReader.InterfaceInfo.InterfaceVersion;
SetConfigRegisterDefinitions(registerReader.ConfigRegistersDefinitions);
SetConfigApplicationDefinitions(registerReader.ConfigApplicationDefinitions);
}
catch (Exception ex)
{
var msg = $"\'configuration.json\' file is incompatible!\nConfig Reader reports: {ex.Message}";
Logger.Error(ex, $"Slot:{Slot} - {msg}.");
throw new Exception(msg);
}
}
event EventHandler IMeter.OnDisposeCompleted
{
add
{
// throw new NotImplementedException();
}
remove
{
// throw new NotImplementedException();
}
}
/// <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, 30000);
}
}
}
}
/// <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();
Configuration.ReadProcessConfig();
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)
{
var msg = $"Slot:{Slot} - {Resources.StrErrorMsgComPort} {ex.Message}";
Logger.Fatal(msg);
throw new ApplicationException(msg);
}
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 a 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 = EventArgs.Empty;
}
handler.Invoke(sender, e);
}
// ReSharper disable once RedundantIfElseBlock
else
{
//nobody is listening
}
}
/// <summary>
///
/// </summary>
public Dictionary<Int32, Tuple<Int32, DateTime>> ErrorCurrent = new Dictionary<Int32, Tuple<Int32, DateTime>>();
/// <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;
}
//if (calRec.Validation != 0)
//{
// if (!ErrorCurrent.ContainsKey(calRec.Channel))
// {
// ErrorCurrent.Add(calRec.Channel, new Tuple<int, DateTime>(1, DateTime.Now));
// }
// else
// {
// ErrorCurrent[calRec.Channel] = new Tuple<int, DateTime>(ErrorCurrent[calRec.Channel].Item1 + 1, DateTime.Now);
// }
// _logger.Info($"MessuremntError on {calRec.Channel} Code: {calRec.Validation}");
//}
}
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>
/// <remarks date="2025-Oct-10" author="T.Wiedebusch">
/// - Optional the offline passwords will be used.
/// </remarks>
/// <remarks date="2025-Oct-21" author="T.Wiedebusch">
/// - BUGFIX: Avoid request of password from DB if offline password usage fored.
/// </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 "";
}
}
}
var password = "";
// if NOT offline password usage required try to get it from DB
if (!UseOfflinePasswords && MeterPwdHandlerDb.GetPasswordFromDb(PcbId, out password) )
{
Logger.Info($"Slot:{Slot} - Got password from GenesisPasswordService, " +
$"URL({ServiceUrls.GenesisGetPasswordServiceUrl() + PcbId})");
OfflinePassword = "";
}
else
{
if (!UseOfflinePasswords)
Logger.Info($"Slot:{Slot} - Cannot get password from GenesisPasswordService, " +
$"URL({ServiceUrls.GenesisGetPasswordServiceUrl() + PcbId}))");
// try to read the password from an offline password file
var offlinePwdPathName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Genesis", ProgramConfig.OfflineInfoFile);
if (!File.Exists(offlinePwdPathName))
return password;
var listOfOfflinePasswords = JsonConvert.DeserializeObject<List<OfflinePasswordItem>>(
File.ReadAllText(offlinePwdPathName));
if (listOfOfflinePasswords != null && listOfOfflinePasswords.Any(x => x.PcbId == PcbId ))
password = listOfOfflinePasswords.First(x => x.PcbId == PcbId).Password;
if (!string.IsNullOrEmpty(password))
{
Logger.Info($"Slot:{Slot} - Got password for PcbId:{PcbId} from 'offline password' file!");
OfflinePassword = password;
}
else
{
Logger.Info($"Slot:{Slot} - 'offline password' file does not contain a password for PcbId:{PcbId} !");
}
}
return password;
}
/// <inheritdoc />
/// <remarks date="2018JAN08" author="drabesch">
/// Get password from server
/// </remarks>
public virtual Boolean Login()
{
if (string.IsNullOrEmpty(Password) || string.IsNullOrEmpty(PcbId))
{
var password = GetPassword();
if (string.IsNullOrEmpty(password))
{
return false;
}
return Login(password);
}
return Login(Password);
}
/// <inheritdoc />
/// <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 log in</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 quickly connect being able to immediately switch off the pulse mode before App detection.
/// </remarks>
public virtual 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 like "R1.1.07" or "B1.1.07" or optional reduced form for configuration capability check "1107",
/// - hexadecimal version like 0x1107 or 0x9107 for the 'B' version.
///
/// Input:
/// - optional string like "1107", "11.07", "R1.1.07", "B1.1.07", "1.3.0B" or "9.0.2D".
/// - optional 2 bytes representing the msb and lsb like 0x11 and 0x07.
///
/// REASON: FLEXNETVERSION is one application which does not follow the same rule of decimal numbers.
/// Instead, it uses hexadecimal digits.
/// </summary>
/// <param name="fwVersion">version as hexadecimal result of version e.g. 0x1107 or 0x130B</param>
/// <param name="strVersion">any string to evaluate like "11.07" or "R1.1.07" or "1.3.0B"</param>
/// <param name="msb">most significant byte representing the major and minor version</param>
/// <param name="lsb">last significant byte representing the built version</param>
/// <param name="buildShortVersion">will force to reduce the string output of FW version to e.g. "1107"</param>
/// <returns>FLEXNET version string e.g. "R1.2.47" or "B1.2.0C" </returns>
/// <remarks date="2023-Mar-02" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
/// <remarks date="2023-Mar-08" author="Thomas Wiedebusch">
/// - Removed the optional leading "B" or "R" on strVersion input to create the fwVersion.
/// </remarks>
/// <remarks date="2025-Sep-30" author="Thomas Wiedebusch">
/// - .
/// </remarks>
public static String BuildFlexnetFwVersion(out UInt32 fwVersion, String strVersion, Byte? msb = null,
Byte? lsb = null, Boolean buildShortVersion = false)
{
var major = 0;
var minor = 0;
var builtMajor = 0;
var builtMinor = 0;
const Int32 majorIndicatorForB = 8;
// preset version with 0 as undetected
fwVersion = 0;
// msb and lsb input have the highest priority
if (msb != null && lsb != null)
{
major = (Int32)((msb & 0xF0) >> 4);
minor = (Int32)(msb & 0x0F);
builtMajor = (Int32)((lsb & 0xF0) >> 4);
builtMinor = (Int32)(lsb & 0x0F);
fwVersion = (UInt32)(((msb & 0xFF) << 8) + lsb);
}
else if (!string.IsNullOrEmpty(strVersion))
{
// this is always a string like "11.07" or "R1.1.07" or "B1.1.07" or "1.3.0B" or "9.0.2D",
// a msb higher as 8 is indicating "B" version
// remove the optional leading "B" or "R" if strVersion is of form "R1.3.0B" or "B1.3.0B"
// and remind the "B" by adding to major the value of 8
var initialIdx = 0;
var majorAdd = 0;
var strLeadingSign = strVersion.Substring(0, 1);
if (strLeadingSign == "B")
{
initialIdx = 1;
majorAdd = majorIndicatorForB;
}
if (strLeadingSign == "R")
initialIdx = 1;
var strVersionWithoutLeadingId = strVersion.Substring(initialIdx, strVersion.Length - initialIdx);
var strResult = Regex.Replace(strVersionWithoutLeadingId, "[^0-9,A-F,a-f]", "");
// here the leading characters and decimal points are removed e.g. "1107"
var strMajor = strResult.Substring(0, 1);
var strMinor = strResult.Substring(1, 1);
var strBuiltMajor = strResult.Substring(2, 1);
var strBuiltMinor = strResult.Substring(3, 1);
if (!uint.TryParse(strResult, NumberStyles.HexNumber, new CultureInfo("en"), out fwVersion)
|| !int.TryParse(strMajor, NumberStyles.HexNumber, new CultureInfo("en"), out major)
|| !int.TryParse(strMinor, NumberStyles.HexNumber, new CultureInfo("en"), out minor)
|| !int.TryParse(strBuiltMajor, NumberStyles.HexNumber, new CultureInfo("en"), out builtMajor)
|| !int.TryParse(strBuiltMinor, NumberStyles.HexNumber, new CultureInfo("en"), out builtMinor))
{
// if parsing is not possible return the original strVersion
return strVersion;
}
// mark "R" or "B" version
major += majorAdd;
}
// Build the release type id: 'R' for released version or 'B' for test version
var releaseTypeId = "R";
if (major >= majorIndicatorForB)
{
releaseTypeId = "B";
// Debug version marked with 'B'
major -= majorIndicatorForB;
}
return buildShortVersion ?
$"{major:X0}{minor:X0}{builtMajor:X0}{builtMinor:X0}" :
$"{releaseTypeId}{major:X0}.{minor:X0}.{builtMajor:X0}{builtMinor:X0}";
}
/// <summary>
/// Building the FW Version string out of 2 bytes of data and a decimal version.
/// Example:
/// Inputs msb = 2, msb = 34,
/// Outputs fwVersion = 234, string "2.34"
/// </summary>
/// <param name="fwVersion">firmware version as decimal e.g. 234 </param>
/// <param name="msb">most significant byte hex</param>
/// <param name="lsb">last significant byte hex</param>
/// <returns>converted as string e.g. "2.34" or "2.0C" </returns>
/// <remarks date="2023-Mar-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public static String BuildFwVersion(out UInt32 fwVersion, Byte msb, Byte lsb)
{
fwVersion = (UInt32)((((msb & 0xF0) >> 4) * 10) + (msb & 0x0F)) * 100;
fwVersion += (UInt32)((((lsb & 0xF0) >> 4) * 10) + (lsb & 0x0F));
//skip leading 0
var msbPartString = (msb & 0xF0) > 0 ? $"{(msb & 0xF0) >> 4:X0}." : "";
msbPartString += $"{msb & 0x0F:X0}" + "." + $"{(lsb & 0xF0) >> 4:X1}" + $"{lsb & 0x0F:X1}";
return msbPartString;
}
/// <summary>
/// Building the FW Version string out of UInt32 as hexadecimal input like 0x1107 (meaning 1.1.07).
/// Example:
/// Input fwVersion = 1107,
/// Outputs string "1.1.07"
/// </summary>
/// <param name="fwVersion">firmware version as hex</param>
/// <returns>converted to "2.34" or "2.0C" </returns>
/// <remarks date="2023-Mar-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public static String BuildFwVersionStringFromHex(UInt32 fwVersion)
{
//skip leading 0
var msbPartString = (fwVersion & 0xF000) > 0 ? $"{(fwVersion & 0xF000) >> 12:X0}." : "";
msbPartString += $"{(fwVersion & 0xF00) >> 8:X0}.{(fwVersion & 0x0F0) >> 4:X0}" +
$"{fwVersion & 0x00F:X0}";
return msbPartString;
}
/// <summary>
/// Building the FW Version string out of Int32 as decimal input.
/// Example:
/// Input fwVersion = 238,
/// Outputs string "2.38"
/// </summary>
/// <param name="fwVersion">firmware version as hex</param>
/// <returns>converted to "2.34" or "2.01" </returns>
/// <remarks date="2023-Mar-07" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public static String BuildFwVersionStringFromDec(Int32? fwVersion)
{
return fwVersion == null ? "0.00" : $"{fwVersion / 100}.{fwVersion % 100:D2}";
}
/// <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>
/// <remarks date="2025-Feb-04" author="Thomas Wiedebusch">
/// - Frequency indicator == 0 means it is a NA-Region Octopus with EMEA FW installed.
/// </remarks>
/// <remarks date="2025-Mai-28" author="Thomas Wiedebusch">
/// - Pressure sensor detection.
/// </remarks>
public void CheckRegionSizeLutCrc()
{
//read metrology lookup table CRC
try
{
var lutCrcRaw = ReadRegister(Register.Genesisflow.LookupFileCrc);
if (lutCrcRaw != null)
{
var lutCrc = RegisterConverter.ByteArrayToValue<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 pressure sensor assembled
try
{
PressureSensorAssembled = RegisterConverter.ByteArrayToValue<Boolean>(
ReadRegister(Register.Metrologyasst.PressurePresent));
}
catch (Exception)// exception will be thrown if register is not present
{
PressureSensorAssembled = false;
}
//read meter size
MeterSize = MeterSizeConverter.ConvertMeterSizeEnumToSizeName(
(MeterSize)RegisterConverter.ByteArrayToValue<Byte>(
ReadRegister(Register.Genesisflow.MeterSize)));
//read radio frequency
Region = Constants.StrUnknown;
RadioFrequencyMhz = null;
try
{
var radioFrequencyRaw = ReadRegister(Register.Sensusradio.FrequencyIndicator);
if (radioFrequencyRaw != null &&
RegisterConverter.ByteArrayToValue<Int32>(radioFrequencyRaw) != 0)
{
Region = "EMEA";
RadioFrequencyMhz = RegisterConverter.ByteArrayToValue<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>
/// Build a list of all registers based on the interface (configuration.json) and the installed
/// meter applications with a specific version.
/// </summary>
/// <remarks date="2024-Apr-04" author="Roland Drabesch/Thomas Wiedebusch">
/// - Initial, extracted from <see cref="ReadMeterFirmwareAndAssignRegisters"/>
/// </remarks>
/// <remarks date="2025-Dec-09" author="Thomas Wiedebusch">
/// - Excluded register versions explicit specified in the 'configuration.json' as version.exclude list.
/// </remarks>
/// <remarks date="2025-Dec-16" author="Thomas Wiedebusch">
/// - Excluded all registers from apps which are NOT installed (isInstalled == false).
/// </remarks>
public void BuildValidMeterRegisters(List<KeyValuePair<RegisterDefinition, Byte[]>> tempConfigRegisters)
{
if (tempConfigRegisters == null || tempConfigRegisters.Count == 0)
return;
ConfigRegister.MeterRegisterDic.Clear();
// ReSharper disable once CollectionNeverQueried.Local this is used for debug to check the configuration.json
// for overlapping versions (indicated multiple assignment of identical register)
var configRegisterListForDebug = new List<String>();
// ReSharper disable once CollectionNeverQueried.Local this is used for debug to check the configuration.json
// for overlapping versions (indicated multiple assignment of identical register)
var configRegisterListOverlappingVersionForDebug = new List<String>();
try
{
foreach (var registerToCheck in tempConfigRegisters)
{
if (registerToCheck.Key?.RegisterDetail?.Version == null ||
MeterAppListVersion.All(f => f.AppId != registerToCheck.Key.AppAddress || !f.IsInstalled))
continue;
var currentGroup = MeterAppListVersion.First(f => f.AppId == registerToCheck.Key.AppAddress);
if ( // Legacy registers may not have a version assigned
(!registerToCheck.Key.RegisterDetail.Version.First.HasValue &&
!registerToCheck.Key.RegisterDetail.Version.Last.HasValue) ||
// Normal version assignment with first and last value
(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.RegisterDetail.Version.Exclude == null ||
// Exclude named versions
registerToCheck.Key.RegisterDetail.Version.Exclude.All(exVer => exVer != currentGroup.Version))))
{
configRegisterListForDebug.Add(registerToCheck.Key.GetIdent());
// Avoid multiple assignment of identical register in dictionary if one has passed the test
if (ConfigRegister.MeterRegisterDic.Any(reg =>
reg.Key.GetIdent().Equals(registerToCheck.Key.GetIdent())))
{
configRegisterListOverlappingVersionForDebug.Add(registerToCheck.Key.GetIdent());
continue;
}
registerToCheck.Key.IsAvailable = true;
ConfigRegister.MeterRegisterDic.TryAdd(registerToCheck.Key, null);
}
}
configRegisterListOverlappingVersionForDebug.Sort();
configRegisterListForDebug.Sort();
}
catch (Exception)
{
// ignored
}
}
/// <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", then 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>
/// <remarks date="2023-Nov-27" author="Thomas Wiedebusch">
/// - Remind installed FW version of FLEXNETVERSION for StoreAllConfigurations.
/// </remarks>
/// <remarks date="2023-Dec-08..11" author="Thomas Wiedebusch">
/// - Avoid multiple assignment of identical register in dictionary as the configuration.json may overlap
/// on some versions. If one register has been added, this passed the test and is valid for this FW.
/// It has to be avoided to have the register multiple times as it may cause unpredictable assignments
/// of values to one and reading and compare from another which doesn't have a value!
/// </remarks>
/// <remarks date="2024-Feb-22" author="Thomas Wiedebusch">
/// - Build dictionary for debug to observe if configuration.json contains overlapping versions.
/// </remarks>
/// <remarks date="2024-Apr-23" author="Thomas Wiedebusch">
/// - On missing answer mark as communication error being able to compare against not-installed. This issue
/// was responsible to start a FW update process in the CUST as it assumes the installation was incomplete,
/// but it couldn't detect those applications due to communication issues.
/// </remarks>
/// <remarks date="2024-May-06" author="Thomas Wiedebusch">
/// - BuildFlexnetFwVersion,
/// - Calculate core revision.
/// </remarks>
/// <remarks date="2025-Aug-05" author="Thomas Wiedebusch">
/// - Extract interface version (configuration.json) from "OPTICALINTERFACE".
/// - AppId from Byte to UInt16 including typecast for Byte on WriteRegister. To external, it will be used as Byte
/// as before this change. But being able to parse the configuration.json with OPTICALINTERFACE using the
/// AppId 256, which exceeds the byte range as this isn't a real application, but will be used to identify
/// the interface (configuration.json) version.
/// </remarks>
/// <remarks date="2025-Sep-30" author="Thomas Wiedebusch">
/// - Handle the interface compatibility FW version with short string like "1421" and discrete FW versions.
/// </remarks>
protected virtual void ReadMeterFirmwareAndAssignRegisters()
{
// Avoid overwriting of list if this already exits
if (MeterAppListVersion.Any() && MeterAppListVersion.All(f => f.Status != MeterAppState.Unknown))
{
Logger.Warn($"Slot:{Slot} - Trying to overwride installed FW. Skipping generation of new " +
"App list and meter register dictionary");
return;
}
// On new readout reset the installed version
InstalledFwVersion = 0;
MeterAppListVersion.Clear();
CheckRegionSizeLutCrc();
var strShortFwVersion = "";
// Get all existing applications from meter register dictionary
foreach (var meterApp in ConfigApplications.OrderBy(o => o.AppId))
{
UInt32 readFwVersion = 0;
UInt16 readFwCrc = 0;
var isInstalled = false;
var installationStatus = MeterAppState.MeterAppNotInstalled;
var versionString = "";
if (WriteRegister(Register.System.CheckFwPresence, (Byte)meterApp.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)
{
//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
if (meterApp.AppName.Equals("FLEXNETVERSION"))
{
// msb and lsb are swapped during communication
FwVersion = BuildFlexnetFwVersion(out readFwVersion, null, readFwVersionBytes[1],
readFwVersionBytes[0]);
versionString = FwVersion;
InstalledFwVersion = readFwVersion;
// create a short FW version info being able to compare the interface compatibility
strShortFwVersion = BuildFlexnetFwVersion(out _, FwVersion, buildShortVersion: true);
}
else
{
// msb and lsb are swapped during communication
versionString = BuildFwVersion(out readFwVersion, readFwVersionBytes[1], readFwVersionBytes[0]);
}
if (readFwVersion != 0)
{
isInstalled = true;
installationStatus = MeterAppState.MeterAppInstalledUnchecked;
//write application Id to CRC register
if (WriteRegister(Register.System.CheckFwCrc, meterApp.AppId))
{
readFwCrc = RegisterConverter.ByteArrayToValue<UInt16>(ReadRegister(Register.System.CheckFwCrc));
}
}
}
// missing answer
else if (RequestAcknowledgeState.NoResponse == _acknowledgeCode)
{
installationStatus = MeterAppState.Unknown;
}
}
// missing answer
else if (RequestAcknowledgeState.NoResponse == _acknowledgeCode)
{
installationStatus = MeterAppState.Unknown;
}
//add application always, even if this is not installed. This will be used for FW updates
MeterAppListVersion.Add(new MeterApplications
{
AppId = meterApp.AppId,
Version = readFwVersion,
StrVersion = versionString,
AppName = meterApp.AppName,
Crc = readFwCrc,
Status = installationStatus,
IsInstalled = isInstalled
});
}
//read metrology upgrade permission
MetrologyUpgradePermission = RegisterConverter.ByteArrayToValue<Byte>(
ReadRegister(Register.System.MetrologyUpgradePermission));
try
{
//check IRDA adapter ID to check if it was installed in the last few minutes to indicate this
PulseAdapterAutoDetected = !string.IsNullOrEmpty(RegisterConverter.ByteArrayToValue<String>(
ReadRegister(Register.Irda.AdapterId)));
Logger.Info($"Slot:{Slot} - Pulse adapter was installed: {PulseAdapterAutoDetected}");
}
catch (Exception) // IRDA application not installed
{
Logger.Info($"Slot:{Slot} - Pulse adapter was installed: {PulseAdapterAutoDetected}");
}
//read core revision
var coreRegisterRead = RegisterConverter.ByteArrayToValue<String>(ReadRegister(Register.System.CoreRevision, 16));
CoreRevision = null;
StrCoreRevision = "";
if (!string.IsNullOrEmpty(coreRegisterRead))
{
var coreRevisionStrings = coreRegisterRead.Split('\0');
// remove Cordonel sub-string and the blanks
var coreRevisionSubString = $"{coreRevisionStrings[0].ToLower().Replace("cordonel ", "")}";
var strResult = Regex.Replace(coreRevisionSubString, "[^0-9,A-F,a-f]", "");
if (int.TryParse(strResult, out var coreRevision))
{
CoreRevision = coreRevision;
StrCoreRevision = BuildFwVersionStringFromDec(CoreRevision);
}
}
// log application information to file
Logger.Info($"Slot:{Slot} - PCB ID: {PcbId}");
Logger.Info($"Slot:{Slot} - Date time (UTC): {DateTimeOffset.UtcNow}");
if (CoreRevision != 0)
{
Logger.Info($"Slot:{Slot} - System Core Revision: {StrCoreRevision}");
}
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";
if (fm.Status == MeterAppState.Unknown)
versionString = "Communication error";
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}");
Logger.Info($"Slot:{Slot} - Interface (configuration.json) version: {InterfaceInfo.InterfaceVersion}");
if (InterfaceInfo.SupportedFwVersions.Any(strFwVersion => strFwVersion.Contains(strShortFwVersion)))
{
InterfaceSupportsFwVersion = true;
Logger.Info($"Slot:{Slot} - Interface supports this FW version: {FwVersion}");
}
else
{
InterfaceSupportsFwVersion = false;
Logger.Warn($"Slot:{Slot} - Interface does not support this FW version: {FwVersion}");
}
// initial list of ALL registers defined in configuration.json
var tempConfigRegisters = ConfigRegister.MeterRegisterDic.ToList();
// reduce registers to valid registers for a specific application with a specific version
BuildValidMeterRegisters(tempConfigRegisters);
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.Message, $"Slot:{Slot} - Unable 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 virtual 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()
{
var 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
};
var 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;
}
/// <inheritdoc/>
public void ClearAlarm()
{
var beforeCancelAlarmEnableMask = RegisterConverter.ByteArrayToValue<UInt32>(ReadRegister("CUSTOMER_AlarmEnableMask"));
var beforeCancelAlarmBroadcastMask = RegisterConverter.ByteArrayToValue<UInt32>(ReadRegister("CUSTOMER_AlarmBroadcastMask"));
WriteRegister("CUSTOMER_AlarmEnableMask", Alarm.ALL);
WriteRegister("CUSTOMER_AlarmBroadcastMask", Alarm.ALL);
WriteRegister(Register.Customer.TriggerAlarmCancel, Alarm.ALL);
if (GetRegistersDic().Any(a => a.Key.RegisterName == "PersistenceGroup1"))
{
// ReSharper disable once UnusedVariable used for debug
var x = ReadRegister("SENSUSRADIO_PersistenceGroup1");
WriteRegister("SENSUSRADIO_PersistenceGroup1", new Byte[] { 0x00, 0xff, 0x00, 0xff });
}
if (GetRegistersDic().Any(a => a.Key.RegisterName == "PersistenceGroup2"))
{
WriteRegister("SENSUSRADIO_PersistenceGroup2", new Byte[] { 0x00, 0xff, 0x00, 0xff });
}
if (GetRegistersDic().Any(a => a.Key.RegisterName == "PersistenceGroup3"))
{
WriteRegister("SENSUSRADIO_PersistenceGroup3", new Byte[] { 0x00, 0xff, 0x00, 0xff });
}
if (GetRegistersDic().Any(a => a.Key.RegisterName == "PersistenceGroup4"))
{
WriteRegister("SENSUSRADIO_PersistenceGroup4", new Byte[] { 0x00, 0xff, 0x00, 0xff });
}
if (GetRegistersDic().Any(a => a.Key.RegisterName == "PersistenceGroup5"))
{
WriteRegister("SENSUSRADIO_PersistenceGroup5", new Byte[] { 0x00, 0xff, 0x00, 0xff });
}
if (GetRegistersDic().Any(a => a.Key.RegisterName == "PersistenceGroup6"))
{
WriteRegister("SENSUSRADIO_PersistenceGroup6", new Byte[] { 0x00, 0xff, 0x00, 0xff });
}
WriteRegister("CUSTOMER_AlarmEnableMask", beforeCancelAlarmEnableMask);
WriteRegister("CUSTOMER_AlarmBroadcastMask", beforeCancelAlarmBroadcastMask);
}
/// <summary>
/// Reset the empty pipe alarm
/// </summary>
public void ResetAlarm(Alarm clear)
{
WriteRegister(Register.Customer.TriggerAlarmCancel, clear.GetHashCode());
}
/// <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 virtual 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>
/// <remarks date="2024-May-17" author="Thomas Wiedebusch">
/// - Changed sensitive information from "*****" to SHA256.
/// </remarks>
/// <remarks date="2024-Dec-03" author="Thomas Wiedebusch">
/// - PreRegisterWrite enabled to check register accessibility and range.
/// </remarks>
public virtual 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.ValueToByteArray(value);
var hideDataInLog = regDef.RegisterName.Contains("EncryptionKey") ||
regDef.RegisterName.Contains("Password");
var rawMsg = RegisterConverter.GetRegisterRawText(regDef, data, hideDataInLog);
Logger.Info($"Slot:{Slot} - Write register({regDef.GetIdent()}), record({rawMsg})");
if (!PreRegisterWrite(regDef, data))
return false;
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 acknowledgment state
if (!checkRegister || regDef.RegisterDetail.Privilege.Lvl8 == 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="regName"></param>
/// <param name="expectedLength">expected length for response</param>
/// <param name="skipRetryErrorCode">skip retries on this error code return</param>
/// <returns></returns>
/// <remarks date="2024-May-17" author="Thomas Wiedebusch">
/// - Changed sensitive information from "*****" to SHA256.
/// </remarks>
/// <remarks date="2025-Oct-13" author="Thomas Wiedebusch">
/// - Date size directly from register object.
/// </remarks>
public virtual Byte[] ReadRegister(String regName, Int32? expectedLength = null,
UInt16 skipRetryErrorCode = CommunicationConfig.SkipRetryErrorCode)
{
try
{
var regDef = ConfigRegister.GetRegisterDefinitionByName(regName);
ConfigRegister.Set(regDef, null);
var hideDataInLog = regDef.RegisterName.Contains("EncryptionKey");
// setup length based on data type
if (!expectedLength.HasValue)
{
expectedLength = regDef.DataSize;
}
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(regName);
var rawMsg = RegisterConverter.GetRegisterRawText(regDef, result, hideDataInLog);
Logger.Info($"Slot:{Slot} - Read register({regDef.GetIdent()}), record({rawMsg})");
PostRegisterRead(regDef, result);
return result;
}
catch (Exception e)
{
Logger.Error($"Slot:{Slot} - {e}");
}
return null;
}
/// <summary>
/// Execute request protocol and acts on response code.
/// </summary>
/// <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>
/// <remarks date="2024-Apr-22" author="Thomas Wiedebusch">
/// - Remind last communication acknowledge code.
/// </remarks>
private void RequestProtocolProcess()
{
_acknowledgeCode = RequestProtocol.ProcessRecordList();
switch (_acknowledgeCode)
{
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()
{
try
{
Logger.Trace($"Slot:{Slot} - Start to dispose");
LogRawData(false);
LogManager.Flush();
//isPart of Logout
//StopKeepSession();
//Cancel receive tokens
_keepSessionToken?.Cancel();
try
{
//todo only from Bench
//WriteRegister<Byte>(Register.Genesisflow.SampleRate, 2, checkRegister: true);
//WriteRegister(Register.Genesisflow.LedMode, 0, checkRegister: true);
//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");
}
catch (Exception ex)
{
Logger.Trace($"Slot:{Slot} -dispose exception {ex.Message}");
}
}
#endregion
#region Logging
/// <inheritdoc />
public void WriteLog(String text)
{
try
{
Logger.Info($"Slot:{Slot} - {text}");
}
catch (Exception)
{
// ignored
}
}
/// <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 database
/// </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
{
if (int.TryParse(LocalWebRequest.GetRequest(url, 30000), out var 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;
}
/// <summary>
/// Check for min and max of register values to avoid out of range access
/// </summary>
/// <param name="regDef"></param>
/// <param name="value"></param>
/// <remarks date="2019-Apr-06" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2024-Dec-03" author="Thomas Wiedebusch">
/// - Reactivated to validate the register range.
/// </remarks>
protected Boolean PreRegisterWrite(RegisterDefinition regDef, Byte[] value)
{
try
{
// Avoid writing of NOT writable registers
if (regDef.RegisterDetail.Privilege.Lvl8 != Access.WO &&
regDef.RegisterDetail.Privilege.Lvl8 != Access.RW)
return false;
// check the limits and avoid writing if not in range
var retVal = RegisterCheck.CheckRange(regDef, value, out var errorMsg);
if (StatusReturn.Okay == retVal)
return true;
Logger.Error($"Slot:{Slot} - {errorMsg}");
return false;
}
catch (Exception ex)
{
Logger.Error($"Slot:{Slot} - {ex.Message}");
return false;
}
}
/// <summary>
/// Common routine for reboot being able to override this routine which will be called in
/// MeteResetPsu to simulate a reboot.
/// </summary>
/// <returns></returns>
/// <remarks date="2024-Dec-11" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public virtual StatusReturn RebootGenesis()
{
return StatusReturn.Unknown;
}
/// <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 fewer 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..23" author="Thomas Wiedebusch">
/// - Store first all configurations for every app in a bulk then read the status back.
/// </remarks>
/// <remarks date="2023-Nov-27" author="Thomas Wiedebusch">
/// - Installed FW version taken to enable read back for radio and na2walarms.
/// </remarks>
/// <remarks date="2024-May-07" author="Thomas Wiedebusch">
/// - Changed threshold check from decimal 1200 to hexadecimal 0x1200.
/// </remarks>Cl
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 (!a.Key.IsAvailable)
continue;
// 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 (!a.Key.IsAvailable)
continue;
// For R1.2.x the read back is corrected use as hexadecimal value outline
if (InstalledFwVersion < 0x1200 && (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 database
/// </summary>
/// <param name="progressName"></param>
/// <param name="version"></param>
/// <remarks date="2019-Apr-06" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2024-Aug-06" author="Thomas Wiedebusch">
/// - Using MetrologyUpgradePermission to flag if metrology is updateable as for "NA" versions this is the case.
/// </remarks>
/// <remarks date="2024-Aug-30" author="Thomas Wiedebusch">
/// - returns status.
/// </remarks>
public StatusReturn PushProgress(String progressName, String version)
{
var retVal = StatusReturn.Okay;
try
{
var apps = new List<CordonelAppVersion>();
foreach (var item in MeterAppListVersion)
{
var isUpdateable = !(item.AppName.Equals(MetrologyDefinition.MetrologyName) &&
MetrologyUpgradePermission != MetrologyDefinition.MetrologyUpgradePermitted);
apps.Add(new CordonelAppVersion(item.AppId, item.StrVersion, isUpdateable));
}
apps.Add(new CordonelAppVersion(-1, StrCoreRevision, false));
var url = $"{ServiceUrls.MeterProcessStateUrl()}SetCordonelProgress?PcbId={PcbId}" +
$"&ProgressName={progressName}&version={version}";
if (!LocalWebRequest.PostRequestAsync(url, json: apps))
retVal = StatusReturn.Failed;
}
catch (Exception ex)
{
Logger.Error($"Slot:{Slot} - {ex} - Cannot push progress");
retVal = StatusReturn.Failed;
}
return retVal;
}
/// <summary>
/// Backup current register dictionary to database
/// </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>
/// <remarks date="2025-Jun-30" author="Thomas Wiedebusch">
/// - Hide all file access commands and passwords.
/// </remarks>
private void PostRegisterRead(RegisterDefinition reg, Byte[] value)
{
var avoidLoggingRegisterNameList = Register.GetAvoidLogRegisters();
if (Configuration.UseRegisterWatchService && avoidLoggingRegisterNameList.All(regName => regName != reg.GetIdent()))
{
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, 30000, 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");
}
}
}
/// <summary>
/// Dummy
/// </summary>
/// <remarks date="2023-Dec-11" author="Roland Drabesch">
/// - Initial.
/// </remarks>
public void TestBenchDispose()
{
if (IsLoggedOn)
{
}
}
/// <summary>
/// Dummy
/// </summary>
/// <remarks date="???" author="Roland Drabesch">
/// - Initial.
/// </remarks>
public void BuildAndCheckCalibFactorsAllChannels(Double refVolumeCm, Double? refTimeS = null, Double reqDeviationPercent = 0, Double maxCalibFactorTolerancePercent = 0)
{
throw new NotImplementedException();
}
#endregion
}
}