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(); }); _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); }); _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); }); _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.IsPreInVisible = 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; } } }