initial commit
This commit is contained in:
463
Services/Impl/KwpService.cs
Normal file
463
Services/Impl/KwpService.cs
Normal file
@@ -0,0 +1,463 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user