From 52d486e2b1b8dade34c383c4aeb9b85bb7a08bd3 Mon Sep 17 00:00:00 2001 From: hsc Date: Fri, 10 Jul 2026 09:56:59 +0800 Subject: [PATCH] =?UTF-8?q?can=E4=BF=A1=E5=8F=B7=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E6=9B=B2=E7=BA=BF=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MonitorModule/ViewModels/MonitorViewModel.cs | 199 +++++++++++++++++- .../GlobalVariable/CANSignalBroadcaster.cs | 114 ++++++++++ UIShare/GlobalVariable/DeviceManager.cs | 53 +++-- UIShare/PubEvent/DBCFileNotFoundEvent.cs | 17 -- UIShare/PubEvent/DBCLoadedEvent.cs | 17 ++ UIShare/PubEvent/DBCUnloadedEvent.cs | 18 ++ UIShare/UIViewModel/DeviceInfoVM.cs | 7 +- 7 files changed, 383 insertions(+), 42 deletions(-) create mode 100644 UIShare/GlobalVariable/CANSignalBroadcaster.cs delete mode 100644 UIShare/PubEvent/DBCFileNotFoundEvent.cs create mode 100644 UIShare/PubEvent/DBCLoadedEvent.cs create mode 100644 UIShare/PubEvent/DBCUnloadedEvent.cs diff --git a/MonitorModule/ViewModels/MonitorViewModel.cs b/MonitorModule/ViewModels/MonitorViewModel.cs index 5fd15d4..44b685d 100644 --- a/MonitorModule/ViewModels/MonitorViewModel.cs +++ b/MonitorModule/ViewModels/MonitorViewModel.cs @@ -22,6 +22,7 @@ using UIShare.GlobalVariable; using UIShare.PubEvent; using UIShare.UIViewModel; using UIShare.ViewModelBase; +using ZLGUSBCANFD; namespace MonitorModule.ViewModels @@ -119,6 +120,8 @@ namespace MonitorModule.ViewModels /// 事件订阅令牌,用于 Dispose 时取消订阅 private SubscriptionToken? _subscriptionToken; + private SubscriptionToken? _dbcLoadedToken; + private SubscriptionToken? _dbcUnloadedToken; // ========================================== // 批量入库核心结构 @@ -166,6 +169,10 @@ namespace MonitorModule.ViewModels // 取消 EventAggregator 订阅 if (_subscriptionToken != null) _eventAggregator.GetEvent().Unsubscribe(_subscriptionToken); + if (_dbcLoadedToken != null) + _eventAggregator.GetEvent().Unsubscribe(_dbcLoadedToken); + if (_dbcUnloadedToken != null) + _eventAggregator.GetEvent().Unsubscribe(_dbcUnloadedToken); if (_dbFlushTask != null) { @@ -332,6 +339,15 @@ namespace MonitorModule.ViewModels _subscriptionToken = _eventAggregator.GetEvent() .Subscribe(OnHardwareDataReceived, ThreadOption.UIThread); + // 5b. 订阅 DBC 加载/卸载事件,动态发现或清除 CAN 信号 + _dbcLoadedToken = _eventAggregator.GetEvent() + .Subscribe(OnDbcLoaded, ThreadOption.UIThread); + _dbcUnloadedToken = _eventAggregator.GetEvent() + .Subscribe(OnDbcUnloaded, ThreadOption.UIThread); + + // 5c. 根据 SystemConfig.ConfigurationList 刷新已加载 DBC 中的 CAN 信号 + RefreshConfiguredCanSignals(); + // 6. 启动 OxyPlot 视觉刷新定时器 _stopwatch.Start(); _plotRefreshTimer.Start(); @@ -449,7 +465,6 @@ namespace MonitorModule.ViewModels private void RestoreChannelsFromConfig() { if (_systemConfig?.MonitorChannels == null || _systemConfig.MonitorChannels.Count == 0) return; - if (AvailableMethods.Count == 0) return; int restored = 0; foreach (var config in _systemConfig.MonitorChannels) @@ -508,6 +523,188 @@ namespace MonitorModule.ViewModels StatusMessage = $"已从配置自动恢复 {restored} 个监测通道"; } } + + /// + /// 根据 SystemConfig.ConfigurationList 刷新所有已加载 DBC 中的 CAN 信号。 + /// 只添加 ConfigurationList 中配置且实际存在于 DBC 的信号;不存在则清理。 + /// + private void RefreshConfiguredCanSignals() + { + if (_systemConfig?.ConfigurationList == null || _systemConfig.ConfigurationList.Count == 0) return; + if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return; + + var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; + foreach (var channel in _systemConfig.ConfigurationList.Select(c => c.Channel).Distinct()) + { + if (channel < 0 || channel >= msgDb.Count) continue; + RefreshCanSignalsForChannel((uint)channel, msgDb[channel]); + } + } + + /// + /// 刷新单个通道的 CAN 信号:以 ConfigurationList 为白名单,对照 DBC 实际存在性。 + /// - DBC 中存在 → 加入 AvailableMethods;若 MonitorChannels 中也有,则恢复 Channel。 + /// - DBC 中不存在 → 从 AvailableMethods 移除;若已在 Channels 中,则删除。 + /// + private void RefreshCanSignalsForChannel(uint channel, List<_Msg_> messages) + { + if (_systemConfig?.ConfigurationList == null) return; + + string fingerprint = CANSignalBroadcaster.BuildFingerprint(channel); + var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList(); + bool changed = false; + + foreach (var cfg in configs) + { + if (string.IsNullOrEmpty(cfg.SignalName)) continue; + + string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName); + bool existsInDbc = messages.Any(m => + m.msg_id == cfg.MessageID && + m.signal_Name != null && + m.signal_Name.Contains(cfg.SignalName)); + + if (existsInDbc) + { + // 加入 AvailableMethods(去重) + if (!AvailableMethods.Any(m => m.Fingerprint == fingerprint && m.MethodName == methodName)) + { + string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName); + AvailableMethods.Add(new AvailableMethodItem + { + DeviceName = $"CAN{channel}", + Fingerprint = fingerprint, + MethodName = methodName, + DisplayName = displayName, + MethodInfo = null!, + Device = null! + }); + changed = true; + } + + // 如果 MonitorChannels 中有持久化记录,自动恢复 Channel + var monitorConfig = _systemConfig.MonitorChannels?.FirstOrDefault(m => + m.Fingerprint == fingerprint && m.MethodName == methodName); + if (monitorConfig != null && !Channels.Any(c => + c.Fingerprint == fingerprint && c.MethodName == methodName)) + { + AddCanChannel(cfg, fingerprint, methodName, monitorConfig.IsDisplayed); + changed = true; + } + } + else + { + // DBC 中不存在:从 AvailableMethods 移除 + var methodToRemove = AvailableMethods.FirstOrDefault(m => + m.Fingerprint == fingerprint && m.MethodName == methodName); + if (methodToRemove != null) + { + AvailableMethods.Remove(methodToRemove); + changed = true; + } + + // 如果该信号已经在 Channels(或 MonitorChannels)中,删除 + var channelToRemove = Channels.FirstOrDefault(c => + c.Fingerprint == fingerprint && c.MethodName == methodName); + if (channelToRemove != null) + { + if (channelToRemove.Series != null) Plot.Series.Remove(channelToRemove.Series); + Channels.Remove(channelToRemove); + changed = true; + } + } + } + + if (changed) + { + Plot.InvalidatePlot(true); + StatusMessage = $"CAN{channel} 已刷新配置信号"; + } + } + + /// + /// DBCLoadedEvent 回调:DBC 加载后刷新该通道的配置信号。 + /// + private void OnDbcLoaded(DBCLoadedArgs args) + { + if (args.Scope != TestStatus) return; + if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return; + + int channel = (int)args.Channel; + var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; + if (channel < 0 || channel >= msgDb.Count) return; + + RefreshCanSignalsForChannel(args.Channel, msgDb[channel]); + } + + /// + /// DBCUnloadedEvent 回调:移除对应通道的 CAN 信号(AvailableMethods + Channels)。 + /// + private void OnDbcUnloaded(DBCUnloadedArgs args) + { + if (args.Scope != TestStatus) return; + + string fingerprint = CANSignalBroadcaster.BuildFingerprint(args.Channel); + + // 从 AvailableMethods 中移除 + var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList(); + foreach (var m in toRemoveMethods) + AvailableMethods.Remove(m); + + // 从 Channels 中移除(并清理 Plot) + var toRemoveChannels = Channels.Where(c => c.Fingerprint == fingerprint).ToList(); + foreach (var c in toRemoveChannels) + { + if (c.Series != null) Plot.Series.Remove(c.Series); + Channels.Remove(c); + } + + if (toRemoveMethods.Count > 0 || toRemoveChannels.Count > 0) + { + Plot.InvalidatePlot(true); + StatusMessage = $"CAN{args.Channel} DBC 已卸载,移除 {toRemoveMethods.Count} 个信号"; + } + } + + /// + /// 将单个 CAN 信号添加为监测通道。 + /// + private void AddCanChannel(CANSignalConfig cfg, string fingerprint, string methodName, bool isDisplayed) + { + var color = _palette[_colorIndex % _palette.Length]; + _colorIndex++; + + string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName); + var channel = new MonitorChannelVM + { + DeviceName = $"CAN{cfg.Channel}", + Fingerprint = fingerprint, + MethodName = methodName, + DisplayName = displayName, + Color = color, + IsMonitored = true, + IsDisplayed = isDisplayed + }; + + if (channel.IsDisplayed) + { + channel.Series = new LineSeries + { + Title = channel.DisplayName, + Color = color, + StrokeThickness = 1.5 + }; + Plot.Series.Add(channel.Series); + } + + channel.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed)) + OnChannelDisplayChanged(channel); + }; + + Channels.Add(channel); + } #endregion #region 命令处理 diff --git a/UIShare/GlobalVariable/CANSignalBroadcaster.cs b/UIShare/GlobalVariable/CANSignalBroadcaster.cs new file mode 100644 index 0000000..e207a4c --- /dev/null +++ b/UIShare/GlobalVariable/CANSignalBroadcaster.cs @@ -0,0 +1,114 @@ +using Model.Models; +using Prism.Events; +using System; +using System.Text; +using UIShare.PubEvent; +using ZLGUSBCANFD; + +namespace UIShare.GlobalVariable +{ + /// + /// CAN 信号广播器:订阅 ZLGCANFD.OnDbcMessageDecoded 事件, + /// 将解码后的 DBC 信号值通过 HardwareDataReportedEvent 广播给监控系统。 + /// 由 DeviceManager 创建和管理,生命周期与 CAN 设备绑定。 + /// + public class CANSignalBroadcaster : IDisposable + { + private readonly IEventAggregator _eventAggregator; + private readonly string _scopeName; + private ZLGCANFD? _canfd; + private bool _disposed; + + public CANSignalBroadcaster(IEventAggregator eventAggregator, string scopeName) + { + _eventAggregator = eventAggregator; + _scopeName = scopeName; + } + + /// + /// 绑定 CANFD 实例并订阅解码事件。 + /// + public void Bind(ZLGCANFD canfd) + { + Unbind(); + _canfd = canfd; + _canfd.OnDbcMessageDecoded += OnDbcMessageDecoded; + } + + /// + /// 取消订阅。 + /// + public void Unbind() + { + if (_canfd != null) + { + _canfd.OnDbcMessageDecoded -= OnDbcMessageDecoded; + _canfd = null; + } + } + + /// + /// DBC 解码回调:提取所有信号并广播。 + /// + private void OnDbcMessageDecoded(uint channel, ZDBC.DBCMessage msg) + { + if (_disposed) return; + + string fingerprint = $"CAN:{channel}"; + var now = DateTime.Now; + + for (int i = 0; i < msg.nSignalCount; i++) + { + var signal = msg.vSignals[i]; + string signalName = Encoding.Default.GetString(signal.strName).TrimEnd('\0'); + if (string.IsNullOrEmpty(signalName)) continue; + + // 计算物理值:physical = raw * factor + offset + double physicalValue = signal.nRawvalue * signal.nFactor + signal.nOffset; + + // MethodName 格式:"{MessageId:X}.{SignalName}" + string methodName = $"{msg.nID:X}.{signalName}"; + + _eventAggregator.GetEvent().Publish(new HardwareReportArgs + { + Scope = _scopeName, + HardwareFingerprint = fingerprint, + MethodName = methodName, + Value = physicalValue, + Time = now + }); + } + } + + /// + /// 生成 CAN 信号的 DisplayName 格式:"{MessageName}.{SignalName}" + /// + public static string BuildDisplayName(string messageName, string signalName) + { + return $"{messageName}.{signalName}"; + } + + /// + /// 生成 CAN 信号的 MethodName 格式:"{MessageId:X}.{SignalName}" + /// + public static string BuildMethodName(uint messageId, string signalName) + { + return $"{messageId:X}.{signalName}"; + } + + /// + /// 生成 CAN 信号的 Fingerprint 格式:"CAN:{Channel}" + /// + public static string BuildFingerprint(uint channel) + { + return $"CAN:{channel}"; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + Unbind(); + } + } +} diff --git a/UIShare/GlobalVariable/DeviceManager.cs b/UIShare/GlobalVariable/DeviceManager.cs index 8fd6e44..c0bacdc 100644 --- a/UIShare/GlobalVariable/DeviceManager.cs +++ b/UIShare/GlobalVariable/DeviceManager.cs @@ -27,6 +27,9 @@ namespace UIShare.GlobalVariable private readonly string _scopeName; private readonly IEventAggregator _eventAggregator; + /// CAN 信号广播器:将 DBC 解码后的信号值广播给监控系统 + private CANSignalBroadcaster? _canSignalBroadcaster; + /// 按 DeviceName 索引的设备字典,便于业务层按名取实例。 public IDictionary DeviceMap { get; private set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -412,7 +415,7 @@ namespace UIShare.GlobalVariable } /// - /// 打开 CAN 卡:打开设备 + 初始化并启动所有通道 + 自动加载 DBC + /// 打开 CAN 卡:打开设备 + 初始化并启动所有通道 + 自动加载 DBC + 启动信号广播 /// private async Task ConnectCanAsync(DeviceInfoVM info) { @@ -436,51 +439,49 @@ namespace UIShare.GlobalVariable bool ok = await Task.Run(() => { if (!CANFD.打开设备()) return false; - - // 初始化并启动所有通道 - int maxCh = CANFD.DBCParser.MaxChannels; - for (uint i = 0; i < maxCh; i++) - { - try - { - CANFD.初始化并启动通道(i); - } - catch (Exception ex) - { - LoggerHelper.Warn($"CAN 通道 {i} 初始化失败:{ex.Message}"); - break; - } - } + // 启动 CAN 信号广播器 + _canSignalBroadcaster = new CANSignalBroadcaster(_eventAggregator, _scopeName); + _canSignalBroadcaster.Bind(CANFD); // 自动加载 DBC 文件 if (_systemConfig?.DBCAutoLoadList != null) { foreach (var item in _systemConfig.DBCAutoLoadList) { - if (item.DBCChannel < 0 || item.DBCChannel >= maxCh) continue; + if (item.DBCChannel < 0 || item.DBCChannel >= 4) continue; if (string.IsNullOrWhiteSpace(item.DBCFilePath)) continue; if (!File.Exists(item.DBCFilePath)) { - // 文件不存在,发布事件广播 - _eventAggregator.GetEvent().Publish(new DBCFileNotFoundArgs + // 发布 DBC 卸载事件,通知监控系统清除对应信号 + _eventAggregator.GetEvent().Publish(new DBCUnloadedArgs { - Channel = item.DBCChannel, - FilePath = item.DBCFilePath, + Channel = (uint)item.DBCChannel, Scope = _scopeName }); LoggerHelper.Warn($"CAN 通道 {item.DBCChannel} 自动加载 DBC 失败:文件不存在 [{item.DBCFilePath}]"); continue; } - + CANFD.初始化并启动通道((uint)item.DBCChannel); bool loadOk = CANFD.加载通道DBC文件((uint)item.DBCChannel, item.DBCFilePath); if (loadOk) { LoggerHelper.Info($"CAN 通道 {item.DBCChannel} 已自动加载 DBC:{item.DBCFilePath}"); + // 发布 DBC 加载完成事件,通知监控系统刷新信号列表 + _eventAggregator.GetEvent().Publish(new DBCLoadedArgs + { + Channel = (uint)item.DBCChannel, + Scope = _scopeName + }); } else { LoggerHelper.Warn($"CAN 通道 {item.DBCChannel} 自动加载 DBC 失败:{item.DBCFilePath}"); + _eventAggregator.GetEvent().Publish(new DBCUnloadedArgs + { + Channel = (uint)item.DBCChannel, + Scope = _scopeName + }); } } } @@ -527,6 +528,10 @@ namespace UIShare.GlobalVariable return; } + // 先释放 CAN 信号广播器,取消事件订阅,防止关闭过程中仍触发回调 + _canSignalBroadcaster?.Dispose(); + _canSignalBroadcaster = null; + await Task.Run(() => CANFD.关闭CAN卡设备()); LoggerHelper.Info($"CAN 设备 [{name}] 已成功关闭连接。"); } @@ -586,6 +591,10 @@ namespace UIShare.GlobalVariable public void Dispose() { + // 释放 CAN 信号广播器 + _canSignalBroadcaster?.Dispose(); + _canSignalBroadcaster = null; + if (string.IsNullOrEmpty(_scopeName)) return; // 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用 diff --git a/UIShare/PubEvent/DBCFileNotFoundEvent.cs b/UIShare/PubEvent/DBCFileNotFoundEvent.cs deleted file mode 100644 index 66f62dd..0000000 --- a/UIShare/PubEvent/DBCFileNotFoundEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace UIShare.PubEvent -{ - /// - /// DBC 自动加载时找不到文件事件。 - /// 参数:(通道号, 文件路径) - /// - public class DBCFileNotFoundEvent : PubSubEvent - { - } - - public class DBCFileNotFoundArgs - { - public int Channel { get; set; } - public string FilePath { get; set; } = string.Empty; - public string Scope { get; set; } = string.Empty; - } -} diff --git a/UIShare/PubEvent/DBCLoadedEvent.cs b/UIShare/PubEvent/DBCLoadedEvent.cs new file mode 100644 index 0000000..1e5e34e --- /dev/null +++ b/UIShare/PubEvent/DBCLoadedEvent.cs @@ -0,0 +1,17 @@ +namespace UIShare.PubEvent +{ + /// + /// DBC 加载完成事件:通知订阅者某个通道的 DBC 已加载,可以刷新信号列表。 + /// + public class DBCLoadedEvent : PubSubEvent + { + } + + public class DBCLoadedArgs + { + public uint Channel { get; set; } + public string Scope { get; set; } = string.Empty; + } + + +} diff --git a/UIShare/PubEvent/DBCUnloadedEvent.cs b/UIShare/PubEvent/DBCUnloadedEvent.cs new file mode 100644 index 0000000..c987193 --- /dev/null +++ b/UIShare/PubEvent/DBCUnloadedEvent.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UIShare.PubEvent +{ + public class DBCUnloadedEvent : PubSubEvent + { + } + + public class DBCUnloadedArgs + { + public uint Channel { get; set; } + public string Scope { get; set; } = string.Empty; + } +} diff --git a/UIShare/UIViewModel/DeviceInfoVM.cs b/UIShare/UIViewModel/DeviceInfoVM.cs index bab204c..441877a 100644 --- a/UIShare/UIViewModel/DeviceInfoVM.cs +++ b/UIShare/UIViewModel/DeviceInfoVM.cs @@ -1,5 +1,7 @@  +using Newtonsoft.Json; + namespace UIShare.UIViewModel { public class DeviceInfoVM : BindableBase @@ -31,8 +33,9 @@ namespace UIShare.UIViewModel get => _isEnabled; set => SetProperty(ref _isEnabled, value); } - - private bool _isConnected; + + private bool _isConnected=false; + [JsonIgnore] public bool IsConnected { get => _isConnected;