using System; using System.Collections.ObjectModel; using System.Globalization; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using HC_APTBS.Models; using HC_APTBS.Services; namespace HC_APTBS.ViewModels.Pages { /// /// Developer Tools — EEPROM unlock password library. Persisted via /// . /// /// "Apply" sends the standard KWP unlock packet /// [0x18 0x00 Zone KeyHi KeyLo] over the persistent K-Line session /// (per docs/kline_eeprom_spec.md) and logs ACK/NAK in the parent's /// transaction log. "Add" pushes a new entry built from the small inline editor. /// /// Compiled into Debug builds only — see HC_APTBS.csproj. /// public sealed partial class DeveloperToolsPasswordsViewModel : ObservableObject { private readonly DeveloperPageViewModel _parent; private readonly IKwpService _kwp; private readonly IConfigurationService _config; private readonly IAppLogger _log; private const string LogId = nameof(DeveloperToolsPasswordsViewModel); public DeveloperToolsPasswordsViewModel( DeveloperPageViewModel parent, IKwpService kwp, IConfigurationService config, IAppLogger log) { _parent = parent; _kwp = kwp; _config = config; _log = log; } /// Persistent collection from . public ObservableCollection Items => _config.EepromPasswords; /// Currently selected entry. Drives Apply / Delete enable state. [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ApplySelectedCommand))] [NotifyCanExecuteChangedFor(nameof(DeleteSelectedCommand))] private EepromPassword? _selected; // ── Inline "Add new" editor ────────────────────────────────────────────── /// Display name for the new entry. [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddCommand))] private string _newName = string.Empty; /// Zone byte for the new entry, hex (e.g. "03"). [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddCommand))] private string _newZoneHex = "00"; /// 16-bit key for the new entry, hex (e.g. "00FF"). [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddCommand))] private string _newKeyHex = "0000"; // ── Commands ────────────────────────────────────────────────────────────── /// Sends [0x18 0x00 Zone KeyHi KeyLo] for the selected password. [RelayCommand(CanExecute = nameof(CanApplySelected))] private async Task ApplySelectedAsync() { if (Selected is null) return; byte[] payload = { 0x18, 0x00, Selected.Zone, (byte)(Selected.Key >> 8), (byte)(Selected.Key & 0xFF), }; var hex = DeveloperPageViewModel.FormatHex(payload); _parent.AppendLog(DeveloperLogDirection.Tx, $"[Apply '{Selected.Name}'] {hex}"); _parent.SetStatus($"Applying '{Selected.Name}' (zone 0x{Selected.Zone:X2}, key 0x{Selected.Key:X4})…"); try { var responses = await _kwp.SendRawCustomAsync(payload, CancellationToken.None); if (responses.Count == 0) { _parent.AppendLog(DeveloperLogDirection.Info, "(no response)"); _parent.SetStatus("No response — session may not be open."); return; } foreach (var pkt in responses) _parent.AppendLog(DeveloperLogDirection.Rx, DeveloperPageViewModel.FormatHex(pkt)); // Heuristic: a NAK is a single 3-byte packet with title 0x0A; // anything else is treated as success at the protocol layer. bool nak = responses.Count == 1 && responses[0].Length >= 3 && responses[0][2] == 0x0A; _parent.SetStatus(nak ? $"'{Selected.Name}' rejected (NAK)." : $"'{Selected.Name}' accepted."); } catch (Exception ex) { _parent.AppendLog(DeveloperLogDirection.Info, $"ERROR: {ex.Message}"); _parent.SetStatus($"Apply failed: {ex.Message}"); _log.Warning(LogId, $"ApplySelected failed: {ex.Message}"); } } private bool CanApplySelected() => Selected is not null; /// Adds a new entry from the inline editor and persists. [RelayCommand(CanExecute = nameof(CanAdd))] private void Add() { var name = (NewName ?? string.Empty).Trim(); if (name.Length == 0) { _parent.SetStatus("Enter a name before adding a password."); return; } if (!TryParseHexByte(NewZoneHex, out byte zone)) { _parent.SetStatus($"Bad zone hex '{NewZoneHex}'."); return; } if (!TryParseHexUshort(NewKeyHex, out ushort key)) { _parent.SetStatus($"Bad key hex '{NewKeyHex}'."); return; } Items.Add(new EepromPassword { Name = name, Zone = zone, Key = key }); _config.SaveEepromPasswords(); _parent.AppendLog(DeveloperLogDirection.Info, $"ADDED password '{name}' (zone 0x{zone:X2}, key 0x{key:X4})"); _parent.SetStatus($"Added '{name}'."); NewName = string.Empty; NewZoneHex = "00"; NewKeyHex = "0000"; } private bool CanAdd() => !string.IsNullOrWhiteSpace(NewName) && TryParseHexByte(NewZoneHex, out _) && TryParseHexUshort(NewKeyHex, out _); /// Removes the selected entry from the library and persists. [RelayCommand(CanExecute = nameof(CanDeleteSelected))] private void DeleteSelected() { if (Selected is null) return; var name = Selected.Name; Items.Remove(Selected); _config.SaveEepromPasswords(); _parent.AppendLog(DeveloperLogDirection.Info, $"DELETED password '{name}'"); _parent.SetStatus($"Deleted '{name}'."); Selected = null; } private bool CanDeleteSelected() => Selected is not null; // ── Helpers ─────────────────────────────────────────────────────────────── private static bool TryParseHexByte(string text, out byte value) { value = 0; if (string.IsNullOrWhiteSpace(text)) return false; var t = text.Trim(); if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t.Substring(2); return byte.TryParse(t, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value); } private static bool TryParseHexUshort(string text, out ushort value) { value = 0; if (string.IsNullOrWhiteSpace(text)) return false; var t = text.Trim(); if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t.Substring(2); return ushort.TryParse(t, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value); } } }