using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using HC_APTBS.Models; using HC_APTBS.Services; namespace HC_APTBS.ViewModels { /// /// 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 IBenchService _bench; private readonly IConfigurationService _config; private readonly IPdfService _pdf; private readonly IUnlockService _unlock; private readonly IAppLogger _log; private const string LogId = "MainViewModel"; // ── CancellationToken for test runs ─────────────────────────────────────── private CancellationTokenSource? _testCts; // ── 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; } = new(); /// ViewModel for the measurement results table. public ResultDisplayViewModel ResultDisplay { get; } = new(); /// 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(); // ── 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, IAppLogger logger) { _can = canService; _bench = benchService; _config = configService; _pdf = pdfService; _unlock = unlockService; _log = logger; PumpIdentification = new PumpIdentificationViewModel(kwpService, configService, logger); DfiViewModel = new DfiManageViewModel(kwpService, configService); PumpControl = new PumpControlViewModel(benchService); BenchControl = new BenchControlViewModel(benchService, configService); AngleDisplay = new AngleDisplayViewModel(configService); // 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); // Bench service events _bench.TestStarted += OnTestStarted; _bench.TestFinished += OnTestFinished; _bench.PhaseChanged += phase => App.Current.Dispatcher.Invoke(() => { CurrentPhaseName = phase; TestPanel.SetActivePhase(phase); }); _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); }); // 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(); } // ── CAN connection ──────────────────────────────────────────────────────── /// CAN bus status display text. [ObservableProperty] private string _canStatusText = "Disconnected"; /// 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 ? "Connected" : "Connection failed"; 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 = "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; /// 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 (bar). [ObservableProperty] private double _pressure; /// PSG encoder position value. [ObservableProperty] private double _psgEncoderValue; // ── 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; /// True when a K-Line session is active. [ObservableProperty] private bool _isKLineConnected; // ── 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; // ── Operator / client info ──────────────────────────────────────────────── /// Operator name for report generation. [ObservableProperty] private string _operatorName = string.Empty; /// Client name for report generation. [ObservableProperty] private string _clientName = string.Empty; // ── Test saved state ────────────────────────────────────────────────────── /// True when the current test results have been saved to a report. [ObservableProperty] private bool _isTestSaved = true; // ── Commands: test ──────────────────────────────────────────────────────── /// Starts the test sequence for the current pump. [RelayCommand(CanExecute = nameof(CanStartTest))] private async Task StartTestAsync() { if (CurrentPump == null) return; _testCts = new CancellationTokenSource(); IsTestRunning = true; IsTestSaved = false; // Run immobilizer unlock if required (e.g. Ford pumps). if (CurrentPump.UnlockType != 0) { VerboseStatus = "Immobilizer unlock in progress..."; await _unlock.UnlockAsync(CurrentPump, _testCts.Token); if (_testCts.Token.IsCancellationRequested) return; } 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; // ── 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; try { string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); string path = _pdf.GenerateReport(CurrentPump, OperatorName, ClientName, desktop); _log.Info(LogId, $"Report saved: {path}"); IsTestSaved = true; // Open the generated PDF with the default viewer. System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); } catch (Exception ex) { _log.Error(LogId, $"GenerateReport: {ex.Message}"); MessageBox.Show($"Failed to generate report:\n{ex.Message}", "Report Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private bool CanGenerateReport() => CurrentPump != null && !IsTestRunning && CurrentPump.Tests.Count > 0; // ── Commands: settings ──────────────────────────────────────────────────── /// Saves all current settings and bench configuration to disk. [RelayCommand] private void SaveSettings() { _config.SaveSettings(); _config.SaveBench(); } // ── 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); QDelivery = _bench.ReadBenchParameter(BenchParameterNames.QDelivery); QOver = _bench.ReadBenchParameter(BenchParameterNames.QOver); Pressure = _bench.ReadBenchParameter(BenchParameterNames.Pressure); PsgEncoderValue = _bench.ReadBenchParameter(BenchParameterNames.PsgEncoderValue); // 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(); 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 = "Test started..."; 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(() => { IsTestRunning = false; LastTestSuccess = !interrupted && success; VerboseStatus = interrupted ? "Test stopped." : (success ? "PASS" : "FAIL"); TestPanel.IsRunning = false; _bench.StopPumpSender(); StartTestCommand.NotifyCanExecuteChanged(); StopTestCommand.NotifyCanExecuteChanged(); GenerateReportCommand.NotifyCanExecuteChanged(); // Populate results table from all completed tests. if (!interrupted && CurrentPump != null) ResultDisplay.LoadAllResults(CurrentPump.Tests); _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 static void ShowPsgSyncError() => MessageBox.Show( "PSG sync pulse not detected. Check encoder connection.", "PSG Error", MessageBoxButton.OK, MessageBoxImage.Warning); // ── 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; } } }