Initial commit: BMS CAN monitor
This commit is contained in:
+20
@@ -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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)]
|
||||
@@ -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>
|
||||
@@ -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 波特率。")
|
||||
};
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,不包含任何发送或参数配置功能。
|
||||
Reference in New Issue
Block a user