Files
ADP/MonitorModule/ViewModels/MonitorViewModel.cs
2026-06-09 15:12:01 +08:00

328 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Windows.Threading;
using UIShare.GlobalVariable;
using UIShare.PubEvent;
using UIShare.ViewModelBase;
namespace MonitorModule.ViewModels
{
/// <summary>
/// 监控界面 VM —— 基于 OxyPlot 的曲线监控壳。
/// 当前阶段不接入真实数据源,仅搭好框架:
/// - 添加信号 / 删除信号
/// - 重置视图(按数据范围复原)
/// - 刷新(重绘 PlotView
/// 仍延续 ScopedContext 隔离 + 双击展开 的范式,每个工位独立一份图表与信号列表。
/// </summary>
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
public bool KeepAlive => true;
public ScopedContext _scopedContext { get; set; }
public GlobalInfo _globalInfo { get; set; }
public string TestStatus
{
get => _testStatus;
set => SetProperty(ref _testStatus, value);
}
/// <summary>PlotView 直接绑这个 PlotModel</summary>
public PlotModel Plot { get; }
/// <summary>当前已添加的信号集合(左侧列表展示)</summary>
public ObservableCollection<SignalItem> Signals { get; } = new();
public SignalItem? SelectedSignal
{
get => _selectedSignal;
set => SetProperty(ref _selectedSignal, value);
}
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
#endregion
#region
public ICommand AddSignalCommand { get; }
public ICommand DeleteSignalCommand { 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 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;
// ===== 模拟数据相关 =====
// 模拟定时器:周期性给每条信号追加新点,形成滚动效果
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 string _testStatus = string.Empty;
private SignalItem? _selectedSignal;
private string _statusMessage = "图表已就绪,暂无信号";
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
Plot = BuildEmptyPlot();
AddSignalCommand = new DelegateCommand(OnAddSignal);
DeleteSignalCommand = new DelegateCommand(OnDeleteSignal);
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
}
public void Dispose()
{
_simTimer?.Stop();
_scope?.Dispose();
}
#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 = "X",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "Y",
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
/// <summary>
/// 添加一条信号:按 _signalCounter 轮转选择不同波形 generator
/// 后续 _simTimer 每帧会调用 generator(t) 给曲线追加新点形成滚动效果。
/// </summary>
private void OnAddSignal()
{
_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
{
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} 条";
}
/// <summary>
/// 按索引返回一种模拟波形发生器。
/// 振幅 / 频率 / 相位都做了区分,让多条曲线视觉上分开。
/// </summary>
private static (string waveName, Func<double, double> 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),
};
}
/// <summary>构造一个随机游走 generator在前一次值基础上叠加高斯噪声。</summary>
private static (string, Func<double, double>) 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;
});
}
/// <summary>删除当前选中信号;若未选中则删除最后一条。</summary>
private void OnDeleteSignal()
{
var target = SelectedSignal ?? Signals.LastOrDefault();
if (target == null)
{
StatusMessage = "无可删除的信号";
return;
}
Plot.Series.Remove(target.Series);
Signals.Remove(target);
SelectedSignal = Signals.LastOrDefault();
Plot.InvalidatePlot(true);
StatusMessage = $"已删除信号 [{target.Name}],剩余 {Signals.Count} 条";
}
/// <summary>按数据范围复原视图(重置所有坐标轴的缩放/平移)。</summary>
private void OnResetView()
{
Plot.ResetAllAxes();
Plot.InvalidatePlot(false);
StatusMessage = "视图已按数据范围复原";
}
/// <summary>刷新:触发 PlotView 重绘;后续接数据源时可在此重拉数据。</summary>
private void OnRefreshData()
{
Plot.InvalidatePlot(true);
StatusMessage = $"已刷新({DateTime.Now:HH:mm:ss}";
}
/// <summary>双击展开 / 折叠九宫格。</summary>
private void OnExpand()
{
if (string.IsNullOrEmpty(TestStatus)) return;
_globalInfo.CurrentScope = TestStatus;
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
}
/// <summary>
/// 模拟数据 tick每条信号按当前 _simT 算出新点并追加。
/// 超过 _maxPoints 时丢弃最旧的点形成滚动窗口。
/// </summary>
private void OnSimTick(object? sender, EventArgs e)
{
_simT += _simInterval;
if (Signals.Count == 0)
{
return;
}
foreach (var item in Signals)
{
double y = item.Generator(_simT);
item.Series.Points.Add(new DataPoint(_simT, y));
if (item.Series.Points.Count > _maxPoints)
{
item.Series.Points.RemoveAt(0);
}
}
// 让 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;
}
Plot.InvalidatePlot(true);
}
#endregion
#region
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>();
IsInitiated = true;
}
}
#endregion
}
/// <summary>
/// 信号列表项UI 显示名 + 对应的 OxyPlot LineSeries 引用 + 模拟数据 generator。
/// generator(t) 接收当前模拟时间,返回该时刻 y 值。
/// </summary>
public class SignalItem
{
public string Name { get; }
public LineSeries Series { get; }
public Func<double, double> Generator { get; }
public SignalItem(string name, LineSeries series, Func<double, double> generator)
{
Name = name;
Series = series;
Generator = generator;
}
}
}