initial commit

This commit is contained in:
2026-04-11 12:45:18 +02:00
commit 6e1b929e2f
1246 changed files with 177580 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// ViewModel for the BenchParamConfig user control.
///
/// <para>
/// Exposes all editable fields for one <see cref="CanBusParameter"/>: CAN message ID,
/// byte positions, IIR filter alpha, and the six calibration coefficients P1P6.
/// Call <see cref="LoadParameter"/> to populate from a model instance, then
/// <see cref="ApplyToParameter"/> to write edited values back.
/// </para>
/// </summary>
public sealed partial class BenchConfigViewModel : ObservableObject
{
// ── Display ───────────────────────────────────────────────────────────────
/// <summary>Human-readable name of the parameter being edited.</summary>
[ObservableProperty] private string _name = string.Empty;
// ── CAN frame fields ──────────────────────────────────────────────────────
/// <summary>CAN message ID as a hex string (e.g. "1A0").</summary>
[ObservableProperty] private string _messageIdHex = string.Empty;
/// <summary>High-byte index within the CAN payload (0-based).</summary>
[ObservableProperty] private string _byteH = string.Empty;
/// <summary>Low-byte index within the CAN payload (0-based).</summary>
[ObservableProperty] private string _byteL = string.Empty;
/// <summary>IIR smoothing factor α (01). 1 = no filtering.</summary>
[ObservableProperty] private string _alpha = string.Empty;
// ── Calibration formula ───────────────────────────────────────────────────
/// <summary>True when the 6-parameter calibration formula is enabled.</summary>
[ObservableProperty] private bool _formulaEnabled;
/// <summary>Calibration coefficient P1.</summary>
[ObservableProperty] private string _p1 = string.Empty;
/// <summary>Calibration coefficient P2.</summary>
[ObservableProperty] private string _p2 = string.Empty;
/// <summary>Calibration coefficient P3.</summary>
[ObservableProperty] private string _p3 = string.Empty;
/// <summary>Calibration coefficient P4.</summary>
[ObservableProperty] private string _p4 = string.Empty;
/// <summary>Calibration coefficient P5.</summary>
[ObservableProperty] private string _p5 = string.Empty;
/// <summary>Calibration coefficient P6.</summary>
[ObservableProperty] private string _p6 = string.Empty;
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>Populates all editable fields from <paramref name="parameter"/>.</summary>
public void LoadParameter(CanBusParameter parameter)
{
Name = parameter.Name;
MessageIdHex = parameter.ID.ToString("X");
ByteH = parameter.ByteH.ToString();
ByteL = parameter.ByteL.ToString();
Alpha = parameter.Alpha.ToString();
FormulaEnabled = !parameter.DisableCalibration;
P1 = parameter.P1.ToString();
P2 = parameter.P2.ToString();
P3 = parameter.P3.ToString();
P4 = parameter.P4.ToString();
P5 = parameter.P5.ToString();
P6 = parameter.P6.ToString();
}
/// <summary>
/// Writes all edited values back to <paramref name="parameter"/>.
/// Returns <see langword="false"/> if any field fails to parse.
/// </summary>
public bool ApplyToParameter(CanBusParameter parameter)
{
if (!uint.TryParse(MessageIdHex,
System.Globalization.NumberStyles.HexNumber,
null, out uint id)) return false;
if (!ushort.TryParse(ByteH, out ushort byteH)) return false;
if (!ushort.TryParse(ByteL, out ushort byteL)) return false;
if (!double.TryParse(Alpha,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out double alpha)) return false;
parameter.ID = id;
parameter.ByteH = byteH;
parameter.ByteL = byteL;
parameter.Alpha = alpha;
parameter.DisableCalibration = !FormulaEnabled;
if (FormulaEnabled)
{
if (!TryParseInvariant(P1, out double p1)) return false;
if (!TryParseInvariant(P2, out double p2)) return false;
if (!TryParseInvariant(P3, out double p3)) return false;
if (!TryParseInvariant(P4, out double p4)) return false;
if (!TryParseInvariant(P5, out double p5)) return false;
if (!TryParseInvariant(P6, out double p6)) return false;
parameter.P1 = p1;
parameter.P2 = p2;
parameter.P3 = p3;
parameter.P4 = p4;
parameter.P5 = p5;
parameter.P6 = p6;
}
return true;
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static bool TryParseInvariant(string text, out double result)
=> double.TryParse(text,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out result);
}
}

View File

