diff --git a/GenesisCordonelInterface/API/GciPublicModels.cs b/GenesisCordonelInterface/API/GciPublicModels.cs
new file mode 100644
index 000000000..aee36896a
--- /dev/null
+++ b/GenesisCordonelInterface/API/GciPublicModels.cs
@@ -0,0 +1,312 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static GenesisCordonelInterface.API.InterfaceOutsideToGCI;
+using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
+using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
+
+namespace GenesisCordonelInterface.API
+{
+ ///
+ /// Public data contract layer for the Genesis Cordonel Interface (GCI).
+ ///
+ /// This class defines all Data Transfer Objects (DTOs) that are exposed
+ /// to external consumers (e.g. TBF, UI, or other integration layers).
+ ///
+ /// Responsibilities:
+ /// - Provide stable, dependency-free models for external usage
+ /// - Decouple internal GCI implementation (GenesisMeter, Xylem libraries)
+ /// from external systems
+ /// - Define request/response contracts for all supported operations
+ /// - Contain mapping methods between public DTOs and internal domain models
+ ///
+ /// Architecture:
+ /// External world (TBF / UI)
+ /// ↓
+ /// GciPublicModels (this layer)
+ /// ↓
+ /// Internal GCI API (InterfaceGCIToLaatzen, GenesisMeter, etc.)
+ ///
+ /// Notes:
+ /// - Public models must NOT expose internal types (e.g. GenesisMeter, IPort, etc.)
+ /// - All mapping between internal and external representations must be done here
+ /// - DTOs are designed to be simple, serializable, and stable over time
+ /// - Any change in internal implementation should not affect these models
+ ///
+ /// Pattern:
+ /// Each operation follows a consistent structure:
+ /// Request → Operation → Result
+ ///
+ /// Example:
+ /// GciInitSlotRequest → InitSlot → GciInitSlotResult
+ /// GetSlot → GciSlotInfo
+ /// GetPcbId → GciGetPcbIdResult
+ ///
+ /// This layer acts as a boundary between domain logic and integration logic.
+ ///
+ public class GciPublicModels
+ {
+ ///
+ /// Public DTOs exposed to external systems.
+ /// These models represent the contract of the GCI API.
+ /// They must remain stable and independent of internal implementation.
+ ///
+ #region ================================== PUBLIC MODELS ===========================================
+
+ public class GciSlotInfo
+ {
+ public int SlotId { get; set; }
+
+ public bool Exists { get; set; }
+ public bool Success { get; set; }
+ public string Message { get; set; }
+ public string PcbId { get; set; }
+
+ public GciConfigSource ConfigSource { get; set; }
+ public GciPasswordSource PasswordSource { get; set; }
+
+ public GciPortConfig RequestPort { get; set; }
+ public GciPortConfig StreamingPort { get; set; }
+ }
+
+ public class Result
+ {
+ public int SlotId { get; set; }
+ public bool Success { get; set; }
+ public string Message { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format(
+ "SlotId={0}, Success={1}, Message={2}",
+ SlotId,
+ Success,
+ Message);
+ }
+ }
+
+ public enum GciPasswordSource
+ {
+ RestApi = 0,
+ OfflineFile = 1,
+ InterfaceInputPassword = 2,
+ }
+ public enum GciConfigSource
+ {
+ FileConfig = 0,
+ InterfaceInputConfig = 1,
+ }
+
+ ///
+ /// Collection of port settings
+ ///
+ public class GciPortConfig
+ {
+ public string PortName { get; set; }
+ public string Type { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format("PortName={0}, Type={1}", PortName, Type);
+ }
+ }
+
+ public class GciInitSlotRequest
+ {
+ public int SlotId { get; set; }
+ public GciConfigSource ConfigSource { get; set; }
+ public GciPasswordSource PasswordSource { get; set; }
+ public GciPortConfig RequestPort { get; set; }
+ public GciPortConfig StreamingPort { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format(
+ "SlotId={0}, GciConfigSource={1}, PasswordSource={2}, RequestPort={3}, StreamingPort={4}",
+ SlotId,
+ ConfigSource,
+ PasswordSource,
+ RequestPort,
+ StreamingPort);
+ }
+ }
+
+ public class GciInitSlotResult
+ {
+ public int SlotId { get; set; }
+
+ public bool Success { get; set; }
+
+ public string Message { get; set; }
+
+ public string PcbId { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format(
+ "SlotId={0}, Success={1}, Message={2}, PcbId={3}",
+ SlotId,
+ Success,
+ Message,
+ PcbId);
+ }
+ }
+
+ public class GciCleanSlotsResult
+ {
+ public bool Success { get; set; }
+ public string Message { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format(
+ "Success={0}, Message={1}",
+ Success,
+ Message);
+ }
+ }
+
+ public class GciGetPcbIdResult
+ {
+ public int SlotId { get; set; }
+
+ public bool Success { get; set; }
+
+ public string PcbId { get; set; }
+
+ public string Message { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format(
+ "SlotId={0}, Success={1}, PcbId={2}, Message={3}",
+ SlotId,
+ Success,
+ PcbId,
+ Message);
+ }
+ }
+ public class GciConnectResult
+ {
+ public int SlotId { get; set; }
+ public bool Success { 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 Registers { get; set; } = new List();
+ public string Message { get; set; }
+ }
+
+ public class GciRegisterSnapshot
+ {
+ 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 class GciDisconnectResult
+ {
+ public int SlotId { get; set; }
+ public bool Success { get; set; }
+ public string Message { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format(
+ "SlotId={0}, Success={1}, Message={2}",
+ SlotId,
+ Success,
+ Message);
+ }
+ }
+ #endregion
+
+ ///
+ /// Mapping methods between public DTOs and internal GCI models.
+ /// Ensures separation between external contracts and internal domain objects.
+ ///
+ #region ================================== OUTERN/INTERN and back models mapping ==================================
+ public static PasswordSource MapPasswordSource(GciPasswordSource src)
+ {
+ return (PasswordSource)src;
+ }
+
+ public static ConfigSource MapConfigSource(GciConfigSource src)
+ {
+ return (ConfigSource)src;
+ }
+
+ public static Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig? MapPort(GciPortConfig port)
+ {
+ if (port == null)
+ return null;
+
+ Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig result = new Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig();
+
+ result.PortName = port.PortName;
+ result.Type = port.Type;
+
+ return result;
+ }
+
+ public static GciInitSlotResult MapInitResult(GciInitSlotResult result)
+ {
+ if (result == null)
+ return null;
+
+ return new GciInitSlotResult
+ {
+ SlotId = result.SlotId,
+ Success = result.Success,
+ Message = result.Message
+ };
+ }
+
+ public static GciPasswordSource MapPasswordSourceBack(PasswordSource src)
+ {
+ return (GciPasswordSource)src;
+ }
+
+ public static GciConfigSource MapConfigSourceBack(ConfigSource src)
+ {
+ return (GciConfigSource)src;
+ }
+
+ public static GciPortConfig MapPortBack(PortConfig? port)
+ {
+ if (!port.HasValue)
+ return null;
+
+ PortConfig value = port.Value;
+
+ return new GciPortConfig
+ {
+ PortName = value.PortName,
+ Type = value.Type
+ };
+ }
+
+ public static GciPortConfig MapPortBack(IPort port)
+ {
+ if (port == null)
+ return null;
+
+ return new GciPortConfig
+ {
+ PortName = port.GetPortName(),
+ Type = port.GetType().Name
+ };
+ }
+ #endregion
+ }
+}
diff --git a/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs b/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs
index 36b702bac..e0fdc31e6 100644
--- a/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs
+++ b/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs
@@ -1,925 +1,772 @@
-using Logic.ProductionToProductMapper.Cordonel;
-using Newtonsoft.Json;
+using GenesisCordonelInterface.Core.Threading;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Drawing;
-using System.Globalization;
-using System.IO;
using System.Linq;
-using System.Net;
-using System.Reflection;
-using System.Security.Policy;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using System.Windows.Forms;
-using Xylem.Common.CommonCore.Configuration;
-using Xylem.Common.CommonCore.Consts;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
-using Xylem.Common.Hardware.WaterMeter.Genesis.Applications.Const;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore;
-using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.Consts;
using Xylem.Common.Hardware.WaterMeter.Genesis.Registers;
-using Xylem.Common.Hardware.WaterMeter.Genesis.Registers.DataTypes;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
-using Xylem.Common.Logic.ProductionOrderCore.TestResults;
-using Xylem.Common.Logic.SoftwareAccessHelper;
-using Xylem.Common.Utils.Logging;
+using static GenesisCordonelInterface.API.GciPublicModels;
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
-using Access = Xylem.Common.Hardware.WaterMeter.Genesis.Registers.Access;
-using Register = Xylem.Common.Hardware.WaterMeter.Genesis.Registers.Register;
namespace GenesisCordonelInterface.API
{
- ///
- /// Provides a public API for Genesis meter operations.
- ///
- /// This class exposes reusable functionality extracted from the original UI code
- /// so it can be used from other projects within the solution.
- ///
- /// The API is intended to gradually consolidate meter-related operations such as:
- /// - port detection
- /// - PCB ID reading
- /// - communication setup
- /// - requests and commands
- /// - additional service actions
- ///
- ///
- /// This class should contain business logic only and should not depend on UI elements
- /// such as forms, controls, MessageBox, or DataGridView.
- ///
- /// UI-specific code should remain outside this class and call this API instead.
- ///
- ///
- ///
- /// var api = new Api2();
- ///
- /// var request = api.DetectRequestPort(3);
- /// if (request.Success)
- /// {
- /// Console.WriteLine($"PCB ID: {request.PcbId}");
- /// }
- ///
- /// var streaming = api.DetectStreamingPort(3);
- /// if (streaming.Success)
- /// {
- /// Console.WriteLine($"Streaming port: {streaming.PortName}");
- /// }
- ///
- ///
public class InterfaceGCIToLaatzen
{
- #region Declaration region
- private static readonly Lazy Logger = new Lazy(() => NLogHelper.CreateOrGetLogger("GenesisCordonelInterface"));
+ #region Fields
- public class regStore
+ //private static readonly Lazy Logger = new Lazy(() => LogManager.GetLogger("GCI"));
+ private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface");
+
+ private readonly MeterBatch _meterBatch = new MeterBatch();
+
+ private readonly ConcurrentDictionary _workers =
+ new ConcurrentDictionary();
+
+ private readonly ConcurrentDictionary _selectedSlots =
+ new ConcurrentDictionary();
+
+ #endregion
+
+ #region ================================== Worker ==================================
+
+ private ApiWorker GetWorker(int slot)
{
- public String PcbId;
- public DateTimeOffset created;
- public List keyValues;
+ if (slot <= 0)
+ throw new ArgumentOutOfRangeException(nameof(slot));
+
+ return _workers.GetOrAdd(slot, s => new ApiWorker($"GCI Worker Slot {s}"));
}
- public class regDefValue
+ #endregion
+
+ #region ================================== Worker Debug ==================================
+ public class WorkerDebugStatus
{
- public RegisterDefinition def;
- public String value;
+ 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; }
}
- private GenesisMeter _currentGenesis;
- private MeterBatch _meterBatch = new MeterBatch();
- private regStore _regsToStore;
- private String _currentPcbId = "";
-
- private Boolean IsBusy
+ public List GetWorkerDebugStatuses()
{
- get;
- set;
- }
-
- ///
- /// Gets a value indicating whether the meter is connected and logged on.
- ///
- public bool IsConnected
- {
- get
- {
- return _currentGenesis != null && _currentGenesis.IsLoggedOn;
- }
+ 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 API - Port Detection region(extracted from FrmSetup:DgvConfig_CellContentClick)
+ #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 GetMeterBatchDebugStatuses()
+ {
+ return _meterBatch.ListOfMeters
+ .OfType()
+ .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 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()
+ .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) ? "" : port;
+ }
+
+ #endregion
+
+ #region ================================== INIT ==================================
+
+ public Task 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() : "")}, " +
+ $"StreamingPort={(str.HasValue ? str.Value.PortName.ToString() : "")}");
+
+ 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 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()
+ .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 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
{
- ///
- /// Indicates whether the detection was successful.
- ///
public bool Success { get; set; }
-
- ///
- /// Slot number used for the detection.
- ///
public int Slot { get; set; }
-
- ///
- /// Name of the detected communication port.
- ///
public string PortName { get; set; }
-
- ///
- /// PCB ID read from the device (available for request detection).
- ///
public string PcbId { get; set; }
-
- ///
- /// Error message describing why detection failed (if not successful).
- ///
public string ErrorMessage { get; set; }
}
- ///
- /// Slot number.
- ///
- /// Result containing success status and detected port name.
- ///
- ///
- ///
- /// var api = new Api2();
- /// var result = api.DetectStreamingPort(3);
- ///
- /// if (result.Success)
- /// {
- /// Console.WriteLine($"Port: {result.PortName}");
- /// }
- /// else
- /// {
- /// Console.WriteLine("Streaming detection failed");
- /// }
- ///
- ///
+ public Task DetectStreamingPortAsync(
+ int slot,
+ CancellationToken token = default(CancellationToken))
+ {
+ return GetWorker(slot).RunAsync(() => DetectStreamingPort(slot), token);
+ }
+
public PortDetectionResult DetectStreamingPort(int slot)
{
- if (slot <= 0)
- throw new ArgumentOutOfRangeException(nameof(slot));
+ const string operation = nameof(DetectStreamingPort);
- using (var mb = new MeterBatch())
- using (var meter = new GenesisMeter())
- {
- //meter.SetupFromConfigFile(slot, false);
- mb.AddMeter(meter);
-
- var rawData = new ConcurrentBag();
-
- meter.StreamingPort.OnRawRecordReceived += (o, rawMsg) =>
- {
- var data = (string)rawMsg.GetData();
- rawData.Add(data);
- };
-
- Thread.Sleep(500);
-
- var success = rawData.Any();
- var portName = meter.StreamingPort.GetPortName();
-
- return new PortDetectionResult
- {
- Success = success,
- Slot = slot,
- PortName = portName,
- ErrorMessage = success ? null : "No streaming data received."
- };
- }
- }
-
- ///
- /// Detects the request port by attempting to read the PCB ID.
- ///
- /// Slot number.
- ///
- /// Result containing success status, port name, and PCB ID if successful.
- ///
- ///
- ///
- /// var api = new Api2();
- /// var result = api.DetectRequestPort(3);
- ///
- /// if (result.Success)
- /// {
- /// Console.WriteLine($"PCB ID: {result.PcbId}");
- /// }
- /// else
- /// {
- /// Console.WriteLine("Detection failed");
- /// }
- ///
- ///
- public PortDetectionResult DetectRequestPort(int slot)
- {
- if (slot <= 0)
- throw new ArgumentOutOfRangeException(nameof(slot));
-
- using (var mb = new MeterBatch())
- using (var meter = new GenesisMeter())
- {
- //meter.SetupFromConfigFile(slot, false);
- mb.AddMeter(meter);
-
- meter.Logout();
-
- var pcbId = meter.GetPcbId();
- var success = !string.IsNullOrEmpty(pcbId);
- var portName = meter.RequestPort.GetPortName();
-
- return new PortDetectionResult
- {
- Success = success,
- Slot = slot,
- PortName = portName,
- PcbId = pcbId,
- ErrorMessage = success ? null : "PCB ID was empty."
- };
- }
- }
- #endregion
-
- #region API - PCB ID region
-
- ///
- /// Reads PCB ID for the specified slot.
- ///
- /// Slot number.
- /// PCB ID read from the meter.
- ///
- ///
- /// var api = new Api2();
- /// string pcbId = api.GetPcbId(3);
- /// Console.WriteLine(pcbId);
- ///
- ///
- public string GetPcbId(int slot)
- {
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot), "Slot number must be greater than zero.");
- using (var mb = new MeterBatch())
- using (var meter = new GenesisMeter())
+ try
{
- meter.SetupFromConfigFile(slot, false);
- mb.AddMeter(meter);
+ LogInfo(operation, $"Start. Slot={slot}");
- meter.Logout();
+ using (var mb = new MeterBatch())
+ using (var meter = new GenesisMeter())
+ {
+ mb.AddMeter(meter);
- return meter.GetPcbId();
+ var rawData = new ConcurrentBag();
+
+ 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 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 ?? ""}");
+
+ 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 Password and Login
- ///
- /// Sets meter password.
- ///
- public bool SetMeterPassword(string password)
- {
- EnsureConnected();
+ #region ================================== CONNECT ==================================
- if (string.IsNullOrWhiteSpace(password))
- throw new ArgumentException("Password cannot be null or empty.", nameof(password));
+ public Task 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
{
- // Replace this with the real Genesis API call if available.
- // Example:
- // return _currentGenesis.SetMeterPassword(password);
+ LogInfo(operation, $"Start. Slot={slot}");
- var result = WriteRegister("SECURITY_Password", password, true, true);
- return result.Success;
- }
- catch (Exception ex)
- {
- Logger.Value.Error(ex, "SetMeterPassword failed.");
- return false;
- }
- }
+ var meter = GetMeter(slot);
- ///
- /// Performs login using provided password.
- ///
- public bool Login(string password)
- {
- if (_currentGenesis == null)
- throw new InvalidOperationException("Genesis meter is not initialized. Call Connect first.");
+ _meterBatch.MetersLogin();
- if (string.IsNullOrWhiteSpace(password))
- throw new ArgumentException("Password cannot be null or empty.", nameof(password));
+ if (!meter.IsLoggedOn)
+ {
+ LogInfo(operation, $"Failed. Slot={slot}, Reason=Login failed.");
- try
- {
- // IMPORTANT:
- // Replace with actual Genesis API method if available
+ return new GciPublicModels.GciConnectResult
+ {
+ Success = false,
+ SlotId = slot,
+ Message = "Login failed."
+ };
+ }
- // Variant A – direct login method (preferred)
- // return _currentGenesis.Login(password);
-
- // Variant B – if password must be set first
- // _currentGenesis.Password = password;
- // return _currentGenesis.Login();
-
- // TEMP fallback (if no direct method known)
- bool result = _currentGenesis.Login();
-
- if (!result)
- Logger.Value.Warn("Login failed.");
-
- return result;
- }
- catch (Exception ex)
- {
- Logger.Value.Error(ex, "Login failed.");
- return false;
- }
- }
- #endregion
-
- #region API - Connect
-
- public class InitResult
- {
- public bool Success { get; set; }
- public int Slot { get; set; }
- public string ErrorMessage { get; set; }
- public string InterfaceVersion { get; set; }
- public bool InterfaceSupportsFwVersion { get; set; }
- }
-
- ///
- /// Represents the result of a connect operation.
- ///
- public class ConnectResult
- {
- ///
- /// Indicates whether the connect operation was successful.
- ///
- public bool Success { get; set; }
-
- ///
- /// Slot number used for connect.
- ///
- public int Slot { get; set; }
-
- ///
- /// Connected PCB ID.
- ///
- public string PcbId { get; set; }
-
- ///
- /// Indicates whether the meter is logged on.
- ///
- public bool IsLoggedOn { get; set; }
-
- ///
- /// Firmware version reported by the meter.
- ///
- public string FwVersion { get; set; }
-
- ///
- /// Interface version from configuration.
- ///
- public string InterfaceVersion { get; set; }
-
- ///
- /// Indicates whether the loaded configuration supports the detected firmware version.
- ///
- public bool InterfaceSupportsFwVersion { get; set; }
-
- ///
- /// Registers available after successful connect.
- ///
- public List Registers { get; set; } = new List();
-
- ///
- /// Error message if connect failed.
- ///
- public string ErrorMessage { get; set; }
- }
-
- ///
- /// Represents one register returned after connect.
- ///
- 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 InitResult InitOneMeterFromExtern(Int32 slotNo, ConfigSource useConfigSource, PasswordSource usePasswordSource, PortConfig? requestPort, PortConfig? streamingPort)
- {
- try
- {
- if (slotNo <= 0)
- throw new ArgumentOutOfRangeException(nameof(slotNo), "Slot number must be greater than zero.");
-
- //_meterBatch.RemoveMeter(slotNo);
- _currentGenesis?.DisposeMeter();
-
- _currentGenesis = new GenesisMeter();
- _currentGenesis.UseOfflinePasswords = usePasswordSource == PasswordSource.OfflineFile;
- _currentGenesis.usePasswordSource = usePasswordSource;
- _currentGenesis.useConfigSource = useConfigSource;
-
- _currentGenesis.SetupFromExternConfig(
- slotNo,
- requestPort,
- streamingPort,
- true);
-
- _meterBatch.AddMeter(_currentGenesis);
-
- return new InitResult
+ var result = new GciPublicModels.GciConnectResult
{
Success = true,
- Slot = slotNo,
- InterfaceVersion = _currentGenesis.InterfaceInfo?.InterfaceVersion,
- InterfaceSupportsFwVersion = _currentGenesis.InterfaceSupportsFwVersion
+ SlotId = slot,
+ PcbId = meter.PcbId,
+ IsLoggedOn = true,
+ FwVersion = meter.FwVersion,
+ InterfaceVersion = meter.InterfaceInfo?.InterfaceVersion,
+ InterfaceSupportsFwVersion = meter.InterfaceSupportsFwVersion,
+ Registers = BuildRegisters(meter)
};
- }
- catch (Exception ex)
- {
- //_meterBatch.RemoveAllMeters();
- _currentGenesis?.DisposeMeter();
- _currentGenesis = null;
- return new InitResult
- {
- Success = false,
- Slot = slotNo,
- ErrorMessage = ex.Message
- };
- }
- }
-
- ///
- /// Connects to a Genesis meter for the specified slot.
- ///
- /// Slot number.
- /// Specifies whether offline passwords should be used.
- ///
- /// Connect operation result including PCB ID, firmware/configuration info, and register snapshots.
- ///
- ///
- ///
- /// var api = new GenesisAPI();
- /// var result = api.Connect(3, true);
- ///
- /// if (result.Success)
- /// {
- /// Console.WriteLine(result.PcbId);
- /// Console.WriteLine(result.InterfaceVersion);
- /// }
- /// else
- /// {
- /// Console.WriteLine(result.ErrorMessage);
- /// }
- ///
- ///
- public ConnectResult ConnectOneMeter(int slotNo)
- {
- try
- {
- // Validate input
- if (slotNo <= 0)
- throw new ArgumentOutOfRangeException(nameof(slotNo), "Slot number must be greater than zero.");
-
- // Find meter in batch by slot
- var meter = _meterBatch.ListOfMeters
- .OfType()
- .FirstOrDefault(m => m.Slot == slotNo);
-
- // Meter not initialized
- if (meter == null)
- {
- return new ConnectResult
- {
- Success = false,
- Slot = slotNo,
- ErrorMessage = $"Meter for slot {slotNo} not found in batch."
- };
- }
-
- // Set current working meter
- _currentGenesis = meter;
-
- // Perform login for meters in batch
- _meterBatch.MetersLogin();
-
- // Validate connection result
- if (!_currentGenesis.IsLoggedOn && string.IsNullOrEmpty(_currentGenesis.PcbId))
- {
- return new ConnectResult
- {
- Success = false,
- Slot = slotNo,
- IsLoggedOn = false,
- PcbId = _currentGenesis.PcbId,
- ErrorMessage = "Cannot read out PcbId. Access to Cordonel denied."
- };
- }
-
- // Build successful result
- var result = new ConnectResult
- {
- Success = _currentGenesis.IsLoggedOn,
- Slot = slotNo,
- PcbId = _currentGenesis.PcbId,
- IsLoggedOn = _currentGenesis.IsLoggedOn,
- FwVersion = _currentGenesis.FwVersion,
- InterfaceVersion = _currentGenesis.InterfaceInfo?.InterfaceVersion,
- InterfaceSupportsFwVersion = _currentGenesis.InterfaceSupportsFwVersion
- };
+ LogInfo(operation,
+ $"Success. Slot={slot}, PcbId={result.PcbId ?? ""}, " +
+ $"FwVersion={result.FwVersion ?? ""}, InterfaceVersion={result.InterfaceVersion ?? ""}, " +
+ $"Registers={result.Registers.Count}");
return result;
}
catch (Exception ex)
{
- // Return failure result on exception
- return new ConnectResult
+ LogError(operation, ex);
+
+ return new GciPublicModels.GciConnectResult
{
Success = false,
- Slot = slotNo,
- ErrorMessage = ex.Message
+ SlotId = slot,
+ Message = ex.Message
};
}
}
- ///
- /// Connects to a Genesis meter for the specified slot.
- ///
- /// Slot number.
- /// Specifies whether offline passwords should be used.
- ///
- /// Connect operation result including PCB ID, firmware/configuration info, and register snapshots.
- ///
- ///
- ///
- /// var api = new GenesisAPI();
- /// var result = api.Connect(3, true);
- ///
- /// if (result.Success)
- /// {
- /// Console.WriteLine(result.PcbId);
- /// Console.WriteLine(result.InterfaceVersion);
- /// }
- /// else
- /// {
- /// Console.WriteLine(result.ErrorMessage);
- /// }
- ///
- ///
- public ConnectResult ConnectAllMeters(int slotNo)
+ private List BuildRegisters(GenesisMeter meter)
{
- /*try
+ var list = new List();
+
+ foreach (var item in meter.GetRegistersDic())
{
- if (slotNo <= 0)
- throw new ArgumentOutOfRangeException(nameof(slotNo), "Slot number must be greater than zero.");
+ var from = item.Key.RegisterDetail.Version.First?.ToString() ?? "-";
+ var to = item.Key.RegisterDetail.Version.Last?.ToString() ?? "-";
- _currentGenesis?.DisposeMeter();
- _meterBatch.RemoveAllMeters();
- _currentGenesis = null;
-
- _currentGenesis = new GenesisMeter();
- _currentGenesis.UseOfflinePasswords = usePasswordSource == PasswordSource.OfflineFile;
- _currentGenesis.usePasswordSource = usePasswordSource;
- _currentGenesis.useConfigSource = useConfigSource;
- _currentGenesis.SetupFromConfigFile(slotNo);//...MF
- _currentGenesis.SetupFromExternConfig(slotNo);
- _meterBatch.AddMeter(_currentGenesis);
-
- _meterBatch.MetersLogin();
-
- if (!_currentGenesis.IsLoggedOn && string.IsNullOrEmpty(_currentGenesis.PcbId))
+ list.Add(new GciRegisterSnapshot
{
- return new ConnectResult
- {
- Success = false,
- Slot = slotNo,
- IsLoggedOn = false,
- PcbId = _currentGenesis.PcbId,
- ErrorMessage = "Cannot read out PcbId. Access to Cordonel denied."
- };
- }
-
- var result = new ConnectResult
- {
- Success = _currentGenesis.IsLoggedOn,
- Slot = slotNo,
- PcbId = _currentGenesis.PcbId,
- IsLoggedOn = _currentGenesis.IsLoggedOn,
- FwVersion = _currentGenesis.FwVersion,
- InterfaceVersion = _currentGenesis.InterfaceInfo?.InterfaceVersion,
- InterfaceSupportsFwVersion = _currentGenesis.InterfaceSupportsFwVersion
- };
-
- foreach (var item in _currentGenesis.GetRegistersDic())
- {
- var from = item.Key.RegisterDetail.Version.First.HasValue
- ? item.Key.RegisterDetail.Version.First.Value.ToString()
- : "-";
-
- var to = item.Key.RegisterDetail.Version.Last.HasValue
- ? item.Key.RegisterDetail.Version.Last.Value.ToString()
- : "-";
-
- result.Registers.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 result;
+ 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()
+ });
}
- catch (Exception ex)
- {
- _meterBatch.RemoveAllMeters();
- _currentGenesis?.DisposeMeter();
- return new ConnectResult
- {
- Success = false,
- Slot = slotNo,
- ErrorMessage = ex.Message
- };
- }*/
- return null;
+ return list;
}
#endregion
- #region Meter Registers
- ///
- /// Result of a register read operation.
- ///
+ #region ================================== DISCONNECT ==================================
+
+ public Task 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 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 ?? ""}");
+
+ 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
{
- ///
- /// Indicates whether the read operation was successful.
- ///
public bool Success { get; set; }
-
- ///
- /// Name of the register.
- ///
public string RegisterName { get; set; }
-
- ///
- /// Raw bytes returned from the device.
- ///
- public byte[] RawBytes { get; set; }
-
- ///
- /// Raw value formatted as hexadecimal string.
- ///
public string RawHex { get; set; }
-
- ///
- /// Converted value based on register data type (if possible).
- ///
- public object TypedValue { get; set; }
-
- ///
- /// String representation of the converted value.
- ///
- public string TypedValueText { get; set; }
-
- ///
- /// Data type of the register.
- ///
- public string DataType { get; set; }
-
- ///
- /// Error message if operation failed.
- ///
public string ErrorMessage { get; set; }
}
- ///
- /// Result of a register write operation.
- ///
- public class RegisterWriteResult
+ public Task ReadRegisterAsync(int slot, string name, CancellationToken token = default)
{
- ///
- /// Indicates whether the write operation was successful.
- ///
- public bool Success { get; set; }
-
- ///
- /// Name of the register.
- ///
- public string RegisterName { get; set; }
-
- ///
- /// Value that was written to the register.
- ///
- public object WrittenValue { get; set; }
-
- ///
- /// Indicates whether configuration was stored to the device.
- ///
- public bool StoreToDevice { get; set; }
-
- ///
- /// Indicates whether system state refresh was triggered.
- ///
- public bool RefreshSystemState { get; set; }
-
- ///
- /// Error message if operation failed.
- ///
- public string ErrorMessage { get; set; }
+ return GetWorker(slot).RunAsync(() => ReadRegister(slot, name), token);
}
- //Helper methods
-
- ///
- /// Ensures that the meter is connected and logged on.
- ///
- private void EnsureConnected()
+ public RegisterReadResult ReadRegister(int slot, string name)
{
- if (_currentGenesis == null)
- throw new InvalidOperationException("Genesis meter is not initialized. Call Connect first.");
+ const string operation = nameof(ReadRegister);
- if (!_currentGenesis.IsLoggedOn)
- throw new InvalidOperationException("Genesis meter is not logged on. Call Connect first.");
- }
-
- ///
- /// Finds register definition by name.
- ///
- private RegisterDefinition FindRegisterDefinition(string registerName)
- {
- if (string.IsNullOrWhiteSpace(registerName))
- throw new ArgumentException("Register name cannot be null or empty.", nameof(registerName));
-
- var match = _currentGenesis
- .GetRegistersDic()
- .Keys
- .FirstOrDefault(r => string.Equals(r.GetIdent(), registerName, StringComparison.OrdinalIgnoreCase));
-
- if (match == null)
- throw new KeyNotFoundException($"Register '{registerName}' was not found.");
-
- return match;
- }
-
- ///
- /// Converts byte array to hex string.
- ///
- private string ToHex(byte[] data)
- {
- if (data == null || data.Length == 0)
- return string.Empty;
-
- return BitConverter.ToString(data).Replace("-", " ");
- }
-
- //Read register
- ///
- /// Reads register value by register name.
- ///
- public RegisterReadResult ReadRegister(string registerName)
- {
try
{
- EnsureConnected();
+ LogInfo(operation, $"Start. Slot={slot}, Register={name}");
- var register = FindRegisterDefinition(registerName);
- var raw = _currentGenesis.ReadRegister(registerName);
+ var meter = GetMeter(slot);
+ EnsureConnected(meter);
- object typedValue = null;
- string typedValueText = null;
+ var raw = meter.ReadRegister(name);
- try
- {
- typedValue = ConvertRegisterValue(register, raw);
- typedValueText = typedValue?.ToString();
- }
- catch
- {
- // Ignore conversion errors, raw value is still valid
- }
-
- return new RegisterReadResult
+ var result = new RegisterReadResult
{
Success = true,
- RegisterName = registerName,
- RawBytes = raw,
- RawHex = ToHex(raw),
- TypedValue = typedValue,
- TypedValueText = typedValueText,
- DataType = register.DataType?.Name
+ RegisterName = name,
+ RawHex = ToHex(raw)
};
+
+ LogInfo(operation, $"Success. Slot={slot}, Register={name}, RawHex={result.RawHex}");
+
+ return result;
}
catch (Exception ex)
{
- Logger.Value.Error(ex, $"ReadRegister failed for '{registerName}'.");
+ LogError(operation, ex);
return new RegisterReadResult
{
Success = false,
- RegisterName = registerName,
+ RegisterName = name,
ErrorMessage = ex.Message
};
}
}
- //Typed conversion
- ///
- /// Converts raw register value to a typed value based on register definition.
- ///
- private object ConvertRegisterValue(RegisterDefinition register, byte[] raw)
+ #endregion
+
+ #region ================================== REGISTER WRITE ==================================
+
+ public class RegisterWriteResult
{
- var typeName = register.DataType?.Name;
-
- switch (typeName)
- {
- case "Boolean":
- return RegisterConverter.ByteArrayToValue(raw);
-
- case "Byte":
- return RegisterConverter.ByteArrayToValue(raw);
-
- case "Int32":
- return RegisterConverter.ByteArrayToValue(raw);
-
- case "UInt32":
- return RegisterConverter.ByteArrayToValue(raw);
-
- case "Double":
- return RegisterConverter.ByteArrayToValue(raw);
-
- case "Single":
- return RegisterConverter.ByteArrayToValue(raw);
-
- case "String":
- return Encoding.ASCII.GetString(raw).TrimEnd('\0');
-
- default:
- return ToHex(raw);
- }
+ 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; }
}
- //Generic login
-
- ///
- /// Reads register and converts it directly to specified type.
- ///
- public T ReadRegisterValue(string registerName)
+ public Task WriteRegisterAsync(
+ int slot,
+ string registerName,
+ object value,
+ bool storeToDevice = false,
+ bool refreshSystemState = false,
+ CancellationToken token = default(CancellationToken))
{
- EnsureConnected();
-
- var raw = _currentGenesis.ReadRegister(registerName);
- return RegisterConverter.ByteArrayToValue(raw);
+ return GetWorker(slot).RunAsync(
+ () => WriteRegister(slot, registerName, value, storeToDevice, refreshSystemState),
+ token,
+ "WriteRegister");
}
- //Write register
- ///
- /// Writes value to register.
- ///
public RegisterWriteResult WriteRegister(
+ int slot,
string registerName,
object value,
bool storeToDevice = false,
bool refreshSystemState = false)
{
+ const string operation = nameof(WriteRegister);
+
try
{
- EnsureConnected();
+ LogInfo(operation,
+ $"Start. Slot={slot}, Register={registerName}, Value={value}, " +
+ $"StoreToDevice={storeToDevice}, RefreshSystemState={refreshSystemState}");
- bool writeOk = _currentGenesis.WriteRegister(registerName, value);
+ 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,
@@ -931,13 +778,17 @@ namespace GenesisCordonelInterface.API
if (storeToDevice)
{
- if (!_currentGenesis.StoreAllConfigurations())
+ 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."
};
}
@@ -945,18 +796,24 @@ namespace GenesisCordonelInterface.API
if (refreshSystemState)
{
- if (!_currentGenesis.WriteRegister("SENSUSRADIO_SYSTEMSTATE", 0xFF, true, false))
+ 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,
@@ -968,88 +825,134 @@ namespace GenesisCordonelInterface.API
}
catch (Exception ex)
{
- Logger.Value.Error(ex, $"WriteRegister failed for '{registerName}'.");
+ LogError(operation, ex);
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
+ StoreToDevice = storeToDevice,
+ RefreshSystemState = refreshSystemState,
ErrorMessage = ex.Message
};
}
}
- //Bulk operations
+ #endregion
- ///
- /// Reads multiple registers.
- ///
- public List ReadRegisters(IEnumerable registerNames)
+ #region ================================== PASSWORD ==================================
+
+ public Task SetMeterPasswordAsync(
+ int slot,
+ string password,
+ CancellationToken token = default(CancellationToken))
{
- var result = new List();
-
- foreach (var name in registerNames)
- {
- result.Add(ReadRegister(name));
- }
-
- return result;
+ return GetWorker(slot).RunAsync(
+ () => SetMeterPassword(slot, password),
+ token,
+ "SetMeterPassword");
}
- ///
- /// Writes multiple registers.
- ///
- public List WriteRegisters(
- Dictionary registerValues,
- bool storeToDevice = false,
- bool refreshSystemState = false)
+ public bool SetMeterPassword(int slot, string password)
{
- var results = new List();
+ const string operation = nameof(SetMeterPassword);
- int index = 0;
- int total = registerValues.Count;
+ if (string.IsNullOrWhiteSpace(password))
+ throw new ArgumentException("Password cannot be empty.", nameof(password));
- foreach (var pair in registerValues)
- {
- bool doStore = storeToDevice && index == total - 1;
- bool doRefresh = refreshSystemState && index == total - 1;
+ LogInfo(operation, $"Start. Slot={slot}");
- results.Add(WriteRegister(pair.Key, pair.Value, doStore, doRefresh));
- index++;
- }
+ var result = WriteRegister(
+ slot,
+ "SECURITY_Password",
+ password,
+ true,
+ true);
- return results;
+ LogInfo(operation, $"Finish. Slot={slot}, Success={result.Success}");
+
+ return result.Success;
}
- //Disconnect
+ #endregion
- ///
- /// Disconnects from the meter and releases resources.
- ///
- public void Disconnect()
+ #region ================================== METER BATCH SETUP ==================================
+ // ----------------------------------------------------
+
+ public void ReloadSlotSetup()
{
+ const string operation = nameof(ReloadSlotSetup);
+
try
{
- if (_currentGenesis != null && _currentGenesis.IsLoggedOn)
- {
- _currentGenesis.Logout();
- }
+ 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)
{
- Logger.Value.Error(ex, "Disconnect failed.");
- }
- finally
- {
- _meterBatch.RemoveAllMeters();
- _currentGenesis?.DisposeMeter();
- _currentGenesis = null;
- _currentPcbId = string.Empty;
+ LogError(operation, ex);
+ throw;
}
}
- #endregion
+ public void SaveSlotSetup(List 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
}
-}
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs
index a1d948462..e29aa1e92 100644
--- a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs
+++ b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
-using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
-using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
+using System.IO.Ports;
+using System.Threading;
+using System.Threading.Tasks;
namespace GenesisCordonelInterface.API
{
@@ -14,111 +14,346 @@ namespace GenesisCordonelInterface.API
{
private readonly InterfaceGCIToLaatzen _innerMeterAPI;
- ///
- /// Initializes a new instance of the class.
- ///
+ public event Action> MeterBatchStatusChanged;
+
public InterfaceOutsideToGCI()
{
_innerMeterAPI = new InterfaceGCIToLaatzen();
}
- ///
- /// Gets a value indicating whether the meter is currently connected and logged on.
- ///
- public bool IsConnected
+ #region ================================== PORT DETECTION ==================================
+
+ public InterfaceGCIToLaatzen.PortDetectionResult DetectStreamingPort(int slot)
{
- get
- {
- return _innerMeterAPI.IsConnected;
- }
+ var result = _innerMeterAPI.DetectStreamingPort(slot);
+ RaiseMeterBatchStatusChanged();
+ return result;
}
- ///
- /// Connects to the meter on the specified slot.
- ///
- /// Slot number.
- /// Specifies whether offline passwords should be used.
- /// Connect operation result.
- public InterfaceGCIToLaatzen.InitResult InitOneMeterFromExtern(Int32 slotNo, ConfigSource useConfigSource, PasswordSource usePasswordSource, PortConfig? requestPort, PortConfig? streamingPort)
+ public async Task DetectStreamingPortAsync(
+ int slot,
+ CancellationToken token = default(CancellationToken))
{
- return _innerMeterAPI.InitOneMeterFromExtern(slotNo, useConfigSource, usePasswordSource, requestPort, streamingPort);
+ var result = await _innerMeterAPI.DetectStreamingPortAsync(slot, token);
+ RaiseMeterBatchStatusChanged();
+ return result;
}
- ///
- /// Connects to the meter on the specified slot.
- ///
- /// Slot number.
- /// Specifies whether offline passwords should be used.
- /// Connect operation result.
- public InterfaceGCIToLaatzen.ConnectResult ConnectOneMeter(int slotNo)
+ public InterfaceGCIToLaatzen.PortDetectionResult DetectRequestPort(int slot)
{
- return _innerMeterAPI.ConnectOneMeter(slotNo);
+ var result = _innerMeterAPI.DetectRequestPort(slot);
+ RaiseMeterBatchStatusChanged();
+ return result;
}
- ///
- /// Connects to the meter on the specified slot.
- ///
- /// Slot number.
- /// Specifies whether offline passwords should be used.
- /// Connect operation result.
- public InterfaceGCIToLaatzen.ConnectResult ConnectAllMeters(int slotNo)
+ public async Task DetectRequestPortAsync(
+ int slot,
+ CancellationToken token = default(CancellationToken))
{
- return _innerMeterAPI.ConnectAllMeters(slotNo);
+ var result = await _innerMeterAPI.DetectRequestPortAsync(slot, token);
+ RaiseMeterBatchStatusChanged();
+ return result;
}
- ///
- /// Disconnects from the currently connected meter.
- ///
- public void Disconnect()
+ #endregion
+
+ #region ================================== INIT ==================================
+
+ public async Task InitSlotAsync(GciPublicModels.GciInitSlotRequest request, CancellationToken token = default)
{
- _innerMeterAPI.Disconnect();
+ if (request == null)
+ throw new ArgumentNullException(nameof(request));
+
+ var result = await _innerMeterAPI.InitOneMeterFromExternAsync(
+ request.SlotId,
+ GciPublicModels.MapConfigSource(request.ConfigSource),
+ GciPublicModels.MapPasswordSource(request.PasswordSource),
+ GciPublicModels.MapPort(request.RequestPort),
+ GciPublicModels.MapPort(request.StreamingPort),
+ token);
+
+ RaiseMeterBatchStatusChanged();
+
+ return result;
}
- ///
- /// Reads PCB ID from the specified slot.
- ///
- /// Slot number.
- /// PCB ID string.
- public string GetPcbId(int slot)
+ public async Task GetSlotAsync(
+ int slotId,
+ CancellationToken token = default)
{
- return _innerMeterAPI.GetPcbId(slot);
+ if (slotId <= 0)
+ throw new ArgumentException("Invalid slot id.");
+
+ var result = await _innerMeterAPI.GetOneMeterInfo(slotId, token);
+
+ return result;
}
- ///
- /// Reads a register by name.
- ///
- /// Register name.
- /// Register read result.
- public InterfaceGCIToLaatzen.RegisterReadResult ReadRegister(string registerName)
+ public async Task CleanSlotsAsync(
+ CancellationToken token = default)
{
- return _innerMeterAPI.ReadRegister(registerName);
+ var result = await _innerMeterAPI.CleanSlotsAsync(token);
+
+ RaiseMeterBatchStatusChanged();
+
+ return result;
+ }
+ #endregion
+
+ #region ================================== CONNECTION ==================================
+
+ public async Task ConnectOneSlotAsync(
+ int slot,
+ CancellationToken token = default)
+ {
+ GciPublicModels.GciConnectResult result = await _innerMeterAPI.ConnectOneSlotAsync(slot, token);
+
+ RaiseMeterBatchStatusChanged();
+
+ return result;
}
- ///
- /// Writes a value to a register.
- ///
- /// Register name.
- /// Value to write.
- /// Specifies whether configuration should be stored after write.
- /// Specifies whether system state refresh should be triggered after write.
- /// Register write result.
- public InterfaceGCIToLaatzen.RegisterWriteResult WriteRegister(
+ public async Task DisconnectAsync(
+ int slot,
+ CancellationToken token = default)
+ {
+ if (slot <= 0)
+ throw new ArgumentException("Invalid slot id.");
+
+ var result = await _innerMeterAPI.DisconnectAsync(slot, token);
+
+ RaiseMeterBatchStatusChanged();
+
+ return result;
+ }
+ #endregion
+
+ #region ================================== PCB ==================================
+ public async Task GetPcbIdAsync(
+ int slot,
+ CancellationToken token = default)
+ {
+ if (slot <= 0)
+ throw new ArgumentException("Invalid slot id.");
+
+ var result = await _innerMeterAPI.GetPcbIdAsync(slot, token);
+
+ return result;
+ }
+ #endregion
+
+ #region ================================== READ ==================================
+ // ----------------------------------------------------
+
+ public InterfaceGCIToLaatzen.RegisterReadResult ReadRegister(
+ int slot,
+ string registerName)
+ {
+ return _innerMeterAPI.ReadRegister(slot, registerName);
+ }
+
+ public Task ReadRegisterAsync(
+ int slot,
+ string registerName,
+ CancellationToken token = default(CancellationToken))
+ {
+ return _innerMeterAPI.ReadRegisterAsync(slot, registerName, token);
+ }
+
+ // ----------------------------------------------------
+ #endregion
+
+ #region ================================== WRITE ==================================
+ // ----------------------------------------------------
+
+ public async Task WriteRegisterAsync(
+ int slot,
string registerName,
object value,
bool storeToDevice = false,
bool refreshSystemState = false)
{
- return _innerMeterAPI.WriteRegister(registerName, value, storeToDevice, refreshSystemState);
+ var result = await _innerMeterAPI.WriteRegisterAsync(
+ slot,
+ registerName,
+ value,
+ storeToDevice,
+ refreshSystemState);
+
+ RaiseMeterBatchStatusChanged();
+ return result;
}
- ///
- /// Sets meter password.
- ///
- /// Password value.
- /// True if operation succeeded; otherwise false.
- public bool SetMeterPassword(string password)
+ public InterfaceGCIToLaatzen.RegisterWriteResult WriteRegister(
+ int slot,
+ string registerName,
+ object value,
+ bool storeToDevice = false,
+ bool refreshSystemState = false)
{
- return _innerMeterAPI.SetMeterPassword(password);
+ var result = _innerMeterAPI.WriteRegister(
+ slot,
+ registerName,
+ value,
+ storeToDevice,
+ refreshSystemState);
+
+ RaiseMeterBatchStatusChanged();
+ return result;
}
+
+ public async Task SetMeterPasswordAsync(int slot, string password)
+ {
+ var result = await _innerMeterAPI.SetMeterPasswordAsync(slot, password);
+ RaiseMeterBatchStatusChanged();
+ return result;
+ }
+
+ public bool SetMeterPassword(int slot, string password)
+ {
+ var result = _innerMeterAPI.SetMeterPassword(slot, password);
+ RaiseMeterBatchStatusChanged();
+ return result;
+ }
+
+ // ----------------------------------------------------
+ #endregion
+
+ #region ================================== DEBUG STATUS ==================================
+ // ----------------------------------------------------
+
+ public List GetWorkerDebugStatuses()
+ {
+ return _innerMeterAPI.GetWorkerDebugStatuses();
+ }
+
+ public List GetMeterBatchDebugStatuses()
+ {
+ return _innerMeterAPI.GetMeterBatchDebugStatuses();
+ }
+
+ public void RaiseMeterBatchStatusChanged()
+ {
+ var statuses = GetMeterBatchDebugStatuses();
+
+ var handler = MeterBatchStatusChanged;
+ if (handler != null)
+ handler(statuses);
+ }
+
+ // ----------------------------------------------------
+ #endregion
+
+ #region ================================== SLOT SELECTION ==================================
+ // ----------------------------------------------------
+
+ public void SetSlotSelected(int slot, bool selected)
+ {
+ _innerMeterAPI.SetSlotSelected(slot, selected);
+ RaiseMeterBatchStatusChanged();
+ }
+
+ public bool IsSlotSelected(int slot)
+ {
+ return _innerMeterAPI.IsSlotSelected(slot);
+ }
+
+ public List GetSelectedSlots()
+ {
+ return _innerMeterAPI.GetSelectedSlots();
+ }
+
+ // ----------------------------------------------------
+ #endregion
+
+ #region ================================== SLOT PORT CONFIG ==================================
+ // ----------------------------------------------------
+
+ /*public void SetSlotRequestPort(int slot, string portName)
+ {
+ lock (_portLock)
+ {
+ if (string.IsNullOrWhiteSpace(portName))
+ {
+ _requestPorts.Remove(slot);
+ }
+ else
+ {
+ _requestPorts[slot] = new GciPortConfig
+ {
+ PortName = portName,
+ Type = "Serial"
+ };
+ }
+ }
+
+ RaiseMeterBatchStatusChanged();
+ }
+
+ public void SetSlotStreamingPort(int slot, string portName)
+ {
+ lock (_portLock)
+ {
+ if (string.IsNullOrWhiteSpace(portName))
+ {
+ _streamingPorts.Remove(slot);
+ }
+ else
+ {
+ _streamingPorts[slot] = new GciPortConfig
+ {
+ PortName = portName,
+ Type = "Serial"
+ };
+ }
+ }
+
+ RaiseMeterBatchStatusChanged();
+ }
+
+ public GciPortConfig? GetSlotRequestPort(int slot)
+ {
+ lock (_portLock)
+ {
+ GciPortConfig port;
+ if (_requestPorts.TryGetValue(slot, out port))
+ return port;
+
+ return null;
+ }
+ }
+
+ public GciPortConfig? GetSlotStreamingPort(int slot)
+ {
+ lock (_portLock)
+ {
+ GciPortConfig port;
+ if (_streamingPorts.TryGetValue(slot, out port))
+ return port;
+
+ return null;
+ }
+ }*/
+
+ // ----------------------------------------------------
+ #endregion
+
+ #region ================================== METER BATCH SETUP ==================================
+ // ----------------------------------------------------
+
+ public void ReloadSlotSetup()
+ {
+ _innerMeterAPI.ReloadSlotSetup();
+ RaiseMeterBatchStatusChanged();
+ }
+
+ public void SaveSlotSetup(List data)
+ {
+ _innerMeterAPI.SaveSlotSetup(data);
+ RaiseMeterBatchStatusChanged();
+ }
+
+ // ----------------------------------------------------
+ #endregion
}
}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/Threading/ApiWorker/ApiWorker.cs b/GenesisCordonelInterface/Core/Threading/ApiWorker/ApiWorker.cs
new file mode 100644
index 000000000..6d6ba7c31
--- /dev/null
+++ b/GenesisCordonelInterface/Core/Threading/ApiWorker/ApiWorker.cs
@@ -0,0 +1,314 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GenesisCordonelInterface.Core.Threading
+{
+ /*
+ ApiWorker – per-slot sequential execution worker
+
+ This class provides a lightweight background worker that executes actions
+ sequentially on a dedicated thread.
+
+ PRIMARY PURPOSE
+ ---------------
+ ApiWorker is designed to safely execute hardware-related operations
+ (e.g. meter communication) without blocking the UI thread and without
+ allowing concurrent access to the same device.
+
+ Each ApiWorker instance typically represents:
+ 1 worker = 1 slot = 1 meter = 1 communication channel
+
+ KEY PROPERTIES
+ --------------
+ - Single dedicated background thread
+ - FIFO queue (first-in, first-out)
+ - Sequential execution (NO parallelism inside one worker)
+ - Thread-safe enqueueing
+ - Task-based async interface for callers
+
+ WHY THIS IS IMPORTANT
+ --------------------
+ Hardware communication (serial ports, meters, etc.) is usually NOT thread-safe.
+ If multiple commands are executed in parallel, communication may break or corrupt data.
+
+ ApiWorker guarantees:
+ - operations are executed one-by-one
+ - order is preserved
+ - no race conditions on the device
+
+ HIGH-LEVEL FLOW
+ ---------------
+ Caller (UI/API)
+ |
+ v
+ RunAsync(...)
+ |
+ v
+ TaskCompletionSource created
+ |
+ v
+ Action wrapped into queue item
+ |
+ v
+ Added to BlockingCollection queue
+ |
+ v
+ Worker thread consumes queue
+ |
+ v
+ Action executed (blocking HW call)
+ |
+ v
+ Result propagated via TaskCompletionSource
+ |
+ v
+ Caller receives result via await
+
+ GRAPH
+ -----
+ Caller thread (UI)
+ |
+ v
+ RunAsync()
+ |
+ v
+ Queue (BlockingCollection)
+ |
+ v
+ -----------------------------
+ | Worker Thread (background)|
+ | while(queue) |
+ | Execute Action |
+ -----------------------------
+ |
+ v
+ Task result (await)
+
+ THREADING MODEL
+ ---------------
+ - Producer/Consumer pattern
+ - Producer: any thread calling RunAsync
+ - Consumer: single worker thread
+ - Synchronization handled by BlockingCollection
+
+ MAIN COMPONENTS
+ ---------------
+ 1. BlockingCollection queue
+ - thread-safe queue
+ - stores work items
+ - supports blocking consumption
+
+ 2. Dedicated Thread
+ - runs WorkerLoop()
+ - continuously processes queue
+
+ 3. TaskCompletionSource
+ - bridges sync execution → async API
+ - allows caller to await result
+
+ METHODS
+ -------
+
+ RunAsync(Func)
+ --------------------
+ - Enqueues a function returning a value
+ - Wraps it into Action
+ - Executes on worker thread
+ - Returns Task to caller
+
+ RunAsync(Action)
+ ----------------
+ - Convenience overload for void methods
+ - Internally wraps into Func