Files
HC_APTBS/ViewModels/Pages/PumpPageViewModel.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

145 lines
6.0 KiB
C#

using System;
using System.ComponentModel;
using System.Windows.Media;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
using HC_APTBS.ViewModels.Dialogs;
using SkiaSharp;
namespace HC_APTBS.ViewModels.Pages
{
/// <summary>
/// ViewModel for the Pump navigation page.
///
/// <para>Thin façade that groups the pump-related child ViewModels owned by
/// <see cref="MainViewModel"/> and adds banner flags. Holds a <see cref="Root"/>
/// reference so page XAML can bind to MainViewModel-owned properties (PumpRpm,
/// PumpTemp, KLineState, …) via <c>{Binding Root.X}</c>.</para>
/// </summary>
public sealed partial class PumpPageViewModel : ObservableObject
{
/// <summary>Root ViewModel — owns services, live readings, and global commands.</summary>
public MainViewModel Root { get; }
// ── Child VM façades ──────────────────────────────────────────────────────
/// <summary>Pump selector and K-Line read.</summary>
public PumpIdentificationViewModel Identification => Root.PumpIdentification;
/// <summary>Diagnostic Trouble Code list.</summary>
public DtcListViewModel DtcList { get; }
/// <summary>DFI management.</summary>
public DfiManageViewModel DfiViewModel => Root.DfiViewModel;
/// <summary>Manual pump control sliders.</summary>
public PumpControlViewModel PumpControl => Root.PumpControl;
/// <summary>First pump status display — Status word.</summary>
public StatusDisplayViewModel StatusDisplay1 => Root.StatusDisplay1;
/// <summary>Second pump status display — Empf3 word.</summary>
public StatusDisplayViewModel StatusDisplay2 => Root.StatusDisplay2;
/// <summary>BIP-STATUS display (PSG5-PI pumps only; hidden via HasDefinition for others).</summary>
public BipDisplayViewModel BipDisplay => Root.BipDisplay;
/// <summary>Current immobilizer unlock VM. Null when no unlock is in progress.</summary>
public UnlockProgressViewModel? UnlockVm => Root.CurrentUnlockVm;
/// <summary>Real-time RPM chart (120-sample rolling window).</summary>
public SingleFlowChartViewModel RpmChart { get; }
private readonly DispatcherTimer _rpmChartTimer;
// ── Banner flags (derived from Root state) ────────────────────────────────
/// <summary>True when a pump has been loaded from the database.</summary>
[ObservableProperty] private bool _isPumpSelected;
/// <summary>True when the K-Line session is currently open.</summary>
[ObservableProperty] private bool _isKLineSessionOpen;
/// <summary>True when the K-Line session is in the failed state.</summary>
[ObservableProperty] private bool _isKLineSessionFailed;
/// <summary>True for pumps that require a Ford immobilizer unlock (Type 1 or 2).</summary>
[ObservableProperty] private bool _isUnlockApplicable;
/// <summary>Constructs the page VM and subscribes to relevant Root state changes.</summary>
public PumpPageViewModel(
MainViewModel root,
DtcListViewModel dtcList)
{
Root = root;
DtcList = dtcList;
RpmChart = new SingleFlowChartViewModel(
"RPM",
new SKColor(0x21, 0x96, 0xF3),
maxSamples: 120,
smoothScroll: true);
_rpmChartTimer = new DispatcherTimer
{
Interval = HzToInterval(root.Config.Settings.RpmChartUpdateHz)
};
_rpmChartTimer.Tick += (_, _) => RpmChart.AddValue(Root.PumpRpm);
_rpmChartTimer.Start();
// Per-frame viewport slide — produces smooth continuous leftward motion independent
// of data cadence. Safe to leave subscribed for the VM's lifetime (one DateTime.UtcNow
// and two property sets per render frame).
CompositionTarget.Rendering += OnRenderFrame;
RefreshDerivedFlags();
Root.PropertyChanged += OnRootPropertyChanged;
Root.SettingsSaved += OnSettingsSaved;
Root.PumpIdentification.PumpChanged += _ => RefreshDerivedFlags();
}
private void OnRenderFrame(object? sender, EventArgs e)
{
int hz = Root.Config.Settings.RpmChartUpdateHz;
if (hz <= 0) hz = 15;
RpmChart.UpdateViewport(DateTime.UtcNow, 1000.0 / hz);
}
private void OnRootPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(MainViewModel.KLineState):
IsKLineSessionOpen = Root.KLineState == KLineConnectionState.Connected;
IsKLineSessionFailed = Root.KLineState == KLineConnectionState.Failed;
break;
case nameof(MainViewModel.CurrentUnlockVm):
OnPropertyChanged(nameof(UnlockVm));
break;
}
}
private void OnSettingsSaved()
{
_rpmChartTimer.Interval = HzToInterval(Root.Config.Settings.RpmChartUpdateHz);
}
private static System.TimeSpan HzToInterval(int hz) =>
System.TimeSpan.FromMilliseconds(hz > 0 ? 1000.0 / hz : 1000.0 / 15);
private void RefreshDerivedFlags()
{
IsPumpSelected = Root.CurrentPump != null;
IsKLineSessionOpen = Root.KLineState == KLineConnectionState.Connected;
IsKLineSessionFailed = Root.KLineState == KLineConnectionState.Failed;
IsUnlockApplicable = Root.CurrentPump != null && Root.CurrentPump.UnlockType != 0;
OnPropertyChanged(nameof(UnlockVm));
DtcList.Reset();
RpmChart.Clear();
}
}
}