initial commit

This commit is contained in:
2026-04-11 12:45:18 +02:00
commit 6e1b929e2f
1246 changed files with 177580 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
using HC_APTBS.Infrastructure.Kwp.Packets;
using System;
using System.Collections.Generic;
using System.Text;
namespace HC_APTBS.Infrastructure.Kwp
{
/// <summary>
/// The info returned by the controller to a ReadIdent packet.
/// </summary>
public class ControllerIdent
{
public ControllerIdent(IEnumerable<Packet> packets)
{
var sb = new StringBuilder();
foreach (var packet in packets)
{
if (packet is AsciiDataPacket asciiPacket)
{
sb.Append(asciiPacket);
}
else if (packet is CodingWscPacket codingWscPacket)
{
sb.AppendLine();
sb.Append(codingWscPacket);
}
else
{
System.Diagnostics.Debug.WriteLine($"ReadIdent returned packet of type {packet.GetType()}");
}
}
Text = sb.ToString();
}
public string Text { get; }
public override string ToString()
{
return Text;
}
}
}

View File

@@ -0,0 +1,53 @@
using HC_APTBS.Infrastructure.Kwp.Packets;
using System;
using System.Collections.Generic;
using System.Text;
namespace HC_APTBS.Infrastructure.Kwp
{
/// <summary>
/// The info returned when a controller wakes up.
/// </summary>
public class ControllerInfo
{
public ControllerInfo(IEnumerable<Packet> packets)
{
var sb = new StringBuilder();
foreach (var packet in packets)
{
if (packet is AsciiDataPacket asciiPacket)
{
sb.Append(asciiPacket);
if (asciiPacket.MoreDataAvailable)
{
MoreDataAvailable = true;
}
}
else if (packet is CodingWscPacket codingPacket)
{
sb.Append($"{Environment.NewLine}{codingPacket}");
SoftwareCoding = codingPacket.SoftwareCoding;
WorkshopCode = codingPacket.WorkshopCode;
}
else
{
System.Diagnostics.Debug.WriteLine($"Controller wakeup returned packet of type {packet.GetType()}");
}
}
Text = sb.ToString();
}
public string Text { get; }
public bool MoreDataAvailable { get; }
public int SoftwareCoding { get; }
public int WorkshopCode { get; }
public override string ToString()
{
return Text;
}
}
}

View File

