Compare commits

...

3 Commits

23 changed files with 2111 additions and 415 deletions

View File

@ -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<int, ApiWorker> _workers = new ConcurrentDictionary<int, ApiWorker>();
private readonly ConcurrentDictionary<int, bool> _selectedSlots = new ConcurrentDictionary<int, bool>();
@ -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<MeterBatchDebugStatus> GetMeterBatchDebugStatuses()
{
return _meterBatch.ListOfMeters
.OfType<GenesisMeter>()
List<GenesisMeter> meters;
//just snapshot of list under lock
lock (_meterBatchLock)
{
meters = _meterBatch.ListOfMeters
.OfType<GenesisMeter>()
.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<GenesisMeter>()
.FirstOrDefault(m => m.Slot == slot);
lock (_meterBatchLock)
{
return _meterBatch.ListOfMeters
.OfType<GenesisMeter>()
.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<GenesisMeter>())
{
meter.DisposeMeter();
}
}
}
private void EnsureConnected(GenesisMeter meter)
@ -201,10 +263,11 @@ 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)
{
UpdateMeterForSlot(existingMeter, slot, cfg, pwd, req, str);
return SlotAlreadyExists(slot, existingMeter);
}
@ -252,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)
{
@ -292,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;
}
@ -352,7 +418,7 @@ namespace GenesisCordonelInterface.API
{
SlotId = slot,
Success = true,
Message = "Meter already exists.",
Message = "Meter already exists. Meter config was only updated",
PcbId = meter != null ? meter.SerialNumber : null,
Created = false,
Updated = false,
@ -393,7 +459,7 @@ namespace GenesisCordonelInterface.API
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeter(slot);
var meter = GetMeterThreadSafe(slot);
if (meter == null)
{
@ -423,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)
@ -457,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<GenesisMeter>()
.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<GenesisMeter>()
.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)
{
@ -511,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;
@ -560,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
{
@ -719,7 +775,7 @@ namespace GenesisCordonelInterface.API
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeter(slot);
var meter = GetMeterThreadSafe(slot);
if (!meter.IsLoggedOn)
{
@ -793,7 +849,7 @@ namespace GenesisCordonelInterface.API
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeter(slot);
var meter = GetMeterThreadSafe(slot);
if (!meter.IsConnected)
{
@ -884,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)
@ -923,6 +994,7 @@ namespace GenesisCordonelInterface.API
if (_workers.TryRemove(slot, out var worker))
{
worker.Dispose();
LogInfo(operation, $"Worker disposed. Slot={slot}");
}
}
@ -947,9 +1019,9 @@ namespace GenesisCordonelInterface.API
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeter(slot);
var meter = GetMeterThreadSafe(slot);
meter.Logout();
//meter.Logout();
string pcbId = meter.GetPcbId();
@ -1000,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)
};
@ -1039,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(
@ -1062,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))
@ -1173,7 +1244,7 @@ namespace GenesisCordonelInterface.API
LogInfo(operation, $"Start. Slot={slot}");
GenesisMeter meter = GetMeter(slot);
GenesisMeter meter = GetMeterThreadSafe(slot);
meter?.SetPassword(password);//SetupFromExternConfig();

View File

@ -58,7 +58,7 @@ namespace GenesisCordonelInterface.API
#endregion
#region ================================== INIT ==================================
#region ================================== INIT/UPDATE/GET slot ==================================
public async Task<GciInitSlotResult> 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<GciAllSlotsInfo> 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<GciCleanAllSlotsResult> 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<PublicModels.GciLoginResult> LoginOneSlotAsync(
public async Task<PublicModels.GciLoginResult> 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<RegisterReadResult> ReadRegisterAsync(
public async Task<RegisterReadResult> 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<RegisterWriteResult> 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<GciSetPasswordResult> 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<GciSetPasswordResult> 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<WorkerDebugStatus> GetWorkerDebugStatuses()
{

View File

@ -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)

View File

@ -49,6 +49,18 @@ namespace GenesisCordonelInterface.API
/// </summary>
public class PublicModels
{
public static bool HideSensitiveValues { get; set; } = true;
private static string FormatPassword(string password)
{
if (!HideSensitiveValues)
return password ?? "<null>";
if (string.IsNullOrEmpty(password))
return "<empty>";
return "********";
}
/// <summary>
/// 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);
}
}

View File

@ -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");
/// <summary>
/// Thread-safe FIFO queue holding work items.
/// </summary>
@ -236,9 +238,9 @@ namespace GenesisCordonelInterface.Core.Threading
/// Enqueues a function returning a value for sequential execution.
/// </summary>
public Task<T> RunAsync<T>(
Func<T> action,
CancellationToken token = default(CancellationToken),
string operationName = null)
Func<T> 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<T>();
var tcs = new TaskCompletionSource<T>(
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;
}

View File

@ -0,0 +1,95 @@
using System;
using System.Threading.Tasks;
namespace GenesisCordonelInterface.Core.Threading
{
public sealed class RetryResult<T>
{
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 ? "<null>" : Result.ToString());
}
}
public static class RetryWorker
{
public static async Task<RetryResult<T>> RunWithRetryAsync<T>(
Func<Task<T>> action,
Func<T, bool> isSuccess,
Action<string> log,
Action<string, T> 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<T>
{
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<T>
{
Result = lastResult,
Success = false,
Attempts = maxAttempts,
Duration = DateTime.Now - started,
TimedOut = timedOut
};
}
public static void EnsureSuccess<T>(
RetryResult<T> retryResult,
string operationName)
{
if (!retryResult.Success)
throw new Exception($"{operationName} failed after {retryResult.Attempts} attempts.");
}
}
}

View File

@ -63,6 +63,7 @@
<Compile Include="Core\Logging\UiLogBus.cs" />
<Compile Include="Core\Logging\UiTarget.cs" />
<Compile Include="Core\Threading\ApiWorker\ApiWorker.cs" />
<Compile Include="Core\Threading\RetryWorker\RetryWorker.cs" />
<Compile Include="UI\Debug\MeterBatchConfigPanel.cs">
<SubType>UserControl</SubType>
</Compile>

File diff suppressed because it is too large Load Diff

View File

@ -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
/// </summary>
public class PublicModels
{
public static bool HideSensitiveValues { get; set; } = true;
private static string FormatPassword(string password)
{
if (!HideSensitiveValues)
return password ?? "<null>";
if (string.IsNullOrEmpty(password))
return "<empty>";
return "********";
}
/// <summary>
/// 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) ? "<empty>" : Password,
FormatPassword(Password),
Message);
}
}
public class GciFullLoginResult
{
public int SlotId { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
public RetryResult<GciPublicModels.GciConnectResult> ConnectResult { get; set; }
public RetryResult<GciPublicModels.GciGetPcbIdResult> PcbResult { get; set; }
public RetryResult<UdsPasswordResult> PasswordResult { get; set; }
public RetryResult<GciPublicModels.GciSetPasswordResult> SetPasswordResult { get; set; }
public RetryResult<GciPublicModels.GciLoginResult> 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
}
}

View File

@ -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)

View File

@ -31,8 +31,15 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Debug
/// </summary>
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

View File

@ -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;

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -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;

View File

@ -4,19 +4,70 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI.Grid
{
public static class MeterGridConfigProvider
{
public enum MeterGridColumnType
{
Text,
CheckBox
}
public static List<MeterGridColumnConfig> GetDefault()
{
return new List<MeterGridColumnConfig>
{
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
}
};
}
}

View File