@@ -0,0 +1,178 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// ViewModel for the DFIManageDisplay user control.
///
/// <para>
/// Exposes the current DFI (injection timing offset) value, a slider for manual
/// adjustment, KWP version selection, and commands to read and write DFI via K-Line.
/// Auto-mode is respected by the bench service when executing a DFI test phase.
/// </para>
/// </summary>
public sealed partial class DfiManageViewModel : ObservableObject
{
// ── Services ──────────────────────────────────────────────────────────────
private readonly IKwpService _kwp;
private readonly IConfigurationService _config;
private const string LogId = "DfiManageViewModel";
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>Initialises the ViewModel with the required services.</summary>
public DfiManageViewModel(IKwpService kwpService, IConfigurationService configService)
{
_kwp = kwpService;
_config = configService;
}
// ── DFI display ───────────────────────────────────────────────────────────
/// <summary>Current DFI value read from the ECU (displayed in the label).</summary>
[ObservableProperty] private double _currentDfi;
/// <summary>
/// Slider position in integer hundredths of a DFI unit (range 300 to 300).
/// Divide by 100 to get the actual DFI value.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SliderDfiValue))]
private int _sliderRaw;
/// <summary>The DFI value represented by the current slider position.</summary>
public double SliderDfiValue => Math.Round(SliderRaw / 100.0, 2);
// ── Options ───────────────────────────────────────────────────────────────
/// <summary>
/// KWP protocol version index (0, 1, or 2) used when writing DFI.
/// Corresponds to the three known VP44 authentication variants.
/// </summary>
[ObservableProperty] private int _versionIndex = 1;
/// <summary>
/// When <see langword="true"/> the bench service may perform automatic DFI
/// correction during test phases; when <see langword="false"/> it skips them.
/// </summary>
[ObservableProperty] private bool _isAutoMode = true;
// ── Operation state ───────────────────────────────────────────────────────
/// <summary>True while a read or write K-Line operation is in progress.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ReadDfiCommand))]
[NotifyCanExecuteChangedFor(nameof(WriteDfiCommand))]
private bool _isBusy;
/// <summary>Progress percentage (0100) for the current K-Line operation.</summary>
[ObservableProperty] private int _progressPercent;
/// <summary>Verbose status message for the current K-Line operation.</summary>
[ObservableProperty] private string _progressMessage = string.Empty;
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Sets the displayed DFI value and repositions the slider to match.
/// Safe to call from any thread.
/// </summary>
public void SetDfi(double dfi)
{
double rounded = Math.Round(dfi, 2);
Application.Current.Dispatcher.Invoke(() =>
{
CurrentDfi = rounded;
SliderRaw = (int)(rounded * 100);
});
}
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>Reads the current DFI value from the ECU over K-Line.</summary>
[RelayCommand(CanExecute = nameof(CanOperate))]
private async Task ReadDfiAsync()
{
string? port = _kwp.DetectKLinePort();
if (string.IsNullOrEmpty(port))
{
MessageBox.Show("K-Line device not found. Check that the FTDI adapter is connected.",
"K-Line Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
IsBusy = true;
_kwp.ProgressChanged += OnProgress;
try
{
string dfiStr = await _kwp.ReadDfiAsync(port);
if (double.TryParse(dfiStr,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out double val))
SetDfi(val);
}
finally
{
_kwp.ProgressChanged -= OnProgress;
IsBusy = false;
ProgressPercent = 0;
ProgressMessage = string.Empty;
}
}
/// <summary>Writes the slider DFI value to the ECU and reads back the result.</summary>
[RelayCommand(CanExecute = nameof(CanOperate))]
private async Task WriteDfiAsync()
{
string? port = _kwp.DetectKLinePort();
if (string.IsNullOrEmpty(port))
{
MessageBox.Show("K-Line device not found. Check that the FTDI adapter is connected.",
"K-Line Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
float value = (float)SliderDfiValue;
int version = VersionIndex;
IsBusy = true;
_kwp.ProgressChanged += OnProgress;
try
{
string dfiStr = await _kwp.WriteDfiAsync(port, value, version);
if (double.TryParse(dfiStr,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out double val))
SetDfi(val);
}
finally
{
_kwp.ProgressChanged -= OnProgress;
IsBusy = false;
ProgressPercent = 0;
ProgressMessage = string.Empty;
}
}
private bool CanOperate() => !IsBusy;
// ── Helpers ───────────────────────────────────────────────────────────────
private void OnProgress(int pct, string msg)
{
Application.Current.Dispatcher.Invoke(() =>
{
ProgressPercent = pct;
ProgressMessage = msg;
});
}
}
}

View File

@@ -0,0 +1,137 @@
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels.Dialogs
{
/// <summary>
/// ViewModel for the WKlineErrors dialog.
///
/// <para>
/// Displays the raw DTC (Diagnostic Trouble Code) text returned by the VP44 ECU
/// over K-Line and provides commands to read or clear fault codes.
/// </para>
/// </summary>
public sealed partial class KlineErrorsViewModel : ObservableObject
{
// ── Services ──────────────────────────────────────────────────────────────
private readonly IKwpService _kwp;
private readonly IConfigurationService _config;
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>Initialises the ViewModel and shows the initially known error text.</summary>
public KlineErrorsViewModel(
IKwpService kwpService,
IConfigurationService configService,
string initialErrors = "")
{
_kwp = kwpService;
_config = configService;
ErrorText = initialErrors;
_kwp.ProgressChanged += OnProgress;
}
// ── Properties ────────────────────────────────────────────────────────────
/// <summary>Raw DTC string returned by the ECU, shown in the text box.</summary>
[ObservableProperty] private string _errorText = string.Empty;
/// <summary>True while a K-Line read or clear operation is in progress.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ReadErrorsCommand))]
[NotifyCanExecuteChangedFor(nameof(ClearErrorsCommand))]
private bool _isBusy;
/// <summary>Progress percentage (0100) for the current K-Line operation.</summary>
[ObservableProperty] private int _progressPercent;
/// <summary>Verbose status message for the current K-Line operation.</summary>
[ObservableProperty] private string _progressMessage = string.Empty;
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>Reads the current fault codes from the ECU.</summary>
[RelayCommand(CanExecute = nameof(CanOperate))]
private async Task ReadErrorsAsync()
{
string? port = GetPort();
if (port == null) return;
IsBusy = true;
try
{
ErrorText = await _kwp.ReadFaultCodesAsync(port);
}
finally
{
IsBusy = false;
ProgressPercent = 0;
ProgressMessage = string.Empty;
}
}
/// <summary>Clears fault codes on the ECU, then reads back the updated list.</summary>
[RelayCommand(CanExecute = nameof(CanOperate))]
private async Task ClearErrorsAsync()
{
string? port = GetPort();
if (port == null) return;
IsBusy = true;
try
{
ErrorText = await _kwp.ClearFaultCodesAsync(port);
// Re-read after clearing to confirm
ErrorText = await _kwp.ReadFaultCodesAsync(port);
}
finally
{
IsBusy = false;
ProgressPercent = 0;
ProgressMessage = string.Empty;
}
}
/// <summary>Closes the dialog.</summary>
[RelayCommand]
private void Close()
{
_kwp.ProgressChanged -= OnProgress;
RequestClose?.Invoke();
}
// ── Events ────────────────────────────────────────────────────────────────
/// <summary>Raised when the dialog should close itself.</summary>
public event System.Action? RequestClose;
// ── Helpers ───────────────────────────────────────────────────────────────
private bool CanOperate() => !IsBusy;
private string? GetPort()
{
string? port = _kwp.DetectKLinePort();
if (!string.IsNullOrEmpty(port)) return port;
MessageBox.Show(
"K-Line device not found. Check that the FTDI adapter is connected.",
"K-Line Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return null;
}
private void OnProgress(int pct, string msg)
{
Application.Current.Dispatcher.Invoke(() =>
{
ProgressPercent = pct;
ProgressMessage = msg;
});
}
}
}

View File

@@ -0,0 +1,36 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace HC_APTBS.ViewModels.Dialogs
{
/// <summary>
/// ViewModel for the WProgressDisplay dialog.
///
/// <para>
/// Shows a progress bar (0100) and a verbose text line during long-running
/// K-Line operations. The dialog is closed by the parent when the operation
/// completes; this ViewModel simply exposes observable properties for binding.
/// </para>
/// </summary>
public sealed partial class ProgressViewModel : ObservableObject
{
/// <summary>Title text shown in the dialog's title bar.</summary>
[ObservableProperty] private string _title = string.Empty;
/// <summary>Current progress value (0100).</summary>
[ObservableProperty] private int _progressPercent;
/// <summary>Verbose status message describing the current step.</summary>
[ObservableProperty] private string _verboseMessage = string.Empty;
/// <summary>
/// Updates both <see cref="ProgressPercent"/> and <see cref="VerboseMessage"/>
/// in a single call. Thread-safe: may be called from any thread because
/// the consumer is expected to marshal to the UI thread before calling.
/// </summary>
public void Update(int percent, string message)
{
ProgressPercent = percent;
VerboseMessage = message;
}
}
}

View File

@@ -0,0 +1,158 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels.Dialogs
{
/// <summary>
/// ViewModel for the WReport dialog.
///
/// <para>
/// Lets the operator fill in client and company information before generating
/// the PDF report. The client list is backed by the
/// <see cref="IConfigurationService"/> and persisted when the dialog is accepted.
/// </para>
/// </summary>
public sealed partial class ReportViewModel : ObservableObject
{
// ── Services ──────────────────────────────────────────────────────────────
private readonly IConfigurationService _config;
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>Initialises and populates the dialog from configuration.</summary>
public ReportViewModel(IConfigurationService configService)
{
_config = configService;
CompanyName = _config.Settings.CompanyName;
CompanyInfo = _config.Settings.CompanyInfo;
foreach (var name in _config.Clients.Keys)
ClientNames.Add(name);
}
// ── Company ───────────────────────────────────────────────────────────────
/// <summary>Company name shown in the report header.</summary>
[ObservableProperty] private string _companyName = string.Empty;
/// <summary>Company address / contact info shown in the report header.</summary>
[ObservableProperty] private string _companyInfo = string.Empty;
// ── Client ────────────────────────────────────────────────────────────────
/// <summary>List of known client names for the autocomplete combo box.</summary>
public ObservableCollection<string> ClientNames { get; } = new();
/// <summary>Currently selected or typed client name.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveClientCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteClientCommand))]
private string _selectedClientName = string.Empty;
/// <summary>Free-text notes for the selected client.</summary>
[ObservableProperty] private string _clientInfo = string.Empty;
/// <summary>Operator name.</summary>
[ObservableProperty] private string _operatorName = string.Empty;
/// <summary>Free-text observations appended to the report.</summary>
[ObservableProperty] private string _observations = string.Empty;
// ── Dialog result ─────────────────────────────────────────────────────────
/// <summary>True after the user clicks Accept; false if they cancel.</summary>
public bool Accepted { get; private set; }
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>Saves the current client entry, closes the dialog with Accepted=true.</summary>
[RelayCommand]
private void Accept()
{
SaveCurrentClient();
_config.Settings.CompanyName = CompanyName;
_config.Settings.CompanyInfo = CompanyInfo;
_config.SaveClients();
_config.SaveSettings();
Accepted = true;
RequestClose?.Invoke();
}
/// <summary>Closes the dialog without saving.</summary>
[RelayCommand]
private void Cancel()
{
Accepted = false;
RequestClose?.Invoke();
}
/// <summary>Persists the current client entry and refreshes the client list.</summary>
[RelayCommand(CanExecute = nameof(HasClientName))]
private void SaveClient()
{
SaveCurrentClient();
RefreshClientList();
}
/// <summary>Deletes the currently selected client from the list.</summary>
[RelayCommand(CanExecute = nameof(HasClientName))]
private void DeleteClient()
{
string name = SelectedClientName.Trim();
if (string.IsNullOrEmpty(name)) return;
if (_config.Clients.ContainsKey(name))
_config.Clients.Remove(name);
SelectedClientName = string.Empty;
ClientInfo = string.Empty;
RefreshClientList();
}
/// <summary>
/// Called when the user picks a client name from the combo box.
/// Loads the associated info text.
/// </summary>
public void SelectClient(string name)
{
SelectedClientName = name ?? string.Empty;
if (!string.IsNullOrEmpty(name) && _config.Clients.TryGetValue(name, out var info))
ClientInfo = info;
}
// ── Events ────────────────────────────────────────────────────────────────
/// <summary>Raised when the dialog should close itself.</summary>
public event System.Action? RequestClose;
// ── Helpers ───────────────────────────────────────────────────────────────
private bool HasClientName() => !string.IsNullOrWhiteSpace(SelectedClientName);
private void SaveCurrentClient()
{
string name = SelectedClientName.Trim();
if (string.IsNullOrEmpty(name)) return;
if (_config.Clients.ContainsKey(name))
_config.Clients[name] = ClientInfo;
else
_config.Clients.Add(name, ClientInfo);
}
private void RefreshClientList()
{
ClientNames.Clear();
foreach (var name in _config.Clients.Keys)
ClientNames.Add(name);
}
}
}

View File

@@ -0,0 +1,114 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Represents the vertical graphic result indicator for a single receive parameter
/// within a phase card. Displays expected value, tolerance bounds, and live/final
/// measurement result as a vertical progress bar.
/// </summary>
public sealed partial class GraphicIndicatorViewModel : ObservableObject
{
/// <summary>CAN parameter name (e.g. "QDelivery", "QOver").</summary>
[ObservableProperty] private string _parameterName = string.Empty;
/// <summary>Target/expected measurement value.</summary>
[ObservableProperty] private double _expectedValue;
/// <summary>Acceptable deviation from the expected value.</summary>
[ObservableProperty] private double _tolerance;
/// <summary>
/// Current live measurement value, updated in real-time during the measurement phase.
/// Triggers recalculation of <see cref="ProgressPercent"/> and <see cref="IsWithinTolerance"/>.
/// </summary>
[ObservableProperty] private double _currentValue;
/// <summary>
/// Vertical progress bar fill percentage (0-100), computed from <see cref="CurrentValue"/>
/// relative to the display range that includes tolerance margins.
/// </summary>
[ObservableProperty] private double _progressPercent;
/// <summary>True when <see cref="CurrentValue"/> falls within the tolerance window.</summary>
[ObservableProperty] private bool _isWithinTolerance = true;
/// <summary>True once a measurement has been recorded for this indicator.</summary>
[ObservableProperty] private bool _hasValue;
/// <summary>Lower tolerance bound: <see cref="ExpectedValue"/> - <see cref="Tolerance"/>.</summary>
public double MinBound => ExpectedValue - Tolerance;
/// <summary>Upper tolerance bound: <see cref="ExpectedValue"/> + <see cref="Tolerance"/>.</summary>
public double MaxBound => ExpectedValue + Tolerance;
/// <summary>Formatted display string for the current value.</summary>
public string DisplayValue => HasValue ? CurrentValue.ToString("F1") : "---";
// ── Recalculation on value change ─────────────────────────────────────────
partial void OnCurrentValueChanged(double value)
{
HasValue = true;
RecalculateProgress(value);
IsWithinTolerance = Math.Abs(value - ExpectedValue) <= Tolerance;
OnPropertyChanged(nameof(DisplayValue));
}
partial void OnExpectedValueChanged(double value)
{
OnPropertyChanged(nameof(MinBound));
OnPropertyChanged(nameof(MaxBound));
if (HasValue) RecalculateProgress(CurrentValue);
}
partial void OnToleranceChanged(double value)
{
OnPropertyChanged(nameof(MinBound));
OnPropertyChanged(nameof(MaxBound));
if (HasValue) RecalculateProgress(CurrentValue);
}
/// <summary>
/// Computes the progress bar fill percentage using the same algorithm as the
/// original GraphicResultDisplay. The display range extends 20% beyond the
/// tolerance bounds on each side so that out-of-tolerance readings are still visible.
/// </summary>
private void RecalculateProgress(double value)
{
double range = 2.0 * Tolerance;
if (range <= 0)
{
ProgressPercent = 50;
return;
}
// The tolerance band occupies the middle 60% of the bar.
// Add 20% margin above and below.
double margin = ((100.0 * range / 60.0) - range) / 2.0;
double bottom = ExpectedValue - Tolerance - margin;
double top = ExpectedValue + Tolerance + margin;
double span = top - bottom;
if (span <= 0)
{
ProgressPercent = 50;
return;
}
double pct = (value - bottom) / span * 100.0;
ProgressPercent = Math.Clamp(pct, 5.0, 100.0);
}
/// <summary>Resets the indicator to its initial state for a new test run.</summary>
public void Reset()
{
CurrentValue = 0;
ProgressPercent = 0;
IsWithinTolerance = true;
HasValue = false;
OnPropertyChanged(nameof(DisplayValue));
}
}
}

524
ViewModels/MainViewModel.cs Normal file
View File

@@ -0,0 +1,524 @@
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);
}
}

