Files
HC_APTBS/Services/Impl/KwpService.cs
LucianoDev 197e9d1775 feat: redesign dashboard with Fluent KPI tiles, connection strip, and devices column
- Replace LCD-style readings with a 3×2 KPI tile grid (Fluent card surfaces, 52pt values)
- Add persistent top connection strip with horizontal chips + pump name badge
- Add elapsed test timer (DispatcherTimer, mm:ss) to Test Summary card
- Restyle Test Summary and Active Alarms with Fluent brushes/iconography
- Add Devices column (CAN / K-Line / Bench tiles) between KPI grid and test/alarms
  - Enumerates attached PCAN USB channels via PCAN_ATTACHED_CHANNELS API
  - Enumerates FTDI K-Line adapters via existing FtdiInterface helpers
  - Click to connect/disconnect; confirmation dialog when session active or test running
  - Hover tint: blue = will connect, red = will disconnect; Bench row is read-only stub
- Extend ICanService with SelectedChannel + EnumerateAttachedChannels()
- Expose IKwpService.ConnectedPort for active session device tracking
- Add DeviceRow button style with MultiDataTrigger hover colour logic
- Add 30+ new localization keys (ES + EN) for KPI labels, devices, confirmations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 22:25:00 +02:00

999 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// Implements <see cref="IKwpService"/> using the FTDI USB-to-K-Line adapter
/// and the KW1281 protocol stack from <see cref="HC_APTBS.Infrastructure.Kwp"/>.
///
/// <para>
/// The ECU initialisation address for all VP44 pumps is <c>0xF1</c> (broadcast).
/// K-Line baud rate is 9600 bps.
/// </para>
/// </summary>
public sealed class KwpService : IKwpService, IDisposable
{
// ── Protocol constants ────────────────────────────────────────────────────
/// <summary>ECU initialisation address used in the 5-baud wake-up sequence.</summary>
private const byte EcuInitAddress = 0xF1;
/// <summary>K-Line baud rate (bps) for all VP44 communications.</summary>
private const int KLineBaudRate = 9600;
/// <summary>Interval between keep-alive ACK packets (ms).</summary>
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 ────────────────────────────────────────────────────────────────
/// <inheritdoc/>
public event Action<int, string>? ProgressChanged;
/// <inheritdoc/>
public event Action<string>? PumpIdentified;
/// <inheritdoc/>
public event Action<double>? DfiRead;
/// <inheritdoc/>
public event Action? PumpDisconnectRequested;
/// <inheritdoc/>
public event Action? PumpReconnectRequested;
/// <inheritdoc/>
public event Action<KLineConnectionState>? KLineStateChanged;
// ── Session state ─────────────────────────────────────────────────────────
private KLineConnectionState _kLineState = KLineConnectionState.Disconnected;
/// <inheritdoc/>
public KLineConnectionState KLineState => _kLineState;
/// <inheritdoc/>
public string? ConnectedPort => _connectedPort;
// ── Constructor ───────────────────────────────────────────────────────────
/// <param name="logger">Application logger.</param>
public KwpService(IAppLogger logger)
{
_log = logger;
}
// ── IKwpService: session lifecycle ────────────────────────────────────────
/// <inheritdoc/>
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();
}
/// <inheritdoc/>
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.");
}
/// <inheritdoc/>
public void Dispose()
{
StopKeepAlive();
CleanupSession();
_busLock.Dispose();
}
// ── IKwpService: full read ────────────────────────────────────────────────
/// <inheritdoc/>
public async Task<Dictionary<string, string>> 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<string, string> ReadAllInfo(string port, int pumpVersion, CancellationToken ct)
{
var result = new Dictionary<string, string> { [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<byte> { 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<byte> { 0x18, 0x00, 0x00, 0x82, 0x33 }); break;
case 1: kwp.SendCustom(new List<byte> { 0x18, 0x00, 0x01, 0x72, 0x53 }); break;
case 2: kwp.SendCustom(new List<byte> { 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<byte> { 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<byte> { 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<byte> { 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<byte> { 0x00 });
if (pumpVersion != 2)
{
kwp.SendCustom(new List<byte> { 0x02, 0x88, 0x01, 0x04, 0x06, 0x01 });
}
else
{
kwp.SendCustom(new List<byte> { 0x02, 0x55, 0x01, 0x04, 0x06, 0x01 });
kwp.SendCustom(new List<byte> { 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;
}
/// <summary>
/// Session-aware variant of <see cref="ReadAllInfo"/>. Reuses the persistent
/// K-Line session, skipping WakeUp and ReadEcuInfo. The session stays alive
/// afterward (no EndCommunication).
/// </summary>
private Dictionary<string, string> ReadAllInfoWithSession(int pumpVersion, CancellationToken ct)
{
var result = new Dictionary<string, string> { [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<byte> { 0x18, 0x00, 0x01, 0x53, 0x72 });
// Version-specific session unlock.
kwp.KeepAlive();
switch (pumpVersion)
{
case 0: kwp.SendCustom(new List<byte> { 0x18, 0x00, 0x00, 0x82, 0x33 }); break;
case 1: kwp.SendCustom(new List<byte> { 0x18, 0x00, 0x01, 0x72, 0x53 }); break;
case 2: kwp.SendCustom(new List<byte> { 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<byte> { 0x18, 0x00, 0x03, 0xFF, 0xFF });
kwp.KeepAlive();
var dfiPackets = kwp.SendCustom(new List<byte> { 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<byte> { 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<byte> { 0x00 });
if (pumpVersion != 2)
{
kwp.SendCustom(new List<byte> { 0x02, 0x88, 0x01, 0x04, 0x06, 0x01 });
}
else
{
kwp.SendCustom(new List<byte> { 0x02, 0x55, 0x01, 0x04, 0x06, 0x01 });
kwp.SendCustom(new List<byte> { 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 ───────────────────────────────────────────
/// <inheritdoc/>
public async Task<string> 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);
}
/// <inheritdoc/>
public async Task<string> 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 ───────────────────────────────────────────
/// <inheritdoc/>
public async Task<string> 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<byte> { 0x18, 0x00, 0x03, 0xFF, 0xFF });
kwp.KeepAlive();
var packets = kwp.SendCustom(new List<byte> { 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);
}
/// <inheritdoc/>
public async Task<string> 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);
}
/// <inheritdoc/>
public async Task<string> 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: fast immobilizer unlock ──────────────────────────────────
/// <inheritdoc/>
public async Task<bool> TryFastUnlockAsync()
{
if (_kLineState != KLineConnectionState.Connected || _sessionKwp == null)
{
_log.Info(LogId, "TryFastUnlock: no active K-Line session — skipping");
return false;
}
return await Task.Run(() =>
{
_busLock.Wait();
try
{
_log.Info(LogId, "TryFastUnlock: sending unlock command over K-Line");
var packets = _sessionKwp!.SendCustom(
new List<byte> { 0x02, 0x88, 0x02, 0x03, 0xA8, 0x01, 0x00 });
bool nak = packets.Count == 1
&& packets[0] is NakPacket;
_log.Info(LogId, $"TryFastUnlock: {(nak ? "NAK pump rejected" : "ACK pump unlocked")}");
return !nak;
}
catch (Exception ex)
{
_log.Warning(LogId, $"TryFastUnlock failed: {ex.Message}");
return false;
}
finally
{
_busLock.Release();
}
});
}
// ── IKwpService: device detection ────────────────────────────────────────
/// <inheritdoc/>
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)
{
// 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))
{
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();
}
}
try
{
await Task.Delay(KeepAliveIntervalMs, ct);
}
catch (OperationCanceledException) { return; }
}
}
// ── 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<byte> { 0x18, 0x00, 0x03, 0xFF, 0xFF });
_sessionKwp.KeepAlive();
var packets = _sessionKwp.SendCustom(new List<byte> { 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<byte> { 0x18, 0x00, 0x03, 0x2F, 0xFF, 0x30, 0x35, 0x30, 0x30, 0x30, 0x31, 0x1C, 0x09, 0x04 },
2 or 3 => new List<byte> { 0x18, 0x00, 0x03, 0xFF, 0xF2, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 },
_ => new List<byte> { 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<byte> { 0x1A, 0x02, 0x00, 0x44, (byte)rawValue, checksum, 0x03 });
_sessionKwp.KeepAlive();
Report(60, "Verifying write...");
_sessionKwp.SendCustom(new List<byte> { 0x18, 0x00, 0x03, 0xFF, 0xFF });
_sessionKwp.KeepAlive();
var packets = _sessionKwp.SendCustom(new List<byte> { 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<byte> { 0x18, 0x00, 0x03, 0x2F, 0xF2, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 },
1 => new List<byte> { 0x18, 0x00, 0x03, 0x2F, 0xFF, 0x30, 0x35, 0x30, 0x30, 0x30, 0x31, 0x1C, 0x09, 0x04 },
2 or 3 => new List<byte> { 0x18, 0x00, 0x03, 0xFF, 0xF2, 0x4B, 0x48, 0x54, 0x43, 0x41, 0x38, 0x47, 0x30, 0x45 },
_ => new List<byte> { 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<byte> { 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<byte> { 0x18, 0x00, 0x03, 0xFF, 0xFF });
kwp.KeepAlive();
var packets = kwp.SendCustom(new List<byte> { 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);
}
/// <summary>
/// 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.
/// </summary>
private ushort ReadBaseRomAddress(KW1281Connection kwp)
{
var packets = kwp.SendCustom(new List<byte> { 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;
}
/// <summary>
/// Reads the customer-change ROM address for non-V2 pumps using
/// the legacy ROM pointer at 0x9FFE.
/// </summary>
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<byte> 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);
}
}