525 lines
24 KiB
C#
525 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
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
|
|
{
|
|
/// <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
|
|
{
|
|
// ── 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 ──────────────────────────────────────────────────────
|
|
|
|
/// <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; } = new();
|
|
|
|
/// <summary>ViewModel for the measurement results table.</summary>
|
|
public ResultDisplayViewModel ResultDisplay { get; } = new();
|
|
|
|
/// <summary>ViewModel for the manual pump control sliders (FBKW, ME, PreIn).</summary>
|
|
public PumpControlViewModel PumpControl { get; private set; } = null!;
|
|
|
|
/// <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();
|
|
|
|
// ── 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,
|
|
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);
|
|
|
|
// 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 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, _) => App.Current.Dispatcher.Invoke(
|
|
() => TestPanel.UpdateLiveIndicator(paramName, value));
|
|
|
|
// 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 ──────────────────────────────────────────────────
|
|
|
|
/// <summary>Convenience accessor for the currently loaded pump definition.</summary>
|
|
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);
|
|
|
|
// 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.StartElectronicMsgSender();
|
|
_bench.StartMemoryRequestSender();
|
|
|
|
// Notify commands that depend on pump availability.
|
|
StartTestCommand.NotifyCanExecuteChanged();
|
|
GenerateReportCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
// ── CAN connection ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>CAN bus status display text.</summary>
|
|
[ObservableProperty] private string _canStatusText = "Disconnected";
|
|
|
|
/// <summary>True when the CAN bus adapter is connected.</summary>
|
|
[ObservableProperty] private bool _isCanConnected;
|
|
|
|
/// <summary>Connects to the CAN bus adapter.</summary>
|
|
[RelayCommand]
|
|
private void ConnectCan()
|
|
{
|
|
_can.SetParameters(_config.Bench.ParametersById);
|
|
bool ok = _can.Connect();
|
|
CanStatusText = ok ? "Connected" : "Connection failed";
|
|
IsCanConnected = ok;
|
|
}
|
|
|
|
/// <summary>Disconnects from the CAN bus adapter.</summary>
|
|
[RelayCommand]
|
|
private void DisconnectCan()
|
|
{
|
|
_bench.StopElectronicMsgSender();
|
|
_bench.StopMemoryRequestSender();
|
|
_bench.StopPumpSender();
|
|
_can.Disconnect();
|
|
IsCanConnected = false;
|
|
CanStatusText = "Disconnected";
|
|
}
|
|
|
|
// ── 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>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 (bar).</summary>
|
|
[ObservableProperty] private double _pressure;
|
|
|
|
/// <summary>PSG encoder position value.</summary>
|
|
[ObservableProperty] private double _psgEncoderValue;
|
|
|
|
// ── 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] 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;
|
|
|
|
// ── Test status ───────────────────────────────────────────────────────────
|
|
|
|
/// <summary>True while a test sequence is running.</summary>
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(StartTestCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(StopTestCommand))]
|
|
private bool _isTestRunning;
|
|
|
|
/// <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;
|
|
|
|
// ── Operator / client info ────────────────────────────────────────────────
|
|
|
|
/// <summary>Operator name for report generation.</summary>
|
|
[ObservableProperty] private string _operatorName = string.Empty;
|
|
|
|
/// <summary>Client name for report generation.</summary>
|
|
[ObservableProperty] private string _clientName = string.Empty;
|
|
|
|
// ── Test saved state ──────────────────────────────────────────────────────
|
|
|
|
/// <summary>True when the current test results have been saved to a report.</summary>
|
|
[ObservableProperty] private bool _isTestSaved = true;
|
|
|
|
// ── Commands: test ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Starts the test sequence for the current pump.</summary>
|
|
[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;
|
|
|
|
/// <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;
|
|
|
|
// ── 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;
|
|
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 ────────────────────────────────────────────────────
|
|
|
|
/// <summary>Saves all current settings and bench configuration to disk.</summary>
|
|
[RelayCommand]
|
|
private void SaveSettings()
|
|
{
|
|
_config.SaveSettings();
|
|
_config.SaveBench();
|
|
}
|
|
|
|
// ── 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.Connect();
|
|
|
|
// 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);
|
|
|
|
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);
|
|
}
|
|
}
|