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; } /// 已添加的监测通道列表 public ObservableCollection Channels { get => _systemConfig?.Channels ?? new ObservableCollection(); set { if (_systemConfig != null && _systemConfig.Channels != value) { _systemConfig.Channels = value; RaisePropertyChanged(); } } } /// 可添加的设备方法列表(供用户选择) public ObservableCollection 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; /// 设备数据广播器(全局单例) private HardwareDataBroadcaster? _broadcaster; /// CAN 信号广播器(全局单例) private CANSignalBroadcaster? _canSignalBroadcaster; /// 用于 OxyPlot X 轴相对秒数 private readonly Stopwatch _stopwatch = new(); /// OxyPlot 刷新定时器(仅负责滚动 X 轴 + InvalidatePlot) private readonly DispatcherTimer _plotRefreshTimer; /// 事件订阅令牌,用于 Dispose 时取消订阅 private SubscriptionToken? _subscriptionToken; private SubscriptionToken? _dbcLoadedToken; private SubscriptionToken? _dbcUnloadedToken; // ========================================== // 批量入库核心结构 // ========================================== private readonly ConcurrentQueue _insertQueue = new(); private Task? _dbFlushTask; private const int BulkInsertThreshold = 50; private int _isFlushing = 0; #endregion public MonitorViewModel(IContainerExtension container) : base(container) { _globalInfo = container.Resolve(); _monitorValueService = container.Resolve(); 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().Unsubscribe(_subscriptionToken); if (_dbcLoadedToken != null) _eventAggregator.GetEvent().Unsubscribe(_dbcLoadedToken); if (_dbcUnloadedToken != null) _eventAggregator.GetEvent().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(Channels .Select(c => new MonitorChannelConfig { Fingerprint = c.Fingerprint, MethodName = c.MethodName, IsDisplayed = c.IsDisplayed })); } ConfigService.Save(_systemConfig); } #region 事件驱动:接收广播数据 /// /// 由 HardwareDataBroadcaster 通过 EventAggregator 广播触发。 /// 已通过 ThreadOption.UIThread 确保在 UI 线程执行。 /// 通过 HardwareFingerprint + MethodName 匹配监测通道。 /// 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; double time = _stopwatch.Elapsed.TotalSeconds; // 记录数据点(线程安全:Record 内部 ConcurrentQueue + Series 操作) channel.Record(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; double elapsed = _stopwatch.Elapsed.TotalSeconds; var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom); if (xAxis != null) { double window = 10000 * 0.1; // 20s 视窗 xAxis.Minimum = Math.Max(0, elapsed - window); xAxis.Maximum = elapsed + 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(); 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("Name"); Plot.Title = $"监控 - {TestStatus}"; Plot.InvalidatePlot(false); _scope = _globalInfo.ScopeDic[TestStatus]; _scopedContext = _scope.Resolve(); _deviceManager = _scope.Resolve(); _systemConfig = _scope.Resolve(); RaisePropertyChanged(nameof(Channels)); _broadcaster = _scope.Resolve(); _canSignalBroadcaster = _scope.Resolve(); 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() .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(); } } #endregion #region PlotModel 构建 private static PlotModel BuildEmptyPlot() { var pm = new PlotModel { Title = string.Empty, PlotAreaBorderColor = OxyColors.LightGray, Background = OxyColors.White }; pm.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom, Title = "时间 (s)", 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 展示可选项) /// /// 通过 GlobalInfo.HardwarePool 构建 设备实例引用→硬件指纹 的反向查找表。 /// private Dictionary BuildInstanceToFingerprint() { var map = new Dictionary(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() == null) return false; if (m.ReturnType != typeof(Task)) 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(); 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} 个可监测项" : "未发现可监测的设备方法"; } /// /// 从 SystemConfig.MonitorChannels 持久化列表自动还原监测通道。 /// 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} 个监测通道"; } } /// /// 根据 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 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} 已刷新配置信号"; } } /// /// 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 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} 个信号"; } } /// /// 将单个 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 命令处理 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().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, 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, points[i].DisplayValue)); Plot.InvalidatePlot(true); } #endregion } }