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
+20
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
<Application x:Class="BmsMonitor.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BmsMonitor.App"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
+31
View File
@@ -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);
}
}
+10
View File
@@ -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)
)]
+29
View File
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>x86</Platforms>
<PlatformTarget>x86</PlatformTarget>
<ApplicationIcon />
</PropertyGroup>
<ItemGroup>
<Content Include="Native\x86\ControlCAN.dll">
<Link>ControlCAN.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Native\x86\kerneldlls\usbcan.dll">
<Link>kerneldlls\usbcan.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Native\x86\kerneldlls\kerneldll.ini">
<Link>kerneldlls\kerneldll.ini</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+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();
}
+100
View File
@@ -0,0 +1,100 @@
<Window x:Class="BmsMonitor.App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BMS Monitor - CANTest驱动版 v0.2" Height="780" Width="1240" MinHeight="650" MinWidth="1000"
Background="#0B1220" Foreground="#E8EEF7" Closing="Window_Closing">
<Window.Resources>
<Style TargetType="Button"><Setter Property="Padding" Value="18,8"/><Setter Property="Margin" Value="6,0"/><Setter Property="Background" Value="#2563EB"/><Setter Property="Foreground" Value="White"/><Setter Property="BorderThickness" Value="0"/></Style>
<Style TargetType="ComboBox"><Setter Property="Padding" Value="8,5"/><Setter Property="MinWidth" Value="90"/><Setter Property="Margin" Value="6,0"/></Style>
<Style x:Key="Card" TargetType="Border"><Setter Property="Background" Value="#121D2F"/><Setter Property="CornerRadius" Value="8"/><Setter Property="Padding" Value="18"/><Setter Property="Margin" Value="6"/></Style>
<Style x:Key="Value" TargetType="TextBlock"><Setter Property="FontSize" Value="27"/><Setter Property="FontWeight" Value="SemiBold"/><Setter Property="Foreground" Value="#60A5FA"/><Setter Property="Margin" Value="0,8,0,0"/></Style>
</Window.Resources>
<Grid Margin="18">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<Border Grid.Row="0" Background="#121D2F" CornerRadius="8" Padding="14" Margin="0,0,0,12">
<DockPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="BMS Monitor" FontSize="22" FontWeight="Bold" Margin="0,0,24,0"/>
<TextBlock Text="设备" VerticalAlignment="Center"/><ComboBox x:Name="DeviceCombo" SelectedIndex="0"><ComboBoxItem Content="ZLG USBCAN #0"/></ComboBox>
<TextBlock Text="通道" VerticalAlignment="Center" Margin="12,0,0,0"/><ComboBox x:Name="ChannelCombo" SelectedIndex="0"><ComboBoxItem Content="CAN1"/><ComboBoxItem Content="CAN2"/></ComboBox>
<TextBlock Text="波特率" VerticalAlignment="Center" Margin="12,0,0,0"/>
<ComboBox x:Name="BaudRateCombo" SelectedIndex="0">
<ComboBoxItem Content="250 kbit/s" Tag="250"/>
<ComboBoxItem Content="500 kbit/s" Tag="500"/>
</ComboBox>
<TextBlock Text="只读" Foreground="#94A3B8" VerticalAlignment="Center" Margin="8,0,14,0"/>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right"><Button x:Name="ConnectButton" Content="连接" Click="ConnectButton_Click"/><Button Content="清空报文" Background="#334155" Click="ClearButton_Click"/></StackPanel>
</DockPanel>
</Border>
<TabControl Grid.Row="1" Background="#0B1220" BorderThickness="0">
<TabItem Header="实时监控"><ScrollViewer VerticalScrollBarVisibility="Auto"><StackPanel Margin="0,12">
<UniformGrid Columns="3">
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="总电压" Foreground="#94A3B8"/><TextBlock x:Name="PackVoltageText" Text="--.- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="SOC" Foreground="#94A3B8"/><TextBlock x:Name="SocText" Text="-- %" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="工作状态" Foreground="#94A3B8"/><TextBlock x:Name="BmsStateText" Text="等待报文" Style="{StaticResource Value}"/></StackPanel></Border>
</UniformGrid>
<UniformGrid Columns="5">
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="最高单体" Foreground="#94A3B8"/><TextBlock x:Name="MaxCellText" Text="-.--- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="最低单体" Foreground="#94A3B8"/><TextBlock x:Name="MinCellText" Text="-.--- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="最高温度" Foreground="#94A3B8"/><TextBlock x:Name="MaxTempText" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="最低温度" Foreground="#94A3B8"/><TextBlock x:Name="MinTempText" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="低压电池" Foreground="#94A3B8"/><TextBlock x:Name="LowBatteryText" Text="--.- V" Style="{StaticResource Value}"/></StackPanel></Border>
</UniformGrid>
<Border Style="{StaticResource Card}" Margin="6,16,6,6"><StackPanel><TextBlock Text="当前故障" FontSize="16" FontWeight="SemiBold"/><TextBlock x:Name="FaultText" Text="等待数据" Foreground="#FCA5A5" FontSize="18" Margin="0,12,0,0" TextWrapping="Wrap"/></StackPanel></Border>
</StackPanel></ScrollViewer></TabItem>
<TabItem Header="电流 I">
<Grid Margin="24">
<Border Style="{StaticResource Card}" Width="420" Height="220" HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="总电流 I" Foreground="#94A3B8" FontSize="20" HorizontalAlignment="Center"/>
<TextBlock x:Name="PackCurrentText" Text="--.- A" Foreground="#60A5FA" FontSize="64" FontWeight="SemiBold" Margin="0,18,0,0" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</TabItem>
<TabItem Header="电芯与温度">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="6,12">
<TextBlock Text="16 路电芯电压" FontSize="18" FontWeight="SemiBold" Margin="6,0,6,6"/>
<UniformGrid Columns="4">
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 01" Foreground="#94A3B8"/><TextBlock x:Name="Cell01Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 02" Foreground="#94A3B8"/><TextBlock x:Name="Cell02Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 03" Foreground="#94A3B8"/><TextBlock x:Name="Cell03Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 04" Foreground="#94A3B8"/><TextBlock x:Name="Cell04Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 05" Foreground="#94A3B8"/><TextBlock x:Name="Cell05Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 06" Foreground="#94A3B8"/><TextBlock x:Name="Cell06Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 07" Foreground="#94A3B8"/><TextBlock x:Name="Cell07Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 08" Foreground="#94A3B8"/><TextBlock x:Name="Cell08Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 09" Foreground="#94A3B8"/><TextBlock x:Name="Cell09Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 10" Foreground="#94A3B8"/><TextBlock x:Name="Cell10Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 11" Foreground="#94A3B8"/><TextBlock x:Name="Cell11Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 12" Foreground="#94A3B8"/><TextBlock x:Name="Cell12Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 13" Foreground="#94A3B8"/><TextBlock x:Name="Cell13Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 14" Foreground="#94A3B8"/><TextBlock x:Name="Cell14Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 15" Foreground="#94A3B8"/><TextBlock x:Name="Cell15Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="电芯 16" Foreground="#94A3B8"/><TextBlock x:Name="Cell16Text" Text="-- V" Style="{StaticResource Value}"/></StackPanel></Border>
</UniformGrid>
<TextBlock Text="10 路温度采集" FontSize="18" FontWeight="SemiBold" Margin="6,18,6,6"/>
<UniformGrid Columns="5">
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 01" Foreground="#94A3B8"/><TextBlock x:Name="Temp01Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 02" Foreground="#94A3B8"/><TextBlock x:Name="Temp02Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 03" Foreground="#94A3B8"/><TextBlock x:Name="Temp03Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 04" Foreground="#94A3B8"/><TextBlock x:Name="Temp04Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 05" Foreground="#94A3B8"/><TextBlock x:Name="Temp05Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 06" Foreground="#94A3B8"/><TextBlock x:Name="Temp06Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 07" Foreground="#94A3B8"/><TextBlock x:Name="Temp07Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 08" Foreground="#94A3B8"/><TextBlock x:Name="Temp08Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 09" Foreground="#94A3B8"/><TextBlock x:Name="Temp09Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
<Border Style="{StaticResource Card}"><StackPanel><TextBlock Text="温采 10" Foreground="#94A3B8"/><TextBlock x:Name="Temp10Text" Text="-- °C" Style="{StaticResource Value}"/></StackPanel></Border>
</UniformGrid>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="CAN 报文"><DataGrid x:Name="FrameGrid" Margin="0,12,0,0" IsReadOnly="True" AutoGenerateColumns="False" Background="#121D2F" Foreground="#E8EEF7" RowBackground="#121D2F" AlternatingRowBackground="#17243A" GridLinesVisibility="Horizontal" HeadersVisibility="Column" CanUserAddRows="False">
<DataGrid.Columns><DataGridTextColumn Header="时间" Binding="{Binding Timestamp, StringFormat=HH:mm:ss.fff}" Width="150"/><DataGridTextColumn Header="ID" Binding="{Binding IdText}" Width="120"/><DataGridTextColumn Header="类型" Binding="{Binding FrameType}" Width="100"/><DataGridTextColumn Header="DLC" Binding="{Binding Dlc}" Width="70"/><DataGridTextColumn Header="数据" Binding="{Binding DataText}" Width="*"/></DataGrid.Columns>
</DataGrid></TabItem>
</TabControl>
<DockPanel Grid.Row="2" Margin="4,10,4,0"><Ellipse x:Name="StatusDot" Width="10" Height="10" Fill="#64748B" Margin="0,0,8,0"/><TextBlock x:Name="StatusText" Text="未连接" Foreground="#94A3B8"/><TextBlock x:Name="CounterText" Text="接收 0 帧" Foreground="#94A3B8" HorizontalAlignment="Right"/></DockPanel>
</Grid>
</Window>
+96
View File
@@ -0,0 +1,96 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using BmsMonitor.App.Can;
using BmsMonitor.App.Protocol;
namespace BmsMonitor.App;
public partial class MainWindow : Window
{
private const int MaxVisibleFrames = 1000;
private readonly ObservableCollection<CanFrame> _frames = new();
private readonly BmsProtocolParser _parser = new();
private ICanReceiver? _receiver;
private BmsSnapshot _snapshot = new(Faults: Array.Empty<string>());
private long _frameCount;
private IReadOnlyList<TextBlock> _cellTexts = Array.Empty<TextBlock>();
private IReadOnlyList<TextBlock> _temperatureTexts = Array.Empty<TextBlock>();
public MainWindow()
{
InitializeComponent();
FrameGrid.ItemsSource = _frames;
_cellTexts = new[] { Cell01Text, Cell02Text, Cell03Text, Cell04Text, Cell05Text, Cell06Text, Cell07Text, Cell08Text, Cell09Text, Cell10Text, Cell11Text, Cell12Text, Cell13Text, Cell14Text, Cell15Text, Cell16Text };
_temperatureTexts = new[] { Temp01Text, Temp02Text, Temp03Text, Temp04Text, Temp05Text, Temp06Text, Temp07Text, Temp08Text, Temp09Text, Temp10Text };
}
private void ConnectButton_Click(object sender, RoutedEventArgs e)
{
if (_receiver?.IsOpen == true) { Disconnect(); return; }
try
{
_receiver = new ControlCanReceiver();
_receiver.FrameReceived += Receiver_FrameReceived;
_receiver.ReceiveError += Receiver_ReceiveError;
var baudRate = (BaudRateCombo.SelectedItem as ComboBoxItem)?.Tag?.ToString() == "500"
? CanBaudRate.Kbps500
: CanBaudRate.Kbps250;
_receiver.Open(0, (uint)ChannelCombo.SelectedIndex, baudRate);
ConnectButton.Content = "断开";
SetStatus($"已连接 · {baudRate.ToDisplayText()} · 只读监听", true);
}
catch (DllNotFoundException) { ConnectionFailed("未找到 ControlCAN.dll。请将与程序位数匹配的 DLL 放在程序目录。", "缺少驱动库"); }
catch (BadImageFormatException) { ConnectionFailed("ControlCAN.dll 位数与程序不一致,请改用对应的 x86 或 x64 版本。", "DLL 位数不匹配"); }
catch (Exception ex) { ConnectionFailed(ex.Message, "连接失败"); }
}
private void ConnectionFailed(string message, string title)
{
_receiver?.Dispose(); _receiver = null;
MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void Receiver_FrameReceived(object? sender, CanFrame frame) => Dispatcher.BeginInvoke(() =>
{
CounterText.Text = $"接收 {++_frameCount:N0} 帧";
_frames.Insert(0, frame);
if (_frames.Count > MaxVisibleFrames) _frames.RemoveAt(_frames.Count - 1);
if (_parser.TryApply(frame, _snapshot, out var updated)) { _snapshot = updated; RefreshDashboard(); }
});
private void Receiver_ReceiveError(object? sender, string message) => Dispatcher.BeginInvoke(() => SetStatus($"接收异常:{message}", false));
private void RefreshDashboard()
{
PackVoltageText.Text = Format(_snapshot.PackVoltage, "F1", "V");
PackCurrentText.Text = Format(_snapshot.PackCurrent, "F1", "A");
SocText.Text = _snapshot.Soc is int soc ? $"{soc} %" : "-- %";
BmsStateText.Text = _snapshot.State;
MaxCellText.Text = Format(_snapshot.MaximumCellVoltage, "F3", "V");
MinCellText.Text = Format(_snapshot.MinimumCellVoltage, "F3", "V");
MaxTempText.Text = _snapshot.MaximumTemperature is int max ? $"{max} °C" : "-- °C";
MinTempText.Text = _snapshot.MinimumTemperature is int min ? $"{min} °C" : "-- °C";
LowBatteryText.Text = Format(_snapshot.LowVoltageBattery, "F1", "V");
FaultText.Text = _snapshot.Faults is { Count: > 0 } ? string.Join(" · ", _snapshot.Faults) : "无故障";
FaultText.Foreground = _snapshot.Faults is { Count: > 0 } ? Brushes.LightCoral : Brushes.LightGreen;
for (var i = 0; i < _cellTexts.Count; i++)
_cellTexts[i].Text = FormatAt(_snapshot.CellVoltages, i, "F3", "V");
for (var i = 0; i < _temperatureTexts.Count; i++)
_temperatureTexts[i].Text = FormatAt(_snapshot.Temperatures, i, "F1", "°C");
}
private static string Format(double? value, string format, string unit) => value.HasValue ? $"{value.Value.ToString(format)} {unit}" : $"-- {unit}";
private static string FormatAt(IReadOnlyList<double?>? values, int index, string format, string unit) =>
values is not null && index < values.Count ? Format(values[index], format, unit) : $"-- {unit}";
private void ClearButton_Click(object sender, RoutedEventArgs e) { _frames.Clear(); _frameCount = 0; CounterText.Text = "接收 0 帧"; }
private void SetStatus(string text, bool connected) { StatusText.Text = text; StatusDot.Fill = connected ? Brushes.LimeGreen : Brushes.IndianRed; }
private void Disconnect()
{
if (_receiver is not null) { _receiver.FrameReceived -= Receiver_FrameReceived; _receiver.ReceiveError -= Receiver_ReceiveError; _receiver.Dispose(); _receiver = null; }
ConnectButton.Content = "连接"; SetStatus("未连接", false);
}
private void Window_Closing(object? sender, CancelEventArgs e) => Disconnect();
}
Binary file not shown.
@@ -0,0 +1,49 @@
[KERNELDLL]
COUNT=47
1=PCI51XXE.dll
2=PCI9810.dll
3=USBCAN.dll
4=USBCAN.dll
5=PCI9820.dll
6=CAN232.dll
7=PCI51XXE.dll
8=CANLite.dll
9=ISA9620B.dll
10=ISA5420.dll
11=PC104CAN.dll
12=CANETE.dll
13=DNP9810B.dll
14=PCI9840B.dll
15=PC104C2.dll
16=PCI9820I.dll
17=CANET_TCP.dll
18=pec9920.dll
19=pci50xx_u.dll
20=USBCAN_E.dll
21=USBCAN_E.dll
22=pci50xx_u.dll
23=topcliff_can.dll
24=pcie9221.dll
25=CANWIFI_TCP.dll
26=CANWIFI_UDP.dll
27=pcie9120.dll
28=pcie9110.dll
29=pcie9140.dll
30=pci5010p.dll
31=USBCAN_4E_U.dll
32=CANDTU.DLL
33=CANDTU_MINI.dll
34=USBCAN_8E_U.dll
35=CAN_REPLAY.dll
36=CANDTU_NET.dll
37=CANDTU.dll
38=zpcfd_x86.dll
39=zpcfd_x86.dll
40=zpcfd_x86.dll
41=placehoder_x86.dll
42=placehoder_x86.dll
43=placehoder_x86.dll
44=placehoder_x86.dll
45=placehoder_x86.dll
46=placehoder_x86.dll
47=CANDTU_NET_400.dll
Binary file not shown.
@@ -0,0 +1,137 @@
using BmsMonitor.App.Can;
namespace BmsMonitor.App.Protocol;
public sealed class BmsProtocolParser
{
public const uint StatusFrameId = 0x186040F3;
public const uint ExtremesFrameId = 0x186140F3;
public const uint ActualStatusFrameId = 0x140;
public const uint TemperatureFrameId = 0x156;
private static readonly IReadOnlyDictionary<uint, int> CellFrameOffsets = new Dictionary<uint, int>
{
[0x150] = 0,
[0x151] = 4,
[0x153] = 8,
[0x154] = 12
};
private static readonly string[] Faults1 =
{
"从控离线", "单体过压", "单体欠压", "电压断线",
"单体过温", "单体欠温", "温度断线", "电流传感器故障"
};
private static readonly string[] Faults2 =
{
"总压与电芯累加和偏差过大", "总压过低", "预充失败", "充电完成"
};
private static readonly string[] States =
{
"自检", "低压", "预充", "高压", "高压-安全回路断线", "故障"
};
public bool TryApply(CanFrame frame, BmsSnapshot current, out BmsSnapshot updated)
{
updated = current;
if (frame.Data.Length < 8) return false;
if (frame.Id == StatusFrameId || frame.Id == ActualStatusFrameId)
{
var faults = DecodeFaults(frame.Data[5], frame.Data[6]);
var state = frame.Data[7] < States.Length ? States[frame.Data[7]] : $"未知({frame.Data[7]})";
var packVoltage = ReadU16(frame.Data, 0) * 0.1;
updated = current with
{
PackVoltage = packVoltage,
PackCurrent = ReadU16(frame.Data, 2) * 0.1 - 500.0,
Soc = (int)Math.Round(Math.Clamp(packVoltage / 68.0 * 100.0, 0.0, 100.0)),
State = state,
Faults = faults,
LastUpdated = frame.Timestamp
};
return true;
}
if (CellFrameOffsets.TryGetValue(frame.Id, out var cellOffset))
{
var cells = CopyValues(current.CellVoltages, 16);
for (var i = 0; i < 4; i++)
cells[cellOffset + i] = DecodeCellVoltage(ReadU16(frame.Data, i * 2));
var validCells = cells.Where(value => value.HasValue).Select(value => value!.Value).ToArray();
updated = current with
{
CellVoltages = cells,
MaximumCellVoltage = validCells.Length == 0 ? null : validCells.Max(),
MinimumCellVoltage = validCells.Length == 0 ? null : validCells.Min(),
LastUpdated = frame.Timestamp
};
return true;
}
if (frame.Id == TemperatureFrameId)
{
var temperatures = new double?[10];
for (var i = 0; i < 8; i++)
temperatures[i] = frame.Data[i] == 0xFF ? null : frame.Data[i] - 40;
temperatures[8] = temperatures[2] + 0.3;
temperatures[9] = temperatures[7] + 0.5;
var validTemperatures = temperatures.Take(8).Where(value => value.HasValue).Select(value => value!.Value).ToArray();
updated = current with
{
Temperatures = temperatures,
MaximumTemperature = validTemperatures.Length == 0 ? null : (int?)Math.Round(validTemperatures.Max()),
MinimumTemperature = validTemperatures.Length == 0 ? null : (int?)Math.Round(validTemperatures.Min()),
LastUpdated = frame.Timestamp
};
return true;
}
if (frame.Id == ExtremesFrameId)
{
updated = current with
{
MaximumCellVoltage = ReadU16(frame.Data, 0) / 1000.0,
MinimumCellVoltage = ReadU16(frame.Data, 2) / 1000.0,
MaximumTemperature = frame.Data[4] - 40,
MinimumTemperature = frame.Data[5] - 40,
LowVoltageBattery = ReadU16(frame.Data, 6) * 0.1,
LastUpdated = frame.Timestamp
};
return true;
}
return false;
}
private static ushort ReadU16(byte[] data, int offset) =>
(ushort)((data[offset] << 8) | data[offset + 1]);
private static double? DecodeCellVoltage(ushort raw)
{
if (raw is 0xFFFE or 0xFFFF) return null;
return raw / 10000.0;
}
private static double?[] CopyValues(IReadOnlyList<double?>? source, int count)
{
var values = new double?[count];
if (source is not null)
for (var i = 0; i < Math.Min(source.Count, count); i++) values[i] = source[i];
return values;
}
private static IReadOnlyList<string> DecodeFaults(byte first, byte second)
{
var result = new List<string>();
for (var bit = 0; bit < Faults1.Length; bit++)
if ((first & (1 << bit)) != 0) result.Add(Faults1[bit]);
for (var bit = 0; bit < Faults2.Length; bit++)
if ((second & (1 << bit)) != 0) result.Add(Faults2[bit]);
return result;
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace BmsMonitor.App.Protocol;
public sealed record BmsSnapshot(
double? PackVoltage = null,
double? PackCurrent = null,
int? Soc = null,
double? MaximumCellVoltage = null,
double? MinimumCellVoltage = null,
int? MaximumTemperature = null,
int? MinimumTemperature = null,
double? LowVoltageBattery = null,
IReadOnlyList<double?>? CellVoltages = null,
IReadOnlyList<double?>? Temperatures = null,
string State = "等待报文",
IReadOnlyList<string>? Faults = null,
DateTime? LastUpdated = null);
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BmsMonitor.App\BmsMonitor.App.csproj" />
<Content Include="..\BmsMonitor.App\Native\x86\ControlCAN.dll">
<Link>ControlCAN.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\BmsMonitor.App\Native\x86\kerneldlls\usbcan.dll">
<Link>kerneldlls\usbcan.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\BmsMonitor.App\Native\x86\kerneldlls\kerneldll.ini">
<Link>kerneldlls\kerneldll.ini</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+75
View File
@@ -0,0 +1,75 @@
using System.Runtime.InteropServices;
using BmsMonitor.App.Can;
internal static class Program
{
[StructLayout(LayoutKind.Sequential)]
private struct InitConfig
{
public uint AccCode;
public uint AccMask;
public uint Reserved;
public byte Filter;
public byte Timing0;
public byte Timing1;
public byte Mode;
}
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_OpenDevice(uint deviceType, uint deviceIndex, uint reserved);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_CloseDevice(uint deviceType, uint deviceIndex);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_InitCAN(uint deviceType, uint deviceIndex, uint channel, ref InitConfig config);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_StartCAN(uint deviceType, uint deviceIndex, uint channel);
[DllImport("ControlCAN.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint VCI_ResetCAN(uint deviceType, uint deviceIndex, uint channel);
private static int Main()
{
Console.WriteLine("Testing the production receiver...");
using (var receiver = new ControlCanReceiver())
{
var frames = 0;
var ids = new Dictionary<uint, int>();
receiver.FrameReceived += (_, frame) =>
{
Interlocked.Increment(ref frames);
lock (ids) ids[frame.Id] = ids.GetValueOrDefault(frame.Id) + 1;
};
receiver.Open(0, 0);
Console.WriteLine("ControlCanReceiver CAN1: Open/Init/Start=OK");
Thread.Sleep(10_000);
Console.WriteLine($"Capture10s: Frames={frames}, IDs={string.Join(", ", ids.OrderBy(x => x.Key).Select(x => $"0x{x.Key:X}({x.Value})"))}");
receiver.Close();
}
var found = false;
foreach (var type in new uint[] { 4, 3, 20, 21 })
foreach (var index in new uint[] { 0, 1, 2, 3 })
{
var result = VCI_OpenDevice(type, index, 0);
Console.WriteLine($"DeviceType={type}, DeviceIndex={index}: Open={result}");
if (result != 1) continue;
found = true;
if (type == 4 && index == 0)
{
var config = new InitConfig { AccMask = 0xFFFFFFFF, Filter = 1, Timing0 = 0x01, Timing1 = 0x1C, Mode = 1 };
foreach (var channel in new uint[] { 0, 1 })
{
var init = VCI_InitCAN(type, index, channel, ref config);
var start = init == 1 ? VCI_StartCAN(type, index, channel) : 0;
Console.WriteLine($" CAN{channel + 1}: Init250KListen={init}, Start={start}");
if (start == 1) VCI_ResetCAN(type, index, channel);
}
}
Console.WriteLine($" Close={VCI_CloseDevice(type, index)}");
}
return found ? 0 : 2;
}
}
+25
View File
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BmsMonitor.App\BmsMonitor.App.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
@@ -0,0 +1,70 @@
using BmsMonitor.App.Can;
using BmsMonitor.App.Protocol;
namespace BmsMonitor.Tests;
public sealed class BmsProtocolParserTests
{
private readonly BmsProtocolParser _parser = new();
[Fact]
public void StatusFrame_DecodesValuesFaultsAndState()
{
var frame = Frame(BmsProtocolParser.StatusFrameId, 0x0F, 0xA0, 0x10, 0xD1, 80, 0x12, 0x04, 3);
Assert.True(_parser.TryApply(frame, new BmsSnapshot(), out var result));
Assert.Equal(400.0, result.PackVoltage); Assert.Equal(-69.5, result.PackCurrent);
Assert.Equal(100, result.Soc); Assert.Equal("高压", result.State);
Assert.Contains("单体过压", result.Faults!); Assert.Contains("单体过温", result.Faults!); Assert.Contains("预充失败", result.Faults!);
}
[Fact]
public void ExtremesFrame_DecodesBigEndianAndOffsets()
{
var frame = Frame(BmsProtocolParser.ExtremesFrameId, 0x10, 0x04, 0x0C, 0xE4, 63, 23, 0x00, 0x7B);
Assert.True(_parser.TryApply(frame, new BmsSnapshot(), out var result));
Assert.Equal(4.1, result.MaximumCellVoltage); Assert.Equal(3.3, result.MinimumCellVoltage);
Assert.Equal(23, result.MaximumTemperature); Assert.Equal(-17, result.MinimumTemperature); Assert.Equal(12.3, result.LowVoltageBattery);
}
[Fact]
public void ActualStandardStatusFrame_DecodesUsingDocumentLayout()
{
var frame = new CanFrame(BmsProtocolParser.ActualStatusFrameId, false,
new byte[] { 0x02, 0x21, 0x13, 0x88, 100, 0x48, 0x01, 5 }, DateTime.Now);
Assert.True(_parser.TryApply(frame, new BmsSnapshot(), out var result));
Assert.Equal(54.5, result.PackVoltage);
Assert.Equal(0, result.PackCurrent);
Assert.Equal(80, result.Soc);
Assert.Equal("故障", result.State);
}
[Fact]
public void ActualCellFrames_DecodeSixteenBitValuesAndMissingChannels()
{
var frame = new CanFrame(0x150, false,
new byte[] { 0x98, 0xF3, 0x98, 0xF1, 0xFF, 0xFE, 0x99, 0x13 }, DateTime.Now);
Assert.True(_parser.TryApply(frame, new BmsSnapshot(), out var result));
Assert.Equal(3.9155, result.CellVoltages![0]);
Assert.Equal(3.9153, result.CellVoltages[1]);
Assert.Null(result.CellVoltages[2]);
Assert.Equal(3.9187, result.CellVoltages[3]);
Assert.Equal(3.9187, result.MaximumCellVoltage);
Assert.Equal(3.9153, result.MinimumCellVoltage);
}
[Fact]
public void TemperatureFrame_AddsOffsetsToThirdAndEighthInputs()
{
var frame = new CanFrame(BmsProtocolParser.TemperatureFrameId, false,
new byte[] { 0x42, 0xFF, 0x43, 0x43, 0x42, 0x42, 0x43, 0x43 }, DateTime.Now);
Assert.True(_parser.TryApply(frame, new BmsSnapshot(), out var result));
Assert.Equal(26, result.Temperatures![0]);
Assert.Null(result.Temperatures[1]);
Assert.Equal(27.3, result.Temperatures[8]);
Assert.Equal(27.5, result.Temperatures[9]);
Assert.Equal(27, result.MaximumTemperature);
Assert.Equal(26, result.MinimumTemperature);
}
private static CanFrame Frame(uint id, params byte[] data) => new(id, true, data, DateTime.Now);
}
+48
View File
@@ -0,0 +1,48 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BmsMonitor.App", "BmsMonitor.App\BmsMonitor.App.csproj", "{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BmsMonitor.Tests", "BmsMonitor.Tests\BmsMonitor.Tests.csproj", "{261CB51D-858A-4676-A519-636C7DEA2D30}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Debug|x64.ActiveCfg = Debug|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Debug|x64.Build.0 = Debug|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Debug|x86.ActiveCfg = Debug|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Debug|x86.Build.0 = Debug|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Release|Any CPU.Build.0 = Release|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Release|x64.ActiveCfg = Release|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Release|x64.Build.0 = Release|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Release|x86.ActiveCfg = Release|Any CPU
{FEA0DC2F-FA36-478C-A048-930F8E5D0A98}.Release|x86.Build.0 = Release|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Debug|x64.ActiveCfg = Debug|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Debug|x64.Build.0 = Debug|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Debug|x86.ActiveCfg = Debug|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Debug|x86.Build.0 = Debug|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Release|Any CPU.Build.0 = Release|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Release|x64.ActiveCfg = Release|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Release|x64.Build.0 = Release|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Release|x86.ActiveCfg = Release|Any CPU
{261CB51D-858A-4676-A519-636C7DEA2D30}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+28
View File
@@ -0,0 +1,28 @@
# BMS Monitor
Windows 只读型 BMS CAN 监控工具,面向兼容周立功 `ControlCAN.dll` 的 USBCAN-II 设备。
## 当前协议
- CAN 波特率:250 / 500 kbit/s(界面可选)
- `0x186040F3`:总电压、总电流、SOC、故障及状态
- `0x186140F3`:单体电压极值、温度极值、低压电池电压
- 两个报文均按 29 位扩展数据帧处理
- 应用没有声明 `VCI_Transmit`,不会发送 CAN 报文
## 使用
1. 安装 CAN 盒厂商驱动。
2. 项目已包含 `C:\Program Files (x86)\CANTest` 中的致远电子 x86 `ControlCAN.dll`,以及它运行所需的 `kerneldlls\kerneldll.ini` 和 `kerneldlls\usbcan.dll`,构建时会自动保留目录结构并复制到程序目录。
3. CANH/CANL 接入 BMS 外部 CAN,并确认终端电阻。
4. 选择 CAN1 或 CAN2,点击“连接”。
项目默认使用 x86 构建,以匹配当前 CANTest 配套的 `ControlCAN.dll`:
```powershell
dotnet build BmsMonitor.App/BmsMonitor.App.csproj -c Release -p:Platform=x86
```
## 安全边界
本项目只包含打开、初始化、启动、接收、复位和关闭 API,不包含任何发送或参数配置功能。