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

160 lines
7.6 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.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels.Dialogs
{
/// <summary>
/// ViewModel for the Dashboard "Connect &amp; Auto Test" snackbar.
/// Receives <see cref="AutoTestState"/> transitions and failure reasons from the
/// orchestrator, exposes snackbar-friendly bindings (progress, phase text, success),
/// and forwards the Cancel click to the orchestrator's CTS.
/// Modelled on <see cref="UnlockProgressViewModel"/>.
/// </summary>
public sealed partial class AutoTestProgressViewModel : ObservableObject, IDisposable
{
private readonly IAutoTestOrchestrator _orchestrator;
private readonly ILocalizationService _loc;
/// <summary>Creates the ViewModel and subscribes to orchestrator events.</summary>
public AutoTestProgressViewModel(IAutoTestOrchestrator orchestrator, ILocalizationService loc)
{
_orchestrator = orchestrator;
_loc = loc;
_typeLabel = _loc.GetString("AutoTest.TypeLabel");
_phaseText = _loc.GetString("AutoTest.State.Preflight");
_isCancellable = true;
_orchestrator.StateChanged += OnStateChanged;
_orchestrator.Failed += OnFailed;
}
// ── Observable properties ────────────────────────────────────────────────
/// <summary>Progress percentage (0100). Meaningful during Unlocking and test Running phases.</summary>
[ObservableProperty] private int _progress;
/// <summary>Leading label (localised "Auto Test").</summary>
[ObservableProperty] private string _typeLabel;
/// <summary>Current phase description shown in the snackbar.</summary>
[ObservableProperty] private string _phaseText;
/// <summary>Terminal result text — populated on Completed/Aborted.</summary>
[ObservableProperty] private string _resultText = string.Empty;
/// <summary>True once the sequence reaches Completed or Aborted.</summary>
[NotifyCanExecuteChangedFor(nameof(CloseCommand))]
[ObservableProperty] private bool _isComplete;
/// <summary>True while the Cancel button should be enabled (all non-terminal states).</summary>
[NotifyCanExecuteChangedFor(nameof(CancelCommand))]
[ObservableProperty] private bool _isCancellable;
/// <summary>Tri-state: null while running, true = success, false = failure.</summary>
[ObservableProperty] private bool? _isSuccess;
// ── Commands ─────────────────────────────────────────────────────────────
/// <summary>Cancels the orchestrator's current sequence.</summary>
[RelayCommand(CanExecute = nameof(IsCancellable))]
private void Cancel() => _orchestrator.Cancel();
/// <summary>Closes the snackbar (emits <see cref="RequestClose"/>).</summary>
[RelayCommand(CanExecute = nameof(IsComplete))]
private void Close() => RequestClose?.Invoke();
// ── Events ───────────────────────────────────────────────────────────────
/// <summary>Raised when the snackbar should close itself (after success / user dismiss).</summary>
public event Action? RequestClose;
// ── Orchestrator event handlers ──────────────────────────────────────────
private void OnStateChanged(AutoTestState state, string? detail)
{
Application.Current?.Dispatcher?.Invoke(() =>
{
switch (state)
{
case AutoTestState.Preflight:
PhaseText = _loc.GetString("AutoTest.State.Preflight");
break;
case AutoTestState.ConnectingKLine:
PhaseText = _loc.GetString("AutoTest.State.Connecting");
break;
case AutoTestState.ReadingPump:
PhaseText = _loc.GetString("AutoTest.State.Reading");
if (!string.IsNullOrEmpty(detail) && int.TryParse(detail, out int pct))
Progress = pct;
break;
case AutoTestState.Unlocking:
PhaseText = string.IsNullOrEmpty(detail)
? _loc.GetString("AutoTest.State.Unlocking")
: string.Format(_loc.GetString("AutoTest.State.UnlockingWithDetail"), detail);
break;
case AutoTestState.TurningOnBench:
PhaseText = _loc.GetString("AutoTest.State.BenchOn");
break;
case AutoTestState.StartingOilPump:
PhaseText = _loc.GetString("AutoTest.State.OilPump");
break;
case AutoTestState.StartingTest:
PhaseText = _loc.GetString("AutoTest.State.TestStart");
break;
case AutoTestState.Running:
PhaseText = string.IsNullOrEmpty(detail)
? _loc.GetString("AutoTest.State.Running")
: string.Format(_loc.GetString("AutoTest.State.RunningWithPhase"), detail);
break;
case AutoTestState.Completed:
PhaseText = _loc.GetString("AutoTest.State.Completed");
ResultText = PhaseText;
Progress = 100;
IsComplete = true;
IsCancellable = false;
IsSuccess = true;
break;
case AutoTestState.Aborted:
// Detail populated by OnFailed; still terminal.
IsComplete = true;
IsCancellable = false;
IsSuccess = false;
break;
}
});
}
private void OnFailed(AutoTestFailureReason reason, string message)
{
Application.Current?.Dispatcher?.Invoke(() =>
{
string key = "AutoTest.Failure." + reason;
string localised = _loc.GetString(key);
if (string.IsNullOrEmpty(localised) || localised == key)
localised = reason.ToString();
ResultText = string.IsNullOrEmpty(message)
? localised
: $"{localised}: {message}";
PhaseText = string.Format(_loc.GetString("AutoTest.State.Aborted"), ResultText);
IsComplete = true;
IsCancellable = false;
IsSuccess = false;
});
}
// ── IDisposable ──────────────────────────────────────────────────────────
/// <summary>Unsubscribes from orchestrator events.</summary>
public void Dispose()
{
_orchestrator.StateChanged -= OnStateChanged;
_orchestrator.Failed -= OnFailed;
}
}
}