fix: gate Ford VP44 unlock on CAN liveness to prevent false-unlocked reads
Before this fix, StartUnlockIfRequired was called immediately after registering the pump's CAN parameters, before any frames had been decoded. The TestUnlock parameter's zero-initialised Value was interpreted as "unlocked" for Type 1 pumps, causing Phase 1 to be skipped and UnlockCompleted(true) to fire falsely. Changes: - ICanService: add IsPumpAlive property (volatile-backed in PcanAdapter) - PcanAdapter: implement IsPumpAlive; mark _pumpAlive/_benchAlive volatile for safe cross-thread reads - MainViewModel: replace direct StartUnlockIfRequired call with a fire-and-forget WaitForPumpCanThenUnlockAsync that waits for PumpLivenessChanged(true) + 250 ms grace, then invokes unlock on the UI thread; cancellation on pump change or CAN disconnect via _pumpLivenessCts - UnlockService.UnlockAsync: skip Phase 2 state-machine when observer seed already reports unlocked (senders still run to prevent re-lock) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ namespace HC_APTBS.ViewModels
|
||||
/// <para>Pump selection and K-Line ECU identification are delegated to
|
||||
/// <see cref="PumpIdentification"/>.</para>
|
||||
/// </summary>
|
||||
public sealed partial class MainViewModel : ObservableObject
|
||||
public sealed partial class MainViewModel : ObservableObject, IAutoTestHost
|
||||
{
|
||||
// ── Services ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -60,12 +60,19 @@ namespace HC_APTBS.ViewModels
|
||||
private readonly IUnlockService _unlock;
|
||||
private readonly ILocalizationService _loc;
|
||||
private readonly IAppLogger _log;
|
||||
private readonly IAutoTestOrchestrator _auto;
|
||||
private const string LogId = "MainViewModel";
|
||||
|
||||
// ── CancellationToken for test runs ───────────────────────────────────────
|
||||
|
||||
private CancellationTokenSource? _testCts;
|
||||
|
||||
// ── BIP poll counter ──────────────────────────────────────────────────────
|
||||
|
||||
// Rate-limits KWP RAM reads to ~1 s intervals regardless of the refresh timer frequency.
|
||||
private int _bipPollCounter;
|
||||
private const int BipPollEveryNTicks = 10;
|
||||
|
||||
// ── Test elapsed timer ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Ticks every second while a test is running to update <see cref="TestElapsed"/>.</summary>
|
||||
@@ -79,9 +86,27 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>CTS for the currently running immobilizer unlock, if any.</summary>
|
||||
private CancellationTokenSource? _unlockCts;
|
||||
|
||||
/// <summary>The in-flight <see cref="IUnlockService.UnlockAsync"/> task, tracked so
|
||||
/// a rapid pump switch can await prior cancellation before starting a new unlock.</summary>
|
||||
private Task? _unlockTask;
|
||||
|
||||
/// <summary>ViewModel for the non-modal unlock progress window.</summary>
|
||||
private UnlockProgressViewModel? _unlockVm;
|
||||
|
||||
/// <summary>The pump active before the most recent <see cref="OnPumpChanged"/> call.
|
||||
/// Used to unregister the prior pump's CAN parameters from the bus adapter so stale
|
||||
/// parameter objects don't keep absorbing frames meant for the new pump.</summary>
|
||||
private PumpDefinition? _previousPump;
|
||||
|
||||
/// <summary>CTS for the "wait for pump CAN liveness, then start unlock" gate.
|
||||
/// Cancelled whenever a new pump is selected so a rapid pump switch doesn't
|
||||
/// leak a stale wait task that would race against the new pump's unlock flow.</summary>
|
||||
private CancellationTokenSource? _pumpLivenessCts;
|
||||
|
||||
/// <summary>True if the most recent unlock for the current pump succeeded.
|
||||
/// Reset on pump change so the test-start gate survives snackbar auto-dismiss.</summary>
|
||||
private bool _lastUnlockSucceeded;
|
||||
|
||||
/// <summary>
|
||||
/// Publicly observable accessor for the currently running (or last completed)
|
||||
/// immobilizer unlock VM. Used by the Pump page's inline unlock panel to
|
||||
@@ -101,12 +126,18 @@ namespace HC_APTBS.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Raised after operator saves settings — consumed by child VMs that have settings-dependent runtime state.</summary>
|
||||
public event Action? SettingsSaved;
|
||||
|
||||
/// <summary>Remembers the last authenticated username to pre-fill the next auth dialog.</summary>
|
||||
private string _lastAuthenticatedUser = string.Empty;
|
||||
|
||||
/// <summary>Tracks whether the last selected pump required 27 V, for transition-based voltage warnings.</summary>
|
||||
private bool _lastPumpWas27V;
|
||||
|
||||
/// <summary>Configuration service — exposed for child VMs that need settings at construction time.</summary>
|
||||
public IConfigurationService Config => _config;
|
||||
|
||||
// ── Child ViewModels ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>ViewModel for pump selection and K-Line ECU identification.</summary>
|
||||
@@ -139,6 +170,12 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>ViewModel for the second pump status display (Empf3 word).</summary>
|
||||
public StatusDisplayViewModel StatusDisplay2 { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
|
||||
/// <summary>ViewModel for the Dashboard's active-alarm list.</summary>
|
||||
public DashboardAlarmsViewModel DashboardAlarms { get; }
|
||||
|
||||
@@ -184,7 +221,8 @@ namespace HC_APTBS.ViewModels
|
||||
IPdfService pdfService,
|
||||
IUnlockService unlockService,
|
||||
ILocalizationService localizationService,
|
||||
IAppLogger logger)
|
||||
IAppLogger logger,
|
||||
IAutoTestOrchestrator autoTestOrchestrator)
|
||||
{
|
||||
_can = canService;
|
||||
_kwp = kwpService;
|
||||
@@ -194,6 +232,7 @@ namespace HC_APTBS.ViewModels
|
||||
_unlock = unlockService;
|
||||
_loc = localizationService;
|
||||
_log = logger;
|
||||
_auto = autoTestOrchestrator;
|
||||
|
||||
_loc.LanguageChanged += RefreshLocalisedStrings;
|
||||
|
||||
@@ -220,6 +259,21 @@ namespace HC_APTBS.ViewModels
|
||||
// React to pump changes from the identification child VM.
|
||||
PumpIdentification.PumpChanged += OnPumpChanged;
|
||||
|
||||
// Dashboard auto-test button is gated on DashboardAlarms.HasCritical.
|
||||
DashboardAlarms.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(DashboardAlarmsViewModel.HasCritical))
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
ConnectAndAutoTestCommand.NotifyCanExecuteChanged());
|
||||
};
|
||||
|
||||
// Orchestrator state transitions also gate the button.
|
||||
_auto.StateChanged += (_, _) => App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ConnectAndAutoTestCommand.NotifyCanExecuteChanged();
|
||||
CancelAutoTestCommand.NotifyCanExecuteChanged();
|
||||
});
|
||||
|
||||
// Sync sliders when test execution sets pump control values.
|
||||
_bench.PumpControlValueSet += (name, value) => App.Current.Dispatcher.Invoke(
|
||||
() => PumpControl.SetValueFromTest(name, value));
|
||||
@@ -242,6 +296,13 @@ namespace HC_APTBS.ViewModels
|
||||
_kwp.KLineStateChanged += state =>
|
||||
App.Current.Dispatcher.Invoke(() => KLineState = state);
|
||||
|
||||
// BIP status word → BipDisplay (PSG5-PI pumps only)
|
||||
_kwp.BipStatusRead += word =>
|
||||
{
|
||||
if (CurrentPump?.BipStatus is { } bipDef)
|
||||
App.Current.Dispatcher.Invoke(() => BipDisplay.UpdateBipWord(bipDef, word));
|
||||
};
|
||||
|
||||
// Bench service events
|
||||
_bench.TestStarted += OnTestStarted;
|
||||
_bench.TestFinished += OnTestFinished;
|
||||
@@ -269,7 +330,7 @@ namespace HC_APTBS.ViewModels
|
||||
_bench.ToleranceUpdated += (paramName, value, tolerance) => App.Current.Dispatcher.Invoke(
|
||||
() =>
|
||||
{
|
||||
TestPanel.UpdateLiveIndicator(paramName, value);
|
||||
TestPanel.ApplyToleranceUpdate(paramName, value, tolerance);
|
||||
FlowmeterChart.SetTolerance(paramName, value, tolerance);
|
||||
if (paramName == BenchParameterNames.Pressure)
|
||||
BenchPage.PressureTrace.P1.SetTolerance(value, tolerance);
|
||||
@@ -309,6 +370,8 @@ namespace HC_APTBS.ViewModels
|
||||
// Unlock service status → verbose display
|
||||
_unlock.StatusChanged += msg => App.Current.Dispatcher.Invoke(
|
||||
() => VerboseStatus = msg);
|
||||
_unlock.UnlockCompleted += success => App.Current.Dispatcher.Invoke(
|
||||
() => _lastUnlockSucceeded = success);
|
||||
|
||||
// KWP pump power-cycle callbacks
|
||||
kwpService.PumpDisconnectRequested += OnKwpDisconnectPump;
|
||||
@@ -320,14 +383,78 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>Convenience accessor for the currently loaded pump definition.</summary>
|
||||
public PumpDefinition? CurrentPump => PumpIdentification.CurrentPump;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> EnsureOilPumpOnAsync(bool skipConfirmation)
|
||||
{
|
||||
// Always marshal to the UI thread: ShowDialog and BenchControlViewModel
|
||||
// are UI-affine. Invoked from AutoTestOrchestrator which may run on a
|
||||
// background continuation.
|
||||
return Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// Already on — short-circuit. Covers the case where the operator
|
||||
// turned the pump on manually before pressing "Connect & Auto Test".
|
||||
if (BenchControl.IsOilPumpOn) return true;
|
||||
|
||||
if (skipConfirmation)
|
||||
{
|
||||
BenchControl.TurnOilPumpOnSilent();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Present the same leak-check dialog the manual Bench page uses.
|
||||
var vm = new OilPumpConfirmViewModel();
|
||||
var dlg = new OilPumpConfirmDialog(vm) { Owner = Application.Current.MainWindow };
|
||||
dlg.ShowDialog();
|
||||
if (!vm.Accepted) return false;
|
||||
|
||||
BenchControl.TurnOilPumpOnSilent();
|
||||
return true;
|
||||
}).Task;
|
||||
}
|
||||
|
||||
private void OnPumpChanged(PumpDefinition? pump)
|
||||
{
|
||||
if (pump == null) return;
|
||||
|
||||
_log.Info(LogId, $"OnPumpChanged: {pump.Id}");
|
||||
|
||||
// Open the slider gate FIRST so the operator can command the pump as soon
|
||||
// as the identifier is known — even if the K-Line read is still running
|
||||
// and the heavier work below (LoadAllTests, senders, unlock) hasn't finished.
|
||||
PumpControl.IsPreInAvailable = pump.HasPreInjection;
|
||||
PumpControl.IsEnabled = true;
|
||||
PumpControl.Reset();
|
||||
_log.Info(LogId, $"OnPumpChanged: slider gate opened for {pump.Id}");
|
||||
|
||||
// Cancel any in-flight "wait for CAN liveness then unlock" gate from
|
||||
// the previous pump selection. Must happen before StopSenders so a
|
||||
// pending wait can't race and start an unlock against the outgoing pump.
|
||||
_pumpLivenessCts?.Cancel();
|
||||
_pumpLivenessCts?.Dispose();
|
||||
_pumpLivenessCts = null;
|
||||
|
||||
// New pump lifecycle — stop any persistent unlock senders from the
|
||||
// previous pump. Must happen before CloseUnlockDialog tears down the
|
||||
// previous VM. The Ford ECU re-locks if 0x300/0x700 stops mid-session,
|
||||
// so senders are only stopped here (on pump change), not on dismiss.
|
||||
_unlock.StopSenders();
|
||||
// Tear down the previous pump's unlock-state observer; a new one is
|
||||
// started below in StartUnlockIfRequired once the new pump's CAN
|
||||
// parameters are registered.
|
||||
_unlock.StopObserver();
|
||||
_lastUnlockSucceeded = false;
|
||||
|
||||
// Stop any senders from the previous pump.
|
||||
_bench.StopMemoryRequestSender();
|
||||
_bench.StopPumpSender();
|
||||
|
||||
// Unregister the previous pump's CAN parameters BEFORE adding the new
|
||||
// pump's map. Most VP44 pumps share CAN IDs (Status, Empf3, etc.), so
|
||||
// without this step the new pump's parameter objects would be masked
|
||||
// by stale ones still decoding frames into the old pump's state.
|
||||
if (_previousPump != null && !ReferenceEquals(_previousPump, pump))
|
||||
_can.RemoveParameters(_previousPump.ParametersById);
|
||||
|
||||
// Register the pump with BenchService so ReadParameter/SetParameter resolve pump params.
|
||||
_bench.SetActivePump(pump);
|
||||
|
||||
@@ -338,11 +465,6 @@ namespace HC_APTBS.ViewModels
|
||||
_can.AddParameters(pump.ParametersById);
|
||||
_can.RegisterPumpMessageIds(GetReceiveMessageIds(pump.ParametersById));
|
||||
|
||||
// Configure pump control sliders.
|
||||
PumpControl.IsPreInAvailable = pump.HasPreInjection;
|
||||
PumpControl.IsEnabled = true;
|
||||
PumpControl.Reset();
|
||||
|
||||
// Initialise status displays with zero values.
|
||||
StatusDisplay1.Reset();
|
||||
StatusDisplay2.Reset();
|
||||
@@ -357,6 +479,9 @@ namespace HC_APTBS.ViewModels
|
||||
if (def != null) StatusDisplay2.UpdateStatusWord(def, 0);
|
||||
}
|
||||
|
||||
// Load BIP-STATUS definitions (PSG5-PI pumps only; null = hide the control).
|
||||
BipDisplay.LoadDefinition(pump.BipStatus);
|
||||
|
||||
// Start periodic senders for the new pump.
|
||||
_bench.StartMemoryRequestSender();
|
||||
_bench.StartPumpSender();
|
||||
@@ -366,10 +491,98 @@ namespace HC_APTBS.ViewModels
|
||||
GenerateReportCommand.NotifyCanExecuteChanged();
|
||||
|
||||
// Show voltage warning on 27V ↔ 13.5V transitions (WAlert27v equivalent).
|
||||
CheckVoltageWarning(pump);
|
||||
// Defer onto a later dispatcher frame so the modal ShowDialog() call cannot
|
||||
// block the current frame — the one that carries the slider-enable paint.
|
||||
App.Current.Dispatcher.BeginInvoke(
|
||||
new Action(() => CheckVoltageWarning(pump)),
|
||||
System.Windows.Threading.DispatcherPriority.Background);
|
||||
|
||||
// Start immobilizer unlock if this pump requires it (Ford VP44).
|
||||
StartUnlockIfRequired(pump);
|
||||
// Dismiss the previous pump's unlock snackbar immediately so the UI
|
||||
// doesn't linger showing stale progress while we wait for CAN liveness.
|
||||
CloseUnlockDialog();
|
||||
|
||||
_previousPump = pump;
|
||||
|
||||
// Start immobilizer unlock if this pump requires it (Ford VP44), but
|
||||
// first wait until the pump ECU is actually broadcasting on CAN so
|
||||
// VerifyUnlock reads a real decoded TestUnlock value rather than the
|
||||
// zero-initialised default (which Type 1 misinterprets as "unlocked").
|
||||
if (pump.UnlockType != 0)
|
||||
{
|
||||
_pumpLivenessCts = new CancellationTokenSource();
|
||||
_ = WaitForPumpCanThenUnlockAsync(pump, _pumpLivenessCts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the pump ECU to start broadcasting on CAN (or until a short
|
||||
/// timeout elapses), then invokes <see cref="StartUnlockIfRequired"/> on
|
||||
/// the UI thread. Exists to ensure the unlock observer seeds from a real
|
||||
/// decoded TestUnlock value, not the zero-initialised parameter default.
|
||||
/// </summary>
|
||||
private async Task WaitForPumpCanThenUnlockAsync(PumpDefinition pump, CancellationToken ct)
|
||||
{
|
||||
const int LivenessTimeoutMs = 10_000;
|
||||
const int PostAliveGraceMs = 250;
|
||||
|
||||
try
|
||||
{
|
||||
if (!_can.IsPumpAlive)
|
||||
{
|
||||
_log.Info(LogId, $"Waiting for CAN liveness for pump {pump.Id}...");
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
void OnLiveness(bool alive)
|
||||
{
|
||||
if (alive) tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
_can.PumpLivenessChanged += OnLiveness;
|
||||
try
|
||||
{
|
||||
// Race guard — pump may have become live between the
|
||||
// IsPumpAlive check above and this subscription.
|
||||
if (_can.IsPumpAlive) tcs.TrySetResult(true);
|
||||
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(LivenessTimeoutMs);
|
||||
using var reg = timeoutCts.Token.Register(() => tcs.TrySetCanceled());
|
||||
|
||||
try { await tcs.Task.ConfigureAwait(false); }
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (ct.IsCancellationRequested) return; // pump changed
|
||||
_log.Warning(LogId, $"CAN liveness timeout for pump {pump.Id} — starting unlock anyway");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_can.PumpLivenessChanged -= OnLiveness;
|
||||
}
|
||||
}
|
||||
|
||||
// Short grace delay so the full pump broadcast cycle has a chance
|
||||
// to deliver every frame — in particular the one carrying the
|
||||
// TestUnlock parameter — before the observer seeds from it.
|
||||
try { await Task.Delay(PostAliveGraceMs, ct).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) { return; }
|
||||
|
||||
// Verify we're still the current pump (a rapid switch may have
|
||||
// cancelled our token before we reached this point).
|
||||
if (!ReferenceEquals(_previousPump, pump)) return;
|
||||
|
||||
await App.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (!ReferenceEquals(_previousPump, pump)) return;
|
||||
StartUnlockIfRequired(pump);
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warning(LogId, $"WaitForPumpCanThenUnlockAsync failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Immobilizer unlock ────────────────────────────────────────────────────
|
||||
@@ -377,33 +590,43 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>
|
||||
/// Starts the immobilizer unlock sequence in a non-modal window if the pump
|
||||
/// requires it (UnlockType != 0). Cancels any previously running unlock first.
|
||||
/// The prior <see cref="IUnlockService.UnlockAsync"/> task is left to unwind
|
||||
/// on its own token — we don't await it because <see cref="IUnlockService.StopSenders"/>
|
||||
/// was already called synchronously in <see cref="OnPumpChanged"/>, and
|
||||
/// <see cref="UnlockService.StartSenders"/> re-stops the sender CTS idempotently
|
||||
/// before creating a fresh one, so there is no sender race to guard against.
|
||||
/// </summary>
|
||||
private void StartUnlockIfRequired(PumpDefinition pump)
|
||||
{
|
||||
// Cancel and close any previous unlock window.
|
||||
CloseUnlockDialog();
|
||||
|
||||
if (pump.UnlockType == 0) return;
|
||||
|
||||
// Start the 1 s background observer BEFORE kicking off UnlockAsync.
|
||||
// It watches the CAN TestUnlock parameter and raises PumpUnlocked
|
||||
// on every LOCKED → UNLOCKED transition, regardless of which code
|
||||
// path triggered the unlock. Subscribers (the auto-test orchestrator
|
||||
// and the unlock-progress dialog) latch onto this so they don't miss
|
||||
// a fast unlock or an external manual unlock while still setting up.
|
||||
_unlock.StartObserver(pump);
|
||||
|
||||
_unlockCts = new CancellationTokenSource();
|
||||
CurrentUnlockVm = new UnlockProgressViewModel(_unlock, pump.UnlockType, _unlockCts, _loc);
|
||||
CurrentUnlockVm.RequestClose += CloseUnlockDialog;
|
||||
|
||||
// Start unlock in background — ViewModel tracks via event subscriptions.
|
||||
var unlockTask = _unlock.UnlockAsync(pump, _unlockCts.Token);
|
||||
_ = unlockTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnFaulted);
|
||||
_unlockTask = _unlock.UnlockAsync(pump, _unlockCts.Token);
|
||||
_ = _unlockTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any running unlock, stops persistent CAN senders, closes the
|
||||
/// window, and disposes resources. Safe to call when no unlock is active.
|
||||
/// Dismisses the unlock snackbar and disposes its ViewModel. Does NOT stop
|
||||
/// the persistent CAN senders — those continue running until the next pump
|
||||
/// selection (see <see cref="OnPumpChanged"/>), because the Ford ECU re-locks
|
||||
/// if the 0x300/0x700 flood stops.
|
||||
/// </summary>
|
||||
private void CloseUnlockDialog()
|
||||
{
|
||||
// Stop the persistent CAN unlock senders (prevents re-lock until
|
||||
// this point — only called when the pump is deselected).
|
||||
_unlock.StopSenders();
|
||||
|
||||
if (_unlockCts != null)
|
||||
{
|
||||
_unlockCts.Cancel();
|
||||
@@ -411,6 +634,8 @@ namespace HC_APTBS.ViewModels
|
||||
_unlockCts = null;
|
||||
}
|
||||
|
||||
_unlockTask = null;
|
||||
|
||||
if (_unlockVm != null)
|
||||
{
|
||||
_unlockVm.RequestClose -= CloseUnlockDialog;
|
||||
@@ -425,7 +650,9 @@ namespace HC_APTBS.ViewModels
|
||||
[ObservableProperty] private string _canStatusText = string.Empty;
|
||||
|
||||
/// <summary>True when the CAN bus adapter is connected.</summary>
|
||||
[ObservableProperty] private bool _isCanConnected;
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(ConnectAndAutoTestCommand))]
|
||||
private bool _isCanConnected;
|
||||
|
||||
/// <summary>Connects to the CAN bus adapter.</summary>
|
||||
[RelayCommand]
|
||||
@@ -450,6 +677,13 @@ namespace HC_APTBS.ViewModels
|
||||
[RelayCommand]
|
||||
private void DisconnectCan()
|
||||
{
|
||||
// Abort any in-flight "wait for CAN liveness" gate — without the bus
|
||||
// up it will never complete, and we don't want it latching onto a
|
||||
// future reconnect and firing an unlock against stale state.
|
||||
_pumpLivenessCts?.Cancel();
|
||||
_pumpLivenessCts?.Dispose();
|
||||
_pumpLivenessCts = null;
|
||||
|
||||
_bench.StopElectronicMsgSender();
|
||||
_bench.StopRelaySender();
|
||||
_bench.StopMemoryRequestSender();
|
||||
@@ -457,6 +691,11 @@ namespace HC_APTBS.ViewModels
|
||||
_can.Disconnect();
|
||||
IsCanConnected = false;
|
||||
CanStatusText = _loc.GetString("Status.Disconnected");
|
||||
|
||||
// Clear the previous-pump handle so a fresh connect starts with an empty
|
||||
// CAN parameter map (the adapter also drops its map on Disconnect, but we
|
||||
// keep these two in sync explicitly).
|
||||
_previousPump = null;
|
||||
}
|
||||
|
||||
// ── Live bench readings ───────────────────────────────────────────────────
|
||||
@@ -518,7 +757,9 @@ namespace HC_APTBS.ViewModels
|
||||
// ── Bench/pump connection status ──────────────────────────────────────────
|
||||
|
||||
/// <summary>True when the bench controller is connected.</summary>
|
||||
[ObservableProperty] private bool _isBenchConnected;
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(ConnectAndAutoTestCommand))]
|
||||
private bool _isBenchConnected;
|
||||
|
||||
/// <summary>True when the pump ECU is responding on CAN.</summary>
|
||||
[ObservableProperty] private bool _isPumpConnected;
|
||||
@@ -535,8 +776,24 @@ namespace HC_APTBS.ViewModels
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(StartTestCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(StopTestCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ConnectAndAutoTestCommand))]
|
||||
private bool _isTestRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Snackbar ViewModel for the Dashboard "Connect & Auto Test" sequence.
|
||||
/// Non-null while a sequence is running or has just completed; the Dashboard
|
||||
/// button uses this to toggle between "Auto Test" and "Cancel" appearances.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsAutoTestActive))]
|
||||
private AutoTestProgressViewModel? _autoTestProgress;
|
||||
|
||||
/// <summary>
|
||||
/// True while the Dashboard auto-test snackbar is visible. Bound by the
|
||||
/// Dashboard button's style DataTrigger so it transforms into "Cancel".
|
||||
/// </summary>
|
||||
public bool IsAutoTestActive => AutoTestProgress != null;
|
||||
|
||||
/// <summary>True if the last test passed.</summary>
|
||||
[ObservableProperty] private bool _lastTestSuccess;
|
||||
|
||||
@@ -554,6 +811,49 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>Elapsed time since the current test started. Updated every second; retains last value when idle.</summary>
|
||||
[ObservableProperty] private TimeSpan _testElapsed;
|
||||
|
||||
// ── Commands: Dashboard auto-test ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Runs the Dashboard's single-click auto-test sequence: connect K-Line,
|
||||
/// read pump, unlock (if required), turn on bench, start oil pump, start test.
|
||||
/// </summary>
|
||||
[RelayCommand(CanExecute = nameof(CanAutoTest))]
|
||||
private async Task ConnectAndAutoTestAsync()
|
||||
{
|
||||
var vm = new AutoTestProgressViewModel(_auto, _loc);
|
||||
vm.RequestClose += () => App.Current.Dispatcher.Invoke(() => AutoTestProgress = null);
|
||||
AutoTestProgress = vm;
|
||||
ConnectAndAutoTestCommand.NotifyCanExecuteChanged();
|
||||
CancelAutoTestCommand.NotifyCanExecuteChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await _auto.RunAsync(CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"ConnectAndAutoTestAsync: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ConnectAndAutoTestCommand.NotifyCanExecuteChanged();
|
||||
CancelAutoTestCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanAutoTest()
|
||||
=> !IsTestRunning
|
||||
&& !_auto.State.IsRunning()
|
||||
&& IsCanConnected
|
||||
&& IsBenchConnected
|
||||
&& !DashboardAlarms.HasCritical;
|
||||
|
||||
/// <summary>Cancels the currently running auto-test sequence (if any).</summary>
|
||||
[RelayCommand(CanExecute = nameof(CanCancelAutoTest))]
|
||||
private void CancelAutoTest() => _auto.Cancel();
|
||||
|
||||
private bool CanCancelAutoTest() => _auto.State.IsRunning();
|
||||
|
||||
// ── Commands: test ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Starts the test sequence for the current pump.</summary>
|
||||
@@ -562,19 +862,11 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
if (CurrentPump == null) return;
|
||||
|
||||
// Block test start if an unlock is still in progress.
|
||||
// If a background unlock is still pending (e.g. K-Line is faulty and
|
||||
// the sequence will never complete), cancel it so the operator can
|
||||
// run tests anyway. Unlock is a best-effort pre-flight, not a gate.
|
||||
if (_unlockVm != null && !_unlockVm.IsComplete)
|
||||
{
|
||||
VerboseStatus = _loc.GetString("Status.UnlockInProgress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Block test start if the unlock failed or was cancelled.
|
||||
if (CurrentPump.UnlockType != 0 && _unlockVm?.IsSuccess != true)
|
||||
{
|
||||
VerboseStatus = _loc.GetString("Status.UnlockRequired");
|
||||
return;
|
||||
}
|
||||
CloseUnlockDialog();
|
||||
|
||||
_testCts = new CancellationTokenSource();
|
||||
IsTestRunning = true;
|
||||
@@ -691,6 +983,7 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
if (_refreshTimer != null)
|
||||
_refreshTimer.Interval = TimeSpan.FromMilliseconds(_config.Settings.RefreshBenchInterfaceMs);
|
||||
SettingsSaved?.Invoke();
|
||||
}
|
||||
|
||||
// ── Initialisation ────────────────────────────────────────────────────────
|
||||
@@ -802,6 +1095,29 @@ namespace HC_APTBS.ViewModels
|
||||
if (def != null) StatusDisplay2.UpdateStatusWord(def, (int)empf3Param.Value);
|
||||
empf3Param.NeedsUpdate = false;
|
||||
}
|
||||
|
||||
// Poll BIP status word ~once per second for PSG5-PI pumps with an active K-Line session.
|
||||
if (CurrentPump.BipStatus != null
|
||||
&& _kwp.KLineState == KLineConnectionState.Connected
|
||||
&& ++_bipPollCounter >= BipPollEveryNTicks)
|
||||
{
|
||||
_bipPollCounter = 0;
|
||||
_ = _kwp.ReadBipStatusAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Push live readings into every indicator on the active phase card so
|
||||
// each gauge animates continuously through conditioning and measurement.
|
||||
// TestPanel.ActivePhaseIndicators returns empty when no phase is active.
|
||||
var liveIndicators = TestPanel.ActivePhaseIndicators;
|
||||
if (liveIndicators.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < liveIndicators.Count; i++)
|
||||
{
|
||||
var ind = liveIndicators[i];
|
||||
TestPanel.UpdateLiveIndicator(ind.ParameterName,
|
||||
_bench.ReadParameter(ind.ParameterName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user