Files
HC_APTBS/ViewModels/Pages/SettingsPageViewModel.cs
LucianoDev 0280a2fad1 feat: page-based navigation shell + Tests page wizard
Replace the monolithic MainWindow with a SelectedPage-driven shell
(Dashboard / Pump / Bench / Tests / Results / Settings). The Tests
page gets the Plan -> Preconditions -> Running -> Done wizard from
ui-structure.md \u00a74, backed by a 7-item precondition gate and
shared sub-views (PhaseCardView / TestSectionView / GraphicIndicatorView)
extracted from the now-deleted monolithic TestPanelView.

New VMs / views:
- Tests wizard: TestPreconditions, PhaseCard, GraphicIndicator,
  TestSection, TestPlan, TestRunning, TestDone
- Dashboard panels: DashboardConnection, DashboardReadings,
  DashboardAlarms, InterlockBanner, ResultHistory
- Pump / bench panels: PumpIdentificationPanel, PumpLiveData,
  UnlockPanel, BenchDriveControl, BenchReadings, RelayBank,
  TemperatureControl, DtcList, AuthGate
- Dialogs: generic ConfirmDialog, UserManageDialog, UserPromptDialog

Supporting changes:
- IsOilPumpOn exposed on MainViewModel for precondition evaluation
- RequiresAuth added to TestDefinition (XML round-trip)
- BipStatusDefinition + CompletedTestRun models
- ~35 new Test.* localization keys (en + es)
- Settings moved from modal dialog to full page
- Pause / Retry / Skip stubs in TestRunningView; full spec in
  docs/gap-test-running-controls.md for follow-up implementation
- docs/ui-structure.md captures the wizard design

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 13:11:34 +02:00

326 lines
14 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Windows;
using HC_APTBS.Infrastructure.Kwp;
using HC_APTBS.Models;
using HC_APTBS.Services;
using HC_APTBS.ViewModels.Dialogs;
using HC_APTBS.Views.Dialogs;
namespace HC_APTBS.ViewModels.Pages
{
/// <summary>
/// ViewModel for the Settings navigation page.
/// Loads a local copy of every <see cref="AppSettings"/> property so that
/// Discard reverts all pending changes without touching persisted state.
/// </summary>
public sealed partial class SettingsPageViewModel : ObservableObject
{
private readonly IConfigurationService _config;
private readonly ILocalizationService _loc;
/// <summary>
/// Raised after <see cref="SaveCommand"/> successfully persists settings.
/// The shell subscribes to reseed timers or other settings-dependent state.
/// </summary>
public event Action? SettingsSaved;
// ── 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 SettingsPageViewModel(IConfigurationService configService, ILocalizationService localizationService)
{
_config = configService;
_loc = localizationService;
LoadFromConfig();
EnumerateFtdiDevices();
}
// ── Commands ──────────────────────────────────────────────────────────
/// <summary>Copies all local values back to AppSettings, saves to disk, applies language if changed.</summary>
[RelayCommand]
private void Save()
{
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();
SettingsSaved?.Invoke();
}
/// <summary>Reverts all local fields to the currently persisted <see cref="AppSettings"/> values.</summary>
[RelayCommand]
private void Discard()
{
LoadFromConfig();
}
/// <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);
}
/// <summary>
/// Opens the user management dialog after a successful admin authentication.
/// The auth dialog is re-prompted on every invocation — there is no session cache.
/// </summary>
[RelayCommand]
private void ManageUsers()
{
var owner = GetOwnerWindow();
// Step 1: admin authentication.
var authVm = new UserCheckViewModel(_config, _loc);
var authDlg = new UserCheckDialog(authVm) { Owner = owner };
authDlg.ShowDialog();
if (!authVm.Accepted) return;
// Step 2: management dialog.
var manageVm = new UserManageViewModel(_config, _loc);
var manageDlg = new UserManageDialog(manageVm) { Owner = owner };
manageDlg.ShowDialog();
}
/// <summary>Finds a plausible dialog owner (active window, else main window).</summary>
private static Window? GetOwnerWindow()
{
foreach (Window w in Application.Current.Windows)
{
if (w.IsActive) return w;
}
return Application.Current.MainWindow;
}
// ── Helpers ───────────────────────────────────────────────────────────
/// <summary>Copies every persisted setting into the local mirror fields.</summary>
private void LoadFromConfig()
{
var s = _config.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
Relations.Clear();
foreach (var r in s.Relations)
Relations.Add(new RpmVoltageRelation(r.Voltage, r.Rpm));
}
/// <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.
}
}
}
}