Initial commit: BMS CAN monitor

This commit is contained in:
acetaffy
2026-09-16 13:26:51 +08:00
commit 2b92f10a0c
23 changed files with 965 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
namespace BmsMonitor.App.Can;
public enum CanBaudRate
{
Kbps250,
Kbps500
}
internal static class CanBaudRateExtensions
{
internal static (byte Timing0, byte Timing1) ToTiming(this CanBaudRate baudRate) => baudRate switch
{
CanBaudRate.Kbps250 => (0x01, 0x1C),
CanBaudRate.Kbps500 => (0x00, 0x1C),
_ => throw new ArgumentOutOfRangeException(nameof(baudRate), baudRate, "不支持的 CAN 波特率。")
};
internal static string ToDisplayText(this CanBaudRate baudRate) => baudRate switch
{
CanBaudRate.Kbps250 => "250K",
CanBaudRate.Kbps500 => "500K",
_ => throw new ArgumentOutOfRangeException(nameof(baudRate), baudRate, "不支持的 CAN 波特率。")
};
}
+9
View File
@@ -0,0 +1,9 @@
namespace BmsMonitor.App.Can;
public sealed record CanFrame(uint Id, bool IsExtended, byte[] Data, DateTime Timestamp)
{
public string IdText => IsExtended ? $"{Id:X8}" : $"{Id:X3}";
public string FrameType => IsExtended ? "扩展帧" : "标准帧";
public int Dlc => Data.Length;
public string DataText => string.Join(" ", Data.Select(value => value.ToString("X2")));
}
+54
View File
@@ -0,0 +1,54 @@
using System.Runtime.InteropServices;
namespace BmsMonitor.App.Can;
internal static class ControlCanNative
{
internal const uint VciUsbCan2 = 4;
internal const uint StatusOk = 1;
[StructLayout(LayoutKind.Sequential)]
internal struct VciInitConfig
{
public uint AccCode;
public uint AccMask;
public uint Reserved;
public byte Filter;
public byte Timing0;
public byte Timing1;
public byte Mode;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct VciCanObj
{
public uint Id;
public uint TimeStamp;
public byte TimeFlag;
public byte SendType;
public byte RemoteFlag;
public byte ExternFlag;
public byte DataLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public byte[] Data;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] public byte[] Reserved;
}
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern uint VCI_OpenDevice(uint deviceType, uint deviceIndex, uint reserved);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern uint VCI_CloseDevice(uint deviceType, uint deviceIndex);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern uint VCI_InitCAN(uint deviceType, uint deviceIndex, uint canIndex, ref VciInitConfig config);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern uint VCI_StartCAN(uint deviceType, uint deviceIndex, uint canIndex);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern uint VCI_ResetCAN(uint deviceType, uint deviceIndex, uint canIndex);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern uint VCI_Receive(uint deviceType, uint deviceIndex, uint canIndex,
[Out] VciCanObj[] receive, uint length, int waitTime);
}
+98
View File
@@ -0,0 +1,98 @@
namespace BmsMonitor.App.Can;
public sealed class ControlCanReceiver : ICanReceiver
{
private CancellationTokenSource? _cts;
private Task? _receiveTask;
private uint _deviceIndex;
private uint _channelIndex;
public bool IsOpen { get; private set; }
public event EventHandler<CanFrame>? FrameReceived;
public event EventHandler<string>? ReceiveError;
public void Open(uint deviceIndex, uint channelIndex, CanBaudRate baudRate = CanBaudRate.Kbps250)
{
if (IsOpen) return;
_deviceIndex = deviceIndex;
_channelIndex = channelIndex;
if (ControlCanNative.VCI_OpenDevice(ControlCanNative.VciUsbCan2, deviceIndex, 0) != ControlCanNative.StatusOk)
throw new InvalidOperationException("打开 USBCAN-II 失败,请检查驱动、设备索引及 ControlCAN.dll。");
var (timing0, timing1) = baudRate.ToTiming();
var config = new ControlCanNative.VciInitConfig
{
AccCode = 0,
AccMask = 0xFFFFFFFF,
Filter = 1,
Timing0 = timing0,
Timing1 = timing1,
Mode = 1 // 硬件只听模式:仅接收,不发送 ACK
};
var initResult = ControlCanNative.VCI_InitCAN(ControlCanNative.VciUsbCan2, deviceIndex, channelIndex, ref config);
if (initResult != ControlCanNative.StatusOk)
{
ControlCanNative.VCI_CloseDevice(ControlCanNative.VciUsbCan2, deviceIndex);
throw new InvalidOperationException($"VCI_InitCAN 失败(返回 {initResult},通道 CAN{channelIndex + 1},{baudRate.ToDisplayText()},只听模式)。");
}
var startResult = ControlCanNative.VCI_StartCAN(ControlCanNative.VciUsbCan2, deviceIndex, channelIndex);
if (startResult != ControlCanNative.StatusOk)
{
ControlCanNative.VCI_ResetCAN(ControlCanNative.VciUsbCan2, deviceIndex, channelIndex);
ControlCanNative.VCI_CloseDevice(ControlCanNative.VciUsbCan2, deviceIndex);
throw new InvalidOperationException($"VCI_StartCAN 失败(返回 {startResult},通道 CAN{channelIndex + 1})。");
}
IsOpen = true;
_cts = new CancellationTokenSource();
_receiveTask = Task.Run(() => ReceiveLoop(_cts.Token));
}
private void ReceiveLoop(CancellationToken token)
{
var buffer = Enumerable.Range(0, 100).Select(_ => new ControlCanNative.VciCanObj
{
Data = new byte[8], Reserved = new byte[3]
}).ToArray();
try
{
while (!token.IsCancellationRequested)
{
var count = ControlCanNative.VCI_Receive(ControlCanNative.VciUsbCan2, _deviceIndex, _channelIndex,
buffer, (uint)buffer.Length, 50);
if (count == 0 || count == uint.MaxValue) continue;
for (var i = 0; i < Math.Min(count, (uint)buffer.Length); i++)
{
var item = buffer[i];
if (item.RemoteFlag != 0) continue;
var length = Math.Min(item.DataLen, (byte)8);
FrameReceived?.Invoke(this, new CanFrame(item.Id, item.ExternFlag != 0,
item.Data.Take(length).ToArray(), DateTime.Now));
}
}
}
catch (Exception ex) when (!token.IsCancellationRequested)
{
ReceiveError?.Invoke(this, ex.Message);
}
}
public void Close()
{
if (!IsOpen) return;
_cts?.Cancel();
_receiveTask?.Wait(300);
ControlCanNative.VCI_ResetCAN(ControlCanNative.VciUsbCan2, _deviceIndex, _channelIndex);
ControlCanNative.VCI_CloseDevice(ControlCanNative.VciUsbCan2, _deviceIndex);
_cts?.Dispose();
_cts = null;
IsOpen = false;
}
public void Dispose() => Close();
}
+10
View File
@@ -0,0 +1,10 @@
namespace BmsMonitor.App.Can;
public interface ICanReceiver : IDisposable
{
bool IsOpen { get; }
event EventHandler<CanFrame>? FrameReceived;
event EventHandler<string>? ReceiveError;
void Open(uint deviceIndex, uint channelIndex, CanBaudRate baudRate = CanBaudRate.Kbps250);
void Close();
}