using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using HC_APTBS.Services; namespace HC_APTBS.ViewModels.Pages { /// Memory region selector for the dump card. public enum DumpRegion { /// ROM range 0x0000–0x9FFF, read via KWP ReadRomEeprom (0x03). Rom, /// EEPROM range 0x00–0xFF, read via KWP ReadEeprom (0x19). /// Note: only offsets 0x00–0xBF are valid per 256-byte block. Eeprom, } /// /// Developer Tools — ROM / EEPROM dump card. Iterates a user-supplied address /// range in 13-byte chunks aligned to 256-byte block boundaries (mirroring the /// legacy DumpRom / DumpEeprom routines in /// docs/dump functions.txt) and writes the result to /// %UserProfile%\.HC_APTBS\dumps\{ident}_{swver1}_{start:X4}-{end:X4}.bin. /// /// Compiled into Debug builds only — see HC_APTBS.csproj. /// public sealed partial class DeveloperToolsDumpViewModel : ObservableObject { private const int MaxChunk = 13; private const int BlockSize = 0x0100; // EEPROM has only 0x00–0xBF readable per 256-byte block via the unauth path; // the legacy DumpEeprom routine treats this as the "valid bytes per block" cap. private const int EepromValidBytesPerBlock = 0x00C0; private readonly DeveloperPageViewModel _parent; private readonly IKwpService _kwp; private readonly IAppLogger _log; private const string LogId = nameof(DeveloperToolsDumpViewModel); /// /// Threshold above which an in-flight dump shows the prominent overlay banner /// (instead of just the inline progress bar). 0x0A00 = 2560 bytes; below that, /// reads complete in a few seconds and don't need the bigger indicator. /// public const int LargeDumpByteThreshold = 0x0A00; public DeveloperToolsDumpViewModel(DeveloperPageViewModel parent, IKwpService kwp, IAppLogger log) { _parent = parent; _kwp = kwp; _log = log; // Sensible default: full ROM dump. StartAddressHex = "0000"; EndAddressHex = "9FFF"; } // ── Inputs ──────────────────────────────────────────────────────────────── /// Region radio: ROM or EEPROM. Updates address-range bounds + defaults. [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DumpCommand))] private DumpRegion _region = DumpRegion.Rom; /// Start address in hex (no 0x prefix required). [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DumpCommand))] private string _startAddressHex = string.Empty; /// End address in hex (inclusive). [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DumpCommand))] private string _endAddressHex = string.Empty; /// Per-byte progress, 0..1, for the progress bar. [ObservableProperty] private double _progress; /// True while a dump is in flight — disables Dump button. [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DumpCommand))] private bool _isDumping; /// /// True while a dump is in flight AND the requested range is larger than /// . Drives the page-level banner overlay /// so the operator sees clearly that a long dump is running. /// [ObservableProperty] private bool _isLargeDumpInProgress; /// Total bytes the current dump intends to read. [ObservableProperty] private int _totalBytes; /// Bytes received so far for the current dump. [ObservableProperty] private int _bytesCollected; /// Address of the next byte to read (for the banner display). [ObservableProperty] private int _currentAddress; /// Status / hint text shown under the inputs. [ObservableProperty] private string _statusText = string.Empty; /// True when the ROM region is selected. Two-way bindable for radio buttons. public bool IsRomSelected { get => Region == DumpRegion.Rom; set { if (value) Region = DumpRegion.Rom; } } /// True when the EEPROM region is selected. Two-way bindable for radio buttons. public bool IsEepromSelected { get => Region == DumpRegion.Eeprom; set { if (value) Region = DumpRegion.Eeprom; } } partial void OnRegionChanged(DumpRegion value) { // Reset to typical full-range defaults so the user doesn't accidentally // dump a ROM-sized range out of EEPROM and get NAKs all the way. switch (value) { case DumpRegion.Rom: StartAddressHex = "0000"; EndAddressHex = "9FFF"; break; case DumpRegion.Eeprom: StartAddressHex = "0000"; EndAddressHex = "00BF"; break; } OnPropertyChanged(nameof(IsRomSelected)); OnPropertyChanged(nameof(IsEepromSelected)); } // ── Command ─────────────────────────────────────────────────────────────── /// /// Iterates the selected range in 13-byte chunks aligned to 256-byte blocks /// and writes the bytes to disk. Filename built from the current pump /// identifier and SW ver 1 read by the K-Line session. /// [RelayCommand(CanExecute = nameof(CanDump))] private async Task DumpAsync() { if (!TryParseAddresses(out int startAddr, out int endAddr, out string parseError)) { StatusText = parseError; return; } var ident = (_parent.Root.PumpIdentification.KlinePumpId ?? string.Empty).Trim(); var swVer1 = (_parent.Root.PumpIdentification.KlineSwVersion1 ?? string.Empty).Trim(); if (ident.Length == 0 || swVer1.Length == 0) { StatusText = "Read pump identification first — ident and SW ver 1 are required for the filename."; return; } var dir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".HC_APTBS", "dumps"); try { Directory.CreateDirectory(dir); } catch (Exception ex) { StatusText = $"Could not create dump folder: {ex.Message}"; return; } var fileName = $"{Sanitize(ident)}_{Sanitize(swVer1)}_{startAddr:X4}-{endAddr:X4}.bin"; var fullPath = Path.Combine(dir, fileName); int totalBytes = endAddr - startAddr + 1; TotalBytes = totalBytes; BytesCollected = 0; CurrentAddress = startAddr; IsDumping = true; IsLargeDumpInProgress = totalBytes > LargeDumpByteThreshold; Progress = 0; var buffer = new List(totalBytes); int collected = 0; try { _parent.AppendLog(DeveloperLogDirection.Info, $"DUMP {Region} 0x{startAddr:X4}-0x{endAddr:X4} ({totalBytes} bytes) → {fileName}"); int addr = startAddr; while (addr <= endAddr) { CurrentAddress = addr; int blockBase = addr & 0xFF00; int offsetInBlock = addr & 0x00FF; int blockEndAbs; if (Region == DumpRegion.Eeprom) { if (offsetInBlock >= EepromValidBytesPerBlock) { // Skip the unreadable tail of this block. addr = blockBase + BlockSize; continue; } blockEndAbs = blockBase + EepromValidBytesPerBlock - 1; } else { blockEndAbs = blockBase + BlockSize - 1; } int maxReadableAbs = Math.Min(endAddr, blockEndAbs); while (addr <= maxReadableAbs) { int remaining = maxReadableAbs - addr + 1; byte len = (byte)Math.Min(MaxChunk, remaining); IReadOnlyList chunk = Region == DumpRegion.Rom ? await _kwp.ReadRomEepromAsync((ushort)addr, len) : await _kwp.ReadEepromAsync((ushort)addr, len); if (chunk.Count == 0) { StatusText = $"Read failed at 0x{addr:X4} (NAK or no session). Saving partial dump."; _parent.AppendLog(DeveloperLogDirection.Info, $"DUMP aborted at 0x{addr:X4} — {collected}/{totalBytes} bytes captured."); break; } buffer.AddRange(chunk); addr += chunk.Count; collected += chunk.Count; BytesCollected = collected; CurrentAddress = addr; Progress = (double)collected / totalBytes; } if (addr <= endAddr && (addr & 0x00FF) == 0) { // Already advanced to the next block — keep going. continue; } if (addr <= endAddr) { // We broke out of the inner loop early due to a read failure. break; } } if (buffer.Count > 0) { File.WriteAllBytes(fullPath, buffer.ToArray()); _parent.AppendLog(DeveloperLogDirection.Info, $"DUMP saved {buffer.Count} byte(s) → {fullPath}"); StatusText = buffer.Count == totalBytes ? $"Dump complete: {buffer.Count} bytes saved." : $"Partial dump: {buffer.Count}/{totalBytes} bytes saved."; } else { StatusText = "Dump produced no bytes — nothing was written."; _parent.AppendLog(DeveloperLogDirection.Info, "DUMP produced no bytes — skipped file write."); } } catch (Exception ex) { _log.Error(LogId, $"DumpAsync failed: {ex.Message}"); StatusText = $"Dump failed: {ex.Message}"; _parent.AppendLog(DeveloperLogDirection.Info, $"DUMP error: {ex.Message}"); } finally { IsDumping = false; IsLargeDumpInProgress = false; } // Modal confirmation so the operator can't miss completion of a long dump. var savedToText = buffer.Count > 0 ? $"\n\nFile: {fullPath}" : string.Empty; var summary = $"{StatusText}{savedToText}"; var image = buffer.Count == totalBytes ? System.Windows.MessageBoxImage.Information : System.Windows.MessageBoxImage.Warning; System.Windows.Application.Current?.Dispatcher.BeginInvoke(() => System.Windows.MessageBox.Show( summary, "Dump finished", System.Windows.MessageBoxButton.OK, image)); } private bool CanDump() { if (IsDumping) return false; if (string.IsNullOrWhiteSpace(StartAddressHex) || string.IsNullOrWhiteSpace(EndAddressHex)) return false; return TryParseAddresses(out _, out _, out _); } // ── Helpers ─────────────────────────────────────────────────────────────── private bool TryParseAddresses(out int startAddr, out int endAddr, out string error) { startAddr = 0; endAddr = 0; error = string.Empty; if (!TryParseHexAddress(StartAddressHex, out startAddr)) { error = $"Bad start address '{StartAddressHex}'."; return false; } if (!TryParseHexAddress(EndAddressHex, out endAddr)) { error = $"Bad end address '{EndAddressHex}'."; return false; } if (startAddr > endAddr) { error = "Invalid range: start > end."; return false; } int min, max; switch (Region) { case DumpRegion.Rom: min = 0x0000; max = 0x9FFF; break; case DumpRegion.Eeprom: min = 0x0000; max = 0x00FF; break; default: error = "Unknown region."; return false; } if (startAddr < min || endAddr > max) { error = $"Range outside {Region} bounds (0x{min:X4}–0x{max:X4})."; return false; } return true; } private static bool TryParseHexAddress(string text, out int value) { value = 0; if (string.IsNullOrWhiteSpace(text)) return false; var t = text.Trim(); if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t.Substring(2); return int.TryParse(t, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value); } private static string Sanitize(string s) { var chars = s.Select(c => char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '-' ? c : '_').ToArray(); return new string(chars); } } }