diff --git a/Common/Attributes/MonitorableAttribute.cs b/Common/Attributes/MonitorableAttribute.cs
new file mode 100644
index 0000000..95f3970
--- /dev/null
+++ b/Common/Attributes/MonitorableAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Common.Attributes
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class MonitorableAttribute : Attribute
+ {
+ public string? Description { get; set; }
+
+ public MonitorableAttribute(string? description = null)
+ {
+ Description = description;
+ }
+ }
+}
diff --git a/DeviceCommand/Devices/IT7800E.cs b/DeviceCommand/Devices/IT7800E.cs
index 1e20ebe..6cc21a5 100644
--- a/DeviceCommand/Devices/IT7800E.cs
+++ b/DeviceCommand/Devices/IT7800E.cs
@@ -146,6 +146,7 @@ namespace DeviceCommand.Device
///
/// 查询通道实时测得的电压有效值(RMS,支持 AC 或 DC 模式下的回测)(单位: V)
///
+ [Monitorable("交流电源电压")]
public virtual async Task 查询实际电压(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
@@ -154,6 +155,7 @@ namespace DeviceCommand.Device
///
/// 查询通道实时测得的电流有效值(RMS)(单位: A)
///
+ [Monitorable("交流电源电流")]
public virtual async Task 查询实际电流(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
@@ -162,6 +164,7 @@ namespace DeviceCommand.Device
///
/// 查询通道实时测得的有功功率值 (单位: W)
///
+ [Monitorable("交流电源功率")]
public virtual async Task 查询实际功率(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
diff --git a/DeviceCommand/Devices/N36200.cs b/DeviceCommand/Devices/N36200.cs
index 935e49b..132e621 100644
--- a/DeviceCommand/Devices/N36200.cs
+++ b/DeviceCommand/Devices/N36200.cs
@@ -226,6 +226,7 @@ namespace DeviceCommand.Device
///
/// 3.4.1. 回读通道输出端子上的实时测得电压值 (单位: V)
///
+ [Monitorable("高压直流电源电压")]
public virtual async Task 查询实际电压(CancellationToken ct = default)
{
return await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
@@ -234,6 +235,7 @@ namespace DeviceCommand.Device
///
/// 3.4.2. 回读通道输出端子上的实时测得电流值 (单位: A)
///
+ [Monitorable("高压直流电源电流")]
public virtual async Task 查询实际电流(CancellationToken ct = default)
{
return await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
@@ -242,6 +244,7 @@ namespace DeviceCommand.Device
///
/// 3.4.3. 回读通道输出端子上的实时测得功率值 (单位: W)
///
+ [Monitorable("高压直流电源功率")]
public virtual async Task 查询实际功率(CancellationToken ct = default)
{
return await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
diff --git a/DeviceCommand/Devices/N36600.cs b/DeviceCommand/Devices/N36600.cs
index 0ab64d3..ada9ade 100644
--- a/DeviceCommand/Devices/N36600.cs
+++ b/DeviceCommand/Devices/N36600.cs
@@ -191,6 +191,7 @@ namespace DeviceCommand.Device
///
/// 3.5.1 查询通道输出端子上测得的直流电流值 (A)
///
+ [Monitorable("低压直流电源电流")]
public virtual async Task 查询实际电流(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:CURRent?{SCPIDelimiter}", SCPIDelimiter, ct);
@@ -199,6 +200,7 @@ namespace DeviceCommand.Device
///
/// 3.5.2 查询通道输出端子上测得的直流功率值 (W)
///
+ [Monitorable("低压直流电源功率")]
public virtual async Task 查询实际功率(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:POWer?{SCPIDelimiter}", SCPIDelimiter, ct);
@@ -206,7 +208,8 @@ namespace DeviceCommand.Device
///
/// 3.5.3 查询通道输出端子上测得的直流电压值 (V)
- ///
+ /// 查询实际电压(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:VOLTage?{SCPIDelimiter}", SCPIDelimiter, ct);
diff --git a/DeviceCommand/Devices/N69200.cs b/DeviceCommand/Devices/N69200.cs
index 36484ec..0345449 100644
--- a/DeviceCommand/Devices/N69200.cs
+++ b/DeviceCommand/Devices/N69200.cs
@@ -193,6 +193,7 @@ namespace DeviceCommand.Device
///
/// 回读电子负载输入端子上的实时测得电压值 (单位: V)
///
+ [Monitorable("高压直流负载电压")]
public virtual async Task 查询实际电压(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
@@ -201,6 +202,7 @@ namespace DeviceCommand.Device
///
/// 回读电子负载输入端子上的实时测得电流值 (单位: A)
///
+ [Monitorable("高压直流负载电流")]
public virtual async Task 查询实际电流(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
@@ -209,6 +211,7 @@ namespace DeviceCommand.Device
///
/// 回读电子负载输入端子上的实时测得功率值 (单位: W)
///
+ [Monitorable("高压直流负载功率")]
public virtual async Task 查询实际功率(CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
diff --git a/MonitorModule/ViewModels/MonitorChannel.cs b/MonitorModule/ViewModels/MonitorChannel.cs
new file mode 100644
index 0000000..d2038ce
--- /dev/null
+++ b/MonitorModule/ViewModels/MonitorChannel.cs
@@ -0,0 +1,105 @@
+using OxyPlot.Series;
+using OxyPlot;
+using System.Collections.Concurrent;
+using Prism.Mvvm;
+using NCalc;
+
+namespace MonitorModule.ViewModels
+{
+ ///
+ /// 监测通道:一个设备方法对应一个通道。
+ /// 数据记录(DataPoints)与图表显示(Series)完全分离:
+ /// - IsMonitored=true 时始终记录 DataPoints
+ /// - IsDisplayed=true 时才创建 LineSeries 并绘制
+ ///
+ public class MonitorChannel : BindableBase
+ {
+ // ===== 标识 =====
+ public string DeviceName { get; init; } = string.Empty;
+ public string MethodName { get; init; } = string.Empty;
+ public string DisplayName { get; init; } = string.Empty;
+
+ // ===== 状态 =====
+ private bool _isMonitored = true;
+ /// 是否在记录数据点(添加后始终为 true)
+ public bool IsMonitored
+ {
+ get => _isMonitored;
+ set => SetProperty(ref _isMonitored, value);
+ }
+
+ private bool _isDisplayed = true;
+ /// 是否在 OxyPlot 上显示
+ public bool IsDisplayed
+ {
+ get => _isDisplayed;
+ set => SetProperty(ref _isDisplayed, value);
+ }
+
+ // ===== 数学变换 =====
+ private string? _mathExpression;
+ /// 可选数学变换表达式,如 "x*0.001"。为空则 DisplayValue=RawValue
+ public string? MathExpression
+ {
+ get => _mathExpression;
+ set => SetProperty(ref _mathExpression, value);
+ }
+
+ // ===== 颜色 =====
+ public OxyColor Color { get; set; } = OxyColors.SteelBlue;
+
+ // ===== 数据存储 =====
+ /// 所有采样数据点(线程安全),与 OxyPlot 无关
+ public ConcurrentQueue<(double Time, double RawValue, double DisplayValue)> DataPoints { get; } = new();
+
+ /// 最大缓冲数据点数,超出则丢弃最旧的
+ public int MaxBuffer { get; set; } = 5000;
+
+ // ===== OxyPlot Series(仅 IsDisplayed=true 时存在)=====
+ private LineSeries? _series;
+ public LineSeries? Series
+ {
+ get => _series;
+ set => SetProperty(ref _series, value);
+ }
+
+ // ===== 辅助方法 =====
+ /// 记录一个数据点:RawValue 经数学变换后得到 DisplayValue,一并入队
+ public void Record(double time, double rawValue)
+ {
+ double displayValue = rawValue;
+ if (!string.IsNullOrWhiteSpace(MathExpression))
+ {
+ try
+ {
+ var expr = new NCalc.Expression(MathExpression);
+ expr.Parameters["x"] = rawValue;
+ var result = expr.Evaluate();
+ displayValue = Convert.ToDouble(result);
+ }
+ catch
+ {
+ displayValue = rawValue;
+ }
+ }
+
+ DataPoints.Enqueue((time, rawValue, displayValue));
+
+ // 超出缓冲上限时批量丢弃旧数据
+ while (DataPoints.Count > MaxBuffer)
+ {
+ DataPoints.TryDequeue(out _);
+ }
+
+ // 如果正在显示,同步到 Series
+ if (_series != null)
+ {
+ _series.Points.Add(new DataPoint(time, displayValue));
+ while (_series.Points.Count > 200)
+ {
+ _series.Points.RemoveAt(0);
+ }
+ }
+ }
+ }
+}
diff --git a/MonitorModule/ViewModels/MonitorViewModel.cs b/MonitorModule/ViewModels/MonitorViewModel.cs
index 9966fc4..10a26f1 100644
--- a/MonitorModule/ViewModels/MonitorViewModel.cs
+++ b/MonitorModule/ViewModels/MonitorViewModel.cs
@@ -3,22 +3,17 @@ using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using System.Collections.ObjectModel;
+using System.Reflection;
using System.Windows.Input;
using System.Windows.Threading;
using UIShare.GlobalVariable;
using UIShare.PubEvent;
using UIShare.ViewModelBase;
+using Prism.Mvvm;
+using Common.Attributes;
namespace MonitorModule.ViewModels
{
- ///
- /// 监控界面 VM —— 基于 OxyPlot 的曲线监控壳。
- /// 当前阶段不接入真实数据源,仅搭好框架:
- /// - 添加信号 / 删除信号
- /// - 重置视图(按数据范围复原)
- /// - 刷新(重绘 PlotView)
- /// 仍延续 ScopedContext 隔离 + 双击展开 的范式,每个工位独立一份图表与信号列表。
- ///
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region 属性
@@ -32,16 +27,26 @@ namespace MonitorModule.ViewModels
set => SetProperty(ref _testStatus, value);
}
- /// PlotView 直接绑这个 PlotModel
public PlotModel Plot { get; }
- /// 当前已添加的信号集合(左侧列表展示)
- public ObservableCollection Signals { get; } = new();
+ /// 已添加的监测通道列表
+ public ObservableCollection Channels { get; } = new();
- public SignalItem? SelectedSignal
+ /// 可添加的设备方法列表(供用户选择)
+ public ObservableCollection AvailableMethods { get; } = new();
+
+ private MonitorChannel? _selectedChannel;
+ public MonitorChannel? SelectedChannel
{
- get => _selectedSignal;
- set => SetProperty(ref _selectedSignal, value);
+ get => _selectedChannel;
+ set => SetProperty(ref _selectedChannel, value);
+ }
+
+ private AvailableMethodItem? _selectedAvailableMethod;
+ public AvailableMethodItem? SelectedAvailableMethod
+ {
+ get => _selectedAvailableMethod;
+ set => SetProperty(ref _selectedAvailableMethod, value);
}
public string StatusMessage
@@ -50,66 +55,58 @@ namespace MonitorModule.ViewModels
set => SetProperty(ref _statusMessage, value);
}
#endregion
+
#region 命令
- public ICommand AddSignalCommand { get; }
- public ICommand DeleteSignalCommand { get; }
+ public ICommand AddChannelCommand { get; }
+ public ICommand DeleteChannelCommand { get; }
public ICommand ResetViewCommand { get; }
public ICommand RefreshDataCommand { get; }
- // 双击展开/折叠:与 RecordView/AutomatedTestingView 共用同一套 ExpandViewEvent
public ICommand RefreshCommand { get; }
#endregion
+
#region 私有字段
private bool IsInitiated = false;
private IScopedProvider _scope;
- // 颜色轮转池,给新加的信号自动分配区分色
+ private DeviceManager? _deviceManager;
+
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 _signalCounter;
+ OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
+ OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod,
+ OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab
+ };
+ private int _colorIndex;
- // ===== 模拟数据相关 =====
- // 模拟定时器:周期性给每条信号追加新点,形成滚动效果
- private readonly DispatcherTimer _simTimer;
- // 全局采样时间(秒),每 tick 自增 _simInterval
- private double _simT;
- // 采样间隔(秒),与 DispatcherTimer.Interval 保持一致
- private const double _simInterval = 0.1;
- // 滚动窗口大小:每条曲线最多保留 200 个点(约 20 秒)
- private const int _maxPoints = 200;
- // 用于 random walk 的随机源
- private static readonly Random _rng = new();
+ private readonly DispatcherTimer _sampleTimer;
+ private double _sampleTime;
+ private const double _sampleInterval = 0.1; // 100ms
private string _testStatus = string.Empty;
- private SignalItem? _selectedSignal;
- private string _statusMessage = "图表已就绪,暂无信号";
+ private string _statusMessage = "图表已就绪,暂无监测项";
+
+ /// 反射方法缓存:Channel → (MethodInfo, DeviceInstance)
+ private readonly Dictionary _methodCache = new();
+
+ /// 方法名前缀:匹配这些开头的公共方法被视为可监测项
+ private static readonly string[] _methodPrefixes = { "查询", "读取", "获取", "测量" };
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve();
Plot = BuildEmptyPlot();
- AddSignalCommand = new DelegateCommand(OnAddSignal);
- DeleteSignalCommand = new DelegateCommand(OnDeleteSignal);
+ AddChannelCommand = new DelegateCommand(OnAddChannel);
+ DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
ResetViewCommand = new DelegateCommand(OnResetView);
RefreshDataCommand = new DelegateCommand(OnRefreshData);
RefreshCommand = new DelegateCommand(OnExpand);
- // 启动模拟数据定时器:100ms 一帧,给每条信号喂一个新点
- _simTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_simInterval) };
- _simTimer.Tick += OnSimTick;
- _simTimer.Start();
-
- // 默认预置 3 条模拟信号,便于直接看到滚动效果
- OnAddSignal(); // sin
- OnAddSignal(); // cos
- OnAddSignal(); // random walk
+ _sampleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_sampleInterval) };
+ _sampleTimer.Tick += OnSampleTick;
}
public void Dispose()
{
- _simTimer?.Stop();
+ _sampleTimer?.Stop();
_scope?.Dispose();
}
@@ -125,14 +122,14 @@ namespace MonitorModule.ViewModels
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Bottom,
- Title = "X",
+ Title = "时间 (s)",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
- Title = "Y",
+ Title = "值",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
@@ -146,92 +143,149 @@ namespace MonitorModule.ViewModels
}
#endregion
- #region 命令处理
+ #region 设备方法发现
///
- /// 添加一条信号:按 _signalCounter 轮转选择不同波形 generator,
- /// 后续 _simTimer 每帧会调用 generator(t) 给曲线追加新点形成滚动效果。
+ /// 扫描 DeviceManager.DeviceMap 中所有设备,反射找出可监测的方法。
///
- private void OnAddSignal()
+ private void DiscoverAvailableMethods()
{
- _signalCounter++;
- var color = _palette[(_signalCounter - 1) % _palette.Length];
-
- // 6 种波形轮转:sin / cos / 锯齿 / 方波 / 衰减正弦 / random walk
- var (waveName, generator) = BuildGenerator(_signalCounter);
- var name = $"{waveName}_{_signalCounter}";
-
- var series = new LineSeries
+ AvailableMethods.Clear();
+ if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0)
{
- Title = name,
- Color = color,
- StrokeThickness = 1.5
- };
-
- var item = new SignalItem(name, series, generator);
- Signals.Add(item);
- SelectedSignal = item;
-
- Plot.Series.Add(series);
- Plot.InvalidatePlot(true);
-
- StatusMessage = $"已添加信号 [{name}],当前共 {Signals.Count} 条";
- }
-
- ///
- /// 按索引返回一种模拟波形发生器。
- /// 振幅 / 频率 / 相位都做了区分,让多条曲线视觉上分开。
- ///
- private static (string waveName, Func generator) BuildGenerator(int index)
- {
- // 给同种波形不同实例一些随机偏移,避免完全重叠
- double phase = (index * 0.7) % (2 * Math.PI);
- double amp = 1.0 + (index % 3) * 0.3;
- double freq = 0.5 + (index % 4) * 0.2;
-
- return (index % 6) switch
- {
- 0 => ("Sin", t => amp * Math.Sin(2 * Math.PI * freq * t + phase)),
- 1 => ("Cos", t => amp * Math.Cos(2 * Math.PI * freq * t + phase)),
- 2 => ("Saw", t => amp * (2 * ((t * freq) - Math.Floor(t * freq + 0.5)))),
- 3 => ("Square", t => amp * Math.Sign(Math.Sin(2 * Math.PI * freq * t + phase))),
- 4 => ("Decay", t => amp * Math.Exp(-0.05 * t) * Math.Sin(2 * Math.PI * freq * t + phase)),
- _ => RandomWalkGenerator(amp),
- };
- }
-
- /// 构造一个随机游走 generator:在前一次值基础上叠加高斯噪声。
- private static (string, Func) RandomWalkGenerator(double amp)
- {
- double last = 0;
- return ("Walk", _ =>
- {
- last += (_rng.NextDouble() - 0.5) * 0.2 * amp;
- // 软约束在 [-amp*3, amp*3],避免一直跑偏
- if (last > amp * 3) last = amp * 3;
- if (last < -amp * 3) last = -amp * 3;
- return last;
- });
- }
-
- /// 删除当前选中信号;若未选中则删除最后一条。
- private void OnDeleteSignal()
- {
- var target = SelectedSignal ?? Signals.LastOrDefault();
- if (target == null)
- {
- StatusMessage = "无可删除的信号";
+ StatusMessage = "当前工位无可用设备";
return;
}
- Plot.Series.Remove(target.Series);
- Signals.Remove(target);
- SelectedSignal = Signals.LastOrDefault();
+ foreach (var kvp in _deviceManager.DeviceMap)
+ {
+ string deviceName = kvp.Key;
+ var device = kvp.Value;
+ var deviceType = device.GetType();
+
+ // 核心优化:直接通过特性和参数过滤方法
+ var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .Where(m =>
+ {
+ // 1. 核心过滤:必须包含 [Monitorable] 特性
+ var attribute = m.GetCustomAttribute();
+ if (attribute == null) return false;
+
+ // 2. 返回类型校验(如果强制要求是 Task)
+ if (m.ReturnType != typeof(Task)) return false;
+
+ // 3. 参数校验:只取无参或仅有 CancellationToken 参数的方法
+ var parms = m.GetParameters();
+ bool validParams = parms.Length == 0 ||
+ (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
+
+ return validParams;
+ });
+
+ foreach (var method in methods)
+ {
+ // 如果你在特性里写了 Description,这里可以拿出来赋给 DisplayName
+ var attr = method.GetCustomAttribute();
+ string displayName = !string.IsNullOrEmpty(attr?.Description)
+ ? $"{deviceName}.{attr.Description}"
+ : $"{deviceName}.{method.Name}";
+
+ var item = new AvailableMethodItem
+ {
+ DeviceName = deviceName,
+ MethodName = method.Name,
+ DisplayName = displayName, // 优先使用特性的中文描述
+ MethodInfo = method,
+ Device = device
+ };
+ AvailableMethods.Add(item);
+ }
+ }
+
+ StatusMessage = AvailableMethods.Count > 0
+ ? $"发现 {AvailableMethods.Count} 个可监测项"
+ : "未发现可监测的设备方法";
+ }
+ #endregion
+
+ #region 命令处理
+ private void OnAddChannel()
+ {
+ var method = SelectedAvailableMethod;
+ if (method == null)
+ {
+ StatusMessage = "请先在列表中选择一个监测项";
+ return;
+ }
+
+ // 防止重复添加同一设备方法
+ if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName))
+ {
+ StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
+ return;
+ }
+
+ var color = _palette[_colorIndex % _palette.Length];
+ _colorIndex++;
+
+ var channel = new MonitorChannel
+ {
+ DeviceName = method.DeviceName,
+ MethodName = method.MethodName,
+ DisplayName = method.DisplayName,
+ Color = color,
+ IsMonitored = true,
+ IsDisplayed = true
+ };
+
+ // 创建 Series 并加入 Plot
+ channel.Series = new LineSeries
+ {
+ Title = channel.DisplayName,
+ Color = color,
+ StrokeThickness = 1.5
+ };
+ Plot.Series.Add(channel.Series);
+
+ // 缓存反射信息
+ _methodCache[channel] = (method.MethodInfo, method.Device);
+
+ // 订阅属性变化:IsDisplayed 控制显示/隐藏,MathExpression 控制数学变换
+ channel.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == nameof(MonitorChannel.IsDisplayed))
+ OnChannelDisplayChanged(channel);
+ else if (e.PropertyName == nameof(MonitorChannel.MathExpression))
+ OnChannelMathChanged(channel);
+ };
+
+ Channels.Add(channel);
+ SelectedChannel = channel;
Plot.InvalidatePlot(true);
- StatusMessage = $"已删除信号 [{target.Name}],剩余 {Signals.Count} 条";
+ 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);
+ }
+ _methodCache.Remove(target);
+ Channels.Remove(target);
+ SelectedChannel = Channels.LastOrDefault();
+
+ Plot.InvalidatePlot(true);
+ StatusMessage = $"已删除监测项 [{target.DisplayName}],剩余 {Channels.Count} 项";
}
- /// 按数据范围复原视图(重置所有坐标轴的缩放/平移)。
private void OnResetView()
{
Plot.ResetAllAxes();
@@ -239,52 +293,119 @@ namespace MonitorModule.ViewModels
StatusMessage = "视图已按数据范围复原";
}
- /// 刷新:触发 PlotView 重绘;后续接数据源时可在此重拉数据。
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 显示/隐藏切换
+ ///
+ /// 当 Channel.IsDisplayed 变化时调用:控制 Series 的创建/移除
+ ///
+ public void OnChannelDisplayChanged(MonitorChannel channel)
+ {
+ if (channel.IsDisplayed)
+ {
+ // 从 false → true:创建 Series,回填最近数据
+ if (channel.Series == null)
+ {
+ channel.Series = new LineSeries
+ {
+ Title = channel.DisplayName,
+ Color = channel.Color,
+ StrokeThickness = 1.5
+ };
+
+ // 回填最近 200 个数据点
+ var recent = channel.DataPoints.ToArray();
+ var startIdx = Math.Max(0, recent.Length - 200);
+ 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
+ {
+ // 从 true → false:移除 Series,数据继续记录
+ if (channel.Series != null)
+ {
+ Plot.Series.Remove(channel.Series);
+ channel.Series = null;
+ Plot.InvalidatePlot(true);
+ }
+ }
}
///
- /// 模拟数据 tick:每条信号按当前 _simT 算出新点并追加。
- /// 超过 _maxPoints 时丢弃最旧的点形成滚动窗口。
+ /// 当 Channel.MathExpression 变化时调用:重新计算所有已记录数据点的 DisplayValue
///
- private void OnSimTick(object? sender, EventArgs e)
+ public void OnChannelMathChanged(MonitorChannel channel)
{
- _simT += _simInterval;
+ if (channel.Series == null) return;
- if (Signals.Count == 0)
+ // 用新表达式重算 Series 中的点
+ var points = channel.DataPoints.ToArray();
+ channel.Series.Points.Clear();
+ var startIdx = Math.Max(0, points.Length - 200);
+ for (int i = startIdx; i < points.Length; i++)
{
- return;
+ channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
}
+ Plot.InvalidatePlot(true);
+ }
+ #endregion
- foreach (var item in Signals)
+ #region 采样定时器
+ private async void OnSampleTick(object? sender, EventArgs e)
+ {
+ _sampleTime += _sampleInterval;
+
+ if (Channels.Count == 0 || _methodCache.Count == 0) return;
+
+ foreach (var channel in Channels)
{
- double y = item.Generator(_simT);
- item.Series.Points.Add(new DataPoint(_simT, y));
- if (item.Series.Points.Count > _maxPoints)
+ if (!channel.IsMonitored) continue;
+ if (!_methodCache.TryGetValue(channel, out var entry)) continue;
+
+ try
{
- item.Series.Points.RemoveAt(0);
+ var (method, device) = entry;
+ // 调用设备方法获取返回值
+ var task = (Task)method.Invoke(device, null)!;
+ var raw = await task.ConfigureAwait(true);
+
+ // 尝试解析为 double
+ if (double.TryParse(raw, out double value))
+ {
+ channel.Record(_sampleTime, value);
+ }
+ }
+ catch
+ {
+ // 设备读取失败时跳过该通道本次采样
}
}
- // 让 X 轴跟着最新数据滚动
+ // 让 X 轴跟随最新数据滚动
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
- double window = _maxPoints * _simInterval;
- xAxis.Minimum = Math.Max(0, _simT - window);
- xAxis.Maximum = _simT + 0.5;
+ double window = 200 * _sampleInterval;
+ xAxis.Minimum = Math.Max(0, _sampleTime - window);
+ xAxis.Maximum = _sampleTime + 0.5;
}
Plot.InvalidatePlot(true);
@@ -302,26 +423,28 @@ namespace MonitorModule.ViewModels
Plot.InvalidatePlot(false);
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve();
+ _deviceManager = _scope.Resolve();
IsInitiated = true;
+
+ // 扫描可用监测项
+ DiscoverAvailableMethods();
+
+ // 启动采样定时器
+ _sampleTimer.Start();
}
}
#endregion
}
///
- /// 信号列表项:UI 显示名 + 对应的 OxyPlot LineSeries 引用 + 模拟数据 generator。
- /// generator(t) 接收当前模拟时间,返回该时刻 y 值。
+ /// 可用设备方法项(供用户选择添加为监测通道)
///
- public class SignalItem
+ public class AvailableMethodItem
{
- public string Name { get; }
- public LineSeries Series { get; }
- public Func Generator { get; }
- public SignalItem(string name, LineSeries series, Func generator)
- {
- Name = name;
- Series = series;
- Generator = generator;
- }
+ public string DeviceName { get; set; } = string.Empty;
+ public string MethodName { get; set; } = string.Empty;
+ public string DisplayName { get; set; } = string.Empty;
+ public MethodInfo MethodInfo { get; set; } = null!;
+ public object Device { get; set; } = null!;
}
}
diff --git a/MonitorModule/Views/MonitorViewView.xaml b/MonitorModule/Views/MonitorViewView.xaml
index 087152e..cc55bbc 100644
--- a/MonitorModule/Views/MonitorViewView.xaml
+++ b/MonitorModule/Views/MonitorViewView.xaml
@@ -16,17 +16,14 @@
-
-
-
-
-
-
-
-
+ Margin="4,0,0,8"/>
-
-
-
-
-
-
+
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
-
-
+ Background="Transparent"/>
-
+
-
-
-