Bundles several feature streams that have been iterating on the working tree: - Developer Tools page (Debug-only via DEVELOPER_TOOLS symbol): hosts the identification card, manual KWP write + transaction log, ROM/EEPROM dump card with progress banner and completion message, persisted custom-commands library, persisted EEPROM passwords library. New service primitives: IKwpService.SendRawCustomAsync / ReadEepromAsync / ReadRomEepromAsync. Persistence mirrors the Clients XML pattern in two new files (custom_commands.xml, eeprom_passwords.xml). - Auto-test orchestrator (IAutoTestOrchestrator + AutoTestState): linear K-Line read -> unlock -> bench-on -> test sequence with snackbar UI and progress dialog VM, gated on dashboard alarms. - BIP-STATUS display: BipDisplayViewModel + BipDisplayView, RAM read at 0x0106 via IKwpService.ReadBipStatusAsync; status definitions in BipStatusDefinition. - Tests page redesign: TestSectionCard + PhaseTileView replacing the old TestPlanView/TestRunningView/TestDoneView/TestPreconditionsView/ TestSectionView controls and their VMs. - Pump command sliders: Fluent thick-track style with overhang thumb, click-anywhere-and-drag, mouse-wheel adjustment. - Window startup: app.manifest declares PerMonitorV2 DPI awareness, MainWindow installs a WM_GETMINMAXINFO hook in OnSourceInitialized and maximizes there (after the hook is in place) so the app fits the work area exactly on any display configuration. - Misc: PercentToPixelsConverter, seed_aliases.py one-shot pump-alias importer, tools/Import-BipStatus.ps1, kline_eeprom_spec.md and dump-functions reference docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
342 lines
15 KiB
C#
342 lines
15 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.Infrastructure.Logging;
|
|
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;
|
|
private readonly IAppLogger _log;
|
|
|
|
/// <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;
|
|
|
|
/// <summary>
|
|
/// When true, the Dashboard "Connect & Auto Test" flow bypasses the oil-pump
|
|
/// leak-check dialog. Operator opts in once; does not affect manual controls.
|
|
/// </summary>
|
|
[ObservableProperty] private bool _autoTestSkipsOilPumpConfirm;
|
|
|
|
// ── 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;
|
|
[ObservableProperty] private int _rpmChartUpdateHz = 15;
|
|
|
|
// ── Constructor ───────────────────────────────────────────────────────
|
|
|
|
/// <param name="configService">Configuration service for loading/saving settings.</param>
|
|
/// <param name="localizationService">Localization service for language switching.</param>
|
|
/// <param name="logger">Application logger.</param>
|
|
public SettingsPageViewModel(IConfigurationService configService, ILocalizationService localizationService, IAppLogger logger)
|
|
{
|
|
_config = configService;
|
|
_loc = localizationService;
|
|
_log = logger;
|
|
|
|
LoadFromConfig();
|
|
EnumerateFtdiDevices();
|
|
}
|
|
|
|
// ── Commands ──────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Copies all local values back to AppSettings, saves to disk, applies language if changed.</summary>
|
|
[RelayCommand]
|
|
private void Save()
|
|
{
|
|
_log.Info("SETTINGSVM", $"Save invoked. SelectedLanguage='{SelectedLanguage}', loc.CurrentLanguage='{_loc.CurrentLanguage}'");
|
|
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;
|
|
s.AutoTestSkipsOilPumpConfirm = AutoTestSkipsOilPumpConfirm;
|
|
|
|
// 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;
|
|
s.RpmChartUpdateHz = RpmChartUpdateHz;
|
|
|
|
// 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;
|
|
AutoTestSkipsOilPumpConfirm = s.AutoTestSkipsOilPumpConfirm;
|
|
|
|
// 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;
|
|
RpmChartUpdateHz = s.RpmChartUpdateHz;
|
|
|
|
// 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.
|
|
}
|
|
}
|
|
}
|
|
}
|