Initial commit: BMS CAN monitor
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user