feat: add Ford VP44 unlock progress dialog, K-Line fast unlock, localization, safety dialogs, and settings
Unlock progress UI:
- UnlockProgressDialog with dark-themed progress ring, phase indicator, elapsed
time, and cancel/close buttons (non-modal, draggable borderless window)
- UnlockProgressViewModel with event-driven progress tracking via IUnlockService
- Triggers on pump selection (manual or K-Line auto-detect), not test start
UnlockService rewrite:
- Persistent CAN senders that outlive the unlock sequence (StopSenders on pump change)
- Concurrent K-Line fast unlock: awaits session Connected, sends RAM timer shortcut
({02 88 02 03 A8 01 00}), verifies via CAN TestUnlock before skipping wait
- Fix Type 1 verification (Value == 0 means unlocked, was inverted)
K-Line fast unlock support:
- IKwpService.TryFastUnlockAsync / KwpService implementation
Additional features:
- ILocalizationService with ES/EN resource dictionaries and runtime switching
- Safety dialogs: VoltageWarning, OilPumpConfirm, RpmSafetyWarning
- SettingsDialog for app configuration
- BenchService enhancements, ConfigurationService improvements, PDF report updates
- All UI strings localized via DynamicResource
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
using HC_APTBS.ViewModels.Dialogs;
|
||||
using HC_APTBS.Views.Dialogs;
|
||||
|
||||
namespace HC_APTBS.ViewModels
|
||||
{
|
||||
@@ -86,6 +88,22 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
partial void OnIsOilPumpOnChanged(bool value)
|
||||
{
|
||||
// Show confirmation dialog when turning oil pump ON (WAcceptOilTurnOn equivalent).
|
||||
if (value)
|
||||
{
|
||||
var vm = new OilPumpConfirmViewModel();
|
||||
var dlg = new OilPumpConfirmDialog(vm) { Owner = Application.Current.MainWindow };
|
||||
dlg.ShowDialog();
|
||||
|
||||
if (!vm.Accepted)
|
||||
{
|
||||
// Revert without re-triggering this handler.
|
||||
_isOilPumpOn = false;
|
||||
OnPropertyChanged(nameof(IsOilPumpOn));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_bench.SetRelay(RelayNames.OilPump, value);
|
||||
}
|
||||
|
||||
@@ -100,22 +118,31 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
/// <summary>
|
||||
/// Starts the bench motor at the RPM specified in <see cref="RpmInputText"/>.
|
||||
/// Warns the operator if the oil pump is off.
|
||||
/// Shows a safety warning dialog if the oil pump is off.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void StartBench()
|
||||
{
|
||||
if (!int.TryParse(RpmInputText, out int rpm) || rpm <= 0) return;
|
||||
|
||||
// Safety warning if oil pump is not running.
|
||||
// Safety warning if oil pump is not running (WCareOnRpmOn equivalent).
|
||||
if (!IsOilPumpOn)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
"Oil pump is OFF. Start bench without oil circulation?",
|
||||
"Oil Pump Warning",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Warning);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
var vm = new RpmSafetyWarningViewModel();
|
||||
var dlg = new RpmSafetyWarningDialog(vm) { Owner = Application.Current.MainWindow };
|
||||
dlg.ShowDialog();
|
||||
|
||||
switch (vm.Result)
|
||||
{
|
||||
case RpmSafetyResult.Cancel:
|
||||
return;
|
||||
case RpmSafetyResult.ProceedWithOil:
|
||||
IsOilPumpOn = true;
|
||||
break;
|
||||
case RpmSafetyResult.ProceedWithoutOil:
|
||||
// Operator accepted the risk.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure direction relays are set.
|
||||
|
||||
@@ -22,15 +22,17 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
private readonly IKwpService _kwp;
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
private const string LogId = "DfiManageViewModel";
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Initialises the ViewModel with the required services.</summary>
|
||||
public DfiManageViewModel(IKwpService kwpService, IConfigurationService configService)
|
||||
public DfiManageViewModel(IKwpService kwpService, IConfigurationService configService, ILocalizationService loc)
|
||||
{
|
||||
_kwp = kwpService;
|
||||
_config = configService;
|
||||
_loc = loc;
|
||||
|
||||
// Update the slider and LCD display in real time when the DFI is
|
||||
// read during a full K-Line read (PumpIdentificationViewModel flow).
|
||||
@@ -106,8 +108,8 @@ namespace HC_APTBS.ViewModels
|
||||
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);
|
||||
MessageBox.Show(_loc.GetString("Error.KLineNotFound"),
|
||||
_loc.GetString("Error.KLineTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,8 +140,8 @@ namespace HC_APTBS.ViewModels
|
||||
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);
|
||||
MessageBox.Show(_loc.GetString("Error.KLineNotFound"),
|
||||
_loc.GetString("Error.KLineTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace HC_APTBS.ViewModels.Dialogs
|
||||
|
||||
private readonly IKwpService _kwp;
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -27,10 +28,12 @@ namespace HC_APTBS.ViewModels.Dialogs
|
||||
public KlineErrorsViewModel(
|
||||
IKwpService kwpService,
|
||||
IConfigurationService configService,
|
||||
ILocalizationService loc,
|
||||
string initialErrors = "")
|
||||
{
|
||||
_kwp = kwpService;
|
||||
_config = configService;
|
||||
_loc = loc;
|
||||
ErrorText = initialErrors;
|
||||
|
||||
_kwp.ProgressChanged += OnProgress;
|
||||
@@ -120,8 +123,8 @@ namespace HC_APTBS.ViewModels.Dialogs
|
||||
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);
|
||||
_loc.GetString("Error.KLineNotFound"),
|
||||
_loc.GetString("Error.KLineTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
51
ViewModels/Dialogs/OilPumpConfirmViewModel.cs
Normal file
51
ViewModels/Dialogs/OilPumpConfirmViewModel.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the oil pump confirmation dialog shown before activating
|
||||
/// the oil pump relay. The operator must confirm that oil level and
|
||||
/// connections have been checked.
|
||||
/// Equivalent to the old <c>WAcceptOilTurnOn</c> dialog.
|
||||
/// </summary>
|
||||
public sealed partial class OilPumpConfirmViewModel : ObservableObject
|
||||
{
|
||||
// ── Dialog result ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True if the operator confirmed and accepted.</summary>
|
||||
public bool Accepted { get; private set; }
|
||||
|
||||
// ── Checkbox ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True when the "I have checked for leaks" checkbox is ticked.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
|
||||
private bool _leaksChecked;
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Confirms the oil pump activation and closes the dialog.</summary>
|
||||
[RelayCommand(CanExecute = nameof(CanAccept))]
|
||||
private void Accept()
|
||||
{
|
||||
Accepted = true;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
private bool CanAccept() => LeaksChecked;
|
||||
|
||||
/// <summary>Cancels the oil pump activation and closes the dialog.</summary>
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Accepted = false;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Raised when the dialog should close itself.</summary>
|
||||
public event System.Action? RequestClose;
|
||||
}
|
||||
}
|
||||
72
ViewModels/Dialogs/RpmSafetyWarningViewModel.cs
Normal file
72
ViewModels/Dialogs/RpmSafetyWarningViewModel.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of the RPM safety warning dialog.
|
||||
/// </summary>
|
||||
public enum RpmSafetyResult
|
||||
{
|
||||
/// <summary>User cancelled — do not start the motor.</summary>
|
||||
Cancel,
|
||||
|
||||
/// <summary>Turn on oil pump first, then start the motor.</summary>
|
||||
ProceedWithOil,
|
||||
|
||||
/// <summary>Proceed without oil pump (operator acknowledges risk).</summary>
|
||||
ProceedWithoutOil
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the RPM safety warning dialog shown when the operator
|
||||
/// starts the bench motor while the oil pump is OFF.
|
||||
/// Equivalent to the old <c>WCareOnRpmOn</c> dialog.
|
||||
/// </summary>
|
||||
public sealed partial class RpmSafetyWarningViewModel : ObservableObject
|
||||
{
|
||||
// ── Dialog result ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>The operator's chosen action.</summary>
|
||||
public RpmSafetyResult Result { get; private set; } = RpmSafetyResult.Cancel;
|
||||
|
||||
// ── Radio button selection ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True when the "turn on oil pump and proceed" option is selected.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
|
||||
private bool _isOilAndProceedSelected;
|
||||
|
||||
/// <summary>True when the "proceed without oil" option is selected.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
|
||||
private bool _isProceedWithoutOilSelected;
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Accepts the selected option and closes the dialog.</summary>
|
||||
[RelayCommand(CanExecute = nameof(CanAccept))]
|
||||
private void Accept()
|
||||
{
|
||||
Result = IsOilAndProceedSelected
|
||||
? RpmSafetyResult.ProceedWithOil
|
||||
: RpmSafetyResult.ProceedWithoutOil;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
private bool CanAccept() => IsOilAndProceedSelected || IsProceedWithoutOilSelected;
|
||||
|
||||
/// <summary>Cancels and closes the dialog.</summary>
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Result = RpmSafetyResult.Cancel;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Raised when the dialog should close itself.</summary>
|
||||
public event System.Action? RequestClose;
|
||||
}
|
||||
}
|
||||
290
ViewModels/Dialogs/SettingsViewModel.cs
Normal file
290
ViewModels/Dialogs/SettingsViewModel.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HC_APTBS.Infrastructure.Kwp;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the application settings dialog.
|
||||
/// Loads a local copy of every <see cref="AppSettings"/> property so that
|
||||
/// Cancel discards all changes.
|
||||
/// </summary>
|
||||
public sealed partial class SettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
// ── Dialog result ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True when the user clicked Accept.</summary>
|
||||
public bool Accepted { get; private set; }
|
||||
|
||||
/// <summary>Raised to close the owning dialog window.</summary>
|
||||
public event Action? RequestClose;
|
||||
|
||||
// ── Collections ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Available language codes for the language dropdown.</summary>
|
||||
public ObservableCollection<string> AvailableLanguages { get; } = new() { "ESP", "ENG" };
|
||||
|
||||
/// <summary>RPM-voltage lookup table, editable via DataGrid.</summary>
|
||||
public ObservableCollection<RpmVoltageRelation> Relations { get; } = new();
|
||||
|
||||
/// <summary>Available FTDI device serial numbers for K-Line port selection.</summary>
|
||||
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||
|
||||
// ── General ───────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _selectedLanguage = "ESP";
|
||||
[ObservableProperty] private int _daysKeepLogs = 7;
|
||||
|
||||
// ── Safety ────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _tempMax = 45;
|
||||
[ObservableProperty] private int _tempMin = 35;
|
||||
[ObservableProperty] private int _securityRpmLimit = 2500;
|
||||
[ObservableProperty] private int _maxPressureBar = 26;
|
||||
[ObservableProperty] private double _toleranceUpExtension = 0.08;
|
||||
[ObservableProperty] private double _tolerancePfpExtension = 0.1;
|
||||
[ObservableProperty] private bool _defaultIgnoreTin = true;
|
||||
|
||||
// ── PID ───────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private double _pidP = 0.1;
|
||||
[ObservableProperty] private double _pidI = 0.1;
|
||||
[ObservableProperty] private double _pidD = 0.04;
|
||||
[ObservableProperty] private int _pidLoopMs = 250;
|
||||
|
||||
// ── Motor ─────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _encoderResolution = 4096;
|
||||
[ObservableProperty] private double _voltageForMaxRpm = 10;
|
||||
[ObservableProperty] private int _maxRpm = 2500;
|
||||
[ObservableProperty] private bool _rightRelayValue = true;
|
||||
|
||||
// ── Company ───────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _companyName = string.Empty;
|
||||
[ObservableProperty] private string _companyInfo = string.Empty;
|
||||
[ObservableProperty] private string _reportLogoPath = string.Empty;
|
||||
|
||||
// ── K-Line ────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _selectedKLinePort = string.Empty;
|
||||
|
||||
// ── Advanced (refresh intervals) ──────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _refreshBenchInterfaceMs = 20;
|
||||
[ObservableProperty] private int _refreshWhileReadingMs = 1500;
|
||||
[ObservableProperty] private int _refreshCanBusReadMs = 2;
|
||||
[ObservableProperty] private int _refreshPumpRequestMs = 250;
|
||||
[ObservableProperty] private int _refreshPumpParamsMs = 4;
|
||||
[ObservableProperty] private int _blinkIntervalMs = 1000;
|
||||
[ObservableProperty] private int _flasherIntervalMs = 800;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────
|
||||
|
||||
/// <param name="configService">Configuration service for loading/saving settings.</param>
|
||||
/// <param name="localizationService">Localization service for language switching.</param>
|
||||
public SettingsViewModel(IConfigurationService configService, ILocalizationService localizationService)
|
||||
{
|
||||
_config = configService;
|
||||
_loc = localizationService;
|
||||
|
||||
var s = configService.Settings;
|
||||
|
||||
// General
|
||||
_selectedLanguage = s.Language;
|
||||
_daysKeepLogs = s.DaysKeepLogs;
|
||||
|
||||
// Safety
|
||||
_tempMax = s.TempMax;
|
||||
_tempMin = s.TempMin;
|
||||
_securityRpmLimit = s.SecurityRpmLimit;
|
||||
_maxPressureBar = s.MaxPressureBar;
|
||||
_toleranceUpExtension = s.ToleranceUpExtension;
|
||||
_tolerancePfpExtension = s.TolerancePfpExtension;
|
||||
_defaultIgnoreTin = s.DefaultIgnoreTin;
|
||||
|
||||
// PID
|
||||
_pidP = s.PidP;
|
||||
_pidI = s.PidI;
|
||||
_pidD = s.PidD;
|
||||
_pidLoopMs = s.PidLoopMs;
|
||||
|
||||
// Motor
|
||||
_encoderResolution = s.EncoderResolution;
|
||||
_voltageForMaxRpm = s.VoltageForMaxRpm;
|
||||
_maxRpm = s.MaxRpm;
|
||||
_rightRelayValue = s.RightRelayValue;
|
||||
|
||||
// Company
|
||||
_companyName = s.CompanyName;
|
||||
_companyInfo = s.CompanyInfo;
|
||||
_reportLogoPath = s.ReportLogoPath;
|
||||
|
||||
// K-Line
|
||||
_selectedKLinePort = s.KLinePort;
|
||||
|
||||
// Advanced
|
||||
_refreshBenchInterfaceMs = s.RefreshBenchInterfaceMs;
|
||||
_refreshWhileReadingMs = s.RefreshWhileReadingMs;
|
||||
_refreshCanBusReadMs = s.RefreshCanBusReadMs;
|
||||
_refreshPumpRequestMs = s.RefreshPumpRequestMs;
|
||||
_refreshPumpParamsMs = s.RefreshPumpParamsMs;
|
||||
_blinkIntervalMs = s.BlinkIntervalMs;
|
||||
_flasherIntervalMs = s.FlasherIntervalMs;
|
||||
|
||||
// Deep-copy the RPM-voltage relation table
|
||||
foreach (var r in s.Relations)
|
||||
Relations.Add(new RpmVoltageRelation(r.Voltage, r.Rpm));
|
||||
|
||||
// Enumerate connected FTDI devices
|
||||
EnumerateFtdiDevices();
|
||||
}
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Copies all local values back to AppSettings, saves, and closes.</summary>
|
||||
[RelayCommand]
|
||||
private void Accept()
|
||||
{
|
||||
var s = _config.Settings;
|
||||
|
||||
// General
|
||||
s.DaysKeepLogs = DaysKeepLogs;
|
||||
|
||||
// Safety
|
||||
s.TempMax = TempMax;
|
||||
s.TempMin = TempMin;
|
||||
s.SecurityRpmLimit = SecurityRpmLimit;
|
||||
s.MaxPressureBar = MaxPressureBar;
|
||||
s.ToleranceUpExtension = ToleranceUpExtension;
|
||||
s.TolerancePfpExtension = TolerancePfpExtension;
|
||||
s.DefaultIgnoreTin = DefaultIgnoreTin;
|
||||
|
||||
// PID
|
||||
s.PidP = PidP;
|
||||
s.PidI = PidI;
|
||||
s.PidD = PidD;
|
||||
s.PidLoopMs = PidLoopMs;
|
||||
|
||||
// Motor
|
||||
s.EncoderResolution = EncoderResolution;
|
||||
s.VoltageForMaxRpm = VoltageForMaxRpm;
|
||||
s.MaxRpm = MaxRpm;
|
||||
s.RightRelayValue = RightRelayValue;
|
||||
s.Relations = Relations.Select(r => new RpmVoltageRelation(r.Voltage, r.Rpm)).ToList();
|
||||
|
||||
// Company
|
||||
s.CompanyName = CompanyName;
|
||||
s.CompanyInfo = CompanyInfo;
|
||||
s.ReportLogoPath = ReportLogoPath;
|
||||
|
||||
// K-Line
|
||||
s.KLinePort = SelectedKLinePort;
|
||||
|
||||
// Advanced
|
||||
s.RefreshBenchInterfaceMs = RefreshBenchInterfaceMs;
|
||||
s.RefreshWhileReadingMs = RefreshWhileReadingMs;
|
||||
s.RefreshCanBusReadMs = RefreshCanBusReadMs;
|
||||
s.RefreshPumpRequestMs = RefreshPumpRequestMs;
|
||||
s.RefreshPumpParamsMs = RefreshPumpParamsMs;
|
||||
s.BlinkIntervalMs = BlinkIntervalMs;
|
||||
s.FlasherIntervalMs = FlasherIntervalMs;
|
||||
|
||||
// Language — switch if changed (also persists via LocalizationService)
|
||||
if (SelectedLanguage != _loc.CurrentLanguage)
|
||||
_loc.SetLanguage(SelectedLanguage);
|
||||
|
||||
_config.SaveSettings();
|
||||
|
||||
Accepted = true;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Discards changes and closes.</summary>
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Accepted = false;
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Opens a file dialog to select a company logo image.</summary>
|
||||
[RelayCommand]
|
||||
private void BrowseLogo()
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = _loc.GetString("Dialog.Settings.BrowseLogoTitle"),
|
||||
Filter = "Image files|*.png;*.jpg;*.jpeg;*.bmp|All files|*.*"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(ReportLogoPath))
|
||||
{
|
||||
try { dlg.InitialDirectory = System.IO.Path.GetDirectoryName(ReportLogoPath); }
|
||||
catch { /* ignore invalid path */ }
|
||||
}
|
||||
|
||||
if (dlg.ShowDialog() == true)
|
||||
ReportLogoPath = dlg.FileName;
|
||||
}
|
||||
|
||||
/// <summary>Re-enumerates connected FTDI devices into <see cref="AvailablePorts"/>.</summary>
|
||||
[RelayCommand]
|
||||
private void RefreshPorts()
|
||||
{
|
||||
EnumerateFtdiDevices();
|
||||
}
|
||||
|
||||
/// <summary>Appends a new empty row to the RPM-voltage relation table.</summary>
|
||||
[RelayCommand]
|
||||
private void AddRelation()
|
||||
{
|
||||
Relations.Add(new RpmVoltageRelation(0.0, 0));
|
||||
}
|
||||
|
||||
/// <summary>Removes the selected row from the RPM-voltage relation table.</summary>
|
||||
[RelayCommand]
|
||||
private void RemoveRelation(RpmVoltageRelation? relation)
|
||||
{
|
||||
if (relation != null)
|
||||
Relations.Remove(relation);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Populates <see cref="AvailablePorts"/> with serial numbers of connected
|
||||
/// FTDI devices. Fails silently if the FTDI driver DLL is not present.
|
||||
/// </summary>
|
||||
private void EnumerateFtdiDevices()
|
||||
{
|
||||
AvailablePorts.Clear();
|
||||
try
|
||||
{
|
||||
uint count = FtdiInterface.GetDevicesCount();
|
||||
if (count == 0) return;
|
||||
|
||||
var list = new FT_DEVICE_INFO_NODE[count];
|
||||
FtdiInterface.GetDeviceList(list);
|
||||
|
||||
foreach (var device in list)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(device.SerialNumber))
|
||||
AvailablePorts.Add(device.SerialNumber);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// FTDI DLL not loaded or no devices — leave list empty.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
ViewModels/Dialogs/UnlockProgressViewModel.cs
Normal file
152
ViewModels/Dialogs/UnlockProgressViewModel.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the Ford VP44 immobilizer unlock progress dialog.
|
||||
/// Tracks Phase 1 (CAN flood ~600 s), Phase 2 (handshake ~4 s), and verification.
|
||||
/// Equivalent to the old <c>WUnlocker</c> window.
|
||||
/// </summary>
|
||||
public sealed partial class UnlockProgressViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IUnlockService _unlockService;
|
||||
private readonly ILocalizationService _loc;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
|
||||
/// <summary>Regex to extract percentage and elapsed time from Phase 1 status messages.</summary>
|
||||
private static readonly Regex ProgressRegex =
|
||||
new(@"Unlocking\.\.\. (\d+)% \((\d{2}:\d{2})\)", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>Creates the ViewModel and subscribes to unlock service events.</summary>
|
||||
/// <param name="unlockService">The unlock service to monitor.</param>
|
||||
/// <param name="unlockType">Pump unlock type (1 or 2).</param>
|
||||
/// <param name="cts">Cancellation token source to cancel the unlock.</param>
|
||||
public UnlockProgressViewModel(IUnlockService unlockService, int unlockType, CancellationTokenSource cts, ILocalizationService loc)
|
||||
{
|
||||
_unlockService = unlockService;
|
||||
_loc = loc;
|
||||
_cts = cts;
|
||||
_unlockTypeLabel = string.Format(_loc.GetString("Dialog.Unlock.TypeLabel"), unlockType);
|
||||
_phaseText = _loc.GetString("Dialog.Unlock.Phase1");
|
||||
_elapsedTime = "00:00";
|
||||
_isCancellable = true;
|
||||
|
||||
_unlockService.StatusChanged += OnStatusChanged;
|
||||
_unlockService.UnlockCompleted += OnUnlockCompleted;
|
||||
}
|
||||
|
||||
// ── Observable properties ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Progress percentage (0–100).</summary>
|
||||
[ObservableProperty] private int _progress;
|
||||
|
||||
/// <summary>Elapsed time formatted as MM:SS.</summary>
|
||||
[ObservableProperty] private string _elapsedTime;
|
||||
|
||||
/// <summary>Current phase description.</summary>
|
||||
[ObservableProperty] private string _phaseText;
|
||||
|
||||
/// <summary>Result text shown after completion.</summary>
|
||||
[ObservableProperty] private string _resultText = string.Empty;
|
||||
|
||||
/// <summary>Label for unlock type (e.g. "Type 1").</summary>
|
||||
[ObservableProperty] private string _unlockTypeLabel;
|
||||
|
||||
/// <summary>True when the unlock sequence has finished (success, failure, or cancelled).</summary>
|
||||
[NotifyCanExecuteChangedFor(nameof(CloseCommand))]
|
||||
[ObservableProperty] private bool _isComplete;
|
||||
|
||||
/// <summary>True while cancellation is allowed (Phase 1 only).</summary>
|
||||
[NotifyCanExecuteChangedFor(nameof(CancelCommand))]
|
||||
[ObservableProperty] private bool _isCancellable;
|
||||
|
||||
/// <summary>Tri-state result: null = in progress, true = success, false = failure.</summary>
|
||||
[ObservableProperty] private bool? _isSuccess;
|
||||
|
||||
// ── Commands ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Cancels the unlock sequence (only available during Phase 1).</summary>
|
||||
[RelayCommand(CanExecute = nameof(IsCancellable))]
|
||||
private void Cancel()
|
||||
{
|
||||
_cts.Cancel();
|
||||
IsCancellable = false;
|
||||
IsComplete = true;
|
||||
IsSuccess = false;
|
||||
ResultText = _loc.GetString("Dialog.Unlock.Cancelled");
|
||||
}
|
||||
|
||||
/// <summary>Closes the dialog (only available after completion).</summary>
|
||||
[RelayCommand(CanExecute = nameof(IsComplete))]
|
||||
private void Close()
|
||||
{
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Raised when the dialog should close itself.</summary>
|
||||
public event Action? RequestClose;
|
||||
|
||||
// ── Service event handlers ───────────────────────────────────────────────
|
||||
|
||||
private void OnStatusChanged(string msg)
|
||||
{
|
||||
Application.Current?.Dispatcher?.Invoke(() =>
|
||||
{
|
||||
var match = ProgressRegex.Match(msg);
|
||||
if (match.Success)
|
||||
{
|
||||
Progress = int.Parse(match.Groups[1].Value);
|
||||
ElapsedTime = match.Groups[2].Value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg == "Fast unlock attempt...")
|
||||
{
|
||||
PhaseText = _loc.GetString("Dialog.Unlock.FastAttempt");
|
||||
}
|
||||
else if (msg == "Unlocking...")
|
||||
{
|
||||
PhaseText = _loc.GetString("Dialog.Unlock.Phase1");
|
||||
}
|
||||
else if (msg == "Testing unlock...")
|
||||
{
|
||||
PhaseText = _loc.GetString("Dialog.Unlock.Phase2Testing");
|
||||
IsCancellable = false;
|
||||
Progress = 100;
|
||||
}
|
||||
else if (msg == "Sending...")
|
||||
{
|
||||
PhaseText = _loc.GetString("Dialog.Unlock.Phase2Sending");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUnlockCompleted(bool success)
|
||||
{
|
||||
Application.Current?.Dispatcher?.Invoke(() =>
|
||||
{
|
||||
IsComplete = true;
|
||||
IsCancellable = false;
|
||||
IsSuccess = success;
|
||||
ResultText = success ? _loc.GetString("Dialog.Unlock.Unlocked") : _loc.GetString("Dialog.Unlock.Failed");
|
||||
});
|
||||
}
|
||||
|
||||
// ── IDisposable ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Unsubscribes from service events to prevent leaks.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_unlockService.StatusChanged -= OnStatusChanged;
|
||||
_unlockService.UnlockCompleted -= OnUnlockCompleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,13 @@ namespace HC_APTBS.ViewModels.Dialogs
|
||||
public sealed partial class UserCheckViewModel : ObservableObject
|
||||
{
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Initialises the dialog, optionally pre-filling the last used username.</summary>
|
||||
public UserCheckViewModel(IConfigurationService config, string lastUsername = "")
|
||||
public UserCheckViewModel(IConfigurationService config, ILocalizationService loc, string lastUsername = "")
|
||||
{
|
||||
_config = config;
|
||||
_loc = loc;
|
||||
_username = lastUsername;
|
||||
}
|
||||
|
||||
@@ -55,8 +57,8 @@ namespace HC_APTBS.ViewModels.Dialogs
|
||||
else
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Invalid username or password.\n(Both are case-sensitive.)",
|
||||
"Authentication Error",
|
||||
_loc.GetString("Error.AuthInvalid"),
|
||||
_loc.GetString("Error.AuthTitle"),
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Stop);
|
||||
}
|
||||
|
||||
41
ViewModels/Dialogs/VoltageWarningViewModel.cs
Normal file
41
ViewModels/Dialogs/VoltageWarningViewModel.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Dialogs
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the voltage warning dialog shown when a pump requiring
|
||||
/// a specific supply voltage (27 V or 13.5 V) is selected.
|
||||
/// Equivalent to the old <c>WAlert27v</c> dialog.
|
||||
/// </summary>
|
||||
public sealed partial class VoltageWarningViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the voltage warning ViewModel for the specified voltage.
|
||||
/// </summary>
|
||||
/// <param name="voltage">Voltage string to display (e.g. "27 V" or "13.5 V").</param>
|
||||
public VoltageWarningViewModel(string voltage)
|
||||
{
|
||||
Voltage = voltage;
|
||||
}
|
||||
|
||||
// ── Display properties ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>The required voltage string shown in the dialog (e.g. "27 V").</summary>
|
||||
[ObservableProperty] private string _voltage = string.Empty;
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Acknowledges the warning and closes the dialog.</summary>
|
||||
[RelayCommand]
|
||||
private void Acknowledge()
|
||||
{
|
||||
RequestClose?.Invoke();
|
||||
}
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Raised when the dialog should close itself.</summary>
|
||||
public event System.Action? RequestClose;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ namespace HC_APTBS.ViewModels
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly IPdfService _pdf;
|
||||
private readonly IUnlockService _unlock;
|
||||
private readonly ILocalizationService _loc;
|
||||
private readonly IAppLogger _log;
|
||||
private const string LogId = "MainViewModel";
|
||||
|
||||
@@ -46,9 +47,23 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
private CancellationTokenSource? _testCts;
|
||||
|
||||
// ── Unlock tracking ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>CTS for the currently running immobilizer unlock, if any.</summary>
|
||||
private CancellationTokenSource? _unlockCts;
|
||||
|
||||
/// <summary>ViewModel for the non-modal unlock progress window.</summary>
|
||||
private UnlockProgressViewModel? _unlockVm;
|
||||
|
||||
/// <summary>The non-modal unlock progress window, if open.</summary>
|
||||
private UnlockProgressDialog? _unlockDlg;
|
||||
|
||||
/// <summary>Remembers the last authenticated username to pre-fill the next auth dialog.</summary>
|
||||
private string _lastAuthenticatedUser = string.Empty;
|
||||
|
||||
/// <summary>Tracks whether the last selected pump required 27 V, for transition-based voltage warnings.</summary>
|
||||
private bool _lastPumpWas27V;
|
||||
|
||||
// ── Child ViewModels ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>ViewModel for pump selection and K-Line ECU identification.</summary>
|
||||
@@ -58,10 +73,10 @@ namespace HC_APTBS.ViewModels
|
||||
public DfiManageViewModel DfiViewModel { get; }
|
||||
|
||||
/// <summary>ViewModel for the test panel showing all test sections and phase cards.</summary>
|
||||
public TestPanelViewModel TestPanel { get; } = new();
|
||||
public TestPanelViewModel TestPanel { get; }
|
||||
|
||||
/// <summary>ViewModel for the measurement results table.</summary>
|
||||
public ResultDisplayViewModel ResultDisplay { get; } = new();
|
||||
public ResultDisplayViewModel ResultDisplay { get; }
|
||||
|
||||
/// <summary>ViewModel for the manual pump control sliders (FBKW, ME, PreIn).</summary>
|
||||
public PumpControlViewModel PumpControl { get; private set; } = null!;
|
||||
@@ -94,6 +109,7 @@ namespace HC_APTBS.ViewModels
|
||||
IConfigurationService configService,
|
||||
IPdfService pdfService,
|
||||
IUnlockService unlockService,
|
||||
ILocalizationService localizationService,
|
||||
IAppLogger logger)
|
||||
{
|
||||
_can = canService;
|
||||
@@ -102,10 +118,15 @@ namespace HC_APTBS.ViewModels
|
||||
_config = configService;
|
||||
_pdf = pdfService;
|
||||
_unlock = unlockService;
|
||||
_loc = localizationService;
|
||||
_log = logger;
|
||||
|
||||
PumpIdentification = new PumpIdentificationViewModel(kwpService, configService, logger);
|
||||
DfiViewModel = new DfiManageViewModel(kwpService, configService);
|
||||
_loc.LanguageChanged += RefreshLocalisedStrings;
|
||||
|
||||
TestPanel = new TestPanelViewModel(localizationService);
|
||||
ResultDisplay = new ResultDisplayViewModel(localizationService);
|
||||
PumpIdentification = new PumpIdentificationViewModel(kwpService, configService, localizationService, logger);
|
||||
DfiViewModel = new DfiManageViewModel(kwpService, configService, localizationService);
|
||||
PumpControl = new PumpControlViewModel(benchService);
|
||||
BenchControl = new BenchControlViewModel(benchService, configService);
|
||||
AngleDisplay = new AngleDisplayViewModel(configService);
|
||||
@@ -159,6 +180,18 @@ namespace HC_APTBS.ViewModels
|
||||
FlowmeterChart.SetTolerance(paramName, value, tolerance);
|
||||
});
|
||||
|
||||
_bench.MeasurementSampled += (name, value) => App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (name == BenchParameterNames.QDelivery)
|
||||
FlowmeterChart.Delivery.AddValue(value);
|
||||
else if (name == BenchParameterNames.QOver)
|
||||
FlowmeterChart.Over.AddValue(value);
|
||||
});
|
||||
_bench.EmergencyStopTriggered += reason => App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
VerboseStatus = string.Format(_loc.GetString("Error.EmergencyStop"), reason);
|
||||
});
|
||||
|
||||
// Angle display: lock angle and PSG zero from test phases
|
||||
_bench.LockAngleFaseReady += () => App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
@@ -226,12 +259,73 @@ namespace HC_APTBS.ViewModels
|
||||
// Notify commands that depend on pump availability.
|
||||
StartTestCommand.NotifyCanExecuteChanged();
|
||||
GenerateReportCommand.NotifyCanExecuteChanged();
|
||||
|
||||
// Show voltage warning on 27V ↔ 13.5V transitions (WAlert27v equivalent).
|
||||
CheckVoltageWarning(pump);
|
||||
|
||||
// Start immobilizer unlock if this pump requires it (Ford VP44).
|
||||
StartUnlockIfRequired(pump);
|
||||
}
|
||||
|
||||
// ── Immobilizer unlock ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Starts the immobilizer unlock sequence in a non-modal window if the pump
|
||||
/// requires it (UnlockType != 0). Cancels any previously running unlock first.
|
||||
/// </summary>
|
||||
private void StartUnlockIfRequired(PumpDefinition pump)
|
||||
{
|
||||
// Cancel and close any previous unlock window.
|
||||
CloseUnlockDialog();
|
||||
|
||||
if (pump.UnlockType == 0) return;
|
||||
|
||||
_unlockCts = new CancellationTokenSource();
|
||||
_unlockVm = new UnlockProgressViewModel(_unlock, pump.UnlockType, _unlockCts, _loc);
|
||||
_unlockDlg = new UnlockProgressDialog(_unlockVm)
|
||||
{ Owner = Application.Current.MainWindow };
|
||||
|
||||
// Start unlock in background — ViewModel tracks via event subscriptions.
|
||||
var unlockTask = _unlock.UnlockAsync(pump, _unlockCts.Token);
|
||||
_ = unlockTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
_unlockDlg.Show(); // Non-modal — user can continue working.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any running unlock, stops persistent CAN senders, closes the
|
||||
/// window, and disposes resources. Safe to call when no unlock is active.
|
||||
/// </summary>
|
||||
private void CloseUnlockDialog()
|
||||
{
|
||||
// Stop the persistent CAN unlock senders (prevents re-lock until
|
||||
// this point — only called when the pump is deselected).
|
||||
_unlock.StopSenders();
|
||||
|
||||
if (_unlockCts != null)
|
||||
{
|
||||
_unlockCts.Cancel();
|
||||
_unlockCts.Dispose();
|
||||
_unlockCts = null;
|
||||
}
|
||||
|
||||
if (_unlockVm != null)
|
||||
{
|
||||
_unlockVm.Dispose();
|
||||
_unlockVm = null;
|
||||
}
|
||||
|
||||
if (_unlockDlg != null)
|
||||
{
|
||||
_unlockDlg.ForceClose();
|
||||
_unlockDlg = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── CAN connection ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>CAN bus status display text.</summary>
|
||||
[ObservableProperty] private string _canStatusText = "Disconnected";
|
||||
[ObservableProperty] private string _canStatusText = string.Empty;
|
||||
|
||||
/// <summary>True when the CAN bus adapter is connected.</summary>
|
||||
[ObservableProperty] private bool _isCanConnected;
|
||||
@@ -243,7 +337,7 @@ namespace HC_APTBS.ViewModels
|
||||
_can.SetParameters(_config.Bench.ParametersById);
|
||||
_can.RegisterBenchMessageIds(GetReceiveMessageIds(_config.Bench.ParametersById));
|
||||
bool ok = _can.Connect();
|
||||
CanStatusText = ok ? "Connected" : "Connection failed";
|
||||
CanStatusText = ok ? _loc.GetString("Status.Connected") : _loc.GetString("Status.ConnectionFailed");
|
||||
IsCanConnected = ok;
|
||||
|
||||
if (ok)
|
||||
@@ -265,7 +359,7 @@ namespace HC_APTBS.ViewModels
|
||||
_bench.StopPumpSender();
|
||||
_can.Disconnect();
|
||||
IsCanConnected = false;
|
||||
CanStatusText = "Disconnected";
|
||||
CanStatusText = _loc.GetString("Status.Disconnected");
|
||||
}
|
||||
|
||||
// ── Live bench readings ───────────────────────────────────────────────────
|
||||
@@ -361,18 +455,24 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
if (CurrentPump == null) return;
|
||||
|
||||
// Block test start if an unlock is still in progress.
|
||||
if (_unlockVm != null && !_unlockVm.IsComplete)
|
||||
{
|
||||
VerboseStatus = _loc.GetString("Status.UnlockInProgress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Block test start if the unlock failed or was cancelled.
|
||||
if (CurrentPump.UnlockType != 0 && _unlockVm?.IsSuccess != true)
|
||||
{
|
||||
VerboseStatus = _loc.GetString("Status.UnlockRequired");
|
||||
return;
|
||||
}
|
||||
|
||||
_testCts = new CancellationTokenSource();
|
||||
IsTestRunning = true;
|
||||
IsTestSaved = false;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -418,7 +518,7 @@ namespace HC_APTBS.ViewModels
|
||||
if (CurrentPump == null) return;
|
||||
|
||||
// Step 1: Authenticate operator.
|
||||
var authVm = new UserCheckViewModel(_config, _lastAuthenticatedUser);
|
||||
var authVm = new UserCheckViewModel(_config, _loc, _lastAuthenticatedUser);
|
||||
var authDlg = new UserCheckDialog(authVm) { Owner = Application.Current.MainWindow };
|
||||
authDlg.ShowDialog();
|
||||
if (!authVm.Accepted) return;
|
||||
@@ -444,22 +544,43 @@ namespace HC_APTBS.ViewModels
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"GenerateReport: {ex.Message}");
|
||||
MessageBox.Show($"Failed to generate report:\n{ex.Message}",
|
||||
"Report Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show(string.Format(_loc.GetString("Error.ReportGeneration"), ex.Message),
|
||||
_loc.GetString("Error.ReportTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanGenerateReport()
|
||||
=> CurrentPump != null && !IsTestRunning && CurrentPump.Tests.Count > 0;
|
||||
|
||||
// ── Commands: language toggle ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>Toggles the UI language between Spanish and English.</summary>
|
||||
[RelayCommand]
|
||||
private void ToggleLanguage()
|
||||
{
|
||||
_loc.SetLanguage(_loc.CurrentLanguage == "ESP" ? "ENG" : "ESP");
|
||||
}
|
||||
|
||||
/// <summary>Refreshes all ViewModel-cached localised strings after a language change.</summary>
|
||||
private void RefreshLocalisedStrings()
|
||||
{
|
||||
CanStatusText = IsCanConnected
|
||||
? _loc.GetString("Status.Connected")
|
||||
: _loc.GetString("Status.Disconnected");
|
||||
}
|
||||
|
||||
// ── Commands: settings ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Saves all current settings and bench configuration to disk.</summary>
|
||||
/// <summary>Opens the settings dialog for editing application configuration.</summary>
|
||||
[RelayCommand]
|
||||
private void SaveSettings()
|
||||
private void OpenSettings()
|
||||
{
|
||||
_config.SaveSettings();
|
||||
_config.SaveBench();
|
||||
var vm = new SettingsViewModel(_config, _loc);
|
||||
var dlg = new SettingsDialog(vm) { Owner = Application.Current.MainWindow };
|
||||
dlg.ShowDialog();
|
||||
|
||||
if (vm.Accepted && _refreshTimer != null)
|
||||
_refreshTimer.Interval = TimeSpan.FromMilliseconds(_config.Settings.RefreshBenchInterfaceMs);
|
||||
}
|
||||
|
||||
// ── Initialisation ────────────────────────────────────────────────────────
|
||||
@@ -571,7 +692,7 @@ namespace HC_APTBS.ViewModels
|
||||
=> App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
IsTestRunning = true;
|
||||
VerboseStatus = "Test started...";
|
||||
VerboseStatus = _loc.GetString("Test.Started");
|
||||
TestPanel.IsRunning = true;
|
||||
TestPanel.ResetResults();
|
||||
ResultDisplay.Clear();
|
||||
@@ -585,7 +706,7 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
IsTestRunning = false;
|
||||
LastTestSuccess = !interrupted && success;
|
||||
VerboseStatus = interrupted ? "Test stopped." : (success ? "PASS" : "FAIL");
|
||||
VerboseStatus = interrupted ? _loc.GetString("Test.Stopped") : (success ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail"));
|
||||
TestPanel.IsRunning = false;
|
||||
_bench.StopPumpSender();
|
||||
StartTestCommand.NotifyCanExecuteChanged();
|
||||
@@ -611,10 +732,40 @@ namespace HC_APTBS.ViewModels
|
||||
_bench.SetRelay(RelayNames.Electronic, true);
|
||||
});
|
||||
|
||||
private static void ShowPsgSyncError()
|
||||
private void ShowPsgSyncError()
|
||||
=> MessageBox.Show(
|
||||
"PSG sync pulse not detected. Check encoder connection.",
|
||||
"PSG Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_loc.GetString("Error.PsgSync"),
|
||||
_loc.GetString("Error.PsgTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
|
||||
// ── Voltage warning ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Shows a voltage warning dialog when the pump supply voltage requirement
|
||||
/// changes between 27 V and 13.5 V (or vice versa). Only triggers on
|
||||
/// state transitions, matching the old <c>WAlert27v</c> behaviour.
|
||||
/// </summary>
|
||||
private void CheckVoltageWarning(PumpDefinition pump)
|
||||
{
|
||||
bool is27V = !string.IsNullOrEmpty(pump.Tension)
|
||||
&& pump.Tension.Contains("27");
|
||||
|
||||
if (is27V && !_lastPumpWas27V)
|
||||
{
|
||||
var vm = new Dialogs.VoltageWarningViewModel("27 V");
|
||||
var dlg = new Views.Dialogs.VoltageWarningDialog(vm)
|
||||
{ Owner = Application.Current.MainWindow };
|
||||
dlg.ShowDialog();
|
||||
_lastPumpWas27V = true;
|
||||
}
|
||||
else if (!is27V && _lastPumpWas27V)
|
||||
{
|
||||
var vm = new Dialogs.VoltageWarningViewModel("13.5 V");
|
||||
var dlg = new Views.Dialogs.VoltageWarningDialog(vm)
|
||||
{ Owner = Application.Current.MainWindow };
|
||||
dlg.ShowDialog();
|
||||
_lastPumpWas27V = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels
|
||||
{
|
||||
@@ -12,6 +13,10 @@ namespace HC_APTBS.ViewModels
|
||||
/// </summary>
|
||||
public sealed partial class PhaseCardViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Initialises a new phase card with a localization service.</summary>
|
||||
public PhaseCardViewModel(ILocalizationService loc) => _loc = loc;
|
||||
// ── Identity ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Display name of the phase (e.g. "1 - S_001").</summary>
|
||||
@@ -82,7 +87,7 @@ namespace HC_APTBS.ViewModels
|
||||
if (Source != null)
|
||||
Source.Enabled = value;
|
||||
|
||||
ResultText = value ? "\u2013" : "disabled";
|
||||
ResultText = value ? "\u2013" : _loc.GetString("Common.Disabled");
|
||||
|
||||
// Notify parent.
|
||||
EnabledChanged?.Invoke(this);
|
||||
@@ -96,7 +101,7 @@ namespace HC_APTBS.ViewModels
|
||||
IsActive = false;
|
||||
IsPassed = false;
|
||||
IsFailed = false;
|
||||
ResultText = IsEnabled ? "\u2013" : "disabled";
|
||||
ResultText = IsEnabled ? "\u2013" : _loc.GetString("Common.Disabled");
|
||||
|
||||
foreach (var indicator in ResultIndicators)
|
||||
indicator.Reset();
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
private readonly IKwpService _kwp;
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
private readonly IAppLogger _log;
|
||||
private const string LogId = "PumpIdentVM";
|
||||
|
||||
@@ -35,10 +36,12 @@ namespace HC_APTBS.ViewModels
|
||||
public PumpIdentificationViewModel(
|
||||
IKwpService kwpService,
|
||||
IConfigurationService configService,
|
||||
ILocalizationService loc,
|
||||
IAppLogger logger)
|
||||
{
|
||||
_kwp = kwpService;
|
||||
_config = configService;
|
||||
_loc = loc;
|
||||
_log = logger;
|
||||
|
||||
// Wire KWP progress events to local properties.
|
||||
@@ -171,7 +174,7 @@ namespace HC_APTBS.ViewModels
|
||||
if (string.IsNullOrEmpty(port))
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
KlineConnectError = "No K-Line device found");
|
||||
KlineConnectError = _loc.GetString("Error.KLineNotFound"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels
|
||||
{
|
||||
@@ -10,6 +11,11 @@ namespace HC_APTBS.ViewModels
|
||||
/// </summary>
|
||||
public sealed partial class ResultRowViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Initialises a new result row with a localization service.</summary>
|
||||
public ResultRowViewModel(ILocalizationService loc) => _loc = loc;
|
||||
|
||||
[ObservableProperty] private string _phaseName = string.Empty;
|
||||
[ObservableProperty] private string _parameterName = string.Empty;
|
||||
[ObservableProperty] private double _target;
|
||||
@@ -17,8 +23,8 @@ namespace HC_APTBS.ViewModels
|
||||
[ObservableProperty] private double _average;
|
||||
[ObservableProperty] private bool _passed;
|
||||
|
||||
/// <summary>"PASS" or "FAIL".</summary>
|
||||
public string ResultLabel => Passed ? "PASS" : "FAIL";
|
||||
/// <summary>Localised "PASS" or "FAIL" label.</summary>
|
||||
public string ResultLabel => Passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -31,6 +37,11 @@ namespace HC_APTBS.ViewModels
|
||||
/// </summary>
|
||||
public sealed partial class ResultDisplayViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Initialises a new result display with a localization service.</summary>
|
||||
public ResultDisplayViewModel(ILocalizationService loc) => _loc = loc;
|
||||
|
||||
// ── Properties ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Name of the test whose results are displayed.</summary>
|
||||
@@ -60,7 +71,7 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
if (tp.Result == null) continue;
|
||||
allPassed = allPassed && tp.Result.Passed;
|
||||
Results.Add(new ResultRowViewModel
|
||||
Results.Add(new ResultRowViewModel(_loc)
|
||||
{
|
||||
PhaseName = phase.Name,
|
||||
ParameterName = tp.Name,
|
||||
@@ -86,7 +97,7 @@ namespace HC_APTBS.ViewModels
|
||||
return;
|
||||
}
|
||||
}
|
||||
Results.Add(new ResultRowViewModel
|
||||
Results.Add(new ResultRowViewModel(_loc)
|
||||
{
|
||||
PhaseName = phaseName,
|
||||
ParameterName = paramName,
|
||||
@@ -115,7 +126,7 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
if (tp.Result == null) continue;
|
||||
allPassed = allPassed && tp.Result.Passed;
|
||||
Results.Add(new ResultRowViewModel
|
||||
Results.Add(new ResultRowViewModel(_loc)
|
||||
{
|
||||
PhaseName = phase.Name,
|
||||
ParameterName = tp.Name,
|
||||
@@ -128,7 +139,7 @@ namespace HC_APTBS.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
TestName = tests.Count > 0 ? "All Tests" : string.Empty;
|
||||
TestName = tests.Count > 0 ? _loc.GetString("Result.AllTests") : string.Empty;
|
||||
OverallPassed = allPassed && Results.Count > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels
|
||||
{
|
||||
@@ -39,6 +40,10 @@ namespace HC_APTBS.ViewModels
|
||||
/// </summary>
|
||||
public sealed partial class TestDisplayViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Initialises a new test display with a localization service.</summary>
|
||||
public TestDisplayViewModel(ILocalizationService loc) => _loc = loc;
|
||||
// ── Properties ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Name of the test currently being executed (e.g. "F", "SVME").</summary>
|
||||
@@ -72,7 +77,7 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
Name = phase.Name,
|
||||
IsEnabled = phase.Enabled,
|
||||
ResultText = phase.Enabled ? "–" : "disabled"
|
||||
ResultText = phase.Enabled ? "\u2013" : _loc.GetString("Common.Disabled")
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -98,7 +103,7 @@ namespace HC_APTBS.ViewModels
|
||||
row.IsActive = false;
|
||||
row.IsPassed = passed;
|
||||
row.IsFailed = !passed;
|
||||
row.ResultText = passed ? "PASS" : "FAIL";
|
||||
row.ResultText = passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +116,7 @@ namespace HC_APTBS.ViewModels
|
||||
row.IsActive = false;
|
||||
row.IsPassed = false;
|
||||
row.IsFailed = false;
|
||||
row.ResultText = row.IsEnabled ? "–" : "disabled";
|
||||
row.ResultText = row.IsEnabled ? "\u2013" : _loc.GetString("Common.Disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels
|
||||
{
|
||||
@@ -18,6 +19,11 @@ namespace HC_APTBS.ViewModels
|
||||
/// </summary>
|
||||
public sealed partial class TestPanelViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Initialises a new test panel with a localization service.</summary>
|
||||
public TestPanelViewModel(ILocalizationService loc) => _loc = loc;
|
||||
|
||||
// ── Cached active phase for fast live-indicator lookup ─────────────────────
|
||||
|
||||
private PhaseCardViewModel? _activePhaseCard;
|
||||
@@ -91,7 +97,7 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
foreach (var testDef in pump.Tests)
|
||||
{
|
||||
var section = TestSectionViewModel.FromDefinition(testDef, ShowOperationValues);
|
||||
var section = TestSectionViewModel.FromDefinition(testDef, ShowOperationValues, _loc);
|
||||
Tests.Add(section);
|
||||
}
|
||||
|
||||
@@ -152,7 +158,7 @@ namespace HC_APTBS.ViewModels
|
||||
phase.IsActive = false;
|
||||
phase.IsPassed = passed;
|
||||
phase.IsFailed = !passed;
|
||||
phase.ResultText = passed ? "PASS" : "FAIL";
|
||||
phase.ResultText = passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels
|
||||
{
|
||||
@@ -11,6 +12,11 @@ namespace HC_APTBS.ViewModels
|
||||
/// </summary>
|
||||
public sealed partial class TestSectionViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>Initialises a new test section with a localization service.</summary>
|
||||
public TestSectionViewModel(ILocalizationService loc) => _loc = loc;
|
||||
|
||||
// ── Suppress cascade guard ────────────────────────────────────────────────
|
||||
|
||||
private bool _suppressCascade;
|
||||
@@ -105,12 +111,13 @@ namespace HC_APTBS.ViewModels
|
||||
/// </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)
|
||||
/// <param name="loc">Localization service for user-facing strings.</param>
|
||||
public static TestSectionViewModel FromDefinition(TestDefinition test, bool showValues, ILocalizationService loc)
|
||||
{
|
||||
var section = new TestSectionViewModel
|
||||
var section = new TestSectionViewModel(loc)
|
||||
{
|
||||
TestName = test.Name,
|
||||
Description = MapDescription(test.Name),
|
||||
Description = loc.GetString(MapDescriptionKey(test.Name)),
|
||||
ConditioningTimeSec = test.ConditioningTimeSec,
|
||||
MeasurementTimeSec = test.MeasurementTimeSec,
|
||||
MeasurementsPerSecond = test.MeasurementsPerSecond,
|
||||
@@ -119,12 +126,12 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
foreach (var phaseDef in test.Phases)
|
||||
{
|
||||
var card = new PhaseCardViewModel
|
||||
var card = new PhaseCardViewModel(loc)
|
||||
{
|
||||
Name = phaseDef.Name,
|
||||
IsCritical = phaseDef.IsCritical,
|
||||
IsEnabled = phaseDef.Enabled,
|
||||
ResultText = phaseDef.Enabled ? "\u2013" : "disabled",
|
||||
ResultText = phaseDef.Enabled ? "\u2013" : loc.GetString("Common.Disabled"),
|
||||
ShowOperationValues = showValues,
|
||||
Source = phaseDef,
|
||||
EnabledChanged = section.OnChildEnabledChanged
|
||||
@@ -157,16 +164,17 @@ namespace HC_APTBS.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a test type identifier to a human-readable description.
|
||||
/// Maps a test type identifier to a localization resource key.
|
||||
/// Returns the test name itself for unknown types (fail-visible).
|
||||
/// </summary>
|
||||
private static string MapDescription(string testName) => testName switch
|
||||
private static string MapDescriptionKey(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",
|
||||
TestType.Wl => "TestType.Warmup",
|
||||
TestType.Dfi => "TestType.Adjustment",
|
||||
TestType.F => "TestType.Flow",
|
||||
TestType.Svme => "TestType.ServoValve",
|
||||
TestType.Up => "TestType.Upstroke",
|
||||
TestType.Pfp => "TestType.PreInjection",
|
||||
_ => testName
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user