commit 2b92f10a0c3922c8417dae19949c6d5a0170670b Author: acetaffy Date: Wed Sep 16 13:26:51 2026 +0800 Initial commit: BMS CAN monitor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..609ffc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# .NET build outputs +**/bin/ +**/obj/ +.vs/ + +# Local tooling and temporary files +tmp/ +work/ + +# Generated distributions and downloaded vendor archives +dist/ +third_party/ +*.zip +*.rar + +# User-specific IDE files +*.user +*.suo +*.userosscache +*.sln.docstates diff --git a/BmsMonitor.App/App.xaml b/BmsMonitor.App/App.xaml new file mode 100644 index 0000000..bc06223 --- /dev/null +++ b/BmsMonitor.App/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/BmsMonitor.App/App.xaml.cs b/BmsMonitor.App/App.xaml.cs new file mode 100644 index 0000000..8dfc96e --- /dev/null +++ b/BmsMonitor.App/App.xaml.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Windows; + +namespace BmsMonitor.App; + +public partial class App : Application +{ + private Mutex? _singleInstanceMutex; + private bool _ownsMutex; + + protected override void OnStartup(StartupEventArgs e) + { + _singleInstanceMutex = new Mutex(true, "Local\\BmsMonitor.ReadOnly.SingleInstance", out _ownsMutex); + if (!_ownsMutex) + { + MessageBox.Show("BMS Monitor 已经在运行。请关闭已有窗口后再启动。", "程序已运行", + MessageBoxButton.OK, MessageBoxImage.Information); + Shutdown(); + return; + } + + base.OnStartup(e); + } + + protected override void OnExit(ExitEventArgs e) + { + if (_ownsMutex) _singleInstanceMutex?.ReleaseMutex(); + _singleInstanceMutex?.Dispose(); + base.OnExit(e); + } +} diff --git a/BmsMonitor.App/AssemblyInfo.cs b/BmsMonitor.App/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/BmsMonitor.App/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/BmsMonitor.App/BmsMonitor.App.csproj b/BmsMonitor.App/BmsMonitor.App.csproj new file mode 100644 index 0000000..bb56cae --- /dev/null +++ b/BmsMonitor.App/BmsMonitor.App.csproj @@ -0,0 +1,29 @@ + + + + WinExe + net8.0-windows + enable + enable + true + x86 + x86 + + + + + + ControlCAN.dll + PreserveNewest + + + kerneldlls\usbcan.dll + PreserveNewest + + + kerneldlls\kerneldll.ini + PreserveNewest + + + + diff --git a/BmsMonitor.App/Can/CanBaudRate.cs b/BmsMonitor.App/Can/CanBaudRate.cs new file mode 100644 index 0000000..ecb40b7 --- /dev/null +++ b/BmsMonitor.App/Can/CanBaudRate.cs @@ -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 波特率。") + }; +} diff --git a/BmsMonitor.App/Can/CanFrame.cs b/BmsMonitor.App/Can/CanFrame.cs new file mode 100644 index 0000000..7168fb7 --- /dev/null +++ b/BmsMonitor.App/Can/CanFrame.cs @@ -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"))); +} diff --git a/BmsMonitor.App/Can/ControlCanNative.cs b/BmsMonitor.App/Can/ControlCanNative.cs new file mode 100644 index 0000000..91b03c7 --- /dev/null +++ b/BmsMonitor.App/Can/ControlCanNative.cs @@ -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); +} diff --git a/BmsMonitor.App/Can/ControlCanReceiver.cs b/BmsMonitor.App/Can/ControlCanReceiver.cs new file mode 100644 index 0000000..ac178be --- /dev/null +++ b/BmsMonitor.App/Can/ControlCanReceiver.cs @@ -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? FrameReceived; + public event EventHandler? 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(); +} diff --git a/BmsMonitor.App/Can/ICanReceiver.cs b/BmsMonitor.App/Can/ICanReceiver.cs new file mode 100644 index 0000000..09a31f5 --- /dev/null +++ b/BmsMonitor.App/Can/ICanReceiver.cs @@ -0,0 +1,10 @@ +namespace BmsMonitor.App.Can; + +public interface ICanReceiver : IDisposable +{ + bool IsOpen { get; } + event EventHandler? FrameReceived; + event EventHandler? ReceiveError; + void Open(uint deviceIndex, uint channelIndex, CanBaudRate baudRate = CanBaudRate.Kbps250); + void Close(); +} diff --git a/BmsMonitor.App/MainWindow.xaml b/BmsMonitor.App/MainWindow.xaml new file mode 100644 index 0000000..7354771 --- /dev/null +++ b/BmsMonitor.App/MainWindow.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + +