feat: redesign dashboard with Fluent KPI tiles, connection strip, and devices column
- Replace LCD-style readings with a 3×2 KPI tile grid (Fluent card surfaces, 52pt values) - Add persistent top connection strip with horizontal chips + pump name badge - Add elapsed test timer (DispatcherTimer, mm:ss) to Test Summary card - Restyle Test Summary and Active Alarms with Fluent brushes/iconography - Add Devices column (CAN / K-Line / Bench tiles) between KPI grid and test/alarms - Enumerates attached PCAN USB channels via PCAN_ATTACHED_CHANNELS API - Enumerates FTDI K-Line adapters via existing FtdiInterface helpers - Click to connect/disconnect; confirmation dialog when session active or test running - Hover tint: blue = will connect, red = will disconnect; Bench row is read-only stub - Extend ICanService with SelectedChannel + EnumerateAttachedChannels() - Expose IKwpService.ConnectedPort for active session device tracking - Add DeviceRow button style with MultiDataTrigger hover colour logic - Add 30+ new localization keys (ES + EN) for KPI labels, devices, confirmations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,22 @@ namespace HC_APTBS.Services
|
||||
/// <summary>True when the CAN read thread is running and the channel is open.</summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The PCAN channel handle that will be used on the next <see cref="Connect"/> call.
|
||||
/// Defaults to the channel supplied at construction.
|
||||
/// Throws <see cref="System.InvalidOperationException"/> when set while <see cref="IsConnected"/> is true.
|
||||
/// </summary>
|
||||
TPCANHandle SelectedChannel { get; set; }
|
||||
|
||||
// ── Discovery ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates PCAN USB channels that are physically attached to the system.
|
||||
/// Returns an empty list if no adapters are connected or if the PCAN-Basic DLL
|
||||
/// is unavailable. Never throws.
|
||||
/// </summary>
|
||||
System.Collections.Generic.IReadOnlyList<AttachedPcanChannel> EnumerateAttachedChannels();
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -110,6 +110,12 @@ namespace HC_APTBS.Services
|
||||
/// </summary>
|
||||
string? DetectKLinePort();
|
||||
|
||||
/// <summary>
|
||||
/// The FTDI serial number of the device that is currently holding an open
|
||||
/// K-Line session, or <see langword="null"/> when no session is active.
|
||||
/// </summary>
|
||||
string? ConnectedPort { get; }
|
||||
|
||||
// ── Mid-read notifications ────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -73,6 +73,9 @@ namespace HC_APTBS.Services.Impl
|
||||
/// <inheritdoc/>
|
||||
public KLineConnectionState KLineState => _kLineState;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? ConnectedPort => _connectedPort;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <param name="logger">Application logger.</param>
|
||||
@@ -289,7 +292,7 @@ namespace HC_APTBS.Services.Impl
|
||||
Report(85, "Reading fault codes...");
|
||||
kwp.KeepAlive();
|
||||
var faultCodes = kwp.ReadFaultCodes();
|
||||
result[KlineKeys.Errors] = faultCodes.Count > 0
|
||||
result[KlineKeys.Errors] = faultCodes?.Count > 0
|
||||
? string.Join(Environment.NewLine, faultCodes)
|
||||
: KlineKeys.NoErrors;
|
||||
|
||||
@@ -418,7 +421,7 @@ namespace HC_APTBS.Services.Impl
|
||||
Report(85, "Reading fault codes...");
|
||||
kwp.KeepAlive();
|
||||
var faultCodes = kwp.ReadFaultCodes();
|
||||
result[KlineKeys.Errors] = faultCodes.Count > 0
|
||||
result[KlineKeys.Errors] = faultCodes?.Count > 0
|
||||
? string.Join(Environment.NewLine, faultCodes)
|
||||
: KlineKeys.NoErrors;
|
||||
|
||||
@@ -621,14 +624,15 @@ namespace HC_APTBS.Services.Impl
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
_busLock.Wait();
|
||||
try
|
||||
{
|
||||
_log.Info(LogId, "TryFastUnlock: sending unlock command over K-Line");
|
||||
var packets = _sessionKwp.SendCustom(
|
||||
var packets = _sessionKwp!.SendCustom(
|
||||
new List<byte> { 0x02, 0x88, 0x02, 0x03, 0xA8, 0x01, 0x00 });
|
||||
|
||||
bool nak = packets.Count == 1
|
||||
&& packets[0] is HC_APTBS.Infrastructure.Kwp.Packets.NakPacket;
|
||||
&& packets[0] is NakPacket;
|
||||
|
||||
_log.Info(LogId, $"TryFastUnlock: {(nak ? "NAK — pump rejected" : "ACK — pump unlocked")}");
|
||||
return !nak;
|
||||
@@ -638,6 +642,10 @@ namespace HC_APTBS.Services.Impl
|
||||
_log.Warning(LogId, $"TryFastUnlock failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busLock.Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -686,33 +694,33 @@ namespace HC_APTBS.Services.Impl
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
// Non-blocking try-acquire: if an operation holds the lock
|
||||
// we skip this cycle — the operation itself keeps the bus alive.
|
||||
if (await _busLock.WaitAsync(0, ct))
|
||||
{
|
||||
try
|
||||
{
|
||||
_sessionKwp!.KeepAlive();
|
||||
}
|
||||
catch (OperationCanceledException) { return; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"Keep-alive failed: {ex.Message}");
|
||||
CleanupSession();
|
||||
SetState(KLineConnectionState.Failed);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(KeepAliveIntervalMs, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { return; }
|
||||
|
||||
// Non-blocking try-acquire: if an operation holds the lock
|
||||
// we skip this cycle — the operation itself keeps the bus alive.
|
||||
if (!await _busLock.WaitAsync(0, ct))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_sessionKwp!.KeepAlive();
|
||||
}
|
||||
catch (OperationCanceledException) { return; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"Keep-alive failed: {ex.Message}");
|
||||
CleanupSession();
|
||||
SetState(KLineConnectionState.Failed);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,7 +754,7 @@ namespace HC_APTBS.Services.Impl
|
||||
var codes = _sessionKwp.ReadFaultCodes();
|
||||
_sessionKwp.KeepAlive();
|
||||
Report(100, "Done.");
|
||||
return codes.Count > 0
|
||||
return codes?.Count > 0
|
||||
? string.Join(Environment.NewLine, codes)
|
||||
: KlineKeys.NoErrors;
|
||||
}
|
||||
@@ -773,7 +781,7 @@ namespace HC_APTBS.Services.Impl
|
||||
var codes = _sessionKwp.ReadFaultCodes();
|
||||
_sessionKwp.KeepAlive();
|
||||
Report(100, "Done.");
|
||||
return codes.Count > 0
|
||||
return codes?.Count > 0
|
||||
? string.Join(Environment.NewLine, codes)
|
||||
: KlineKeys.NoErrors;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user