View File

@@ -0,0 +1,17 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Represents a single named parameter value displayed in a phase card
/// (e.g. RPM, ME, FBKW, or a temperature readiness condition).
/// </summary>
public sealed partial class OperationValueViewModel : ObservableObject
{
/// <summary>Parameter display name (e.g. "RPM", "ME", "FBKW").</summary>
[ObservableProperty] private string _name = string.Empty;
/// <summary>Parameter value in engineering units.</summary>
[ObservableProperty] private double _value;
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Represents a single phase card within a test section.
/// Displays the phase name, enable/disable toggle, operation values,
/// and graphic result indicators.
/// </summary>
public sealed partial class PhaseCardViewModel : ObservableObject
{
// ── Identity ──────────────────────────────────────────────────────────────
/// <summary>Display name of the phase (e.g. "1 - S_001").</summary>
[ObservableProperty] private string _name = string.Empty;
/// <summary>True when failure in this phase halts the entire test sequence.</summary>
[ObservableProperty] private bool _isCritical;
// ── Enable/disable ────────────────────────────────────────────────────────
/// <summary>
/// Whether this phase is included in the test run.
/// Writing this property also updates the underlying <see cref="PhaseDefinition.Enabled"/>
/// and notifies the parent <see cref="TestSectionViewModel"/> to recalculate its
/// <c>AllPhasesChecked</c> state.
/// </summary>
[ObservableProperty] private bool _isEnabled = true;
// ── Execution state ───────────────────────────────────────────────────────
/// <summary>True while this phase is actively executing.</summary>
[ObservableProperty] private bool _isActive;
/// <summary>True when the phase completed and passed all criteria.</summary>
[ObservableProperty] private bool _isPassed;
/// <summary>True when the phase completed but failed one or more criteria.</summary>
[ObservableProperty] private bool _isFailed;
/// <summary>Short result label shown in the card (e.g. "PASS", "FAIL", "-").</summary>
[ObservableProperty] private string _resultText = string.Empty;
// ── Operation values visibility ───────────────────────────────────────────
/// <summary>
/// Controls visibility of the operation values section (RPM, ME, FBKW).
/// Bound from <see cref="TestPanelViewModel.ShowOperationValues"/>.
/// </summary>
[ObservableProperty] private bool _showOperationValues;
// ── Collections ───────────────────────────────────────────────────────────
/// <summary>Send parameters displayed in the card (RPM, ME, FBKW, etc.).</summary>
public ObservableCollection<OperationValueViewModel> OperationValues { get; } = new();
/// <summary>Readiness conditions displayed in the card (temperature, etc.).</summary>
public ObservableCollection<OperationValueViewModel> ReadyValues { get; } = new();
/// <summary>Graphic result indicators, one per receive parameter.</summary>
public ObservableCollection<GraphicIndicatorViewModel> ResultIndicators { get; } = new();
// ── Back-references ───────────────────────────────────────────────────────
/// <summary>Back-reference to the model for writing enabled state changes.</summary>
internal PhaseDefinition? Source { get; set; }
/// <summary>
/// Callback invoked when <see cref="IsEnabled"/> changes, so the parent
/// <see cref="TestSectionViewModel"/> can recalculate <c>AllPhasesChecked</c>.
/// </summary>
internal Action<PhaseCardViewModel>? EnabledChanged { get; set; }
// ── Change handlers ───────────────────────────────────────────────────────
partial void OnIsEnabledChanged(bool value)
{
// Write back to the model.
if (Source != null)
Source.Enabled = value;
ResultText = value ? "\u2013" : "disabled";
// Notify parent.
EnabledChanged?.Invoke(this);
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>Resets execution state for a new test run.</summary>
public void Reset()
{
IsActive = false;
IsPassed = false;
IsFailed = false;
ResultText = IsEnabled ? "\u2013" : "disabled";
foreach (var indicator in ResultIndicators)
indicator.Reset();
}
}
}

View File

@@ -0,0 +1,153 @@
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// ViewModel for the three manual pump control sliders: FBKW (advance), ME (quantity),
/// and PreIn (pre-injection quantity). Each slider change calls
/// <see cref="IBenchService.SetPumpControlValue"/> to update the CAN bus.
/// </summary>
public sealed partial class PumpControlViewModel : ObservableObject
{
private readonly IBenchService _bench;
/// <summary>
/// When true, suppress forwarding slider changes to the bench service.
/// Used during programmatic updates to avoid re-entrant CAN sends.
/// </summary>
private bool _suppressSend;
// ── FBKW (Advance Control) ───────────────────────────────────────────────
/// <summary>Current FBKW slider value.</summary>
[ObservableProperty] private double _fbkwValue;
/// <summary>FBKW slider minimum.</summary>
[ObservableProperty] private double _fbkwMin;
/// <summary>FBKW slider maximum.</summary>
[ObservableProperty] private double _fbkwMax = 100;
/// <summary>FBKW slider tick frequency / step.</summary>
[ObservableProperty] private double _fbkwStep = 10;
// ── ME (Quantity Control) ─────────────────────────────────────────────────
/// <summary>Current ME slider value.</summary>
[ObservableProperty] private double _meValue;
/// <summary>ME slider minimum.</summary>
[ObservableProperty] private double _meMin;
/// <summary>ME slider maximum.</summary>
[ObservableProperty] private double _meMax = 100;
/// <summary>ME slider tick frequency / step.</summary>
[ObservableProperty] private double _meStep = 10;
// ── PreIn (Pre-injection Quantity) ────────────────────────────────────────
/// <summary>Current PreIn slider value.</summary>
[ObservableProperty] private double _preInValue;
/// <summary>PreIn slider minimum.</summary>
[ObservableProperty] private double _preInMin;
/// <summary>PreIn slider maximum.</summary>
[ObservableProperty] private double _preInMax = 100;
/// <summary>PreIn slider tick frequency / step.</summary>
[ObservableProperty] private double _preInStep = 10;
// ── Visibility / enablement ───────────────────────────────────────────────
/// <summary>True when the current pump supports pre-injection.</summary>
[ObservableProperty] private bool _isPreInVisible;
/// <summary>True when a pump is selected and CAN is connected.</summary>
[ObservableProperty] private bool _isEnabled;
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>Creates the ViewModel wired to the bench service for CAN output.</summary>
public PumpControlViewModel(IBenchService bench)
{
_bench = bench;
}
// ── Property-changed callbacks → CAN send ─────────────────────────────────
partial void OnFbkwValueChanged(double value)
{
if (!_suppressSend)
_bench.SetPumpControlValue(PumpParameterNames.Fbkw, value);
}
partial void OnMeValueChanged(double value)
{
if (!_suppressSend)
_bench.SetPumpControlValue(PumpParameterNames.Me, value);
}
partial void OnPreInValueChanged(double value)
{
if (!_suppressSend)
_bench.SetPumpControlValue(PumpParameterNames.PreIn, value);
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Called when the test executor sets a pump parameter value.
/// Auto-expands slider min/max if the value exceeds the current range.
/// Must be called on the UI thread.
/// </summary>
public void SetValueFromTest(string paramName, double value)
{
_suppressSend = true;
try
{
switch (paramName)
{
case PumpParameterNames.Me:
if (value > MeMax) MeMax = value;
else if (value < MeMin) MeMin = value;
MeValue = value;
break;
case PumpParameterNames.Fbkw:
if (value > FbkwMax) FbkwMax = value;
else if (value < FbkwMin) FbkwMin = value;
FbkwValue = value;
break;
case PumpParameterNames.PreIn:
if (value > PreInMax) PreInMax = value;
else if (value < PreInMin) PreInMin = value;
PreInValue = value;
break;
}
}
finally
{
_suppressSend = false;
}
}
/// <summary>Resets all slider values to zero and restores default min/max/step.</summary>
public void Reset()
{
_suppressSend = true;
try
{
FbkwValue = 0; FbkwMin = 0; FbkwMax = 100; FbkwStep = 10;
MeValue = 0; MeMin = 0; MeMax = 100; MeStep = 10;
PreInValue = 0; PreInMin = 0; PreInMax = 100; PreInStep = 10;
}
finally
{
_suppressSend = false;
}
}
}
}

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.ObjectModel;
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>
/// ViewModel for the pump identification user control.
///
/// <para>
/// Owns the pump selector (dropdown), K-Line ECU read command,
/// and all read-only K-Line fields (DFI, serial number, software versions, etc.).
/// After a successful K-Line read the pump is auto-selected from the database
/// using the ECU's pump identifier — matching the old-source behaviour where
/// <c>OnPumpConnectClick</c> would call <c>LoadPump</c> after reading.
/// </para>
/// </summary>
public sealed partial class PumpIdentificationViewModel : ObservableObject
{
// ── Services ──────────────────────────────────────────────────────────────
private readonly IKwpService _kwp;
private readonly IConfigurationService _config;
private readonly IAppLogger _log;
private const string LogId = "PumpIdentVM";
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>Initialises the ViewModel with the required services.</summary>
public PumpIdentificationViewModel(
IKwpService kwpService,
IConfigurationService configService,
IAppLogger logger)
{
_kwp = kwpService;
_config = configService;
_log = logger;
// Wire KWP progress events to local properties.
_kwp.ProgressChanged += (pct, msg) => App.Current.Dispatcher.Invoke(() =>
{
ProgressPercent = pct;
ProgressMessage = msg;
});
}
// ── Pump selection ────────────────────────────────────────────────────────
/// <summary>List of available pump IDs loaded from the pump database.</summary>
public ObservableCollection<string> PumpIds { get; } = new();
/// <summary>Currently selected pump ID in the dropdown.</summary>
[ObservableProperty]
private string? _selectedPumpId;
/// <summary>Currently loaded pump definition.</summary>
[ObservableProperty]
private PumpDefinition? _currentPump;
/// <summary>
/// Raised when <see cref="CurrentPump"/> changes so the parent ViewModel
/// can react (e.g. register CAN parameters, update test display).
/// </summary>
public event Action<PumpDefinition?>? PumpChanged;
/// <summary>Populates the pump ID list from the configuration database.</summary>
public void LoadPumpIds()
{
PumpIds.Clear();
foreach (var id in _config.GetPumpIds())
PumpIds.Add(id);
}
partial void OnSelectedPumpIdChanged(string? value)
{
if (string.IsNullOrEmpty(value)) return;
LoadPump(value);
}
private void LoadPump(string pumpId)
{
var pump = _config.LoadPump(pumpId);
if (pump == null)
{
_log.Warning(LogId, $"Pump {pumpId} not found in database.");
return;
}
CurrentPump = pump;
PumpChanged?.Invoke(pump);
_log.Info(LogId, $"Loaded pump: {pumpId}");
}
// ── K-Line display properties ─────────────────────────────────────────────
/// <summary>K-Line read progress percentage (0100).</summary>
[ObservableProperty] private int _progressPercent;
/// <summary>K-Line read progress status message.</summary>
[ObservableProperty] private string _progressMessage = string.Empty;
/// <summary>True while a K-Line read is in progress.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ReadKlineCommand))]
private bool _isReading;
/// <summary>DFI calibration angle read from ECU EEPROM.</summary>
[ObservableProperty] private string _klineDfi = "-";
/// <summary>Pump identifier string read from ECU ROM.</summary>
[ObservableProperty] private string _klinePumpId = string.Empty;
/// <summary>ECU serial number (EEPROM 0x0080).</summary>
[ObservableProperty] private string _klineSerialNumber = string.Empty;
/// <summary>Model reference from ECU identification text.</summary>
[ObservableProperty] private string _klineModelRef = string.Empty;
/// <summary>Data record from ECU identification text.</summary>
[ObservableProperty] private string _klineDataRecord = string.Empty;
/// <summary>Pump control field from ECU identification text (V2+ pumps).</summary>
[ObservableProperty] private string _klinePumpControl = string.Empty;
/// <summary>Customer change index read from ECU ROM.</summary>
[ObservableProperty] private string _klineModelIndex = string.Empty;
/// <summary>Software version 1 from ECU identification text.</summary>
[ObservableProperty] private string _klineSwVersion1 = string.Empty;
/// <summary>Software version 2 from ECU identification text (V2+ pumps).</summary>
[ObservableProperty] private string _klineSwVersion2 = string.Empty;
/// <summary>Fault code text returned by the ECU.</summary>
[ObservableProperty] private string _klineErrors = string.Empty;
/// <summary>Connection error message (empty when OK, auto-collapsed in the UI).</summary>
[ObservableProperty] private string _klineConnectError = string.Empty;
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>Reads all pump ECU data over K-Line in a background task.</summary>
[RelayCommand(CanExecute = nameof(CanReadKline))]
private async Task ReadKlineAsync()
{
// Always freshly detect the FTDI device — never rely on a stored config value.
// This matches the old source where Config.GetKLinePortName() was called every time.
var port = _kwp.DetectKLinePort();
if (string.IsNullOrEmpty(port))
{
App.Current.Dispatcher.Invoke(() =>
KlineConnectError = "No K-Line device found");
return;
}
// Use the pump's KWP version if one is selected; default to 0 otherwise.
int kwpVersion = CurrentPump?.KwpVersion ?? 0;
IsReading = true;
try
{
var info = await _kwp.ReadAllInfoAsync(port, kwpVersion);
info.TryGetValue(KlineKeys.Dfi, out string? dfi);
info.TryGetValue(KlineKeys.Errors, out string? errors);
info.TryGetValue(KlineKeys.PumpId, out string? pumpId);
info.TryGetValue(KlineKeys.SerialNumber, out string? serial);
info.TryGetValue(KlineKeys.ModelReference, out string? modelRef);
info.TryGetValue(KlineKeys.ModelIndex, out string? modelIndex);
info.TryGetValue(KlineKeys.SwVersion1, out string? sw1);
info.TryGetValue(KlineKeys.SwVersion2, out string? sw2);
info.TryGetValue(KlineKeys.DataRecord, out string? dataRecord);
info.TryGetValue(KlineKeys.PumpControl, out string? pumpControl);
info.TryGetValue(KlineKeys.ConnectError, out string? connectErr);
info.TryGetValue(KlineKeys.Result, out string? result);
App.Current.Dispatcher.Invoke(() =>
{
KlineDfi = dfi ?? "-";
KlineErrors = errors ?? string.Empty;
KlinePumpId = pumpId ?? string.Empty;
KlineSerialNumber = serial ?? string.Empty;
KlineModelRef = modelRef ?? string.Empty;
KlineModelIndex = modelIndex ?? string.Empty;
KlineSwVersion1 = sw1 ?? string.Empty;
KlineSwVersion2 = sw2 ?? string.Empty;
KlineDataRecord = dataRecord ?? string.Empty;
KlinePumpControl = pumpControl ?? string.Empty;
KlineConnectError = connectErr ?? string.Empty;
});
// Auto-select pump from K-Line pump ID — matches old source behaviour
// where OnPumpConnectClick would call LoadPump(pumpId) after reading.
if (result == "1" && !string.IsNullOrEmpty(pumpId))
{
AutoSelectPumpByKlineId(pumpId);
}
// Attach K-Line info to the (now possibly auto-selected) pump.
if (CurrentPump != null)
CurrentPump.KlineInfo = info;
}
finally
{
IsReading = false;
}
}
private bool CanReadKline() => !IsReading;
// ── Helpers ───────────────────────────────────────────────────────────────
/// <summary>
/// Tries to match a K-Line pump identifier to a pump in the database and auto-select it.
/// If the K-Line ID is directly in the pump list, select it. Otherwise, try to find
/// a pump whose ID is contained in the K-Line identifier string.
/// </summary>
private void AutoSelectPumpByKlineId(string klinePumpId)
{
// Direct match — the K-Line ID is itself a pump ID in the database.
if (PumpIds.Contains(klinePumpId))
{
App.Current.Dispatcher.Invoke(() => SelectedPumpId = klinePumpId);
return;
}
// Substring match — the K-Line ident string may contain the pump ID.
foreach (var id in PumpIds)
{
if (klinePumpId.Contains(id, StringComparison.OrdinalIgnoreCase))
{
App.Current.Dispatcher.Invoke(() => SelectedPumpId = id);
return;
}
}
_log.Warning(LogId, $"K-Line pump ID '{klinePumpId}' not found in pump database.");
}
}
}

