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>
This commit is contained in:
@@ -30,7 +30,11 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>Application configuration: safety limits, PID, motor, report, K-Line, language.</summary>
|
||||
Settings = 4,
|
||||
/// <summary>Session-only history of completed test runs with detail view and PDF export.</summary>
|
||||
Results = 5
|
||||
Results = 5,
|
||||
#if DEVELOPER_TOOLS
|
||||
/// <summary>Developer Tools page: raw K-Line / KWP custom command console. Debug builds only.</summary>
|
||||
Developer = 6
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -174,7 +178,7 @@ namespace HC_APTBS.ViewModels
|
||||
/// ViewModel for the BIP-STATUS display (PSG5-PI pumps only).
|
||||
/// <see cref="BipDisplayViewModel.HasDefinition"/> is false for non-PSG5-PI pumps.
|
||||
/// </summary>
|
||||
public BipDisplayViewModel BipDisplay { get; } = new();
|
||||
public BipDisplayViewModel BipDisplay { get; }
|
||||
|
||||
/// <summary>ViewModel for the Dashboard's active-alarm list.</summary>
|
||||
public DashboardAlarmsViewModel DashboardAlarms { get; }
|
||||
@@ -202,6 +206,11 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>Results navigation page VM (session-only test-run history).</summary>
|
||||
public ResultsPageViewModel ResultsPage { get; private set; } = null!;
|
||||
|
||||
#if DEVELOPER_TOOLS
|
||||
/// <summary>Developer Tools page VM. Debug builds only — excluded from consumer Release builds.</summary>
|
||||
public Pages.DeveloperPageViewModel DeveloperPage { get; private set; } = null!;
|
||||
#endif
|
||||
|
||||
// ── Navigation state ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Currently selected top-level navigation page.</summary>
|
||||
@@ -245,6 +254,7 @@ namespace HC_APTBS.ViewModels
|
||||
AngleDisplay = new AngleDisplayViewModel(configService);
|
||||
DashboardAlarms = new DashboardAlarmsViewModel(configService.Settings.Alarms);
|
||||
DtcList = new DtcListViewModel(kwpService, localizationService, logger);
|
||||
BipDisplay = new BipDisplayViewModel(localizationService);
|
||||
// Page ViewModels are thin façades over the child VMs above; they hold a
|
||||
// reference back to this coordinator so page XAML can bind MainViewModel-owned
|
||||
// values via {Binding Root.X}.
|
||||
@@ -252,9 +262,12 @@ namespace HC_APTBS.ViewModels
|
||||
BenchPage = new BenchPageViewModel(this, benchService, configService);
|
||||
PumpPage = new PumpPageViewModel(this, DtcList);
|
||||
TestsPage = new TestsPageViewModel(this, configService, localizationService);
|
||||
SettingsPage = new SettingsPageViewModel(configService, localizationService);
|
||||
SettingsPage = new SettingsPageViewModel(configService, localizationService, logger);
|
||||
SettingsPage.SettingsSaved += OnSettingsSaved;
|
||||
ResultsPage = new ResultsPageViewModel(this, pdfService, configService, localizationService, logger);
|
||||
#if DEVELOPER_TOOLS
|
||||
DeveloperPage = new Pages.DeveloperPageViewModel(this, kwpService, configService, logger);
|
||||
#endif
|
||||
|
||||
// React to pump changes from the identification child VM.
|
||||
PumpIdentification.PumpChanged += OnPumpChanged;
|
||||
@@ -373,6 +386,12 @@ namespace HC_APTBS.ViewModels
|
||||
_unlock.UnlockCompleted += success => App.Current.Dispatcher.Invoke(
|
||||
() => _lastUnlockSucceeded = success);
|
||||
|
||||
// Re-trigger unlock on any UNLOCKED → LOCKED transition (pump swap, power glitch, etc.)
|
||||
_unlock.PumpRelocked += OnPumpRelocked;
|
||||
|
||||
// Safety-net: if a K-Line read completes and the pump is still LOCKED, re-run unlock.
|
||||
PumpIdentification.KlineReadCompleted += OnKlineReadCompleted;
|
||||
|
||||
// KWP pump power-cycle callbacks
|
||||
kwpService.PumpDisconnectRequested += OnKwpDisconnectPump;
|
||||
kwpService.PumpReconnectRequested += OnKwpReconnectPump;
|
||||
@@ -424,6 +443,7 @@ namespace HC_APTBS.ViewModels
|
||||
PumpControl.IsPreInAvailable = pump.HasPreInjection;
|
||||
PumpControl.IsEnabled = true;
|
||||
PumpControl.Reset();
|
||||
DfiViewModel.Reset();
|
||||
_log.Info(LogId, $"OnPumpChanged: slider gate opened for {pump.Id}");
|
||||
|
||||
// Cancel any in-flight "wait for CAN liveness then unlock" gate from
|
||||
@@ -613,12 +633,74 @@ namespace HC_APTBS.ViewModels
|
||||
_unlockCts = new CancellationTokenSource();
|
||||
CurrentUnlockVm = new UnlockProgressViewModel(_unlock, pump.UnlockType, _unlockCts, _loc);
|
||||
CurrentUnlockVm.RequestClose += CloseUnlockDialog;
|
||||
CurrentUnlockVm.RequestRetry += () => RestartUnlockForSameSelection(pump);
|
||||
|
||||
// Start unlock in background — ViewModel tracks via event subscriptions.
|
||||
_unlockTask = _unlock.UnlockAsync(pump, _unlockCts.Token);
|
||||
_ = _unlockTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the UNLOCKED → LOCKED transition raised by the unlock observer on the CAN
|
||||
/// read thread. Re-runs the unlock flow against the current pump without touching CAN
|
||||
/// parameter registrations, the test panel, or bench senders (the pump model is unchanged).
|
||||
/// </summary>
|
||||
private void OnPumpRelocked()
|
||||
{
|
||||
App.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var pump = _previousPump;
|
||||
if (pump == null || pump.UnlockType == 0) return;
|
||||
|
||||
// Skip if an unlock is already in-flight — the LOCKED frames that arrive
|
||||
// during Phase 1 of an ongoing unlock would otherwise cause infinite restarts.
|
||||
if (_unlockTask != null && !_unlockTask.IsCompleted) return;
|
||||
|
||||
_log.Warning(LogId, $"Pump {pump.Id} transitioned UNLOCKED → LOCKED — re-triggering unlock");
|
||||
RestartUnlockForSameSelection(pump);
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles K-Line read completion. If the pump requires unlock and the observer reports
|
||||
/// LOCKED, re-runs the unlock flow. This is a safety net for the first-contact window
|
||||
/// where the CAN observer may not yet have received a frame from the new pump.
|
||||
/// </summary>
|
||||
private void OnKlineReadCompleted(string pumpId, string serial)
|
||||
{
|
||||
var pump = _previousPump;
|
||||
if (pump == null || !string.Equals(pump.Id, pumpId, StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (pump.UnlockType == 0) return;
|
||||
if (_unlock.IsPumpUnlocked) return;
|
||||
|
||||
// Skip if an unlock is already running.
|
||||
if (_unlockTask != null && !_unlockTask.IsCompleted) return;
|
||||
|
||||
_log.Info(LogId, $"K-Line read completed on {pumpId}; observer reports LOCKED — re-triggering unlock");
|
||||
RestartUnlockForSameSelection(pump);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tears down the active unlock state and re-runs the liveness-wait → unlock pipeline
|
||||
/// against the already-selected pump. Used when the pump re-locks without a model change
|
||||
/// (physical swap of a same-ID unit, power instability, etc.).
|
||||
/// </summary>
|
||||
private void RestartUnlockForSameSelection(PumpDefinition pump)
|
||||
{
|
||||
_pumpLivenessCts?.Cancel();
|
||||
_pumpLivenessCts?.Dispose();
|
||||
_pumpLivenessCts = null;
|
||||
|
||||
_unlockCts?.Cancel();
|
||||
|
||||
_unlock.StopSenders();
|
||||
_unlock.StopObserver();
|
||||
_lastUnlockSucceeded = false;
|
||||
|
||||
_pumpLivenessCts = new CancellationTokenSource();
|
||||
_ = WaitForPumpCanThenUnlockAsync(pump, _pumpLivenessCts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dismisses the unlock snackbar and disposes its ViewModel. Does NOT stop
|
||||
/// the persistent CAN senders — those continue running until the next pump
|
||||
|
||||
Reference in New Issue
Block a user