添加项目文件。
This commit is contained in:
852
MonitorModule/ViewModels/MonitorViewModel.cs
Normal file
852
MonitorModule/ViewModels/MonitorViewModel.cs
Normal file
@@ -0,0 +1,852 @@
|
||||
using Common.Attributes;
|
||||
using Model.Entity;
|
||||
using Model.Models;
|
||||
using NLog;
|
||||
using OxyPlot;
|
||||
using OxyPlot.Axes;
|
||||
using OxyPlot.Legends;
|
||||
using OxyPlot.Series;
|
||||
using Service.Interface;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
using UIShare.ViewModelBase;
|
||||
using ZLGUSBCANFD;
|
||||
|
||||
|
||||
namespace MonitorModule.ViewModels
|
||||
{
|
||||
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||
{
|
||||
#region 属性
|
||||
private IMonitorValueService _monitorValueService;
|
||||
public bool KeepAlive => true;
|
||||
public ScopedContext _scopedContext { get; set; } = null!;
|
||||
public GlobalInfo _globalInfo { get; set; }
|
||||
|
||||
private string _testStatus = string.Empty;
|
||||
public string TestStatus
|
||||
{
|
||||
get => _testStatus;
|
||||
set => SetProperty(ref _testStatus, value);
|
||||
}
|
||||
|
||||
public PlotModel Plot { get; }
|
||||
|
||||
/// <summary>已添加的监测通道列表</summary>
|
||||
public ObservableCollection<MonitorChannelVM> Channels
|
||||
{
|
||||
get => _systemConfig?.Channels ?? new ObservableCollection<MonitorChannelVM>();
|
||||
set
|
||||
{
|
||||
if (_systemConfig != null && _systemConfig.Channels != value)
|
||||
{
|
||||
_systemConfig.Channels = value;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>可添加的设备方法列表(供用户选择)</summary>
|
||||
public ObservableCollection<AvailableMethodItem> AvailableMethods { get; } = new();
|
||||
|
||||
private MonitorChannelVM? _selectedChannel;
|
||||
public MonitorChannelVM? SelectedChannel
|
||||
{
|
||||
get => _selectedChannel;
|
||||
set => SetProperty(ref _selectedChannel, value);
|
||||
}
|
||||
|
||||
private AvailableMethodItem? _selectedAvailableMethod;
|
||||
public AvailableMethodItem? SelectedAvailableMethod
|
||||
{
|
||||
get => _selectedAvailableMethod;
|
||||
set => SetProperty(ref _selectedAvailableMethod, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = "图表已就绪,暂无监测项";
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand AddChannelCommand { get; }
|
||||
public ICommand DeleteChannelCommand { get; }
|
||||
public ICommand ResetViewCommand { get; }
|
||||
public ICommand RefreshDataCommand { get; }
|
||||
public ICommand RefreshCommand { get; }
|
||||
public ICommand SaveCommand { get; }
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
private bool IsInitiated = false;
|
||||
private IScopedProvider? _scope;
|
||||
private DeviceManager? _deviceManager;
|
||||
private SystemConfig? _systemConfig;
|
||||
private CancellationTokenSource? _monitorCTS = new();
|
||||
|
||||
// 颜色调色盘
|
||||
private static readonly OxyColor[] _palette =
|
||||
{
|
||||
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
|
||||
OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod,
|
||||
OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab
|
||||
};
|
||||
private int _colorIndex;
|
||||
|
||||
/// <summary>设备数据广播器(全局单例)</summary>
|
||||
private HardwareDataBroadcaster? _broadcaster;
|
||||
|
||||
/// <summary>CAN 信号广播器(全局单例)</summary>
|
||||
private CANSignalBroadcaster? _canSignalBroadcaster;
|
||||
|
||||
/// <summary>用于 OxyPlot X 轴相对秒数</summary>
|
||||
private readonly Stopwatch _stopwatch = new();
|
||||
|
||||
/// <summary>OxyPlot 刷新定时器(仅负责滚动 X 轴 + InvalidatePlot)</summary>
|
||||
private readonly DispatcherTimer _plotRefreshTimer;
|
||||
|
||||
/// <summary>事件订阅令牌,用于 Dispose 时取消订阅</summary>
|
||||
private SubscriptionToken? _subscriptionToken;
|
||||
private SubscriptionToken? _dbcLoadedToken;
|
||||
private SubscriptionToken? _dbcUnloadedToken;
|
||||
|
||||
// ==========================================
|
||||
// 批量入库核心结构
|
||||
// ==========================================
|
||||
private readonly ConcurrentQueue<MonitorValueEntity> _insertQueue = new();
|
||||
private Task? _dbFlushTask;
|
||||
private const int BulkInsertThreshold = 50;
|
||||
private int _isFlushing = 0;
|
||||
#endregion
|
||||
|
||||
public MonitorViewModel(IContainerExtension container) : base(container)
|
||||
{
|
||||
_globalInfo = container.Resolve<GlobalInfo>();
|
||||
_monitorValueService = container.Resolve<IMonitorValueService>();
|
||||
|
||||
Plot = BuildEmptyPlot();
|
||||
|
||||
AddChannelCommand = new DelegateCommand(OnAddChannel);
|
||||
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
|
||||
ResetViewCommand = new DelegateCommand(OnResetView);
|
||||
RefreshDataCommand = new DelegateCommand(OnRefreshData);
|
||||
RefreshCommand = new DelegateCommand(OnExpand);
|
||||
SaveCommand = new DelegateCommand(OnSave);
|
||||
|
||||
// OxyPlot 刷新定时器(1000ms,只负责视觉更新)
|
||||
_plotRefreshTimer = new DispatcherTimer(DispatcherPriority.Background)
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(1000)
|
||||
};
|
||||
_plotRefreshTimer.Tick += OnPlotRefreshTick;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_monitorCTS?.Cancel();
|
||||
_monitorCTS?.Dispose();
|
||||
_monitorCTS = null;
|
||||
|
||||
_plotRefreshTimer.Stop();
|
||||
_plotRefreshTimer.Tick -= OnPlotRefreshTick;
|
||||
_stopwatch.Stop();
|
||||
|
||||
// 取消 EventAggregator 订阅
|
||||
if (_subscriptionToken != null)
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Unsubscribe(_subscriptionToken);
|
||||
if (_dbcLoadedToken != null)
|
||||
_eventAggregator.GetEvent<DBCLoadedEvent>().Unsubscribe(_dbcLoadedToken);
|
||||
if (_dbcUnloadedToken != null)
|
||||
_eventAggregator.GetEvent<DBCUnloadedEvent>().Unsubscribe(_dbcUnloadedToken);
|
||||
|
||||
if (_dbFlushTask != null)
|
||||
{
|
||||
try { _dbFlushTask.Wait(TimeSpan.FromSeconds(1.5)); } catch { }
|
||||
}
|
||||
|
||||
|
||||
// 3. 释放容器作用域
|
||||
_scope?.Dispose();
|
||||
Channels.Clear();
|
||||
AvailableMethods.Clear();
|
||||
}
|
||||
private void OnSave()
|
||||
{
|
||||
// 将当前 Channels 序列化为 MonitorChannelConfig 列表保存到 MonitorChannels
|
||||
if (_systemConfig != null)
|
||||
{
|
||||
_systemConfig.MonitorChannels = new ObservableCollection<MonitorChannelConfig>(Channels
|
||||
.Select(c => new MonitorChannelConfig
|
||||
{
|
||||
Fingerprint = c.Fingerprint,
|
||||
MethodName = c.MethodName,
|
||||
IsDisplayed = c.IsDisplayed
|
||||
}));
|
||||
}
|
||||
ConfigService.Save(_systemConfig);
|
||||
}
|
||||
#region 事件驱动:接收广播数据
|
||||
/// <summary>
|
||||
/// 由 HardwareDataBroadcaster 通过 EventAggregator 广播触发。
|
||||
/// 已通过 ThreadOption.UIThread 确保在 UI 线程执行。
|
||||
/// 通过 HardwareFingerprint + MethodName 匹配监测通道。
|
||||
/// </summary>
|
||||
private void OnHardwareDataReceived(HardwareReportArgs args)
|
||||
{
|
||||
// 仅处理属于当前 Scope 的事件
|
||||
if (string.IsNullOrEmpty(TestStatus) || args.Scope != TestStatus) return;
|
||||
|
||||
// 以硬件指纹 + 方法名唯一匹配通道(与逻辑名解耦)
|
||||
var channel = Channels.FirstOrDefault(c =>
|
||||
c.Fingerprint == args.HardwareFingerprint &&
|
||||
c.MethodName == args.MethodName &&
|
||||
c.IsMonitored);
|
||||
|
||||
if (channel == null) return;
|
||||
|
||||
// 记录数据点(线程安全:Record 内部 ConcurrentQueue + Series 操作)
|
||||
channel.Record(args.Time, args.Value);
|
||||
|
||||
// 入队数据库批量写入
|
||||
// CreateTime 使用 args.Time(采样触发时刻),而不是 DateTime.Now(设备响应到达时刻),
|
||||
// 这样即使设备响应有延迟,数据库里的时间戳仍然按设定采样间隔分布。
|
||||
var entity = new MonitorValueEntity
|
||||
{
|
||||
MonitorName = channel.DisplayName,
|
||||
MonitorValue = args.Value,
|
||||
Scope = args.Scope,
|
||||
CreateTime = args.Time,
|
||||
IsDel = 0
|
||||
};
|
||||
_insertQueue.Enqueue(entity);
|
||||
|
||||
if (_insertQueue.Count >= BulkInsertThreshold)
|
||||
{
|
||||
_ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OxyPlot 视觉刷新定时器
|
||||
private void OnPlotRefreshTick(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_stopwatch.IsRunning) return;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
|
||||
if (xAxis != null)
|
||||
{
|
||||
xAxis.Minimum = DateTimeAxis.ToDouble(now.AddSeconds(-20));
|
||||
xAxis.Maximum = DateTimeAxis.ToDouble(now.AddSeconds(0.5));
|
||||
}
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 后台数据库批量入库
|
||||
private void StartDbFlushWorker(CancellationToken token)
|
||||
{
|
||||
_dbFlushTask = Task.Run(async () =>
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
await DoFlushWorkAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DB Worker Error] {ex.Message}");
|
||||
}
|
||||
}
|
||||
await DoFlushWorkAsync().ConfigureAwait(false);
|
||||
}, token);
|
||||
}
|
||||
|
||||
private async Task DoFlushWorkAsync()
|
||||
{
|
||||
if (_insertQueue.IsEmpty) return;
|
||||
if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
var listToInsert = new List<MonitorValueEntity>();
|
||||
while (_insertQueue.TryDequeue(out var entity)) listToInsert.Add(entity);
|
||||
|
||||
if (listToInsert.Count > 0)
|
||||
{
|
||||
var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally { Interlocked.Exchange(ref _isFlushing, 0); }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Navigation 导航
|
||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||
{
|
||||
base.OnNavigatedTo(navigationContext);
|
||||
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
|
||||
{
|
||||
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
||||
Plot.Title = $"监控 - {TestStatus}";
|
||||
Plot.InvalidatePlot(false);
|
||||
|
||||
_scope = _globalInfo.ScopeDic[TestStatus];
|
||||
_scopedContext = _scope.Resolve<ScopedContext>();
|
||||
_deviceManager = _scope.Resolve<DeviceManager>();
|
||||
_systemConfig = _scope.Resolve<SystemConfig>();
|
||||
RaisePropertyChanged(nameof(Channels));
|
||||
_broadcaster = _scope.Resolve<HardwareDataBroadcaster>();
|
||||
_canSignalBroadcaster = _scope.Resolve<CANSignalBroadcaster>();
|
||||
IsInitiated = true;
|
||||
|
||||
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
|
||||
|
||||
// 1. 启动后台数据库消费
|
||||
StartDbFlushWorker(_monitorCTS.Token);
|
||||
|
||||
// 2. 扫描可监测方法(供 UI 选择)
|
||||
DiscoverAvailableMethods();
|
||||
|
||||
// 3. 从配置自动恢复已保存的监测通道
|
||||
RestoreChannelsFromConfig();
|
||||
|
||||
// 4. 启动广播器(Discover + Start)
|
||||
_broadcaster.Discover();
|
||||
_broadcaster.Start();
|
||||
_canSignalBroadcaster.Discover();
|
||||
_canSignalBroadcaster.Start();
|
||||
|
||||
// 5. 订阅 HardwareDataReportedEvent(UI 线程回调)
|
||||
_subscriptionToken = _eventAggregator.GetEvent<HardwareDataReportedEvent>()
|
||||
.Subscribe(OnHardwareDataReceived, ThreadOption.UIThread);
|
||||
|
||||
// 5b. 订阅 DBC 加载/卸载事件,动态发现或清除 CAN 信号
|
||||
_dbcLoadedToken = _eventAggregator.GetEvent<DBCLoadedEvent>()
|
||||
.Subscribe(OnDbcLoaded, ThreadOption.UIThread);
|
||||
_dbcUnloadedToken = _eventAggregator.GetEvent<DBCUnloadedEvent>()
|
||||
.Subscribe(OnDbcUnloaded, ThreadOption.UIThread);
|
||||
|
||||
// 5c. 根据 SystemConfig.ConfigurationList 刷新已加载 DBC 中的 CAN 信号
|
||||
RefreshConfiguredCanSignals();
|
||||
|
||||
// 6. 启动 OxyPlot 视觉刷新定时器
|
||||
_stopwatch.Start();
|
||||
_plotRefreshTimer.Start();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PlotModel 构建
|
||||
private static PlotModel BuildEmptyPlot()
|
||||
{
|
||||
var pm = new PlotModel
|
||||
{
|
||||
Title = string.Empty,
|
||||
PlotAreaBorderColor = OxyColors.LightGray,
|
||||
Background = OxyColors.White
|
||||
};
|
||||
pm.Axes.Add(new DateTimeAxis
|
||||
{
|
||||
Position = AxisPosition.Bottom,
|
||||
Title = "时间",
|
||||
StringFormat = "HH:mm:ss",
|
||||
MajorGridlineStyle = LineStyle.Dot,
|
||||
MinorGridlineStyle = LineStyle.None
|
||||
});
|
||||
pm.Axes.Add(new LinearAxis
|
||||
{
|
||||
Position = AxisPosition.Left,
|
||||
Title = "值",
|
||||
MajorGridlineStyle = LineStyle.Dot,
|
||||
MinorGridlineStyle = LineStyle.None
|
||||
});
|
||||
pm.Legends.Add(new Legend
|
||||
{
|
||||
LegendPosition = LegendPosition.RightTop,
|
||||
LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
|
||||
LegendBorder = OxyColors.LightGray
|
||||
});
|
||||
return pm;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 设备方法发现(仅用于 UI 展示可选项)
|
||||
/// <summary>
|
||||
/// 通过 GlobalInfo.HardwarePool 构建 设备实例引用→硬件指纹 的反向查找表。
|
||||
/// </summary>
|
||||
private Dictionary<object, string> BuildInstanceToFingerprint()
|
||||
{
|
||||
var map = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||
{
|
||||
var lazy = poolEntry.Value;
|
||||
if (lazy.IsValueCreated && lazy.Value != null)
|
||||
map[lazy.Value] = poolEntry.Key;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private void DiscoverAvailableMethods()
|
||||
{
|
||||
AvailableMethods.Clear();
|
||||
if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0)
|
||||
{
|
||||
StatusMessage = "当前工位无可用设备";
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceToFp = BuildInstanceToFingerprint();
|
||||
|
||||
foreach (var kvp in _deviceManager.DeviceMap)
|
||||
{
|
||||
string deviceName = kvp.Key;
|
||||
var device = kvp.Value;
|
||||
var deviceType = device.GetType();
|
||||
|
||||
// 反查该实例的硬件指纹
|
||||
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
|
||||
continue;
|
||||
|
||||
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m =>
|
||||
{
|
||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
||||
var parms = m.GetParameters();
|
||||
return parms.Length == 0 ||
|
||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||
});
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var attr = method.GetCustomAttribute<MonitorableAttribute>();
|
||||
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
||||
? $"{deviceName}.{attr.Description}"
|
||||
: $"{deviceName}.{method.Name}";
|
||||
|
||||
AvailableMethods.Add(new AvailableMethodItem
|
||||
{
|
||||
DeviceName = deviceName,
|
||||
Fingerprint = fingerprint,
|
||||
MethodName = method.Name,
|
||||
DisplayName = displayName,
|
||||
MethodInfo = method,
|
||||
Device = device
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
StatusMessage = AvailableMethods.Count > 0
|
||||
? $"发现 {AvailableMethods.Count} 个可监测项"
|
||||
: "未发现可监测的设备方法";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 SystemConfig.MonitorChannels 持久化列表自动还原监测通道。
|
||||
/// </summary>
|
||||
private void RestoreChannelsFromConfig()
|
||||
{
|
||||
if (_systemConfig?.MonitorChannels == null || _systemConfig.MonitorChannels.Count == 0) return;
|
||||
|
||||
int restored = 0;
|
||||
foreach (var config in _systemConfig.MonitorChannels)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Fingerprint) ||
|
||||
string.IsNullOrWhiteSpace(config.MethodName)) continue;
|
||||
|
||||
// 已在 Channels 中存在则跳过
|
||||
if (Channels.Any(c => c.Fingerprint == config.Fingerprint && c.MethodName == config.MethodName))
|
||||
continue;
|
||||
|
||||
// 在 AvailableMethods 中查找匹配项
|
||||
var method = AvailableMethods.FirstOrDefault(m =>
|
||||
m.Fingerprint == config.Fingerprint && m.MethodName == config.MethodName);
|
||||
if (method == null) continue;
|
||||
|
||||
var color = _palette[_colorIndex % _palette.Length];
|
||||
_colorIndex++;
|
||||
|
||||
var channel = new MonitorChannelVM
|
||||
{
|
||||
DeviceName = method.DeviceName,
|
||||
Fingerprint = method.Fingerprint,
|
||||
MethodName = method.MethodName,
|
||||
DisplayName = method.DisplayName,
|
||||
Color = color,
|
||||
IsMonitored = true,
|
||||
IsDisplayed = config.IsDisplayed
|
||||
};
|
||||
|
||||
// 仅当 IsDisplayed=true 时才创建 LineSeries
|
||||
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);
|
||||
restored++;
|
||||
}
|
||||
|
||||
if (restored > 0)
|
||||
{
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"已从配置自动恢复 {restored} 个监测通道";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 SystemConfig.ConfigurationList 刷新所有已加载 DBC 中的 CAN 信号。
|
||||
/// 只添加 ConfigurationList 中配置且实际存在于 DBC 的信号;不存在则清理。
|
||||
/// </summary>
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新单个通道的 CAN 信号:以 ConfigurationList 为白名单,对照 DBC 实际存在性。
|
||||
/// - DBC 中存在 → 加入 AvailableMethods;若 MonitorChannels 中也有,则恢复 Channel。
|
||||
/// - DBC 中不存在 → 从 AvailableMethods 移除;若已在 Channels 中,则删除。
|
||||
/// </summary>
|
||||
private void RefreshCanSignalsForChannel(uint channel, List<_Msg_> messages)
|
||||
{
|
||||
if (_systemConfig?.ConfigurationList == null) return;
|
||||
|
||||
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
|
||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, 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} 已刷新配置信号";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBCLoadedEvent 回调:DBC 加载后刷新该通道的配置信号。
|
||||
/// </summary>
|
||||
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]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBCUnloadedEvent 回调:移除对应通道的 CAN 信号(AvailableMethods + Channels)。
|
||||
/// </summary>
|
||||
private void OnDbcUnloaded(DBCUnloadedArgs args)
|
||||
{
|
||||
if (args.Scope != TestStatus) return;
|
||||
|
||||
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
|
||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, 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} 个信号";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将单个 CAN 信号添加为监测通道。
|
||||
/// </summary>
|
||||
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 命令处理
|
||||
private void OnAddChannel()
|
||||
{
|
||||
var method = SelectedAvailableMethod;
|
||||
if (method == null)
|
||||
{
|
||||
StatusMessage = "请先在列表中选择一个监测项";
|
||||
return;
|
||||
}
|
||||
|
||||
// 以指纹+方法名去重,防止同一物理设备重复添加
|
||||
if (Channels.Any(c => c.Fingerprint == method.Fingerprint && c.MethodName == method.MethodName))
|
||||
{
|
||||
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
|
||||
return;
|
||||
}
|
||||
|
||||
var color = _palette[_colorIndex % _palette.Length];
|
||||
_colorIndex++;
|
||||
|
||||
var channel = new MonitorChannelVM
|
||||
{
|
||||
DeviceName = method.DeviceName,
|
||||
Fingerprint = method.Fingerprint,
|
||||
MethodName = method.MethodName,
|
||||
DisplayName = method.DisplayName,
|
||||
Color = color,
|
||||
IsMonitored = true,
|
||||
IsDisplayed = true
|
||||
};
|
||||
|
||||
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);
|
||||
SelectedChannel = channel;
|
||||
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"已添加监测项 [{channel.DisplayName}],当前共 {Channels.Count} 项";
|
||||
}
|
||||
|
||||
private void OnDeleteChannel()
|
||||
{
|
||||
var target = SelectedChannel ?? Channels.LastOrDefault();
|
||||
if (target == null)
|
||||
{
|
||||
StatusMessage = "无可删除的监测项";
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.Series != null) Plot.Series.Remove(target.Series);
|
||||
Channels.Remove(target);
|
||||
SelectedChannel = Channels.LastOrDefault();
|
||||
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"已删除监测项 [{target.DisplayName}],剩余 {Channels.Count} 项";
|
||||
}
|
||||
|
||||
private void OnResetView()
|
||||
{
|
||||
Plot.ResetAllAxes();
|
||||
Plot.InvalidatePlot(false);
|
||||
StatusMessage = "视图已按数据范围复原";
|
||||
}
|
||||
|
||||
private void OnRefreshData()
|
||||
{
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"已刷新({DateTime.Now:HH:mm:ss})";
|
||||
}
|
||||
|
||||
private void OnExpand()
|
||||
{
|
||||
if (string.IsNullOrEmpty(TestStatus)) return;
|
||||
_globalInfo.CurrentScope = TestStatus;
|
||||
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 显示/隐藏切换与数学变换
|
||||
public void OnChannelDisplayChanged(MonitorChannelVM channel)
|
||||
{
|
||||
if (channel.IsDisplayed)
|
||||
{
|
||||
if (channel.Series == null)
|
||||
{
|
||||
channel.Series = new LineSeries
|
||||
{
|
||||
Title = channel.DisplayName,
|
||||
Color = channel.Color,
|
||||
StrokeThickness = 1.5
|
||||
};
|
||||
|
||||
var recent = channel.DataPoints.ToArray();
|
||||
var startIdx = Math.Max(0, recent.Length - 10000);
|
||||
for (int i = startIdx; i < recent.Length; i++)
|
||||
channel.Series.Points.Add(new DataPoint(recent[i].Time.ToOADate(), recent[i].DisplayValue));
|
||||
|
||||
Plot.Series.Add(channel.Series);
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (channel.Series != null)
|
||||
{
|
||||
Plot.Series.Remove(channel.Series);
|
||||
channel.Series = null;
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnChannelMathChanged(MonitorChannelVM channel)
|
||||
{
|
||||
if (channel.Series == null) return;
|
||||
var points = channel.DataPoints.ToArray();
|
||||
channel.Series.Points.Clear();
|
||||
var startIdx = Math.Max(0, points.Length - 10000);
|
||||
for (int i = startIdx; i < points.Length; i++)
|
||||
channel.Series.Points.Add(new DataPoint(points[i].Time.ToOADate(), points[i].DisplayValue));
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user