View File

@@ -0,0 +1,142 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// A single result row shown in the results grid for one receive parameter.
/// </summary>
public sealed partial class ResultRowViewModel : ObservableObject
{
[ObservableProperty] private string _phaseName = string.Empty;
[ObservableProperty] private string _parameterName = string.Empty;
[ObservableProperty] private double _target;
[ObservableProperty] private double _tolerance;
[ObservableProperty] private double _average;
[ObservableProperty] private bool _passed;
/// <summary>"PASS" or "FAIL".</summary>
public string ResultLabel => Passed ? "PASS" : "FAIL";
}
/// <summary>
/// ViewModel for the ResultDisplay user control.
///
/// <para>
/// Shows a flat table of all phase/parameter combinations with their
/// measured average, target, tolerance, and pass/fail result.
/// </para>
/// </summary>
public sealed partial class ResultDisplayViewModel : ObservableObject
{
// ── Properties ────────────────────────────────────────────────────────────
/// <summary>Name of the test whose results are displayed.</summary>
[ObservableProperty] private string _testName = string.Empty;
/// <summary>Overall pass/fail for all displayed results.</summary>
[ObservableProperty] private bool _overallPassed;
/// <summary>All result rows.</summary>
public ObservableCollection<ResultRowViewModel> Results { get; } = new();
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Populates the results table from a completed <see cref="TestDefinition"/>.
/// </summary>
public void LoadResults(TestDefinition test)
{
TestName = test.Name;
Results.Clear();
bool allPassed = true;
foreach (var phase in test.Phases)
{
if (!phase.Enabled || phase.Receives == null) continue;
foreach (var tp in phase.Receives)
{
if (tp.Result == null) continue;
allPassed = allPassed && tp.Result.Passed;
Results.Add(new ResultRowViewModel
{
PhaseName = phase.Name,
ParameterName = tp.Name,
Target = tp.Value,
Tolerance = tp.Tolerance,
Average = tp.Result.Average,
Passed = tp.Result.Passed
});
}
}
OverallPassed = allPassed && Results.Count > 0;
}
/// <summary>Adds or updates a live measurement row during test execution.</summary>
public void UpdateLiveValue(string phaseName, string paramName, double currentValue,
double target, double tolerance)
{
foreach (var row in Results)
{
if (row.PhaseName == phaseName && row.ParameterName == paramName)
{
row.Average = currentValue;
return;
}
}
Results.Add(new ResultRowViewModel
{
PhaseName = phaseName,
ParameterName = paramName,
Target = target,
Tolerance = tolerance,
Average = currentValue
});
}
/// <summary>
/// Populates the results table from all completed tests in the pump's test list.
/// Clears existing results first, then appends rows from every test that has results.
/// </summary>
/// <param name="tests">All test definitions for the current pump.</param>
public void LoadAllResults(IReadOnlyList<TestDefinition> tests)
{
Results.Clear();
bool allPassed = true;
foreach (var test in tests)
{
foreach (var phase in test.Phases)
{
if (!phase.Enabled || phase.Receives == null) continue;
foreach (var tp in phase.Receives)
{
if (tp.Result == null) continue;
allPassed = allPassed && tp.Result.Passed;
Results.Add(new ResultRowViewModel
{
PhaseName = phase.Name,
ParameterName = tp.Name,
Target = tp.Value,
Tolerance = tp.Tolerance,
Average = tp.Result.Average,
Passed = tp.Result.Passed
});
}
}
}
TestName = tests.Count > 0 ? "All Tests" : string.Empty;
OverallPassed = allPassed && Results.Count > 0;
}
/// <summary>Clears all results.</summary>
public void Clear()
{
Results.Clear();
OverallPassed = false;
}
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Collections;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Represents the state of a single bit indicator in the 16-bit pump status word display.
/// </summary>
public sealed class BitIndicatorViewModel : ObservableObject
{
[System.Runtime.CompilerServices.CompilerGenerated]
private string _color = "#26C200";
/// <summary>HTML hex colour for the indicator background (e.g. "#26C200" = green, "#FF1E1E" = red).</summary>
public string Color
{
get => _color;
set => SetProperty(ref _color, value);
}
[System.Runtime.CompilerServices.CompilerGenerated]
private string _description = string.Empty;
/// <summary>Tooltip / label for this bit's current state.</summary>
public string Description
{
get => _description;
set => SetProperty(ref _description, value);
}
[System.Runtime.CompilerServices.CompilerGenerated]
private bool _isActive;
/// <summary>True when this bit is set in the status word.</summary>
public bool IsActive
{
get => _isActive;
set => SetProperty(ref _isActive, value);
}
}
/// <summary>
/// ViewModel for the StatusDisplay user control.
///
/// <para>
/// Maintains a 16-element collection of <see cref="BitIndicatorViewModel"/> objects,
/// one per bit of the pump status word. Call <see cref="UpdateStatusWord"/> whenever
/// a new value arrives from the CAN bus.
/// </para>
/// </summary>
public sealed partial class StatusDisplayViewModel : ObservableObject
{
// ── Properties ────────────────────────────────────────────────────────────
/// <summary>Title shown above the bit display (e.g. "Table 2 Status").</summary>
[ObservableProperty] private string _title = "STATUS";
/// <summary>16 bit indicators for bits 015 of the current status word.</summary>
public ObservableCollection<BitIndicatorViewModel> Bits { get; } = new();
/// <summary>
/// Fired when a bit that is flagged as an error transitions to active.
/// The argument is the bit position (0-based).
/// </summary>
public event Action<int>? ErrorBitDetected;
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>Initialises the collection with 16 green indicator placeholders.</summary>
public StatusDisplayViewModel()
{
for (int i = 0; i < 16; i++)
Bits.Add(new BitIndicatorViewModel { Color = "#26C200", Description = $"Bit {i}" });
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Updates all 16 bit indicators from a <see cref="PumpStatusDefinition"/> and a live value.
/// Must be called on the UI thread.
/// </summary>
/// <param name="statusDefinition">Definition describing what each bit means.</param>
/// <param name="rawValue">Integer value from the pump CAN status parameter.</param>
public void UpdateStatusWord(PumpStatusDefinition statusDefinition, int rawValue)
{
if (statusDefinition == null) return;
Title = $"Table {statusDefinition.Id} {statusDefinition.Name}";
var bits = new BitArray(new[] { rawValue });
foreach (var statusBit in statusDefinition.Bits)
{
int index = statusBit.Bit;
if (index < 0 || index >= 16) continue;
bool isSet = index < bits.Length && bits[index];
var indicator = Bits[index];
indicator.IsActive = isSet;
// Find the matching state definition and apply its colour/description.
foreach (var val in statusBit.Values)
{
int expectedState = isSet ? 1 : 0;
if (val.State == expectedState)
{
indicator.Color = "#" + val.Color;
indicator.Description = val.Description;
break;
}
}
// Notify the bench service / main VM of error bits.
if (isSet && statusBit.Enabled)
ErrorBitDetected?.Invoke(index);
}
}
/// <summary>Resets all indicators to the default green / inactive state.</summary>
public void Reset()
{
for (int i = 0; i < Bits.Count; i++)
{
Bits[i].IsActive = false;
Bits[i].Color = "#26C200";
Bits[i].Description = $"Bit {i}";
}
}
}
}

View File

@@ -0,0 +1,118 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Represents the display state for one phase within the test list UI.
/// Binds to a single row in the test phase grid.
/// </summary>
public sealed partial class PhaseRowViewModel : ObservableObject
{
/// <summary>Display name of the phase.</summary>
[ObservableProperty] private string _name = string.Empty;
/// <summary>True while this phase is actively executing.</summary>
[ObservableProperty] private bool _isActive;
/// <summary>True when the phase has completed and passed all criteria.</summary>
[ObservableProperty] private bool _isPassed;
/// <summary>True when the phase has completed but failed one or more criteria.</summary>
[ObservableProperty] private bool _isFailed;
/// <summary>Whether this phase is enabled for the current test run.</summary>
[ObservableProperty] private bool _isEnabled = true;
/// <summary>Short result string shown in the row (e.g. "PASS", "FAIL", "…").</summary>
[ObservableProperty] private string _resultText = string.Empty;
}
/// <summary>
/// ViewModel for the TestDisplay user control.
///
/// <para>
/// Shows the current test name, the list of phases with their pass/fail status,
/// and the verbose status message from the bench service.
/// </para>
/// </summary>
public sealed partial class TestDisplayViewModel : ObservableObject
{
// ── Properties ────────────────────────────────────────────────────────────
/// <summary>Name of the test currently being executed (e.g. "F", "SVME").</summary>
[ObservableProperty] private string _testName = string.Empty;
/// <summary>Current verbose status line from the bench service.</summary>
[ObservableProperty] private string _statusText = string.Empty;
/// <summary>Estimated total remaining time (seconds) for the entire test sequence.</summary>
[ObservableProperty] private int _remainingSeconds;
/// <summary>True while a test sequence is in progress.</summary>
[ObservableProperty] private bool _isRunning;
/// <summary>Phase rows shown in the phase list.</summary>
public ObservableCollection<PhaseRowViewModel> Phases { get; } = new();
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Populates the phase list from a <see cref="TestDefinition"/>.
/// Call when the active test changes.
/// </summary>
public void LoadTest(TestDefinition test)
{
TestName = test.Name;
Phases.Clear();
foreach (var phase in test.Phases)
{
Phases.Add(new PhaseRowViewModel
{
Name = phase.Name,
IsEnabled = phase.Enabled,
ResultText = phase.Enabled ? "" : "disabled"
});
}
}
/// <summary>
/// Marks a phase as active (running), clearing any previous active state.
/// </summary>
public void SetActivePhase(string phaseName)
{
StatusText = phaseName;
foreach (var row in Phases)
{
row.IsActive = row.Name == phaseName;
}
}
/// <summary>Updates a phase row with the result of a completed phase.</summary>
public void SetPhaseResult(string phaseName, bool passed)
{
foreach (var row in Phases)
{
if (row.Name != phaseName) continue;
row.IsActive = false;
row.IsPassed = passed;
row.IsFailed = !passed;
row.ResultText = passed ? "PASS" : "FAIL";
break;
}
}
/// <summary>Clears all phase result states for a fresh test run.</summary>
public void ResetResults()
{
foreach (var row in Phases)
{
row.IsActive = false;
row.IsPassed = false;
row.IsFailed = false;
row.ResultText = row.IsEnabled ? "" : "disabled";
}
}
}
}

