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>
1244 lines
59 KiB
C#
1244 lines
59 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Threading;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using HC_APTBS.Models;
|
|
using HC_APTBS.Services;
|
|
using HC_APTBS.ViewModels.Dialogs;
|
|
using HC_APTBS.ViewModels.Pages;
|
|
using HC_APTBS.Views.Dialogs;
|
|
|
|
namespace HC_APTBS.ViewModels
|
|
{
|
|
/// <summary>Identifies the top-level navigation page shown in the shell.</summary>
|
|
public enum AppPage
|
|
{
|
|
/// <summary>Bench controls, flowmeter charts, encoder angles.</summary>
|
|
Bench = 0,
|
|
/// <summary>Pump manual control, DFI, status displays.</summary>
|
|
Pump = 1,
|
|
/// <summary>Test suite, live progress, results.</summary>
|
|
Tests = 2,
|
|
/// <summary>At-a-glance operator landing page: readings, connections, alarms, quick actions.</summary>
|
|
Dashboard = 3,
|
|
/// <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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Root ViewModel for the application's main window.
|
|
///
|
|
/// <para>Responsibilities:</para>
|
|
/// <list type="bullet">
|
|
/// <item>CAN connection lifecycle.</item>
|
|
/// <item>Bench status display (RPM, temperatures, flow measurements).</item>
|
|
/// <item>Test start/stop and progress reporting.</item>
|
|
/// <item>Relay toggle commands.</item>
|
|
/// <item>Report generation trigger.</item>
|
|
/// </list>
|
|
///
|
|
/// <para>Pump selection and K-Line ECU identification are delegated to
|
|
/// <see cref="PumpIdentification"/>.</para>
|
|
/// </summary>
|
|
public sealed partial class MainViewModel : ObservableObject, IAutoTestHost
|
|
{
|
|
// ── Services ──────────────────────────────────────────────────────────────
|
|
|
|
private readonly ICanService _can;
|
|
private readonly IKwpService _kwp;
|
|
private readonly IBenchService _bench;
|
|
private readonly IConfigurationService _config;
|
|
private readonly IPdfService _pdf;
|
|
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>
|
|
private DispatcherTimer? _testTimer;
|
|
|
|
/// <summary>UTC start time of the current test; used by the timer to compute elapsed duration.</summary>
|
|
private DateTime _testStartedUtc;
|
|
|
|
// ── Unlock tracking ──────────────────────────────────────────────────────
|
|
|
|
/// <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
|
|
/// display the same state that the floating dialog shows. Null while no
|
|
/// unlock has been started for the current pump.
|
|
/// </summary>
|
|
public UnlockProgressViewModel? CurrentUnlockVm
|
|
{
|
|
get => _unlockVm;
|
|
private set
|
|
{
|
|
if (!ReferenceEquals(_unlockVm, value))
|
|
{
|
|
_unlockVm = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
public PumpIdentificationViewModel PumpIdentification { get; }
|
|
|
|
/// <summary>ViewModel for the DFI manage user control.</summary>
|
|
public DfiManageViewModel DfiViewModel { get; }
|
|
|
|
/// <summary>ViewModel for the test panel showing all test sections and phase cards.</summary>
|
|
public TestPanelViewModel TestPanel { get; }
|
|
|
|
/// <summary>ViewModel for the measurement results table.</summary>
|
|
public ResultDisplayViewModel ResultDisplay { get; }
|
|
|
|
/// <summary>ViewModel for the manual pump control sliders (FBKW, ME, PreIn).</summary>
|
|
public PumpControlViewModel PumpControl { get; private set; } = null!;
|
|
|
|
/// <summary>ViewModel for manual bench controls (direction, RPM, oil pump, counter).</summary>
|
|
public BenchControlViewModel BenchControl { get; }
|
|
|
|
/// <summary>ViewModel for the two flowmeter real-time charts (Q-Delivery, Q-Over).</summary>
|
|
public FlowmeterChartViewModel FlowmeterChart { get; } = new();
|
|
|
|
/// <summary>ViewModel for the encoder angle monitoring display (PSG, INJ, Manual, Lock Angle).</summary>
|
|
public AngleDisplayViewModel AngleDisplay { get; }
|
|
|
|
/// <summary>ViewModel for the first pump status display (Status word).</summary>
|
|
public StatusDisplayViewModel StatusDisplay1 { get; } = new();
|
|
|
|
/// <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; }
|
|
|
|
/// <summary>Diagnostic Trouble Code list for the Pump page §3.b sub-section.</summary>
|
|
public DtcListViewModel DtcList { get; }
|
|
|
|
// ── Page ViewModels (thin façades over the child VMs above) ───────────────
|
|
|
|
/// <summary>Dashboard navigation page VM.</summary>
|
|
public DashboardPageViewModel DashboardPage { get; private set; } = null!;
|
|
|
|
/// <summary>Bench navigation page VM.</summary>
|
|
public BenchPageViewModel BenchPage { get; private set; } = null!;
|
|
|
|
/// <summary>Pump navigation page VM.</summary>
|
|
public PumpPageViewModel PumpPage { get; private set; } = null!;
|
|
|
|
/// <summary>Tests navigation page VM.</summary>
|
|
public TestsPageViewModel TestsPage { get; private set; } = null!;
|
|
|
|
/// <summary>Settings navigation page VM.</summary>
|
|
public SettingsPageViewModel SettingsPage { get; private set; } = null!;
|
|
|
|
/// <summary>Results navigation page VM (session-only test-run history).</summary>
|
|
public ResultsPageViewModel ResultsPage { get; private set; } = null!;
|
|
|
|
// ── Navigation state ──────────────────────────────────────────────────────
|
|
|
|
/// <summary>Currently selected top-level navigation page.</summary>
|
|
[ObservableProperty] private AppPage _selectedPage = AppPage.Dashboard;
|
|
|
|
// ── Constructor ───────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Constructs the MainViewModel and wires all service events to UI-bound properties.
|
|
/// Call <see cref="InitialiseAsync"/> after construction.
|
|
/// </summary>
|
|
public MainViewModel(
|
|
ICanService canService,
|
|
IKwpService kwpService,
|
|
IBenchService benchService,
|
|
IConfigurationService configService,
|
|
IPdfService pdfService,
|
|
IUnlockService unlockService,
|
|
ILocalizationService localizationService,
|
|
IAppLogger logger,
|
|
IAutoTestOrchestrator autoTestOrchestrator)
|
|
{
|
|
_can = canService;
|
|
_kwp = kwpService;
|
|
_bench = benchService;
|
|
_config = configService;
|
|
_pdf = pdfService;
|
|
_unlock = unlockService;
|
|
_loc = localizationService;
|
|
_log = logger;
|
|
_auto = autoTestOrchestrator;
|
|
|
|
_loc.LanguageChanged += RefreshLocalisedStrings;
|
|
|
|
TestPanel = new TestPanelViewModel(localizationService);
|
|
ResultDisplay = new ResultDisplayViewModel(localizationService);
|
|
PumpIdentification = new PumpIdentificationViewModel(kwpService, configService, localizationService, logger);
|
|
DfiViewModel = new DfiManageViewModel(kwpService, configService, localizationService);
|
|
PumpControl = new PumpControlViewModel(benchService);
|
|
BenchControl = new BenchControlViewModel(benchService, configService);
|
|
AngleDisplay = new AngleDisplayViewModel(configService);
|
|
DashboardAlarms = new DashboardAlarmsViewModel(configService.Settings.Alarms);
|
|
DtcList = new DtcListViewModel(kwpService, localizationService, logger);
|
|
// 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}.
|
|
DashboardPage = new DashboardPageViewModel(this, canService, kwpService);
|
|
BenchPage = new BenchPageViewModel(this, benchService, configService);
|
|
PumpPage = new PumpPageViewModel(this, DtcList);
|
|
TestsPage = new TestsPageViewModel(this, configService, localizationService);
|
|
SettingsPage = new SettingsPageViewModel(configService, localizationService);
|
|
SettingsPage.SettingsSaved += OnSettingsSaved;
|
|
ResultsPage = new ResultsPageViewModel(this, pdfService, configService, localizationService, logger);
|
|
|
|
// 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));
|
|
|
|
// CAN status → status bar
|
|
_can.StatusChanged += (msg, ok) =>
|
|
App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
CanStatusText = msg;
|
|
IsCanConnected = ok;
|
|
});
|
|
|
|
// Bench/pump liveness → connection indicators
|
|
_can.BenchLivenessChanged += alive =>
|
|
App.Current.Dispatcher.Invoke(() => IsBenchConnected = alive);
|
|
_can.PumpLivenessChanged += alive =>
|
|
App.Current.Dispatcher.Invoke(() => IsPumpConnected = alive);
|
|
|
|
// K-Line session state → indicator
|
|
_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;
|
|
_bench.PhaseChanged += phase => App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
CurrentPhaseName = phase;
|
|
TestPanel.SetActivePhase(phase);
|
|
// Clear real-time plot traces at each new phase boundary.
|
|
FlowmeterChart.Delivery.Clear();
|
|
FlowmeterChart.Over.Clear();
|
|
BenchPage.PressureTrace.P1.Clear();
|
|
BenchPage.PressureTrace.P2.Clear();
|
|
});
|
|
_bench.PhaseTimerTick += (section, remaining, total) => App.Current.Dispatcher.Invoke(
|
|
() => TestPanel.ApplyPhaseTimerTick(section, remaining, total));
|
|
_bench.VerboseMessage += msg => App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
VerboseStatus = msg;
|
|
TestPanel.StatusText = msg;
|
|
});
|
|
_bench.PsgSyncError += () => App.Current.Dispatcher.Invoke(
|
|
() => ShowPsgSyncError());
|
|
_bench.PhaseCompleted += (phase, passed) => App.Current.Dispatcher.Invoke(
|
|
() => TestPanel.SetPhaseResult(phase, passed));
|
|
_bench.ToleranceUpdated += (paramName, value, tolerance) => App.Current.Dispatcher.Invoke(
|
|
() =>
|
|
{
|
|
TestPanel.ApplyToleranceUpdate(paramName, value, tolerance);
|
|
FlowmeterChart.SetTolerance(paramName, value, tolerance);
|
|
if (paramName == BenchParameterNames.Pressure)
|
|
BenchPage.PressureTrace.P1.SetTolerance(value, tolerance);
|
|
else if (paramName == BenchParameterNames.AnalogSensor2)
|
|
BenchPage.PressureTrace.P2.SetTolerance(value, tolerance);
|
|
});
|
|
|
|
_bench.MeasurementSampled += (name, value) => App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
if (name == BenchParameterNames.QDelivery)
|
|
FlowmeterChart.Delivery.AddValue(value);
|
|
else if (name == BenchParameterNames.QOver)
|
|
FlowmeterChart.Over.AddValue(value);
|
|
else if (name == BenchParameterNames.Pressure)
|
|
BenchPage.PressureTrace.P1.AddValue(value);
|
|
else if (name == BenchParameterNames.AnalogSensor2)
|
|
BenchPage.PressureTrace.P2.AddValue(value);
|
|
});
|
|
_bench.EmergencyStopTriggered += reason => App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
VerboseStatus = string.Format(_loc.GetString("Error.EmergencyStop"), reason);
|
|
});
|
|
_bench.StatusReactionTriggered += (bit, reaction, desc) => App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
VerboseStatus = $"[STATUS] bit {bit} reaction={reaction}: {desc}";
|
|
});
|
|
|
|
// Angle display: lock angle and PSG zero from test phases
|
|
_bench.LockAngleFaseReady += () => App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
if (CurrentPump != null)
|
|
CurrentPump.LockAngleResult = AngleDisplay.SetLockAngle(CurrentPump.LockAngle);
|
|
});
|
|
_bench.PsgModeFaseReady += () => App.Current.Dispatcher.Invoke(
|
|
() => AngleDisplay.SetPsgZeroFromTest());
|
|
|
|
// 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;
|
|
kwpService.PumpReconnectRequested += OnKwpReconnectPump;
|
|
}
|
|
|
|
// ── Pump change handling ──────────────────────────────────────────────────
|
|
|
|
/// <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);
|
|
|
|
// Load all test sections into the test panel.
|
|
TestPanel.LoadAllTests(pump);
|
|
|
|
// Register the pump's CAN parameters with the bus adapter.
|
|
_can.AddParameters(pump.ParametersById);
|
|
_can.RegisterPumpMessageIds(GetReceiveMessageIds(pump.ParametersById));
|
|
|
|
// Initialise status displays with zero values.
|
|
StatusDisplay1.Reset();
|
|
StatusDisplay2.Reset();
|
|
if (pump.ParametersByName.TryGetValue(PumpParameterNames.Status, out var statusParam))
|
|
{
|
|
var def = _config.LoadPumpStatus(statusParam.Type);
|
|
if (def != null) StatusDisplay1.UpdateStatusWord(def, 0);
|
|
}
|
|
if (pump.ParametersByName.TryGetValue(PumpParameterNames.Empf3, out var empf3Param))
|
|
{
|
|
var def = _config.LoadPumpStatus(empf3Param.Type);
|
|
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();
|
|
|
|
// Notify commands that depend on pump availability.
|
|
StartTestCommand.NotifyCanExecuteChanged();
|
|
GenerateReportCommand.NotifyCanExecuteChanged();
|
|
|
|
// Show voltage warning on 27V ↔ 13.5V transitions (WAlert27v equivalent).
|
|
// 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);
|
|
|
|
// 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 ────────────────────────────────────────────────────
|
|
|
|
/// <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)
|
|
{
|
|
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.
|
|
_unlockTask = _unlock.UnlockAsync(pump, _unlockCts.Token);
|
|
_ = _unlockTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnFaulted);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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()
|
|
{
|
|
if (_unlockCts != null)
|
|
{
|
|
_unlockCts.Cancel();
|
|
_unlockCts.Dispose();
|
|
_unlockCts = null;
|
|
}
|
|
|
|
_unlockTask = null;
|
|
|
|
if (_unlockVm != null)
|
|
{
|
|
_unlockVm.RequestClose -= CloseUnlockDialog;
|
|
_unlockVm.Dispose();
|
|
CurrentUnlockVm = null;
|
|
}
|
|
}
|
|
|
|
// ── CAN connection ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>CAN bus status display text.</summary>
|
|
[ObservableProperty] private string _canStatusText = string.Empty;
|
|
|
|
/// <summary>True when the CAN bus adapter is connected.</summary>
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(ConnectAndAutoTestCommand))]
|
|
private bool _isCanConnected;
|
|
|
|
/// <summary>Connects to the CAN bus adapter.</summary>
|
|
[RelayCommand]
|
|
private void ConnectCan()
|
|
{
|
|
_can.SetParameters(_config.Bench.ParametersById);
|
|
_can.RegisterBenchMessageIds(GetReceiveMessageIds(_config.Bench.ParametersById));
|
|
bool ok = _can.Connect();
|
|
CanStatusText = ok ? _loc.GetString("Status.Connected") : _loc.GetString("Status.ConnectionFailed");
|
|
IsCanConnected = ok;
|
|
|
|
if (ok)
|
|
{
|
|
// ElectronicMsg keepalive (0x51) and relay bitmask (0x15) must
|
|
// begin transmitting as soon as the CAN bus is up.
|
|
_bench.StartElectronicMsgSender();
|
|
_bench.StartRelaySender();
|
|
}
|
|
}
|
|
|
|
/// <summary>Disconnects from the CAN bus adapter.</summary>
|
|
[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();
|
|
_bench.StopPumpSender();
|
|
_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 ───────────────────────────────────────────────────
|
|
|
|
/// <summary>Bench motor speed (RPM), updated by the refresh timer.</summary>
|
|
[ObservableProperty] private double _benchRpm;
|
|
|
|
/// <summary>Oil inlet temperature T-in (°C).</summary>
|
|
[ObservableProperty] private double _tempIn;
|
|
|
|
/// <summary>Oil outlet temperature T-out (°C).</summary>
|
|
[ObservableProperty] private double _tempOut;
|
|
|
|
/// <summary>Auxiliary temperature T4 (°C).</summary>
|
|
[ObservableProperty] private double _temp4;
|
|
|
|
/// <summary>Oil tank temperature (°C).</summary>
|
|
[ObservableProperty] private double _benchTemp;
|
|
|
|
/// <summary>Fuel delivery measurement Q-delivery (cc/stroke).</summary>
|
|
[ObservableProperty] private double _qDelivery;
|
|
|
|
/// <summary>Fuel overflow/pilot measurement Q-over (cc/stroke).</summary>
|
|
[ObservableProperty] private double _qOver;
|
|
|
|
/// <summary>Bench oil pressure P1 (bar), sensor-calibrated.</summary>
|
|
[ObservableProperty] private double _pressure;
|
|
|
|
/// <summary>Analogue sensor 2 pressure P2 (bar), sensor-calibrated.</summary>
|
|
[ObservableProperty] private double _pressure2;
|
|
|
|
/// <summary>PSG encoder position value.</summary>
|
|
[ObservableProperty] private double _psgEncoderValue;
|
|
|
|
/// <summary>
|
|
/// True when the Oil Pump relay is currently energised. Mirrored on each refresh
|
|
/// tick from <c>_config.Bench.Relays[RelayNames.OilPump]</c> so the Tests page
|
|
/// preconditions checklist can bind to it without walking the relay dictionary.
|
|
/// </summary>
|
|
[ObservableProperty] private bool _isOilPumpOn;
|
|
|
|
// ── Pump live readings (from pump CAN parameters) ──────────────────────────
|
|
|
|
/// <summary>Pump RPM reported by the ECU over CAN.</summary>
|
|
[ObservableProperty] private double _pumpRpm;
|
|
|
|
/// <summary>Pump internal temperature reported by the ECU over CAN.</summary>
|
|
[ObservableProperty] private double _pumpTemp;
|
|
|
|
/// <summary>Pump ME (metering) value from CAN.</summary>
|
|
[ObservableProperty] private double _pumpMe;
|
|
|
|
/// <summary>Pump FBkW (feedback) value from CAN.</summary>
|
|
[ObservableProperty] private double _pumpFbkw;
|
|
|
|
/// <summary>Pump T-ein (inlet timing) value from CAN, in microseconds.</summary>
|
|
[ObservableProperty] private double _pumpTein;
|
|
|
|
// ── Bench/pump connection status ──────────────────────────────────────────
|
|
|
|
/// <summary>True when the bench controller is connected.</summary>
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(ConnectAndAutoTestCommand))]
|
|
private bool _isBenchConnected;
|
|
|
|
/// <summary>True when the pump ECU is responding on CAN.</summary>
|
|
[ObservableProperty] private bool _isPumpConnected;
|
|
|
|
/// <summary>True when oil circulation has been detected.</summary>
|
|
[ObservableProperty] private bool _isOilCirculating;
|
|
|
|
/// <summary>Current K-Line session state (Disconnected / Connected / Failed).</summary>
|
|
[ObservableProperty] private KLineConnectionState _kLineState = KLineConnectionState.Disconnected;
|
|
|
|
// ── Test status ───────────────────────────────────────────────────────────
|
|
|
|
/// <summary>True while a test sequence is running.</summary>
|
|
[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;
|
|
|
|
/// <summary>Name of the currently executing test phase.</summary>
|
|
[ObservableProperty] private string _currentPhaseName = string.Empty;
|
|
|
|
/// <summary>Verbose status message from bench/test operations.</summary>
|
|
[ObservableProperty] private string _verboseStatus = string.Empty;
|
|
|
|
// ── Test saved state ──────────────────────────────────────────────────────
|
|
|
|
/// <summary>True when the current test results have been saved to a report.</summary>
|
|
[ObservableProperty] private bool _isTestSaved = true;
|
|
|
|
/// <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>
|
|
[RelayCommand(CanExecute = nameof(CanStartTest))]
|
|
private async Task StartTestAsync()
|
|
{
|
|
if (CurrentPump == null) return;
|
|
|
|
// 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)
|
|
CloseUnlockDialog();
|
|
|
|
_testCts = new CancellationTokenSource();
|
|
IsTestRunning = true;
|
|
IsTestSaved = false;
|
|
|
|
await _bench.RunTestsAsync(CurrentPump, _testCts.Token);
|
|
}
|
|
|
|
private bool CanStartTest()
|
|
=> CurrentPump != null && !IsTestRunning && IsCanConnected;
|
|
|
|
/// <summary>Requests a controlled stop of the running test.</summary>
|
|
[RelayCommand(CanExecute = nameof(CanStopTest))]
|
|
private void StopTest()
|
|
{
|
|
_bench.StopTests();
|
|
_testCts?.Cancel();
|
|
}
|
|
|
|
private bool CanStopTest() => IsTestRunning;
|
|
|
|
/// <summary>
|
|
/// Operator-initiated emergency stop from the Dashboard.
|
|
/// Zeros the motor, zeros pump parameters, and cancels any running test.
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private void EmergencyStop()
|
|
{
|
|
_bench.RequestEmergencyStop("Operator pressed E-Stop on Dashboard");
|
|
_testCts?.Cancel();
|
|
}
|
|
|
|
// ── Commands: relay toggles ───────────────────────────────────────────────
|
|
|
|
/// <summary>Toggles the electronic relay (pump solenoid power).</summary>
|
|
[RelayCommand] private void ToggleElectronic() => ToggleRelay(RelayNames.Electronic);
|
|
|
|
/// <summary>Toggles the oil pump relay.</summary>
|
|
[RelayCommand] private void ToggleOilPump() => ToggleRelay(RelayNames.OilPump);
|
|
|
|
/// <summary>Toggles the deposit cooler relay.</summary>
|
|
[RelayCommand] private void ToggleDepositCooler() => ToggleRelay(RelayNames.DepositCooler);
|
|
|
|
/// <summary>Toggles the deposit heater relay.</summary>
|
|
[RelayCommand] private void ToggleDepositHeater() => ToggleRelay(RelayNames.DepositHeater);
|
|
|
|
private void ToggleRelay(string name)
|
|
{
|
|
if (!_config.Bench.Relays.TryGetValue(name, out var relay)) return;
|
|
_bench.SetRelay(name, !relay.State);
|
|
}
|
|
|
|
// ── Commands: report ──────────────────────────────────────────────────────
|
|
|
|
/// <summary>Generates and opens the PDF report for the last completed test.</summary>
|
|
[RelayCommand(CanExecute = nameof(CanGenerateReport))]
|
|
private void GenerateReport()
|
|
{
|
|
if (CurrentPump == null) return;
|
|
|
|
// Step 1: Authenticate operator.
|
|
var authVm = new UserCheckViewModel(_config, _loc, _lastAuthenticatedUser);
|
|
var authDlg = new UserCheckDialog(authVm) { Owner = Application.Current.MainWindow };
|
|
authDlg.ShowDialog();
|
|
if (!authVm.Accepted) return;
|
|
_lastAuthenticatedUser = authVm.AuthenticatedUser;
|
|
|
|
// Step 2: Collect report details (client, company, observations).
|
|
var reportVm = new ReportViewModel(_config) { OperatorName = authVm.AuthenticatedUser };
|
|
var reportDlg = new ReportDialog(reportVm) { Owner = Application.Current.MainWindow };
|
|
reportDlg.ShowDialog();
|
|
if (!reportVm.Accepted) return;
|
|
|
|
try
|
|
{
|
|
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
|
string path = _pdf.GenerateReport(
|
|
CurrentPump,
|
|
reportVm.OperatorName,
|
|
reportVm.SelectedClientName,
|
|
desktop,
|
|
clientInfo: reportVm.ClientInfo,
|
|
observations: reportVm.Observations);
|
|
_log.Info(LogId, $"Report saved: {path}");
|
|
IsTestSaved = true;
|
|
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path)
|
|
{ UseShellExecute = true });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Error(LogId, $"GenerateReport: {ex.Message}");
|
|
MessageBox.Show(string.Format(_loc.GetString("Error.ReportGeneration"), ex.Message),
|
|
_loc.GetString("Error.ReportTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private bool CanGenerateReport()
|
|
=> CurrentPump != null && !IsTestRunning && CurrentPump.Tests.Count > 0;
|
|
|
|
/// <summary>Refreshes all ViewModel-cached localised strings after a language change.</summary>
|
|
private void RefreshLocalisedStrings()
|
|
{
|
|
CanStatusText = IsCanConnected
|
|
? _loc.GetString("Status.Connected")
|
|
: _loc.GetString("Status.Disconnected");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reseeds settings-dependent runtime state after the operator saves on the Settings page.
|
|
/// Currently only the bench refresh-timer interval needs re-application.
|
|
/// </summary>
|
|
private void OnSettingsSaved()
|
|
{
|
|
if (_refreshTimer != null)
|
|
_refreshTimer.Interval = TimeSpan.FromMilliseconds(_config.Settings.RefreshBenchInterfaceMs);
|
|
SettingsSaved?.Invoke();
|
|
}
|
|
|
|
// ── Initialisation ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Loads pump IDs, wires the refresh timer, and connects to the CAN bus.
|
|
/// Call once from the View after construction.
|
|
/// </summary>
|
|
public async Task InitialiseAsync()
|
|
{
|
|
// Populate the pump selector.
|
|
PumpIdentification.LoadPumpIds();
|
|
|
|
// Connect CAN bus.
|
|
_can.SetParameters(_config.Bench.ParametersById);
|
|
_can.RegisterBenchMessageIds(GetReceiveMessageIds(_config.Bench.ParametersById));
|
|
bool canOk = _can.Connect();
|
|
|
|
if (canOk)
|
|
{
|
|
_bench.StartElectronicMsgSender();
|
|
_bench.StartRelaySender();
|
|
}
|
|
|
|
// Start the UI refresh timer.
|
|
StartRefreshTimer();
|
|
|
|
_log.Info(LogId, "MainViewModel initialised.");
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// ── Refresh timer ─────────────────────────────────────────────────────────
|
|
|
|
private System.Windows.Threading.DispatcherTimer? _refreshTimer;
|
|
|
|
private void StartRefreshTimer()
|
|
{
|
|
_refreshTimer = new System.Windows.Threading.DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromMilliseconds(_config.Settings.RefreshBenchInterfaceMs)
|
|
};
|
|
_refreshTimer.Tick += OnRefreshTick;
|
|
_refreshTimer.Start();
|
|
}
|
|
|
|
private void OnRefreshTick(object? sender, EventArgs e)
|
|
{
|
|
// Read all bench parameters that have been updated by the CAN receive thread.
|
|
BenchRpm = _bench.ReadBenchParameter(BenchParameterNames.BenchRpm);
|
|
TempIn = _bench.ReadBenchParameter(BenchParameterNames.TempIn);
|
|
TempOut = _bench.ReadBenchParameter(BenchParameterNames.TempOut);
|
|
Temp4 = _bench.ReadBenchParameter(BenchParameterNames.Temp4);
|
|
BenchTemp = _bench.ReadBenchParameter(BenchParameterNames.Temp);
|
|
QDelivery = _bench.ReadBenchParameter(BenchParameterNames.QDelivery);
|
|
QOver = _bench.ReadBenchParameter(BenchParameterNames.QOver);
|
|
PsgEncoderValue = _bench.ReadBenchParameter(BenchParameterNames.PsgEncoderValue);
|
|
|
|
// Apply analogue sensor calibration for pressure channels.
|
|
double rawP1 = _bench.ReadBenchParameter(BenchParameterNames.Pressure);
|
|
Pressure = _config.Settings.Sensors.TryGetValue(1, out var s1) ? s1.GetValueFromRaw(rawP1) : rawP1;
|
|
double rawP2 = _bench.ReadBenchParameter(BenchParameterNames.AnalogSensor2);
|
|
Pressure2 = _config.Settings.Sensors.TryGetValue(2, out var s2) ? s2.GetValueFromRaw(rawP2) : rawP2;
|
|
|
|
// Feed the angle display with all three encoder channels + status.
|
|
AngleDisplay.Update(
|
|
PsgEncoderValue,
|
|
_bench.ReadBenchParameter(BenchParameterNames.PsgEncoderWorking) == 1,
|
|
_bench.ReadBenchParameter(BenchParameterNames.InjEncoderValue),
|
|
_bench.ReadBenchParameter(BenchParameterNames.InjEncoderWorking) == 1,
|
|
_bench.ReadBenchParameter(BenchParameterNames.ManualEncoderValue),
|
|
BenchRpm,
|
|
BenchControl.IsDirectionRight);
|
|
|
|
// Feed flowmeter charts and refresh bench controls.
|
|
FlowmeterChart.AddSamples(QDelivery, QOver);
|
|
BenchControl.RefreshFromTick();
|
|
|
|
// Mirror the oil pump relay state for the Tests page preconditions checklist.
|
|
IsOilPumpOn = _config.Bench.Relays.TryGetValue(RelayNames.OilPump, out var oilRelay) && oilRelay.State;
|
|
|
|
// Feed page-scoped Bench VMs (pressure trace + interlock banner).
|
|
BenchPage.RefreshFromTick();
|
|
|
|
// Refresh Dashboard's active-alarm list from the bench alarm bitmask.
|
|
DashboardAlarms.Update((int)_bench.ReadBenchParameter(BenchParameterNames.Alarms));
|
|
|
|
if (CurrentPump != null)
|
|
{
|
|
PumpRpm = _bench.ReadPumpParameter(PumpParameterNames.Rpm);
|
|
PumpTemp = _bench.ReadPumpParameter(PumpParameterNames.Temp);
|
|
PumpMe = _bench.ReadPumpParameter(PumpParameterNames.Me);
|
|
PumpFbkw = _bench.ReadPumpParameter(PumpParameterNames.Fbkw);
|
|
PumpTein = _bench.ReadPumpParameter(PumpParameterNames.Tein);
|
|
|
|
// Update status display 1 (Status word) when the CAN receiver flags an update.
|
|
if (CurrentPump.ParametersByName.TryGetValue(PumpParameterNames.Status, out var statusParam)
|
|
&& statusParam.NeedsUpdate)
|
|
{
|
|
var def = _config.LoadPumpStatus(statusParam.Type);
|
|
if (def != null) StatusDisplay1.UpdateStatusWord(def, (int)statusParam.Value);
|
|
statusParam.NeedsUpdate = false;
|
|
}
|
|
|
|
// Update status display 2 (Empf3 word).
|
|
if (CurrentPump.ParametersByName.TryGetValue(PumpParameterNames.Empf3, out var empf3Param)
|
|
&& empf3Param.NeedsUpdate)
|
|
{
|
|
var def = _config.LoadPumpStatus(empf3Param.Type);
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Service event handlers ────────────────────────────────────────────────
|
|
|
|
private void OnTestStarted()
|
|
=> App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
IsTestRunning = true;
|
|
VerboseStatus = _loc.GetString("Test.Started");
|
|
|
|
_testStartedUtc = DateTime.UtcNow;
|
|
TestElapsed = TimeSpan.Zero;
|
|
_testTimer = new DispatcherTimer(
|
|
TimeSpan.FromSeconds(1),
|
|
DispatcherPriority.Normal,
|
|
(_, _) => TestElapsed = DateTime.UtcNow - _testStartedUtc,
|
|
App.Current.Dispatcher);
|
|
TestPanel.IsRunning = true;
|
|
TestPanel.ResetResults();
|
|
ResultDisplay.Clear();
|
|
PumpControl.Reset();
|
|
_bench.StartPumpSender();
|
|
_log.Info(LogId, "Test sequence started.");
|
|
});
|
|
|
|
private void OnTestFinished(bool interrupted, bool success)
|
|
=> App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
_testTimer?.Stop();
|
|
_testTimer = null;
|
|
|
|
IsTestRunning = false;
|
|
LastTestSuccess = !interrupted && success;
|
|
VerboseStatus = interrupted ? _loc.GetString("Test.Stopped") : (success ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail"));
|
|
TestPanel.IsRunning = false;
|
|
TestPanel.ClearPhaseTimer();
|
|
_bench.StopPumpSender();
|
|
StartTestCommand.NotifyCanExecuteChanged();
|
|
StopTestCommand.NotifyCanExecuteChanged();
|
|
GenerateReportCommand.NotifyCanExecuteChanged();
|
|
|
|
// Populate results table from all completed tests.
|
|
if (!interrupted && CurrentPump != null)
|
|
ResultDisplay.LoadAllResults(CurrentPump.Tests);
|
|
|
|
// Capture a session-only history entry (Results page §5) — covers normal
|
|
// and interrupted completions. Snapshot is deep-cloned so later runs
|
|
// cannot mutate this entry's data.
|
|
if (CurrentPump != null)
|
|
ResultsPage.CaptureRun(CurrentPump, interrupted, success);
|
|
|
|
_log.Info(LogId,
|
|
$"Test finished — interrupted={interrupted}, success={success}");
|
|
});
|
|
|
|
private void OnKwpDisconnectPump()
|
|
=> App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
_bench.SetRelay(RelayNames.Electronic, false);
|
|
});
|
|
|
|
private void OnKwpReconnectPump()
|
|
=> App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
_bench.SetRelay(RelayNames.Electronic, true);
|
|
});
|
|
|
|
private void ShowPsgSyncError()
|
|
=> MessageBox.Show(
|
|
_loc.GetString("Error.PsgSync"),
|
|
_loc.GetString("Error.PsgTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
|
|
// ── Voltage warning ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Shows a voltage warning dialog when the pump supply voltage requirement
|
|
/// changes between 27 V and 13.5 V (or vice versa). Only triggers on
|
|
/// state transitions, matching the old <c>WAlert27v</c> behaviour.
|
|
/// </summary>
|
|
private void CheckVoltageWarning(PumpDefinition pump)
|
|
{
|
|
bool is27V = !string.IsNullOrEmpty(pump.Tension)
|
|
&& pump.Tension.Contains("27");
|
|
|
|
if (is27V && !_lastPumpWas27V)
|
|
{
|
|
var vm = new Dialogs.VoltageWarningViewModel("27 V");
|
|
var dlg = new Views.Dialogs.VoltageWarningDialog(vm)
|
|
{ Owner = Application.Current.MainWindow };
|
|
dlg.ShowDialog();
|
|
_lastPumpWas27V = true;
|
|
}
|
|
else if (!is27V && _lastPumpWas27V)
|
|
{
|
|
var vm = new Dialogs.VoltageWarningViewModel("13.5 V");
|
|
var dlg = new Views.Dialogs.VoltageWarningDialog(vm)
|
|
{ Owner = Application.Current.MainWindow };
|
|
dlg.ShowDialog();
|
|
_lastPumpWas27V = false;
|
|
}
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Returns only the message IDs that contain at least one receive parameter.
|
|
/// Transmit-only IDs (RPM command, ElectronicMsg, etc.) are excluded because
|
|
/// they are frames we send, not frames the remote device sends to us.
|
|
/// </summary>
|
|
private static HashSet<uint> GetReceiveMessageIds(
|
|
Dictionary<uint, List<CanBusParameter>> parametersById)
|
|
{
|
|
var ids = new HashSet<uint>();
|
|
foreach (var kv in parametersById)
|
|
{
|
|
if (kv.Value.Any(p => p.IsReceive))
|
|
ids.Add(kv.Key);
|
|
}
|
|
return ids;
|
|
}
|
|
}
|
|
}
|