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:
2026-04-19 22:25:00 +02:00
parent 0280a2fad1
commit 197e9d1775
26 changed files with 1638 additions and 515 deletions

View File

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