Files
laatzen/Common/Hardware/WaterMeter/Genesis/GenesisCore/GenesisMeter.cs
T
2022-08-24 17:23:11 +02:00

2305 lines
85 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.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 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.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.Protocols.ProtocolCore;
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.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");
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;
/// <inheritdoc />
/// 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, Preadjustemtn for logging
/// </summary>
public String CurrentActionText
{
get
{
return _currentActionText;
}
set
{
_currentActionText = value;
SetLogger();
}
}
/// <inheritdoc />
public void SetupFromConfigFile(Int32 slot, Boolean ignoreCorruptedData = true)
{
SetupFromConfigFile(slot, ignoreCorruptedData, true, true);
}
/// <summary>
/// Setup water meter from configuration file:
/// -slot,
/// -streaming protocol,
/// -streaming port
/// -request port
/// </summary>
/// <param name="slot"></param>
/// <param name="ignoreCorruptedData"></param>
/// <param name="useRequest"></param>
/// <param name="useStreaming"></param>
/// <exception cref="ApplicationException"></exception>
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());
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);
}
}
}
}
/// <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}/{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)
{
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)
{
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)
{
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;
}
private String GetPassword()
{
var url = ServiceUrls.GenesisGetPasswordServiceUrl();
String password;
//without PCB ID it is not possible to login
var firstPcbId = GetPcbId();
if (string.IsNullOrEmpty(firstPcbId) || firstPcbId.Length < 8)
{
if (firstPcbId != null && firstPcbId.Length < 8)
{
Thread.Sleep(250);
}
if (string.IsNullOrEmpty(GetPcbId()))
{
return "";
}
}
url += PcbId;
try
{
//use
var http = (HttpWebRequest)WebRequest.Create(url);
var response = (HttpWebResponse)http.GetResponse();
var responseStream = response.GetResponseStream();
if (responseStream == null)
{
throw new InvalidOperationException();
}
using (var sr = new StreamReader(responseStream))
{
var responseJson = sr.ReadToEnd();
password = responseJson.Trim('"');
}
_logger.Info($"Slot:{Slot} - Got password from GenesisPasswordServiceUrl, URL({url})");
}
catch (Exception ex)
{
_logger.Info($"Slot:{Slot} - Cannot get password from GenesisPasswordService, " +
$"URL({url}), Error({ex.Message})");
var listOfOfflinePasswords = JsonConvert.DeserializeObject<List<OfflinePasswordItem>>(File.ReadAllText(ProgramConfig.OfflineInfoFile));
password = listOfOfflinePasswords.First(x => x.PcbId == PcbId).Password;
if (!string.IsNullOrEmpty(password))
{
_logger.Info($"Slot:{Slot} - Got password from password file!");
}
}
return password;
}
/// <inheritdoc />
/// <remarks date="2018JAN08" author="drabesch">
/// Get password from server
/// </remarks>
public Boolean Login()
{
var password = GetPassword();
return !string.IsNullOrEmpty(password) && Login(password);
}
/// <remarks date="2018JAN08" author="drabesch">
/// Get password from server
/// </remarks>
public Boolean QuickLogin()
{
var password = GetPassword();
return !string.IsNullOrEmpty(password) && Login(password, true, true);
}
/// <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;
}
}
/// <inheritdoc />
/// <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="quickLogin"></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>
public Boolean Login(String password, Boolean runImmediately = true, Boolean quickLogin = 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 (quickLogin)
{
WriteRegister(Register.Mertrologyasst.PulseMode, 0, true, true);
WriteRegister(Register.Sensusradio.WakeupInterval, 6, true, true);
WriteRegister("SENSUSRADIO_SystemState", 1, true, true);
}
if (Configuration != null && !Configuration.ProductionMode)
{
IsDevelopmentUsage = true;
}
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>
/// List of all present meter applications
/// </summary>
public List<MeterApplications> MeterAppListVersion = new List<MeterApplications>();
/// <summary>
/// Core revision of boot code.
/// </summary>
public String CoreRevision;
/// <summary>
/// This is the FLEXNET 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 or NA) and Radio frequency (433 or 868 or null).
/// </summary>
public String RegionRadio;
/// <summary>
/// Metrology upgrade permission.
/// </summary>
public Byte MetrologyUpgradePermission;
/// <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>
private void ReadMeterFirmwareAndAssignRegisters()
{
//avoid overwriting of list if this already exits
if (MeterAppListVersion.Any())
{
_logger.Warn("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 = "";
var recount = 4;
while (recount > 0)
{
recount -= 1;
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));
}
break;
}
}
}
}
//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,
});
}
//read metrology upgrade permission
MetrologyUpgradePermission = RegisterConverter.ConvertTo<Byte>(
ReadRegister(Register.System.MetrologyUpgradePermission));
//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)
{
// ignored
}
//read meter size
MeterSize = MeterSizeConverter.ConvertMeterSizeEnumToSizeName((MeterSize)RegisterConverter.ConvertTo<Byte>(
ReadRegister(Register.Genesisflow.MeterSize)));
//read core revision
var coreRegisterRead = RegisterConverter.ConvertTo<String>(ReadRegister(Register.System.CoreRevision, 16));
var coreRevision = coreRegisterRead?.Split('\0');
CoreRevision = coreRevision == null ? "0.00" : $"{coreRevision[0].ToLower().Replace("cordonel ", "")}";
//read radio frequency
RegionRadio = "NA";
try
{
var radioFrequencyRaw = ReadRegister(Register.Sensusradio.FrequencyIndicator);
if (radioFrequencyRaw != null)
{
var radioFrequency = RegisterConverter.ConvertTo<UInt16>(radioFrequencyRaw);
RegionRadio = $"EMEA_{radioFrequency}";
}
}
catch (Exception)
{
// ignored
}
//log application information to file
_logger.Info($"PCB ID: {PcbId}");
_logger.Info($"Date time (UTC): {DateTimeOffset.UtcNow}");
if (CoreRevision != null)
{
_logger.Info("System Core Revision: " + CoreRevision);
}
_logger.Info("Region and radio frequency[Mhz]: " + RegionRadio);
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($"AppId: 0x{fm.AppId:X2} - {versionString} - AppName: {fm.AppName}");
}
_logger.Info($"Meter size: {MeterSize}");
//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.Any(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, $"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>
public String GetPcbId(Int32 expectedLength = 12, Int32 retriesLeft = 2)
{
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} - Get PCB identification old{PcbId}");
var listPcb = new List<Byte>();
var hasZero = false;
var maxReads = 4;
while (!hasZero)
{
var chunk = ReadRegister(Register.Configexchange.PCBSerialNumber, 4);
maxReads -= 1;
if (chunk == null)
{
break;
}
listPcb.AddRange(chunk);
if (listPcb.Contains(0))
{
hasZero = true;
}
if (maxReads == 0)
{
break;
}
}
_logger.Info("First read ");
if (listPcb.Any())
{
var response = listPcb.ToArray();
PcbId = Encoding.ASCII.GetString(response).Split('\0')[0];
if (PcbId.Length < 8 && retriesLeft >= 1)
{
return GetPcbId(expectedLength, retriesLeft - 1);
}
return PcbId;
}
if (retriesLeft >= 1)
{
return GetPcbId(expectedLength, retriesLeft - 1);
}
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)
{
var regDef = _configRegister.GetRegisterDefinitionByName(reg);
var data = RegisterConverter.ConvertFrom(value);
_logger.Info($"Slot:{Slot} - Write register({regDef.RegisterName}), " +
$"record({BitConverter.ToString(data.ToArray())})");
PreRegisterWrite(regDef, data);
var command = data.Length > RegisterDefinition.ChunkSize ?
Commands.MultipleWriteData : Commands.WriteData;
var responseRecord = RequestProtocol.CommandToMeter(command, regDef, data,
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;
}
/// <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)
{
var regDef = _configRegister.GetRegisterDefinitionByName(reg);
_configRegister.Set(regDef, null);
_logger.Info($"Slot:{Slot} - Read register({regDef.RegisterName})");
if (!expectedLength.HasValue || expectedLength.Value <= RegisterDefinition.ChunkSize)
{
RequestProtocol?.CommandToMeter(Commands.ReadData, regDef,
skipRetryErrorCode: skipRetryErrorCode);
}
else
{
RequestProtocol?.CommandToMeter(Commands.MultipleReadData, regDef, expectedLength: expectedLength,
skipRetryErrorCode: skipRetryErrorCode);
}
RequestProtocolProcess();
var result = _configRegister.Get(reg);
PostRegisterRead(regDef, result);
return result;
}
/// <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");
// Logout();
// ReLogin();
//removed because this stops all processing which is critical on FW-Update!
//if (!_autoLogon || !RequestProtocol.ContainsRegisterIdent("password"))
//{
// throw new AuthenticationException("Can not access meter. " +
// "Reason AuthorizationRequired");
//}
if (!string.IsNullOrEmpty(_password))
{
IsLoggedOn = false;
if (Login(_password, false))
{
}
// //removed because this stops all processing which is critical on FW-Update!
// //throw new AuthenticationException("Login failed");
//}
}
}
break;
case RequestAcknowledgeState.Ok:
break;
//try out this
case RequestAcknowledgeState.CommandError:
case RequestAcknowledgeState.MeterError:
Thread.Sleep(200);
break;
}
}
public byte[] SendUI1236Command(byte[] UI1236Frame,bool waitForResult = true,bool checkRegister = false,ushort skipRetryErrorCode = 4)
{
this._logger.Info(string.Format("Slot:{0} - Sending Ui-1236 frame: {1} ", (object)this.Slot, (object)UI1236Frame));
RequestRecord sendFifoUi1236 = this.RequestProtocol.AddRecordToSendFifoUI1236(UI1236Frame);
this.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}");
//if (_enableRawDataLogging)
//{
// if (_loggerRawData == null)
// {
// _loggerRawData = NLogHelper.CreateOrGetMultiLogger(
// StreamingPort.GetPortName(), "", "LedRawData", "TargetLedRawDataBase", "LedRawDataBase");
// }
// _loggerRawData.Info($"{text}");
//}
}
/// <inheritdoc />
public void LogRawData(Boolean on)
{
_enableRawDataLogging = on;
if (StreamingPort is LedSerialPort port)
{
port.RecordStreamingRawData = _enableRawDataLogging;
}
}
/// <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 readback as this has no handler in the FW.
/// </remarks>
public Boolean StoreAllConfigurations()
{
var allConfigRegisters = _configRegister.MeterRegisterDic.Count(a =>
a.Key.RegisterName.ToLower() == "storeconfiguration" ||
a.Key.RegisterName.ToLower() == "storecalibration");
try
{
foreach (var a in _configRegister.MeterRegisterDic.Where(a =>
a.Key.RegisterName.ToLower() == "storeconfiguration" ||
a.Key.RegisterName.ToLower() == "storecalibration"))
{
var done = false;
//retries for each register access
var retryLeft = 3;
while (retryLeft > 0 && !done)
{
Thread.Sleep(50);
var result = WriteRegister(a.Key.GetIdent(), 1);
if (!result)
{
retryLeft -= 1;
if (retryLeft == 0)
{
return false;
}
continue;
}
// Wait for storing
Thread.Sleep(400);
//TODO enable this for R1.2.x
if (a.Key.AppName == "SENSUSRADIO" || a.Key.AppName == "NA2WALARMS")
{
done = true;
allConfigRegisters -= 1;
continue;
}
var resultRead = ReadRegister(a.Key.GetIdent());
if (resultRead == null || resultRead.ToList().Any(ra => ra != 0x00))
{
retryLeft -= 1;
if (retryLeft == 0)
{
return false;
}
continue;
}
done = true;
allConfigRegisters -= 1;
}
}
}
catch (Exception)
{
// ignored
}
return allConfigRegisters == 0;
}
/// <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, 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(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($"HTTP status not OK"),
"Can't store at RegisterWatchService");
}
}
catch (Exception ex)
{
_logger.Error(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($"HTTP status not OK({response.StatusCode})"), "can not store at RegisterWatchService");
}
}
catch (Exception ex)
{
_logger.Error(ex, "can not store at RegisterWatchService");
}
}
}
#endregion
/// <summary>
/// Set the process state
/// </summary>
/// <param name="newState"></param>
public void SetProcessState(DisplayCodes newState)
{
SetProcessState(newState, true);
}
}
}