Files
HC_APTBS/ViewModels/DtcListViewModel.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

190 lines
6.9 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// ViewModel for the Pump page §3.b DTC list (Diagnostic Trouble Codes).
///
/// <para>Exposes a list of fault-code lines parsed from
/// <see cref="IKwpService.ReadFaultCodesAsync"/>, with read/clear commands.
/// Each line is surfaced as a structured <see cref="DtcEntry"/> so the UI
/// can render them as rows rather than a raw blob.</para>
/// </summary>
public sealed partial class DtcListViewModel : ObservableObject
{
private readonly IKwpService _kwp;
private readonly ILocalizationService _loc;
private readonly IAppLogger _log;
private const string LogId = "DtcListVM";
/// <summary>Initialises the ViewModel with the required services.</summary>
public DtcListViewModel(IKwpService kwp, ILocalizationService loc, IAppLogger log)
{
_kwp = kwp;
_loc = loc;
_log = log;
}
// ── State ─────────────────────────────────────────────────────────────────
/// <summary>Parsed fault-code entries, one per line returned from K-Line.</summary>
public ObservableCollection<DtcEntry> Codes { get; } = new();
/// <summary>True while a read or clear operation is in progress.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ReadCommand))]
[NotifyCanExecuteChangedFor(nameof(ClearCommand))]
private bool _isBusy;
/// <summary>True when the last read returned no fault codes.</summary>
[ObservableProperty] private bool _isClear;
/// <summary>Status text shown above the list (empty, error, or "last read at …").</summary>
[ObservableProperty] private string _statusText = string.Empty;
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>Reads the current DTCs from the ECU over K-Line.</summary>
[RelayCommand(CanExecute = nameof(CanOperate))]
private async Task ReadAsync()
{
var port = _kwp.DetectKLinePort();
if (string.IsNullOrEmpty(port))
{
StatusText = _loc.GetString("Error.KLineNotFound");
return;
}
IsBusy = true;
try
{
string raw = await _kwp.ReadFaultCodesAsync(port);
ApplyRawText(raw);
StatusText = string.Format(_loc.GetString("Dtc.LastRead"), DateTime.Now);
}
catch (Exception ex)
{
_log.Error(LogId, $"ReadAsync: {ex.Message}");
StatusText = ex.Message;
}
finally
{
IsBusy = false;
}
}
/// <summary>Clears all DTCs on the ECU and refreshes the list.</summary>
[RelayCommand(CanExecute = nameof(CanOperate))]
private async Task ClearAsync()
{
var port = _kwp.DetectKLinePort();
if (string.IsNullOrEmpty(port))
{
StatusText = _loc.GetString("Error.KLineNotFound");
return;
}
IsBusy = true;
try
{
string raw = await _kwp.ClearFaultCodesAsync(port);
ApplyRawText(raw);
StatusText = _loc.GetString("Dtc.Cleared");
}
catch (Exception ex)
{
_log.Error(LogId, $"ClearAsync: {ex.Message}");
StatusText = ex.Message;
}
finally
{
IsBusy = false;
}
}
private bool CanOperate() => !IsBusy;
// ── Helpers ───────────────────────────────────────────────────────────────
/// <summary>
/// Populates <see cref="Codes"/> from the raw K-Line fault-code string.
/// Handles the special "No fault codes" response by setting <see cref="IsClear"/>.
/// </summary>
private void ApplyRawText(string raw)
{
Application.Current.Dispatcher.Invoke(() =>
{
Codes.Clear();
if (string.IsNullOrWhiteSpace(raw)
|| raw.Contains("No fault", StringComparison.OrdinalIgnoreCase))
{
IsClear = true;
return;
}
IsClear = false;
foreach (var line in raw.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
string trimmed = line.Trim();
if (trimmed.Length == 0) continue;
Codes.Add(DtcEntry.Parse(trimmed));
}
});
}
/// <summary>Clears any cached DTCs (used when the pump selection changes).</summary>
public void Reset()
{
Application.Current.Dispatcher.Invoke(() =>
{
Codes.Clear();
IsClear = false;
StatusText = string.Empty;
});
}
}
/// <summary>
/// A single diagnostic trouble code row. Split into <see cref="Code"/> and
/// <see cref="Description"/> when the raw line follows the usual "CODE — text"
/// layout, otherwise the raw text is surfaced in <see cref="Description"/>.
/// </summary>
public sealed class DtcEntry
{
/// <summary>DTC code identifier (e.g. "P1688").</summary>
public string Code { get; set; } = string.Empty;
/// <summary>Human-readable description text.</summary>
public string Description { get; set; } = string.Empty;
/// <summary>Parses one line of K-Line fault-code output into a structured entry.</summary>
public static DtcEntry Parse(string line)
{
// Common K-Line formats: "P1234 — Description", "P1234: Description",
// "P1234 Description", or just raw text without a leading code.
int split = -1;
foreach (char sep in new[] { '—', '-', ':', '\t' })
{
split = line.IndexOf(sep);
if (split > 0) break;
}
if (split <= 0 || split > 10)
return new DtcEntry { Description = line };
return new DtcEntry
{
Code = line[..split].Trim(),
Description = line[(split + 1)..].Trim()
};
}
}
}