@@ -0,0 +1,868 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.IO;
using System.Text;
namespace HC_APTBS.Infrastructure.Kwp
{
public class FT_DEVICE_INFO_NODE
{
/// <summary>
/// Indicates device state. Can be any combination of the following: FT_FLAGS_OPENED, FT_FLAGS_HISPEED
/// </summary>
public UInt32 Flags;
/// <summary>
/// Indicates the device type. Can be one of the following: FT_DEVICE_232R, FT_DEVICE_2232C, FT_DEVICE_BM, FT_DEVICE_AM, FT_DEVICE_100AX or FT_DEVICE_UNKNOWN
/// </summary>
public byte Type;
/// <summary>
/// The Vendor ID and Product ID of the device
/// </summary>
public UInt32 ID;
/// <summary>
/// The physical location identifier of the device
/// </summary>
public UInt32 LocId;
/// <summary>
/// The device serial number
/// </summary>
public string SerialNumber;
/// <summary>
/// The device description
/// </summary>
public string Description;
/// <summary>
/// The device handle. This value is not used externally and is provided for information only.
/// If the device is not open, this value is 0.
/// </summary>
public IntPtr ftHandle;
}
public class FtdiInterface : IInterface
{
private FT _ft = null;
private IntPtr _handle = IntPtr.Zero;
private readonly byte[] _buf = new byte[1];
public FtdiInterface(string serialNumber, int baudRate)
{
_ft = new FT();
var status = _ft.Open(
serialNumber, FT.OpenExFlags.BySerialNumber, out _handle);
FT.AssertOk(status);
status = _ft.SetBaudRate(_handle, (uint)baudRate);
FT.AssertOk(status);
status = _ft.SetDataCharacteristics(
_handle,
FT.Bits.Eight,
FT.StopBits.One,
FT.Parity.None);
FT.AssertOk(status);
status = _ft.SetFlowControl(
_handle,
FT.FlowControl.None, 0, 0);
FT.AssertOk(status);
status = _ft.ClrRts(_handle);
FT.AssertOk(status);
status = _ft.SetDtr(_handle);
FT.AssertOk(status);
status = _ft.SetTimeouts(
_handle,
500,
500);
FT.AssertOk(status);
// Should allow faster response times for small packets
status = _ft.SetLatencyTimer(_handle, 1);
FT.AssertOk(status);
}
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
var status = _ft.Close(_handle);
_handle = IntPtr.Zero;
FT.AssertOk(status);
}
if (_ft != null)
{
_ft.Dispose();
_ft = null;
}
}
public byte ReadByte()
{
var status = _ft.Read(_handle, _buf, 1, out uint countOfBytesRead);
FT.AssertOk(status);
if (countOfBytesRead != 1)
{
throw new TimeoutException("Read timed out");
}
var b = _buf[0];
return b;
}
/// <summary>
/// Write a byte to the interface but do not read/discard its echo.
/// </summary>
public void WriteByteRaw(byte b)
{
_buf[0] = b;
var status = _ft.Write(_handle, _buf, 1, out uint countOfBytesWritten);
FT.AssertOk(status);
if (countOfBytesWritten != 1)
{
throw new InvalidOperationException(
$"Expected to write 1 byte but wrote {countOfBytesWritten} bytes");
}
}
public void SetBreakOn()
{
var status = _ft.SetBreakOn(_handle);
FT.AssertOk(status);
}
public void SetBreakOff()
{
var status = _ft.SetBreakOff(_handle);
FT.AssertOk(status);
}
public void ClearReceiveBuffer()
{
var status = _ft.Purge(_handle, FT.PurgeMask.RX);
FT.AssertOk(status);
}
public static uint GetDevicesCount()
{
uint numdevs = 0;
FT tmp_ft = new FT();
try
{
var status = tmp_ft.CreateDeviceInfoList(out numdevs);
FT.AssertOk(status);
}
finally
{
tmp_ft.Dispose();
}
return numdevs;
}
public static void GetDeviceList(FT_DEVICE_INFO_NODE[] devicelist)
{
FT tmp_ft = new FT();
try
{
uint numdevs;
var status = tmp_ft.CreateDeviceInfoList(out numdevs);
FT.AssertOk(status);
byte[] sernum = new byte[16];
byte[] desc = new byte[64];
for (UInt32 i = 0; i < numdevs; i++)
{
devicelist[i] = new FT_DEVICE_INFO_NODE();
var ftStatus = tmp_ft.GetDeviceInfoDetail(i, out devicelist[i].Flags, out devicelist[i].Type, out devicelist[i].ID, out devicelist[i].LocId, sernum, desc, out devicelist[i].ftHandle);
devicelist[i].SerialNumber = Encoding.ASCII.GetString(sernum);
devicelist[i].Description = Encoding.ASCII.GetString(desc);
// Trim strings to first occurrence of a null terminator character
int nullIndex = devicelist[i].SerialNumber.IndexOf("\0", StringComparison.Ordinal);
if (nullIndex != -1)
devicelist[i].SerialNumber = devicelist[i].SerialNumber.Substring(0, nullIndex);
nullIndex = devicelist[i].Description.IndexOf("\0");
if (nullIndex != -1)
devicelist[i].Description = devicelist[i].Description.Substring(0, nullIndex);
}
}
finally
{
tmp_ft.Dispose();
}
}
public byte ReadEEPROM(uint Address, ref ushort EEValue)
{
var status = _ft.ReadEEPROM(_handle, Address, ref EEValue);
FT.AssertOk(status);
return (byte) status;
}
private static byte DecodeByte(byte b)
{
byte tmp = (byte)(((((b & 2) << 1) | (b & 0xF8)) << 3) | (b & 1));
byte tmp2 = (byte)((((((b >> 2) & 0x10) | (b & 0x87)) >> 1) | (b & 0x30)) >> 1);
return (byte)((tmp << 1) | tmp2); ;
}
private static UInt32 DecodeFTChipID(UInt32 id)
{
return (UInt32)((DecodeByte((byte)(id & 0xFF)) << 24) | (DecodeByte((byte)(id >> 8)) << 16) | (DecodeByte((byte)(id >> 16)) << 8) | DecodeByte((byte)((id >> 24) & 0xFF))) ^ 0xA5F0F7D1;
}
public static UInt32 CalculateKey(UInt32 id)
{
UInt32 result = 0;
result = id;
for (UInt32 i = 0; i < 10; i++)
{
if ((result & 0x00000001) == 0x00000001)
{
result = (result >> 1) | 0x80000000;
result ^= 0x63AC294D;
}
else
{
result >>= 1;
result ^= 0xF19A5712;
}
if ((result + 0x4A2FDC49) > 0xFFFFFFFF)
{
result += 0x4A2FDC49;
if ((result & 0x00000001) == 0x00000001)
{
result = (result >> 1) | 0x80000000;
}
else
{
result = (result >> 1);
}
}
else
{
result += 0x4A2FDC49;
}
}
return result;
}
public bool ReadChipID(ref UInt32 ChipID)
{
ushort a = 0, b = 0;
var status = _ft.ReadEEPROM(_handle, 0x44, ref a);
FT.AssertOk(status);
status = _ft.ReadEEPROM(_handle, 0x43, ref b);
FT.AssertOk(status);
ChipID = DecodeFTChipID( (UInt32) (a << 16 | b) );
return (status == FT.Status.Ok);
}
public bool CheckIsChipIDVaild(UInt32 Key)
{
UInt32 ChipID = 0;
if (!ReadChipID(ref ChipID))
{
return false;
}
UInt32 CorrectKey = CalculateKey(ChipID);
return (Key == CorrectKey);
}
public bool EEUserAreaSize(ref uint UASize)
{
var status = _ft.EE_UASize(_handle, ref UASize);
FT.AssertOk(status);
return (status == FT.Status.Ok);
}
public bool EEReadUserArea(byte[] UserAreaDataBuffer, ref uint numBytesRead)
{
var status = _ft.EE_UARead(_handle, UserAreaDataBuffer, UserAreaDataBuffer.Length, ref numBytesRead);
FT.AssertOk(status);
return (status == FT.Status.Ok);
}
public bool EEWriteUserArea(byte[] UserAreaDataBuffer)
{
var status = _ft.EE_UAWrite(_handle, UserAreaDataBuffer, UserAreaDataBuffer.Length);
FT.AssertOk(status);
return (status == FT.Status.Ok);
}
public bool CheckCableValidity()
{
UInt32 user_area_size = 0;
if (!this.EEUserAreaSize(ref user_area_size) || (user_area_size == 0))
{
throw new InvalidOperationException($"Cable validation error");
}
byte[] user_area_data = new byte[user_area_size];
UInt32 bytes_read = 0;
if (!this.EEReadUserArea(user_area_data, ref bytes_read) || (bytes_read == 0))
{
throw new InvalidOperationException($"Cable validation error");
}
UInt32 Key = (UInt32)((user_area_data[0] << 24) | (user_area_data[1] << 16) | (user_area_data[2] << 8) | user_area_data[3]);
if (!this.CheckIsChipIDVaild(Key))
{
throw new InvalidOperationException($"Cable validation error");
}
return true;
}
}
[System.Reflection.ObfuscationAttribute(Feature = "renaming")] //cuidao
class FT : IDisposable
{
private IntPtr _d2xx = IntPtr.Zero;
// Delegates used to call into the FTID D2xx DLL
#pragma warning disable CS0649
private readonly FTDll.SetVidPid _setVidPid;
private readonly FTDll.OpenBySerialNumber _openBySerialNumber;
private readonly FTDll.Close _close;
private readonly FTDll.SetBaudRate _setBaudRate;
private readonly FTDll.SetDataCharacteristics _setDataCharacteristics;
private readonly FTDll.SetFlowControl _setFlowControl;
private readonly FTDll.SetDtr _setDtr;
private readonly FTDll.ClrDtr _clrDtr;
private readonly FTDll.SetRts _setRts;
private readonly FTDll.ClrRts _clrRts;
private readonly FTDll.SetTimeouts _setTimeouts;
private readonly FTDll.SetLatencyTimer _setLatencyTimer;
private readonly FTDll.Purge _purge;
private readonly FTDll.SetBreakOn _setBreakOn;
private readonly FTDll.SetBreakOff _setBreakOff;
private readonly FTDll.Read _read;
private readonly FTDll.Write _write;
private readonly FTDll.CreateDeviceInfoList _createDeviceInfoList;
private readonly FTDll.GetDeviceInfoDetail _getDeviceInfoDetail;
private readonly FTDll.ReadEE _readEE;
private readonly FTDll.EE_UASize _ee_UASize;
private readonly FTDll.EE_UARead _ee_UARead;
private readonly FTDll.EE_UAWrite _ee_UAWrite;
#pragma warning restore CS0649
//[System.Reflection.ObfuscationAttribute(Feature = "renaming")] //no funciona imposible
public FT()
{
string libName;
bool isMacOs = false;
bool isLinux = false;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
libName = "libftd2xx.dylib";
isMacOs = true;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
libName = "ftd2xx.so";
isLinux = true;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
//libName = Environment.Is64BitProcess ? "ftd2xx64.dll" : "ftd2xx.dll";
libName = "ftd2xx.dll";
}
else
{
throw new InvalidOperationException($"Unknown OS: {RuntimeInformation.OSDescription}");
}
//_d2xx = NativeLibrary.Load(
// libName, typeof(FT).Assembly, DllImportSearchPath.SafeDirectories);
_d2xx = LoadLibrary(libName);
if (_d2xx == IntPtr.Zero)
{
_d2xx = LoadLibrary(@Path.GetDirectoryName(GetType().Assembly.Location) + "\\"+ libName);
}
if (_d2xx == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to load FTD2XX.DLL");
}
var fieldNames = new List<string>
{
nameof(_openBySerialNumber),
nameof(_close),
nameof(_setBaudRate),
nameof(_setDataCharacteristics),
nameof(_setFlowControl),
nameof(_setDtr),
nameof(_clrDtr),
nameof(_setRts),
nameof(_clrRts),
nameof(_setTimeouts),
nameof(_setLatencyTimer),
nameof(_purge),
nameof(_setBreakOn),
nameof(_setBreakOff),
nameof(_read),
nameof(_write),
nameof(_createDeviceInfoList),
nameof(_getDeviceInfoDetail),
nameof(_readEE),
nameof(_ee_UASize),
nameof(_ee_UARead),
nameof(_ee_UAWrite)
};
if (isMacOs || isLinux)
{
fieldNames.Add(nameof(_setVidPid));
}
foreach (var fieldName in fieldNames)
{
var fieldInfo = typeof(FT).GetField(
fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
var nativeMethodName = fieldInfo.FieldType.GetCustomAttribute<SymbolNameAttribute>().Name;
//var export = NativeLibrary.GetExport(_d2xx, nativeMethodName);
var export = GetProcAddress(_d2xx, nativeMethodName);
var delegateVal = Marshal.GetDelegateForFunctionPointer(export, fieldInfo.FieldType);
fieldInfo.SetValue(this, delegateVal);
}
if (isMacOs || isLinux)
{
var vidStr = Environment.GetEnvironmentVariable("FTDI_VID");
var pidStr = Environment.GetEnvironmentVariable("FTDI_PID");
if (!string.IsNullOrEmpty(vidStr) && !string.IsNullOrEmpty(pidStr))
{
//var vid = Utils.ParseUint(vidStr);
var vid = UInt32.Parse(vidStr);
var pid = UInt32.Parse(pidStr);
//var pid = Utils.ParseUint(pidStr);
System.Diagnostics.Debug.WriteLine($"Setting FTDI VID=0x{vid:X4}, PID=0x{pid:X4}");
var status = SetVidPid(vid, pid);
AssertOk(status);
}
}
}
public void Dispose()
{
if (_d2xx != IntPtr.Zero)
{
//NativeLibrary.Free(_d2xx);
FreeLibrary(_d2xx);
_d2xx = IntPtr.Zero;
}
}
public static void AssertOk(FT.Status status)
{
if (status != FT.Status.Ok)
{
throw new InvalidOperationException(
$"D2xx library returned {status} instead of Ok");
}
}
public Status SetVidPid(
uint vid,
uint pid)
{
return _setVidPid(vid, pid);
}
public Status Open(
string serialNumber,
OpenExFlags flags,
out IntPtr handle)
{
return _openBySerialNumber(serialNumber, flags, out handle);
}
public Status Close(
IntPtr handle)
{
return _close(handle);
}
public Status SetBaudRate(
IntPtr handle,
uint baudRate)
{
return _setBaudRate(handle, baudRate);
}
public Status SetDataCharacteristics(
IntPtr handle,
Bits wordLength,
StopBits stopBits,
Parity parity)
{
return _setDataCharacteristics(handle, wordLength, stopBits, parity);
}
public Status SetFlowControl(
IntPtr handle,
FlowControl flowControl,
byte xonChar,
byte xoffChar)
{
return _setFlowControl(handle, flowControl, xonChar, xoffChar);
}
public Status SetDtr(
IntPtr handle)
{
return _setDtr(handle);
}
public Status ClrDtr(
IntPtr handle)
{
return _clrDtr(handle);
}
public Status SetRts(
IntPtr handle)
{
return _setRts(handle);
}
public Status ClrRts(
IntPtr handle)
{
return _clrRts(handle);
}
public Status SetTimeouts(
IntPtr handle,
uint readTimeoutMS,
uint writeTimeoutMS)
{
return _setTimeouts(handle, readTimeoutMS, writeTimeoutMS);
}
public Status SetLatencyTimer(
IntPtr handle,
byte timerMS)
{
return _setLatencyTimer(handle, timerMS);
}
public Status Purge(
IntPtr handle,
PurgeMask mask)
{
return _purge(handle, mask);
}
public Status SetBreakOn(
IntPtr handle)
{
return _setBreakOn(handle);
}
public Status SetBreakOff(
IntPtr handle)
{
return _setBreakOff(handle);
}
public Status Read(
IntPtr handle,
byte[] buffer,
uint countOfBytesToRead,
out uint countOfBytesRead)
{
return _read(handle, buffer, countOfBytesToRead, out countOfBytesRead);
}
public Status Write(
IntPtr handle,
byte[] buffer,
uint countOfBytesToWrite,
out uint countOfBytesWritten)
{
return _write(handle, buffer, countOfBytesToWrite, out countOfBytesWritten);
}
public Status CreateDeviceInfoList(out uint devcount)
{
return _createDeviceInfoList(out devcount);
}
public Status GetDeviceInfoDetail(
UInt32 index,
out UInt32 flags,
out byte chiptype,
out UInt32 id,
out UInt32 locid,
byte[] serialnumber,
byte[] description,
out IntPtr ftHandle)
{
return _getDeviceInfoDetail(index, out flags, out chiptype, out id, out locid, serialnumber, description, out ftHandle);
}
public Status ReadEEPROM(
IntPtr ftHandle,
uint dwWordOffset,
ref ushort lpwValue)
{
return _readEE(ftHandle, dwWordOffset, ref lpwValue);
}
public Status EE_UASize(
IntPtr ftHandle,
ref uint dwSize)
{
return _ee_UASize(ftHandle, ref dwSize);
}
public Status EE_UARead(
IntPtr ftHandle,
byte[] pucData,
int dwDataLen,
ref uint lpdwDataRead)
{
return _ee_UARead(ftHandle, pucData, dwDataLen, ref lpdwDataRead);
}
public Status EE_UAWrite(
IntPtr ftHandle,
byte[] pucData,
int dwDataLen)
{
return _ee_UAWrite(ftHandle, pucData, dwDataLen);
}
public enum Status : uint
{
Ok = 0,
InvalidHandle,
DeviceNotFound,
DeviceNotOpened,
IOError,
insufficient_resources,
InvalidParameter,
InvalidBaudRate,
DeviceNotOpenedForErase,
DeviceNotOpenedForWrite,
FailedToWriteDevice,
EepromReadFailed,
EepromWriteFailed,
EepromEraseFailed,
EepromNotPresent,
EepromNotProgrammed,
InvalidArgs,
NotSupported,
OtherError,
DeviceListNotReady,
};
[Flags]
public enum OpenExFlags : uint
{
BySerialNumber = 1,
ByDescription = 2,
ByLocation = 4
};
public enum Bits : byte
{
Eight = 8,
Seven = 7
};
public enum StopBits : byte
{
One = 0,
Two = 2
};
public enum Parity : byte
{
None = 0,
Odd = 1,
Even = 2,
Mark = 3,
Space = 4
};
public enum FlowControl : ushort
{
None = 0x0000,
RtsCts = 0x0100,
DtrDsr = 0x0200,
XonXoff = 0x0400
};
[Flags]
public enum PurgeMask : uint
{
RX = 1,
TX = 2
};
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string dllToLoad);
[DllImport("kernel32.dll")]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
[DllImport("kernel32.dll")]
private static extern bool FreeLibrary(IntPtr hModule);
}
[AttributeUsage(AttributeTargets.Delegate)]
public class SymbolNameAttribute : Attribute
{
public SymbolNameAttribute(string name)
{
Name = name;
}
public string Name { get; private set; }
}
static class FTDll
{
[SymbolName("FT_SetVIDPID")]
public delegate FT.Status SetVidPid(
uint vid, uint pid);
[SymbolName("FT_OpenEx")]
public delegate FT.Status OpenBySerialNumber(
[MarshalAs(UnmanagedType.LPStr)] string serialNumber,
FT.OpenExFlags flags,
out IntPtr handle);
[SymbolName("FT_Close")]
public delegate FT.Status Close(
IntPtr handle);
[SymbolName("FT_SetBaudRate")]
public delegate FT.Status SetBaudRate(
IntPtr handle,
uint baudRate);
[SymbolName("FT_SetDataCharacteristics")]
public delegate FT.Status SetDataCharacteristics(
IntPtr handle,
FT.Bits wordLength,
FT.StopBits stopBits,
FT.Parity parity);
[SymbolName("FT_SetFlowControl")]
public delegate FT.Status SetFlowControl(
IntPtr handle,
FT.FlowControl flowControl,
byte xonChar,
byte xoffChar);
[SymbolName("FT_SetDtr")]
public delegate FT.Status SetDtr(
IntPtr handle);
[SymbolName("FT_ClrDtr")]
public delegate FT.Status ClrDtr(
IntPtr handle);
[SymbolName("FT_SetRts")]
public delegate FT.Status SetRts(
IntPtr handle);
[SymbolName("FT_ClrRts")]
public delegate FT.Status ClrRts(
IntPtr handle);
[SymbolName("FT_SetTimeouts")]
public delegate FT.Status SetTimeouts(
IntPtr handle,
uint readTimeoutMS,
uint writeTimeoutMS);
[SymbolName("FT_SetLatencyTimer")]
public delegate FT.Status SetLatencyTimer(
IntPtr handle,
byte timerMS);
[SymbolName("FT_Purge")]
public delegate FT.Status Purge(
IntPtr handle,
FT.PurgeMask mask);
[SymbolName("FT_SetBreakOn")]
public delegate FT.Status SetBreakOn(
IntPtr handle);
[SymbolName("FT_SetBreakOff")]
public delegate FT.Status SetBreakOff(
IntPtr handle);
[SymbolName("FT_Read")]
public delegate FT.Status Read(
IntPtr handle,
byte[] buffer,
uint countOfBytesToRead,
out uint countOfBytesRead);
[SymbolName("FT_Write")]
public delegate FT.Status Write(
IntPtr handle,
byte[] buffer,
uint countOfBytesToWrite,
out uint countOfBytesWritten);
[SymbolName("FT_CreateDeviceInfoList")]
public delegate FT.Status CreateDeviceInfoList(
out UInt32 numdevs);
[SymbolName("FT_GetDeviceInfoDetail")]
public delegate FT.Status GetDeviceInfoDetail(
UInt32 index,
out UInt32 flags,
out byte chiptype,
out UInt32 id,
out UInt32 locid,
byte[] serialnumber,
byte[] description,
out IntPtr ftHandle);
[SymbolName("FT_ReadEE")]
public delegate FT.Status ReadEE(
IntPtr ftHandle,
uint dwWordOffset,
ref ushort lpwValue);
[SymbolName("FT_EE_UASize")]
public delegate FT.Status EE_UASize(
IntPtr ftHandle,
ref uint dwSize);
[SymbolName("FT_EE_UARead")]
public delegate FT.Status EE_UARead(
IntPtr ftHandle,
byte[] pucData,
int dwDataLen,
ref uint lpdwDataRead);
[SymbolName("FT_EE_UAWrite")]
public delegate FT.Status EE_UAWrite(
IntPtr ftHandle,
byte[] pucData,
int dwDataLen);
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace HC_APTBS.Infrastructure.Kwp
{
public interface IInterface : IDisposable
{
byte ReadByte();
void WriteByteRaw(byte b);
void SetBreakOn();
void SetBreakOff();
void ClearReceiveBuffer();
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.IO.Ports;
namespace HC_APTBS.Infrastructure.Kwp
{
class KLineInterface : IInterface
{
public KLineInterface(string portName, int baudRate)
{
_port = new SerialPort(portName)
{
BaudRate = baudRate,
DataBits = 8,
Parity = Parity.None,
StopBits = StopBits.One,
Handshake = Handshake.None,
RtsEnable = false,
DtrEnable = true,
ReadTimeout = 1000,
WriteTimeout = 500
};
_port.Open();
}
public void Dispose()
{
_port.Close();
}
public byte ReadByte()
{
try
{
var b = (byte)_port.ReadByte();
return b;
}
catch (TimeoutException ex)
{
throw new TimeoutException("Read timed out");
}
}
public void WriteByteRaw(byte b)
{
_buf[0] = b;
_port.Write(_buf, 0, 1);
}
public void SetBreakOn()
{
_port.BreakState = true;
}
public void SetBreakOff()
{
_port.BreakState = false;
}
public void ClearReceiveBuffer()
{
_port.DiscardInBuffer();
}
private readonly SerialPort _port;
private readonly byte[] _buf = new byte[1];
}
}

View File

@@ -0,0 +1,577 @@
using HC_APTBS.Infrastructure.Kwp.Packets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace HC_APTBS.Infrastructure.Kwp
{
/// <summary>
/// Manages a dialog with a VW controller using the KW1281 protocol.
/// </summary>
public interface IKW1281Connection
{
ControllerInfo ReadEcuInfo();
void EndCommunication();
List<ControllerIdent> ReadIdent();
List<byte> ReadEeprom(ushort address, byte count);
List<byte> ReadRomEeprom(ushort address, byte count);
void CustomReset();
void SendPacket(List<byte> packetBytes);
List<Packet> SendCustom(List<byte> packetCustomBytes);
/// <summary>
/// Keep the dialog alive by sending an ACK and receiving a response.
/// </summary>
void KeepAlive();
List<FaultCode> ReadFaultCodes();
/// <summary>
/// Clear all of the controllers fault codes.
/// </summary>
/// <returns>True if successful.</returns>
bool ClearFaultCodes();
/// <summary>
/// Set the controller's software coding and workshop code.
/// </summary>
/// <param name="controllerAddress"></param>
/// <param name="softwareCoding"></param>
/// <param name="workshopCode"></param>
/// <returns>True if successful.</returns>
bool SetSoftwareCoding(int controllerAddress, int softwareCoding, int workshopCode);
IKwpCommon KwpCommon { get; }
}
public class KW1281Connection : IKW1281Connection
{
public ControllerInfo ReadEcuInfo()
{
var packets = ReceivePackets();
return new ControllerInfo(packets.Where(b => !b.IsAckNak));
}
public ControllerInfo ReadEcuInfoCustom(Int32 pkt_count)
{
var packets = new List<Packet>();
for(var i=0;i<pkt_count; i++)
{
var packet = ReceivePacket();
packets.Add(packet); // TODO: Maybe don't add the packet if it's an Ack
if (packet.Bytes.Count < 0x10) {
break;
}
if (packet is AckPacket || packet is NakPacket)
{
break;
}
if (i == pkt_count - 1) break;
SendAckPacket();
}
return new ControllerInfo(packets.Where(b => !b.IsAckNak));
}
public List<ControllerIdent> ReadIdent()
{
var idents = new List<ControllerIdent>();
bool moreAvailable;
do
{
System.Diagnostics.Debug.WriteLine("Sending ReadIdent packet");
SendPacket(new List<byte> { (byte)PacketCommand.ReadIdent });
var packets = ReceivePackets();
var ident = new ControllerIdent(packets.Where(b => !b.IsAckNak));
idents.Add(ident);
moreAvailable = packets
.OfType<AsciiDataPacket>()
.Any(b => b.MoreDataAvailable);
} while (moreAvailable);
return idents;
}
/// <summary>
/// Reads a range of bytes from the EEPROM.
/// </summary>
/// <param name="address"></param>
/// <param name="count"></param>
/// <returns>The bytes or null if the bytes could not be read</returns>
public List<byte> ReadEeprom(ushort address, byte count)
{
System.Diagnostics.Debug.WriteLine($"Sending ReadEeprom packet (Address: ${address:X4}, Count: ${count:X2})");
SendPacket(new List<byte>
{
(byte)PacketCommand.ReadEeprom,
count,
(byte)(address >> 8),
(byte)(address & 0xFF)
});
var packets = ReceivePackets();
if (packets.Count == 1 && packets[0] is NakPacket)
{
// Permissions issue
return null;
}
packets = packets.Where(b => !b.IsAckNak).ToList();
if (packets.Count != 1)
{
throw new InvalidOperationException($"ReadEeprom returned {packets.Count} blocks instead of 1");
}
return packets[0].Body.ToList();
}
public List<byte> ReadRomEeprom(ushort address, byte count)
{
//System.Diagnostics.Debug.WriteLine($"Sending ReadRomEeprom packet (Address: ${address:X4}, Count: ${count:X2})");
SendPacket(new List<byte>
{
(byte)PacketCommand.ReadRomEeprom,
count,
(byte)(address >> 8),
(byte)(address & 0xFF)
});
var packets = ReceivePackets();
if (packets.Count == 1 && packets[0] is NakPacket)
{
return new List<byte>();
}
packets = packets.Where(b => !b.IsAckNak).ToList();
if (packets.Count != 1)
{
throw new InvalidOperationException($"ReadRomEeprom returned {packets.Count} blocks instead of 1");
}
return packets[0].Body.ToList();
}
public Dictionary<int, Packet> CustomReadSoftwareVersion()
{
var versionPackets = new Dictionary<int, Packet>();
System.Diagnostics.Debug.WriteLine("Sending Custom \"Read Software Version\" packets");
// The cluster can return 4 variations of software version, specified by the 2nd byte
// of the packet:
// 0x00 - Cluster software version
// 0x01 - Unknown
// 0x02 - Unknown
// 0x03 - Unknown
for (byte variation = 0x00; variation < 0x04; variation++)
{
var packets = SendCustom(new List<byte> { 0x84, variation });
foreach (var packet in packets.Where(b => !b.IsAckNak))
{
if (variation == 0x00 || variation == 0x03)
{
System.Diagnostics.Debug.WriteLine($"{variation:X2}: {DumpMixedContent(packet)}");
}
else
{
System.Diagnostics.Debug.WriteLine($"{variation:X2}: {DumpBinaryContent(packet)}");
}
versionPackets[variation] = packet;
}
}
return versionPackets;
}
private static string DumpMixedContent(Packet packet)
{
if (packet.IsNak)
{
return "NAK";
}
return DumpMixedContent(packet.Body);
}
/// <summary>
/// Todo: Move to utility class
/// </summary>
public static string DumpMixedContent(IEnumerable<byte> content)
{
char mode = '?';
var sb = new StringBuilder();
foreach (var b in content)
{
if (b >= 32 && b <= 126)
{
mode = 'A';
sb.Append((char)b);
}
else
{
if (mode == 'A')
{
sb.Append(' ');
}
mode = 'X';
sb.Append($"${b:X2} ");
}
}
return sb.ToString();
}
private static string DumpBinaryContent(Packet packet)
{
if (packet.IsNak)
{
return "NAK";
}
return DumpBytes(packet.Body);
}
private static string DumpBytes(IEnumerable<byte> bytes)
{
var sb = new StringBuilder();
foreach (var b in bytes)
{
sb.Append($"${b:X2} ");
}
return sb.ToString();
}
public void CustomReset()
{
System.Diagnostics.Debug.WriteLine("Sending Custom Reset packet");
SendCustom(new List<byte> { 0x82 });
}
public List<Packet> SendCustom(List<byte> packetCustomBytes)
{
SendPacket(packetCustomBytes);
return ReceivePackets();
}
public void EndCommunication()
{
System.Diagnostics.Debug.WriteLine("Sending EndCommunication packet");
SendPacket(new List<byte> { (byte)PacketCommand.End });
}
public void SendPacket(List<byte> packetBytes)
{
var packetLength = (byte)(packetBytes.Count + 2);
packetBytes.Insert(0, _packetCounter.Value);
_packetCounter++;
packetBytes.Insert(0, packetLength);
foreach (var b in packetBytes)
{
WriteByteAndReadAck(b);
Thread.Sleep(5);
}
KwpCommon.WriteByte(0x03); // Packet end, does not get ACK'd
}
private List<Packet> ReceivePackets()
{
var packets = new List<Packet>();
while (true)
{
var packet = ReceivePacket();
packets.Add(packet); // TODO: Maybe don't add the packet if it's an Ack
if (packet is AckPacket || packet is NakPacket)
{
break;
}
SendAckPacket();
}
return packets;
}
private void WriteByteAndReadAck(byte b)
{
KwpCommon.WriteByte(b);
KwpCommon.ReadComplement(b);
}
private Packet ReceivePacket()
{
var packetBytes = new List<byte>();
var packetLength = KwpCommon.ReadAndAckByte();
packetBytes.Add(packetLength);
var packetCounter = ReadPacketCounter();
packetBytes.Add(packetCounter);
var packetCommand = KwpCommon.ReadAndAckByte();
packetBytes.Add(packetCommand);
for (int i = 0; i < packetLength - 3; i++)
{
var b = KwpCommon.ReadAndAckByte();
packetBytes.Add(b);
}
var packetEnd = KwpCommon.ReadByte();
packetBytes.Add(packetEnd);
if (packetEnd != 0x03)
{
throw new InvalidOperationException(
$"Received packet end ${packetEnd:X2} but expected $03");
}
switch (packetCommand)
{
case (byte)PacketCommand.ACK:
return new AckPacket(packetBytes);
case (byte)PacketCommand.AsciiData:
if (packetBytes[3] == 0x00) return new CodingWscPacket(packetBytes);
return new AsciiDataPacket(packetBytes);
case (byte)PacketCommand.ReadEepromResponse:
return new ReadEepromResponsePacket(packetBytes);
case (byte)PacketCommand.ReadRomEepromResponse:
return new ReadRomEepromResponse(packetBytes);
case (byte)PacketCommand.Custom:
return new CustomPacket(packetBytes);
break;
case (byte)PacketCommand.NAK:
return new NakPacket(packetBytes);
case (byte)PacketCommand.FaultCodesResponse:
return new FaultCodesPacket(packetBytes);
case (byte)PacketCommand.WriteEepromResponse:
return new WriteEepromResponsePacket(packetBytes);
default:
return new UnknownPacket(packetBytes);
}
}
private void SendAckPacket()
{
var packetBytes = new List<byte> { (byte)PacketCommand.ACK };
SendPacket(packetBytes);
}
private byte ReadPacketCounter()
{
var packetCounter = KwpCommon.ReadAndAckByte();
if (!_packetCounter.HasValue)
{
// First packet
_packetCounter = packetCounter;
}
else if (packetCounter != _packetCounter)
{
throw new InvalidOperationException(
$"Received packet counter ${packetCounter:X2} but expected ${_packetCounter:X2}");
}
_packetCounter++;
return packetCounter;
}
public void KeepAlive()
{
SendAckPacket();
var packet = ReceivePacket();
if (!(packet is AckPacket))
{
throw new InvalidOperationException(
$"Received 0x{packet.Title:X2} packet but expected ACK");
}
}
public List<FaultCode> ReadFaultCodes()
{
System.Diagnostics.Debug.WriteLine($"Sending ReadFaultCodes packet");
SendPacket(new List<byte>
{
(byte)PacketCommand.FaultCodesRead
});
var packets = ReceivePackets();
packets = packets.Where(b => !b.IsAckNak).ToList();
var faultCodes = new List<FaultCode>();
var faultCodesData = new List<byte>();
foreach (var packet in packets)
{
if (!(packet is FaultCodesPacket))
{
System.Diagnostics.Debug.WriteLine($"Expected FaultCodesPacket but got {packet.GetType()}");
return null;
}
faultCodesData.AddRange(packet.Body);
}
IEnumerable<byte> data = faultCodesData;
while (true)
{
var code = data.Take(3).ToArray();
if (code.Length == 0)
{
break;
}
var dtc = code[0];
var status = code[1];
var faultCode = new FaultCode(dtc, status);
if (faultCode.Dtc != FaultCode.None.Dtc)
{
faultCodes.Add(faultCode);
}
data = data.Skip(8);
}
return faultCodes;
}
public bool ClearFaultCodes()
{
System.Diagnostics.Debug.WriteLine($"Sending ClearFaultCodes packet");
SendPacket(new List<byte>
{
(byte)PacketCommand.FaultCodesDelete
});
var packets = ReceivePackets();
if (packets.Count == 1)
{
var packet = packets[0];
if (packet is NakPacket)
{
return false;
}
else if (packet is AckPacket)
{
return true;
}
else
{
throw new InvalidOperationException($"ClearFaultCodes returned {packet.GetType()} packet instead of ACK/NAK");
}
}
else
{
throw new InvalidOperationException($"ClearFaultCodes returned {packets.Count} packets instead of 1");
}
}
public bool SetSoftwareCoding(int controllerAddress, int softwareCoding, int workshopCode)
{
// Workshop codes > 65535 overflow into the low bit of the software coding
var bytes = new List<byte>
{
(byte)PacketCommand.SoftwareCoding,
(byte)((softwareCoding * 2) / 256),
(byte)((softwareCoding * 2) % 256),
(byte)((workshopCode & 65535) / 256),
(byte)(workshopCode % 256)
};
if (workshopCode > 65535)
{
bytes[2]++;
}
System.Diagnostics.Debug.WriteLine($"Sending SoftwareCoding packet");
SendPacket(bytes);
var packets = ReceivePackets();
if (packets.Count == 1 && packets[0] is NakPacket)
{
return false;
}
var controllerInfo = new ControllerInfo(packets.Where(b => !b.IsAckNak));
return
controllerInfo.SoftwareCoding == softwareCoding &&
controllerInfo.WorkshopCode == workshopCode;
}
public IKwpCommon KwpCommon { get; }
private byte? _packetCounter = null;
public KW1281Connection(IKwpCommon kwpCommon)
{
KwpCommon = kwpCommon;
}
}
public class KW1281KeepAlive : IDisposable
{
private readonly IKW1281Connection _kw1281Connection;
private volatile bool _cancel = false;
private Task _keepAliveTask = null;
public KW1281KeepAlive(IKW1281Connection kw1281Connection)
{
_kw1281Connection = kw1281Connection;
}
public void Dispose()
{
Pause();
}
private void Pause()
{
_cancel = true;
if (_keepAliveTask != null)
{
_keepAliveTask.Wait();
}
}
private void Resume()
{
_keepAliveTask = Task.Run(KeepAlive);
}
private void KeepAlive()
{
_cancel = false;
while (!_cancel)
{
_kw1281Connection.KeepAlive();
Console.Write(".");
}
}
}
}

View File

@@ -0,0 +1,162 @@
using System;
using System.Diagnostics;
namespace HC_APTBS.Infrastructure.Kwp
{
public interface IKwpCommon
{
IInterface Interface { get; }
int WakeUp(byte controllerAddress, bool evenParity = false);
byte ReadByte();
void WriteByte(byte b);
byte ReadAndAckByte();
void ReadComplement(byte b);
}
public class KwpCommon : IKwpCommon
{
public IInterface Interface => _interface;
public int WakeUp(byte controllerAddress, bool evenParity = false)
{
// Disable garbage collection in this time-critical method
bool noGc = GC.TryStartNoGCRegion(1024 * 1024 * 128); //antes 16
BitBang5Baud(controllerAddress, evenParity);
if (noGc)
{
GC.EndNoGCRegion();
}
// Throw away anything that might be in the receive buffer
_interface.ClearReceiveBuffer();
System.Diagnostics.Debug.WriteLine("Reading sync byte");
var syncByte = _interface.ReadByte();
if (syncByte != 0x55)
{
throw new InvalidOperationException(
$"Unexpected sync byte: Expected $55, Actual ${syncByte:X2}");
}
var keywordLsb = _interface.ReadByte();
System.Diagnostics.Debug.WriteLine($"Keyword LSB: 0x{keywordLsb:X2}");
var keywordMsb = ReadAndAckByte();
System.Diagnostics.Debug.WriteLine($"Keyword MSB: 0x{keywordMsb:X2}");
var protocolVersion = ((keywordMsb & 0x7F) << 7) + (keywordLsb & 0x7F);
return protocolVersion;
}
public byte ReadByte()
{
return _interface.ReadByte();
}
public void WriteByte(byte b)
{
WriteByteAndDiscardEcho(b);
}
public byte ReadAndAckByte()
{
var b = _interface.ReadByte();
WriteComplement(b);
return b;
}
public void ReadComplement(byte b)
{
var expectedComplement = (byte)~b;
var actualComplement = _interface.ReadByte();
if (actualComplement != expectedComplement)
{
throw new InvalidOperationException(
$"Received complement ${actualComplement:X2} but expected ${expectedComplement:X2}");
}
}
private void WriteComplement(byte b)
{
var complement = (byte)~b;
WriteByteAndDiscardEcho(complement);
}
private void BitBang5Baud(byte b, bool evenParity)
{
const int bitsPerSec = 5;
long ticksPerBit = Stopwatch.Frequency / bitsPerSec;
long maxTick;
// Delay the appropriate amount and then set/clear the TxD line
void BitBang(bool bit)
{
while (Stopwatch.GetTimestamp() < maxTick)
;
if (bit)
{
_interface.SetBreakOff();
}
else
{
_interface.SetBreakOn();
}
maxTick += ticksPerBit;
}
bool parity = !evenParity;
maxTick = Stopwatch.GetTimestamp();
BitBang(false); // Start bit
for (int i = 0; i < 7; i++)
{
bool bit = (b & 1) == 1;
parity ^= bit;
b >>= 1;
BitBang(bit);
}
BitBang(parity);
BitBang(true); // Stop bit
// Wait for end of stop bit
while (Stopwatch.GetTimestamp() < maxTick)
;
}
/// <summary>
/// Write a byte to the interface and read/discard its echo.
/// </summary>
private void WriteByteAndDiscardEcho(byte b)
{
_interface.WriteByteRaw(b);
var echo = _interface.ReadByte();
if (echo != b)
{
throw new InvalidOperationException($"Wrote 0x{b:X2} to port but echo was 0x{echo:X2}");
}
}
private readonly IInterface _interface;
public KwpCommon(IInterface @interface)
{
_interface = @interface;
}
}
}

View File

@@ -0,0 +1,28 @@
namespace HC_APTBS.Infrastructure.Kwp
{
public enum PacketCommand : byte
{
ReadIdent = 0x00,
ReadRomEeprom = 0x03,
ActuatorTest = 0x04,
FaultCodesDelete = 0x05,
End = 0x06,
FaultCodesRead = 0x07,
ACK = 0x09,
NAK = 0x0A,
SoftwareCoding = 0x10,
ReadEeprom = 0x19,
WriteEeprom = 0x1A,
Custom = 0x1B,
GroupReading = 0x29,
Login = 0x2B,
GroupReadingResponse = 0xE7,
ReadEepromResponse = 0xEF,
ActuatorTestResponse = 0xF5,
AsciiData = 0xF6,
WriteEepromResponse = 0xF9,
FaultCodesResponse = 0xFC,
ReadRomEepromResponse = 0xFD,
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class AckPacket : Packet
{
public AckPacket(List<byte> bytes) : base(bytes)
{
Dump();
}
private void Dump()
{
//Logger.WriteLine("Received ACK packet");
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class AsciiDataPacket : Packet
{
public AsciiDataPacket(List<byte> bytes) : base(bytes)
{
// Dump();
}
public bool MoreDataAvailable => Bytes[3] > 0x7F;
public override string ToString()
{
var sb = new StringBuilder();
foreach (var b in Body)
{
sb.Append((char)(b & 0x7F));
}
return sb.ToString();
}
private void Dump()
{
System.Diagnostics.Debug.Write($"Received Ascii data packet: \"{ToString()}\"");
if (MoreDataAvailable)
{
System.Diagnostics.Debug.Write(" (More data available via ReadIdent)");
}
System.Diagnostics.Debug.WriteLine("");
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class CodingWscPacket : Packet
{
public CodingWscPacket(List<byte> bytes) : base(bytes)
{
var data = bytes.Skip(4).ToList();
SoftwareCoding = (data[0] * 256 + data[1]) / 2;
WorkshopCode = data[2] * 256 + data[3];
// Workshop codes > 65535 overflow into the low bit of the software coding
if ((data[1] & 1) == 1)
{
WorkshopCode += 65536;
}
}
public override string ToString()
{
return $"Software Coding {SoftwareCoding:d5}, Workshop Code: {WorkshopCode:d5}";
}
public int SoftwareCoding { get; }
public int WorkshopCode { get; }
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class CustomPacket : Packet
{
public CustomPacket(List<byte> bytes) : base(bytes)
{
// Dump();
}
private void Dump()
{
System.Diagnostics.Debug.Write("Received Custom packet:");
for (var i = 3; i < Bytes.Count - 1; i++)
{
System.Diagnostics.Debug.Write($" {Bytes[i]:X2}");
}
System.Diagnostics.Debug.WriteLine("");
}
}
}

View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Linq;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class FaultCodesPacket : Packet
{
public FaultCodesPacket(List<byte> bytes) : base(bytes)
{
Data = bytes;
}
public List<byte> Data { get; }
}
public struct FaultCode
{
public FaultCode(int dtc, int status)
{
Dtc = dtc;
Status = status;
}
public override string ToString()
{
var status1 = Status & 0x7F;
var status2 = (Status >> 7) * 10;
Dictionary<int, string> dtc_text = new Dictionary<int, string>
{
{0x50, "Fuel quantity solenoid valve Output stage error"},
{0x51, "Fuel quantity solenoid valve."},
{0x52, "Angle sensor/ IWZ system."},
{0x53, "Angle sensor/ IWZ system"},
{0x54, "Control unit temperature sensor, temperature to high"},
{0x55, "Control unit temperature sensor"},
{0x56, "Battery voltage out of range"},
{0x57, "Timing device control.Permanent control deviation"},
{0x58, "Fuel quantity / timing solenoid valve"},
{0x59, "BIP Fault(Begin of Injection Point)"},
{0x5A, "Engine speed signal"},
{0x5B, "Engine speed signal"},
{0x5C, "CAN -Bus(sporadic)"},
{0x5D, "CAN bus error"},
{0x5E, "Self - test error"},
};
if (dtc_text.ContainsKey(Dtc)) return $"{Dtc:X2} {dtc_text[Dtc]} ({status1:d2})";
return $"{Dtc:X2} Unknown Error Code ({status1:d2})";
}
public int Dtc { get; }
public int Status { get; }
public static readonly FaultCode None = new FaultCode(0x00, 0xFF);
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
class NakPacket : Packet
{
public NakPacket(List<byte> bytes) : base(bytes)
{
Dump();
}
private void Dump()
{
System.Diagnostics.Debug.WriteLine("Received NAK packet");
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
/// <summary>
/// KWP1281 packet
/// </summary>
public class Packet
{
public Packet(List<byte> bytes)
{
Bytes = bytes;
}
public List<byte> Bytes { get; }
public byte Title => Bytes[2];
/// <summary>
/// Returns the body of the packet, excluding the length, counter, command and end bytes.
/// </summary>
public List<byte> Body => Bytes.Skip(3).Take(Bytes.Count - 4).ToList();
public bool IsAck => Title == (byte)PacketCommand.ACK;
public bool IsNak => Title == (byte)PacketCommand.NAK;
public bool IsAckNak => IsAck || IsNak;
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class ReadEepromResponsePacket : Packet
{
public ReadEepromResponsePacket(List<byte> bytes) : base(bytes)
{
//Dump();
}
private void Dump()
{
System.Diagnostics.Debug.Write("Received \"Read EEPROM Response\" packet:");
foreach (var b in Body)
{
System.Diagnostics.Debug.Write($" {b:X2}");
}
System.Diagnostics.Debug.WriteLine("");
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class ReadRomEepromResponse : Packet
{
public ReadRomEepromResponse(List<byte> bytes) : base(bytes)
{
//Dump();
}
private void Dump()
{
System.Diagnostics.Debug.Write("Received \"Read ROM/EEPROM Response\" packet:");
foreach (var b in Body)
{
System.Diagnostics.Debug.Write($" {b:X2}");
}
System.Diagnostics.Debug.WriteLine("");
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class UnknownPacket : Packet
{
public UnknownPacket(List<byte> bytes) : base(bytes)
{
Dump();
}
private void Dump()
{
/*Logger.Write("Received unknown packet");
foreach (var b in Bytes)
{
Logger.Write($" 0x{b:X2}");
}
Logger.WriteLine();*/
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace HC_APTBS.Infrastructure.Kwp.Packets
{
public class WriteEepromResponsePacket : Packet
{
public WriteEepromResponsePacket(List<byte> bytes) : base(bytes)
{
Dump();
}
private void Dump()
{
System.Diagnostics.Debug.Write("Received \"Write EEPROM Response\" packet:");
foreach (var b in Body)
{
System.Diagnostics.Debug.Write($" {b:X2}");
}
System.Diagnostics.Debug.WriteLine("");
}
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Globalization;
using System.IO;
using HC_APTBS.Services;
namespace HC_APTBS.Infrastructure.Logging
{
/// <summary>
/// File-based application logger.
/// Log files are written to <c>%UserProfile%\.HC_APTBS\log\</c> with
/// daily rotation using the filename pattern <c>LOG_yyyy_MM_dd.txt</c>.
/// Entries are appended with a timestamp prefix and a severity tag.
/// </summary>
public sealed class AppLogger : IAppLogger
{
// ── Constants ────────────────────────────────────────────────────────────
private const string TagError = "ERR";
private const string TagWarning = "WAR";
private const string TagMessage = "MSG";
private const string TagDebug = "DBG";
private static readonly string LogFolder =
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".HC_APTBS", "log");
// ── IAppLogger ────────────────────────────────────────────────────────────
/// <inheritdoc/>
public void Error(string source, string message) => Write(source, message, TagError);
/// <inheritdoc/>
public void Warning(string source, string message) => Write(source, message, TagWarning);
/// <inheritdoc/>
public void Info(string source, string message) => Write(source, message, TagMessage);
/// <inheritdoc/>
public void Debug(string source, string message) => Write(source, message, TagDebug);
// ── Helpers ───────────────────────────────────────────────────────────────
private static void Write(string source, string message, string tag)
{
try
{
Directory.CreateDirectory(LogFolder);
string path = Path.Combine(LogFolder,
$"LOG_{DateTime.Now:yyyy_MM_dd}.txt");
string line = $"{Timestamp()}[{tag}] {source.ToUpperInvariant()}-> {message}{Environment.NewLine}";
File.AppendAllText(path, line);
}
catch
{
// If logging fails (disk full, permissions) we swallow the error
// rather than crashing the application.
Console.WriteLine($"[{tag}] {source}: {message}");
}
}
/// <summary>Returns the current time formatted as <c>[HH:mm:ss] </c>.</summary>
private static string Timestamp()
=> DateTime.Now.ToString("[HH:mm:ss] ", DateTimeFormatInfo.InvariantInfo);
/// <summary>
/// Writes the application start banner to today's log file.
/// Call once from <c>App.OnStartup</c>.
/// </summary>
public void WriteStartupBanner(string version)
{
string sep = "******************************************";
string banner =
sep + Environment.NewLine +
sep + Environment.NewLine +
$"** STARTED {version} {DateTime.Now:dd/MM/yyyy HH:mm:ss} **" + Environment.NewLine +
sep + Environment.NewLine +
sep + Environment.NewLine;
try
{
Directory.CreateDirectory(LogFolder);
File.AppendAllText(
Path.Combine(LogFolder, $"LOG_{DateTime.Now:yyyy_MM_dd}.txt"),
banner);
}
catch { /* swallow */ }
}
/// <summary>
/// Writes the application shutdown footer to today's log file.
/// Call once from <c>App.OnExit</c>.
/// </summary>
public void WriteShutdownFooter()
{
string footer =
"-----------------------------------------------" + Environment.NewLine +
$"-- STOPPED {DateTime.Now:dd/MM/yyyy HH:mm:ss} --" + Environment.NewLine +
"-----------------------------------------------" + Environment.NewLine +
Environment.NewLine;
try
{
File.AppendAllText(
Path.Combine(LogFolder, $"LOG_{DateTime.Now:yyyy_MM_dd}.txt"),
footer);
}
catch { /* swallow */ }
}
}
}

View File

@@ -0,0 +1,512 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using HC_APTBS.Infrastructure.Logging;
using HC_APTBS.Models;
using HC_APTBS.Services;
using Peak.Can.Basic;
using TPCANHandle = System.UInt16;
namespace HC_APTBS.Infrastructure.Pcan
{
/// <summary>
/// Wraps the PCAN-Basic native API behind <see cref="ICanService"/>.
/// The raw P/Invoke declarations live in <see cref="PcanBasic"/> (vendor file,
/// unchanged). This class handles lifecycle, threading, OEM legitimation,
/// message dispatch, and parameter decoding.
/// </summary>
public sealed class PcanAdapter : ICanService, IDisposable
{
// ── Constants ───────────────────────────────────────────────────────────
/// <summary>PEAK-System distributor code embedded into the OEM legitimation token.</summary>
private const ulong OemDistributorCode = 20120378UL;
/// <summary>PEAK-System OEM-ID for the PCAN-USB device.</summary>
private const ulong OemId = 21200UL;
private const string LogId = "PcanAdapter";
// ── State ────────────────────────────────────────────────────────────────
private readonly TPCANHandle _channel;
private TPCANBaudrate _baudrate;
private readonly IAppLogger _log;
/// <summary>
/// Live parameter map: CAN message ID → list of parameters decoded from that frame.
/// All reads/writes on this dictionary happen on the CAN read thread, except for
/// <see cref="AddParameters"/> / <see cref="RemoveParameters"/> which are guarded
/// by <see cref="_mapLock"/>.
/// </summary>
private Dictionary<uint, List<CanBusParameter>> _parameterMap = new();
private readonly object _mapLock = new();
private Thread? _readThread;
private AutoResetEvent? _receiveEvent;
private volatile bool _stopRead = true;
// ── ICanService ──────────────────────────────────────────────────────────
/// <inheritdoc/>
public event Action<string, bool>? StatusChanged;
/// <inheritdoc/>
public TPCANStatus CurrentStatus { get; private set; } = TPCANStatus.PCAN_ERROR_OK;
/// <inheritdoc/>
public bool IsConnected => !_stopRead;
// ── Construction ─────────────────────────────────────────────────────────
/// <summary>
/// Creates a new adapter for the given PCAN channel and baudrate.
/// Call <see cref="Connect"/> to open the hardware.
/// </summary>
/// <param name="channel">PCAN channel handle, e.g. <c>PCANBasic.PCAN_USBBUS1</c>.</param>
/// <param name="baudrate">CAN baudrate, e.g. <c>TPCANBaudrate.PCAN_BAUD_500K</c>.</param>
/// <param name="logger">Application logger.</param>
public PcanAdapter(TPCANHandle channel, TPCANBaudrate baudrate, IAppLogger logger)
{
_channel = channel;
_baudrate = baudrate;
_log = logger;
}
// ── ICanService: lifecycle ────────────────────────────────────────────────
/// <inheritdoc/>
public bool Connect()
{
try
{
// If the channel is already open, reuse it; otherwise initialize.
CurrentStatus = PCANBasic.GetStatus(_channel);
if (CurrentStatus == TPCANStatus.PCAN_ERROR_INITIALIZE ||
CurrentStatus == TPCANStatus.PCAN_ERROR_INITIALIZE2)
{
CurrentStatus = PCANBasic.Initialize(_channel, _baudrate, (TPCANType)0, 0, 0);
EmitStatusChanged(CurrentStatus);
}
if (CurrentStatus == TPCANStatus.PCAN_ERROR_NETINUSE)
{
_log.Error(LogId, "CAN channel is already in use by another application.");
EmitStatusChanged(CurrentStatus);
return false;
}
if (CurrentStatus != TPCANStatus.PCAN_ERROR_OK)
{
LogPcanError("Connect: initialization failed");
EmitStatusChanged(CurrentStatus);
return false;
}
// OEM legitimation: token = (DistributorCode << 32) | OemId
// This authenticates our application with the PCAN hardware.
ulong token = (OemDistributorCode << 32) | OemId;
CurrentStatus = PCANBasic.SetValue(
_channel, TPCANParameter.PCAN_CHANNEL_LEGITIMATION, ref token, 8);
if (CurrentStatus != TPCANStatus.PCAN_ERROR_OK)
{
_log.Error(LogId, "OEM legitimation failed — adapter has no OEM token.");
PCANBasic.Uninitialize(_channel);
EmitStatusChanged(CurrentStatus);
return false;
}
StartReadThread();
return true;
}
catch (Exception ex)
{
_log.Error(LogId, $"Connect exception: {ex}");
return false;
}
}
/// <inheritdoc/>
public void Disconnect()
{
_stopRead = true;
PCANBasic.Uninitialize(_channel);
}
/// <inheritdoc/>
public void SwitchBaudrate(TPCANBaudrate newBaudrate, uint baudrateMessageId)
{
// Send the baudrate-change command to the bench firmware before switching.
SendMessageById(baudrateMessageId);
_stopRead = true;
Thread.Sleep(250);
_baudrate = newBaudrate;
PCANBasic.Uninitialize(_channel);
PCANBasic.Reset(_channel);
Connect();
}
// ── ICanService: parameter map ────────────────────────────────────────────
/// <inheritdoc/>
public void SetParameters(Dictionary<uint, List<CanBusParameter>> parameters)
{
lock (_mapLock)
{
_parameterMap = parameters;
}
}
/// <inheritdoc/>
public void AddParameters(Dictionary<uint, List<CanBusParameter>> parameters)
{
lock (_mapLock)
{
foreach (var kv in parameters)
{
if (!_parameterMap.ContainsKey(kv.Key))
_parameterMap.Add(kv.Key, kv.Value);
}
}
}
/// <inheritdoc/>
public void RemoveParameters(Dictionary<uint, List<CanBusParameter>> parameters)
{
lock (_mapLock)
{
foreach (var key in parameters.Keys)
_parameterMap.Remove(key);
}
}
// ── ICanService: transmit ─────────────────────────────────────────────────
/// <inheritdoc/>
public void SendMessageById(uint messageId)
{
Dictionary<uint, List<CanBusParameter>> snapshot;
lock (_mapLock) { snapshot = _parameterMap; }
if (!snapshot.TryGetValue(messageId, out var parameters) || parameters.Count == 0)
return;
var msg = new TPCANMsg
{
ID = messageId,
LEN = 8,
MSGTYPE = TPCANMessageType.PCAN_MESSAGE_STANDARD,
DATA = new byte[8]
};
// Write only transmit (non-receive) parameters into their assigned byte positions.
foreach (var param in parameters)
{
if (param.IsReceive) continue;
uint raw = (uint)param.GetTransmitValue();
msg.DATA[param.ByteH] = (byte)((raw & 0xFF00) >> 8);
msg.DATA[param.ByteL] = (byte)(raw & 0x00FF);
}
CurrentStatus = PCANBasic.Write(_channel, ref msg);
if (CurrentStatus != TPCANStatus.PCAN_ERROR_OK)
_log.Warning(LogId, $"SendMessageById({messageId:X}): {CurrentStatus}");
}
/// <inheritdoc/>
public void SendRawMessage(uint messageId, byte[] data)
{
if (data.Length != 8)
throw new ArgumentException("CAN standard frame payload must be exactly 8 bytes.", nameof(data));
var msg = new TPCANMsg
{
ID = messageId,
LEN = 8,
MSGTYPE = TPCANMessageType.PCAN_MESSAGE_STANDARD,
DATA = data
};
CurrentStatus = PCANBasic.Write(_channel, ref msg);
if (CurrentStatus != TPCANStatus.PCAN_ERROR_OK)
_log.Warning(LogId, $"SendRawMessage({messageId:X}): {CurrentStatus}");
}
// ── Read thread ───────────────────────────────────────────────────────────
private void StartReadThread()
{
_stopRead = false;
_receiveEvent = new AutoResetEvent(false);
_readThread = new Thread(ReadThreadEntry) { IsBackground = true, Name = "CAN-Read" };
_readThread.Start();
}
private void ReadThreadEntry()
{
// Bind the AutoResetEvent handle to the PCAN receive event so the driver
// signals us whenever a new frame arrives in the hardware FIFO.
uint eventHandle = Convert.ToUInt32(_receiveEvent!.SafeWaitHandle.DangerousGetHandle().ToInt32());
var result = PCANBasic.SetValue(
_channel, TPCANParameter.PCAN_RECEIVE_EVENT, ref eventHandle, sizeof(uint));
if (result != TPCANStatus.PCAN_ERROR_OK)
{
_log.Error(LogId, $"Failed to bind receive event: {result}");
return;
}
DrainMessageQueue();
}
/// <summary>
/// Continuously drains the hardware receive FIFO until stopped.
/// Runs on the dedicated CAN read background thread.
/// </summary>
private void DrainMessageQueue()
{
while (!_stopRead)
{
var status = ReadOnce();
if (status == TPCANStatus.PCAN_ERROR_ILLOPERATION)
{
_log.Error(LogId, "Read thread: illegal operation — stopping.");
_stopRead = true;
break;
}
if (status != TPCANStatus.PCAN_ERROR_QRCVEMPTY && status != TPCANStatus.PCAN_ERROR_OK)
{
_log.Warning(LogId, $"DrainMessageQueue: {status}");
EmitStatusChanged(status);
}
// Configurable polling interval to avoid pegging the CPU.
// Typical value: 250 ms depending on operational phase.
Thread.Sleep(2);
}
}
/// <summary>
/// Reads and decodes all frames currently available in the hardware FIFO.
/// Returns the last <see cref="TPCANStatus"/> encountered.
/// </summary>
private TPCANStatus ReadOnce()
{
TPCANStatus status;
do
{
status = PCANBasic.Read(_channel, out TPCANMsg frame);
if (status != TPCANStatus.PCAN_ERROR_QRCVEMPTY)
DecodeFrame(frame);
if (status != TPCANStatus.PCAN_ERROR_QRCVEMPTY && status != TPCANStatus.PCAN_ERROR_OK)
{
_log.Warning(LogId, $"ReadOnce: {status}");
EmitStatusChanged(status);
}
}
while (status != TPCANStatus.PCAN_ERROR_QRCVEMPTY &&
status != TPCANStatus.PCAN_ERROR_BUSHEAVY);
return status;
}
// ── Frame decoding ────────────────────────────────────────────────────────
/// <summary>
/// Decodes a received CAN frame and updates the associated <see cref="CanBusParameter"/>
/// values, applying the calibration transfer function and exponential smoothing filter.
/// </summary>
private void DecodeFrame(TPCANMsg frame)
{
// Message ID 0 carries internal bus-status info — ignore.
if (frame.ID == 0) return;
Dictionary<uint, List<CanBusParameter>> snapshot;
lock (_mapLock) { snapshot = _parameterMap; }
if (!snapshot.TryGetValue(frame.ID, out var parameters)) return;
byte[] data = frame.DATA;
foreach (var param in parameters)
{
// Only decode receive parameters — skip send-only params to avoid
// overwriting outgoing values with received frame bytes.
if (!param.IsReceive) continue;
double previousValue = param.Value;
if (param.Name == BenchParameterNames.Temp || param.Name == PumpParameterNames.Temp)
{
// Temperature uses a special packed BCD / signed format depending on sensor type.
param.Value = DecodeTempValue(data, param);
}
else if (param.Name == PumpParameterNames.Rpm)
{
// RPM is packed in the upper 12 bits across two bytes (two encoding variants).
param.Value = DecodeRpmValue(data, param);
param.Value = param.GetTransformResult();
}
else
{
// Generic 1-byte, 2-byte, or 3-byte big-endian integer.
int byteSpan = Math.Abs(param.ByteH - param.ByteL);
if (byteSpan == 0)
{
param.Value = data[param.ByteL];
}
else if (byteSpan == 1)
{
param.Value = (data[param.ByteH] << 8) | data[param.ByteL];
}
else
{
// 3-byte little-endian variant used for encoder/pulse counters.
param.Value = (data[param.ByteL + 2] << 16)
| (data[param.ByteL + 1] << 8)
| data[param.ByteL];
}
param.Value = param.GetTransformResult();
if (double.IsInfinity(param.Value)) param.Value = 0;
}
// Spike rejection for BenchRPM: the bench controller occasionally sends
// a spurious value of 1 RPM — discard it and retain the previous value.
if (param.Name == BenchParameterNames.BenchRpm && param.Value == 1)
{
param.Value = previousValue;
return;
}
// Spike rejection for QDelivery: discard values that are more than
// 100x the previous reading (caused by relay switching noise).
if (param.Name == BenchParameterNames.QDelivery)
{
if (previousValue > 0.1 && param.Value > previousValue * 100)
{
_log.Warning(LogId,
$"QDelivery spike suppressed: prev={previousValue:F3}, new={param.Value:F3}");
param.Value = previousValue;
}
}
// Apply single-pole IIR low-pass filter.
// result = prev + alpha * (new - prev)
param.Value = PassFilterUpdate(previousValue, param.Value, param.Alpha);
param.NeedsUpdate = true;
}
}
/// <summary>
/// Decodes a temperature value from the CAN frame.
/// The bench uses three different encoding schemes identified by <c>param.Type</c>:
/// <list type="bullet">
/// <item>0 — 4-nibble BCD with sign extension: [256…+256] + fractional nibble</item>
/// <item>1 — Compact signed: [54…+∞] with 1/16 fractional resolution</item>
/// <item>2 — Kelvin raw integer: value = (raw 273.15) via calibration transform</item>
/// </list>
/// </summary>
private double DecodeTempValue(byte[] data, CanBusParameter param)
{
switch (param.Type)
{
case 0:
{
// Full BCD signed: high byte nibbles encode hundreds/tens, low byte encodes units/fraction.
double val = -256
+ 256 * ((data[param.ByteH] & 0xF0) >> 4)
+ -16
+ 16 * (data[param.ByteH] & 0x0F)
+ ((data[param.ByteL] & 0xF0) >> 4);
val += (data[param.ByteL] & 0x0F) * (1.0 / 16.0);
return val;
}
case 1:
{
// Compact signed encoding.
double val = -54
+ 16 * ((data[param.ByteH] & 0xF0) >> 4)
+ -1
+ (data[param.ByteH] & 0x0F);
val += ((data[param.ByteL] & 0xF0) >> 4) * (1.0 / 16.0);
return val;
}
case 2:
{
// Raw Kelvin integer, converted via calibration formula (subtracts 273.15).
param.Value = (data[param.ByteH] << 8) | data[param.ByteL];
return param.GetTransformResult() - 273.15;
}
default:
return 0;
}
}
/// <summary>
/// Decodes an RPM value from the CAN frame.
/// Two encoding variants exist depending on <c>param.Type</c>:
/// <list type="bullet">
/// <item>0/1 — Upper 12 bits: ByteH shifted left 4, ByteL shifted right 4</item>
/// <item>2 — Lower 12 bits: lower nibble of ByteH as upper bits, ByteL as lower byte</item>
/// </list>
/// </summary>
private static double DecodeRpmValue(byte[] data, CanBusParameter param)
{
int raw = param.Type switch
{
0 or 1 => (data[param.ByteH] << 4) | (data[param.ByteL] >> 4),
2 => ((data[param.ByteH] & 0x0F) << 8) | data[param.ByteL],
_ => 0
};
return raw;
}
// ── IIR low-pass filter ───────────────────────────────────────────────────
/// <summary>
/// Single-pole exponential moving average: result = prev + alpha * (value prev).
/// Rounds to 4 decimal places to avoid floating-point drift accumulation.
/// </summary>
private static double PassFilterUpdate(double prev, double value, double alpha)
=> Math.Round(prev + alpha * (value - prev), 4);
// ── Helpers ───────────────────────────────────────────────────────────────
private void EmitStatusChanged(TPCANStatus status)
{
string text = status.ToString();
// Strip the "PCAN_ERROR_" prefix for brevity in the UI status bar.
string shortText = text.StartsWith("PCAN_ERROR_", StringComparison.Ordinal)
? text[11..]
: text;
StatusChanged?.Invoke(shortText, status == TPCANStatus.PCAN_ERROR_OK);
}
private void LogPcanError(string context)
{
var sb = new StringBuilder(256);
PCANBasic.GetErrorText(CurrentStatus, 0, sb);
_log.Error(LogId, $"{context}: {CurrentStatus} — {sb}");
}
// ── IDisposable ───────────────────────────────────────────────────────────
/// <summary>Stops the read thread and releases the PCAN channel.</summary>
public void Dispose()
{
Disconnect();
_receiveEvent?.Dispose();
}
}
}

File diff suppressed because it is too large Load Diff