initial commit
This commit is contained in:
130
ViewModels/BenchConfigViewModel.cs
Normal file
130
ViewModels/BenchConfigViewModel.cs
Normal 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 P1–P6.
|
||||
/// 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 α (0–1). 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);
|
||||
}
|
||||
}
|
||||
178
ViewModels/DfiManageViewModel.cs
Normal file
178
ViewModels/DfiManageViewModel.cs
Normal 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 (0–100) 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
137
ViewModels/Dialogs/KlineErrorsViewModel.cs
Normal file
137
ViewModels/Dialogs/KlineErrorsViewModel.cs
Normal 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 (0–100) 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
36
ViewModels/Dialogs/ProgressViewModel.cs
Normal file
36
ViewModels/Dialogs/ProgressViewModel.cs
Normal 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 (0–100) 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 (0–100).</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
ViewModels/Dialogs/ReportViewModel.cs
Normal file
158
ViewModels/Dialogs/ReportViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
ViewModels/GraphicIndicatorViewModel.cs
Normal file
114
ViewModels/GraphicIndicatorViewModel.cs
Normal 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
524
ViewModels/MainViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
17
ViewModels/OperationValueViewModel.cs
Normal file
17
ViewModels/OperationValueViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
105
ViewModels/PhaseCardViewModel.cs
Normal file
105
ViewModels/PhaseCardViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
153
ViewModels/PumpControlViewModel.cs
Normal file
153
ViewModels/PumpControlViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
244
ViewModels/PumpIdentificationViewModel.cs
Normal file
244
ViewModels/PumpIdentificationViewModel.cs
Normal 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 (0–100).</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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
142
ViewModels/ResultDisplayViewModel.cs
Normal file
142
ViewModels/ResultDisplayViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
132
ViewModels/StatusDisplayViewModel.cs
Normal file
132
ViewModels/StatusDisplayViewModel.cs
Normal 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 0–15 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
ViewModels/TestDisplayViewModel.cs
Normal file
118
ViewModels/TestDisplayViewModel.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
212
ViewModels/TestPanelViewModel.cs
Normal file
212
ViewModels/TestPanelViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
173
ViewModels/TestSectionViewModel.cs
Normal file
173
ViewModels/TestSectionViewModel.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user