Files
tbf/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs
T

958 lines
32 KiB
C#

using GenesisCordonelInterface.Core.Threading;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
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 GenesisCordonelInterface.API.GciPublicModels;
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 Task<GciPublicModels.GciInitSlotResult> 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 GciPublicModels.GciInitSlotResult InitOneMeterFromExtern(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str)
{
const string operation = nameof(InitOneMeterFromExtern);
try
{
LogInfo(operation,
$"Start. Slot={slot}, GciConfigSource={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 GciPublicModels.GciInitSlotResult { Success = true, SlotId = slot };
}
catch (Exception ex)
{
LogError(operation, ex);
return new GciPublicModels.GciInitSlotResult
{
Success = false,
SlotId = slot,
Message = ex.Message
};
}
}
public Task<GciPublicModels.GciSlotInfo> GetOneMeterInfo(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => GetOneMeterInfo(slot), token);
}
public GciPublicModels.GciSlotInfo GetOneMeterInfo(int slotId)
{
const string operation = nameof(GetOneMeterInfo);
try
{
LogInfo(operation, $"Start. Slot={slotId}");
var meter = _meterBatch.ListOfMeters
.OfType<GenesisMeter>()
.FirstOrDefault(m => m.Slot == slotId);
if (meter == null)
{
return new GciPublicModels.GciSlotInfo
{
SlotId = slotId,
Success = true,
Exists = false,
Message = "Slot is empty."
};
}
return new GciPublicModels.GciSlotInfo
{
SlotId = slotId,
Success = true,
Exists = true,
Message = "Slot found.",
// PcbId = meter.PcbId,
ConfigSource = GciPublicModels.MapConfigSourceBack(meter.useConfigSource),
PasswordSource = GciPublicModels.MapPasswordSourceBack(meter.usePasswordSource),
RequestPort = GciPublicModels.MapPortBack(meter.RequestPort),
StreamingPort = GciPublicModels.MapPortBack(meter.StreamingPort)
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new GciPublicModels.GciSlotInfo
{
SlotId = slotId,
Success = false,
Exists = false,
Message = ex.Message
};
}
}
public Task<GciPublicModels.GciCleanSlotsResult> CleanSlotsAsync(
CancellationToken token = default)
{
return Task.Run(() => CleanSlots(), token);
}
public GciPublicModels.GciCleanSlotsResult CleanSlots()
{
const string operation = nameof(CleanSlots);
try
{
LogInfo(operation, "Start.");
_meterBatch.ListOfMeters.Clear();
_selectedSlots.Clear();
foreach (var worker in _workers.Values)
{
worker.Dispose();
}
_workers.Clear();
LogInfo(operation, "Success. Meter batch, selected slots and workers cleared.");
return new GciPublicModels.GciCleanSlotsResult
{
Success = true,
Message = "Slots cleaned."
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new GciPublicModels.GciCleanSlotsResult
{
Success = false,
Message = 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 Task<GciPublicModels.GciConnectResult> ConnectOneSlotAsync(int slot, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => ConnectOneSlot(slot), token);
}
public GciPublicModels.GciConnectResult ConnectOneSlot(int slot)
{
const string operation = nameof(ConnectOneSlot);
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 GciPublicModels.GciConnectResult
{
Success = false,
SlotId = slot,
Message = "Login failed."
};
}
var result = new GciPublicModels.GciConnectResult
{
Success = true,
SlotId = 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 GciPublicModels.GciConnectResult
{
Success = false,
SlotId = slot,
Message = ex.Message
};
}
}
private List<GciPublicModels.GciRegisterSnapshot> BuildRegisters(GenesisMeter meter)
{
var list = new List<GciRegisterSnapshot>();
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 GciRegisterSnapshot
{
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 ================================== DISCONNECT ==================================
public Task<GciPublicModels.GciDisconnectResult> DisconnectAsync(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => Disconnect(slot), token);
}
public GciPublicModels.GciDisconnectResult 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}");
return new GciPublicModels.GciDisconnectResult
{
SlotId = slot,
Success = true,
Message = "Disconnected successfully."
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new GciPublicModels.GciDisconnectResult
{
SlotId = slot,
Success = false,
Message = ex.Message
};
}
finally
{
if (_workers.TryRemove(slot, out var worker))
{
worker.Dispose();
LogInfo(operation, $"Worker disposed. Slot={slot}");
}
}
}
#endregion
#region ================================== PCB ==================================
public Task<GciPublicModels.GciGetPcbIdResult> GetPcbIdAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(() => GetPcbId(slot), token, "GetPcbId");
}
public GciPublicModels.GciGetPcbIdResult 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 new GciPublicModels.GciGetPcbIdResult
{
SlotId = slot,
Success = !string.IsNullOrWhiteSpace(pcbId),
PcbId = pcbId,
Message = !string.IsNullOrWhiteSpace(pcbId)
? "PCB ID read successfully."
: "PCB ID is empty."
};
}
}
catch (Exception ex)
{
LogError(operation, ex);
return new GciPublicModels.GciGetPcbIdResult
{
SlotId = slot,
Success = false,
PcbId = null,
Message = ex.Message
};
}
}
#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 ================================== 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
}
}