@ -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<string> 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<string> 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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// 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
/// </summary>
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<List<GenesisCordonelInterface.API.PublicModels.MeterBatchDebugStatus>> MeterBatchStatusChanged;// object status from place of his location
public event Action<List<PublicModels.MeterBatchDebugStatus>> 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);
/// <summary>
/// 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.
/// </summary>
private readonly object _uiLogLock = new object();
private readonly Queue<string> _pendingUiLogs = new Queue<string>();
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();
}
/// <summary>
/// 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.
/// </summary>
/// Plynule pridavanie do mema
/*private void UiLogFlushTimer_Tick(object sender, EventArgs e)
{
if (IsDisposed || !IsHandleCreated)
return;
List<string> messages = new List<string>();
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<string> messages = new List<string>();
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;
}
}
/// <summary>
/// Initializes debug/configuration panels hosted inside MainView.
/// </summary>
private void InitializeDebugPanels()
{
_batchPanel = new Debug.MeterBatchConfigPanel(_mainform, this)
@ -72,6 +237,10 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI
}));
}
/// <summary>
/// Safely adjusts SplitContainer distance while respecting
/// minimum panel sizes and current control dimensions.
/// </summary>
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);
}
/// <summary>
/// Saves current slot configuration from the grid into backend storage.
/// </summary>
public void SaveSlots()
{
var data = _batchPanel.GetGridData();
_laatzenApi.SaveSlotSetup(data);
}
/// <summary>
/// Switches currently displayed GCI view inside the host panel.
/// </summary>
private void button2_Click(object sender, EventArgs e)
{
SwitchGciView(
@ -236,6 +415,9 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI
new MeterInitView(_gciApi, AddSlotRow, SaveSlots));
}
/// <summary>
/// Clears the main UI log window.
/// </summary>
public void ClearLog()
{
rtbMainLog.Clear();
@ -244,6 +426,13 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI
#endregion
#region GLOBAL LOGGING to memo in this view
/// <summary>
/// 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.
/// </summary>
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<string, string>(UiLogBus_MessageReceived), loggerName, msg);
return;
}
_pendingUiLogs.Enqueue(msg);
AppendLogMessage(msg);
while (_pendingUiLogs.Count > 1000)
_pendingUiLogs.Dequeue();
}
}
/// <summary>
/// Splits multiline log messages and appends each line separately.
/// </summary>
private void AppendLogMessage(string msg)
{
string[] lines = msg.Replace("\r\n", "\n").Split('\n');
@ -274,6 +466,13 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI
}
}
/// <summary>
/// Appends a single styled log line into RichTextBox.
///
/// Performs syntax highlighting for:
/// - log levels
/// - important keywords
/// </summary>
private void AppendStyledLine(string line)
{
if (rtbMainLog == null || rtbMainLog.IsDisposed)
@ -297,6 +496,9 @@ namespace TBF.Rig.BridgeComponents.GciBridge.UI
HighlightKeywordsInLine(line, start);
}
/// <summary>
/// Returns color associated with a log level.
/// </summary>
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" })
};
/// <summary>
/// Highlights important keywords inside a log line.
/// </summary>
private void HighlightKeywordsInLine(string line, int lineStartIndex)
{
foreach (var group in KeywordGroups)

View File

@ -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;
}
}

View File

@ -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<PublicModels.MeterBatchDebugStatus> GetSelectedSlots()
private List<GciPublicModels.MeterBatchDebugStatus> 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<T>(
string operationName,
RetryResult<T> retryResult)
{
if (retryResult == null)
{
Log($"{operationName}: <not executed>");
return;
}
LogResult(
$"{operationName} | success={retryResult.Success} | attempts={retryResult.Attempts} | duration={retryResult.Duration.TotalSeconds:F1}s | timeout={retryResult.TimedOut}",
retryResult.Result);
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -3301,6 +3301,9 @@
<EmbeddedResource Include="Rig\BridgeComponents\GciBridge\GciBridgeCfgCtrl.resx">
<DependentUpon>GciBridgeCfgCtrl.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Rig\BridgeComponents\GciBridge\UI\Debug\WorkerDebugPanel.resx">
<DependentUpon>WorkerDebugPanel.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Rig\BridgeComponents\GciBridge\UI\MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>