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
+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();
}