tbf/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs

857 lines
28 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GenesisCordonelInterface.Core.Threading;
using NLog;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore;
using Xylem.Common.Hardware.WaterMeter.Genesis.Registers;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
namespace GenesisCordonelInterface.API
{
public class InterfaceGCIToLaatzen
{
#region Fields
//private static readonly Lazy<ILogger> Logger = new Lazy<ILogger>(() => LogManager.GetLogger("GCI"));
private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface");
private readonly MeterBatch _meterBatch = new MeterBatch();
private readonly ConcurrentDictionary<int, ApiWorker> _workers =
new ConcurrentDictionary<int, ApiWorker>();
private readonly ConcurrentDictionary<int, bool> _selectedSlots =
new ConcurrentDictionary<int, bool>();
#endregion
#region Worker
private ApiWorker GetWorker(int slot)
{
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot));
return _workers.GetOrAdd(slot, s => new ApiWorker($"GCI Worker Slot {s}"));
}
#endregion
#region Worker Debug
public class WorkerDebugStatus
{
public int Slot { get; set; }
public string Name { get; set; }
public int QueueLength { get; set; }
public bool IsBusy { get; set; }
public string CurrentOperation { get; set; }
public string LastError { get; set; }
public DateTime LastActivity { get; set; }
}
public List<WorkerDebugStatus> GetWorkerDebugStatuses()
{
return _workers
.Select(x => new WorkerDebugStatus
{
Slot = x.Key,
Name = x.Value.Name,
QueueLength = x.Value.QueueLength,
IsBusy = x.Value.IsBusy,
CurrentOperation = x.Value.CurrentOperation,
LastError = x.Value.LastError,
LastActivity = x.Value.LastActivity
})
.OrderBy(x => x.Slot)
.ToList();
}
#endregion
#region MeterBatch Debug
public class MeterBatchDebugStatus
{
public int Slot { get; set; }
public bool Selected { get; set; }
public string PcbId { get; set; }
public bool IsLoggedOn { get; set; }
public string RequestPort { get; set; }
public string StreamingPort { get; set; }
public string FwVersion { get; set; }
public string InterfaceVersion { get; set; }
}
public List<MeterBatchDebugStatus> GetMeterBatchDebugStatuses()
{
return _meterBatch.ListOfMeters
.OfType<GenesisMeter>()
.Select(m => new MeterBatchDebugStatus
{
Slot = m.Slot,
Selected = IsSlotSelected(m.Slot),
PcbId = m.PcbId,
IsLoggedOn = m.IsLoggedOn,
RequestPort = m.RequestPort?.GetPortName(),
StreamingPort = m.StreamingPort?.GetPortName(),
FwVersion = m.FwVersion,
InterfaceVersion = m.InterfaceInfo?.InterfaceVersion
})
.OrderBy(x => x.Slot)
.ToList();
}
public void SetSlotSelected(int slot, bool selected)
{
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot));
Logger.Debug(
"[{0}] SetSlotSelected: slot={1}, selected={2}",
InterfaceName,
slot,
selected);
_selectedSlots[slot] = selected;
}
public bool IsSlotSelected(int slot)
{
return _selectedSlots.TryGetValue(slot, out bool selected) && selected;
}
public List<int> GetSelectedSlots()
{
return _selectedSlots
.Where(x => x.Value)
.Select(x => x.Key)
.OrderBy(x => x)
.ToList();
}
#endregion
#region Helpers
private GenesisMeter GetMeter(int slot)
{
var meter = _meterBatch.ListOfMeters
.OfType<GenesisMeter>()
.FirstOrDefault(m => m.Slot == slot);
if (meter == null)
throw new InvalidOperationException($"Meter for slot {slot} not initialized.");
return meter;
}
private void EnsureConnected(GenesisMeter meter)
{
if (!meter.IsLoggedOn)
throw new InvalidOperationException("Meter is not connected.");
}
private string ToHex(byte[] data)
{
return data == null ? "" : BitConverter.ToString(data).Replace("-", " ");
}
private const string InterfaceName = "InterfaceGCIToLaatzen";
private void LogInfo(string operation, string message)
{
Logger.Info("[{0}] {1}: {2}", InterfaceName, operation, message);
}
private void LogError(string operation, Exception ex)
{
Logger.Error(ex, "[{0}] {1} failed: {2}", InterfaceName, operation, ex.Message);
}
private string SafePort(string port)
{
return string.IsNullOrWhiteSpace(port) ? "<empty>" : port;
}
#endregion
#region INIT
public class InitResult
{
public bool Success { get; set; }
public int Slot { get; set; }
public string ErrorMessage { get; set; }
}
public Task<InitResult> InitOneMeterFromExternAsync(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() =>
InitOneMeterFromExtern(slot, cfg, pwd, req, str), token);
}
public InitResult InitOneMeterFromExtern(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str)
{
const string operation = nameof(InitOneMeterFromExtern);
try
{
LogInfo(operation,
$"Start. Slot={slot}, ConfigSource={cfg}, PasswordSource={pwd}, " +
$"RequestPort={(req.HasValue ? req.Value.PortName.ToString() : "<empty>")}, " +
$"StreamingPort={(str.HasValue ? str.Value.PortName.ToString() : "<empty>")}");
var meter = new GenesisMeter
{
useConfigSource = cfg,
usePasswordSource = pwd
};
meter.SetupFromExternConfig(slot, req, str, true);
_meterBatch.AddMeter(meter);
LogInfo(operation, $"Success. Slot={slot}, MeterBatchCount={_meterBatch.ListOfMeters.Count}");
return new InitResult { Success = true, Slot = slot };
}
catch (Exception ex)
{
LogError(operation, ex);
return new InitResult
{
Success = false,
Slot = slot,
ErrorMessage = ex.Message
};
}
}
#endregion
#region PORT DETECTION
public class PortDetectionResult
{
public bool Success { get; set; }
public int Slot { get; set; }
public string PortName { get; set; }
public string PcbId { get; set; }
public string ErrorMessage { get; set; }
}
public Task<PortDetectionResult> DetectStreamingPortAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(() => DetectStreamingPort(slot), token);
}
public PortDetectionResult DetectStreamingPort(int slot)
{
const string operation = nameof(DetectStreamingPort);
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot), "Slot number must be greater than zero.");
try
{
LogInfo(operation, $"Start. Slot={slot}");
using (var mb = new MeterBatch())
using (var meter = new GenesisMeter())
{
mb.AddMeter(meter);
var rawData = new ConcurrentBag<string>();
meter.StreamingPort.OnRawRecordReceived += (o, rawMsg) =>
{
var data = (string)rawMsg.GetData();
rawData.Add(data);
};
Thread.Sleep(500);
bool success = rawData.Any();
string portName = meter.StreamingPort.GetPortName();
LogInfo(operation,
$"Finish. Slot={slot}, Success={success}, Port={SafePort(portName)}, RawRecords={rawData.Count}");
return new PortDetectionResult
{
Success = success,
Slot = slot,
PortName = portName,
ErrorMessage = success ? null : "No streaming data received."
};
}
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
public Task<PortDetectionResult> DetectRequestPortAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(() => DetectRequestPort(slot), token);
}
public PortDetectionResult DetectRequestPort(int slot)
{
const string operation = nameof(DetectRequestPort);
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot), "Slot number must be greater than zero.");
try
{
LogInfo(operation, $"Start. Slot={slot}");
using (var mb = new MeterBatch())
using (var meter = new GenesisMeter())
{
mb.AddMeter(meter);
meter.Logout();
string pcbId = meter.GetPcbId();
bool success = !string.IsNullOrEmpty(pcbId);
string portName = meter.RequestPort.GetPortName();
LogInfo(operation,
$"Finish. Slot={slot}, Success={success}, Port={SafePort(portName)}, PcbId={pcbId ?? "<empty>"}");
return new PortDetectionResult
{
Success = success,
Slot = slot,
PortName = portName,
PcbId = pcbId,
ErrorMessage = success ? null : "PCB ID was empty."
};
}
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
#endregion
#region CONNECT
public class ConnectResult
{
public bool Success { get; set; }
public int Slot { get; set; }
public string PcbId { get; set; }
public bool IsLoggedOn { get; set; }
public string FwVersion { get; set; }
public string InterfaceVersion { get; set; }
public bool InterfaceSupportsFwVersion { get; set; }
public List<RegisterSnapshot> Registers { get; set; } = new List<RegisterSnapshot>();
public string ErrorMessage { get; set; }
}
public class RegisterSnapshot
{
public string Name { get; set; }
public string Type { get; set; }
public string RawValue { get; set; }
public string Min { get; set; }
public string Max { get; set; }
public string Description { get; set; }
public string Version { get; set; }
public string IsAvailable { get; set; }
public string Privilege { get; set; }
}
public Task<ConnectResult> ConnectOneMeterAsync(int slot, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => ConnectOneMeter(slot), token);
}
public ConnectResult ConnectOneMeter(int slot)
{
const string operation = nameof(ConnectOneMeter);
try
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeter(slot);
_meterBatch.MetersLogin();
if (!meter.IsLoggedOn)
{
LogInfo(operation, $"Failed. Slot={slot}, Reason=Login failed.");
return new ConnectResult
{
Success = false,
Slot = slot,
ErrorMessage = "Login failed."
};
}
var result = new ConnectResult
{
Success = true,
Slot = slot,
PcbId = meter.PcbId,
IsLoggedOn = true,
FwVersion = meter.FwVersion,
InterfaceVersion = meter.InterfaceInfo?.InterfaceVersion,
InterfaceSupportsFwVersion = meter.InterfaceSupportsFwVersion,
Registers = BuildRegisters(meter)
};
LogInfo(operation,
$"Success. Slot={slot}, PcbId={result.PcbId ?? "<empty>"}, " +
$"FwVersion={result.FwVersion ?? "<empty>"}, InterfaceVersion={result.InterfaceVersion ?? "<empty>"}, " +
$"Registers={result.Registers.Count}");
return result;
}
catch (Exception ex)
{
LogError(operation, ex);
return new ConnectResult
{
Success = false,
Slot = slot,
ErrorMessage = ex.Message
};
}
}
private List<RegisterSnapshot> BuildRegisters(GenesisMeter meter)
{
var list = new List<RegisterSnapshot>();
foreach (var item in meter.GetRegistersDic())
{
var from = item.Key.RegisterDetail.Version.First?.ToString() ?? "-";
var to = item.Key.RegisterDetail.Version.Last?.ToString() ?? "-";
list.Add(new RegisterSnapshot
{
Name = item.Key.GetIdent(),
Type = item.Key.DataType.Name,
RawValue = BitConverter.ToString(item.Value).Replace("-", " "),
Min = item.Key.Minimum?.ToString(),
Max = item.Key.Maximum?.ToString(),
Description = item.Key.RegisterDetail.Description,
Version = $"from {from} to {to}",
IsAvailable = item.Key.IsAvailable.ToString(),
Privilege = item.Key.RegisterDetail.Privilege.Lvl8.ToString()
});
}
return list;
}
#endregion
#region PCB
public Task<string> GetPcbIdAsync(int slot, CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(() => GetPcbId(slot), token, "GetPcbId");
}
public string GetPcbId(int slot)
{
const string operation = nameof(GetPcbId);
try
{
LogInfo(operation, $"Start. Slot={slot}");
using (var mb = new MeterBatch())
using (var meter = new GenesisMeter())
{
meter.SetupFromConfigFile(slot, false);
mb.AddMeter(meter);
meter.Logout();
string pcbId = meter.GetPcbId();
LogInfo(operation, $"Finish. Slot={slot}, PcbId={pcbId ?? "<empty>"}");
return pcbId;
}
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
#endregion
#region REGISTER READ
public class RegisterReadResult
{
public bool Success { get; set; }
public string RegisterName { get; set; }
public string RawHex { get; set; }
public string ErrorMessage { get; set; }
}
public Task<RegisterReadResult> ReadRegisterAsync(int slot, string name, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => ReadRegister(slot, name), token);
}
public RegisterReadResult ReadRegister(int slot, string name)
{
const string operation = nameof(ReadRegister);
try
{
LogInfo(operation, $"Start. Slot={slot}, Register={name}");
var meter = GetMeter(slot);
EnsureConnected(meter);
var raw = meter.ReadRegister(name);
var result = new RegisterReadResult
{
Success = true,
RegisterName = name,
RawHex = ToHex(raw)
};
LogInfo(operation, $"Success. Slot={slot}, Register={name}, RawHex={result.RawHex}");
return result;
}
catch (Exception ex)
{
LogError(operation, ex);
return new RegisterReadResult
{
Success = false,
RegisterName = name,
ErrorMessage = ex.Message
};
}
}
#endregion
#region REGISTER WRITE
public class RegisterWriteResult
{
public bool Success { get; set; }
public string RegisterName { get; set; }
public object WrittenValue { get; set; }
public bool StoreToDevice { get; set; }
public bool RefreshSystemState { get; set; }
public string ErrorMessage { get; set; }
}
public Task<RegisterWriteResult> WriteRegisterAsync(
int slot,
string registerName,
object value,
bool storeToDevice = false,
bool refreshSystemState = false,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(
() => WriteRegister(slot, registerName, value, storeToDevice, refreshSystemState),
token,
"WriteRegister");
}
public RegisterWriteResult WriteRegister(
int slot,
string registerName,
object value,
bool storeToDevice = false,
bool refreshSystemState = false)
{
const string operation = nameof(WriteRegister);
try
{
LogInfo(operation,
$"Start. Slot={slot}, Register={registerName}, Value={value}, " +
$"StoreToDevice={storeToDevice}, RefreshSystemState={refreshSystemState}");
var meter = GetMeter(slot);
EnsureConnected(meter);
if (string.IsNullOrWhiteSpace(registerName))
throw new ArgumentException("Register name cannot be empty.", nameof(registerName));
bool writeOk = meter.WriteRegister(registerName, value);
if (!writeOk)
{
LogInfo(operation, $"Failed. Slot={slot}, Register={registerName}, Reason=Write operation failed.");
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
ErrorMessage = "Write operation failed."
};
}
if (storeToDevice)
{
if (!meter.StoreAllConfigurations())
{
LogInfo(operation, $"Failed. Slot={slot}, Register={registerName}, Reason=StoreAllConfigurations failed.");
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = true,
RefreshSystemState = refreshSystemState,
ErrorMessage = "StoreAllConfigurations failed."
};
}
}
if (refreshSystemState)
{
if (!meter.WriteRegister("SENSUSRADIO_SYSTEMSTATE", 0xFF, true, false))
{
LogInfo(operation, $"Failed. Slot={slot}, Register={registerName}, Reason=System state refresh failed.");
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = storeToDevice,
RefreshSystemState = true,
ErrorMessage = "System state refresh failed."
};
}
}
LogInfo(operation, $"Success. Slot={slot}, Register={registerName}");
return new RegisterWriteResult
{
Success = true,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = storeToDevice,
RefreshSystemState = refreshSystemState
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = storeToDevice,
RefreshSystemState = refreshSystemState,
ErrorMessage = ex.Message
};
}
}
#endregion
#region DISCONNECT
public Task DisconnectAsync(int slot, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => Disconnect(slot), token);
}
public void Disconnect(int slot)
{
const string operation = nameof(Disconnect);
try
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeter(slot);
if (meter.IsLoggedOn)
meter.Logout();
meter.DisposeMeter();
LogInfo(operation, $"Success. Slot={slot}");
}
catch (Exception ex)
{
LogError(operation, ex);
}
finally
{
if (_workers.TryRemove(slot, out var worker))
{
worker.Dispose();
LogInfo(operation, $"Worker disposed. Slot={slot}");
}
}
}
#endregion
#region PASSWORD
public Task<bool> SetMeterPasswordAsync(
int slot,
string password,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(
() => SetMeterPassword(slot, password),
token,
"SetMeterPassword");
}
public bool SetMeterPassword(int slot, string password)
{
const string operation = nameof(SetMeterPassword);
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password cannot be empty.", nameof(password));
LogInfo(operation, $"Start. Slot={slot}");
var result = WriteRegister(
slot,
"SECURITY_Password",
password,
true,
true);
LogInfo(operation, $"Finish. Slot={slot}, Success={result.Success}");
return result.Success;
}
#endregion
#region METER BATCH SETUP
// ----------------------------------------------------
public void ReloadSlotSetup()
{
const string operation = nameof(ReloadSlotSetup);
try
{
LogInfo(operation, "Start.");
_meterBatch.ListOfMeters.Clear();
_selectedSlots.Clear();
// TODO:
// Load meter batch setup from persistent storage.
// Example:
// _meterBatch.SetupFromConfigFile();
LogInfo(operation, "Success. Meter batch and selected slots cleared.");
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
public void SaveSlotSetup(List<MeterBatchDebugStatus> data)
{
const string operation = nameof(SaveSlotSetup);
try
{
LogInfo(operation, $"Start. Rows={data?.Count ?? 0}");
if (data == null)
throw new ArgumentNullException(nameof(data));
_meterBatch.ListOfMeters.Clear();
_selectedSlots.Clear();
foreach (var item in data)
{
var meter = new GenesisMeter();
PortConfig? req = string.IsNullOrWhiteSpace(item.RequestPort)
? (PortConfig?)null
: new PortConfig { PortName = item.RequestPort, Type = "Serial" };
PortConfig? str = string.IsNullOrWhiteSpace(item.StreamingPort)
? (PortConfig?)null
: new PortConfig { PortName = item.StreamingPort, Type = "Serial" };
meter.useConfigSource = ConfigSource.InterfaceInputConfig;
meter.SetupFromExternConfig(item.Slot, req, str, true);
_meterBatch.AddMeter(meter);
_selectedSlots[item.Slot] = item.Selected;
LogInfo(
operation,
$"Saved row. Slot={item.Slot}, Selected={item.Selected}, " +
$"RequestPort={SafePort(item.RequestPort)}, StreamingPort={SafePort(item.StreamingPort)}");
}
LogInfo(operation, $"Success. MeterBatchCount={_meterBatch.ListOfMeters.Count}, SelectedSlots={_selectedSlots.Count}");
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
// ----------------------------------------------------
#endregion
}
}