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>
344 lines
16 KiB
C#
344 lines
16 KiB
C#
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 (0–100).</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><Aliases><KlineId></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;
|
||
}
|
||
}
|
||
}
|