feat: redesign bench calibration (factor/offset), add Ttank/P2 displays, fix sensor calibration
- Replace P1-P6 rational transfer function with factor/offset model for bench params - Add explicit rx/tx direction flags in bench XML configuration - Add T.Tank (BenchTemp) and P2 (AnalogSensor2) to temperature/pressure display - Apply SensorConfiguration calibration to pressure channels, fix empty sensors.xml fallback - Add live value labels to flowmeter charts - Hide pump live values and PSG encoder standalone label - Add K-Line connection state model, improve KWP service and status displays - Restructure .claude/skills into subdirectory format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ namespace HC_APTBS.Services.Impl
|
||||
/// K-Line baud rate is 9600 bps.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class KwpService : IKwpService
|
||||
public sealed class KwpService : IKwpService, IDisposable
|
||||
{
|
||||
// ── Protocol constants ────────────────────────────────────────────────────
|
||||
|
||||
@@ -27,20 +27,52 @@ namespace HC_APTBS.Services.Impl
|
||||
/// <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;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <param name="logger">Application logger.</param>
|
||||
@@ -49,19 +81,110 @@ namespace HC_APTBS.Services.Impl
|
||||
_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)
|
||||
{
|
||||
return await Task.Run(() => ReadAllInfo(port, pumpVersion, ct), ct);
|
||||
// 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
|
||||
{
|
||||
@@ -91,21 +214,54 @@ namespace HC_APTBS.Services.Impl
|
||||
if (text.Length > 40) result[KlineKeys.SwVersion2] = SafeSubstring(text, 32, 10).Trim();
|
||||
if (text.Length > 50) result[KlineKeys.PumpControl] = SafeSubstring(text, 42, 10).Trim();
|
||||
|
||||
// Read diagnostic trouble codes.
|
||||
kwp.KeepAlive();
|
||||
Report(30, "Reading fault codes...");
|
||||
var faultCodes = kwp.ReadFaultCodes();
|
||||
result[KlineKeys.Errors] = faultCodes.Count > 0
|
||||
? string.Join(Environment.NewLine, faultCodes)
|
||||
: KlineKeys.NoErrors;
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Unlock EEPROM for the given pump variant.
|
||||
if (pumpVersion == 2)
|
||||
kwp.SendCustom(new List<byte> { 0x18, 0x00, 0x01, 0x53, 0x72 });
|
||||
|
||||
Report(40, "Reading DFI calibration value...");
|
||||
// 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();
|
||||
@@ -121,35 +277,23 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
result[KlineKeys.Dfi] = dfi.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Notify subscribers so the DFI slider updates in real time.
|
||||
DfiRead?.Invoke(dfi);
|
||||
|
||||
Report(60, "Reading customer change index...");
|
||||
kwp.KeepAlive();
|
||||
ushort custChangeAddr = ReadCustomerChangeAddress(kwp, pumpVersion);
|
||||
string custChangeIndex = ReadRomString(kwp, custChangeAddr, 6);
|
||||
|
||||
Report(80, "Reading pump identifier...");
|
||||
kwp.KeepAlive();
|
||||
ushort identAddr = ReadIdentAddress(kwp);
|
||||
string ident = identAddr != 0 ? ReadRomString(kwp, identAddr, 10) : string.Empty;
|
||||
|
||||
Report(90, "Reading serial number...");
|
||||
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.PumpId] = ident;
|
||||
result[KlineKeys.SerialNumber] = serial;
|
||||
result[KlineKeys.ModelIndex] = custChangeIndex;
|
||||
|
||||
Report(95, "Enabling signal and closing session...");
|
||||
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)
|
||||
@@ -163,9 +307,17 @@ namespace HC_APTBS.Services.Impl
|
||||
for (int i = 0; i < 10; i++) kwp.KeepAlive();
|
||||
}
|
||||
kwp.KeepAlive();
|
||||
kwp.EndCommunication();
|
||||
|
||||
// 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)
|
||||
{
|
||||
@@ -178,7 +330,130 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
finally
|
||||
{
|
||||
iface?.Dispose();
|
||||
// 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;
|
||||
@@ -189,6 +464,9 @@ namespace HC_APTBS.Services.Impl
|
||||
/// <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;
|
||||
@@ -222,6 +500,9 @@ namespace HC_APTBS.Services.Impl
|
||||
/// <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;
|
||||
@@ -259,6 +540,9 @@ namespace HC_APTBS.Services.Impl
|
||||
/// <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;
|
||||
@@ -297,13 +581,27 @@ namespace HC_APTBS.Services.Impl
|
||||
/// <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)
|
||||
{
|
||||
var result = await Task.Run(() => WriteDfiInternal(port, dfi, version, closeSession: true), ct);
|
||||
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();
|
||||
@@ -333,6 +631,204 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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<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)
|
||||
@@ -399,33 +895,31 @@ namespace HC_APTBS.Services.Impl
|
||||
return newDfi.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private ushort ReadCustomerChangeAddress(KW1281Connection kwp, int pumpVersion)
|
||||
{
|
||||
if (pumpVersion == 2)
|
||||
{
|
||||
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]) - 0x1D);
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var data = kwp.ReadRomEeprom(0x9FFE, 2);
|
||||
if (data == null || data.Count < 2) return 0;
|
||||
return (ushort)(((data[1] << 8) | data[0]) + 3);
|
||||
}
|
||||
}
|
||||
|
||||
private ushort ReadIdentAddress(KW1281Connection kwp)
|
||||
/// <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]) - 10);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user