Files
HC_APTBS/ViewModels/PumpIdentificationViewModel.cs
LucianoDev 827b811b39 feat: developer tools page, auto-test orchestrator, BIP display, tests redesign
Bundles several feature streams that have been iterating on the working tree:

- Developer Tools page (Debug-only via DEVELOPER_TOOLS symbol): hosts the
  identification card, manual KWP write + transaction log, ROM/EEPROM dump
  card with progress banner and completion message, persisted custom-commands
  library, persisted EEPROM passwords library. New service primitives:
  IKwpService.SendRawCustomAsync / ReadEepromAsync / ReadRomEepromAsync.
  Persistence mirrors the Clients XML pattern in two new files
  (custom_commands.xml, eeprom_passwords.xml).
- Auto-test orchestrator (IAutoTestOrchestrator + AutoTestState): linear
  K-Line read -> unlock -> bench-on -> test sequence with snackbar UI and
  progress dialog VM, gated on dashboard alarms.
- BIP-STATUS display: BipDisplayViewModel + BipDisplayView, RAM read at
  0x0106 via IKwpService.ReadBipStatusAsync; status definitions in
  BipStatusDefinition.
- Tests page redesign: TestSectionCard + PhaseTileView replacing the old
  TestPlanView/TestRunningView/TestDoneView/TestPreconditionsView/
  TestSectionView controls and their VMs.
- Pump command sliders: Fluent thick-track style with overhang thumb,
  click-anywhere-and-drag, mouse-wheel adjustment.
- Window startup: app.manifest declares PerMonitorV2 DPI awareness,
  MainWindow installs a WM_GETMINMAXINFO hook in OnSourceInitialized and
  maximizes there (after the hook is in place) so the app fits the work
  area exactly on any display configuration.
- Misc: PercentToPixelsConverter, seed_aliases.py one-shot pump-alias
  importer, tools/Import-BipStatus.ps1, kline_eeprom_spec.md and
  dump-functions reference docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:59:50 +02:00

