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
{
/// Identifies the top-level navigation page shown in the shell.
public enum AppPage
{
/// Bench controls, flowmeter charts, encoder angles.
Bench = 0,
/// Pump manual control, DFI, status displays.
Pump = 1,
/// Test suite, live progress, results.
Tests = 2,
/// At-a-glance operator landing page: readings, connections, alarms, quick actions.
Dashboard = 3,
/// Application configuration: safety limits, PID, motor, report, K-Line, language.
Settings = 4,
/// Session-only history of completed test runs with detail view and PDF export.
Results = 5
}
///
/// Root ViewModel for the application's main window.
///
/// Responsibilities:
///
/// - CAN connection lifecycle.
/// - Bench status display (RPM, temperatures, flow measurements).
/// - Test start/stop and progress reporting.
/// - Relay toggle commands.
/// - Report generation trigger.
///
///
/// Pump selection and K-Line ECU identification are delegated to
/// .
///
public sealed partial class MainViewModel : ObservableObject
{
// ── 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 const string LogId = "MainViewModel";
// ── CancellationToken for test runs ───────────────────────────────────────
private CancellationTokenSource? _testCts;
// ── Test elapsed timer ────────────────────────────────────────────────────
/// Ticks every second while a test is running to update .
private DispatcherTimer? _testTimer;
/// UTC start time of the current test; used by the timer to compute elapsed duration.
private DateTime _testStartedUtc;
// ── Unlock tracking ──────────────────────────────────────────────────────
/// CTS for the currently running immobilizer unlock, if any.
private CancellationTokenSource? _unlockCts;
/// ViewModel for the non-modal unlock progress window.
private UnlockProgressViewModel? _unlockVm;
///
/// 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.
///
public UnlockProgressViewModel? CurrentUnlockVm
{
get => _unlockVm;
private set
{
if (!ReferenceEquals(_unlockVm, value))
{
_unlockVm = value;
OnPropertyChanged();
}
}
}
/// Remembers the last authenticated username to pre-fill the next auth dialog.
private string _lastAuthenticatedUser = string.Empty;
/// Tracks whether the last selected pump required 27 V, for transition-based voltage warnings.
private bool _lastPumpWas27V;
// ── Child ViewModels ──────────────────────────────────────────────────────
/// ViewModel for pump selection and K-Line ECU identification.
public PumpIdentificationViewModel PumpIdentification { get; }
/// ViewModel for the DFI manage user control.
public DfiManageViewModel DfiViewModel { get; }
/// ViewModel for the test panel showing all test sections and phase cards.
public TestPanelViewModel TestPanel { get; }
/// ViewModel for the measurement results table.
public ResultDisplayViewModel ResultDisplay { get; }
/// ViewModel for the manual pump control sliders (FBKW, ME, PreIn).
public PumpControlViewModel PumpControl { get; private set; } = null!;
/// ViewModel for manual bench controls (direction, RPM, oil pump, counter).
public BenchControlViewModel BenchControl { get; }
/// ViewModel for the two flowmeter real-time charts (Q-Delivery, Q-Over).
public FlowmeterChartViewModel FlowmeterChart { get; } = new();
/// ViewModel for the encoder angle monitoring display (PSG, INJ, Manual, Lock Angle).
public AngleDisplayViewModel AngleDisplay { get; }
/// ViewModel for the first pump status display (Status word).
public StatusDisplayViewModel StatusDisplay1 { get; } = new();
/// ViewModel for the second pump status display (Empf3 word).
public StatusDisplayViewModel StatusDisplay2 { get; } = new();
/// ViewModel for the Dashboard's active-alarm list.
public DashboardAlarmsViewModel DashboardAlarms { get; }
/// Diagnostic Trouble Code list for the Pump page §3.b sub-section.
public DtcListViewModel DtcList { get; }
// ── Page ViewModels (thin façades over the child VMs above) ───────────────
/// Dashboard navigation page VM.
public DashboardPageViewModel DashboardPage { get; private set; } = null!;
/// Bench navigation page VM.
public BenchPageViewModel BenchPage { get; private set; } = null!;
/// Pump navigation page VM.
public PumpPageViewModel PumpPage { get; private set; } = null!;
/// Tests navigation page VM.
public TestsPageViewModel TestsPage { get; private set; } = null!;
/// Settings navigation page VM.
public SettingsPageViewModel SettingsPage { get; private set; } = null!;
/// Results navigation page VM (session-only test-run history).
public ResultsPageViewModel ResultsPage { get; private set; } = null!;
// ── Navigation state ──────────────────────────────────────────────────────
/// Currently selected top-level navigation page.
[ObservableProperty] private AppPage _selectedPage = AppPage.Dashboard;
// ── Constructor ───────────────────────────────────────────────────────────
///
/// Constructs the MainViewModel and wires all service events to UI-bound properties.
/// Call after construction.
///
public MainViewModel(
ICanService canService,
IKwpService kwpService,
IBenchService benchService,
IConfigurationService configService,
IPdfService pdfService,
IUnlockService unlockService,
ILocalizationService localizationService,
IAppLogger logger)
{
_can = canService;
_kwp = kwpService;
_bench = benchService;
_config = configService;
_pdf = pdfService;
_unlock = unlockService;
_loc = localizationService;
_log = logger;
_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;
// 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);
// 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.UpdateLiveIndicator(paramName, value);
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);
// KWP pump power-cycle callbacks
kwpService.PumpDisconnectRequested += OnKwpDisconnectPump;
kwpService.PumpReconnectRequested += OnKwpReconnectPump;
}
// ── Pump change handling ──────────────────────────────────────────────────
/// Convenience accessor for the currently loaded pump definition.
public PumpDefinition? CurrentPump => PumpIdentification.CurrentPump;
private void OnPumpChanged(PumpDefinition? pump)
{
if (pump == null) return;
// Stop any senders from the previous pump.
_bench.StopMemoryRequestSender();
_bench.StopPumpSender();
// 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));
// Configure pump control sliders.
PumpControl.IsPreInAvailable = pump.HasPreInjection;
PumpControl.IsEnabled = true;
PumpControl.Reset();
// 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);
}
// 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).
CheckVoltageWarning(pump);
// Start immobilizer unlock if this pump requires it (Ford VP44).
StartUnlockIfRequired(pump);
}
// ── Immobilizer unlock ────────────────────────────────────────────────────
///
/// Starts the immobilizer unlock sequence in a non-modal window if the pump
/// requires it (UnlockType != 0). Cancels any previously running unlock first.
///
private void StartUnlockIfRequired(PumpDefinition pump)
{
// Cancel and close any previous unlock window.
CloseUnlockDialog();
if (pump.UnlockType == 0) return;
_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);
}
///
/// Cancels any running unlock, stops persistent CAN senders, closes the
/// window, and disposes resources. Safe to call when no unlock is active.
///
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();
_unlockCts.Dispose();
_unlockCts = null;
}
if (_unlockVm != null)
{
_unlockVm.RequestClose -= CloseUnlockDialog;
_unlockVm.Dispose();
CurrentUnlockVm = null;
}
}
// ── CAN connection ────────────────────────────────────────────────────────
/// CAN bus status display text.
[ObservableProperty] private string _canStatusText = string.Empty;
/// True when the CAN bus adapter is connected.
[ObservableProperty] private bool _isCanConnected;
/// Connects to the CAN bus adapter.
[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();
}
}
/// Disconnects from the CAN bus adapter.
[RelayCommand]
private void DisconnectCan()
{
_bench.StopElectronicMsgSender();
_bench.StopRelaySender();
_bench.StopMemoryRequestSender();
_bench.StopPumpSender();
_can.Disconnect();
IsCanConnected = false;
CanStatusText = _loc.GetString("Status.Disconnected");
}
// ── Live bench readings ───────────────────────────────────────────────────
/// Bench motor speed (RPM), updated by the refresh timer.
[ObservableProperty] private double _benchRpm;
/// Oil inlet temperature T-in (°C).
[ObservableProperty] private double _tempIn;
/// Oil outlet temperature T-out (°C).
[ObservableProperty] private double _tempOut;
/// Auxiliary temperature T4 (°C).
[ObservableProperty] private double _temp4;
/// Oil tank temperature (°C).
[ObservableProperty] private double _benchTemp;
/// Fuel delivery measurement Q-delivery (cc/stroke).
[ObservableProperty] private double _qDelivery;
/// Fuel overflow/pilot measurement Q-over (cc/stroke).
[ObservableProperty] private double _qOver;
/// Bench oil pressure P1 (bar), sensor-calibrated.
[ObservableProperty] private double _pressure;
/// Analogue sensor 2 pressure P2 (bar), sensor-calibrated.
[ObservableProperty] private double _pressure2;
/// PSG encoder position value.
[ObservableProperty] private double _psgEncoderValue;
///
/// True when the Oil Pump relay is currently energised. Mirrored on each refresh
/// tick from _config.Bench.Relays[RelayNames.OilPump] so the Tests page
/// preconditions checklist can bind to it without walking the relay dictionary.
///
[ObservableProperty] private bool _isOilPumpOn;
// ── Pump live readings (from pump CAN parameters) ──────────────────────────
/// Pump RPM reported by the ECU over CAN.
[ObservableProperty] private double _pumpRpm;
/// Pump internal temperature reported by the ECU over CAN.
[ObservableProperty] private double _pumpTemp;
/// Pump ME (metering) value from CAN.
[ObservableProperty] private double _pumpMe;
/// Pump FBkW (feedback) value from CAN.
[ObservableProperty] private double _pumpFbkw;
/// Pump T-ein (inlet timing) value from CAN, in microseconds.
[ObservableProperty] private double _pumpTein;
// ── Bench/pump connection status ──────────────────────────────────────────
/// True when the bench controller is connected.
[ObservableProperty] private bool _isBenchConnected;
/// True when the pump ECU is responding on CAN.
[ObservableProperty] private bool _isPumpConnected;
/// True when oil circulation has been detected.
[ObservableProperty] private bool _isOilCirculating;
/// Current K-Line session state (Disconnected / Connected / Failed).
[ObservableProperty] private KLineConnectionState _kLineState = KLineConnectionState.Disconnected;
// ── Test status ───────────────────────────────────────────────────────────
/// True while a test sequence is running.
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartTestCommand))]
[NotifyCanExecuteChangedFor(nameof(StopTestCommand))]
private bool _isTestRunning;
/// True if the last test passed.
[ObservableProperty] private bool _lastTestSuccess;
/// Name of the currently executing test phase.
[ObservableProperty] private string _currentPhaseName = string.Empty;
/// Verbose status message from bench/test operations.
[ObservableProperty] private string _verboseStatus = string.Empty;
// ── Test saved state ──────────────────────────────────────────────────────
/// True when the current test results have been saved to a report.
[ObservableProperty] private bool _isTestSaved = true;
/// Elapsed time since the current test started. Updated every second; retains last value when idle.
[ObservableProperty] private TimeSpan _testElapsed;
// ── Commands: test ────────────────────────────────────────────────────────
/// Starts the test sequence for the current pump.
[RelayCommand(CanExecute = nameof(CanStartTest))]
private async Task StartTestAsync()
{
if (CurrentPump == null) return;
// Block test start if an unlock is still in progress.
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;
}
_testCts = new CancellationTokenSource();
IsTestRunning = true;
IsTestSaved = false;
await _bench.RunTestsAsync(CurrentPump, _testCts.Token);
}
private bool CanStartTest()
=> CurrentPump != null && !IsTestRunning && IsCanConnected;
/// Requests a controlled stop of the running test.
[RelayCommand(CanExecute = nameof(CanStopTest))]
private void StopTest()
{
_bench.StopTests();
_testCts?.Cancel();
}
private bool CanStopTest() => IsTestRunning;
///
/// Operator-initiated emergency stop from the Dashboard.
/// Zeros the motor, zeros pump parameters, and cancels any running test.
///
[RelayCommand]
private void EmergencyStop()
{
_bench.RequestEmergencyStop("Operator pressed E-Stop on Dashboard");
_testCts?.Cancel();
}
// ── Commands: relay toggles ───────────────────────────────────────────────
/// Toggles the electronic relay (pump solenoid power).
[RelayCommand] private void ToggleElectronic() => ToggleRelay(RelayNames.Electronic);
/// Toggles the oil pump relay.
[RelayCommand] private void ToggleOilPump() => ToggleRelay(RelayNames.OilPump);
/// Toggles the deposit cooler relay.
[RelayCommand] private void ToggleDepositCooler() => ToggleRelay(RelayNames.DepositCooler);
/// Toggles the deposit heater relay.
[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 ──────────────────────────────────────────────────────
/// Generates and opens the PDF report for the last completed test.
[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;
/// Refreshes all ViewModel-cached localised strings after a language change.
private void RefreshLocalisedStrings()
{
CanStatusText = IsCanConnected
? _loc.GetString("Status.Connected")
: _loc.GetString("Status.Disconnected");
}
///
/// Reseeds settings-dependent runtime state after the operator saves on the Settings page.
/// Currently only the bench refresh-timer interval needs re-application.
///
private void OnSettingsSaved()
{
if (_refreshTimer != null)
_refreshTimer.Interval = TimeSpan.FromMilliseconds(_config.Settings.RefreshBenchInterfaceMs);
}
// ── Initialisation ────────────────────────────────────────────────────────
///
/// Loads pump IDs, wires the refresh timer, and connects to the CAN bus.
/// Call once from the View after construction.
///
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;
}
}
}
// ── 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 ────────────────────────────────────────────────────────
///
/// 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 WAlert27v behaviour.
///
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 ───────────────────────────────────────────────────────────────
///
/// 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.
///
private static HashSet GetReceiveMessageIds(
Dictionary> parametersById)
{
var ids = new HashSet();
foreach (var kv in parametersById)
{
if (kv.Value.Any(p => p.IsReceive))
ids.Add(kv.Key);
}
return ids;
}
}
}