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>
321 lines
13 KiB
C#
321 lines
13 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
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>Kind of physical device represented by a <see cref="DeviceItem"/>.</summary>
|
|
public enum DeviceKind { Can, KLine, Bench }
|
|
|
|
/// <summary>
|
|
/// Represents a single detected device row in the Devices column.
|
|
/// </summary>
|
|
public sealed partial class DeviceItem : ObservableObject
|
|
{
|
|
/// <summary>What kind of adapter this row represents.</summary>
|
|
public DeviceKind Kind { get; init; }
|
|
|
|
/// <summary>
|
|
/// Device identity string: PCAN handle as hex for CAN, FTDI serial for K-Line,
|
|
/// empty for the single Bench placeholder row.
|
|
/// </summary>
|
|
public string Id { get; init; } = "";
|
|
|
|
/// <summary>PCAN channel handle (only meaningful when <see cref="Kind"/> is <see cref="DeviceKind.Can"/>).</summary>
|
|
public ushort CanHandle { get; init; }
|
|
|
|
/// <summary>Display name shown in the tile row.</summary>
|
|
[ObservableProperty] private string _name = "";
|
|
|
|
/// <summary>Short state label (right-aligned in the row).</summary>
|
|
[ObservableProperty] private string _stateLabel = "";
|
|
|
|
/// <summary>True when this device is the currently active connection.</summary>
|
|
[ObservableProperty] private bool _isConnected;
|
|
|
|
/// <summary>True when the device session is in a failed state (K-Line only).</summary>
|
|
[ObservableProperty] private bool _isFailed;
|
|
|
|
/// <summary>False for the Bench placeholder, which cannot be clicked.</summary>
|
|
public bool IsEnabled { get; init; } = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snackbar state for an in-flight CAN connect/disconnect transition.
|
|
/// </summary>
|
|
public sealed partial class DeviceTransitionViewModel : ObservableObject
|
|
{
|
|
/// <summary>Localised message shown to the user.</summary>
|
|
[ObservableProperty] private string _message = "";
|
|
|
|
/// <summary>True while the operation is running (spinner visible).</summary>
|
|
[ObservableProperty] private bool _isBusy;
|
|
|
|
/// <summary>Null while running; true on success; false on failure.</summary>
|
|
[ObservableProperty] private bool? _isSuccess;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ViewModel for the Devices column on the Dashboard.
|
|
///
|
|
/// <para>Owns three observable collections of <see cref="DeviceItem"/> (one per kind)
|
|
/// and exposes toggle/refresh commands. Communicates with CAN and K-Line services
|
|
/// through <see cref="ICanService"/> and <see cref="IKwpService"/> and reads
|
|
/// cross-cutting state (test running, connection flags) via <see cref="MainViewModel"/>.</para>
|
|
/// </summary>
|
|
public sealed partial class DashboardDevicesViewModel : ObservableObject
|
|
{
|
|
private readonly MainViewModel _root;
|
|
private readonly ICanService _can;
|
|
private readonly IKwpService _kwp;
|
|
|
|
/// <summary>Detected PCAN USB channels.</summary>
|
|
public ObservableCollection<DeviceItem> CanDevices { get; } = new();
|
|
|
|
/// <summary>Detected FTDI K-Line adapters.</summary>
|
|
public ObservableCollection<DeviceItem> KLineDevices { get; } = new();
|
|
|
|
/// <summary>Single bench-controller placeholder row.</summary>
|
|
public ObservableCollection<DeviceItem> BenchDevices { get; } = new();
|
|
|
|
/// <summary>Active snackbar VM for an in-flight CAN connect/disconnect; null when no transition.</summary>
|
|
[ObservableProperty] private DeviceTransitionViewModel? _transition;
|
|
|
|
public DashboardDevicesViewModel(MainViewModel root, ICanService can, IKwpService kwp)
|
|
{
|
|
_root = root;
|
|
_can = can;
|
|
_kwp = kwp;
|
|
|
|
root.PropertyChanged += OnRootPropertyChanged;
|
|
|
|
RefreshCanDevices();
|
|
RefreshKLineDevices();
|
|
RefreshBenchDevices();
|
|
}
|
|
|
|
// ── Refresh commands ──────────────────────────────────────────────────────
|
|
|
|
/// <summary>Re-enumerates attached PCAN USB channels.</summary>
|
|
[RelayCommand]
|
|
private void RefreshCanDevices()
|
|
{
|
|
CanDevices.Clear();
|
|
try
|
|
{
|
|
var channels = _can.EnumerateAttachedChannels();
|
|
foreach (var ch in channels)
|
|
{
|
|
CanDevices.Add(new DeviceItem
|
|
{
|
|
Kind = DeviceKind.Can,
|
|
Id = ch.Handle.ToString("X"),
|
|
CanHandle = ch.Handle,
|
|
Name = ch.DisplayName,
|
|
IsConnected = _root.IsCanConnected && _can.SelectedChannel == ch.Handle,
|
|
StateLabel = GetCanStateLabel(_root.IsCanConnected && _can.SelectedChannel == ch.Handle),
|
|
});
|
|
}
|
|
}
|
|
catch { /* PCAN DLL missing — leave list empty */ }
|
|
}
|
|
|
|
/// <summary>Re-enumerates connected FTDI K-Line adapters.</summary>
|
|
[RelayCommand]
|
|
private void RefreshKLineDevices()
|
|
{
|
|
KLineDevices.Clear();
|
|
try
|
|
{
|
|
uint count = FtdiInterface.GetDevicesCount();
|
|
if (count == 0) return;
|
|
|
|
var list = new FT_DEVICE_INFO_NODE[count];
|
|
FtdiInterface.GetDeviceList(list);
|
|
|
|
foreach (var dev in list)
|
|
{
|
|
if (string.IsNullOrEmpty(dev.SerialNumber)) continue;
|
|
bool connected = _kwp.KLineState == KLineConnectionState.Connected
|
|
&& _kwp.ConnectedPort == dev.SerialNumber;
|
|
bool failed = _kwp.KLineState == KLineConnectionState.Failed
|
|
&& _kwp.ConnectedPort == dev.SerialNumber;
|
|
KLineDevices.Add(new DeviceItem
|
|
{
|
|
Kind = DeviceKind.KLine,
|
|
Id = dev.SerialNumber,
|
|
Name = string.IsNullOrEmpty(dev.Description)
|
|
? dev.SerialNumber
|
|
: $"{dev.Description} ({dev.SerialNumber})",
|
|
IsConnected = connected,
|
|
IsFailed = failed,
|
|
StateLabel = GetKLineStateLabel(connected, failed),
|
|
IsEnabled = false, // K-Line rows are display-only; session is owned by AutoTestOrchestrator
|
|
});
|
|
}
|
|
}
|
|
catch { /* FTDI DLL not loaded — leave list empty */ }
|
|
}
|
|
|
|
private void RefreshBenchDevices()
|
|
{
|
|
BenchDevices.Clear();
|
|
BenchDevices.Add(new DeviceItem
|
|
{
|
|
Kind = DeviceKind.Bench,
|
|
Id = "bench",
|
|
Name = Str("Dashboard.Devices.BenchRow"),
|
|
IsConnected = _root.IsBenchConnected,
|
|
StateLabel = GetBenchStateLabel(_root.IsBenchConnected),
|
|
IsEnabled = false,
|
|
});
|
|
}
|
|
|
|
// ── Toggle command ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Connects or disconnects the device represented by <paramref name="item"/>.
|
|
/// Shows a confirmation dialog when a session is active or a test is running.
|
|
/// </summary>
|
|
[RelayCommand]
|
|
private async Task ToggleDevice(DeviceItem? item)
|
|
{
|
|
if (item is null || !item.IsEnabled) return;
|
|
|
|
// K-Line rows are non-clickable (IsEnabled=false), so they never reach this point.
|
|
// Sessions for K-Line are started/stopped exclusively by AutoTestOrchestrator.
|
|
if (item.Kind != DeviceKind.Can) return;
|
|
|
|
// A running test owns the CAN bus; never let the user yank it mid-run.
|
|
if (_root.IsTestRunning &&
|
|
!Confirm(Str("Devices.Confirm.Title"), Str("Devices.Confirm.Body.TestRunning")))
|
|
return;
|
|
|
|
await ToggleCanAsync(item);
|
|
}
|
|
|
|
private async Task ToggleCanAsync(DeviceItem item)
|
|
{
|
|
bool connecting = !item.IsConnected;
|
|
|
|
var t = new DeviceTransitionViewModel
|
|
{
|
|
Message = Str(connecting
|
|
? "Dashboard.Devices.Snackbar.Connecting"
|
|
: "Dashboard.Devices.Snackbar.Disconnecting"),
|
|
IsBusy = true,
|
|
};
|
|
Transition = t;
|
|
|
|
// ConnectCan/DisconnectCan mutate observable properties that fan out to
|
|
// CanExecuteChanged listeners on Buttons — those DPs are UI-thread-affine,
|
|
// so the commands must run on the Dispatcher even though they block briefly
|
|
// on the PCAN handle. Yield once so the snackbar paints before we block.
|
|
await Task.Yield();
|
|
|
|
bool ok;
|
|
try
|
|
{
|
|
if (connecting)
|
|
{
|
|
try { _can.SelectedChannel = item.CanHandle; }
|
|
catch { Transition = null; return; }
|
|
|
|
_root.ConnectCanCommand.Execute(null);
|
|
ok = _root.IsCanConnected;
|
|
}
|
|
else
|
|
{
|
|
_root.DisconnectCanCommand.Execute(null);
|
|
ok = !_root.IsCanConnected;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
ok = false;
|
|
}
|
|
|
|
t.IsBusy = false;
|
|
t.IsSuccess = ok;
|
|
t.Message = Str(ok
|
|
? (connecting ? "Dashboard.Devices.Snackbar.Connected"
|
|
: "Dashboard.Devices.Snackbar.Disconnected")
|
|
: "Dashboard.Devices.Snackbar.Failed");
|
|
|
|
RefreshCanDevices();
|
|
|
|
// Auto-dismiss after ~2 s; only clear if a fresh transition has not replaced this one.
|
|
_ = Task.Delay(2000).ContinueWith(_ =>
|
|
App.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
if (Transition == t) Transition = null;
|
|
}));
|
|
}
|
|
|
|
// ── State change wiring ───────────────────────────────────────────────────
|
|
|
|
private void OnRootPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
switch (e.PropertyName)
|
|
{
|
|
case nameof(MainViewModel.IsCanConnected):
|
|
App.Current.Dispatcher.Invoke(RefreshCanDevices);
|
|
break;
|
|
case nameof(MainViewModel.KLineState):
|
|
App.Current.Dispatcher.Invoke(RefreshKLineDevices);
|
|
break;
|
|
case nameof(MainViewModel.IsBenchConnected):
|
|
App.Current.Dispatcher.Invoke(SyncBenchState);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void SyncBenchState()
|
|
{
|
|
if (BenchDevices.Count == 0) return;
|
|
var row = BenchDevices[0];
|
|
row.IsConnected = _root.IsBenchConnected;
|
|
row.StateLabel = GetBenchStateLabel(_root.IsBenchConnected);
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
private static bool Confirm(string title, string message)
|
|
{
|
|
var vm = new ConfirmDialogViewModel
|
|
{
|
|
Title = title,
|
|
Message = message,
|
|
ConfirmText = Str("Common.Yes"),
|
|
CancelText = Str("Common.Cancel"),
|
|
};
|
|
var dlg = new ConfirmDialog(vm) { Owner = Application.Current.MainWindow };
|
|
dlg.ShowDialog();
|
|
return vm.Accepted;
|
|
}
|
|
|
|
private static string Str(string key)
|
|
=> Application.Current.TryFindResource(key) as string ?? key;
|
|
|
|
private static string GetCanStateLabel(bool connected)
|
|
=> connected ? Str("Dashboard.Devices.State.Connected") : Str("Dashboard.Devices.State.Idle");
|
|
|
|
private static string GetKLineStateLabel(bool connected, bool failed)
|
|
{
|
|
if (connected) return Str("Dashboard.Devices.State.Active");
|
|
if (failed) return Str("Dashboard.Devices.State.Failed");
|
|
return Str("Dashboard.Devices.State.Idle");
|
|
}
|
|
|
|
private static string GetBenchStateLabel(bool connected)
|
|
=> connected ? Str("Dashboard.Devices.State.Connected") : Str("Dashboard.Devices.State.Idle");
|
|
}
|
|
}
|