Files
HC_APTBS/ViewModels/Dialogs/UnlockProgressViewModel.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

215 lines
9.5 KiB
C#
Raw Permalink 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.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels.Dialogs
{
/// <summary>
/// ViewModel for the Ford VP44 immobilizer unlock progress dialog.
/// Tracks Phase 1 (CAN flood ~600 s), Phase 2 (handshake ~4 s), and verification.
/// Equivalent to the old <c>WUnlocker</c> window.
/// </summary>
public sealed partial class UnlockProgressViewModel : ObservableObject, IDisposable
{
private readonly IUnlockService _unlockService;
private readonly ILocalizationService _loc;
private readonly CancellationTokenSource _cts;
/// <summary>Regex to extract percentage and elapsed time from Phase 1 status messages.</summary>
private static readonly Regex ProgressRegex =
new(@"Unlocking\.\.\. (\d+)% \((\d{2}:\d{2})\)", RegexOptions.Compiled);
/// <summary>Creates the ViewModel and subscribes to unlock service events.</summary>
/// <param name="unlockService">The unlock service to monitor.</param>
/// <param name="unlockType">Pump unlock type (1 or 2).</param>
/// <param name="cts">Cancellation token source to cancel the unlock.</param>
public UnlockProgressViewModel(IUnlockService unlockService, int unlockType, CancellationTokenSource cts, ILocalizationService loc)
{
_unlockService = unlockService;
_loc = loc;
_cts = cts;
_unlockTypeLabel = string.Format(_loc.GetString("Dialog.Unlock.TypeLabel"), unlockType);
_phaseText = _loc.GetString("Dialog.Unlock.Phase1");
_elapsedTime = "00:00";
_isCancellable = true;
_unlockService.StatusChanged += OnStatusChanged;
_unlockService.UnlockCompleted += OnUnlockCompleted;
_unlockService.PumpRelocked += OnPumpRelocked;
// PumpUnlocked fires as soon as the CAN TestUnlock parameter flips —
// regardless of which code path caused the unlock (fast unlock, Phase 1
// flood finishing, external manual unlock). This lets the dialog flip
// to its success state the instant the hardware confirms unlock, rather
// than waiting for UnlockService.UnlockAsync to reach its final
// verification step.
_unlockService.PumpUnlocked += OnPumpUnlocked;
}
// ── Observable properties ────────────────────────────────────────────────
/// <summary>Progress percentage (0100).</summary>
[ObservableProperty] private int _progress;
/// <summary>Elapsed time formatted as MM:SS.</summary>
[ObservableProperty] private string _elapsedTime;
/// <summary>Current phase description.</summary>
[ObservableProperty] private string _phaseText;
/// <summary>Result text shown after completion.</summary>
[ObservableProperty] private string _resultText = string.Empty;
/// <summary>Label for unlock type (e.g. "Type 1").</summary>
[ObservableProperty] private string _unlockTypeLabel;
/// <summary>True when the unlock sequence has finished (success, failure, or cancelled).</summary>
[NotifyCanExecuteChangedFor(nameof(CloseCommand))]
[ObservableProperty] private bool _isComplete;
/// <summary>True while cancellation is allowed (Phase 1 only).</summary>
[NotifyCanExecuteChangedFor(nameof(CancelCommand))]
[ObservableProperty] private bool _isCancellable;
/// <summary>Tri-state result: null = in progress, true = success, false = failure.</summary>
[ObservableProperty] private bool? _isSuccess;
/// <summary>True when the pump is currently LOCKED and the operator can retry the unlock.</summary>
[NotifyCanExecuteChangedFor(nameof(RetryCommand))]
[ObservableProperty] private bool _canRetry;
// ── Commands ─────────────────────────────────────────────────────────────
/// <summary>Cancels the unlock sequence (only available during Phase 1).</summary>
[RelayCommand(CanExecute = nameof(IsCancellable))]
private void Cancel()
{
_cts.Cancel();
IsCancellable = false;
IsComplete = true;
IsSuccess = false;
ResultText = _loc.GetString("Dialog.Unlock.Cancelled");
}
/// <summary>Closes the dialog (only available after completion).</summary>
[RelayCommand(CanExecute = nameof(IsComplete))]
private void Close()
{
RequestClose?.Invoke();
}
/// <summary>Requests a new unlock attempt (only available when complete and pump is LOCKED).</summary>
[RelayCommand(CanExecute = nameof(CanRetry))]
private void Retry()
{
CanRetry = false;
RequestRetry?.Invoke();
}
// ── Events ───────────────────────────────────────────────────────────────
/// <summary>Raised when the dialog should close itself.</summary>
public event Action? RequestClose;
/// <summary>Raised when the operator presses Retry — parent should restart the unlock sequence.</summary>
public event Action? RequestRetry;
// ── Service event handlers ───────────────────────────────────────────────
private void OnStatusChanged(string msg)
{
Application.Current?.Dispatcher?.Invoke(() =>
{
var match = ProgressRegex.Match(msg);
if (match.Success)
{
Progress = int.Parse(match.Groups[1].Value);
ElapsedTime = match.Groups[2].Value;
return;
}
if (msg == "Fast unlock attempt...")
{
PhaseText = _loc.GetString("Dialog.Unlock.FastAttempt");
}
else if (msg == "Unlocking...")
{
PhaseText = _loc.GetString("Dialog.Unlock.Phase1");
}
else if (msg == "Testing unlock...")
{
PhaseText = _loc.GetString("Dialog.Unlock.Phase2Testing");
IsCancellable = false;
Progress = 100;
}
else if (msg == "Sending...")
{
PhaseText = _loc.GetString("Dialog.Unlock.Phase2Sending");
}
});
}
private void OnUnlockCompleted(bool success)
{
Application.Current?.Dispatcher?.Invoke(() =>
{
IsComplete = true;
IsCancellable = false;
IsSuccess = success;
ResultText = success ? _loc.GetString("Dialog.Unlock.Unlocked") : _loc.GetString("Dialog.Unlock.Failed");
// Enable Retry when the unlock finished but the pump is still LOCKED.
CanRetry = !_unlockService.IsPumpUnlocked;
});
}
/// <summary>
/// Observer says the pump is now unlocked. Flip the dialog to the
/// success state immediately so the operator sees a responsive UI; the
/// later UnlockCompleted(true) event is idempotent and leaves this state
/// intact. If UnlockCompleted later arrives with failure=false, that
/// would overwrite — but that combination (observer unlocks then service
/// reports failure) is not a real scenario in the current state machine.
/// </summary>
private void OnPumpUnlocked()
{
Application.Current?.Dispatcher?.Invoke(() =>
{
if (IsComplete) return;
IsComplete = true;
IsCancellable = false;
IsSuccess = true;
CanRetry = false;
ResultText = _loc.GetString("Dialog.Unlock.Unlocked");
});
}
/// <summary>
/// Observer says the pump re-locked after a previously successful unlock.
/// If the snackbar is still visible (not dismissed), light up the Retry button
/// so the operator has a manual fallback without needing to reselect the pump.
/// </summary>
private void OnPumpRelocked()
{
Application.Current?.Dispatcher?.Invoke(() =>
{
if (IsComplete)
CanRetry = true;
});
}
// ── IDisposable ──────────────────────────────────────────────────────────
/// <summary>Unsubscribes from service events to prevent leaks.</summary>
public void Dispose()
{
_unlockService.StatusChanged -= OnStatusChanged;
_unlockService.UnlockCompleted -= OnUnlockCompleted;
_unlockService.PumpUnlocked -= OnPumpUnlocked;
_unlockService.PumpRelocked -= OnPumpRelocked;
}
}
}