diff --git a/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs b/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs index 6a2d05fd2..85db1aebd 100644 --- a/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs +++ b/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs @@ -1,5 +1,6 @@ using GenesisCordonelInterface.Core.Threading; using NLog; +using NLog.Fluent; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -9,6 +10,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using System.Xml.Linq; using Xylem.Common.Hardware.Interfaces.Ports.PortCore; using Xylem.Common.Hardware.Interfaces.Ports.PortCore.EventArguments; using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore; @@ -30,6 +32,10 @@ namespace GenesisCordonelInterface.API private readonly MeterBatch _meterBatch = new MeterBatch(); + // Protects all access to _meterBatch.ListOfMeters + private readonly object _meterBatchLock = new object(); + private static readonly object _setupGenesisMeterLock = new object(); + private readonly ConcurrentDictionary _workers = new ConcurrentDictionary(); private readonly ConcurrentDictionary _selectedSlots = new ConcurrentDictionary(); @@ -58,7 +64,7 @@ namespace GenesisCordonelInterface.API { Slot = x.Key, Name = x.Value.Name, - QueueLength = x.Value.IsDisposed ? 0 : x.Value.QueueLength, + QueueLength = /*x.Value.IsDisposed ? 0 : */x.Value.QueueLength, IsBusy = !x.Value.IsDisposed && x.Value.IsBusy, CurrentOperation = x.Value.IsDisposed ? null : x.Value.CurrentOperation, LastError = x.Value.LastError, @@ -72,8 +78,17 @@ namespace GenesisCordonelInterface.API public List GetMeterBatchDebugStatuses() { - return _meterBatch.ListOfMeters - .OfType() + List meters; + + //just snapshot of list under lock + lock (_meterBatchLock) + { + meters = _meterBatch.ListOfMeters + .OfType() + .ToList(); + } + + return meters .Select(m => new MeterBatchDebugStatus { Slot = m.Slot, @@ -81,13 +96,19 @@ namespace GenesisCordonelInterface.API PcbId = m.PcbId, IsConnected = m.IsConnected, IsLoggedOn = m.IsLoggedOn, - RequestPort = m.RequestPort?.GetPortName(), - StreamingPort = m.StreamingPort?.GetPortName(), - RequestPortType = - m.RequestPort is Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.RfidSerialPort ? "RFID" : - m.RequestPort is Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.UartSerialPort ? "UART" : - m.RequestPort is Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.IrdaSerialPort ? "IRDA" : - null, + + RequestPort = m.requestPortConfig.HasValue + ? m.requestPortConfig.Value.PortName + : "", + + StreamingPort = m.streamingPortConfig.HasValue + ? m.streamingPortConfig.Value.PortName + : "", + + RequestPortType = m.requestPortConfig.HasValue + ? m.requestPortConfig.Value.Type + : "", + FwVersion = m.FwVersion, InterfaceVersion = m.InterfaceInfo?.InterfaceVersion }) @@ -126,16 +147,57 @@ namespace GenesisCordonelInterface.API #region ================================== Helpers ================================== - private GenesisMeter GetMeter(int slot) + private GenesisMeter GetMeterThreadSafe(int slot) { - var meter = _meterBatch.ListOfMeters - .OfType() - .FirstOrDefault(m => m.Slot == slot); + lock (_meterBatchLock) + { + return _meterBatch.ListOfMeters + .OfType() + .FirstOrDefault(m => m.Slot == slot); + } + } - //if (meter == null) - // throw new InvalidOperationException($"Meter for slot {slot} not initialized."); + private bool RemoveMeterThreadSafe(GenesisMeter meter) + { + if (meter == null) + return false; - return meter; + lock (_meterBatchLock) + { + return _meterBatch.ListOfMeters.Remove(meter); + } + } + + private void RemoveMetersThreadSafe() + { + lock (_meterBatchLock) + { + _meterBatch.ListOfMeters.Clear(); + } + } + + private void RemoveWorkersThreadSafe() + { + foreach (var pair in _workers.ToList()) + { + ApiWorker worker; + + if (_workers.TryRemove(pair.Key, out worker)) + { + worker.Dispose(); + } + } + } + + private void DisposeMetersThreadSafe() + { + lock (_meterBatchLock) + { + foreach (var meter in _meterBatch.ListOfMeters.OfType()) + { + meter.DisposeMeter(); + } + } } private void EnsureConnected(GenesisMeter meter) @@ -201,7 +263,7 @@ namespace GenesisCordonelInterface.API req.HasValue ? req.Value.PortName : "NA", str.HasValue ? str.Value.PortName : "NA")); - var existingMeter = GetMeter(slot); + var existingMeter = GetMeterThreadSafe(slot); if (existingMeter != null) { @@ -253,7 +315,7 @@ namespace GenesisCordonelInterface.API req.HasValue ? req.Value.PortName : "NA", str.HasValue ? str.Value.PortName : "NA")); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); if (meter == null) { @@ -293,7 +355,10 @@ namespace GenesisCordonelInterface.API meter.streamingPortConfig = str; //meter.SetupGenesisMeter(slot, req, str, true); - _meterBatch.AddMeter2(meter); + lock (_meterBatchLock) + { + _meterBatch.AddMeter2(meter); + } return meter; } @@ -394,7 +459,7 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, $"Start. Slot={slot}"); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); if (meter == null) { @@ -424,8 +489,8 @@ namespace GenesisCordonelInterface.API ConfigSource = ModelsMapping.MapConfigSourceBack(meter.useConfigSource), PasswordSource = ModelsMapping.MapPasswordSourceBack(meter.usePasswordSource), - RequestPort = ModelsMapping.MapPortBack(meter.RequestPort), - StreamingPort = ModelsMapping.MapPortBack(meter.StreamingPort) + RequestPort = ModelsMapping.MapPortBack(meter.requestPortConfig), + StreamingPort = ModelsMapping.MapPortBack(meter.streamingPortConfig) }; } catch (Exception ex) @@ -458,28 +523,31 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, "Start."); - var result = new PublicModels.GciAllSlotsInfo + lock (_meterBatchLock) { - Success = true, - Message = "Slots read.", - Slots = _meterBatch.ListOfMeters - .OfType() - .Select(meter => new PublicModels.GciSlotInfo - { - SlotId = meter.Slot, - Success = true, - Exists = true, - Message = "Slot found.", - PcbId = meter.PcbId, - ConfigSource = ModelsMapping.MapConfigSourceBack(meter.useConfigSource), - PasswordSource = ModelsMapping.MapPasswordSourceBack(meter.usePasswordSource), - RequestPort = ModelsMapping.MapPortBack(meter.RequestPort), - StreamingPort = ModelsMapping.MapPortBack(meter.StreamingPort) - }) - .ToList() - }; + var result = new PublicModels.GciAllSlotsInfo + { + Success = true, + Message = "Slots read.", + Slots = _meterBatch.ListOfMeters + .OfType() + .Select(meter => new PublicModels.GciSlotInfo + { + SlotId = meter.Slot, + Success = true, + Exists = true, + Message = "Slot found.", + PcbId = meter.PcbId, + ConfigSource = ModelsMapping.MapConfigSourceBack(meter.useConfigSource), + PasswordSource = ModelsMapping.MapPasswordSourceBack(meter.usePasswordSource), + RequestPort = ModelsMapping.MapPortBack(meter.requestPortConfig), + StreamingPort = ModelsMapping.MapPortBack(meter.streamingPortConfig) + }) + .ToList() + }; - return result; + return result; + } } catch (Exception ex) { @@ -512,12 +580,12 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, string.Format("Start. Slot={0}", slot)); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); if (meter != null) { meter.DisposeMeter(); - _meterBatch.ListOfMeters.Remove(meter); + RemoveMeterThreadSafe(meter); } ApiWorker worker; @@ -561,22 +629,9 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, "Start."); - foreach (var meter in _meterBatch.ListOfMeters) - { - meter.DisposeMeter(); - } - - _meterBatch.ListOfMeters.Clear(); - - foreach (var pair in _workers.ToList()) - { - ApiWorker worker; - - if (_workers.TryRemove(pair.Key, out worker)) - { - worker.Dispose(); - } - } + DisposeMetersThreadSafe(); + RemoveMetersThreadSafe(); + RemoveWorkersThreadSafe(); return new PublicModels.GciCleanAllSlotsResult { @@ -720,7 +775,7 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, $"Start. Slot={slot}"); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); if (!meter.IsLoggedOn) { @@ -794,7 +849,7 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, $"Start. Slot={slot}"); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); if (!meter.IsConnected) { @@ -885,27 +940,42 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, $"Start. Slot={slot}"); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); meter.Logout(); meter.Disconnect(); - //meter.Dispose(); - //meter.SoftDispose(); - LogInfo(operation, $"Success. Slot={slot}"); + bool success = !meter.IsConnected && !meter.IsLoggedOn; + + if (success) + { + LogInfo(operation, $"Success. Slot={slot}"); + } + else + { + LogInfo( + operation, + $"Disconnect incomplete. Slot={slot}, IsConnected={meter.IsConnected}, IsLoggedOn={meter.IsLoggedOn}"); + } + + //if (!meter.IsConnected && meter.IsLoggedOn) + // meter.ConnectMeter(); // repair of this situation return new PublicModels.GciDisconnectResult { SlotId = slot, - Success = true, + Success = success, Message = string.Join( - " | ", - new[] - { - meter.LastLogoutStatus, - meter.LastDisposeStatus - } - .Where(x => !string.IsNullOrWhiteSpace(x))) + " | ", + new[] + { + meter.LastLogoutStatus, + meter.LastDisposeStatus, + success + ? null + : $"Disconnect state invalid. IsConnected={meter.IsConnected}, IsLoggedOn={meter.IsLoggedOn}" + } + .Where(x => !string.IsNullOrWhiteSpace(x))) }; } catch (Exception ex) @@ -924,6 +994,7 @@ namespace GenesisCordonelInterface.API if (_workers.TryRemove(slot, out var worker)) { worker.Dispose(); + LogInfo(operation, $"Worker disposed. Slot={slot}"); } } @@ -948,7 +1019,7 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, $"Start. Slot={slot}"); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); //meter.Logout(); @@ -1001,14 +1072,16 @@ namespace GenesisCordonelInterface.API { LogInfo(operation, $"Start. Slot={slot}, Register={name}"); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); EnsureConnected(meter); var raw = meter.ReadRegister(name); + bool success = raw != null && raw.Length > 0; + var result = new RegisterReadResult { - Success = true, + Success = success, RegisterName = name, RawHex = ToHex(raw) }; @@ -1040,12 +1113,9 @@ namespace GenesisCordonelInterface.API object value, bool storeToDevice = false, bool refreshSystemState = false, - CancellationToken token = default(CancellationToken)) + CancellationToken token = default) { - return GetWorker(slot).RunAsync( - () => WriteRegister(slot, registerName, value, storeToDevice, refreshSystemState), - token, - "WriteRegister"); + return GetWorker(slot).RunAsync(() => WriteRegister(slot, registerName, value, storeToDevice, refreshSystemState), token); } public RegisterWriteResult WriteRegister( @@ -1063,7 +1133,7 @@ namespace GenesisCordonelInterface.API $"Start. Slot={slot}, Register={registerName}, Value={value}, " + $"StoreToDevice={storeToDevice}, RefreshSystemState={refreshSystemState}"); - var meter = GetMeter(slot); + var meter = GetMeterThreadSafe(slot); EnsureConnected(meter); if (string.IsNullOrWhiteSpace(registerName)) @@ -1174,7 +1244,7 @@ namespace GenesisCordonelInterface.API LogInfo(operation, $"Start. Slot={slot}"); - GenesisMeter meter = GetMeter(slot); + GenesisMeter meter = GetMeterThreadSafe(slot); meter?.SetPassword(password);//SetupFromExternConfig(); diff --git a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs index ffbfa7635..3e57351e8 100644 --- a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs +++ b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs @@ -58,7 +58,7 @@ namespace GenesisCordonelInterface.API #endregion - #region ================================== INIT ================================== + #region ================================== INIT/UPDATE/GET slot ================================== public async Task InitSlotAsync( GciInitSlotRequest request, @@ -73,7 +73,7 @@ namespace GenesisCordonelInterface.API ModelsMapping.MapPasswordSource(request.PasswordSource), ModelsMapping.MapPort(request.RequestPort), ModelsMapping.MapPort(request.StreamingPort), - token); + token).ConfigureAwait(false); //RaiseMeterBatchStatusChanged(); @@ -93,7 +93,7 @@ namespace GenesisCordonelInterface.API ModelsMapping.MapPasswordSource(request.PasswordSource), ModelsMapping.MapPort(request.RequestPort), ModelsMapping.MapPort(request.StreamingPort), - token); + token).ConfigureAwait(false); //RaiseMeterBatchStatusChanged(); @@ -107,7 +107,7 @@ namespace GenesisCordonelInterface.API if (slotId <= 0) throw new ArgumentException("Invalid slot id."); - var result = await _innerMeterAPI.GetOneMeterInfo(slotId, token); + var result = await _innerMeterAPI.GetOneMeterInfo(slotId, token).ConfigureAwait(false); return result; } @@ -115,7 +115,7 @@ namespace GenesisCordonelInterface.API public async Task GetAllSlotsAsync( CancellationToken token = default) { - var result = await _innerMeterAPI.GetAllMetersInfo(token); + var result = await _innerMeterAPI.GetAllMetersInfo(token).ConfigureAwait(false); return result; } @@ -127,7 +127,7 @@ namespace GenesisCordonelInterface.API if (slot <= 0) throw new ArgumentException("Invalid slot id.", nameof(slot)); - var result = await _innerMeterAPI.CleanSlotAsync(slot, token); + var result = await _innerMeterAPI.CleanSlotAsync(slot, token).ConfigureAwait(false); //RaiseMeterBatchStatusChanged(); @@ -137,7 +137,7 @@ namespace GenesisCordonelInterface.API public async Task CleanAllSlotsAsync( CancellationToken token = default) { - var result = await _innerMeterAPI.CleanAllSlotsAsync(token); + var result = await _innerMeterAPI.CleanAllSlotsAsync(token).ConfigureAwait(false); //RaiseMeterBatchStatusChanged(); @@ -157,9 +157,9 @@ namespace GenesisCordonelInterface.API if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Password is empty."); - GciSetPasswordResult result = await _innerMeterAPI.SetMeterPasswordAsync(slot, password, token); + var result = await _innerMeterAPI.SetMeterPasswordAsync(slot, password, token).ConfigureAwait(false); - RaiseMeterBatchStatusChanged(); + //RaiseMeterBatchStatusChanged(); return result; } @@ -167,11 +167,17 @@ namespace GenesisCordonelInterface.API #endregion #region ================================== LOGIN ================================== - public Task LoginOneSlotAsync( + public async Task LoginOneSlotAsync( int slot, CancellationToken token = default) { - return _innerMeterAPI.LoginOneSlotAsync(slot, token); + if (slot <= 0) + throw new ArgumentException("Invalid slot id."); + + var result = await _innerMeterAPI.LoginOneSlotAsync(slot, token).ConfigureAwait(false); + return result; + + //RaiseMeterBatchStatusChanged(); } #endregion @@ -181,7 +187,10 @@ namespace GenesisCordonelInterface.API int slot, CancellationToken token = default) { - GciConnectResult result = await _innerMeterAPI.ConnectOneSlotAsync(slot, token); + if (slot <= 0) + throw new ArgumentException("Invalid slot id."); + + var result = await _innerMeterAPI.ConnectOneSlotAsync(slot, token).ConfigureAwait(false); //RaiseMeterBatchStatusChanged(); @@ -195,7 +204,7 @@ namespace GenesisCordonelInterface.API if (slot <= 0) throw new ArgumentException("Invalid slot id."); - var result = await _innerMeterAPI.DisconnectAsync(slot, token); + var result = await _innerMeterAPI.DisconnectAsync(slot, token).ConfigureAwait(false); //RaiseMeterBatchStatusChanged(); @@ -211,93 +220,92 @@ namespace GenesisCordonelInterface.API if (slot <= 0) throw new ArgumentException("Invalid slot id."); - var result = await _innerMeterAPI.GetPcbIdAsync(slot, token); + var result = await _innerMeterAPI.GetPcbIdAsync(slot, token).ConfigureAwait(false); return result; } #endregion #region ================================== READ ================================== - // ---------------------------------------------------- - public RegisterReadResult ReadRegister( - int slot, - string registerName) - { - return _innerMeterAPI.ReadRegister(slot, registerName); - } - - public Task ReadRegisterAsync( + public async Task ReadRegisterAsync( int slot, string registerName, - CancellationToken token = default(CancellationToken)) + CancellationToken token = default) { - return _innerMeterAPI.ReadRegisterAsync(slot, registerName, token); + if (slot <= 0) + throw new ArgumentException("Invalid slot id.", nameof(slot)); + + if (string.IsNullOrWhiteSpace(registerName)) + throw new ArgumentException("Register name is empty.", nameof(registerName)); + + var result = await _innerMeterAPI + .ReadRegisterAsync(slot, registerName, token) + .ConfigureAwait(false); + + //RaiseMeterBatchStatusChanged(); + + return result; } - // ---------------------------------------------------- #endregion #region ================================== WRITE ================================== - // ---------------------------------------------------- public async Task WriteRegisterAsync( + int slot, + string registerName, + object value, + bool storeToDevice = false, + bool refreshSystemState = false, + CancellationToken token = default) + { + if (slot <= 0) + throw new ArgumentException("Invalid slot id.", nameof(slot)); + + if (string.IsNullOrWhiteSpace(registerName)) + throw new ArgumentException("Register name is empty.", nameof(registerName)); + + var result = await _innerMeterAPI + .WriteRegisterAsync( + slot, + registerName, + value, + storeToDevice, + refreshSystemState, + token) + .ConfigureAwait(false); + + //RaiseMeterBatchStatusChanged(); + + return result; + } + #endregion + + #region ================================== Password ================================== + + public async Task SetMeterPasswordAsync( int slot, - string registerName, - object value, - bool storeToDevice = false, - bool refreshSystemState = false, + string password, CancellationToken token = default) { - var result = await _innerMeterAPI.WriteRegisterAsync( - slot, - registerName, - value, - storeToDevice, - refreshSystemState, - token); + if (slot <= 0) + throw new ArgumentException("Invalid slot id.", nameof(slot)); + + if (string.IsNullOrWhiteSpace(password)) + throw new ArgumentException("Password is empty.", nameof(password)); + + var result = await _innerMeterAPI + .SetMeterPasswordAsync(slot, password, token) + .ConfigureAwait(false); //RaiseMeterBatchStatusChanged(); + return result; } - - public RegisterWriteResult WriteRegister( - int slot, - string registerName, - object value, - bool storeToDevice = false, - bool refreshSystemState = false) - { - 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 GciSetPasswordResult SetMeterPassword(int slot, string password) - { - var result = _innerMeterAPI.SetMeterPassword(slot, password); - //RaiseMeterBatchStatusChanged(); - return result; - } - - // ---------------------------------------------------- #endregion #region ================================== DEBUG STATUS ================================== - // ---------------------------------------------------- public List GetWorkerDebugStatuses() { diff --git a/GenesisCordonelInterface/API/ModelsMapping.cs b/GenesisCordonelInterface/API/ModelsMapping.cs index 2821674df..41ef3b33f 100644 --- a/GenesisCordonelInterface/API/ModelsMapping.cs +++ b/GenesisCordonelInterface/API/ModelsMapping.cs @@ -126,6 +126,15 @@ namespace GenesisCordonelInterface.API }; } + public static PublicModels.GciPortConfig MapPortBack(string portName, string portTyoe) + { + return new PublicModels.GciPortConfig + { + PortName = portName, + Type = portTyoe + }; + } + public static MeterBatchDebugStatus ToMeterBatchDebugStatus(GciSlotInfo slot) { if (slot == null) diff --git a/GenesisCordonelInterface/API/PublicModels.cs b/GenesisCordonelInterface/API/PublicModels.cs index 33cc7cb12..7de9904c7 100644 --- a/GenesisCordonelInterface/API/PublicModels.cs +++ b/GenesisCordonelInterface/API/PublicModels.cs @@ -49,6 +49,18 @@ namespace GenesisCordonelInterface.API /// public class PublicModels { + public static bool HideSensitiveValues { get; set; } = true; + + private static string FormatPassword(string password) + { + if (!HideSensitiveValues) + return password ?? ""; + + if (string.IsNullOrEmpty(password)) + return ""; + + return "********"; + } /// /// Public DTOs exposed to external systems. /// These models represent the contract of the GCI API. @@ -165,7 +177,7 @@ namespace GenesisCordonelInterface.API IsLoggedOn, Message, PcbId, - Password, + FormatPassword(Password), ConfigSource, PasswordSource, RequestPort, @@ -270,12 +282,13 @@ namespace GenesisCordonelInterface.API public override string ToString() { return string.Format( - "SlotId={0}, GciConfigSource={1}, PasswordSource={2}, RequestPort={3}, StreamingPort={4}, Password=hidden", + "SlotId={0}, GciConfigSource={1}, PasswordSource={2}, RequestPort={3}, StreamingPort={4}, Password={5}", SlotId, ConfigSource, PasswordSource, RequestPort, - StreamingPort); + StreamingPort, + FormatPassword(Password)); } } @@ -430,7 +443,7 @@ namespace GenesisCordonelInterface.API "SlotId={0}, Success={1}, Password={2}, Message={3}", SlotId, Success, - Password, + FormatPassword(Password), Message); } } diff --git a/GenesisCordonelInterface/Core/Threading/ApiWorker/ApiWorker.cs b/GenesisCordonelInterface/Core/Threading/ApiWorker/ApiWorker.cs index 3cdbc03e6..e154e4112 100644 --- a/GenesisCordonelInterface/Core/Threading/ApiWorker/ApiWorker.cs +++ b/GenesisCordonelInterface/Core/Threading/ApiWorker/ApiWorker.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -173,6 +174,7 @@ namespace GenesisCordonelInterface.Core.Threading public sealed class ApiWorker : IDisposable { + private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface-threading"); /// /// Thread-safe FIFO queue holding work items. /// @@ -236,9 +238,9 @@ namespace GenesisCordonelInterface.Core.Threading /// Enqueues a function returning a value for sequential execution. /// public Task RunAsync( - Func action, - CancellationToken token = default(CancellationToken), - string operationName = null) + Func action, + CancellationToken token = default, + string operationName = null) { if (action == null) throw new ArgumentNullException(nameof(action)); @@ -246,58 +248,52 @@ namespace GenesisCordonelInterface.Core.Threading if (disposed) throw new ObjectDisposedException(nameof(ApiWorker)); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); - queue.Add(() => + try { - if (token.IsCancellationRequested) + Debug.WriteLine($"ENQUEUE {operationName} slot worker={Name} time={DateTime.Now:HH:mm:ss.fff}"); + queue.Add(() => { - tcs.TrySetCanceled(); - return; - } - - try - { - queue.Add(() => + if (token.IsCancellationRequested) { - if (token.IsCancellationRequested) - { - tcs.TrySetCanceled(); - return; - } + tcs.TrySetCanceled(); + return; + } - try - { - IsBusy = true; - CurrentOperation = operationName ?? action.Method.Name; - LastActivity = DateTime.Now; - LastError = null; + 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); - } - catch (ObjectDisposedException ex) - { - tcs.TrySetException(ex); - } - catch (InvalidOperationException ex) - { - tcs.TrySetException(new ObjectDisposedException(nameof(ApiWorker), ex)); - } - }, token); + var result = action(); + + tcs.TrySetResult(result); + } + catch (Exception ex) + { + LastError = ex.Message; + tcs.TrySetException(ex); + } + finally + { + IsBusy = false; + CurrentOperation = null; + LastActivity = DateTime.Now; + } + }, token); + } + catch (ObjectDisposedException ex) + { + tcs.TrySetException(ex); + } + catch (InvalidOperationException ex) + { + tcs.TrySetException(new ObjectDisposedException(nameof(ApiWorker), ex)); + } return tcs.Task; } diff --git a/GenesisCordonelInterface/Core/Threading/RetryWorker/RetryWorker.cs b/GenesisCordonelInterface/Core/Threading/RetryWorker/RetryWorker.cs new file mode 100644 index 000000000..003395fbf --- /dev/null +++ b/GenesisCordonelInterface/Core/Threading/RetryWorker/RetryWorker.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading.Tasks; + +namespace GenesisCordonelInterface.Core.Threading +{ + public sealed class RetryResult + { + public T Result { get; set; } + public bool Success { get; set; } + public int Attempts { get; set; } + public TimeSpan Duration { get; set; } + public bool TimedOut { get; set; } + + public override string ToString() + { + return string.Format( + "Success={0}, Attempts={1}, Duration={2} ms, TimedOut={3}, Result={4}", + Success, + Attempts, + (int)Duration.TotalMilliseconds, + TimedOut, + Result == null ? "" : Result.ToString()); + } + } + + public static class RetryWorker + { + public static async Task> RunWithRetryAsync( + Func> action, + Func isSuccess, + Action log, + Action logResult, + string operationName, + int maxAttempts = 3, + int delayMs = 500, + int timeoutMs = 30000) + { + var started = DateTime.Now; + T lastResult = default(T); + bool timedOut = false; + + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + var actionTask = action(); + var timeoutTask = Task.Delay(timeoutMs); + + var completedTask = await Task.WhenAny(actionTask, timeoutTask); + + if (completedTask == timeoutTask) + { + timedOut = true; + log?.Invoke($"{operationName} timeout attempt {attempt}/{maxAttempts}"); + } + else + { + lastResult = await actionTask; + + if (isSuccess(lastResult)) + { + return new RetryResult + { + Result = lastResult, + Success = true, + Attempts = attempt, + Duration = DateTime.Now - started, + TimedOut = false + }; + } + + logResult?.Invoke($"{operationName} failed attempt {attempt}/{maxAttempts}", lastResult); + } + + if (attempt < maxAttempts) + await Task.Delay(delayMs); + } + + return new RetryResult + { + Result = lastResult, + Success = false, + Attempts = maxAttempts, + Duration = DateTime.Now - started, + TimedOut = timedOut + }; + } + + public static void EnsureSuccess( + RetryResult retryResult, + string operationName) + { + if (!retryResult.Success) + throw new Exception($"{operationName} failed after {retryResult.Attempts} attempts."); + } + } +} \ No newline at end of file diff --git a/GenesisCordonelInterface/GenesisCordonelInterface.csproj b/GenesisCordonelInterface/GenesisCordonelInterface.csproj index 5602bc870..868979e85 100644 --- a/GenesisCordonelInterface/GenesisCordonelInterface.csproj +++ b/GenesisCordonelInterface/GenesisCordonelInterface.csproj @@ -63,6 +63,7 @@ + UserControl diff --git a/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs b/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs index f9dbac1bc..15e48586f 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/GciBridge.cs @@ -20,6 +20,7 @@ using GciType = GenesisCordonelInterface.API.InterfaceOutsideToGCI; using UdsReaderType = TBF.Rig.Input.DataStorage.UniDataStorageReader.Reader; using UDSRPublicModels = TBF.Rig.Input.DataStorage.UniDataStorageReader.Interfaces.PublicModels; using UdsWriterType = TBF.Rig.Output.DataStorage.UniDataStorageWriter.Writer; +using GenesisCordonelInterface.Core.Threading; namespace TBF.Rig.BridgeComponents.GciBridge { @@ -262,36 +263,6 @@ namespace TBF.Rig.BridgeComponents.GciBridge // API: #region ======================================= GCI Public Interface ======================================= - /*/// - /// Initializes one GCI slot through the external GCI interface. - /// - /// Trace: - /// GciBridge.InitSlotAsync() - /// -> InterfaceOutsideToGCI.InitSlotAsync() - /// -> InterfaceGCIToLaatzen.InitOneMeterFromExternAsync() - /// -> per-slot GCI worker - /// -> GenesisMeter.SetupFromExternConfig() - /// - /// Public GCI slot initialization request. - /// Initialization result for the requested slot. - public async Task InitSlotAsync(GciPublicModels.GciInitSlotRequest request) - { - 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; - }*/ - /// /// Initializes one GCI slot through the external GCI interface. /// @@ -303,9 +274,11 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// -> GenesisMeter.SetupFromExternConfig() /// /// Public GCI slot initialization request. + /// Cancellation token. /// Initialization result for the requested slot. public async Task InitSlotAsync( - GciPublicModels.GciInitSlotRequest request) + GciPublicModels.GciInitSlotRequest request, + CancellationToken token = default) { EnsureExternalInterface(); @@ -313,15 +286,64 @@ namespace TBF.Rig.BridgeComponents.GciBridge throw new ArgumentNullException(nameof(request)); if (request.SlotId <= 0) - throw new ArgumentException("Invalid slot id."); + throw new ArgumentException("Invalid slot id.", nameof(request)); - var result = await gciExternalInterface.InitSlotAsync(request); + var result = await gciExternalInterface + .InitSlotAsync(request, token) + .ConfigureAwait(false); log.InfoFormat("{0}: InitSlotAsync invoked. {1}, Result={2}", Name, request, result); return result; } + /// + /// Initializes one GCI slot using retry and timeout protection. + /// + /// Features: + /// - retries failed slot initialization attempts + /// - validates Success=true + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// InitSlotWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> InitSlotAsync() + /// -> InterfaceOutsideToGCI.InitSlotAsync() + /// -> InterfaceGCIToLaatzen.InitSlotAsync() + /// -> per-slot GCI worker + /// -> GenesisMeter.SetupFromExternConfig() + /// + /// Returns: + /// RetryResult containing: + /// - GciInitSlotResult + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Public GCI slot initialization request. + /// Cancellation token. + /// Initialization result wrapped inside RetryResult. + public Task> InitSlotWithRetryAsync( + GciPublicModels.GciInitSlotRequest request, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => InitSlotAsync(request, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"InitSlotAsync slot {request?.SlotId}", + maxAttempts: 3, + delayMs: 500, + timeoutMs: 30000); + } + + /// /// Updates an already initialized GCI slot. /// @@ -333,9 +355,11 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// -> GenesisMeter.SetupFromExternConfig() /// /// Public GCI slot update request. + /// Cancellation token. /// Update result for the requested slot. public async Task UpdateSlotAsync( - GciPublicModels.GciInitSlotRequest request) + GciPublicModels.GciInitSlotRequest request, + CancellationToken token = default) { EnsureExternalInterface(); @@ -343,15 +367,61 @@ namespace TBF.Rig.BridgeComponents.GciBridge throw new ArgumentNullException(nameof(request)); if (request.SlotId <= 0) - throw new ArgumentException("Invalid slot id."); + throw new ArgumentException("Invalid slot id.", nameof(request)); - var result = await gciExternalInterface.UpdateSlotAsync(request); - - log.InfoFormat("{0}: UpdateSlotAsync invoked. {1}, Result={2}", Name, request, result); + var result = await gciExternalInterface + .UpdateSlotAsync(request, token) + .ConfigureAwait(false); return result; } + /// + /// Updates an already initialized GCI slot using retry and timeout protection. + /// + /// Features: + /// - retries failed slot update attempts + /// - validates Success=true + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// UpdateSlotWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> UpdateSlotAsync() + /// -> InterfaceOutsideToGCI.UpdateSlotAsync() + /// -> InterfaceGCIToLaatzen.UpdateSlotAsync() + /// -> per-slot GCI worker + /// -> GenesisMeter.SetupFromExternConfig() + /// + /// Returns: + /// RetryResult containing: + /// - GciInitSlotResult + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Public GCI slot update request. + /// Cancellation token. + /// Update result wrapped inside RetryResult. + public Task> UpdateSlotWithRetryAsync( + GciPublicModels.GciInitSlotRequest request, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => UpdateSlotAsync(request, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"UpdateSlotAsync slot {request?.SlotId}", + maxAttempts: 3, + delayMs: 500, + timeoutMs: 30000); + } + /// /// Reads current information about one initialized GCI slot. /// @@ -363,8 +433,11 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// -> MeterBatch / GenesisMeter snapshot /// /// Slot number. + /// Cancellation token. /// Current public slot information. - public async Task GetSlotAsync(int slotId) + public async Task GetSlotAsync( + int slotId, + CancellationToken token = default) { EnsureExternalInterface(); @@ -372,13 +445,59 @@ namespace TBF.Rig.BridgeComponents.GciBridge throw new ArgumentException("Invalid slot id."); GciPublicModels.GciSlotInfo result = - await gciExternalInterface.GetSlotAsync(slotId); + await gciExternalInterface.GetSlotAsync(slotId, token); log.InfoFormat("{0}: GetSlotAsync({1}) invoked. Result={2}", Name, slotId, result); return result; } + /// + /// Reads current information about one initialized GCI slot using retry and timeout protection. + /// + /// Features: + /// - retries failed slot info read attempts + /// - validates that result is not null + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// GetSlotWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> GetSlotAsync() + /// -> InterfaceOutsideToGCI.GetSlotAsync() + /// -> InterfaceGCIToLaatzen.GetOneMeterInfo() + /// -> per-slot GCI worker + /// -> MeterBatch / GenesisMeter snapshot + /// + /// Returns: + /// RetryResult containing: + /// - GciSlotInfo + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Slot number. + /// Cancellation token. + /// Current public slot information wrapped inside RetryResult. + public Task> GetSlotWithRetryAsync( + int slotId, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => GetSlotAsync(slotId, token), + r => r != null, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"GetSlotAsync slot {slotId}", + maxAttempts: 3, + delayMs: 500, + timeoutMs: 30000); + } + /// /// Clears one initialized GCI slot and related GCI worker state. /// @@ -390,8 +509,11 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// -> worker cleanup /// /// GCI slot id. + /// Cancellation token. /// Cleanup operation result. - public async Task CleanSlotAsync(int slot) + public async Task CleanSlotAsync( + int slot, + CancellationToken token = default) { EnsureExternalInterface(); @@ -399,13 +521,60 @@ namespace TBF.Rig.BridgeComponents.GciBridge throw new ArgumentException("Invalid slot id.", nameof(slot)); GciPublicModels.GciCleanSlotResult result = - await gciExternalInterface.CleanSlotAsync(slot); + await gciExternalInterface.CleanSlotAsync(slot, token); log.InfoFormat("{0}: CleanSlotAsync invoked. Slot={1}, Result={2}", Name, slot, result); return result; } + /// + /// Cleans one initialized slot using retry and timeout protection. + /// + /// Features: + /// - retries failed cleanup attempts + /// - validates Success=true + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// CleanSlotWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> CleanSlotAsync() + /// -> InterfaceOutsideToGCI.CleanSlotAsync() + /// -> InterfaceGCIToLaatzen.CleanSlotAsync() + /// -> worker cleanup + /// -> meter cleanup + /// -> slot cleanup + /// + /// Returns: + /// RetryResult containing: + /// - GciCleanSlotResult + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Target slot number. + /// Cancellation token. + /// Cleanup result wrapped inside RetryResult. + public Task> CleanSlotWithRetryAsync( + int slotId, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => CleanSlotAsync(slotId), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"CleanSlotAsync slot {slotId}", + maxAttempts: 3, + delayMs: 5, + timeoutMs: 30000); + } + /// /// Clears all initialized GCI slots and related GCI worker state. /// @@ -417,19 +586,66 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// -> selected slot cleanup /// -> worker cleanup /// + /// Cancellation token. /// Cleanup operation result. - public async Task CleanAllSlotsAsync() + public async Task CleanAllSlotsAsync( + CancellationToken token = default) { EnsureExternalInterface(); GciPublicModels.GciCleanAllSlotsResult result = - await gciExternalInterface.CleanAllSlotsAsync(); + await gciExternalInterface.CleanAllSlotsAsync(token); log.InfoFormat("{0}: CleanAllSlotsAsync invoked.", Name); return result; } + /// + /// Cleans all initialized slots using retry and timeout protection. + /// + /// Features: + /// - retries failed cleanup attempts + /// - validates Success=true + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// CleanAllSlotsWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> CleanAllSlotsAsync() + /// -> InterfaceOutsideToGCI.CleanAllSlotsAsync() + /// -> InterfaceGCIToLaatzen.CleanAllSlotsAsync() + /// -> worker cleanup + /// -> MeterBatch cleanup + /// -> slot cleanup + /// + /// Returns: + /// RetryResult containing: + /// - GciCleanAllSlotsResult + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Cancellation token. + /// Cleanup result wrapped inside RetryResult. + public Task> CleanAllSlotsWithRetryAsync( + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => CleanAllSlotsAsync(), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + "CleanAllSlotsAsync", + maxAttempts: 3, + delayMs: 5, + timeoutMs: 60000); + } + /// /// Reads PCB ID from the meter assigned to the requested slot. /// @@ -444,21 +660,108 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// Cancellation token. /// PCB ID read result. public async Task GetPcbIdAsync( - int slotId, - CancellationToken token = default) + int slotId, + CancellationToken token = default) { EnsureExternalInterface(); if (slotId <= 0) - throw new ArgumentException("Invalid slot id."); + throw new ArgumentException("Invalid slot id.", nameof(slotId)); - var result = await gciExternalInterface.GetPcbIdAsync(slotId, token); + var result = await gciExternalInterface + .GetPcbIdAsync(slotId, token) + .ConfigureAwait(false); - log.InfoFormat("{0}: GetPcbIdAsync({1}) invoked. Result={2}", Name, slotId, result); + log.InfoFormat( + "{0}: GetPcbIdAsync({1}) invoked. Result={2}", + Name, + slotId, + result); return result; } + /// + /// Reads PCB ID from meter in the specified slot using retry and timeout protection. + /// + /// Features: + /// - retries failed PCB reads + /// - validates Success=true + /// - tracks retry statistics and duration + /// + /// Internal flow: + /// + /// GetPcbIdWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> GetPcbIdAsync() + /// + /// Returns: + /// RetryResult containing: + /// - GciGetPcbIdResult + /// - retry statistics + /// - timeout state + /// - execution duration + /// + /// Slot number. + /// Cancellation token. + /// PCB ID result wrapped inside RetryResult. + public Task> GetPcbIdWithRetryAsync( + int slotId, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => GetPcbIdAsync(slotId, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"GetPcbIdAsync slot {slotId}", + maxAttempts: 10, + delayMs: 5, + timeoutMs: 30000); + } + + /// + /// Connects to meter in the specified slot using retry and timeout protection. + /// + /// Features: + /// - retries failed connect attempts + /// - validates Success=true + /// - tracks duration and retry count + /// - supports cancellation token + /// + /// Internal flow: + /// + /// ConnectWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> ConnectAsync() + /// -> InterfaceOutsideToGCI.ConnectOneSlotAsync() + /// + /// Returns: + /// RetryResult containing: + /// - GciConnectResult + /// - retry statistics + /// - timeout state + /// - execution duration + /// + /// Slot number. + /// Cancellation token. + /// Connection result wrapped inside RetryResult. + public Task> ConnectWithRetryAsync( + int slotId, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => ConnectAsync(slotId, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"ConnectAsync slot {slotId}", + maxAttempts: 3, + delayMs: 5, + timeoutMs: 30000); + } + + /// /// Connects/logs in to the meter assigned to the requested slot. /// @@ -474,17 +777,19 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// Cancellation token. /// Connection result including firmware/interface information. public async Task ConnectAsync( - int slotId, - CancellationToken token = default) + int slot, + CancellationToken token = default) { EnsureExternalInterface(); - if (slotId <= 0) - throw new ArgumentException("Invalid slot id."); + if (slot <= 0) + throw new ArgumentException("Invalid slot id.", nameof(slot)); - var result = await gciExternalInterface.ConnectOneSlotAsync(slotId, token); + var result = await gciExternalInterface + .ConnectOneSlotAsync(slot, token) + .ConfigureAwait(false); - log.InfoFormat("{0}: ConnectAsync({1}) invoked. Result={2}", Name, slotId, result); + log.InfoFormat("{0}: ConnectAsync({1}) invoked. Result={2}", Name, slot, result); return result; } @@ -519,6 +824,46 @@ namespace TBF.Rig.BridgeComponents.GciBridge return result; } + /// + /// Performs meter login using retry and timeout protection. + /// + /// Features: + /// - retries failed login attempts + /// - validates Success=true + /// - tracks retry count and execution duration + /// - supports cancellation + /// + /// Internal flow: + /// + /// LoginWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> LoginAsync() + /// + /// Returns: + /// RetryResult containing: + /// - GciLoginResult + /// - retry statistics + /// - timeout state + /// - execution duration + /// + /// Slot number. + /// Cancellation token. + /// Login result wrapped inside RetryResult. + public Task> LoginWithRetryAsync( + int slotId, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => LoginAsync(slotId, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"LoginAsync slot {slotId}", + maxAttempts: 3, + delayMs: 5, + timeoutMs: 60000); + } + /// /// Disconnects/logs out from the meter assigned to the requested slot. /// @@ -551,7 +896,54 @@ namespace TBF.Rig.BridgeComponents.GciBridge } /// - /// Sets password to meter in given slot. + /// Disconnects/logs out meter in the specified slot using retry and timeout protection. + /// + /// Features: + /// - retries failed disconnect attempts + /// - validates Success=true + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// DisconnectWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> DisconnectAsync() + /// -> InterfaceOutsideToGCI.DisconnectAsync() + /// -> InterfaceGCIToLaatzen.DisconnectAsync() + /// -> GenesisMeter.Logout() + /// -> GenesisMeter.DisposeMeter() + /// -> worker cleanup + /// + /// Returns: + /// RetryResult containing: + /// - GciDisconnectResult + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Target slot number. + /// Cancellation token. + /// Disconnect result wrapped inside RetryResult. + public Task> DisconnectWithRetryAsync( + int slotId, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => DisconnectAsync(slotId, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"DisconnectAsync slot {slotId}", + maxAttempts: 3, + delayMs: 5, + timeoutMs: 30000); + } + + /// + /// Sets password to the meter assigned to the requested slot. /// /// Trace: /// GciBridge.SetPasswordAsync() @@ -559,11 +951,16 @@ namespace TBF.Rig.BridgeComponents.GciBridge /// -> InterfaceGCIToLaatzen.SetMeterPasswordAsync() /// -> per-slot GCI worker /// -> GenesisMeter.Password = password + /// -> internal login credentials update /// + /// Slot number. + /// Password assigned to the meter. + /// Cancellation token. + /// Password set operation result. public async Task SetPasswordAsync( - int slotId, - string password, - CancellationToken token = default) + int slotId, + string password, + CancellationToken token = default) { EnsureExternalInterface(); @@ -581,10 +978,65 @@ namespace TBF.Rig.BridgeComponents.GciBridge return result; } + /// + /// Sends password to meter in the specified slot using retry protection. + /// + /// Features: + /// - retries failed password writes + /// - validates Success=true + /// - tracks retry count and duration + /// + /// Internal flow: + /// + /// SetPasswordWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> SetPasswordAsync() + /// + /// Returns: + /// RetryResult containing: + /// - GciSetPasswordResult + /// - retry statistics + /// - timeout state + /// - execution duration + /// + /// Slot number. + /// Password assigned to the meter. + /// Cancellation token. + /// Password set result wrapped inside RetryResult. + public Task> SetPasswordWithRetryAsync( + int slotId, + string password, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => SetPasswordAsync(slotId, password, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"SetPasswordAsync slot {slotId}", + maxAttempts: 5, + delayMs: 5, + timeoutMs: 30000); + } + + /// + /// Reads one register value from the meter assigned to the requested slot. + /// + /// Trace: + /// GciBridge.ReadRegisterAsync() + /// -> InterfaceOutsideToGCI.ReadRegisterAsync() + /// -> InterfaceGCIToLaatzen.ReadRegisterAsync() + /// -> per-slot GCI worker + /// -> GenesisMeter register read + /// + /// Slot number. + /// Register name. + /// Cancellation token. + /// Register read result. public async Task ReadRegisterAsync( - int slotId, - string registerName, - CancellationToken token = default) + int slotId, + string registerName, + CancellationToken token = default) { EnsureExternalInterface(); @@ -602,6 +1054,72 @@ namespace TBF.Rig.BridgeComponents.GciBridge return result; } + /// + /// Reads one register value from the meter using retry and timeout protection. + /// + /// Features: + /// - retries failed register read attempts + /// - validates Success=true + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// ReadRegisterWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> ReadRegisterAsync() + /// -> InterfaceOutsideToGCI.ReadRegisterAsync() + /// -> InterfaceGCIToLaatzen.ReadRegisterAsync() + /// -> GenesisMeter register read + /// + /// Returns: + /// RetryResult containing: + /// - RegisterReadResult + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Target slot number. + /// Register name to read. + /// Cancellation token. + /// Register read result wrapped inside RetryResult. + public Task> ReadRegisterWithRetryAsync( + int slotId, + string registerName, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => ReadRegisterAsync(slotId, registerName, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"ReadRegisterAsync slot {slotId}, register {registerName}", + maxAttempts: 5, + delayMs: 5, + timeoutMs: 30000); + } + + /// + /// Writes one register value to the meter assigned to the requested slot. + /// + /// Trace: + /// GciBridge.WriteRegisterAsync() + /// -> InterfaceOutsideToGCI.WriteRegisterAsync() + /// -> InterfaceGCIToLaatzen.WriteRegisterAsync() + /// -> per-slot GCI worker + /// -> GenesisMeter register write + /// -> optional device store + /// -> optional state refresh + /// + /// Slot number. + /// Register name. + /// Value written to register. + /// Stores value permanently into device memory. + /// Refreshes internal meter state after write. + /// Cancellation token. + /// Register write result. public async Task WriteRegisterAsync( int slotId, string registerName, @@ -632,6 +1150,65 @@ namespace TBF.Rig.BridgeComponents.GciBridge return result; } + /// + /// Writes one register value to the meter using retry and timeout protection. + /// + /// Features: + /// - retries failed register write attempts + /// - validates Success=true + /// - supports cancellation + /// - tracks retry count + /// - tracks total execution duration + /// - detects timeout situations + /// + /// Internal flow: + /// + /// WriteRegisterWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> WriteRegisterAsync() + /// -> InterfaceOutsideToGCI.WriteRegisterAsync() + /// -> InterfaceGCIToLaatzen.WriteRegisterAsync() + /// -> GenesisMeter register write + /// + /// Returns: + /// RetryResult containing: + /// - RegisterWriteResult + /// - retry statistics + /// - timeout information + /// - execution duration + /// + /// Target slot number. + /// Register name to write. + /// Value to write into register. + /// Stores value permanently into device memory. + /// Refreshes internal register cache after write. + /// Cancellation token. + /// Register write result wrapped inside RetryResult. + public Task> WriteRegisterWithRetryAsync( + int slotId, + string registerName, + object value, + bool storeToDevice = false, + bool refreshSystemState = false, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => WriteRegisterAsync( + slotId, + registerName, + value, + storeToDevice, + refreshSystemState, + token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"WriteRegisterAsync slot {slotId}, register {registerName}", + maxAttempts: 5, + delayMs: 5, + timeoutMs: 30000); + } + #endregion #region ======================================= UDSR Public Interface ======================================= @@ -690,6 +1267,154 @@ namespace TBF.Rig.BridgeComponents.GciBridge }; } } + + /// + /// Reads password from UniDataStorageReader using PCB ID with retry support. + /// + /// Features: + /// - retries failed storage queries + /// - validates Success=true + /// - supports timeout handling + /// - tracks retry statistics + /// + /// Internal flow: + /// + /// GetPasswordWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> GetPasswordAsync() + /// -> UniDataStorageReader + /// + /// Returns: + /// RetryResult containing: + /// - UdsPasswordResult + /// - retry statistics + /// - timeout state + /// - execution duration + /// + /// PCB ID used as query parameter. + /// Cancellation token. + /// Password lookup result wrapped inside RetryResult. + public Task> GetPasswordWithRetryAsync( + string pcbId, + CancellationToken token = default) + { + return RetryWorker.RunWithRetryAsync( + () => GetPasswordAsync(pcbId, token), + r => r.Success, + msg => log.Info(msg), + (msg, result) => log.InfoFormat("{0}: {1}", msg, result), + $"GetPasswordAsync pcb {pcbId}", + maxAttempts: 5, + delayMs: 5, + timeoutMs: 30000); + } + #endregion + + #region =================================== Combined Public interface ========================================= + + /// + /// Executes complete meter login workflow for a single slot with retry, + /// timeout handling and automatic recovery. + /// + /// Workflow: + /// Connect + /// -> Read PCB ID + /// -> Read password from UDSR + /// -> Set password to meter + /// -> Login to meter + /// + /// Every operation: + /// - supports retry logic + /// - supports timeout protection + /// - stores execution statistics + /// (attempt count, duration, timeout state) + /// - validates Success=true before continuing + /// + /// If any step fails: + /// - workflow is immediately aborted + /// - DisconnectAsync() cleanup is attempted + /// - final result contains failed operation details + /// + /// Parallelism: + /// - safe to execute for multiple slots in parallel + /// - each slot internally uses serialized GCI worker queue + /// + /// Typical usage: + /// + /// var result = await bridge.ConnectFullPassLoginWithRetryAsync(slot); + /// + /// Result contains: + /// - per-step RetryResult + /// - timing information + /// - retry statistics + /// - final workflow state + /// + /// Internal flow: + /// + /// GciBridge.ConnectFullPassLoginWithRetryAsync() + /// -> RetryWorker.RunWithRetryAsync() + /// -> ConnectAsync() + /// -> GetPcbIdAsync() + /// -> GetPasswordAsync() + /// -> SetPasswordAsync() + /// -> LoginAsync() + /// + /// Used by: + /// - CombinedInterfaceView + /// - automated meter initialization workflows + /// - production batch login scenarios + /// + /// Target slot number. + /// Cancellation token. + /// Complete workflow result including all retry statistics and operation results. + public async Task ConnectFullPassLoginWithRetryAsync(int slotId, CancellationToken token = default) + { + EnsureExternalInterface(); + EnsureReader(); + + if (slotId <= 0) + throw new ArgumentException("Invalid slot id.", nameof(slotId)); + + var finalResult = new GciFullLoginResult { SlotId = slotId, Success = false }; + + try + { + finalResult.ConnectResult = await ConnectWithRetryAsync(slotId, token); + RetryWorker.EnsureSuccess(finalResult.ConnectResult, $"ConnectAsync slot {slotId}"); + + finalResult.PcbResult = await GetPcbIdWithRetryAsync(slotId, token); + RetryWorker.EnsureSuccess(finalResult.PcbResult, $"GetPcbIdAsync slot {slotId}"); + + finalResult.PasswordResult = await GetPasswordWithRetryAsync(finalResult.PcbResult.Result.PcbId, token); + RetryWorker.EnsureSuccess(finalResult.PasswordResult, $"GetPasswordAsync slot {slotId}"); + + finalResult.SetPasswordResult = await SetPasswordWithRetryAsync(slotId, finalResult.PasswordResult.Result.Password, token); + RetryWorker.EnsureSuccess(finalResult.SetPasswordResult, $"SetPasswordAsync slot {slotId}"); + + finalResult.LoginResult = await LoginWithRetryAsync(slotId, token); + RetryWorker.EnsureSuccess(finalResult.LoginResult, $"LoginAsync slot {slotId}"); + + finalResult.Success = true; + finalResult.Message = "Connect / get PCB / get password / set password / login completed."; + } + catch (Exception ex) + { + finalResult.Success = false; + finalResult.Message = ex.Message; + + try + { + await DisconnectAsync(slotId, token); + } + catch (Exception cleanEx) + { + log.ErrorFormat("DisconnectAsync after failed ConnectFullPassLoginAsync failed. Slot={0}, Error={1}", slotId, cleanEx.Message); + } + } + + return finalResult; + } + #endregion #region ======================================= Helpers ======================================= diff --git a/TBF/Rig/BridgeComponents/GciBridge/Interfaces/PublicModels.cs b/TBF/Rig/BridgeComponents/GciBridge/Interfaces/PublicModels.cs index 744bf9430..2baa4c894 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/Interfaces/PublicModels.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/Interfaces/PublicModels.cs @@ -1,5 +1,7 @@ -using System; +using GenesisCordonelInterface.Core.Threading; +using System; using System.Collections.Generic; +using GciPublicModels = GenesisCordonelInterface.API.PublicModels; namespace TBF.Rig.BridgeComponents.GciBridge.Interfaces { @@ -42,6 +44,19 @@ namespace TBF.Rig.BridgeComponents.GciBridge.Interfaces /// public class PublicModels { + public static bool HideSensitiveValues { get; set; } = true; + + private static string FormatPassword(string password) + { + if (!HideSensitiveValues) + return password ?? ""; + + if (string.IsNullOrEmpty(password)) + return ""; + + return "********"; + } + /// /// Public DTOs exposed to external systems. /// These models represent the contract of the GCI API. @@ -75,10 +90,44 @@ namespace TBF.Rig.BridgeComponents.GciBridge.Interfaces "Success={0}, PcbId={1}, Password={2}, Message={3}", Success, PcbId, - string.IsNullOrEmpty(Password) ? "" : Password, + FormatPassword(Password), Message); } } + + public class GciFullLoginResult + { + public int SlotId { get; set; } + public bool Success { get; set; } + public string Message { get; set; } + + public RetryResult ConnectResult { get; set; } + public RetryResult PcbResult { get; set; } + public RetryResult PasswordResult { get; set; } + public RetryResult SetPasswordResult { get; set; } + public RetryResult LoginResult { get; set; } + + public GciFullLoginResult Fail(string message) + { + Success = false; + Message = message; + return this; + } + + public override string ToString() + { + return string.Format( + "SlotId={0}, Success={1}, Message={2}, Connect={3}, Pcb={4}, Password={5}, SetPassword={6}, Login={7}", + SlotId, + Success, + Message, + ConnectResult, + PcbResult, + PasswordResult, + SetPasswordResult, + LoginResult); + } + } #endregion } } diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/MeterBatchConfigPanel.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/MeterBatchConfigPanel.cs index 9a572f878..34e236265 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/MeterBatchConfigPanel.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/MeterBatchConfigPanel.cs @@ -385,6 +385,27 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Debug return _gridManager.GetSelectedGridData(); } + public void ClearSelection() + { + grid.EndEdit(); + + foreach (DataGridViewRow row in grid.Rows) + { + if (row.IsNewRow) + continue; + + if (grid.Columns.Contains("Selected")) + { + row.Cells["Selected"].Value = false; + } + } + + grid.ClearSelection(); + + if (grid.CurrentCell != null) + grid.CurrentCell = null; + } + private void grid_KeyDown(object sender, KeyEventArgs e) { if (e.Control && e.KeyCode == Keys.V) diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.Designer.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.Designer.cs index aa85ddf41..865c3e660 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.Designer.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.Designer.cs @@ -31,8 +31,15 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Debug /// private void InitializeComponent() { - components = new System.ComponentModel.Container(); + this.SuspendLayout(); + // + // WorkerDebugPanel + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Name = "WorkerDebugPanel"; + this.ResumeLayout(false); + } #endregion diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.cs index 6a4fccde1..a18d38f39 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.cs @@ -26,7 +26,7 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Debug Controls.Add(grid); - timer.Interval = 300; + timer.Interval = 150; timer.Tick += (s, e) => { grid.DataSource = null; diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.resx b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.resx new file mode 100644 index 000000000..1af7de150 --- /dev/null +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/Debug/WorkerDebugPanel.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridColumnConfig.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridColumnConfig.cs index 552faa45a..af76b6fa1 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridColumnConfig.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridColumnConfig.cs @@ -1,9 +1,12 @@ -namespace TBF.Rig.BridgeComponents.GciBridge.UI.Grid +using static TBF.Rig.BridgeComponents.GciBridge.UI.Grid.MeterGridConfigProvider; + +namespace TBF.Rig.BridgeComponents.GciBridge.UI.Grid { public class MeterGridColumnConfig { public string Name { get; set; } public string HeaderText { get; set; } + public MeterGridColumnType ColumnType { get; set; } public bool Visible { get; set; } = true; public int DisplayIndex { get; set; } public int Width { get; set; } = 80; diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridConfigProvider.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridConfigProvider.cs index 29c632eff..915245db3 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridConfigProvider.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridConfigProvider.cs @@ -4,19 +4,70 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Grid { public static class MeterGridConfigProvider { + public enum MeterGridColumnType + { + Text, + CheckBox + } + public static List GetDefault() { return new List { - new MeterGridColumnConfig { Name = "Slot", HeaderText = "Slot", DisplayIndex = 0, Width = 30, ReadOnly = true }, - new MeterGridColumnConfig { Name = "Selected", HeaderText = "Selected", DisplayIndex = 1, Width = 30 }, - new MeterGridColumnConfig { Name = "PcbId", HeaderText = "PcbId", DisplayIndex = 2, Width = 80 }, - new MeterGridColumnConfig { Name = "IsLoggedOn", HeaderText = "IsLoggedOn", DisplayIndex = 3, Width = 30 }, - new MeterGridColumnConfig { Name = "IsLoggedOn", HeaderText = "IsLoggedOn", DisplayIndex = 3, Width = 30 }, - 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 }, + new MeterGridColumnConfig + { + Name = "Slot", + HeaderText = "Slot", + ColumnType = MeterGridColumnType.Text, + DisplayIndex = 0, + Width = 30, + ReadOnly = true + }, + + new MeterGridColumnConfig + { + Name = "Selected", + HeaderText = "Selected", + ColumnType = MeterGridColumnType.CheckBox, + DisplayIndex = 1, + Width = 30 + }, + + new MeterGridColumnConfig + { + Name = "PcbId", + HeaderText = "PcbId", + ColumnType = MeterGridColumnType.Text, + DisplayIndex = 2, + Width = 80 + }, + + new MeterGridColumnConfig + { + Name = "IsConnected", + HeaderText = "IsConnected", + ColumnType = MeterGridColumnType.CheckBox, + DisplayIndex = 3, + Width = 30 + }, + + new MeterGridColumnConfig + { + Name = "IsLoggedOn", + HeaderText = "IsLoggedOn", + ColumnType = MeterGridColumnType.CheckBox, + DisplayIndex = 4, + Width = 30 + }, + + new MeterGridColumnConfig + { + Name = "FwVersion", + HeaderText = "FwVersion", + ColumnType = MeterGridColumnType.Text, + DisplayIndex = 5, + Width = 80 + } }; } } diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridManager.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridManager.cs index c0e523df2..f4299069c 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridManager.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/Grid/MeterGridManager.cs @@ -30,32 +30,9 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Grid grid.AllowUserToDeleteRows = false; grid.RowHeadersVisible = true; - 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 = "IsConnected", HeaderText = "IsConnected" }); - grid.Columns.Add(new DataGridViewCheckBoxColumn { Name = "IsLoggedOn", HeaderText = "IsLoggedOn" }); - - grid.Columns.Add(CreateComPortColumn("RequestPort", "RequestPort", comPorts)); - grid.Columns.Add(CreateRequestPortTypeColumn()); - grid.Columns.Add(CreateComPortColumn("StreamingPort", "StreamingPort", comPorts)); - - 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 != "RequestPortType" && - col.Name != "StreamingPort" && - col.Name != "DetectRequest" && - col.Name != "DetectStreaming"; - } + CreateColumns(comPorts); + ConfigureReadOnlyColumns(); + ConfigureSelection(); } finally { @@ -66,6 +43,70 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Grid grid.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText; } + private void CreateColumns(List comPorts) + { + var configs = MeterGridConfigProvider + .GetDefault() + .OrderBy(x => x.DisplayIndex); + + foreach (var config in configs) + { + DataGridViewColumn column; + + switch (config.ColumnType) + { + case MeterGridConfigProvider.MeterGridColumnType.CheckBox: + column = new DataGridViewCheckBoxColumn(); + break; + + default: + column = new DataGridViewTextBoxColumn(); + break; + } + + column.Name = config.Name; + column.HeaderText = config.HeaderText; + column.Width = config.Width; + column.ReadOnly = config.ReadOnly; + column.DisplayIndex = config.DisplayIndex; + + grid.Columns.Add(column); + } + + grid.Columns.Add(CreateComPortColumn("RequestPort", "RequestPort", comPorts)); + grid.Columns.Add(CreateRequestPortTypeColumn()); + grid.Columns.Add(CreateComPortColumn("StreamingPort", "StreamingPort", comPorts)); + + grid.Columns.Add(CreateButtonColumn("DetectRequest", "DetectRequest", "...")); + grid.Columns.Add(CreateButtonColumn("DetectStreaming", "DetectStreaming", "...")); + } + + private void ConfigureReadOnlyColumns() + { + foreach (DataGridViewColumn col in grid.Columns) + { + col.ReadOnly = + col.Name != "Selected" && + col.Name != "RequestPort" && + col.Name != "RequestPortType" && + col.Name != "StreamingPort" && + col.Name != "DetectRequest" && + col.Name != "DetectStreaming"; + } + } + + private void ConfigureSelection() + { + grid.MultiSelect = true; + grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + + grid.CellContentClick -= Grid_CellContentClick; + grid.CellContentClick += Grid_CellContentClick; + + grid.CurrentCellDirtyStateChanged -= Grid_CurrentCellDirtyStateChanged; + grid.CurrentCellDirtyStateChanged += Grid_CurrentCellDirtyStateChanged; + } + public void UpdateComPortItems(List comPorts) { UpdateComPortColumnItems("RequestPort", comPorts); @@ -407,5 +448,33 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Grid BindingFlags.Instance | BindingFlags.NonPublic) ?.SetValue(dgv, true, null); } + + private void Grid_CurrentCellDirtyStateChanged(object sender, EventArgs e) + { + if (grid.IsCurrentCellDirty) + { + grid.CommitEdit(DataGridViewDataErrorContexts.Commit); + } + } + + private void Grid_CellContentClick(object sender, DataGridViewCellEventArgs e) + { + if (e.RowIndex < 0) + return; + + if (grid.Columns[e.ColumnIndex].Name != "Selected") + return; + + bool clickedValue = Convert.ToBoolean( + grid.Rows[e.RowIndex].Cells["Selected"].Value); + + foreach (DataGridViewRow row in grid.SelectedRows) + { + if (row.Index == e.RowIndex) + continue; + + row.Cells["Selected"].Value = clickedValue; + } + } } } \ No newline at end of file diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/MainView.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/MainView.cs index 941c1f154..a0596d048 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/MainView.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/MainView.cs @@ -7,9 +7,20 @@ using System.Text.RegularExpressions; using System.Windows.Forms; using Xylem.Common.Utils.Logging; using TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge; +using System.Runtime.InteropServices; namespace TBF.Rig.BridgeComponents.GciBridge.UI { + /// + /// Main WinForms UserControl for the GCI Bridge application. + /// + /// Responsibilities: + /// - Hosts and switches child UI views + /// - Manages slot configuration panels + /// - Displays global application logs + /// - Buffers log messages to keep UI responsive + /// - Connects UI with GCI bridge APIs + /// public partial class MainView : UserControl { #region DECLARATION @@ -20,7 +31,23 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI public MainForm _mainform; public GciBridge _bridge; public Debug.MeterBatchConfigPanel _batchPanel; - public event Action> MeterBatchStatusChanged;// object status from place of his location + public event Action> MeterBatchStatusChanged;// object status from place of his location + + private const int WM_SETREDRAW = 0x000B; + + [DllImport("user32.dll")] //faster ritchbox redrawing + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + /// + /// Thread-safe UI log buffering infrastructure. + /// + /// Incoming log messages may arrive from multiple worker threads. + /// Messages are queued and periodically flushed to RichTextBox + /// by a UI timer to prevent UI freezes during parallel operations. + /// + private readonly object _uiLogLock = new object(); + private readonly Queue _pendingUiLogs = new Queue(); + private readonly Timer _uiLogFlushTimer = new Timer(); #endregion public MainView(GciBridge bridge, MainForm mainform) @@ -40,8 +67,146 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI rtbMainLog.Font = new Font("Consolas", 9f); rtbMainLog.ReadOnly = true; rtbMainLog.HideSelection = false; + + //UI htread safe + _uiLogFlushTimer.Interval = 250; + _uiLogFlushTimer.Tick += UiLogFlushTimer_Tick; + _uiLogFlushTimer.Start(); } + /// + /// Periodically flushes buffered log messages into RichTextBox. + /// + /// Runs on the UI thread because WinForms Timer executes on UI thread. + /// Processes messages in batches to reduce UI overhead. + /// + /// Plynule pridavanie do mema + /*private void UiLogFlushTimer_Tick(object sender, EventArgs e) + { + if (IsDisposed || !IsHandleCreated) + return; + + List messages = new List(); + + lock (_uiLogLock) + { + while (_pendingUiLogs.Count > 0 && messages.Count < 500) + { + messages.Add(_pendingUiLogs.Dequeue()); + } + } + + if (messages.Count == 0) + return; + + rtbMainLog.SuspendLayout(); + + try + { + foreach (string msg in messages) + { + AppendLogMessage(msg); + } + + const int maxTextLength = 200000; + + if (rtbMainLog.TextLength > maxTextLength) + { + rtbMainLog.Select(0, rtbMainLog.TextLength - maxTextLength); + rtbMainLog.SelectedText = ""; + } + + rtbMainLog.SelectionStart = rtbMainLog.TextLength; + rtbMainLog.ScrollToCaret(); + } + finally + { + rtbMainLog.ResumeLayout(); + } + }*/ + + private void UiLogFlushTimer_Tick(object sender, EventArgs e) + { + if (IsDisposed || !IsHandleCreated || rtbMainLog == null || rtbMainLog.IsDisposed) + return; + + List messages = new List(); + + lock (_uiLogLock) + { + while (_pendingUiLogs.Count > 0 && messages.Count < 500) + { + messages.Add(_pendingUiLogs.Dequeue()); + } + } + + if (messages.Count == 0) + return; + + string batchText = string.Join(Environment.NewLine, messages) + Environment.NewLine; + + SendMessage(rtbMainLog.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero); + + try + { + int batchStart = rtbMainLog.TextLength; + + rtbMainLog.SelectionStart = batchStart; + rtbMainLog.SelectionLength = 0; + rtbMainLog.SelectionColor = Color.Gainsboro; + + rtbMainLog.AppendText(batchText); + + // Optional: re-apply line highlighting after bulk insertion + //ApplyHighlightingToBatch(batchText, batchStart); + + const int maxTextLength = 200000; + + if (rtbMainLog.TextLength > maxTextLength) + { + rtbMainLog.Select(0, rtbMainLog.TextLength - maxTextLength); + rtbMainLog.SelectedText = ""; + } + + rtbMainLog.SelectionStart = rtbMainLog.TextLength; + rtbMainLog.ScrollToCaret(); + } + finally + { + SendMessage(rtbMainLog.Handle, WM_SETREDRAW, new IntPtr(1), IntPtr.Zero); + rtbMainLog.Invalidate(); + } + } + + private void ApplyHighlightingToBatch(string batchText, int batchStart) + { + string[] lines = batchText.Replace("\r\n", "\n").Split('\n'); + + int offset = 0; + + foreach (string line in lines) + { + if (!string.IsNullOrWhiteSpace(line)) + { + Match m = LogLevelRegex.Match(line); + + if (m.Success) + { + rtbMainLog.SelectionStart = batchStart + offset + m.Index; + rtbMainLog.SelectionLength = m.Length; + rtbMainLog.SelectionColor = GetLogLevelColor(m.Groups[1].Value); + } + + HighlightKeywordsInLine(line, batchStart + offset); + } + + offset += line.Length + Environment.NewLine.Length; + } + } + + /// + /// Initializes debug/configuration panels hosted inside MainView. + /// private void InitializeDebugPanels() { _batchPanel = new Debug.MeterBatchConfigPanel(_mainform, this) @@ -72,6 +237,10 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI })); } + /// + /// Safely adjusts SplitContainer distance while respecting + /// minimum panel sizes and current control dimensions. + /// private void SetSafeSplitterDistance(SplitContainer split, int desired) { int width = split.ClientSize.Width; @@ -99,6 +268,10 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI if (disposing) { UiLogBus.MessageReceived -= UiLogBus_MessageReceived; + + _uiLogFlushTimer.Stop(); + _uiLogFlushTimer.Tick -= UiLogFlushTimer_Tick; + _uiLogFlushTimer.Dispose(); } base.Dispose(disposing); @@ -121,12 +294,18 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI pnlGciViewHost.Controls.Add(view); } + /// + /// Saves current slot configuration from the grid into backend storage. + /// public void SaveSlots() { var data = _batchPanel.GetGridData(); _laatzenApi.SaveSlotSetup(data); } + /// + /// Switches currently displayed GCI view inside the host panel. + /// private void button2_Click(object sender, EventArgs e) { SwitchGciView( @@ -236,6 +415,9 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI new MeterInitView(_gciApi, AddSlotRow, SaveSlots)); } + /// + /// Clears the main UI log window. + /// public void ClearLog() { rtbMainLog.Clear(); @@ -244,6 +426,13 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI #endregion #region GLOBAL LOGGING to memo in this view + + /// + /// Receives global log messages from UiLogBus. + /// + /// This method may be called from background threads. + /// Messages are only queued here and later processed by UI timer. + /// void UiLogBus_MessageReceived(string loggerName, string msg) { if (loggerName != "GciBridge" && loggerName != "GenesisCordonelInterface") @@ -252,15 +441,18 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI if (IsDisposed || !IsHandleCreated) return; - if (InvokeRequired) + lock (_uiLogLock) { - BeginInvoke(new Action(UiLogBus_MessageReceived), loggerName, msg); - return; - } + _pendingUiLogs.Enqueue(msg); - AppendLogMessage(msg); + while (_pendingUiLogs.Count > 1000) + _pendingUiLogs.Dequeue(); + } } + /// + /// Splits multiline log messages and appends each line separately. + /// private void AppendLogMessage(string msg) { string[] lines = msg.Replace("\r\n", "\n").Split('\n'); @@ -274,6 +466,13 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI } } + /// + /// Appends a single styled log line into RichTextBox. + /// + /// Performs syntax highlighting for: + /// - log levels + /// - important keywords + /// private void AppendStyledLine(string line) { if (rtbMainLog == null || rtbMainLog.IsDisposed) @@ -297,6 +496,9 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI HighlightKeywordsInLine(line, start); } + /// + /// Returns color associated with a log level. + /// private Color GetLogLevelColor(string level) { switch (level.Trim().ToUpperInvariant()) @@ -318,6 +520,9 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI Tuple.Create(Color.Gold, new[] { "READ-REGISTER-SESSION", "UI-CLICK" }) }; + /// + /// Highlights important keywords inside a log line. + /// private void HighlightKeywordsInLine(string line, int lineStartIndex) { foreach (var group in KeywordGroups) diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.Designer.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.Designer.cs index f09f9b92a..b295192e2 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.Designer.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.Designer.cs @@ -28,7 +28,12 @@ this.btnGetPcb = new System.Windows.Forms.Button(); this.btnCancel = new System.Windows.Forms.Button(); this.txtLog = new System.Windows.Forms.TextBox(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.button1 = new System.Windows.Forms.Button(); + this.button2 = new System.Windows.Forms.Button(); + this.button3 = new System.Windows.Forms.Button(); this.grpStorage.SuspendLayout(); + this.groupBox1.SuspendLayout(); this.SuspendLayout(); // // grpStorage @@ -86,6 +91,9 @@ // // txtLog // + this.txtLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.txtLog.Location = new System.Drawing.Point(220, 10); this.txtLog.Multiline = true; this.txtLog.Name = "txtLog"; @@ -95,17 +103,65 @@ this.txtLog.TabIndex = 4; this.txtLog.WordWrap = false; // + // groupBox1 + // + this.groupBox1.Controls.Add(this.button1); + this.groupBox1.Controls.Add(this.button2); + this.groupBox1.Controls.Add(this.button3); + this.groupBox1.Location = new System.Drawing.Point(10, 166); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(200, 139); + this.groupBox1.TabIndex = 3; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "Combined actions"; + // + // button1 + // + this.button1.Location = new System.Drawing.Point(12, 96); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(175, 28); + this.button1.TabIndex = 2; + this.button1.Text = "..."; + this.button1.UseVisualStyleBackColor = true; + // + // button2 + // + this.button2.Location = new System.Drawing.Point(12, 62); + this.button2.Name = "button2"; + this.button2.Size = new System.Drawing.Size(175, 28); + this.button2.TabIndex = 1; + this.button2.Text = "..."; + this.button2.UseVisualStyleBackColor = true; + // + // button3 + // + this.button3.Location = new System.Drawing.Point(12, 28); + this.button3.Name = "button3"; + this.button3.Size = new System.Drawing.Size(175, 28); + this.button3.TabIndex = 0; + this.button3.Text = "Connect/FullPass/Login"; + this.button3.UseVisualStyleBackColor = true; + this.button3.Click += new System.EventHandler(this.button3_Click); + // // CombinedInterfaceView // + this.BackColor = System.Drawing.SystemColors.Control; + this.Controls.Add(this.groupBox1); this.Controls.Add(this.grpStorage); this.Controls.Add(this.btnCancel); this.Controls.Add(this.txtLog); this.Name = "CombinedInterfaceView"; this.Size = new System.Drawing.Size(740, 370); this.grpStorage.ResumeLayout(false); + this.groupBox1.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); } + + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.Button button1; + private System.Windows.Forms.Button button2; + private System.Windows.Forms.Button button3; } } \ No newline at end of file diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.cs index 5cd00c103..d20633c0c 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/CombinedInterfaceView.cs @@ -4,7 +4,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using PublicModels = GenesisCordonelInterface.API.PublicModels; +using GciPublicModels = GenesisCordonelInterface.API.PublicModels; +using GenesisCordonelInterface.Core.Threading; +using TBF.Rig.BridgeComponents.GciBridge.Interfaces; namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { @@ -25,7 +27,7 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge InitializeComponent(); } - private List GetSelectedSlots() + private List GetSelectedSlots() { var slots = _mainView._batchPanel.GetSelectedGridData(); @@ -41,44 +43,38 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { _pcbBySlot.Clear(); - foreach (var slot in GetSelectedSlots()) + var slots = GetSelectedSlots().ToList(); + + var tasks = slots.Select(async slot => { - var result = await _bridge.GetPcbIdAsync(slot.Slot, token); + //var result = await _bridge.GetPcbIdAsync(slot.Slot, token); + var result = await _bridge.GetPcbIdWithRetryAsync(slot.Slot, token); - LogResult($"GCI/GetPCB slot {slot.Slot}", result); - - if (result != null && - result.Success && - !string.IsNullOrWhiteSpace(result.PcbId)) + return new { - _pcbBySlot[slot.Slot] = result.PcbId; - Log($"Stored PCB: Slot={slot.Slot}, PCB={result.PcbId}"); + Slot = slot.Slot, + Result = result + }; + }).ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"GCI/GetPCB slot {item.Slot}", item.Result); + + if (item.Result != null && + item.Result.Success && + !string.IsNullOrWhiteSpace(item.Result.Result.PcbId)) + { + _pcbBySlot[item.Slot] = item.Result.Result.PcbId; + Log($"Stored PCB: Slot={item.Slot}, PCB={item.Result.Result.PcbId}"); } } Log($"PCB stored count: {_pcbBySlot.Count}"); }); } - /*private void btnGetPcb_Click(object sender, EventArgs e) - { - ExecuteAsync(async token => - { - _pcbBySlot.Clear(); - - foreach (var slot in GetSelectedSlots()) - { - string fakePcbId = "231630243"; // docasne platna PCB - - _pcbBySlot[slot.Slot] = fakePcbId; - - Log($"TEMP Stored PCB: Slot={slot.Slot}, PCB={fakePcbId}"); - - await Task.CompletedTask; - } - - Log($"PCB stored count: {_pcbBySlot.Count}"); - }); - }*/ private void btnGetPasswordByPcb_Click(object sender, EventArgs e) { @@ -94,15 +90,16 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge int slot = item.Key; string pcbId = item.Value; - var result = await _bridge.GetPasswordAsync(pcbId, token); + //var result = await _bridge.GetPasswordAsync(pcbId, token); + var result = await _bridge.GetPasswordWithRetryAsync(pcbId, token); LogResult($"UDSR/GetPassword slot {slot}, PCB={pcbId}", result); if (result != null && result.Success && - !string.IsNullOrWhiteSpace(result.Password)) + !string.IsNullOrWhiteSpace(result.Result.Password)) { - _passwordBySlot[slot] = result.Password; + _passwordBySlot[slot] = result.Result.Password; Log($"Stored password: Slot={slot}, PCB={pcbId}, Password=hidden"); } } @@ -123,7 +120,8 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge int slot = item.Key; string password = item.Value; - var result = await _bridge.SetPasswordAsync(slot, password, token); + //var result = await _bridge.SetPasswordAsync(slot, password, token); + var result = await _bridge.SetPasswordWithRetryAsync(slot, password, token); LogResult($"GCI/SetPassword slot {slot}", result); } @@ -206,5 +204,55 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge message + Environment.NewLine); } + + private void button3_Click(object sender, EventArgs e) + { + ExecuteAsync(async token => + { + var selectedSlots = GetSelectedSlots().ToList(); + + var tasks = selectedSlots.Select(async slot => + { + var result = await _bridge.ConnectFullPassLoginWithRetryAsync(slot.Slot, token); + + LogResult($"ConnectFullPassLoginAsync slot {slot.Slot}", result); + + LogFullLoginResult(result); + + return result; + }).ToList(); + + await Task.WhenAll(tasks); + + RefreshGrid(); + }); + } + + private void LogFullLoginResult(PublicModels.GciFullLoginResult result) + { + Log($"====================== SLOT {result.SlotId} ======================"); + Log($"FullLogin Success={result.Success}, Message={result.Message}"); + + LogRetryResult("Connect", result.ConnectResult); + LogRetryResult("GetPcbId", result.PcbResult); + LogRetryResult("GetPassword", result.PasswordResult); + LogRetryResult("SetPassword", result.SetPasswordResult); + LogRetryResult("Login", result.LoginResult); + } + + private void LogRetryResult( + string operationName, + RetryResult retryResult) + { + if (retryResult == null) + { + Log($"{operationName}: "); + return; + } + + LogResult( + $"{operationName} | success={retryResult.Success} | attempts={retryResult.Attempts} | duration={retryResult.Duration.TotalSeconds:F1}s | timeout={retryResult.TimedOut}", + retryResult.Result); + } } } \ No newline at end of file diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/ConfigurationView.Designer.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/ConfigurationView.Designer.cs index f6be0e49c..796c18181 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/ConfigurationView.Designer.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/ConfigurationView.Designer.cs @@ -49,6 +49,7 @@ // // ConfigurationView // + this.BackColor = System.Drawing.SystemColors.Control; this.Controls.Add(this.readSlotsButton); this.Controls.Add(this.clearSlotsButton); this.Controls.Add(this.btnAddSlot); diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.Designer.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.Designer.cs index c0cc8e52b..6ab933468 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.Designer.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.Designer.cs @@ -235,6 +235,9 @@ // // txtLog // + this.txtLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); this.txtLog.Location = new System.Drawing.Point(10, 356); this.txtLog.Multiline = true; this.txtLog.Name = "txtLog"; @@ -246,6 +249,7 @@ // // GenesisCordonelInterfaceView // + this.BackColor = System.Drawing.SystemColors.Control; this.Controls.Add(this.grpSlots); this.Controls.Add(this.btnCancel); this.Controls.Add(this.txtLog); diff --git a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.cs b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.cs index 82d5b35c7..fe757db8c 100644 --- a/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.cs +++ b/TBF/Rig/BridgeComponents/GciBridge/UI/StaraTuraAPI_GciBridge/GenesisCordonelInterfaceView.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using System.Linq; using GciPublicModels = GenesisCordonelInterface.API.PublicModels; namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge @@ -105,7 +106,7 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge StreamingPort = null }; - var result = await _bridge.InitSlotAsync(request); + var result = await _bridge.InitSlotAsync(request, token); LogResult($"InitSlotAsync new slot {slotId}", result); @@ -113,13 +114,25 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge return; } - foreach (var slot in selectedSlots) + var tasks = selectedSlots.Select(async slot => { var request = CreateSlotRequest(slot); - var result = await _bridge.InitSlotAsync(request); + //var result = await _bridge.InitSlotAsync(request, token); + var result = await _bridge.InitSlotWithRetryAsync(request, token); - LogResult($"InitSlotAsync slot {slot.Slot}", result); + return new + { + Slot = slot.Slot, + Result = result + }; + }).ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"InitSlotAsync slot {item.Slot}", item.Result); } RefreshGrid(); @@ -130,13 +143,33 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - foreach (var slot in GetSelectedSlots()) + var selectedSlots = _mainView._batchPanel.GetSelectedGridData(); + + if (selectedSlots.Count == 0) + { + LogResult("UpdateSlotAsync", "No slot selected."); + return; + } + + var tasks = selectedSlots.Select(async slot => { var request = CreateSlotRequest(slot); - var result = await _bridge.UpdateSlotAsync(request); + //var result = await _bridge.UpdateSlotAsync(request, token); + var result = await _bridge.UpdateSlotWithRetryAsync(request, token); - LogResult($"UpdateSlotAsync slot {slot.Slot}", result); + return new + { + Slot = slot.Slot, + Result = result + }; + }).ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"UpdateSlotAsync slot {item.Slot}", item.Result); } RefreshGrid(); @@ -147,11 +180,25 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - foreach (var slot in GetSelectedSlots()) - { - var result = await _bridge.GetSlotAsync(slot.Slot); + var tasks = GetSelectedSlots() + .Select(async slot => + { + //var result = await _bridge.GetSlotAsync(slot.Slot); + var result = await _bridge.GetSlotWithRetryAsync(slot.Slot); - LogResult($"GetSlotAsync slot {slot.Slot}", result); + return new + { + Slot = slot.Slot, + Result = result + }; + }) + .ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"GetSlotAsync slot {item.Slot}", item.Result); } RefreshGrid(); @@ -162,14 +209,30 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - foreach (var slot in GetSelectedSlots()) + var tasks = GetSelectedSlots() + .Select(async slot => + { + //var result = await _bridge.CleanSlotAsync(slot.Slot); + var result = await _bridge.CleanSlotWithRetryAsync(slot.Slot); + + return new + { + Slot = slot.Slot, + Result = result + }; + }) + .ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) { - var result = await _bridge.CleanSlotAsync(slot.Slot); + LogResult($"CleanSlotAsync slot {item.Slot}", item.Result); - LogResult($"CleanSlotAsync slot {slot.Slot}", result); - - RefreshGrid(slot.Slot); + RefreshGrid(item.Slot); } + + _mainView._batchPanel.ClearSelection(); }); } @@ -177,7 +240,10 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - var result = await _bridge.CleanAllSlotsAsync(); + //var result = await _bridge.CleanAllSlotsAsync(); + var result = await _bridge.CleanAllSlotsWithRetryAsync(); + + _mainView._batchPanel.ClearSelection(); LogResult("CleanAllSlotsAsync", result); @@ -189,11 +255,25 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - foreach (var slot in GetSelectedSlots()) - { - var result = await _bridge.GetPcbIdAsync(slot.Slot, token); + var slots = GetSelectedSlots().ToList(); - LogResult($"GetPcbIdAsync slot {slot.Slot}", result); + var tasks = slots.Select(async slot => + { + //var result = await _bridge.GetPcbIdAsync(slot.Slot, token); + var result = await _bridge.GetPcbIdWithRetryAsync(slot.Slot, token); + + return new + { + Slot = slot.Slot, + Result = result + }; + }).ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"GetPcbIdAsync slot {item.Slot}", item.Result); } RefreshGrid(); @@ -204,11 +284,25 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - foreach (var slot in GetSelectedSlots()) - { - var result = await _bridge.ConnectAsync(slot.Slot, token); + var slots = GetSelectedSlots().ToList(); - LogResult($"ConnectAsync slot {slot.Slot}", result); + var tasks = slots.Select(async slot => + { + //var result = await _bridge.ConnectAsyncc(slot.Slot, token); + var result = await _bridge.ConnectWithRetryAsync(slot.Slot, token); + + return new + { + Slot = slot.Slot, + Result = result + }; + }).ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"ConnectAsync slot {item.Slot}", item.Result); } RefreshGrid(); @@ -219,11 +313,25 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - foreach (var slot in GetSelectedSlots()) - { - var result = await _bridge.LoginAsync(slot.Slot, token); + var tasks = GetSelectedSlots() + .Select(async slot => + { + //var result = await _bridge.LoginAsync(slot.Slot, token); + var result = await _bridge.LoginWithRetryAsync(slot.Slot, token); - LogResult($"LoginAsync slot {slot.Slot}", result); + return new + { + Slot = slot.Slot, + Result = result + }; + }) + .ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"LoginAsync slot {item.Slot}", item.Result); } RefreshGrid(); @@ -234,11 +342,25 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge { ExecuteAsync(async token => { - foreach (var slot in GetSelectedSlots()) - { - var result = await _bridge.DisconnectAsync(slot.Slot, token); + var tasks = GetSelectedSlots() + .Select(async slot => + { + //var result = await _bridge.DisconnectAsync(slot.Slot, token); + var result = await _bridge.DisconnectWithRetryAsync(slot.Slot, token); - LogResult($"DisconnectAsync slot {slot.Slot}", result); + return new + { + Slot = slot.Slot, + Result = result + }; + }) + .ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"DisconnectAsync slot {item.Slot}", item.Result); } RefreshGrid(); @@ -254,14 +376,25 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge if (string.IsNullOrWhiteSpace(registerName)) throw new Exception("Read register name is empty."); - foreach (var slot in GetSelectedSlots()) - { - var result = await _bridge.ReadRegisterAsync( - slot.Slot, - registerName, - token); + var tasks = GetSelectedSlots() + .Select(async slot => + { + //var result = await _bridge.ReadRegisterAsync(slot.Slot, registerName, token); + var result = await _bridge.ReadRegisterWithRetryAsync(slot.Slot, registerName, token); - LogResult($"ReadRegisterAsync slot {slot.Slot}, register {registerName}", result); + return new + { + Slot = slot.Slot, + Result = result + }; + }) + .ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult($"ReadRegisterAsync slot {item.Slot}, register {registerName}", item.Result); } RefreshGrid(); @@ -281,36 +414,44 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.StaraTuraAPI_GciBridge if (string.IsNullOrWhiteSpace(valueText)) throw new Exception("Write register value is empty."); - foreach (var slot in GetSelectedSlots()) + object value; + + if (registerName == "GENESISFLOW_LedMode") { - object value; + value = byte.Parse(valueText); + } + else + { + value = valueText; + } - if (registerName == "GENESISFLOW_LedMode") + var tasks = GetSelectedSlots() + .Select(async slot => { - value = byte.Parse(valueText); - } - else - { - value = valueText; - } + //var result = await _bridge.WriteRegisterAsync(slot.Slot, registerName, value, false, false, token); + var result = await _bridge.WriteRegisterWithRetryAsync(slot.Slot, registerName, value, false, false, token); - var result = await _bridge.WriteRegisterAsync( - slot.Slot, - registerName, - value, - false, - false, - token); + return new + { + Slot = slot.Slot, + Result = result + }; + }) + .ToList(); - LogResult($"WriteRegisterAsync slot {slot.Slot}, register {registerName}, value {value}", result); + var results = await Task.WhenAll(tasks); + + foreach (var item in results.OrderBy(x => x.Slot)) + { + LogResult( + $"WriteRegisterAsync slot {item.Slot}, register {registerName}, value {value}", + item.Result); } RefreshGrid(); }); } - - private void btnCancel_Click(object sender, EventArgs e) { _cts?.Cancel(); diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 35fbed894..223d045ab 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -3301,6 +3301,9 @@ GciBridgeCfgCtrl.cs + + WorkerDebugPanel.cs + MainForm.cs