344 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// ViewModel for the pump identification user control.
///
/// <para>
/// Owns the pump selector (dropdown), K-Line ECU read command,
/// and all read-only K-Line fields (DFI, serial number, software versions, etc.).
/// After a successful K-Line read the pump is auto-selected from the database
/// using the ECU's pump identifier — matching the old-source behaviour where
/// <c>OnPumpConnectClick</c> would call <c>LoadPump</c> after reading.
/// </para>
/// </summary>
public sealed partial class PumpIdentificationViewModel : ObservableObject
{
// ── Services ──────────────────────────────────────────────────────────────
private readonly IKwpService _kwp;
private readonly IConfigurationService _config;
private readonly ILocalizationService _loc;
private readonly IAppLogger _log;
private const string LogId = "PumpIdentVM";
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>Initialises the ViewModel with the required services.</summary>
public PumpIdentificationViewModel(
IKwpService kwpService,
IConfigurationService configService,
ILocalizationService loc,
IAppLogger logger)
{
_kwp = kwpService;
_config = configService;
_loc = loc;
_log = logger;
// Wire KWP progress events to local properties.
_kwp.ProgressChanged += (pct, msg) => App.Current.Dispatcher.Invoke(() =>
{
ProgressPercent = pct;
ProgressMessage = msg;
});
// Start loading the pump as soon as the identifier is read from ROM,
// before the full K-Line read completes. Use BeginInvoke so the K-Line
// background worker thread returns immediately and the read keeps progressing
// even if the UI thread is momentarily blocked (e.g. modal voltage warning).
_kwp.PumpIdentified += (pumpId) => App.Current.Dispatcher.BeginInvoke(() =>
{
_log.Info(LogId, $"PumpIdentified handler: '{pumpId}'");
KlinePumpId = pumpId;
AutoSelectPumpByKlineId(pumpId);
});
// Track K-Line session state for Disconnect button enable/disable.
_kwp.KLineStateChanged += state =>
App.Current.Dispatcher.Invoke(() => KLineState = state);
}
// ── Pump selection ────────────────────────────────────────────────────────
/// <summary>List of available pump IDs loaded from the pump database.</summary>
public ObservableCollection<string> PumpIds { get; } = new();
/// <summary>Currently selected pump ID in the dropdown.</summary>
[ObservableProperty]
private string? _selectedPumpId;
/// <summary>Currently loaded pump definition.</summary>
[ObservableProperty]
private PumpDefinition? _currentPump;
/// <summary>
/// Raised when <see cref="CurrentPump"/> changes so the parent ViewModel
/// can react (e.g. register CAN parameters, update test display).
/// </summary>
public event Action<PumpDefinition?>? PumpChanged;
/// <summary>
/// Raised at the end of a successful K-Line read with the matched pump ID and
/// the ECU serial number (may be empty if the ECU did not return one). Used by
/// the parent ViewModel to detect a physical pump swap when the model ID is unchanged.
/// </summary>
public event Action<string /*pumpId*/, string /*serial*/>? KlineReadCompleted;
/// <summary>Populates the pump ID list from the configuration database.</summary>
public void LoadPumpIds()
{
PumpIds.Clear();
foreach (var id in _config.GetPumpIds())
PumpIds.Add(id);
}
partial void OnSelectedPumpIdChanged(string? value)
{
if (string.IsNullOrEmpty(value)) return;
LoadPump(value);
}
private void LoadPump(string pumpId)
{
_log.Info(LogId, $"LoadPump: {pumpId}");
var pump = _config.LoadPump(pumpId);
if (pump == null)
{
_log.Warning(LogId, $"Pump {pumpId} not found in database.");
return;
}
CurrentPump = pump;
PumpChanged?.Invoke(pump);
_log.Info(LogId, $"Loaded pump: {pumpId}");
}
// ── K-Line display properties ─────────────────────────────────────────────
/// <summary>K-Line read progress percentage (0100).</summary>
[ObservableProperty] private int _progressPercent;
/// <summary>K-Line read progress status message.</summary>
[ObservableProperty] private string _progressMessage = string.Empty;
/// <summary>True while a K-Line read is in progress.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ReadKlineCommand))]
[NotifyCanExecuteChangedFor(nameof(DisconnectKLineCommand))]
private bool _isReading;
/// <summary>Current K-Line session state for Disconnect button enable/disable.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(DisconnectKLineCommand))]
private KLineConnectionState _kLineState = KLineConnectionState.Disconnected;
/// <summary>DFI calibration angle read from ECU EEPROM.</summary>
[ObservableProperty] private string _klineDfi = "-";
/// <summary>Pump identifier string read from ECU ROM.</summary>
[ObservableProperty] private string _klinePumpId = string.Empty;
/// <summary>ECU serial number (EEPROM 0x0080).</summary>
[ObservableProperty] private string _klineSerialNumber = string.Empty;
/// <summary>Model reference from ECU identification text.</summary>
[ObservableProperty] private string _klineModelRef = string.Empty;
/// <summary>Data record from ECU identification text.</summary>
[ObservableProperty] private string _klineDataRecord = string.Empty;
/// <summary>Pump control field from ECU identification text (V2+ pumps).</summary>
[ObservableProperty] private string _klinePumpControl = string.Empty;
/// <summary>Customer change index read from ECU ROM.</summary>
[ObservableProperty] private string _klineModelIndex = string.Empty;
/// <summary>Software version 1 from ECU identification text.</summary>
[ObservableProperty] private string _klineSwVersion1 = string.Empty;
/// <summary>Software version 2 from ECU identification text (V2+ pumps).</summary>
[ObservableProperty] private string _klineSwVersion2 = string.Empty;
/// <summary>Fault code text returned by the ECU.</summary>
[ObservableProperty] private string _klineErrors = string.Empty;
/// <summary>Connection error message (empty when OK, auto-collapsed in the UI).</summary>
[ObservableProperty] private string _klineConnectError = string.Empty;
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>Reads all pump ECU data over K-Line in a background task.</summary>
[RelayCommand(CanExecute = nameof(CanReadKline))]
private async Task ReadKlineAsync()
{
// Always freshly detect the FTDI device — never rely on a stored config value.
// This matches the old source where Config.GetKLinePortName() was called every time.
var port = _kwp.DetectKLinePort();
if (string.IsNullOrEmpty(port))
{
App.Current.Dispatcher.Invoke(() =>
KlineConnectError = _loc.GetString("Error.KLineNotFound"));
return;
}
// Use the pump's KWP version if one is selected; default to 0 otherwise.
int kwpVersion = CurrentPump?.KwpVersion ?? 0;
IsReading = true;
try
{
var info = await _kwp.ReadAllInfoAsync(port, kwpVersion);
info.TryGetValue(KlineKeys.Dfi, out string? dfi);
info.TryGetValue(KlineKeys.Errors, out string? errors);
info.TryGetValue(KlineKeys.PumpId, out string? pumpId);
info.TryGetValue(KlineKeys.SerialNumber, out string? serial);
info.TryGetValue(KlineKeys.ModelReference, out string? modelRef);
info.TryGetValue(KlineKeys.ModelIndex, out string? modelIndex);
info.TryGetValue(KlineKeys.SwVersion1, out string? sw1);
info.TryGetValue(KlineKeys.SwVersion2, out string? sw2);
info.TryGetValue(KlineKeys.DataRecord, out string? dataRecord);
info.TryGetValue(KlineKeys.PumpControl, out string? pumpControl);
info.TryGetValue(KlineKeys.ConnectError, out string? connectErr);
info.TryGetValue(KlineKeys.Result, out string? result);
App.Current.Dispatcher.Invoke(() =>
{
KlineDfi = dfi ?? "-";
KlineErrors = errors ?? string.Empty;
// Preserve the value set by the PumpIdentified event if the
// dictionary entry is empty (e.g. read failed after ident).
if (!string.IsNullOrEmpty(pumpId))
KlinePumpId = pumpId;
KlineSerialNumber = serial ?? string.Empty;
KlineModelRef = modelRef ?? string.Empty;
KlineModelIndex = modelIndex ?? string.Empty;
KlineSwVersion1 = sw1 ?? string.Empty;
KlineSwVersion2 = sw2 ?? string.Empty;
KlineDataRecord = dataRecord ?? string.Empty;
KlinePumpControl = pumpControl ?? string.Empty;
KlineConnectError = connectErr ?? string.Empty;
});
// Pump auto-selection by pumpID/alias/substring already happened mid-read
// via the PumpIdentified event. If still unmatched, try ModelReference now
// that the full ECU text has been read (ModelRef arrives later than pumpID).
if (CurrentPump == null && !string.IsNullOrEmpty(modelRef))
App.Current.Dispatcher.Invoke(() => TryAutoSelectByModelRef(modelRef!));
// Attach K-Line info to the (now possibly auto-selected) pump.
if (CurrentPump != null)
CurrentPump.KlineInfo = info;
// Notify parent VM so it can detect physical pump swaps when the model ID is unchanged.
KlineReadCompleted?.Invoke(CurrentPump?.Id ?? string.Empty, serial ?? string.Empty);
}
finally
{
IsReading = false;
}
}
private bool CanReadKline() => !IsReading;
/// <summary>Closes the persistent K-Line session.</summary>
[RelayCommand(CanExecute = nameof(CanDisconnectKLine))]
private void DisconnectKLine()
{
try
{
_kwp.Disconnect();
}
catch (Exception ex)
{
_log.Error(LogId, $"DisconnectKLine: {ex.Message}");
}
}
private bool CanDisconnectKLine() => !IsReading && KLineState == KLineConnectionState.Connected;
// ── Helpers ───────────────────────────────────────────────────────────────
/// <summary>
/// Tries to match a K-Line pump identifier to a pump in the database and auto-select it.
/// Resolution order:
/// <list type="number">
/// <item>Exact match — K-Line ID equals a canonical pump ID.</item>
/// <item>Alias match — K-Line ID is listed under a pump's <c>&lt;Aliases&gt;&lt;KlineId&gt;</c> entries.</item>
/// <item>Substring match — pump ID appears inside the K-Line ident string (legacy fallback for noisy ROM reads).</item>
/// </list>
/// ModelReference-based equivalence is handled separately after the full K-Line read completes
/// (see <see cref="TryAutoSelectByModelRef"/>).
/// </summary>
private void AutoSelectPumpByKlineId(string klinePumpId)
{
var trimmed = (klinePumpId ?? string.Empty).Trim();
if (trimmed.Length == 0)
{
_log.Warning(LogId, "AutoSelectPumpByKlineId: empty K-Line identifier.");
return;
}
// 1. Direct match — the K-Line ID is itself a pump ID in the database.
foreach (var id in PumpIds)
{
if (string.Equals(id, trimmed, StringComparison.OrdinalIgnoreCase))
{
_log.Info(LogId, $"Auto-selected '{id}' from K-Line id '{trimmed}' (exact match).");
SelectedPumpId = id;
return;
}
}
// 2. Alias match — the K-Line ID is registered as an alias under a canonical pump.
var aliased = _config.FindPumpIdByKlineAlias(trimmed);
if (!string.IsNullOrEmpty(aliased))
{
_log.Info(LogId, $"Auto-selected '{aliased}' from K-Line id '{trimmed}' (alias KlineId).");
SelectedPumpId = aliased;
return;
}
// 3. Substring match — the K-Line ident string may contain the pump ID.
foreach (var id in PumpIds)
{
if (trimmed.Contains(id, StringComparison.OrdinalIgnoreCase))
{
_log.Info(LogId, $"Auto-selected '{id}' from K-Line id '{trimmed}' (substring match).");
SelectedPumpId = id;
return;
}
}
_log.Warning(LogId,
$"K-Line pump ID '{trimmed}' not found in pump database ({PumpIds.Count} candidates).");
}
/// <summary>
/// Late-stage fallback used when the full K-Line read has completed and no pump was
/// matched by ID/alias/substring. Looks up the canonical pump ID by ModelReference alias
/// (e.g. <c>ME190297C150</c>). No-op if a pump is already selected.
/// </summary>
private void TryAutoSelectByModelRef(string modelRef)
{
if (CurrentPump != null) return;
var trimmed = (modelRef ?? string.Empty).Trim();
if (trimmed.Length == 0) return;
var canonical = _config.FindPumpIdByModelRef(trimmed);
if (string.IsNullOrEmpty(canonical)) return;
_log.Info(LogId, $"Auto-selected '{canonical}' from ModelRef '{trimmed}' (alias ModelRef).");
SelectedPumpId = canonical;
}
}
}