Files
HC_APTBS/Services/Impl/KwpService.cs
2026-04-11 12:45:18 +02:00

464 lines
20 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
{
// ── 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;
private readonly IAppLogger _log;
private const string LogId = "KwpService";
// ── Events ────────────────────────────────────────────────────────────────
/// <inheritdoc/>
public event Action<int, string>? ProgressChanged;
/// <inheritdoc/>
public event Action? PumpDisconnectRequested;
/// <inheritdoc/>
public event Action? PumpReconnectRequested;
// ── Constructor ───────────────────────────────────────────────────────────
/// <param name="logger">Application logger.</param>
public KwpService(IAppLogger logger)
{
_log = logger;
}
// ── 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);
}
private Dictionary<string, string> ReadAllInfo(string port, int pumpVersion, CancellationToken ct)
{
var result = new Dictionary<string, string> { [KlineKeys.Result] = "0" };
FtdiInterface? iface = null;
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();
// 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...");
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);
// 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;
}
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...");
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...");
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();
kwp.EndCommunication();
result[KlineKeys.Result] = "1";
}
catch (OperationCanceledException)
{
result[KlineKeys.ConnectError] = "Cancelled";
}
catch (Exception ex)
{
result[KlineKeys.ConnectError] = ex.Message;
_log.Error(LogId, $"ReadAllInfo exception: {ex}");
}
finally
{
iface?.Dispose();
}
return result;
}
// ── IKwpService: DTC operations ───────────────────────────────────────────
/// <inheritdoc/>
public async Task<string> ReadFaultCodesAsync(string port, CancellationToken ct = default)
{
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)
{
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)
{
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)
{
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);
PumpDisconnectRequested?.Invoke();
await Task.Delay(1000, ct);
PumpReconnectRequested?.Invoke();
return result;
}
// ── 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;
}
}
// ── 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);
}
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)
{
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 0;
}
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);
}
}