initial commit
This commit is contained in:
42
Infrastructure/Kwp/ControllerIdent.cs
Normal file
42
Infrastructure/Kwp/ControllerIdent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Infrastructure/Kwp/ControllerInfo.cs
Normal file
53
Infrastructure/Kwp/ControllerInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
868
Infrastructure/Kwp/FtdiInterface.cs
Normal file
868
Infrastructure/Kwp/FtdiInterface.cs
Normal 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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
17
Infrastructure/Kwp/IInterface.cs
Normal file
17
Infrastructure/Kwp/IInterface.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
70
Infrastructure/Kwp/KLineInterface.cs
Normal file
70
Infrastructure/Kwp/KLineInterface.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
577
Infrastructure/Kwp/KW1281Connection.cs
Normal file
577
Infrastructure/Kwp/KW1281Connection.cs
Normal 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(".");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
Infrastructure/Kwp/KwpCommon.cs
Normal file
162
Infrastructure/Kwp/KwpCommon.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Infrastructure/Kwp/PacketCommand.cs
Normal file
28
Infrastructure/Kwp/PacketCommand.cs
Normal 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,
|
||||
}
|
||||
|
||||
}
|
||||
18
Infrastructure/Kwp/Packets/AckPacket.cs
Normal file
18
Infrastructure/Kwp/Packets/AckPacket.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Infrastructure/Kwp/Packets/AsciiDataPacket.cs
Normal file
38
Infrastructure/Kwp/Packets/AsciiDataPacket.cs
Normal 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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Infrastructure/Kwp/Packets/CodingWscPacket.cs
Normal file
31
Infrastructure/Kwp/Packets/CodingWscPacket.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
24
Infrastructure/Kwp/Packets/CustomPacket.cs
Normal file
24
Infrastructure/Kwp/Packets/CustomPacket.cs
Normal 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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Infrastructure/Kwp/Packets/FaultCodesPacket.cs
Normal file
59
Infrastructure/Kwp/Packets/FaultCodesPacket.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
18
Infrastructure/Kwp/Packets/NakPacket.cs
Normal file
18
Infrastructure/Kwp/Packets/NakPacket.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Infrastructure/Kwp/Packets/Packet.cs
Normal file
31
Infrastructure/Kwp/Packets/Packet.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
24
Infrastructure/Kwp/Packets/ReadEepromResponsePacket.cs
Normal file
24
Infrastructure/Kwp/Packets/ReadEepromResponsePacket.cs
Normal 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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Infrastructure/Kwp/Packets/ReadRomEepromResponse.cs
Normal file
23
Infrastructure/Kwp/Packets/ReadRomEepromResponse.cs
Normal 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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Infrastructure/Kwp/Packets/UnknownPacket.cs
Normal file
23
Infrastructure/Kwp/Packets/UnknownPacket.cs
Normal 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();*/
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Infrastructure/Kwp/Packets/WriteEepromResponsePacket.cs
Normal file
24
Infrastructure/Kwp/Packets/WriteEepromResponsePacket.cs
Normal 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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Infrastructure/Logging/AppLogger.cs
Normal file
112
Infrastructure/Logging/AppLogger.cs
Normal 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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
512
Infrastructure/Pcan/PcanAdapter.cs
Normal file
512
Infrastructure/Pcan/PcanAdapter.cs
Normal 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: 2–50 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
1458
Infrastructure/Pcan/PcanBasic.cs
Normal file
1458
Infrastructure/Pcan/PcanBasic.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user