using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using HC_APTBS.Infrastructure.Kwp; using HC_APTBS.Infrastructure.Kwp.Packets; using HC_APTBS.Models; namespace HC_APTBS.Services.Impl { /// /// Implements using the FTDI USB-to-K-Line adapter /// and the KW1281 protocol stack from . /// /// /// The ECU initialisation address for all VP44 pumps is 0xF1 (broadcast). /// K-Line baud rate is 9600 bps. /// /// public sealed class KwpService : IKwpService, IDisposable { // ── Protocol constants ──────────────────────────────────────────────────── /// ECU initialisation address used in the 5-baud wake-up sequence. private const byte EcuInitAddress = 0xF1; /// K-Line baud rate (bps) for all VP44 communications. private const int KLineBaudRate = 9600; /// Interval between keep-alive ACK packets (ms). private const int KeepAliveIntervalMs = 1000; private readonly IAppLogger _log; private const string LogId = "KwpService"; // ── Persistent session fields ───────────────────────────────────────────── private FtdiInterface? _sessionIface; private KwpCommon? _sessionKwpCommon; private KW1281Connection? _sessionKwp; private string? _connectedPort; // ── Synchronization ─────────────────────────────────────────────────────── private readonly SemaphoreSlim _busLock = new(1, 1); private CancellationTokenSource? _keepAliveCts; private Task? _keepAliveTask; // ── Events ──────────────────────────────────────────────────────────────── /// public event Action? ProgressChanged; /// public event Action? PumpIdentified; /// public event Action? DfiRead; /// public event Action? PumpDisconnectRequested; /// public event Action? PumpReconnectRequested; /// public event Action? KLineStateChanged; // ── Session state ───────────────────────────────────────────────────────── private KLineConnectionState _kLineState = KLineConnectionState.Disconnected; /// public KLineConnectionState KLineState => _kLineState; // ── Constructor ─────────────────────────────────────────────────────────── /// Application logger. public KwpService(IAppLogger logger) { _log = logger; } // ── IKwpService: session lifecycle ──────────────────────────────────────── /// public async Task ConnectAsync(string port, CancellationToken ct = default) { if (_kLineState == KLineConnectionState.Connected) throw new InvalidOperationException("K-Line session is already active. Disconnect first."); await Task.Run(() => { Report(10, "Connecting to K-Line interface..."); var iface = new FtdiInterface(port, KLineBaudRate); try { ct.ThrowIfCancellationRequested(); var kwpCommon = new KwpCommon(iface); kwpCommon.WakeUp(EcuInitAddress); var kwp = new KW1281Connection(kwpCommon); Report(50, "Reading ECU identification..."); kwp.ReadEcuInfo(); // Store session objects. _sessionIface = iface; _sessionKwpCommon = kwpCommon; _sessionKwp = kwp; _connectedPort = port; Report(100, "K-Line session established."); _log.Info(LogId, $"Persistent session opened on {port}"); } catch { iface.Dispose(); throw; } }, ct); SetState(KLineConnectionState.Connected); StartKeepAlive(); } /// public void Disconnect() { StopKeepAlive(); _busLock.Wait(); try { if (_sessionKwp != null) { try { _sessionKwp.EndCommunication(); } catch (Exception ex) { _log.Warning(LogId, $"EndCommunication on disconnect: {ex.Message}"); } } CleanupSession(); } finally { _busLock.Release(); } SetState(KLineConnectionState.Disconnected); _log.Info(LogId, "Persistent session disconnected."); } /// public void Dispose() { StopKeepAlive(); CleanupSession(); _busLock.Dispose(); } // ── IKwpService: full read ──────────────────────────────────────────────── /// public async Task> ReadAllInfoAsync( string port, int pumpVersion, CancellationToken ct = default) { // If a persistent session is already active, reuse it — // skip the slow WakeUp + ReadEcuInfo and keep the session alive afterward. if (_kLineState == KLineConnectionState.Connected) return await Task.Run(() => ReadAllInfoWithSession(pumpVersion, ct), ct); var result = await Task.Run(() => ReadAllInfo(port, pumpVersion, ct), ct); // On a successful fresh read, promote the transient session to a // persistent one and start the keep-alive loop so the indicator // turns green and subsequent operations can reuse the connection. if (_sessionKwp != null) { SetState(KLineConnectionState.Connected); StartKeepAlive(); } return result; } private Dictionary ReadAllInfo(string port, int pumpVersion, CancellationToken ct) { var result = new Dictionary { [KlineKeys.Result] = "0" }; FtdiInterface? iface = null; bool promoteSession = false; try { Report(10, "Connecting to K-Line interface..."); iface = new FtdiInterface(port, KLineBaudRate); ct.ThrowIfCancellationRequested(); var kwpCommon = new KwpCommon(iface); kwpCommon.WakeUp(EcuInitAddress); var kwp = new KW1281Connection(kwpCommon); Report(20, "Connected. Reading ECU identification..."); var ecuInfo = pumpVersion == 2 ? kwp.ReadEcuInfoCustom(5) : kwp.ReadEcuInfo(); // ECU text layout (each field is 10 chars, positions are 0-based): // 0-11 Model Reference // 12-21 Data Record // 22-31 SW Version 1 // 32-41 SW Version 2 (pump v2+) // 42-51 Pump Control (pump v2+) string text = ecuInfo.Text; result[KlineKeys.ModelReference] = SafeSubstring(text, 0, 12).Trim(); result[KlineKeys.DataRecord] = SafeSubstring(text, 12, 10).Trim(); result[KlineKeys.SwVersion1] = SafeSubstring(text, 22, 10).Trim(); if (text.Length > 40) result[KlineKeys.SwVersion2] = SafeSubstring(text, 32, 10).Trim(); if (text.Length > 50) result[KlineKeys.PumpControl] = SafeSubstring(text, 42, 10).Trim(); ct.ThrowIfCancellationRequested(); // Unlock EEPROM for the given pump variant. if (pumpVersion == 2) kwp.SendCustom(new List { 0x18, 0x00, 0x01, 0x53, 0x72 }); // Version-specific session unlock — moved before ROM reads so the // pump identifier can be obtained as early as possible. kwp.KeepAlive(); switch (pumpVersion) { case 0: kwp.SendCustom(new List { 0x18, 0x00, 0x00, 0x82, 0x33 }); break; case 1: kwp.SendCustom(new List { 0x18, 0x00, 0x01, 0x72, 0x53 }); break; case 2: kwp.SendCustom(new List { 0x18, 0x00, 0x01, 0x53, 0x72 }); break; } // Read the ROM base address once (0xC6 command). Both the pump // identifier and the V2 customer-change index derive from it. Report(40, "Reading pump identifier..."); kwp.KeepAlive(); ushort baseAddr = ReadBaseRomAddress(kwp); ushort identAddr = (ushort)(baseAddr >= 10 ? baseAddr - 10 : 0); string ident = identAddr != 0 ? ReadRomString(kwp, identAddr, 10) : string.Empty; result[KlineKeys.PumpId] = ident; // Notify subscribers immediately so the pump definition and its // tests can start loading while the K-Line read continues. if (!string.IsNullOrEmpty(ident)) PumpIdentified?.Invoke(ident); Report(55, "Reading customer change index..."); kwp.KeepAlive(); ushort custChangeAddr; if (pumpVersion == 2) { // Reuse the base address from the 0xC6 response. custChangeAddr = (ushort)(baseAddr >= 0x1D ? baseAddr - 0x1D : 0); } else { custChangeAddr = ReadCustomerChangeAddressNonV2(kwp); } string custChangeIndex = custChangeAddr != 0 ? ReadRomString(kwp, custChangeAddr, 6) : string.Empty; result[KlineKeys.ModelIndex] = custChangeIndex; Report(65, "Reading DFI calibration value..."); kwp.KeepAlive(); kwp.SendCustom(new List { 0x18, 0x00, 0x03, 0xFF, 0xFF }); kwp.KeepAlive(); // EEPROM address 0x0044 holds the signed DFI byte. // DFI (degrees) = (signed_byte × 3) / 256 var dfiPackets = kwp.SendCustom(new List { 0x19, 0x02, 0x00, 0x44 }); double dfi = 0; foreach (var pkt in dfiPackets) { if (pkt is ReadEepromResponsePacket && pkt.Body.Count > 0) { dfi = ((sbyte)pkt.Body[0] * 3.0) / 256.0; break; } } result[KlineKeys.Dfi] = dfi.ToString(System.Globalization.CultureInfo.InvariantCulture); // Notify subscribers so the DFI slider updates in real time. DfiRead?.Invoke(dfi); Report(75, "Reading serial number..."); kwp.KeepAlive(); // EEPROM 0x0080, 6 bytes = ASCII serial number string serial = ReadEepromString(kwp, new List { 0x19, 0x06, 0x00, 0x80 }); result[KlineKeys.SerialNumber] = serial; Report(85, "Reading fault codes..."); kwp.KeepAlive(); var faultCodes = kwp.ReadFaultCodes(); result[KlineKeys.Errors] = faultCodes.Count > 0 ? string.Join(Environment.NewLine, faultCodes) : KlineKeys.NoErrors; Report(90, "Enabling signal..."); kwp.KeepAlive(); kwp.SendCustom(new List { 0x00 }); if (pumpVersion != 2) { kwp.SendCustom(new List { 0x02, 0x88, 0x01, 0x04, 0x06, 0x01 }); } else { kwp.SendCustom(new List { 0x02, 0x55, 0x01, 0x04, 0x06, 0x01 }); kwp.SendCustom(new List { 0x01, 0x02, 0x00, 0xC6 }); for (int i = 0; i < 10; i++) kwp.KeepAlive(); } kwp.KeepAlive(); // Promote the connection to a persistent session instead of // closing it. The caller starts the keep-alive loop afterward. _sessionIface = iface; _sessionKwpCommon = kwpCommon; _sessionKwp = kwp; _connectedPort = port; promoteSession = true; result[KlineKeys.Result] = "1"; _log.Info(LogId, $"ReadAllInfo complete — session promoted to persistent on {port}"); } catch (OperationCanceledException) { result[KlineKeys.ConnectError] = "Cancelled"; } catch (Exception ex) { result[KlineKeys.ConnectError] = ex.Message; _log.Error(LogId, $"ReadAllInfo exception: {ex}"); } finally { // Only dispose if we did NOT promote the session. if (!promoteSession) iface?.Dispose(); } return result; } /// /// Session-aware variant of . Reuses the persistent /// K-Line session, skipping WakeUp and ReadEcuInfo. The session stays alive /// afterward (no EndCommunication). /// private Dictionary ReadAllInfoWithSession(int pumpVersion, CancellationToken ct) { var result = new Dictionary { [KlineKeys.Result] = "0" }; _busLock.Wait(ct); try { var kwp = _sessionKwp!; Report(20, "Reading pump data (session active)..."); ct.ThrowIfCancellationRequested(); // Unlock EEPROM for the given pump variant. if (pumpVersion == 2) kwp.SendCustom(new List { 0x18, 0x00, 0x01, 0x53, 0x72 }); // Version-specific session unlock. kwp.KeepAlive(); switch (pumpVersion) { case 0: kwp.SendCustom(new List { 0x18, 0x00, 0x00, 0x82, 0x33 }); break; case 1: kwp.SendCustom(new List { 0x18, 0x00, 0x01, 0x72, 0x53 }); break; case 2: kwp.SendCustom(new List { 0x18, 0x00, 0x01, 0x53, 0x72 }); break; } // Read the ROM base address once (0xC6 command). Report(40, "Reading pump identifier..."); kwp.KeepAlive(); ushort baseAddr = ReadBaseRomAddress(kwp); ushort identAddr = (ushort)(baseAddr >= 10 ? baseAddr - 10 : 0); string ident = identAddr != 0 ? ReadRomString(kwp, identAddr, 10) : string.Empty; result[KlineKeys.PumpId] = ident; if (!string.IsNullOrEmpty(ident)) PumpIdentified?.Invoke(ident); Report(55, "Reading customer change index..."); kwp.KeepAlive(); ushort custChangeAddr; if (pumpVersion == 2) { custChangeAddr = (ushort)(baseAddr >= 0x1D ? baseAddr - 0x1D : 0); } else { custChangeAddr = ReadCustomerChangeAddressNonV2(kwp); } string custChangeIndex = custChangeAddr != 0 ? ReadRomString(kwp, custChangeAddr, 6) : string.Empty; result[KlineKeys.ModelIndex] = custChangeIndex; Report(65, "Reading DFI calibration value..."); kwp.KeepAlive(); kwp.SendCustom(new List { 0x18, 0x00, 0x03, 0xFF, 0xFF }); kwp.KeepAlive(); var dfiPackets = kwp.SendCustom(new List { 0x19, 0x02, 0x00, 0x44 }); double dfi = 0; foreach (var pkt in dfiPackets) { if (pkt is ReadEepromResponsePacket && pkt.Body.Count > 0) { dfi = ((sbyte)pkt.Body[0] * 3.0) / 256.0; break; } } result[KlineKeys.Dfi] = dfi.ToString(System.Globalization.CultureInfo.InvariantCulture); DfiRead?.Invoke(dfi); Report(75, "Reading serial number..."); kwp.KeepAlive(); string serial = ReadEepromString(kwp, new List { 0x19, 0x06, 0x00, 0x80 }); result[KlineKeys.SerialNumber] = serial; Report(85, "Reading fault codes..."); kwp.KeepAlive(); var faultCodes = kwp.ReadFaultCodes(); result[KlineKeys.Errors] = faultCodes.Count > 0 ? string.Join(Environment.NewLine, faultCodes) : KlineKeys.NoErrors; Report(90, "Enabling signal..."); kwp.KeepAlive(); kwp.SendCustom(new List { 0x00 }); if (pumpVersion != 2) { kwp.SendCustom(new List { 0x02, 0x88, 0x01, 0x04, 0x06, 0x01 }); } else { kwp.SendCustom(new List { 0x02, 0x55, 0x01, 0x04, 0x06, 0x01 }); kwp.SendCustom(new List { 0x01, 0x02, 0x00, 0xC6 }); for (int i = 0; i < 10; i++) kwp.KeepAlive(); } kwp.KeepAlive(); // No EndCommunication — keep session alive. result[KlineKeys.Result] = "1"; } catch (OperationCanceledException) { result[KlineKeys.ConnectError] = "Cancelled"; } catch (Exception ex) { result[KlineKeys.ConnectError] = ex.Message; _log.Error(LogId, $"ReadAllInfo (session): {ex}"); CleanupSession(); SetState(KLineConnectionState.Failed); } finally { _busLock.Release(); } return result; } // ── IKwpService: DTC operations ─────────────────────────────────────────── /// public async Task ReadFaultCodesAsync(string port, CancellationToken ct = default) { if (_kLineState == KLineConnectionState.Connected) return await Task.Run(() => ReadFaultCodesWithSession(ct), ct); return await Task.Run(() => { FtdiInterface? iface = null; try { Report(25, "Connecting..."); iface = new FtdiInterface(port, KLineBaudRate); var kwpCommon1 = new KwpCommon(iface); kwpCommon1.WakeUp(EcuInitAddress); var kwp = new KW1281Connection(kwpCommon1); kwp.ReadEcuInfo(); kwp.KeepAlive(); Report(75, "Reading fault codes..."); var codes = kwp.ReadFaultCodes(); kwp.KeepAlive(); kwp.EndCommunication(); Report(100, "Done."); return codes.Count > 0 ? string.Join(Environment.NewLine, codes) : KlineKeys.NoErrors; } catch (Exception ex) { _log.Error(LogId, $"ReadFaultCodes: {ex.Message}"); return $"Error: {ex.Message}"; } finally { iface?.Dispose(); } }, ct); } /// public async Task ClearFaultCodesAsync(string port, CancellationToken ct = default) { if (_kLineState == KLineConnectionState.Connected) return await Task.Run(() => ClearFaultCodesWithSession(ct), ct); return await Task.Run(() => { FtdiInterface? iface = null; try { Report(25, "Connecting..."); iface = new FtdiInterface(port, KLineBaudRate); var kwpCommon2 = new KwpCommon(iface); kwpCommon2.WakeUp(EcuInitAddress); var kwp = new KW1281Connection(kwpCommon2); kwp.ReadEcuInfo(); kwp.KeepAlive(); Report(60, "Clearing fault codes..."); kwp.ClearFaultCodes(); kwp.KeepAlive(); var codes = kwp.ReadFaultCodes(); kwp.KeepAlive(); kwp.EndCommunication(); Report(100, "Done."); return codes.Count > 0 ? string.Join(Environment.NewLine, codes) : KlineKeys.NoErrors; } catch (Exception ex) { _log.Error(LogId, $"ClearFaultCodes: {ex.Message}"); return $"Error: {ex.Message}"; } finally { iface?.Dispose(); } }, ct); } // ── IKwpService: DFI operations ─────────────────────────────────────────── /// public async Task ReadDfiAsync(string port, CancellationToken ct = default) { if (_kLineState == KLineConnectionState.Connected) return await Task.Run(() => ReadDfiWithSession(ct), ct); return await Task.Run(() => { FtdiInterface? iface = null; try { Report(15, "Connecting..."); iface = new FtdiInterface(port, KLineBaudRate); var kwpCommon3 = new KwpCommon(iface); kwpCommon3.WakeUp(EcuInitAddress); var kwp = new KW1281Connection(kwpCommon3); Report(45, "Reading ECU info..."); kwp.ReadEcuInfo(); kwp.KeepAlive(); kwp.SendCustom(new List { 0x18, 0x00, 0x03, 0xFF, 0xFF }); kwp.KeepAlive(); var packets = kwp.SendCustom(new List { 0x19, 0x02, 0x00, 0x44 }); double dfi = 0; foreach (var pkt in packets) if (pkt is ReadEepromResponsePacket && pkt.Body.Count > 0) { dfi = ((sbyte)pkt.Body[0] * 3.0) / 256.0; break; } Report(95, "Closing session..."); kwp.KeepAlive(); kwp.EndCommunication(); Report(100, "Done."); return dfi.ToString(System.Globalization.CultureInfo.InvariantCulture); } catch (Exception ex) { _log.Error(LogId, $"ReadDfi: {ex.Message}"); return "0"; } finally { iface?.Dispose(); } }, ct); } /// public async Task WriteDfiAsync(string port, float dfi, int version, CancellationToken ct = default) { if (_kLineState == KLineConnectionState.Connected) return await Task.Run(() => WriteDfiWithSession(dfi, version, ct), ct); return await Task.Run(() => WriteDfiInternal(port, dfi, version, closeSession: true), ct); } /// public async Task WriteDfiAndRestartAsync(string port, float dfi, int version, CancellationToken ct = default) { string result; if (_kLineState == KLineConnectionState.Connected) { result = await Task.Run(() => WriteDfiWithSession(dfi, version, ct), ct); // Pump power will be cycled — the session is dead after this. Disconnect(); } else { result = await Task.Run(() => WriteDfiInternal(port, dfi, version, closeSession: true), ct); } PumpDisconnectRequested?.Invoke(); await Task.Delay(1000, ct); PumpReconnectRequested?.Invoke(); return result; } // ── IKwpService: device detection ──────────────────────────────────────── /// public string? DetectKLinePort() { try { uint count = FtdiInterface.GetDevicesCount(); _log.Info(LogId, $"FTDI device count: {count}"); if (count == 0) return null; var list = new FT_DEVICE_INFO_NODE[count]; FtdiInterface.GetDeviceList(list); var serial = list[0].SerialNumber; _log.Info(LogId, $"Selected FTDI device: Serial={serial}, Desc={list[0].Description}"); return serial; } catch (Exception ex) { _log.Warning(LogId, $"DetectKLinePort: {ex.Message}"); return null; } } // ── Keep-alive loop ─────────────────────────────────────────────────────── private void StartKeepAlive() { _keepAliveCts = new CancellationTokenSource(); _keepAliveTask = Task.Run(() => KeepAliveLoop(_keepAliveCts.Token)); } private void StopKeepAlive() { if (_keepAliveCts == null) return; _keepAliveCts.Cancel(); try { _keepAliveTask?.Wait(); } catch (AggregateException) { } _keepAliveCts.Dispose(); _keepAliveCts = null; _keepAliveTask = null; } private async Task KeepAliveLoop(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { await Task.Delay(KeepAliveIntervalMs, ct); } catch (OperationCanceledException) { return; } // Non-blocking try-acquire: if an operation holds the lock // we skip this cycle — the operation itself keeps the bus alive. if (!await _busLock.WaitAsync(0, ct)) continue; try { _sessionKwp!.KeepAlive(); } catch (OperationCanceledException) { return; } catch (Exception ex) { _log.Error(LogId, $"Keep-alive failed: {ex.Message}"); CleanupSession(); SetState(KLineConnectionState.Failed); return; } finally { _busLock.Release(); } } } // ── Session state helpers ───────────────────────────────────────────────── private void SetState(KLineConnectionState newState) { if (_kLineState == newState) return; _kLineState = newState; KLineStateChanged?.Invoke(newState); } private void CleanupSession() { _sessionIface?.Dispose(); _sessionIface = null; _sessionKwpCommon = null; _sessionKwp = null; _connectedPort = null; } // ── Session-aware operation helpers ──────────────────────────────────────── private string ReadFaultCodesWithSession(CancellationToken ct) { _busLock.Wait(ct); try { Report(50, "Reading fault codes..."); _sessionKwp!.KeepAlive(); var codes = _sessionKwp.ReadFaultCodes(); _sessionKwp.KeepAlive(); Report(100, "Done."); return codes.Count > 0 ? string.Join(Environment.NewLine, codes) : KlineKeys.NoErrors; } catch (Exception ex) { _log.Error(LogId, $"ReadFaultCodes (session): {ex.Message}"); CleanupSession(); SetState(KLineConnectionState.Failed); return $"Error: {ex.Message}"; } finally { _busLock.Release(); } } private string ClearFaultCodesWithSession(CancellationToken ct) { _busLock.Wait(ct); try { Report(40, "Clearing fault codes..."); _sessionKwp!.KeepAlive(); _sessionKwp.ClearFaultCodes(); _sessionKwp.KeepAlive(); Report(70, "Reading fault codes..."); var codes = _sessionKwp.ReadFaultCodes(); _sessionKwp.KeepAlive(); Report(100, "Done."); return codes.Count > 0 ? string.Join(Environment.NewLine, codes) : KlineKeys.NoErrors; } catch (Exception ex) { _log.Error(LogId, $"ClearFaultCodes (session): {ex.Message}"); CleanupSession(); SetState(KLineConnectionState.Failed); return $"Error: {ex.Message}"; } finally { _busLock.Release(); } } private string ReadDfiWithSession(CancellationToken ct) { _busLock.Wait(ct); try { Report(30, "Reading DFI calibration..."); _sessionKwp!.KeepAlive(); _sessionKwp.SendCustom(new List { 0x18, 0x00, 0x03, 0xFF, 0xFF }); _sessionKwp.KeepAlive(); var packets = _sessionKwp.SendCustom(new List { 0x19, 0x02, 0x00, 0x44 }); double dfi = 0; foreach (var pkt in packets) if (pkt is ReadEepromResponsePacket && pkt.Body.Count > 0) { dfi = ((sbyte)pkt.Body[0] * 3.0) / 256.0; break; } _sessionKwp.KeepAlive(); Report(100, "Done."); return dfi.ToString(System.Globalization.CultureInfo.InvariantCulture); } catch (Exception ex) { _log.Error(LogId, $"ReadDfi (session): {ex.Message}"); CleanupSession(); SetState(KLineConnectionState.Failed); return "0"; } finally { _busLock.Release(); } } private string WriteDfiWithSession(float dfi, int version, CancellationToken ct) { _busLock.Wait(ct); double newDfi = 0; try { var passPacket = version switch { 1 => new List { 0x18, 0x00, 0x03, 0x2F, 0xFF, 0x30, 0x35, 0x30, 0x30, 0x30, 0x31, 0x1C, 0x09, 0x04 }, 2 or 3 => new List { 0x18, 0x00, 0x03, 0xFF, 0xF2, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 }, _ => new List { 0x18, 0x00, 0x03, 0x2F, 0xFF, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 } }; Report(30, "Authenticating and writing DFI..."); _sessionKwp!.KeepAlive(); _sessionKwp.SendCustom(passPacket); _sessionKwp.KeepAlive(); sbyte rawValue = (sbyte)((dfi * 256.0f) / 3.0f); if (rawValue == 0) rawValue = 1; byte checksum = (byte)(0 - (byte)rawValue); _sessionKwp.SendCustom(new List { 0x1A, 0x02, 0x00, 0x44, (byte)rawValue, checksum, 0x03 }); _sessionKwp.KeepAlive(); Report(60, "Verifying write..."); _sessionKwp.SendCustom(new List { 0x18, 0x00, 0x03, 0xFF, 0xFF }); _sessionKwp.KeepAlive(); var packets = _sessionKwp.SendCustom(new List { 0x19, 0x02, 0x00, 0x44 }); foreach (var pkt in packets) if (pkt is ReadEepromResponsePacket && pkt.Body.Count > 0) { newDfi = ((sbyte)pkt.Body[0] * 3.0) / 256.0; break; } _sessionKwp.KeepAlive(); Report(100, "Done."); } catch (Exception ex) { _log.Error(LogId, $"WriteDfi (session): {ex.Message}"); CleanupSession(); SetState(KLineConnectionState.Failed); } finally { _busLock.Release(); } return newDfi.ToString(System.Globalization.CultureInfo.InvariantCulture); } // ── Private helpers ─────────────────────────────────────────────────────── private string WriteDfiInternal(string port, float dfi, int version, bool closeSession) { FtdiInterface? iface = null; double newDfi = 0; try { Report(10, "Connecting..."); iface = new FtdiInterface(port, KLineBaudRate); var kwpCommon4 = new KwpCommon(iface); kwpCommon4.WakeUp(EcuInitAddress); var kwp = new KW1281Connection(kwpCommon4); Report(30, "Reading ECU info..."); kwp.ReadEcuInfo(); kwp.KeepAlive(); // Select the correct authentication password packet for the pump version. // These byte sequences were established by reverse engineering the original firmware. var passPacket = version switch { //1 => new List { 0x18, 0x00, 0x03, 0x2F, 0xF2, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 }, 1 => new List { 0x18, 0x00, 0x03, 0x2F, 0xFF, 0x30, 0x35, 0x30, 0x30, 0x30, 0x31, 0x1C, 0x09, 0x04 }, 2 or 3 => new List { 0x18, 0x00, 0x03, 0xFF, 0xF2, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 }, _ => new List { 0x18, 0x00, 0x03, 0x2F, 0xFF, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 } // V1 }; Report(50, "Authenticating and writing DFI..."); kwp.SendCustom(passPacket); kwp.KeepAlive(); // Encode DFI: signed_byte = (dfi × 256) / 3 // A zero raw byte is not accepted by the ECU — use 1 instead. sbyte rawValue = (sbyte)((dfi * 256.0f) / 3.0f); if (rawValue == 0) rawValue = 1; byte checksum = (byte)(0 - (byte)rawValue); // one's complement checksum var returnpacket = kwp.SendCustom(new List { 0x1A, 0x02, 0x00, 0x44, (byte)rawValue, checksum, 0x03 }); kwp.KeepAlive(); //2 0 68 255 2 0 44 ff Report(60, "Verifying write..."); kwp.SendCustom(new List { 0x18, 0x00, 0x03, 0xFF, 0xFF }); kwp.KeepAlive(); var packets = kwp.SendCustom(new List { 0x19, 0x02, 0x00, 0x44 }); foreach (var pkt in packets) if (pkt is ReadEepromResponsePacket && pkt.Body.Count > 0) { newDfi = ((sbyte)pkt.Body[0] * 3.0) / 256.0; break; } Report(70, "Closing session..."); kwp.KeepAlive(); if (closeSession) kwp.EndCommunication(); } catch (Exception ex) { _log.Error(LogId, $"WriteDfi: {ex.Message}"); } finally { iface?.Dispose(); } return newDfi.ToString(System.Globalization.CultureInfo.InvariantCulture); } /// /// Sends the ROM address lookup command {0x01, 0x02, 0x00, 0xC6} once and /// returns the raw 16-bit base address. Both the pump identifier (base − 10) /// and the V2 customer-change index (base − 0x1D) derive from this value. /// private ushort ReadBaseRomAddress(KW1281Connection kwp) { var packets = kwp.SendCustom(new List { 0x01, 0x02, 0x00, 0xC6 }); foreach (var pkt in packets) if (pkt.Body.Count > 1) return (ushort)((pkt.Body[1] << 8) | pkt.Body[0]); return 0; } /// /// Reads the customer-change ROM address for non-V2 pumps using /// the legacy ROM pointer at 0x9FFE. /// private ushort ReadCustomerChangeAddressNonV2(KW1281Connection kwp) { var data = kwp.ReadRomEeprom(0x9FFE, 2); if (data == null || data.Count < 2) return 0; return (ushort)(((data[1] << 8) | data[0]) + 3); } private string ReadRomString(KW1281Connection kwp, ushort address, byte count) { var data = kwp.ReadRomEeprom(address, count); if (data == null || data.Count == 0) return string.Empty; var sb = new System.Text.StringBuilder(); foreach (var b in data) sb.Append(Convert.ToChar(b)); return sb.ToString(); } private string ReadEepromString(KW1281Connection kwp, List command) { var packets = kwp.SendCustom(command); foreach (var pkt in packets) { if (pkt is ReadEepromResponsePacket) { var sb = new System.Text.StringBuilder(); foreach (var b in pkt.Body) sb.Append(Convert.ToChar(b)); return sb.ToString(); } } return string.Empty; } private static string SafeSubstring(string s, int start, int length) { if (s.Length <= start) return string.Empty; int avail = Math.Min(length, s.Length - start); return s.Substring(start, avail); } private void Report(int percent, string message) => ProgressChanged?.Invoke(percent, message); } }