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>
190 lines
6.9 KiB
C#
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()
|
|
};
|
|
}
|
|
}
|
|
}
|