Files
HC_APTBS/ViewModels/PumpIdentificationViewModel.cs
LucianoDev 37d099cdbd feat: add Ford VP44 unlock progress dialog, K-Line fast unlock, localization, safety dialogs, and settings
Unlock progress UI:
- UnlockProgressDialog with dark-themed progress ring, phase indicator, elapsed
  time, and cancel/close buttons (non-modal, draggable borderless window)
- UnlockProgressViewModel with event-driven progress tracking via IUnlockService
- Triggers on pump selection (manual or K-Line auto-detect), not test start

UnlockService rewrite:
- Persistent CAN senders that outlive the unlock sequence (StopSenders on pump change)
- Concurrent K-Line fast unlock: awaits session Connected, sends RAM timer shortcut
  ({02 88 02 03 A8 01 00}), verifies via CAN TestUnlock before skipping wait
- Fix Type 1 verification (Value == 0 means unlocked, was inverted)

K-Line fast unlock support:
- IKwpService.TryFastUnlockAsync / KwpService implementation

Additional features:
- ILocalizationService with ES/EN resource dictionaries and runtime switching
- Safety dialogs: VoltageWarning, OilPumpConfirm, RpmSafetyWarning
- SettingsDialog for app configuration
- BenchService enhancements, ConfigurationService improvements, PDF report updates
- All UI strings localized via DynamicResource

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 13:22:48 +02:00

281 lines
12 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.
_kwp.PumpIdentified += (pumpId) => App.Current.Dispatcher.Invoke(() =>
{
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>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)
{
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 now happens via the PumpIdentified event
// mid-read, so there is no need to call AutoSelectPumpByKlineId here.
// Attach K-Line info to the (now possibly auto-selected) pump.
if (CurrentPump != null)
CurrentPump.KlineInfo = info;
}
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.
/// If the K-Line ID is directly in the pump list, select it. Otherwise, try to find
/// a pump whose ID is contained in the K-Line identifier string.
/// </summary>
private void AutoSelectPumpByKlineId(string klinePumpId)
{
// Direct match — the K-Line ID is itself a pump ID in the database.
if (PumpIds.Contains(klinePumpId))
{
App.Current.Dispatcher.Invoke(() => SelectedPumpId = klinePumpId);
return;
}
// Substring match — the K-Line ident string may contain the pump ID.
foreach (var id in PumpIds)
{
if (klinePumpId.Contains(id, StringComparison.OrdinalIgnoreCase))
{
App.Current.Dispatcher.Invoke(() => SelectedPumpId = id);
return;
}
}
_log.Warning(LogId, $"K-Line pump ID '{klinePumpId}' not found in pump database.");
}
}
}