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 + + WorkerLoop() + ------------ + - Infinite loop consuming queue + - Executes actions one-by-one + - Stops when queue is completed + + Dispose() + --------- + - Stops accepting new items + - Cleans up queue + - Does NOT forcibly stop running task + + CANCELLATION + ------------ + - CancellationToken is checked BEFORE execution + - If cancelled → Task is cancelled + - Does NOT interrupt running operation + + IMPORTANT LIMITATIONS + -------------------- + - No parallel execution inside one worker (by design) + - Long-running action blocks worker thread + - No built-in timeout handling + - Dispose does not abort running work + + WHEN TO USE + ----------- + ✔ Per-device communication (serial, TCP, HW) + ✔ Ordered execution required + ✔ UI must stay responsive + + WHEN NOT TO USE + --------------- + ✘ CPU parallel processing (use Task.Run / Parallel) + ✘ High-throughput parallel workloads + ✘ Fire-and-forget background tasks + + SUMMARY + ------- + ApiWorker is a simple, robust solution for: + "Execute commands sequentially per resource, asynchronously from UI" + + It is a perfect fit for: + - hardware interfaces + - device drivers + - IO-bound serialized workflows + */ + + public sealed class ApiWorker : IDisposable + { + /// + /// Thread-safe FIFO queue holding work items. + /// + private readonly BlockingCollection queue = new BlockingCollection(); + + /// + /// Dedicated worker thread processing the queue. + /// + private readonly Thread thread; + + /// + /// Indicates whether this worker has been disposed. + /// + private bool disposed; + + public string Name { get; private set; } + public int QueueLength { get { return queue.Count; } } + public bool IsBusy { get; private set; } + public string CurrentOperation { get; private set; } + public string LastError { get; private set; } + public DateTime LastActivity { get; private set; } + + /// + /// Creates a new ApiWorker with its own background thread. + /// + public ApiWorker(string name) + { + Name = name; + LastActivity = DateTime.Now; + + thread = new Thread(WorkerLoop) + { + IsBackground = true, + Name = name + }; + + thread.Start(); + } + + /// + /// Enqueues a function returning a value for sequential execution. + /// + public Task RunAsync( + Func action, + CancellationToken token = default(CancellationToken), + string operationName = null) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + if (disposed) + throw new ObjectDisposedException(nameof(ApiWorker)); + + var tcs = new TaskCompletionSource(); + + queue.Add(() => + { + if (token.IsCancellationRequested) + { + tcs.TrySetCanceled(); + return; + } + + try + { + IsBusy = true; + CurrentOperation = operationName ?? action.Method.Name; + LastActivity = DateTime.Now; + LastError = null; + + var result = action(); + tcs.TrySetResult(result); + } + catch (Exception ex) + { + LastError = ex.Message; + tcs.TrySetException(ex); + } + finally + { + IsBusy = false; + CurrentOperation = null; + LastActivity = DateTime.Now; + } + }, token); + + return tcs.Task; + } + + public Task RunAsync( + Action action, + CancellationToken token = default(CancellationToken), + string operationName = null) + { + return RunAsync(() => + { + action(); + return null; + }, token, operationName); + } + + /// + /// Enqueues a void action for sequential execution. + /// + public Task RunAsync(Action action, CancellationToken token = default) + { + return RunAsync(() => + { + action(); + return null; + }, token); + } + + /// + /// Main worker loop processing queued actions. + /// + private void WorkerLoop() + { + foreach (var item in queue.GetConsumingEnumerable()) + { + item(); + } + } + + /// + /// Stops the worker and releases resources. + /// + public void Dispose() + { + if (disposed) + return; + + disposed = true; + + queue.CompleteAdding(); + queue.Dispose(); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/GenesisCordonelInterface.csproj b/GenesisCordonelInterface/GenesisCordonelInterface.csproj index 772a7dc8b..b9ba6997d 100644 --- a/GenesisCordonelInterface/GenesisCordonelInterface.csproj +++ b/GenesisCordonelInterface/GenesisCordonelInterface.csproj @@ -58,8 +58,26 @@ + + + + UserControl + + + MeterBatchConfigPanel.cs + + + UserControl + + + WorkerDebugPanel.cs + + + + + Form @@ -98,12 +116,31 @@ PreAdjustmentControl.cs + + UserControl + + + MainView.cs + Form FrmGCIAPI.cs + + UserControl + + + MeterInitView.cs + + + UserControl + + + MetersActionView.cs + + ResXFileCodeGenerator Resources.Designer.cs @@ -145,8 +182,7 @@ - - + @@ -221,5 +257,7 @@ Logging + + \ No newline at end of file diff --git a/GenesisCordonelInterface/RuntimePackage/Build/Copy.targets.xml b/GenesisCordonelInterface/RuntimePackage/Build/Copy.targets.xml new file mode 100644 index 000000000..d3373bc83 --- /dev/null +++ b/GenesisCordonelInterface/RuntimePackage/Build/Copy.targets.xml @@ -0,0 +1,37 @@ + + + + + + + + $(ProjectDir)RuntimePackage\Package\ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/Debug/MeterBatchConfigPanel.Designer.cs b/GenesisCordonelInterface/UI/Debug/MeterBatchConfigPanel.Designer.cs new file mode 100644 index 000000000..324df866e --- /dev/null +++ b/GenesisCordonelInterface/UI/Debug/MeterBatchConfigPanel.Designer.cs @@ -0,0 +1,57 @@ +namespace GenesisCordonelInterface.UI.Debug +{ + partial class MeterBatchConfigPanel + { + private System.ComponentModel.IContainer components = null; + private System.Windows.Forms.DataGridView grid; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (components != null) + components.Dispose(); + + if (api != null) + api.MeterBatchStatusChanged -= Api_MeterBatchStatusChanged; + } + + base.Dispose(disposing); + } + + #region Component Designer generated code + + private void InitializeComponent() + { + this.grid = new System.Windows.Forms.DataGridView(); + + ((System.ComponentModel.ISupportInitialize)(this.grid)).BeginInit(); + this.SuspendLayout(); + + // grid + this.grid.AllowUserToAddRows = false; + this.grid.AllowUserToDeleteRows = false; + this.grid.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; + this.grid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.grid.Dock = System.Windows.Forms.DockStyle.Fill; + this.grid.Name = "grid"; + this.grid.RowHeadersWidth = 30; + this.grid.TabIndex = 0; + + // events + this.grid.CellValueChanged += new System.Windows.Forms.DataGridViewCellEventHandler(this.grid_CellValueChanged); + this.grid.CurrentCellDirtyStateChanged += new System.EventHandler(this.grid_CurrentCellDirtyStateChanged); + this.grid.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.grid_DataError); + + // MeterBatchConfigPanel + this.Controls.Add(this.grid); + this.Name = "MeterBatchConfigPanel"; + this.Size = new System.Drawing.Size(600, 200); + + ((System.ComponentModel.ISupportInitialize)(this.grid)).EndInit(); + this.ResumeLayout(false); + } + + #endregion + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/Debug/MeterBatchConfigPanel.cs b/GenesisCordonelInterface/UI/Debug/MeterBatchConfigPanel.cs new file mode 100644 index 000000000..a877d4f41 --- /dev/null +++ b/GenesisCordonelInterface/UI/Debug/MeterBatchConfigPanel.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using GenesisCordonelInterface.API; + +namespace GenesisCordonelInterface.UI.Debug +{ + public partial class MeterBatchConfigPanel : UserControl + { + private readonly InterfaceOutsideToGCI api; + + private bool isRefreshing; + private List comPorts = new List(); + + public MeterBatchConfigPanel(InterfaceOutsideToGCI api) + { + this.api = api; + + InitializeComponent(); + + RefreshComPorts(); + InitializeGridColumns(); + + api.MeterBatchStatusChanged += Api_MeterBatchStatusChanged; + + UpdateGrid(api.GetMeterBatchDebugStatuses()); + } + + #region INIT + // ---------------------------------------------------- + + private void InitializeGridColumns() + { + grid.Columns.Clear(); + + grid.Columns.Add("Slot", "Slot"); + grid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Selected", HeaderText = "Selected" }); + grid.Columns.Add("PcbId", "PcbId"); + grid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "IsLoggedOn", HeaderText = "IsLoggedOn" }); + + grid.Columns.Add(CreateComPortColumn("RequestPort", "RequestPort")); + grid.Columns.Add(CreateComPortColumn("StreamingPort", "StreamingPort")); + + grid.Columns.Add(CreateButtonColumn("DetectRequest", "DetectRequest", "...")); + grid.Columns.Add(CreateButtonColumn("DetectStreaming", "DetectStreaming", "...")); + + grid.Columns.Add("FwVersion", "FwVersion"); + grid.Columns.Add("InterfaceVersion", "InterfaceVersion"); + + foreach (DataGridViewColumn col in grid.Columns) + { + col.ReadOnly = + col.Name != "Selected" && + col.Name != "RequestPort" && + col.Name != "StreamingPort" && + col.Name != "DetectRequest" && + col.Name != "DetectStreaming"; + } + + EnableDoubleBuffering(grid); + } + + private DataGridViewComboBoxColumn CreateComPortColumn(string name, string headerText) + { + return new DataGridViewComboBoxColumn + { + Name = name, + HeaderText = headerText, + DataSource = new List(comPorts), + FlatStyle = FlatStyle.Flat, + DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton + }; + } + + private DataGridViewButtonColumn CreateButtonColumn(string name, string headerText, string text) + { + return new DataGridViewButtonColumn + { + Name = name, + HeaderText = headerText, + Text = text, + UseColumnTextForButtonValue = true + }; + } + + private void RefreshComPorts() + { + comPorts = SerialPort.GetPortNames() + .OrderBy(NaturalComPortOrder) + .ToList(); + + if (!comPorts.Contains("")) + comPorts.Insert(0, ""); + } + + // ---------------------------------------------------- + #endregion + + #region REFRESH EVENT DRIVEN + // ---------------------------------------------------- + + private void Api_MeterBatchStatusChanged( + List data) + { + if (IsDisposed) + return; + + if (InvokeRequired) + { + BeginInvoke(new Action(() => UpdateGrid(data))); + return; + } + + UpdateGrid(data); + } + + private void UpdateGrid(List data) + { + isRefreshing = true; + + foreach (var meter in data) + { + EnsurePortValueExists(meter.RequestPort); + EnsurePortValueExists(meter.StreamingPort); + + var row = FindOrCreateRow(meter.Slot); + + Set(row, "Slot", meter.Slot); + Set(row, "Selected", meter.Selected); + Set(row, "PcbId", meter.PcbId); + Set(row, "IsLoggedOn", meter.IsLoggedOn); + Set(row, "RequestPort", meter.RequestPort); + Set(row, "StreamingPort", meter.StreamingPort); + Set(row, "FwVersion", meter.FwVersion); + Set(row, "InterfaceVersion", meter.InterfaceVersion); + } + + isRefreshing = false; + } + + // ---------------------------------------------------- + #endregion + + #region COM PORT HELPERS + // ---------------------------------------------------- + + private void UpdateComPortColumnItems(string columnName) + { + var col = grid.Columns[columnName] as DataGridViewComboBoxColumn; + if (col == null) + return; + + col.DataSource = null; + col.DataSource = new List(comPorts); + } + + private void EnsurePortValueExists(string port) + { + if (string.IsNullOrWhiteSpace(port)) + return; + + if (comPorts.Contains(port)) + return; + + comPorts.Add(port); + comPorts = comPorts.OrderBy(NaturalComPortOrder).ToList(); + + if (!comPorts.Contains("")) + comPorts.Insert(0, ""); + + UpdateComPortColumnItems("RequestPort"); + UpdateComPortColumnItems("StreamingPort"); + } + + private static int NaturalComPortOrder(string port) + { + if (string.IsNullOrWhiteSpace(port)) + return 0; + + string number = new string(port.Where(char.IsDigit).ToArray()); + + int parsed; + if (int.TryParse(number, out parsed)) + return parsed; + + return int.MaxValue; + } + + // ---------------------------------------------------- + #endregion + + #region GRID HELPERS + // ---------------------------------------------------- + + private DataGridViewRow FindOrCreateRow(int slot) + { + foreach (DataGridViewRow row in grid.Rows) + { + if (row.Cells["Slot"].Value != null && + Convert.ToInt32(row.Cells["Slot"].Value) == slot) + { + return row; + } + } + + int idx = grid.Rows.Add(); + var newRow = grid.Rows[idx]; + newRow.Cells["Slot"].Value = slot; + return newRow; + } + + private void Set(DataGridViewRow row, string col, object value) + { + if (!grid.Columns.Contains(col)) + return; + + if (value == null) + value = ""; + + var cell = row.Cells[col]; + + if (!Equals(cell.Value, value)) + cell.Value = value; + } + + // ---------------------------------------------------- + #endregion + + #region USER EDIT + // ---------------------------------------------------- + + private void grid_CurrentCellDirtyStateChanged(object sender, EventArgs e) + { + if (grid.IsCurrentCellDirty) + grid.CommitEdit(DataGridViewDataErrorContexts.Commit); + } + + private void grid_CellValueChanged(object sender, DataGridViewCellEventArgs e) + { + if (isRefreshing) + return; + + if (e.RowIndex < 0) + return; + + string columnName = grid.Columns[e.ColumnIndex].Name; + + if (columnName != "Selected" && + columnName != "RequestPort" && + columnName != "StreamingPort") + return; + + int slot = Convert.ToInt32(grid.Rows[e.RowIndex].Cells["Slot"].Value); + + if (columnName == "Selected") + { + bool selected = Convert.ToBoolean(grid.Rows[e.RowIndex].Cells["Selected"].Value); + api.SetSlotSelected(slot, selected); + return; + } + + string port = Convert.ToString(grid.Rows[e.RowIndex].Cells[columnName].Value); + + if (columnName == "RequestPort") + { + //api.SetSlotRequestPort(slot, port); + return; + } + + if (columnName == "StreamingPort") + { + //api.SetSlotStreamingPort(slot, port); + return; + } + } + + private async void grid_CellContentClick(object sender, DataGridViewCellEventArgs e) + { + if (e.RowIndex < 0) + return; + + string columnName = grid.Columns[e.ColumnIndex].Name; + + if (columnName != "DetectRequest" && + columnName != "DetectStreaming") + return; + + int slot = Convert.ToInt32(grid.Rows[e.RowIndex].Cells["Slot"].Value); + + if (columnName == "DetectRequest") + { + await DetectRequestPortAsync(slot); + return; + } + + if (columnName == "DetectStreaming") + { + await DetectStreamingPortAsync(slot); + return; + } + } + + private async Task DetectRequestPortAsync(int slot) + { + try + { + grid.Enabled = false; + await api.DetectRequestPortAsync(slot); + api.RaiseMeterBatchStatusChanged(); + } + finally + { + grid.Enabled = true; + } + } + + private async Task DetectStreamingPortAsync(int slot) + { + try + { + grid.Enabled = false; + await api.DetectStreamingPortAsync(slot); + api.RaiseMeterBatchStatusChanged(); + } + finally + { + grid.Enabled = true; + } + } + + private void grid_DataError(object sender, DataGridViewDataErrorEventArgs e) + { + e.ThrowException = false; + } + + // ---------------------------------------------------- + #endregion + + #region PERFORMANCE + // ---------------------------------------------------- + + private void EnableDoubleBuffering(DataGridView dgv) + { + typeof(DataGridView) + .GetProperty( + "DoubleBuffered", + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.NonPublic) + ?.SetValue(dgv, true, null); + } + + // ---------------------------------------------------- + #endregion + + public void AddEmptySlotRow() + { + int nextSlot = 1; + + var existing = grid.Rows + .Cast() + .Where(r => r.Cells["Slot"].Value != null) + .Select(r => Convert.ToInt32(r.Cells["Slot"].Value)) + .ToList(); + + if (existing.Count > 0) + nextSlot = existing.Max() + 1; + + int idx = grid.Rows.Add(); + var row = grid.Rows[idx]; + + row.Cells["Slot"].Value = nextSlot; + row.Cells["Selected"].Value = true; + row.Cells["RequestPort"].Value = ""; + row.Cells["StreamingPort"].Value = ""; + } + + public List GetGridData() + { + var list = new List(); + + foreach (DataGridViewRow row in grid.Rows) + { + if (row.Cells["Slot"].Value == null) + continue; + + list.Add(new InterfaceGCIToLaatzen.MeterBatchDebugStatus + { + Slot = Convert.ToInt32(row.Cells["Slot"].Value), + Selected = Convert.ToBoolean(row.Cells["Selected"].Value), + RequestPort = Convert.ToString(row.Cells["RequestPort"].Value), + StreamingPort = Convert.ToString(row.Cells["StreamingPort"].Value) + }); + } + + return list; + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/Debug/WorkerDebugPanel.Designer.cs b/GenesisCordonelInterface/UI/Debug/WorkerDebugPanel.Designer.cs new file mode 100644 index 000000000..44b445189 --- /dev/null +++ b/GenesisCordonelInterface/UI/Debug/WorkerDebugPanel.Designer.cs @@ -0,0 +1,37 @@ +namespace GenesisCordonelInterface.UI.Debug +{ + partial class WorkerDebugPanel + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + } + + #endregion + } +} diff --git a/GenesisCordonelInterface/UI/Debug/WorkerDebugPanel.cs b/GenesisCordonelInterface/UI/Debug/WorkerDebugPanel.cs new file mode 100644 index 000000000..3d313050d --- /dev/null +++ b/GenesisCordonelInterface/UI/Debug/WorkerDebugPanel.cs @@ -0,0 +1,51 @@ +using System; +using System.Windows.Forms; +using GenesisCordonelInterface.API; + +namespace GenesisCordonelInterface.UI.Debug +{ + public partial class WorkerDebugPanel : UserControl + { + private readonly InterfaceOutsideToGCI api; + private readonly Timer timer = new Timer(); + private readonly DataGridView grid = new DataGridView(); + + public WorkerDebugPanel(InterfaceOutsideToGCI api) + { + this.api = api; + + grid.Dock = DockStyle.Fill; + grid.ReadOnly = true; + grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; + + Controls.Add(grid); + + timer.Interval = 300; + timer.Tick += (s, e) => + { + grid.DataSource = null; + grid.DataSource = api.GetWorkerDebugStatuses(); + }; + + timer.Start(); + } + + private void InitializeLayout() + { + grid.Dock = DockStyle.Fill; + grid.ReadOnly = true; + grid.AllowUserToAddRows = false; + grid.AllowUserToDeleteRows = false; + grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; + + Controls.Add(grid); + Dock = DockStyle.Fill; + } + + private void Timer_Tick(object sender, EventArgs e) + { + grid.DataSource = null; + grid.DataSource = api.GetWorkerDebugStatuses(); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/Grid/MeterGridColumnConfig.cs b/GenesisCordonelInterface/UI/Grid/MeterGridColumnConfig.cs new file mode 100644 index 000000000..9b78a3dbf --- /dev/null +++ b/GenesisCordonelInterface/UI/Grid/MeterGridColumnConfig.cs @@ -0,0 +1,12 @@ +namespace GenesisCordonelInterface.UI.Grid +{ + public class MeterGridColumnConfig + { + public string Name { get; set; } + public string HeaderText { get; set; } + public bool Visible { get; set; } = true; + public int DisplayIndex { get; set; } + public int Width { get; set; } = 80; + public bool ReadOnly { get; set; } = false; + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/Grid/MeterGridConfigProvider.cs b/GenesisCordonelInterface/UI/Grid/MeterGridConfigProvider.cs new file mode 100644 index 000000000..587a29909 --- /dev/null +++ b/GenesisCordonelInterface/UI/Grid/MeterGridConfigProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace GenesisCordonelInterface.UI.Grid +{ + public static class MeterGridConfigProvider + { + public static List GetDefault() + { + return new List + { + new MeterGridColumnConfig { Name = "Slot", HeaderText = "Slot", DisplayIndex = 0, Width = 50, ReadOnly = true }, + new MeterGridColumnConfig { Name = "Selected", HeaderText = "Selected", DisplayIndex = 1, Width = 60 }, + new MeterGridColumnConfig { Name = "PcbId", HeaderText = "PcbId", DisplayIndex = 2, Width = 80 }, + new MeterGridColumnConfig { Name = "IsLoggedOn", HeaderText = "IsLoggedOn", DisplayIndex = 3, Width = 80 }, + new MeterGridColumnConfig { Name = "RequestPort", HeaderText = "RequestPort", DisplayIndex = 4, Width = 90 }, + new MeterGridColumnConfig { Name = "StreamingPort", HeaderText = "StreamingPort", DisplayIndex = 5, Width = 100 }, + new MeterGridColumnConfig { Name = "FwVersion", HeaderText = "FwVersion", DisplayIndex = 6, Width = 80 }, + new MeterGridColumnConfig { Name = "InterfaceVersion", HeaderText = "InterfaceVersion", DisplayIndex = 7, Width = 110 }, + }; + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/Grid/MeterGridManager.cs b/GenesisCordonelInterface/UI/Grid/MeterGridManager.cs new file mode 100644 index 000000000..cc7d651a9 --- /dev/null +++ b/GenesisCordonelInterface/UI/Grid/MeterGridManager.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; + +namespace GenesisCordonelInterface.UI.Grid +{ + public class MeterGridManager + { + private readonly DataGridView grid; + + public MeterGridManager(DataGridView grid) + { + this.grid = grid; + EnableDoubleBuffering(grid); + } + + public void Init(List columns) + { + grid.SuspendLayout(); + + grid.AutoGenerateColumns = false; + grid.Columns.Clear(); + + grid.AllowUserToAddRows = false; + grid.AllowUserToDeleteRows = false; + grid.RowHeadersVisible = true; + + foreach (var cfg in columns.OrderBy(c => c.DisplayIndex)) + { + DataGridViewColumn col; + + if (cfg.Name == "Selected" || cfg.Name == "IsLoggedOn") + col = new DataGridViewCheckBoxColumn(); + else + col = new DataGridViewTextBoxColumn(); + + col.Name = cfg.Name; + col.HeaderText = cfg.HeaderText; + col.Visible = cfg.Visible; + col.Width = cfg.Width; + col.ReadOnly = cfg.ReadOnly; + + grid.Columns.Add(col); + } + + grid.ResumeLayout(); + } + + public void Update(List meters) + { + grid.SuspendLayout(); + + foreach (var meter in meters) + { + var row = FindOrCreateRow(meter.Slot); + + SetCell(row, "Slot", meter.Slot); + SetCell(row, "Selected", meter.Selected); + SetCell(row, "PcbId", meter.PcbId); + SetCell(row, "IsLoggedOn", meter.IsLoggedOn); + SetCell(row, "RequestPort", meter.RequestPort); + SetCell(row, "StreamingPort", meter.StreamingPort); + SetCell(row, "FwVersion", meter.FwVersion); + SetCell(row, "InterfaceVersion", meter.InterfaceVersion); + } + + grid.ResumeLayout(); + } + + private DataGridViewRow FindOrCreateRow(int slot) + { + foreach (DataGridViewRow row in grid.Rows) + { + if (row.Cells["Slot"].Value != null && + Convert.ToInt32(row.Cells["Slot"].Value) == slot) + { + return row; + } + } + + int idx = grid.Rows.Add(); + var newRow = grid.Rows[idx]; + newRow.Cells["Slot"].Value = slot; + return newRow; + } + + private void SetCell(DataGridViewRow row, string colName, object value) + { + if (!grid.Columns.Contains(colName)) + return; + + var cell = row.Cells[colName]; + + if (!Equals(cell.Value, value)) + cell.Value = value; + } + + private void EnableDoubleBuffering(DataGridView dgv) + { + typeof(DataGridView) + .GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic) + ?.SetValue(dgv, true, null); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/Grid/MeterRowDto.cs b/GenesisCordonelInterface/UI/Grid/MeterRowDto.cs new file mode 100644 index 000000000..e767b9246 --- /dev/null +++ b/GenesisCordonelInterface/UI/Grid/MeterRowDto.cs @@ -0,0 +1,14 @@ +namespace GenesisCordonelInterface.UI.Grid +{ + public class MeterRowDto + { + 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; } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/LaatzenAPI_GenesisToolBox/FrmRegisterStore.cs b/GenesisCordonelInterface/UI/LaatzenAPI_GenesisToolBox/FrmRegisterStore.cs index 76e855ce1..c66250f39 100644 --- a/GenesisCordonelInterface/UI/LaatzenAPI_GenesisToolBox/FrmRegisterStore.cs +++ b/GenesisCordonelInterface/UI/LaatzenAPI_GenesisToolBox/FrmRegisterStore.cs @@ -492,7 +492,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox DisableAllButtons(); _dataTable.Rows.Clear(); - var result = await Task.Run(() => interfaceToLaatzen.ConnectOneMeter(slotNo)); + var result = await interfaceToLaatzen.ConnectOneSlotAsync(slotNo); if (result.Success) { @@ -571,7 +571,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox lblState.Text = $@"Not Connected to PcbId:{result.PcbId}"; registerGridView.Visible = false; - MessageBox.Show(result.ErrorMessage ?? "Connect failed.", @"FAILED", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show(result.Message ?? "Connect failed.", @"FAILED", MessageBoxButtons.OK, MessageBoxIcon.Error); } } catch (Exception ex) @@ -857,7 +857,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox ForeColor = Color.Black }; - SetProgress($"Read File {filename}"); + SetProgress($"Read FileConfig {filename}"); var text = File.ReadAllText(filename); var loadedRegStore = JsonConvert.DeserializeObject(text); @@ -941,7 +941,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox } else { - ((DataRow)rowItem)["RawValueFile"] = "Not in File"; + ((DataRow)rowItem)["RawValueFile"] = "Not in FileConfig"; } } } @@ -949,7 +949,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox private void ShowFile(String filename) { - SetProgress($"Read File {filename}"); + SetProgress($"Read FileConfig {filename}"); var text = File.ReadAllText(filename); var loadedRegStore = JsonConvert.DeserializeObject(text); @@ -971,7 +971,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox } else { - ((DataRow)rowItem)["RawValueFile"] = "Not in File"; + ((DataRow)rowItem)["RawValueFile"] = "Not in FileConfig"; } done += 1; @@ -981,8 +981,8 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox private void LoadFile(String filename) { - //SetProgreess($"Read File {filename}"); - //var text = File.ReadAllText(filename); + //SetProgreess($"Read FileConfig {filename}"); + //var text = FileConfig.ReadAllText(filename); //var loadedRegStore = Newtonsoft.Json.JsonConvert.DeserializeObject(text); //if (loadedRegStore.PcbId != _currentPcbId) //{ @@ -1015,7 +1015,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox //{ // if (saveFileDialog.ShowDialog() == DialogResult.OK) - // File.WriteAllText(saveFileDialog.FileName, text); + // FileConfig.WriteAllText(saveFileDialog.FileName, text); //})); @@ -1172,11 +1172,16 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox SetBusy(true, "GetPcbId"); - // as new meter object will be generated the data grid shows outdated data _dataTable.Rows.Clear(); DisableAllButtons(); - _currentPcbId = await Task.Run(() => interfaceToLaatzen.GetPcbId(slotNr)); + GciPublicModels.GciGetPcbIdResult result = + await interfaceToLaatzen.GetPcbIdAsync(slotNr); + + if (!result.Success) + throw new Exception(result.Message); + + _currentPcbId = result.PcbId; btnConnect.Enabled = true; @@ -2026,7 +2031,7 @@ namespace GenesisCordonelInterface.UI.LaatzenAPI_GenesisToolBox //_currentGenesis.WriteRegister("SENSUSRADIO_WakeupInterval", 0); //_currentGenesis.Logout(); - //File.AppendAllLines("RadioActivation.log", new[] { $"{freq};{_currentGenesis.PcbId};{RadioAdress};{DateTime.Now}" }); + //FileConfig.AppendAllLines("RadioActivation.log", new[] { $"{freq};{_currentGenesis.PcbId};{RadioAdress};{DateTime.Now}" }); if (0x02 == RegisterConverter.ByteArrayToValue( _currentGenesis.ReadRegister("SENSUSRADIO_SystemState"))) { diff --git a/GenesisCordonelInterface/UI/MainForm.Designer.cs b/GenesisCordonelInterface/UI/MainForm.Designer.cs index 1185f1038..e54ed11aa 100644 --- a/GenesisCordonelInterface/UI/MainForm.Designer.cs +++ b/GenesisCordonelInterface/UI/MainForm.Designer.cs @@ -11,20 +11,14 @@ private System.Windows.Forms.ToolStripMenuItem miClearLog; private System.Windows.Forms.ToolStripMenuItem miHelp; private System.Windows.Forms.ToolStripMenuItem miHelpAbout; - - private System.Windows.Forms.Panel pnlLeftMenu; - private System.Windows.Forms.Panel pnlMain; - private System.Windows.Forms.RichTextBox rtbMainLog; - + private System.Windows.Forms.Panel mainHostPanel; private System.Windows.Forms.StatusStrip statusStrip1; private System.Windows.Forms.ToolStripStatusLabel tslStatus; protected override void Dispose(bool disposing) { if (disposing && (components != null)) - { components.Dispose(); - } base.Dispose(disposing); } @@ -38,54 +32,39 @@ this.miClearLog = new System.Windows.Forms.ToolStripMenuItem(); this.miHelp = new System.Windows.Forms.ToolStripMenuItem(); this.miHelpAbout = new System.Windows.Forms.ToolStripMenuItem(); - this.pnlLeftMenu = new System.Windows.Forms.Panel(); - this.tabControl1 = new System.Windows.Forms.TabControl(); - this.tabPage1 = new System.Windows.Forms.TabPage(); - this.groupBox2 = new System.Windows.Forms.GroupBox(); - this.preadjustmentButton = new System.Windows.Forms.Button(); - this.groupBox1 = new System.Windows.Forms.GroupBox(); - this.btnPulseSetup = new System.Windows.Forms.Button(); - this.btnSetup = new System.Windows.Forms.Button(); - this.btnRegisterStore = new System.Windows.Forms.Button(); - this.tabPage2 = new System.Windows.Forms.TabPage(); - this.pnlMain = new System.Windows.Forms.Panel(); - this.rtbMainLog = new System.Windows.Forms.RichTextBox(); + this.mainHostPanel = new System.Windows.Forms.Panel(); this.statusStrip1 = new System.Windows.Forms.StatusStrip(); this.tslStatus = new System.Windows.Forms.ToolStripStatusLabel(); - this.groupBox3 = new System.Windows.Forms.GroupBox(); - this.button1 = new System.Windows.Forms.Button(); + this.menuStrip1.SuspendLayout(); - this.pnlLeftMenu.SuspendLayout(); - this.tabControl1.SuspendLayout(); - this.tabPage1.SuspendLayout(); - this.groupBox2.SuspendLayout(); - this.groupBox1.SuspendLayout(); - this.tabPage2.SuspendLayout(); - this.pnlMain.SuspendLayout(); this.statusStrip1.SuspendLayout(); - this.groupBox3.SuspendLayout(); this.SuspendLayout(); + // // menuStrip1 // this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.miFile, - this.miView, - this.miHelp}); + this.miFile, + this.miView, + this.miHelp + }); this.menuStrip1.Location = new System.Drawing.Point(0, 0); this.menuStrip1.Name = "menuStrip1"; this.menuStrip1.Padding = new System.Windows.Forms.Padding(5, 2, 0, 2); this.menuStrip1.Size = new System.Drawing.Size(1309, 24); this.menuStrip1.TabIndex = 0; this.menuStrip1.Text = "menuStrip1"; + // // miFile // this.miFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.miExit}); + this.miExit + }); this.miFile.Name = "miFile"; this.miFile.Size = new System.Drawing.Size(37, 20); - this.miFile.Text = "File"; + this.miFile.Text = "FileConfig"; + // // miExit // @@ -93,14 +72,17 @@ this.miExit.Size = new System.Drawing.Size(92, 22); this.miExit.Text = "Exit"; this.miExit.Click += new System.EventHandler(this.miExit_Click); + // // miView // this.miView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.miClearLog}); + this.miClearLog + }); this.miView.Name = "miView"; this.miView.Size = new System.Drawing.Size(44, 20); this.miView.Text = "View"; + // // miClearLog // @@ -108,14 +90,17 @@ this.miClearLog.Size = new System.Drawing.Size(124, 22); this.miClearLog.Text = "Clear Log"; this.miClearLog.Click += new System.EventHandler(this.miClearLog_Click); + // // miHelp // this.miHelp.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.miHelpAbout}); + this.miHelpAbout + }); this.miHelp.Name = "miHelp"; this.miHelp.Size = new System.Drawing.Size(44, 20); this.miHelp.Text = "Help"; + // // miHelpAbout // @@ -123,177 +108,44 @@ this.miHelpAbout.Size = new System.Drawing.Size(107, 22); this.miHelpAbout.Text = "About"; this.miHelpAbout.Click += new System.EventHandler(this.miHelpAbout_Click); + // - // pnlLeftMenu + // mainHostPanel // - this.pnlLeftMenu.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.pnlLeftMenu.Controls.Add(this.tabControl1); - this.pnlLeftMenu.Dock = System.Windows.Forms.DockStyle.Left; - this.pnlLeftMenu.Location = new System.Drawing.Point(0, 24); - this.pnlLeftMenu.Name = "pnlLeftMenu"; - this.pnlLeftMenu.Size = new System.Drawing.Size(190, 474); - this.pnlLeftMenu.TabIndex = 1; - // - // tabControl1 - // - this.tabControl1.Controls.Add(this.tabPage1); - this.tabControl1.Controls.Add(this.tabPage2); - this.tabControl1.Location = new System.Drawing.Point(3, 5); - this.tabControl1.Name = "tabControl1"; - this.tabControl1.SelectedIndex = 0; - this.tabControl1.Size = new System.Drawing.Size(186, 464); - this.tabControl1.TabIndex = 4; - // - // tabPage1 - // - this.tabPage1.Controls.Add(this.groupBox2); - this.tabPage1.Controls.Add(this.groupBox1); - this.tabPage1.Location = new System.Drawing.Point(4, 22); - this.tabPage1.Name = "tabPage1"; - this.tabPage1.Padding = new System.Windows.Forms.Padding(3); - this.tabPage1.Size = new System.Drawing.Size(178, 438); - this.tabPage1.TabIndex = 0; - this.tabPage1.Text = "Laatzen API"; - this.tabPage1.UseVisualStyleBackColor = true; - // - // groupBox2 - // - this.groupBox2.Controls.Add(this.preadjustmentButton); - this.groupBox2.Location = new System.Drawing.Point(6, 190); - this.groupBox2.Name = "groupBox2"; - this.groupBox2.Size = new System.Drawing.Size(166, 66); - this.groupBox2.TabIndex = 5; - this.groupBox2.TabStop = false; - this.groupBox2.Text = "CordonelPreadjustmentUI"; - // - // preadjustmentButton - // - this.preadjustmentButton.Location = new System.Drawing.Point(6, 19); - this.preadjustmentButton.Name = "preadjustmentButton"; - this.preadjustmentButton.Size = new System.Drawing.Size(153, 35); - this.preadjustmentButton.TabIndex = 3; - this.preadjustmentButton.Text = "Preadjustment"; - this.preadjustmentButton.UseVisualStyleBackColor = true; - this.preadjustmentButton.Click += new System.EventHandler(this.preadjustmentButton_Click); - // - // groupBox1 - // - this.groupBox1.Controls.Add(this.btnPulseSetup); - this.groupBox1.Controls.Add(this.btnSetup); - this.groupBox1.Controls.Add(this.btnRegisterStore); - this.groupBox1.Location = new System.Drawing.Point(6, 17); - this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(166, 153); - this.groupBox1.TabIndex = 4; - this.groupBox1.TabStop = false; - this.groupBox1.Text = "GenesisToolBox"; - // - // btnPulseSetup - // - this.btnPulseSetup.Location = new System.Drawing.Point(7, 101); - this.btnPulseSetup.Name = "btnPulseSetup"; - this.btnPulseSetup.Size = new System.Drawing.Size(153, 35); - this.btnPulseSetup.TabIndex = 2; - this.btnPulseSetup.Text = "Pulse Setup"; - this.btnPulseSetup.UseVisualStyleBackColor = true; - this.btnPulseSetup.Click += new System.EventHandler(this.btnPulseSetup_Click); - // - // btnSetup - // - this.btnSetup.Location = new System.Drawing.Point(7, 19); - this.btnSetup.Name = "btnSetup"; - this.btnSetup.Size = new System.Drawing.Size(153, 35); - this.btnSetup.TabIndex = 0; - this.btnSetup.Text = "Setup"; - this.btnSetup.UseVisualStyleBackColor = true; - this.btnSetup.Click += new System.EventHandler(this.btnSetup_Click); - // - // btnRegisterStore - // - this.btnRegisterStore.Location = new System.Drawing.Point(7, 60); - this.btnRegisterStore.Name = "btnRegisterStore"; - this.btnRegisterStore.Size = new System.Drawing.Size(153, 35); - this.btnRegisterStore.TabIndex = 1; - this.btnRegisterStore.Text = "Register Store"; - this.btnRegisterStore.UseVisualStyleBackColor = true; - this.btnRegisterStore.Click += new System.EventHandler(this.btnRegisterStore_Click); - // - // tabPage2 - // - this.tabPage2.Controls.Add(this.groupBox3); - this.tabPage2.Location = new System.Drawing.Point(4, 22); - this.tabPage2.Name = "tabPage2"; - this.tabPage2.Padding = new System.Windows.Forms.Padding(3); - this.tabPage2.Size = new System.Drawing.Size(178, 438); - this.tabPage2.TabIndex = 1; - this.tabPage2.Text = "GCI API"; - this.tabPage2.UseVisualStyleBackColor = true; - // - // pnlMain - // - this.pnlMain.Controls.Add(this.rtbMainLog); - this.pnlMain.Dock = System.Windows.Forms.DockStyle.Fill; - this.pnlMain.Location = new System.Drawing.Point(190, 24); - this.pnlMain.Name = "pnlMain"; - this.pnlMain.Padding = new System.Windows.Forms.Padding(9); - this.pnlMain.Size = new System.Drawing.Size(1119, 474); - this.pnlMain.TabIndex = 2; - // - // rtbMainLog - // - this.rtbMainLog.Dock = System.Windows.Forms.DockStyle.Fill; - this.rtbMainLog.Font = new System.Drawing.Font("Consolas", 10F); - this.rtbMainLog.Location = new System.Drawing.Point(9, 9); - this.rtbMainLog.Name = "rtbMainLog"; - this.rtbMainLog.ReadOnly = true; - this.rtbMainLog.Size = new System.Drawing.Size(1101, 456); - this.rtbMainLog.TabIndex = 0; - this.rtbMainLog.Text = ""; + this.mainHostPanel.Dock = System.Windows.Forms.DockStyle.Fill; + this.mainHostPanel.Location = new System.Drawing.Point(0, 24); + this.mainHostPanel.Name = "mainHostPanel"; + this.mainHostPanel.Padding = new System.Windows.Forms.Padding(0); + this.mainHostPanel.Size = new System.Drawing.Size(1309, 474); + this.mainHostPanel.TabIndex = 1; + // // statusStrip1 // this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.tslStatus}); + this.tslStatus + }); this.statusStrip1.Location = new System.Drawing.Point(0, 498); this.statusStrip1.Name = "statusStrip1"; this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 12, 0); this.statusStrip1.Size = new System.Drawing.Size(1309, 22); - this.statusStrip1.TabIndex = 3; + this.statusStrip1.TabIndex = 2; this.statusStrip1.Text = "statusStrip1"; + // // tslStatus // this.tslStatus.Name = "tslStatus"; this.tslStatus.Size = new System.Drawing.Size(39, 17); this.tslStatus.Text = "Ready"; - // - // groupBox3 - // - this.groupBox3.Controls.Add(this.button1); - this.groupBox3.Location = new System.Drawing.Point(6, 15); - this.groupBox3.Name = "groupBox3"; - this.groupBox3.Size = new System.Drawing.Size(166, 66); - this.groupBox3.TabIndex = 6; - this.groupBox3.TabStop = false; - this.groupBox3.Text = "GenesisCordonelInterface"; - // - // button1 - // - this.button1.Location = new System.Drawing.Point(6, 19); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(153, 35); - this.button1.TabIndex = 3; - this.button1.Text = "API"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); + // // MainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(1309, 520); - this.Controls.Add(this.pnlMain); - this.Controls.Add(this.pnlLeftMenu); + this.Controls.Add(this.mainHostPanel); this.Controls.Add(this.statusStrip1); this.Controls.Add(this.menuStrip1); this.MainMenuStrip = this.menuStrip1; @@ -301,33 +153,14 @@ this.Name = "MainForm"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "Genesis Cordonel Interface"; + this.Load += new System.EventHandler(this.MainForm_Load); + this.menuStrip1.ResumeLayout(false); this.menuStrip1.PerformLayout(); - this.pnlLeftMenu.ResumeLayout(false); - this.tabControl1.ResumeLayout(false); - this.tabPage1.ResumeLayout(false); - this.groupBox2.ResumeLayout(false); - this.groupBox1.ResumeLayout(false); - this.tabPage2.ResumeLayout(false); - this.pnlMain.ResumeLayout(false); this.statusStrip1.ResumeLayout(false); this.statusStrip1.PerformLayout(); - this.groupBox3.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); - } - - private System.Windows.Forms.TabControl tabControl1; - private System.Windows.Forms.TabPage tabPage1; - private System.Windows.Forms.Button btnSetup; - private System.Windows.Forms.Button preadjustmentButton; - private System.Windows.Forms.Button btnRegisterStore; - private System.Windows.Forms.Button btnPulseSetup; - private System.Windows.Forms.TabPage tabPage2; - private System.Windows.Forms.GroupBox groupBox2; - private System.Windows.Forms.GroupBox groupBox1; - private System.Windows.Forms.GroupBox groupBox3; - private System.Windows.Forms.Button button1; } } \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/MainForm.cs b/GenesisCordonelInterface/UI/MainForm.cs index 3509b7d64..89af690e1 100644 --- a/GenesisCordonelInterface/UI/MainForm.cs +++ b/GenesisCordonelInterface/UI/MainForm.cs @@ -1,173 +1,42 @@ -using GenesisCordonelInterface.UI.LaatzenAPI_CordonelPreadjustmentUI; -using GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface; -using System; +using System; using System.Drawing; -using System.Linq; -using System.Text.RegularExpressions; using System.Windows.Forms; -using Xylem.Common.Ui.GenesisToolBox; -using Xylem.Common.Utils.Logging; namespace GenesisCordonelInterface.UI { public partial class MainForm : Form { - private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface"); - private static readonly Regex LogLevelRegex = new Regex(@"\|\s*(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\s*\|", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private MainView mainView; public MainForm() { InitializeComponent(); - /*_logger = logger; - _logger.MessagePublished += OnLogMessagePublished;*/ - UiLogBus.MessageReceived += UiLogBus_MessageReceived; + StartPosition = FormStartPosition.Manual; + Location = new Point(Screen.PrimaryScreen.WorkingArea.Width - Width, 0); - //TopRight position on screen - this.StartPosition = FormStartPosition.Manual; - this.Location = new Point(Screen.PrimaryScreen.WorkingArea.Width - this.Width, 0); - - //black background - rtbMainLog.BackColor = Color.Black; - rtbMainLog.ForeColor = Color.Gainsboro; - rtbMainLog.Font = new Font("Consolas", 9f); - rtbMainLog.ReadOnly = true; - rtbMainLog.HideSelection = false; + StartPosition = FormStartPosition.CenterScreen; + WindowState = FormWindowState.Maximized; } - private void UiLogBus_MessageReceived(string msg) + private void MainForm_Load(object sender, EventArgs e) { - if (InvokeRequired) - { - BeginInvoke(new Action(UiLogBus_MessageReceived), msg); - return; - } - - string[] lines = msg.Replace("\r\n", "\n").Split('\n'); - - foreach (string originalLine in lines) - { - if (string.IsNullOrWhiteSpace(originalLine)) - continue; - - string line = originalLine; - - // Find header (timestamp|LEVEL|) - Match m = LogLevelRegex.Match(line); - - string indent = ""; - if (m.Success) - { - int indentLength = m.Index + m.Length; - indent = new string(' ', indentLength); - } - - // Split long message manually if needed (optional) - string[] subLines = line.Split(new[] { " - " }, 2, StringSplitOptions.None); - - string firstLine = line; - string rest = null; - - if (subLines.Length == 2 && subLines[1].Length > 120) // heuristic - { - firstLine = subLines[0] + " - " + subLines[1].Substring(0, 120); - rest = subLines[1].Substring(120); - } - - AppendStyledLine(firstLine); - - if (!string.IsNullOrEmpty(rest)) - { - AppendStyledLine(indent + rest); - } - } - } - - private void AppendStyledLine(string line) - { - int start = rtbMainLog.TextLength; - - rtbMainLog.SelectionStart = start; - rtbMainLog.SelectionLength = 0; - rtbMainLog.SelectionColor = Color.Gainsboro; - rtbMainLog.AppendText(line + Environment.NewLine); - - Match m = LogLevelRegex.Match(line); - if (m.Success) - { - rtbMainLog.SelectionStart = start + m.Index; - rtbMainLog.SelectionLength = m.Length; - rtbMainLog.SelectionColor = GetLogLevelColor(m.Groups[1].Value); - } - - HighlightKeywordsInLine(line, start); - } - - private int MeasureTextWidthPx(string text) - { - if (string.IsNullOrEmpty(text)) - return 0; - - return TextRenderer.MeasureText(text, rtbMainLog.Font).Width; - } - - protected override void OnFormClosed(FormClosedEventArgs e) - { - //_logger.MessagePublished -= OnLogMessagePublished; - UiLogBus.MessageReceived -= UiLogBus_MessageReceived; - base.OnFormClosed(e); - } - - private void btnSetup_Click(object sender, EventArgs e) - { - Logger.Trace("FORM: ---------------------------------"); - Logger.Trace("FORM: Setup open"); - - using (FrmSetup frm = new FrmSetup()) - { - frm.ShowDialog(this); - } - - Logger.Trace("FORM: Setup closed."); - } - - private void btnRegisterStore_Click(object sender, EventArgs e) - { - Logger.Trace("FORM: ---------------------------------"); - Logger.Trace("FORM: Register Store open."); - - - using (FrmRegisterStore frm = new FrmRegisterStore()) - { - frm.ShowDialog(this); - } - - Logger.Trace("FORM: Register Store closed."); - } - - private void btnPulseSetup_Click(object sender, EventArgs e) - { - Logger.Trace("FORM: ---------------------------------"); - Logger.Trace("FORM: Pulse Setup open."); - - - using (FrmConfigurations frm = new FrmConfigurations()) - { - frm.ShowDialog(this); - } - - Logger.Trace("FORM: Pulse Setup closed."); + mainView = new MainView(); + mainView.Dock = DockStyle.Fill; + Controls.Add(mainView); + mainView.BringToFront(); + this.WindowState = FormWindowState.Maximized; } private void miExit_Click(object sender, EventArgs e) { - this.Close(); + Close(); } private void miClearLog_Click(object sender, EventArgs e) { - rtbMainLog.Clear(); - Logger.Trace("Log cleared."); + if (mainView != null) + mainView.ClearLog(); } private void miHelpAbout_Click(object sender, EventArgs e) @@ -178,113 +47,5 @@ namespace GenesisCordonelInterface.UI MessageBoxButtons.OK, MessageBoxIcon.Information); } - - private Color GetLogLevelColor(string level) - { - switch (level.Trim().ToUpperInvariant()) - { - case "TRACE": return Color.Gray; - case "DEBUG": return Color.DeepSkyBlue; - case "INFO": return Color.LimeGreen; - case "WARN": return Color.Orange; - case "ERROR": return Color.Red; - case "FATAL": return Color.Magenta; - default: return Color.Gainsboro; - } - } - - private bool IsSeparatorLine(string text) - { - if (string.IsNullOrWhiteSpace(text)) - return false; - - string trimmed = text.Trim(); - - // Remove spaces and tab-like spacing - string compact = new string(trimmed.Where(c => !char.IsWhiteSpace(c)).ToArray()); - - if (compact.Length < 4) - return false; - - // Count non-letter/non-digit characters - int nonAlnumCount = compact.Count(c => !char.IsLetterOrDigit(c)); - - // Consider it a separator if most characters are non-alphanumeric - // Examples: - // -----CommandToMeter()----- - // ========================== - // ///////// - double ratio = (double)nonAlnumCount / compact.Length; - - return ratio >= 0.6; - } - - /// - /// special string highlighting - /// "TX FINAL" have to go before "TX" - /// else "TX" will highlighted in the middle of "TX FINAL" - /// - private static readonly (Color color, string[] keywords)[] KeywordGroups = - { - (Color.DeepSkyBlue, new[] { "REQUEST" }), - (Color.Lime, new[] { "RESPONSE" }), - //(Color.LightGreen, new[] { "START", "END" }), - //(Color.Cyan, new[] { "TX", "TX FINAL" }), - //(Color.DeepSkyBlue,new[] { "RX", "RX FINAL", "RX CHUNK" }), - (Color.Gold, new[] { "READ-REGISTER-SESSION", "UI-CLICK"}), - //(Color.Violet, new[] { "REGADDR" }), - //(Color.Khaki, new[] { "DEFAULT", "ALIGNED", "STRING" }) - }; - - /// - /// special string highlighting - /// - /// - /// - private void HighlightKeywordsInLine(string line, int lineStartIndex) - { - foreach (var group in KeywordGroups) - { - foreach (var keyword in group.keywords) - { - int index = 0; - - while ((index = line.IndexOf(keyword, index, StringComparison.Ordinal)) >= 0) - { - rtbMainLog.SelectionStart = lineStartIndex + index; - rtbMainLog.SelectionLength = keyword.Length; - rtbMainLog.SelectionColor = group.color; - - index += keyword.Length; - } - } - } - } - - private void preadjustmentButton_Click(object sender, EventArgs e) - { - Logger.Trace("FORM: ---------------------------------"); - Logger.Trace("FORM: Preadjustment open."); - - using (FrmCordonelPreadjustmentUI frm = new FrmCordonelPreadjustmentUI()) - { - frm.ShowDialog(this); - } - - Logger.Trace("FORM: Preadjustment closed."); - } - - private void button1_Click(object sender, EventArgs e) - { - Logger.Trace("FORM: ---------------------------------"); - Logger.Trace("FORM: GCI GUI interface open."); - - using (FrmGCIAPI frm = new FrmGCIAPI()) - { - frm.ShowDialog(this); - } - - Logger.Trace("FORM: GCI GUI interface closed."); - } } } \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/MainView.Designer.cs b/GenesisCordonelInterface/UI/MainView.Designer.cs new file mode 100644 index 000000000..5733f9569 --- /dev/null +++ b/GenesisCordonelInterface/UI/MainView.Designer.cs @@ -0,0 +1,311 @@ +namespace GenesisCordonelInterface.UI +{ + partial class MainView + { + private System.ComponentModel.IContainer components = null; + + private System.Windows.Forms.Panel pnlLeftMenu; + private System.Windows.Forms.Panel pnlMain; + + private System.Windows.Forms.SplitContainer splitMain; + private System.Windows.Forms.SplitContainer splitBottom; + + private System.Windows.Forms.RichTextBox rtbMainLog; + private System.Windows.Forms.Panel pnlSlotConfig; + private System.Windows.Forms.Panel pnlWorkerDebug; + + private System.Windows.Forms.TabControl tabControl1; + private System.Windows.Forms.TabPage tabPage1; + private System.Windows.Forms.TabPage tabPage2; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.GroupBox groupBox2; + private System.Windows.Forms.GroupBox groupBox3; + + private System.Windows.Forms.Button btnSetup; + private System.Windows.Forms.Button btnRegisterStore; + private System.Windows.Forms.Button btnPulseSetup; + private System.Windows.Forms.Button preadjustmentButton; + private System.Windows.Forms.Button btnMeterInit; + private System.Windows.Forms.Button btnMetersAction; + private System.Windows.Forms.SplitContainer splitWorkArea; + private System.Windows.Forms.Panel pnlGciViewHost; + + + private void InitializeComponent() + { + this.pnlLeftMenu = new System.Windows.Forms.Panel(); + this.tabControl1 = new System.Windows.Forms.TabControl(); + this.tabPage1 = new System.Windows.Forms.TabPage(); + this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.preadjustmentButton = new System.Windows.Forms.Button(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.btnPulseSetup = new System.Windows.Forms.Button(); + this.btnSetup = new System.Windows.Forms.Button(); + this.btnRegisterStore = new System.Windows.Forms.Button(); + this.tabPage2 = new System.Windows.Forms.TabPage(); + this.groupBox3 = new System.Windows.Forms.GroupBox(); + this.btnMeterInit = new System.Windows.Forms.Button(); + this.btnMetersAction = new System.Windows.Forms.Button(); + this.pnlMain = new System.Windows.Forms.Panel(); + this.splitMain = new System.Windows.Forms.SplitContainer(); + this.splitBottom = new System.Windows.Forms.SplitContainer(); + this.rtbMainLog = new System.Windows.Forms.RichTextBox(); + this.pnlSlotConfig = new System.Windows.Forms.Panel(); + this.pnlWorkerDebug = new System.Windows.Forms.Panel(); + this.splitWorkArea = new System.Windows.Forms.SplitContainer(); + this.pnlGciViewHost = new System.Windows.Forms.Panel(); + + this.pnlLeftMenu.SuspendLayout(); + this.tabControl1.SuspendLayout(); + this.tabPage1.SuspendLayout(); + this.groupBox2.SuspendLayout(); + this.groupBox1.SuspendLayout(); + this.tabPage2.SuspendLayout(); + this.groupBox3.SuspendLayout(); + this.pnlMain.SuspendLayout(); + + ((System.ComponentModel.ISupportInitialize)(this.splitMain)).BeginInit(); + this.splitMain.Panel1.SuspendLayout(); + this.splitMain.Panel2.SuspendLayout(); + this.splitMain.SuspendLayout(); + + ((System.ComponentModel.ISupportInitialize)(this.splitBottom)).BeginInit(); + this.splitBottom.Panel1.SuspendLayout(); + this.splitBottom.Panel2.SuspendLayout(); + this.splitBottom.SuspendLayout(); + + this.SuspendLayout(); + + // pnlLeftMenu + this.pnlLeftMenu.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.pnlLeftMenu.Controls.Add(this.tabControl1); + this.pnlLeftMenu.Dock = System.Windows.Forms.DockStyle.Left; + this.pnlLeftMenu.Location = new System.Drawing.Point(0, 0); + this.pnlLeftMenu.Name = "pnlLeftMenu"; + this.pnlLeftMenu.Size = new System.Drawing.Size(190, 500); + this.pnlLeftMenu.TabIndex = 0; + + // tabControl1 + this.tabControl1.Controls.Add(this.tabPage1); + this.tabControl1.Controls.Add(this.tabPage2); + this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tabControl1.Location = new System.Drawing.Point(0, 0); + this.tabControl1.Name = "tabControl1"; + this.tabControl1.SelectedIndex = 0; + this.tabControl1.Size = new System.Drawing.Size(188, 498); + this.tabControl1.TabIndex = 0; + + // tabPage1 + this.tabPage1.Controls.Add(this.groupBox2); + this.tabPage1.Controls.Add(this.groupBox1); + this.tabPage1.Location = new System.Drawing.Point(4, 22); + this.tabPage1.Name = "tabPage1"; + this.tabPage1.Padding = new System.Windows.Forms.Padding(3); + this.tabPage1.Size = new System.Drawing.Size(180, 472); + this.tabPage1.TabIndex = 0; + this.tabPage1.Text = "Laatzen API"; + this.tabPage1.UseVisualStyleBackColor = true; + + // groupBox1 + this.groupBox1.Controls.Add(this.btnPulseSetup); + this.groupBox1.Controls.Add(this.btnSetup); + this.groupBox1.Controls.Add(this.btnRegisterStore); + this.groupBox1.Location = new System.Drawing.Point(6, 10); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(166, 150); + this.groupBox1.TabIndex = 0; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "GenesisToolBox"; + + // btnSetup + this.btnSetup.Location = new System.Drawing.Point(7, 20); + this.btnSetup.Name = "btnSetup"; + this.btnSetup.Size = new System.Drawing.Size(150, 30); + this.btnSetup.TabIndex = 0; + this.btnSetup.Text = "Setup"; + this.btnSetup.UseVisualStyleBackColor = true; + this.btnSetup.Click += new System.EventHandler(this.btnSetup_Click); + + // btnRegisterStore + this.btnRegisterStore.Location = new System.Drawing.Point(7, 55); + this.btnRegisterStore.Name = "btnRegisterStore"; + this.btnRegisterStore.Size = new System.Drawing.Size(150, 30); + this.btnRegisterStore.TabIndex = 1; + this.btnRegisterStore.Text = "Register Store"; + this.btnRegisterStore.UseVisualStyleBackColor = true; + this.btnRegisterStore.Click += new System.EventHandler(this.btnRegisterStore_Click); + + // btnPulseSetup + this.btnPulseSetup.Location = new System.Drawing.Point(7, 90); + this.btnPulseSetup.Name = "btnPulseSetup"; + this.btnPulseSetup.Size = new System.Drawing.Size(150, 30); + this.btnPulseSetup.TabIndex = 2; + this.btnPulseSetup.Text = "Pulse Setup"; + this.btnPulseSetup.UseVisualStyleBackColor = true; + this.btnPulseSetup.Click += new System.EventHandler(this.btnPulseSetup_Click); + + // groupBox2 + this.groupBox2.Controls.Add(this.preadjustmentButton); + this.groupBox2.Location = new System.Drawing.Point(6, 170); + this.groupBox2.Name = "groupBox2"; + this.groupBox2.Size = new System.Drawing.Size(166, 70); + this.groupBox2.TabIndex = 1; + this.groupBox2.TabStop = false; + this.groupBox2.Text = "CordonelPreadjustmentUI"; + + // preadjustmentButton + this.preadjustmentButton.Location = new System.Drawing.Point(6, 20); + this.preadjustmentButton.Name = "preadjustmentButton"; + this.preadjustmentButton.Size = new System.Drawing.Size(150, 30); + this.preadjustmentButton.TabIndex = 0; + this.preadjustmentButton.Text = "Preadjustment"; + this.preadjustmentButton.UseVisualStyleBackColor = true; + this.preadjustmentButton.Click += new System.EventHandler(this.preadjustmentButton_Click); + + // tabPage2 + this.tabPage2.Controls.Add(this.groupBox3); + this.tabPage2.Location = new System.Drawing.Point(4, 22); + this.tabPage2.Name = "tabPage2"; + this.tabPage2.Padding = new System.Windows.Forms.Padding(3); + this.tabPage2.Size = new System.Drawing.Size(180, 472); + this.tabPage2.TabIndex = 1; + this.tabPage2.Text = "GCI API"; + this.tabPage2.UseVisualStyleBackColor = true; + + // groupBox3 + this.groupBox3.Controls.Add(this.btnMeterInit); + this.groupBox3.Controls.Add(this.btnMetersAction); + this.groupBox3.Size = new System.Drawing.Size(166, 105); + this.groupBox3.Location = new System.Drawing.Point(6, 10); + this.groupBox3.Name = "groupBox3"; + this.groupBox3.TabIndex = 0; + this.groupBox3.TabStop = false; + this.groupBox3.Text = "GenesisCordonelInterface"; + + // btnMeterInit + this.btnMeterInit.Location = new System.Drawing.Point(6, 20); + this.btnMeterInit.Name = "btnMeterInit"; + this.btnMeterInit.Size = new System.Drawing.Size(150, 30); + this.btnMeterInit.Text = "Meter Init"; + this.btnMeterInit.UseVisualStyleBackColor = true; + this.btnMeterInit.Click += new System.EventHandler(this.btnMeterInit_Click); + + // btnMetersAction + this.btnMetersAction.Location = new System.Drawing.Point(6, 58); + this.btnMetersAction.Name = "btnMetersAction"; + this.btnMetersAction.Size = new System.Drawing.Size(150, 30); + this.btnMetersAction.Text = "Meters Action"; + this.btnMetersAction.UseVisualStyleBackColor = true; + this.btnMetersAction.Click += new System.EventHandler(this.btnMetersAction_Click); + + // pnlMain + this.pnlMain.Controls.Add(this.splitMain); + this.pnlMain.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlMain.Location = new System.Drawing.Point(190, 0); + this.pnlMain.Name = "pnlMain"; + this.pnlMain.Padding = new System.Windows.Forms.Padding(5); + this.pnlMain.Size = new System.Drawing.Size(710, 500); + this.pnlMain.TabIndex = 1; + + // splitMain + this.splitMain.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitMain.Location = new System.Drawing.Point(5, 5); + this.splitMain.Name = "splitMain"; + this.splitMain.Orientation = System.Windows.Forms.Orientation.Horizontal; + this.splitMain.Panel1.Controls.Add(this.splitWorkArea); + this.splitMain.Panel1MinSize = 100; + this.splitMain.Panel2.Controls.Add(this.splitBottom); + this.splitMain.Panel2MinSize = 100; + this.splitMain.Size = new System.Drawing.Size(700, 490); + this.splitMain.SplitterDistance = 320; + this.splitMain.SplitterWidth = 6; + this.splitMain.TabIndex = 0; + + // rtbMainLog + this.rtbMainLog.Dock = System.Windows.Forms.DockStyle.Fill; + this.rtbMainLog.Font = new System.Drawing.Font("Consolas", 10F); + this.rtbMainLog.Name = "rtbMainLog"; + this.rtbMainLog.ReadOnly = true; + this.rtbMainLog.Size = new System.Drawing.Size(700, 320); + this.rtbMainLog.TabIndex = 0; + this.rtbMainLog.Text = ""; + + // splitBottom + this.splitBottom.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitBottom.Location = new System.Drawing.Point(0, 0); + this.splitBottom.Name = "splitBottom"; + this.splitBottom.Orientation = System.Windows.Forms.Orientation.Vertical; + this.splitBottom.Panel1.Controls.Add(this.pnlSlotConfig); + this.splitBottom.Panel1MinSize = 250; + this.splitBottom.Panel2.Controls.Add(this.pnlWorkerDebug); + this.splitBottom.Panel2MinSize = 250; + this.splitBottom.Size = new System.Drawing.Size(700, 164); + this.splitBottom.SplitterDistance = 320; + this.splitBottom.SplitterWidth = 6; + this.splitBottom.TabIndex = 0; + + // pnlSlotConfig + this.pnlSlotConfig.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.pnlSlotConfig.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlSlotConfig.Location = new System.Drawing.Point(0, 0); + this.pnlSlotConfig.Name = "pnlSlotConfig"; + this.pnlSlotConfig.Size = new System.Drawing.Size(320, 164); + this.pnlSlotConfig.TabIndex = 0; + + // pnlWorkerDebug + this.pnlWorkerDebug.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.pnlWorkerDebug.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlWorkerDebug.Location = new System.Drawing.Point(0, 0); + this.pnlWorkerDebug.Name = "pnlWorkerDebug"; + this.pnlWorkerDebug.Size = new System.Drawing.Size(374, 164); + this.pnlWorkerDebug.TabIndex = 0; + + // splitWorkArea + this.splitWorkArea.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitWorkArea.Orientation = System.Windows.Forms.Orientation.Vertical; + this.splitWorkArea.Panel1MinSize = 25; + this.splitWorkArea.Panel2MinSize = 25; + this.splitWorkArea.SplitterWidth = 6; + + // left = active GCI view + this.splitWorkArea.Panel1.Controls.Add(this.pnlGciViewHost); + + // right = black memo/log + this.splitWorkArea.Panel2.Controls.Add(this.rtbMainLog); + + // pnlGciViewHost + this.pnlGciViewHost.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlGciViewHost.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + + // rtbMainLog + this.rtbMainLog.Dock = System.Windows.Forms.DockStyle.Fill; + + // MainView + this.Controls.Add(this.pnlMain); + this.Controls.Add(this.pnlLeftMenu); + this.Name = "MainView"; + this.Size = new System.Drawing.Size(900, 500); + + this.pnlLeftMenu.ResumeLayout(false); + this.tabControl1.ResumeLayout(false); + this.tabPage1.ResumeLayout(false); + this.groupBox2.ResumeLayout(false); + this.groupBox1.ResumeLayout(false); + this.tabPage2.ResumeLayout(false); + this.groupBox3.ResumeLayout(false); + this.pnlMain.ResumeLayout(false); + + this.splitMain.Panel1.ResumeLayout(false); + this.splitMain.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitMain)).EndInit(); + this.splitMain.ResumeLayout(false); + + this.splitBottom.Panel1.ResumeLayout(false); + this.splitBottom.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitBottom)).EndInit(); + this.splitBottom.ResumeLayout(false); + + this.ResumeLayout(false); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/MainView.cs b/GenesisCordonelInterface/UI/MainView.cs new file mode 100644 index 000000000..d2997c3a0 --- /dev/null +++ b/GenesisCordonelInterface/UI/MainView.cs @@ -0,0 +1,333 @@ +using GenesisCordonelInterface.UI.LaatzenAPI_CordonelPreadjustmentUI; +using GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface; +using System; +using System.Drawing; +using System.Linq; +using System.Net.NetworkInformation; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using Xylem.Common.Ui.GenesisToolBox; +using Xylem.Common.Utils.Logging; +using GenesisCordonelInterface.API; +using GenesisCordonelInterface.UI.Debug; + +namespace GenesisCordonelInterface.UI +{ + public partial class MainView : UserControl + { + private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface"); + private static readonly Regex LogLevelRegex = new Regex(@"\|\s*(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\s*\|", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly InterfaceOutsideToGCI _api = new InterfaceOutsideToGCI(); + private MeterBatchConfigPanel _batchPanel; + + public MainView() + { + InitializeComponent(); + InitializeDebugPanels(); + //InitializeWorkerDebugPanel(); + + UiLogBus.MessageReceived += UiLogBus_MessageReceived; + + rtbMainLog.BackColor = Color.Black; + rtbMainLog.ForeColor = Color.Gainsboro; + rtbMainLog.Font = new Font("Consolas", 9f); + rtbMainLog.ReadOnly = true; + rtbMainLog.HideSelection = false; + } + + private void InitializeWorkerDebugPanel() + { + pnlWorkerDebug.Controls.Clear(); + + var debugPanel = new WorkerDebugPanel(_api) + { + Dock = DockStyle.Fill + }; + + pnlWorkerDebug.Controls.Add(debugPanel); + } + private void InitializeDebugPanels() + { + _batchPanel = new MeterBatchConfigPanel(_api) + { + Dock = DockStyle.Fill + }; + + pnlSlotConfig.Controls.Add(_batchPanel); + + pnlWorkerDebug.Controls.Add(new WorkerDebugPanel(_api) + { + Dock = DockStyle.Fill + }); + } + + public void AddSlotRow() + { + _batchPanel?.AddEmptySlotRow(); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + BeginInvoke(new Action(() => + { + SetSafeSplitterDistance(splitWorkArea, 420); + })); + } + + private void SetSafeSplitterDistance(SplitContainer split, int desired) + { + int width = split.ClientSize.Width; + + int min1 = split.Panel1MinSize; + int min2 = split.Panel2MinSize; + int splitter = split.SplitterWidth; + + int max = width - min2 - splitter; + + if (width <= min1 + min2 + splitter) + return; + + if (desired < min1) + desired = min1; + + if (desired > max) + desired = max; + + split.SplitterDistance = desired; + } + + protected override void Dispose(bool disposing) + { + UiLogBus.MessageReceived -= UiLogBus_MessageReceived; + base.Dispose(disposing); + } + + private IWin32Window DialogOwner + { + get + { + Form owner = FindForm(); + return owner ?? (IWin32Window)this; + } + } + + private void btnSetup_Click(object sender, EventArgs e) + { + Logger.Trace("FORM: ---------------------------------"); + Logger.Trace("FORM: Setup open"); + + using (FrmSetup frm = new FrmSetup()) + { + frm.ShowDialog(DialogOwner); + } + + Logger.Trace("FORM: Setup closed."); + } + + private void btnRegisterStore_Click(object sender, EventArgs e) + { + Logger.Trace("FORM: ---------------------------------"); + Logger.Trace("FORM: Register Store open."); + + using (FrmRegisterStore frm = new FrmRegisterStore()) + { + frm.ShowDialog(DialogOwner); + } + + Logger.Trace("FORM: Register Store closed."); + } + + private void btnPulseSetup_Click(object sender, EventArgs e) + { + Logger.Trace("FORM: ---------------------------------"); + Logger.Trace("FORM: Pulse Setup open."); + + using (FrmConfigurations frm = new FrmConfigurations()) + { + frm.ShowDialog(DialogOwner); + } + + Logger.Trace("FORM: Pulse Setup closed."); + } + + private void preadjustmentButton_Click(object sender, EventArgs e) + { + Logger.Trace("FORM: ---------------------------------"); + Logger.Trace("FORM: Preadjustment open."); + + using (FrmCordonelPreadjustmentUI frm = new FrmCordonelPreadjustmentUI()) + { + frm.ShowDialog(DialogOwner); + } + + Logger.Trace("FORM: Preadjustment closed."); + } + + private void button1_Click(object sender, EventArgs e) + { + Logger.Trace("FORM: ---------------------------------"); + Logger.Trace("FORM: GCI GUI interface open."); + + using (var frm = new FrmGCIAPI(_api)) + { + frm.ShowDialog(this); + } + + Logger.Trace("FORM: GCI GUI interface closed."); + } + + private void SwitchGciView(string name, Control view) + { + Logger.Trace("FORM: ---------------------------------"); + Logger.Trace($"FORM: GCI VIEW -> {name} OPEN"); + + pnlGciViewHost.Controls.Clear(); + + view.Dock = DockStyle.Fill; + pnlGciViewHost.Controls.Add(view); + view.BringToFront(); + + Logger.Trace($"FORM: GCI VIEW -> {name} LOADED"); + } + + private void btnMeterInit_Click(object sender, EventArgs e) + { + SwitchGciView( + "MeterInit", + new MeterInitView(_api, AddSlotRow, SaveSlots)); + } + + private void btnMetersAction_Click(object sender, EventArgs e) + { + SwitchGciView( + "MetersAction", + new MetersActionView(_api)); + } + + public void ClearLog() + { + rtbMainLog.Clear(); + Logger.Trace("Log cleared."); + } + + private void UiLogBus_MessageReceived(string msg) + { + if (InvokeRequired) + { + BeginInvoke(new Action(UiLogBus_MessageReceived), msg); + return; + } + + string[] lines = msg.Replace("\r\n", "\n").Split('\n'); + + foreach (string originalLine in lines) + { + if (string.IsNullOrWhiteSpace(originalLine)) + continue; + + string line = originalLine; + Match m = LogLevelRegex.Match(line); + + string indent = ""; + if (m.Success) + { + int indentLength = m.Index + m.Length; + indent = new string(' ', indentLength); + } + + string[] subLines = line.Split(new[] { " - " }, 2, StringSplitOptions.None); + + string firstLine = line; + string rest = null; + + if (subLines.Length == 2 && subLines[1].Length > 120) + { + firstLine = subLines[0] + " - " + subLines[1].Substring(0, 120); + rest = subLines[1].Substring(120); + } + + AppendStyledLine(firstLine); + + if (!string.IsNullOrEmpty(rest)) + AppendStyledLine(indent + rest); + } + } + + private void AppendStyledLine(string line) + { + int start = rtbMainLog.TextLength; + + rtbMainLog.SelectionStart = start; + rtbMainLog.SelectionLength = 0; + rtbMainLog.SelectionColor = Color.Gainsboro; + rtbMainLog.AppendText(line + Environment.NewLine); + + Match m = LogLevelRegex.Match(line); + if (m.Success) + { + rtbMainLog.SelectionStart = start + m.Index; + rtbMainLog.SelectionLength = m.Length; + rtbMainLog.SelectionColor = GetLogLevelColor(m.Groups[1].Value); + } + + HighlightKeywordsInLine(line, start); + } + + private Color GetLogLevelColor(string level) + { + switch (level.Trim().ToUpperInvariant()) + { + case "TRACE": return Color.Gray; + case "DEBUG": return Color.DeepSkyBlue; + case "INFO": return Color.LimeGreen; + case "WARN": return Color.Orange; + case "ERROR": return Color.Red; + case "FATAL": return Color.Magenta; + default: return Color.Gainsboro; + } + } + + private static readonly Tuple[] KeywordGroups = + { + Tuple.Create(Color.DeepSkyBlue, new[] { "REQUEST" }), + Tuple.Create(Color.Lime, new[] { "RESPONSE" }), + Tuple.Create(Color.Gold, new[] { "READ-REGISTER-SESSION", "UI-CLICK" }) + }; + + private void HighlightKeywordsInLine(string line, int lineStartIndex) + { + foreach (var group in KeywordGroups) + { + foreach (var keyword in group.Item2) + { + int index = 0; + + while ((index = line.IndexOf(keyword, index, StringComparison.Ordinal)) >= 0) + { + rtbMainLog.SelectionStart = lineStartIndex + index; + rtbMainLog.SelectionLength = keyword.Length; + rtbMainLog.SelectionColor = group.Item1; + + index += keyword.Length; + } + } + } + } + + private void ShowGciView(Control view) + { + pnlGciViewHost.Controls.Clear(); + + view.Dock = DockStyle.Fill; + pnlGciViewHost.Controls.Add(view); + } + + public void SaveSlots() + { + var data = _batchPanel.GetGridData(); + _api.SaveSlotSetup(data); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.Designer.cs b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.Designer.cs index 781298ad1..5a760221d 100644 --- a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.Designer.cs +++ b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.Designer.cs @@ -82,7 +82,7 @@ this.lblConfigSource.Name = "lblConfigSource"; this.lblConfigSource.Size = new System.Drawing.Size(75, 13); this.lblConfigSource.TabIndex = 2; - this.lblConfigSource.Text = "ConfigSource:"; + this.lblConfigSource.Text = "GciConfigSource:"; // // cmbConfigSource // @@ -314,7 +314,7 @@ // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(624, 455); + this.ClientSize = new System.Drawing.Size(624, 650); this.Controls.Add(this.btnSetPassword); this.Controls.Add(this.btnWriteRegister); this.Controls.Add(this.btnReadRegister); diff --git a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.cs b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.cs index 5ba4b7042..d56f7d441 100644 --- a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.cs +++ b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using System.Windows.Forms; using GenesisCordonelInterface.API; using Xylem.Common.Hardware.Interfaces.Ports.PortCore; @@ -8,36 +9,137 @@ namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface { public partial class FrmGCIAPI : Form { - private readonly InterfaceOutsideToGCI _api = new InterfaceOutsideToGCI(); + /* + GCI UI async execution model - public FrmGCIAPI() + The UI thread must never execute long-running meter communication directly. + When the user clicks a button, the UI only reads input values from controls + (for example slot number) and then delegates the real work to the GCI API. + + The API call is executed by a per-slot worker queue: + - one slot / one meter / one communication port has its own worker + - commands for the same meter are executed sequentially + - commands for different meters can run in parallel + - after await completes, execution returns back to the UI thread safely + + This prevents the WinForms UI from freezing while keeping meter communication + safe and ordered. + + workflow: + + User clicks button + | + v + WinForms UI thread + (read textbox values only) + | + v + await _api.GetPcbIdAsync(slot) + | + v + InterfaceOutsideToGCI + (clean public facade) + | + v + InterfaceGCIToLaatzen + (API/business logic) + | + v + Per-slot ApiWorker queue + (slot 1 / slot 2 / slot 3 ...) + | + v + GenesisMeter communication + (blocking HW operation) + | + v + Result returned + | + v + UI thread continues after await + (update labels / show MessageBox) + + The UI does not freeze because hardware communication is not executed directly inside the event handler. + The event handler only calls an async method and waits using await. While waiting, the UI thread remains free. + The actual work is executed on a worker thread assigned to a specific slot/meter. + For a single meter, requests are queued, so they are executed sequentially and do not run at the same time, + which prevents communication conflicts. + */ + + private readonly InterfaceOutsideToGCI _api; + + + public FrmGCIAPI() : this(new InterfaceOutsideToGCI()) { + + } + + /// + /// Initializes the form and default UI values. + /// + public FrmGCIAPI(InterfaceOutsideToGCI api) + { + _api = api ?? throw new ArgumentNullException(nameof(api)); + InitializeComponent(); InitializeDefaults(); } + /// + /// Sets default values for combo boxes. + /// private void InitializeDefaults() { cmbConfigSource.DataSource = Enum.GetValues(typeof(ConfigSource)); cmbPasswordSource.DataSource = Enum.GetValues(typeof(PasswordSource)); - cmbConfigSource.SelectedItem = ConfigSource.ExternStorage; + cmbConfigSource.SelectedItem = ConfigSource.InterfaceInputConfig; cmbPasswordSource.SelectedItem = PasswordSource.OfflineFile; } - private void ExecuteApiAction(Action action) + #region UI Helpers + + /// + /// Executes an async API action with UI busy state and centralized error handling. + /// + private async Task ExecuteApiActionAsync(Func action) { try { - action(); + SetBusy(true); + await action(); } catch (Exception ex) { MessageBox.Show(ex.Message, "API call failed", MessageBoxButtons.OK, MessageBoxIcon.Error); } + finally + { + SetBusy(false); + } } - private int GetSlot() + /// + /// Enables/disables UI controls and updates cursor to indicate busy state. + /// + private void SetBusy(bool busy) + { + Cursor = busy ? Cursors.WaitCursor : Cursors.Default; + + btnInit.Enabled = !busy; + btnConnectOne.Enabled = !busy; + btnConnectAll.Enabled = !busy; + btnDisconnect.Enabled = !busy; + btnGetPcbId.Enabled = !busy; + btnReadRegister.Enabled = !busy; + btnWriteRegister.Enabled = !busy; + btnSetPassword.Enabled = !busy; + } + + /// + /// Safely parses slot number from UI textbox. + /// + private int GetSlotSafe() { if (!int.TryParse(txtSlot.Text, out int slot)) throw new Exception("Invalid slot number."); @@ -45,21 +147,28 @@ namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface return slot; } + /// + /// Converts string input to best matching primitive type. + /// private object ParseValue(string input) { - if (int.TryParse(input, out int intValue)) - return intValue; - - if (uint.TryParse(input, out uint uintValue)) - return uintValue; - - if (bool.TryParse(input, out bool boolValue)) - return boolValue; + if (int.TryParse(input, out int i)) return i; + if (uint.TryParse(input, out uint ui)) return ui; + if (bool.TryParse(input, out bool b)) return b; return input; } - private PortConfig? CreatePortConfig(string portName, string baudRateText) + /// + /// Creates a public GCI port configuration object from UI input values. + /// + /// Name of the port (e.g. COM3). + /// Baud rate entered in the UI. + /// + /// Instance of or null if port name is empty. + /// + /// Thrown when baud rate is invalid. + private GciPublicModels.GciPortConfig CreateGciPortConfig(string portName, string baudRateText) { if (string.IsNullOrWhiteSpace(portName)) return null; @@ -67,127 +176,267 @@ namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface if (!int.TryParse(baudRateText, out int baudRate)) throw new Exception($"Invalid baud rate for port {portName}."); - var cfg = new PortConfig + return new GciPublicModels.GciPortConfig { PortName = portName, - Type = "Serial" + Type = "Serial", }; - - var sp = cfg.GetSerialPort(); - if (sp == null) - throw new Exception($"Serial port object was not created for {portName}."); - - sp.BaudRate = baudRate; - - return cfg; } - private ConfigSource GetConfigSource() + /// + /// Gets selected configuration source from UI and maps it to public GCI model. + /// + /// Selected . + /// Thrown when no value is selected. + private GciPublicModels.GciConfigSource GetConfigSource() { if (cmbConfigSource.SelectedItem == null) - throw new Exception("ConfigSource is not selected."); + throw new Exception("Config source is not selected."); - return (ConfigSource)cmbConfigSource.SelectedItem; + return (GciPublicModels.GciConfigSource)cmbConfigSource.SelectedItem; } - private PasswordSource GetPasswordSource() + /// + /// Gets selected password source from UI and maps it to public GCI model. + /// + /// Selected . + /// Thrown when no value is selected. + private GciPublicModels.GciPasswordSource GetPasswordSource() { if (cmbPasswordSource.SelectedItem == null) - throw new Exception("PasswordSource is not selected."); + throw new Exception("Password source is not selected."); - return (PasswordSource)cmbPasswordSource.SelectedItem; + return (GciPublicModels.GciPasswordSource)cmbPasswordSource.SelectedItem; } - private void btnInit_Click(object sender, EventArgs e) + #endregion + + #region Buttons + + /// + /// Initializes meter using external configuration. + /// + private async void btnInit_Click(object sender, EventArgs e) { - ExecuteApiAction(() => + int slot; + + try { slot = GetSlotSafe(); } + catch (Exception ex) { - int slot = GetSlot(); + MessageBox.Show(ex.Message, "Invalid input"); + return; + } - var requestPort = CreatePortConfig(txtRequestPort.Text, txtRequestBaudRate.Text); - var streamingPort = CreatePortConfig(txtStreamingPort.Text, txtStreamingBaudRate.Text); + await ExecuteApiActionAsync(async () => + { + var request = new GciPublicModels.GciInitSlotRequest + { + SlotId = slot, + ConfigSource = GetConfigSource(), + PasswordSource = GetPasswordSource(), + RequestPort = CreateGciPortConfig(txtRequestPort.Text, txtRequestBaudRate.Text), + StreamingPort = CreateGciPortConfig(txtStreamingPort.Text, txtStreamingBaudRate.Text) + }; - _api.InitOneMeterFromExtern( - slot, - GetConfigSource(), - GetPasswordSource(), - requestPort, - streamingPort); + var result = await _api.InitSlotAsync(request); + + if (!result.Success) + throw new Exception(result.Message); + + MessageBox.Show("Meter initialized."); }); } - private void btnConnectOne_Click(object sender, EventArgs e) + /// + /// Connects to one meter. + /// + private async void btnConnectOne_Click(object sender, EventArgs e) { - ExecuteApiAction(() => + int slot; + + try { slot = GetSlotSafe(); } + catch (Exception ex) { - _api.ConnectOneMeter(GetSlot()); + MessageBox.Show(ex.Message, "Invalid input"); + return; + } + + await ExecuteApiActionAsync(async () => + { + var result = await _api.ConnectOneSlotAsync(slot); + + if (!result.Success) + throw new Exception(result.Message); + + MessageBox.Show($"Connected.\r\nPCB ID: {result.PcbId}"); }); } - private void btnConnectAll_Click(object sender, EventArgs e) + /// + /// Disconnects from current meter. + /// + private async void btnDisconnect_Click(object sender, EventArgs e) { - ExecuteApiAction(() => + int slot; + + try { slot = GetSlotSafe(); } + catch (Exception ex) { - _api.ConnectAllMeters(GetSlot()); + MessageBox.Show(ex.Message, "Invalid input"); + return; + } + + await ExecuteApiActionAsync(async () => + { + await _api.DisconnectAsync(slot); + MessageBox.Show("Disconnected."); + }); + } + /// + /// Reads PCB ID asynchronously. + /// + private async void btnGetPcbId_Click(object sender, EventArgs e) + { + int slot; + + try { slot = GetSlotSafe(); } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Invalid input"); + return; + } + + await ExecuteApiActionAsync(async () => + { + GciPublicModels.GciGetPcbIdResult result = + await _api.GetPcbIdAsync(slot); + + if (!result.Success) + throw new Exception(result.Message); + + MessageBox.Show($"PCB ID: {result.PcbId}"); }); } - private void btnDisconnect_Click(object sender, EventArgs e) + /// + /// Reads register value. + /// + private async void btnReadRegister_Click(object sender, EventArgs e) { - ExecuteApiAction(() => - { - _api.Disconnect(); - }); - } + int slot; + string registerName = txtRegister.Text; - private void btnGetPcbId_Click(object sender, EventArgs e) - { - ExecuteApiAction(() => + try { - _api.GetPcbId(GetSlot()); - }); - } - - private void btnReadRegister_Click(object sender, EventArgs e) - { - ExecuteApiAction(() => - { - string registerName = txtRegister.Text; + slot = GetSlotSafe(); if (string.IsNullOrWhiteSpace(registerName)) throw new Exception("Register name is empty."); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Invalid input"); + return; + } - _api.ReadRegister(registerName); + await ExecuteApiActionAsync(async () => + { + var result = await _api.ReadRegisterAsync(slot, registerName); + + if (!result.Success) + throw new Exception(result.ErrorMessage); + + MessageBox.Show($"Register: {result.RegisterName}\r\nRaw: {result.RawHex}"); }); } - private void btnWriteRegister_Click(object sender, EventArgs e) + private async void btnConnectAll_Click(object sender, EventArgs e) { - ExecuteApiAction(() => + await ExecuteApiActionAsync(async () => { - string registerName = txtRegister.Text; - string valueText = txtValue.Text; + var slots = _api.GetSelectedSlots(); + + if (slots.Count == 0) + throw new Exception("No slots selected."); + + foreach (int slot in slots) + { + var result = await _api.ConnectOneSlotAsync(slot); + + if (!result.Success) + throw new Exception($"Slot {slot}: {result.Message}"); + } + + MessageBox.Show("Selected meters connected."); + }); + } + + private async void btnWriteRegister_Click(object sender, EventArgs e) + { + int slot; + string registerName = txtRegister.Text; + string valueText = txtValue.Text; + + try + { + slot = GetSlotSafe(); if (string.IsNullOrWhiteSpace(registerName)) throw new Exception("Register name is empty."); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Invalid input"); + return; + } + await ExecuteApiActionAsync(async () => + { object value = ParseValue(valueText); - _api.WriteRegister(registerName, value, false, false); + var result = await _api.WriteRegisterAsync( + slot, + registerName, + value, + false, + false); + + if (!result.Success) + throw new Exception(result.ErrorMessage); + + MessageBox.Show($"Write OK: {registerName} = {value}"); }); } - private void btnSetPassword_Click(object sender, EventArgs e) + private async void btnSetPassword_Click(object sender, EventArgs e) { - ExecuteApiAction(() => + int slot; + string password = txtPassword.Text; + + try { - string password = txtPassword.Text; + slot = GetSlotSafe(); if (string.IsNullOrWhiteSpace(password)) throw new Exception("Password is empty."); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Invalid input"); + return; + } - _api.SetMeterPassword(password); + await ExecuteApiActionAsync(async () => + { + bool ok = await _api.SetMeterPasswordAsync(slot, password); + + if (!ok) + throw new Exception("Set password failed."); + + MessageBox.Show("Password set successfully."); }); } + + #endregion } } \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MeterInitView.Designer.cs b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MeterInitView.Designer.cs new file mode 100644 index 000000000..f5c3cea61 --- /dev/null +++ b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MeterInitView.Designer.cs @@ -0,0 +1,49 @@ +namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface +{ + partial class MeterInitView + { + private System.ComponentModel.IContainer components = null; + + private System.Windows.Forms.Button btnReloadSetup; + private System.Windows.Forms.Button btnSaveSetup; + private System.Windows.Forms.Button btnAddSlot; + + protected override void Dispose(bool disposing) + { + if (disposing && components != null) + components.Dispose(); + + base.Dispose(disposing); + } + + private void InitializeComponent() + { + this.btnReloadSetup = new System.Windows.Forms.Button(); + this.btnSaveSetup = new System.Windows.Forms.Button(); + this.btnAddSlot = new System.Windows.Forms.Button(); + + this.SuspendLayout(); + + this.btnReloadSetup.SetBounds(20, 20, 140, 32); + this.btnReloadSetup.Text = "Reload Setup"; + this.btnReloadSetup.Click += new System.EventHandler(this.btnReloadSetup_Click); + + this.btnSaveSetup.SetBounds(170, 20, 140, 32); + this.btnSaveSetup.Text = "Save Setup"; + this.btnSaveSetup.Click += new System.EventHandler(this.btnSaveSetup_Click); + + this.btnAddSlot.SetBounds(20, 65, 290, 32); + this.btnAddSlot.Text = "Add Slot"; + this.btnAddSlot.Click += new System.EventHandler(this.btnAddSlot_Click); + + this.Controls.Add(this.btnReloadSetup); + this.Controls.Add(this.btnSaveSetup); + this.Controls.Add(this.btnAddSlot); + + this.Name = "MeterInitView"; + this.Size = new System.Drawing.Size(340, 130); + + this.ResumeLayout(false); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MeterInitView.cs b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MeterInitView.cs new file mode 100644 index 000000000..022ea43de --- /dev/null +++ b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MeterInitView.cs @@ -0,0 +1,69 @@ +using System; +using System.Windows.Forms; +using GenesisCordonelInterface.API; + +namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface +{ + public partial class MeterInitView : UserControl + { + private readonly InterfaceOutsideToGCI _api; + private readonly Action _addSlotAction; + private readonly Action _saveAction; + + public MeterInitView(InterfaceOutsideToGCI api, Action addSlotAction, Action saveAction) + { + _api = api; + _addSlotAction = addSlotAction; + _saveAction = saveAction; + + InitializeComponent(); + } + + #region BUTTONS + // ---------------------------------------------------- + + private void btnReloadSetup_Click(object sender, EventArgs e) + { + ExecuteApiAction(() => _api.ReloadSlotSetup()); + } + + private void btnSaveSetup_Click(object sender, EventArgs e) + { + _saveAction?.Invoke(); + } + + private void btnAddSlot_Click(object sender, EventArgs e) + { + _addSlotAction?.Invoke(); + } + + // ---------------------------------------------------- + #endregion + + private void ExecuteApiAction(Action action) + { + try + { + SetBusy(true); + action(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "API call failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetBusy(false); + } + } + + private void SetBusy(bool busy) + { + Cursor = busy ? Cursors.WaitCursor : Cursors.Default; + + btnReloadSetup.Enabled = !busy; + btnSaveSetup.Enabled = !busy; + btnAddSlot.Enabled = !busy; + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MetersActionView.Designer.cs b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MetersActionView.Designer.cs new file mode 100644 index 000000000..b14ef8f44 --- /dev/null +++ b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MetersActionView.Designer.cs @@ -0,0 +1,99 @@ +namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface +{ + partial class MetersActionView + { + private System.ComponentModel.IContainer components = null; + + private System.Windows.Forms.Label lblRegister; + private System.Windows.Forms.TextBox txtRegister; + private System.Windows.Forms.Label lblValue; + private System.Windows.Forms.TextBox txtValue; + private System.Windows.Forms.Label lblPassword; + private System.Windows.Forms.TextBox txtPassword; + + private System.Windows.Forms.Button btnConnectSelected; + private System.Windows.Forms.Button btnReadRegister; + private System.Windows.Forms.Button btnWriteRegister; + private System.Windows.Forms.Button btnSetPassword; + private System.Windows.Forms.Button btnDisconnectSelected; + + protected override void Dispose(bool disposing) + { + if (disposing && components != null) + components.Dispose(); + + base.Dispose(disposing); + } + + private void InitializeComponent() + { + this.lblRegister = new System.Windows.Forms.Label(); + this.txtRegister = new System.Windows.Forms.TextBox(); + this.lblValue = new System.Windows.Forms.Label(); + this.txtValue = new System.Windows.Forms.TextBox(); + this.lblPassword = new System.Windows.Forms.Label(); + this.txtPassword = new System.Windows.Forms.TextBox(); + + this.btnConnectSelected = new System.Windows.Forms.Button(); + this.btnReadRegister = new System.Windows.Forms.Button(); + this.btnWriteRegister = new System.Windows.Forms.Button(); + this.btnSetPassword = new System.Windows.Forms.Button(); + this.btnDisconnectSelected = new System.Windows.Forms.Button(); + + this.SuspendLayout(); + + this.lblRegister.SetBounds(20, 20, 100, 20); + this.lblRegister.Text = "Register:"; + this.txtRegister.SetBounds(130, 17, 250, 20); + this.txtRegister.Text = "GENESISFLOW_TriggerTest"; + + this.lblValue.SetBounds(20, 55, 100, 20); + this.lblValue.Text = "Value:"; + this.txtValue.SetBounds(130, 52, 250, 20); + this.txtValue.Text = "1"; + + this.lblPassword.SetBounds(20, 90, 100, 20); + this.lblPassword.Text = "Password:"; + this.txtPassword.SetBounds(130, 87, 250, 20); + this.txtPassword.Text = "1234"; + + this.btnConnectSelected.SetBounds(20, 140, 170, 32); + this.btnConnectSelected.Text = "Connect Selected"; + this.btnConnectSelected.Click += new System.EventHandler(this.btnConnectSelected_Click); + + this.btnDisconnectSelected.SetBounds(210, 140, 170, 32); + this.btnDisconnectSelected.Text = "Disconnect Selected"; + this.btnDisconnectSelected.Click += new System.EventHandler(this.btnDisconnectSelected_Click); + + this.btnReadRegister.SetBounds(20, 185, 170, 32); + this.btnReadRegister.Text = "Read Register"; + this.btnReadRegister.Click += new System.EventHandler(this.btnReadRegister_Click); + + this.btnWriteRegister.SetBounds(210, 185, 170, 32); + this.btnWriteRegister.Text = "Write Register"; + this.btnWriteRegister.Click += new System.EventHandler(this.btnWriteRegister_Click); + + this.btnSetPassword.SetBounds(20, 230, 170, 32); + this.btnSetPassword.Text = "Set Password"; + this.btnSetPassword.Click += new System.EventHandler(this.btnSetPassword_Click); + + this.Controls.Add(this.lblRegister); + this.Controls.Add(this.txtRegister); + this.Controls.Add(this.lblValue); + this.Controls.Add(this.txtValue); + this.Controls.Add(this.lblPassword); + this.Controls.Add(this.txtPassword); + this.Controls.Add(this.btnConnectSelected); + this.Controls.Add(this.btnDisconnectSelected); + this.Controls.Add(this.btnReadRegister); + this.Controls.Add(this.btnWriteRegister); + this.Controls.Add(this.btnSetPassword); + + this.Name = "MetersActionView"; + this.Size = new System.Drawing.Size(420, 320); + + this.ResumeLayout(false); + this.PerformLayout(); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MetersActionView.cs b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MetersActionView.cs new file mode 100644 index 000000000..a3ff9046b --- /dev/null +++ b/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/MetersActionView.cs @@ -0,0 +1,180 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Forms; +using GenesisCordonelInterface.API; + +namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface +{ + public partial class MetersActionView : UserControl + { + private readonly InterfaceOutsideToGCI _api; + + public MetersActionView(InterfaceOutsideToGCI api) + { + _api = api ?? throw new ArgumentNullException(nameof(api)); + InitializeComponent(); + } + + private async Task ExecuteApiActionAsync(Func action) + { + try + { + SetBusy(true); + await action(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "API call failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetBusy(false); + } + } + + private void SetBusy(bool busy) + { + Cursor = busy ? Cursors.WaitCursor : Cursors.Default; + + btnConnectSelected.Enabled = !busy; + btnReadRegister.Enabled = !busy; + btnWriteRegister.Enabled = !busy; + btnSetPassword.Enabled = !busy; + btnDisconnectSelected.Enabled = !busy; + } + + private object ParseValue(string input) + { + if (int.TryParse(input, out int i)) return i; + if (uint.TryParse(input, out uint ui)) return ui; + if (bool.TryParse(input, out bool b)) return b; + + return input; + } + + private void ValidateSelectedSlots() + { + if (_api.GetSelectedSlots().Count == 0) + throw new Exception("No slots selected."); + } + + private async void btnConnectSelected_Click(object sender, EventArgs e) + { + await ExecuteApiActionAsync(async () => + { + ValidateSelectedSlots(); + + foreach (int slot in _api.GetSelectedSlots()) + { + var result = await _api.ConnectOneSlotAsync(slot); + + if (!result.Success) + throw new Exception($"Slot {slot}: {result.Message}"); + } + + MessageBox.Show("Selected meters connected."); + }); + } + + private async void btnReadRegister_Click(object sender, EventArgs e) + { + string registerName = txtRegister.Text; + + if (string.IsNullOrWhiteSpace(registerName)) + { + MessageBox.Show("Register name is empty.", "Invalid input"); + return; + } + + await ExecuteApiActionAsync(async () => + { + ValidateSelectedSlots(); + + foreach (int slot in _api.GetSelectedSlots()) + { + var result = await _api.ReadRegisterAsync(slot, registerName); + + if (!result.Success) + throw new Exception($"Slot {slot}: {result.ErrorMessage}"); + } + + MessageBox.Show("Read register finished."); + }); + } + + private async void btnWriteRegister_Click(object sender, EventArgs e) + { + string registerName = txtRegister.Text; + string valueText = txtValue.Text; + + if (string.IsNullOrWhiteSpace(registerName)) + { + MessageBox.Show("Register name is empty.", "Invalid input"); + return; + } + + await ExecuteApiActionAsync(async () => + { + ValidateSelectedSlots(); + + object value = ParseValue(valueText); + + foreach (int slot in _api.GetSelectedSlots()) + { + var result = await _api.WriteRegisterAsync( + slot, + registerName, + value, + false, + false); + + if (!result.Success) + throw new Exception($"Slot {slot}: {result.ErrorMessage}"); + } + + MessageBox.Show("Write register finished."); + }); + } + + private async void btnSetPassword_Click(object sender, EventArgs e) + { + string password = txtPassword.Text; + + if (string.IsNullOrWhiteSpace(password)) + { + MessageBox.Show("Password is empty.", "Invalid input"); + return; + } + + await ExecuteApiActionAsync(async () => + { + ValidateSelectedSlots(); + + foreach (int slot in _api.GetSelectedSlots()) + { + bool ok = await _api.SetMeterPasswordAsync(slot, password); + + if (!ok) + throw new Exception($"Slot {slot}: Set password failed."); + } + + MessageBox.Show("Password set for selected meters."); + }); + } + + private async void btnDisconnectSelected_Click(object sender, EventArgs e) + { + await ExecuteApiActionAsync(async () => + { + ValidateSelectedSlots(); + + foreach (int slot in _api.GetSelectedSlots()) + { + await _api.DisconnectAsync(slot); + } + + MessageBox.Show("Selected meters disconnected."); + }); + } + } +} \ No newline at end of file diff --git a/TBF/Build/CopyGci.targets.xml b/TBF/Build/CopyGci.targets.xml new file mode 100644 index 000000000..9eb4197ee --- /dev/null +++ b/TBF/Build/CopyGci.targets.xml @@ -0,0 +1,35 @@ + + + + + + + + $(ProjectDir)..\GenesisCordonelInterface\RuntimePackage\Package\ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TBF/Properties/Settings.Designer.cs b/TBF/Properties/Settings.Designer.cs index 1fe8da8ab..abf1d73a2 100644 --- a/TBF/Properties/Settings.Designer.cs +++ b/TBF/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace TBF.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs b/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs index 60f5dfddc..7aed00bb4 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs @@ -1,14 +1,18 @@ -/// +using GenesisCordonelInterface.API; +using log4net; +/// /// Copyright (c) 2015-2021 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; -using log4net; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; using TBF.Rig.Generic; +using GciGUIType = GenesisCordonelInterface.UI.MainView; +using GciType = GenesisCordonelInterface.API.InterfaceOutsideToGCI; using UdsReaderType = TBF.Rig.Input.DataStorage.UniDataStorageReader.Reader; using UdsWriterType = TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writer; -using ExternalInterfaceType = GenesisCordonelInterface.API.InterfaceOutsideToGCI; -using ExternalInterfaceGUIType = GenesisCordonelInterface.UI.MainForm; namespace TBF.Rig.BridgeComponents.GciBridge { @@ -19,37 +23,37 @@ namespace TBF.Rig.BridgeComponents.GciBridge public class GciBridge : ComponentBase { private static readonly ILog log = LogManager.GetLogger(typeof(GciBridge)); - public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } + + public override string ToString() + { + return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); + } readonly GciBridgeCfg gciBridgeCfg; - /// - /// Linked UniDataStorage reader component. - /// readonly UdsReaderType reader; - - /// - /// Linked UniDataStorage writer component. - /// readonly UdsWriterType writer; - /// - /// Placeholder for GCI GUI entry point. - /// Replace object with real GCI MainForm type later. - /// - ExternalInterfaceGUIType gciGUI; + GciGUIType gciGUI; + Form gciGuiHostForm; - /// - /// Placeholder for GCI external/public interface. - /// Replace object with real GCI interface type later. - /// - ExternalInterfaceType gciExternalInterface; + GciType gciExternalInterface; public bool HasReader { get { return reader != null; } } public bool HasWriter { get { return writer != null; } } public bool IsGuiInitialized { get { return gciGUI != null; } } public bool IsExternalInitialized { get { return gciExternalInterface != null; } } + public UdsReaderType GetReader() + { + return reader; + } + + public UdsWriterType GetWriter() + { + return writer; + } + public GciBridge() { } public GciBridge(IComponentCfg cfg, IList components) @@ -91,36 +95,40 @@ namespace TBF.Rig.BridgeComponents.GciBridge log.FatalFormat("{0} initialized: {1}", Name, this); } - /// - /// Initializes access to GCI GUI. - /// Replace placeholder implementation with real MainForm creation. - /// void TryInitializeGui() { try { - using (gciGUI = new ExternalInterfaceGUIType()) - { - gciGUI.ShowDialog(/*this*/); - } + if (gciGuiHostForm != null && !gciGuiHostForm.IsDisposed) + return; - log.InfoFormat("{0}: GCI GUI initialized.", Name); + gciGUI = new GciGUIType(); + gciGUI.Dock = DockStyle.Fill; + + gciGuiHostForm = new Form(); + gciGuiHostForm.Text = "Genesis Cordonel Interface"; + gciGuiHostForm.Width = 1300; + gciGuiHostForm.Height = 600; + gciGuiHostForm.StartPosition = FormStartPosition.CenterScreen; + + gciGuiHostForm.Controls.Add(gciGUI); + + log.InfoFormat("{0}: GCI GUI view initialized.", Name); } catch (Exception ex) { - log.Error("Failed to initialize GCI GUI.", ex); + log.Error("Failed to initialize GCI GUI view.", ex); } } - /// - /// Initializes access to GCI external/public interface. - /// Replace placeholder implementation with real interface creation. - /// void TryInitializeExternalInterface() { try { - gciExternalInterface = new ExternalInterfaceType(); + if (gciExternalInterface != null) + return; + + gciExternalInterface = new GciType(); log.InfoFormat("{0}: GCI external interface initialized.", Name); } @@ -130,80 +138,147 @@ namespace TBF.Rig.BridgeComponents.GciBridge } } - /// - /// Shows GCI GUI if GUI access is enabled. - /// public void ShowGui() { if (!gciBridgeCfg.EnableGuiAccess) return; - if (!IsGuiInitialized) + if (gciGuiHostForm == null || gciGuiHostForm.IsDisposed) { TryInitializeGui(); } + if (gciGuiHostForm == null) return; + + gciGuiHostForm.Show(); + gciGuiHostForm.BringToFront(); + log.InfoFormat("{0}: ShowGui invoked.", Name); } - /// - /// Hides GCI GUI if GUI access is enabled. - /// public void HideGui() { if (!gciBridgeCfg.EnableGuiAccess) return; - if (!IsGuiInitialized) return; + if (gciGuiHostForm == null || gciGuiHostForm.IsDisposed) return; + + gciGuiHostForm.Hide(); - using (gciGUI) - { - gciGUI.Hide(/*this*/); - } log.InfoFormat("{0}: HideGui invoked.", Name); } - /// - /// Connects the external GCI interface. - /// - public void ConnectExternal() + void EnsureExternalInterface() { - if (!gciBridgeCfg.EnableExternalAccess) return; + if (!gciBridgeCfg.EnableExternalAccess) + throw new Exception("GCI external access is disabled."); if (!IsExternalInitialized) - { TryInitializeExternalInterface(); - } - /// TODO: - /// Replace with real external interface connect call. - log.InfoFormat("{0}: ConnectExternal invoked.", Name); + if (gciExternalInterface == null) + throw new Exception("GCI external interface is not initialized."); } - /// - /// Disconnects the external GCI interface. - /// - public void DisconnectExternal() + void EnsureReader() { - if (!gciBridgeCfg.EnableExternalAccess) return; - if (!IsExternalInitialized) return; - - /// TODO: - /// Replace with real external interface disconnect call. - log.InfoFormat("{0}: DisconnectExternal invoked.", Name); + if (reader == null) + throw new Exception("UniDataStorageReader is not linked to GciBridge."); } - /// - /// Returns linked reader component. - /// - public UdsReaderType GetReader() + // API: + #region ======================================= GCI Public Interface ======================================= + + public async Task InitSlotAsync(GciPublicModels.GciInitSlotRequest request) { - return reader; + EnsureExternalInterface(); + + if (request == null) + throw new ArgumentNullException("request"); + + if (request.SlotId <= 0) + throw new ArgumentException("Invalid slot id."); + + GciPublicModels.GciInitSlotResult result = + await gciExternalInterface.InitSlotAsync(request); + + log.InfoFormat("{0}: InitSlotAsync invoked. {1}, Result={2}", Name, request, result); + + return result; } - /// - /// Returns linked writer component. - /// - public UdsWriterType GetWriter() + public async Task GetSlotAsync(int slotId) { - return writer; + EnsureExternalInterface(); + + if (slotId <= 0) + throw new ArgumentException("Invalid slot id."); + + GciPublicModels.GciSlotInfo result = + await gciExternalInterface.GetSlotAsync(slotId); + + log.InfoFormat("{0}: GetSlotAsync({1}) invoked. Result={2}", Name, slotId, result); + + return result; } + + public async Task CleanSlotsAsync() + { + EnsureExternalInterface(); + + GciPublicModels.GciCleanSlotsResult result = + await gciExternalInterface.CleanSlotsAsync(); + + log.InfoFormat("{0}: CleanSlotsAsync invoked.", Name); + + return result; + } + + public async Task GetPcbIdAsync( + int slotId, + CancellationToken token = default) + { + EnsureExternalInterface(); + + if (slotId <= 0) + throw new ArgumentException("Invalid slot id."); + + var result = await gciExternalInterface.GetPcbIdAsync(slotId, token); + + log.InfoFormat("{0}: GetPcbIdAsync({1}) invoked. Result={2}", Name, slotId, result); + + return result; + } + + public async Task ConnectAsync( + int slotId, + CancellationToken token = default) + { + EnsureExternalInterface(); + + if (slotId <= 0) + throw new ArgumentException("Invalid slot id."); + + var result = await gciExternalInterface.ConnectOneSlotAsync(slotId, token); + + log.InfoFormat("{0}: ConnectAsync({1}) invoked. Result={2}", Name, slotId, result); + + return result; + } + + public async Task DisconnectAsync( + int slotId, + CancellationToken token = default) + { + EnsureExternalInterface(); + + if (slotId <= 0) + throw new ArgumentException("Invalid slot id."); + + var result = await gciExternalInterface.DisconnectAsync(slotId, token); + + log.InfoFormat("{0}: DisconnectAsync({1}) invoked. Result={2}", Name, slotId, result); + + return result; + } + + #endregion } } \ No newline at end of file diff --git a/TBF/Rig/BridgeComponents/GciBridge/GciBridgeCfgCtrl.cs b/TBF/Rig/BridgeComponents/GciBridge/GciBridgeCfgCtrl.cs index 68eb84bd2..ab7787721 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/GciBridgeCfgCtrl.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/GciBridgeCfgCtrl.cs @@ -172,12 +172,42 @@ namespace TBF.Rig.BridgeComponents.GciBridge private void connectExternalButton_Click(object sender, EventArgs e) { - /// TODO + try + { + GciBridge bridge = TbfComponents.FindComponent(config.Name) as GciBridge; + + if (bridge == null) + { + MessageBox.Show("GciBridge component was not found.", "GCI Bridge"); + return; + } + + //bridge.ConnectExternal(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "GCI Bridge error"); + } } private void showGuiButton_Click(object sender, EventArgs e) { - /// TODO + try + { + GciBridge bridge = TbfComponents.FindComponent(config.Name) as GciBridge; + + if (bridge == null) + { + MessageBox.Show("GciBridge component was not found.", "GCI Bridge"); + return; + } + + bridge.ShowGui(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "GCI Bridge error"); + } } } } \ No newline at end of file diff --git a/TBF/Rig/BridgeComponents/GciBridge/GciBridgeOp.cs b/TBF/Rig/BridgeComponents/GciBridge/GciBridgeOp.cs new file mode 100644 index 000000000..a318dc1b6 --- /dev/null +++ b/TBF/Rig/BridgeComponents/GciBridge/GciBridgeOp.cs @@ -0,0 +1,14 @@ +using log4net; +/// +/// Copyright (c) 2015-2021 Sensus Slovensko a.s. +/// +using System; +using TBF.Rig.Sequences; + +namespace TBF.Rig.BridgeComponents.GciBridge +{ + public class GciBridgeOp + { + + } +} \ No newline at end of file diff --git a/TBF/Rig/TestMethods/GenesisCommunication/GenesisHead/GenesisHead.cs b/TBF/Rig/TestMethods/GenesisCommunication/GenesisHead/GenesisHead.cs index 332c55b91..e3222174a 100644 --- a/TBF/Rig/TestMethods/GenesisCommunication/GenesisHead/GenesisHead.cs +++ b/TBF/Rig/TestMethods/GenesisCommunication/GenesisHead/GenesisHead.cs @@ -13,6 +13,7 @@ using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore; using Xylem.Common.Hardware.WaterMeter.WaterMeterCore; using Xylem.Common.Metrology.Measurements; using Xylem.Common.Metrology.Measurements.Consts; +using Xylem.Common.Hardware.Interfaces.Ports.PortCore; using Common; using System.IO.Ports; @@ -261,12 +262,12 @@ namespace TBF.Rig.TestMethods.GenesisCommunication.GenesisHead //myGenesis.SetLogger(); - private, but called into basic constructor! myGenesis.SetupGenesisMeter( SlotNr, - new Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig.PortConfig() + new /*Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig.*/PortConfig() { Type = "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.IrdaSerialPort", PortName =$"COM{HeadComPortNr}" }, - new Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig.PortConfig() + new /*Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig.*/PortConfig() { Type = "", PortName = $"COM{OptoComPortNr}" @@ -536,7 +537,7 @@ namespace TBF.Rig.TestMethods.GenesisCommunication.GenesisHead { - myGenesis.BuildAndCheckCalibFactorsAllChannels(refVol.Value / 1000, testTimeS, Q2ErrWOCorrection); + myGenesis.BuildAndCheckCalibFactorsAllChannels(refVol.Value / 1000, testTimeS, Q2ErrWOCorrection, 0.0, (int?)null); myGenesis.SetCalibFactorsAllChannels(false); } diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 5e5042d97..c08787237 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -10,7 +10,7 @@ Properties TBF TBF - v4.7.2 + v4.8 2.0 @@ -173,6 +173,10 @@ ..\packages\Common\Xylem.Common.CommonCore.dll + + False + ..\packages\Common\Xylem.Common.Hardware.Interfaces.Ports.PortCore.dll + ..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig.dll @@ -211,6 +215,7 @@ GciBridgeCfgCtrl.cs + @@ -4185,6 +4190,7 @@ TestProgressCtrl.cs + Always @@ -4402,6 +4408,7 @@ Results +