View File

@@ -0,0 +1,212 @@
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Root ViewModel for the test panel that displays all tests for the selected pump.
///
/// <para>
/// Replaces the former <c>TestDisplayViewModel</c>. Holds one
/// <see cref="TestSectionViewModel"/> per <see cref="TestDefinition"/> and provides
/// the public API that <see cref="MainViewModel"/> calls in response to
/// <see cref="Services.IBenchService"/> events.
/// </para>
/// </summary>
public sealed partial class TestPanelViewModel : ObservableObject
{
// ── Cached active phase for fast live-indicator lookup ─────────────────────
private PhaseCardViewModel? _activePhaseCard;
// ── Global toggles ────────────────────────────────────────────────────────
/// <summary>
/// Controls visibility of operation values (RPM, ME, FBKW) on all phase cards.
/// Cascades to every <see cref="PhaseCardViewModel.ShowOperationValues"/>.
/// </summary>
[ObservableProperty] private bool _showOperationValues;
// ── Status ────────────────────────────────────────────────────────────────
/// <summary>Current verbose status message from the bench service.</summary>
[ObservableProperty] private string _statusText = string.Empty;
/// <summary>True while a test sequence is in progress.</summary>
[ObservableProperty] private bool _isRunning;
/// <summary>Estimated remaining time for the entire test sequence (seconds).</summary>
[ObservableProperty] private int _remainingSeconds;
// ── Test sections ─────────────────────────────────────────────────────────
/// <summary>All test sections for the currently loaded pump.</summary>
public ObservableCollection<TestSectionViewModel> Tests { get; } = new();
// ── Show values cascade ───────────────────────────────────────────────────
partial void OnShowOperationValuesChanged(bool value)
{
foreach (var section in Tests)
foreach (var phase in section.Phases)
phase.ShowOperationValues = value;
}
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>
/// Toggles enable/disable for every phase across all test sections.
/// If any phase is currently disabled, enables all; otherwise disables all.
/// </summary>
[RelayCommand]
private void ToggleCheckAll()
{
bool anyDisabled = Tests.Any(s => s.Phases.Any(p => !p.IsEnabled));
bool newState = anyDisabled;
foreach (var section in Tests)
{
// Bypass per-section cascade guard by setting AllPhasesChecked directly,
// which will cascade down to children.
section.AllPhasesChecked = newState;
}
}
// ── Public API: loading ───────────────────────────────────────────────────
/// <summary>
/// Populates the test panel with all tests from the given pump definition.
/// Call when the selected pump changes.
/// </summary>
/// <param name="pump">The pump whose tests to display.</param>
public void LoadAllTests(PumpDefinition pump)
{
Tests.Clear();
_activePhaseCard = null;
StatusText = string.Empty;
RemainingSeconds = 0;
foreach (var testDef in pump.Tests)
{
var section = TestSectionViewModel.FromDefinition(testDef, ShowOperationValues);
Tests.Add(section);
}
// Compute initial remaining seconds estimate.
RemainingSeconds = pump.Tests.Sum(t => t.EstimatedTotalSeconds());
}
// ── Public API: real-time updates from BenchService events ─────────────────
/// <summary>
/// Marks the named phase as actively executing and clears any previous active state.
/// Caches the active phase card for fast live-indicator updates.
/// </summary>
/// <param name="phaseName">Name of the phase that is now running.</param>
public void SetActivePhase(string phaseName)
{
StatusText = phaseName;
_activePhaseCard = null;
foreach (var section in Tests)
{
bool sectionActive = false;
foreach (var phase in section.Phases)
{
if (phase.Name == phaseName && !phase.IsPassed && !phase.IsFailed)
{
phase.IsActive = true;
_activePhaseCard = phase;
sectionActive = true;
}
else
{
phase.IsActive = false;
}
}
section.IsActiveTest = sectionActive;
}
}
/// <summary>
/// Marks a phase as completed with the given pass/fail result.
/// </summary>
/// <param name="phaseName">Name of the completed phase.</param>
/// <param name="passed">True if the phase passed all criteria.</param>
public void SetPhaseResult(string phaseName, bool passed)
{
foreach (var section in Tests)
{
foreach (var phase in section.Phases)
{
if (phase.Name != phaseName || (!phase.IsActive && !phase.IsPassed && !phase.IsFailed))
continue;
// Only update if this is the active phase (avoid overwriting already-completed phases
// with the same name in different tests).
if (!phase.IsActive) continue;
phase.IsActive = false;
phase.IsPassed = passed;
phase.IsFailed = !passed;
phase.ResultText = passed ? "PASS" : "FAIL";
break;
}
// Recalculate section active state.
section.IsActiveTest = section.Phases.Any(p => p.IsActive);
}
if (_activePhaseCard?.Name == phaseName)
_activePhaseCard = null;
}
/// <summary>
/// Updates the live measurement value on the graphic indicator for the currently
/// active phase that has a matching receive parameter.
/// </summary>
/// <param name="paramName">CAN parameter name (e.g. "QDelivery").</param>
/// <param name="value">Current measured value.</param>
public void UpdateLiveIndicator(string paramName, double value)
{
if (_activePhaseCard == null) return;
foreach (var indicator in _activePhaseCard.ResultIndicators)
{
if (indicator.ParameterName == paramName)
{
indicator.CurrentValue = value;
return;
}
}
}
/// <summary>
/// Resets all phase execution states and graphic indicators for a fresh test run.
/// </summary>
public void ResetResults()
{
_activePhaseCard = null;
StatusText = string.Empty;
foreach (var section in Tests)
{
section.IsActiveTest = false;
foreach (var phase in section.Phases)
phase.Reset();
}
// Recalculate remaining seconds.
int total = 0;
foreach (var section in Tests)
{
if (section.Source != null)
total += section.Source.EstimatedTotalSeconds();
}
RemainingSeconds = total;
}
}
}

