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 { /// /// ViewModel for the pump identification user control. /// /// /// 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 /// OnPumpConnectClick would call LoadPump after reading. /// /// 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 ─────────────────────────────────────────────────────────── /// Initialises the ViewModel with the required services. 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 ──────────────────────────────────────────────────────── /// List of available pump IDs loaded from the pump database. public ObservableCollection PumpIds { get; } = new(); /// Currently selected pump ID in the dropdown. [ObservableProperty] private string? _selectedPumpId; /// Currently loaded pump definition. [ObservableProperty] private PumpDefinition? _currentPump; /// /// Raised when changes so the parent ViewModel /// can react (e.g. register CAN parameters, update test display). /// public event Action? PumpChanged; /// /// 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. /// public event Action? KlineReadCompleted; /// Populates the pump ID list from the configuration database. 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 ───────────────────────────────────────────── /// K-Line read progress percentage (0–100). [ObservableProperty] private int _progressPercent; /// K-Line read progress status message. [ObservableProperty] private string _progressMessage = string.Empty; /// True while a K-Line read is in progress. [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ReadKlineCommand))] [NotifyCanExecuteChangedFor(nameof(DisconnectKLineCommand))] private bool _isReading; /// Current K-Line session state for Disconnect button enable/disable. [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DisconnectKLineCommand))] private KLineConnectionState _kLineState = KLineConnectionState.Disconnected; /// DFI calibration angle read from ECU EEPROM. [ObservableProperty] private string _klineDfi = "-"; /// Pump identifier string read from ECU ROM. [ObservableProperty] private string _klinePumpId = string.Empty; /// ECU serial number (EEPROM 0x0080). [ObservableProperty] private string _klineSerialNumber = string.Empty; /// Model reference from ECU identification text. [ObservableProperty] private string _klineModelRef = string.Empty; /// Data record from ECU identification text. [ObservableProperty] private string _klineDataRecord = string.Empty; /// Pump control field from ECU identification text (V2+ pumps). [ObservableProperty] private string _klinePumpControl = string.Empty; /// Customer change index read from ECU ROM. [ObservableProperty] private string _klineModelIndex = string.Empty; /// Software version 1 from ECU identification text. [ObservableProperty] private string _klineSwVersion1 = string.Empty; /// Software version 2 from ECU identification text (V2+ pumps). [ObservableProperty] private string _klineSwVersion2 = string.Empty; /// Fault code text returned by the ECU. [ObservableProperty] private string _klineErrors = string.Empty; /// Connection error message (empty when OK, auto-collapsed in the UI). [ObservableProperty] private string _klineConnectError = string.Empty; // ── Commands ────────────────────────────────────────────────────────────── /// Reads all pump ECU data over K-Line in a background task. [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; /// Closes the persistent K-Line session. [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 ─────────────────────────────────────────────────────────────── /// /// Tries to match a K-Line pump identifier to a pump in the database and auto-select it. /// Resolution order: /// /// Exact match — K-Line ID equals a canonical pump ID. /// Alias match — K-Line ID is listed under a pump's <Aliases><KlineId> entries. /// Substring match — pump ID appears inside the K-Line ident string (legacy fallback for noisy ROM reads). /// /// ModelReference-based equivalence is handled separately after the full K-Line read completes /// (see ). /// 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)."); } /// /// 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. ME190297C150). No-op if a pump is already selected. /// 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; } } }