View File

@@ -0,0 +1,173 @@
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Models;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Represents one test type section (e.g. "F", "DFI", "SVME") containing
/// a header with metadata and a horizontal list of <see cref="PhaseCardViewModel"/> cards.
/// </summary>
public sealed partial class TestSectionViewModel : ObservableObject
{
// ── Suppress cascade guard ────────────────────────────────────────────────
private bool _suppressCascade;
// ── Identity / metadata ───────────────────────────────────────────────────
/// <summary>Test type identifier (e.g. "F", "DFI", "SVME").</summary>
[ObservableProperty] private string _testName = string.Empty;
/// <summary>Human-readable description of the test type.</summary>
[ObservableProperty] private string _description = string.Empty;
/// <summary>Conditioning time in seconds.</summary>
[ObservableProperty] private int _conditioningTimeSec;
/// <summary>Measurement time in seconds.</summary>
[ObservableProperty] private int _measurementTimeSec;
/// <summary>Measurements per second during the measurement window.</summary>
[ObservableProperty] private double _measurementsPerSecond;
// ── UI state ──────────────────────────────────────────────────────────────
/// <summary>Whether this section's expander is open.</summary>
[ObservableProperty] private bool _isExpanded = true;
/// <summary>True when any phase in this test section is currently executing.</summary>
[ObservableProperty] private bool _isActiveTest;
/// <summary>
/// Bidirectional check state for all phases. Setting this cascades down to
/// all child <see cref="PhaseCardViewModel.IsEnabled"/> properties; child changes
/// cascade back up to recalculate this value.
/// </summary>
[ObservableProperty] private bool _allPhasesChecked = true;
// ── Phases ────────────────────────────────────────────────────────────────
/// <summary>Phase cards shown in the horizontal scroll area.</summary>
public ObservableCollection<PhaseCardViewModel> Phases { get; } = new();
// ── Back-reference ────────────────────────────────────────────────────────
/// <summary>Back-reference to the source model.</summary>
internal TestDefinition? Source { get; set; }
// ── Cascade: parent → children ────────────────────────────────────────────
partial void OnAllPhasesCheckedChanged(bool value)
{
if (_suppressCascade) return;
_suppressCascade = true;
try
{
foreach (var phase in Phases)
phase.IsEnabled = value;
}
finally
{
_suppressCascade = false;
}
}
// ── Cascade: child → parent ──────────────────────────────────────────────
/// <summary>
/// Called by a child <see cref="PhaseCardViewModel"/> when its
/// <see cref="PhaseCardViewModel.IsEnabled"/> changes.
/// Recalculates <see cref="AllPhasesChecked"/> without re-cascading.
/// </summary>
internal void OnChildEnabledChanged(PhaseCardViewModel _)
{
if (_suppressCascade) return;
_suppressCascade = true;
try
{
AllPhasesChecked = Phases.All(p => p.IsEnabled);
}
finally
{
_suppressCascade = false;
}
}
// ── Factory ───────────────────────────────────────────────────────────────
/// <summary>
/// Creates a <see cref="TestSectionViewModel"/> from a <see cref="TestDefinition"/> model,
/// populating all child <see cref="PhaseCardViewModel"/> instances.
/// </summary>
/// <param name="test">Source test definition.</param>
/// <param name="showValues">Initial show-operation-values state.</param>
public static TestSectionViewModel FromDefinition(TestDefinition test, bool showValues)
{
var section = new TestSectionViewModel
{
TestName = test.Name,
Description = MapDescription(test.Name),
ConditioningTimeSec = test.ConditioningTimeSec,
MeasurementTimeSec = test.MeasurementTimeSec,
MeasurementsPerSecond = test.MeasurementsPerSecond,
Source = test
};
foreach (var phaseDef in test.Phases)
{
var card = new PhaseCardViewModel
{
Name = phaseDef.Name,
IsCritical = phaseDef.IsCritical,
IsEnabled = phaseDef.Enabled,
ResultText = phaseDef.Enabled ? "\u2013" : "disabled",
ShowOperationValues = showValues,
Source = phaseDef,
EnabledChanged = section.OnChildEnabledChanged
};
// Populate operation values from Sends.
foreach (var tp in phaseDef.Sends)
card.OperationValues.Add(new OperationValueViewModel { Name = tp.Name, Value = tp.Value });
// Populate readiness conditions from Readies.
foreach (var tp in phaseDef.Readies)
card.ReadyValues.Add(new OperationValueViewModel { Name = tp.Name, Value = tp.Value });
// Populate graphic result indicators from Receives.
foreach (var tp in phaseDef.Receives)
{
card.ResultIndicators.Add(new GraphicIndicatorViewModel
{
ParameterName = tp.Name,
ExpectedValue = tp.Value,
Tolerance = tp.Tolerance
});
}
section.Phases.Add(card);
}
section.AllPhasesChecked = section.Phases.All(p => p.IsEnabled);
return section;
}
/// <summary>
/// Maps a test type identifier to a human-readable description.
/// </summary>
private static string MapDescription(string testName) => testName switch
{
TestType.Wl => "Warm-up",
TestType.Dfi => "Adjustment",
TestType.F => "Flow",
TestType.Svme => "Servo valve",
TestType.Up => "Upstroke",
TestType.Pfp => "Pre-injection",
_ => testName
};
}
}