Files
ADP/MonitorModule/ViewModels/MonitorViewModel.cs
2026-06-26 16:07:22 +08:00

472 lines
17 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 Common.Attributes;
using Model.Entity;
using Model.Models;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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.ViewModelBase;
namespace MonitorModule.ViewModels
{
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
public bool KeepAlive => true;
public ScopedContext _scopedContext { get; set; } = null!;
public GlobalInfo _globalInfo { get; set; }
public string TestStatus
{
get => _testStatus;
set => SetProperty(ref _testStatus, value);
}
public PlotModel Plot { get; }
/// <summary>已添加的监测通道列表</summary>
public ObservableCollection<MonitorChannel> Channels { get; } = new();
/// <summary>可添加的设备方法列表(供用户选择)</summary>
public ObservableCollection<AvailableMethodItem> AvailableMethods { get; } = new();
private MonitorChannel? _selectedChannel;
public MonitorChannel? SelectedChannel
{
get => _selectedChannel;
set => SetProperty(ref _selectedChannel, value);
}
private AvailableMethodItem? _selectedAvailableMethod;
public AvailableMethodItem? SelectedAvailableMethod
{
get => _selectedAvailableMethod;
set => SetProperty(ref _selectedAvailableMethod, value);
}
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; }
#endregion
#region
private bool IsInitiated = false;
private IScopedProvider? _scope;
private DeviceManager? _deviceManager;
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 readonly DispatcherTimer _sampleTimer;
private double _sampleTime;
private const double _sampleInterval = 0.1; // 100ms
private string _testStatus = string.Empty;
private string _statusMessage = "图表已就绪,暂无监测项";
/// <summary>反射方法缓存Channel → (MethodInfo, DeviceInstance)</summary>
private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new();
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
Plot = BuildEmptyPlot();
AddChannelCommand = new DelegateCommand(OnAddChannel);
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
ResetViewCommand = new DelegateCommand(OnResetView);
RefreshDataCommand = new DelegateCommand(OnRefreshData);
RefreshCommand = new DelegateCommand(OnExpand);
_sampleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_sampleInterval) };
_sampleTimer.Tick += OnSampleTick;
}
public void Dispose()
{
// 1. 率先掐断后台正在并行的所有硬件请求
_monitorCTS?.Cancel();
_monitorCTS?.Dispose();
_monitorCTS = null;
// 2. 停掉定时器,停止触发 UI 刷新
_sampleTimer?.Stop();
_sampleTimer.Tick -= OnSampleTick;
// 3. 释放容器作用域
_scope?.Dispose();
// 4. 清理集合
Channels.Clear();
AvailableMethods.Clear();
_methodCache.Clear();
}
#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
private void DiscoverAvailableMethods()
{
AvailableMethods.Clear();
if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0)
{
StatusMessage = "当前工位无可用设备";
return;
}
foreach (var kvp in _deviceManager.DeviceMap)
{
string deviceName = kvp.Key;
var device = kvp.Value;
var deviceType = device.GetType();
// 改用特性过滤:仅扫描带有 [Monitorable] 特性且签名正确的方法
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
var attribute = m.GetCustomAttribute<MonitorableAttribute>();
if (attribute == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
bool validParams = parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
return validParams;
});
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<MonitorableAttribute>();
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
};
channel.Series = new LineSeries
{
Title = channel.DisplayName,
Color = color,
StrokeThickness = 1.5
};
Plot.Series.Add(channel.Series);
_methodCache[channel] = (method.MethodInfo, method.Device);
channel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MonitorChannel.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);
}
_methodCache.Remove(target);
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(MonitorChannel 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 - 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
{
if (channel.Series != null)
{
Plot.Series.Remove(channel.Series);
channel.Series = null;
Plot.InvalidatePlot(true);
}
}
}
public void OnChannelMathChanged(MonitorChannel channel)
{
if (channel.Series == null) return;
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++)
{
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
}
Plot.InvalidatePlot(true);
}
#endregion
#region
private async void OnSampleTick(object? sender, EventArgs e)
{
_sampleTime += _sampleInterval;
if (Channels.Count == 0 || _methodCache.Count == 0) return;
var sampleTasks = new List<Task>();
var token = _monitorCTS?.Token ?? CancellationToken.None;
// 1. 并行发起所有通道的数据采集,防止单一通道硬件断连卡死全局采样
foreach (var channel in Channels)
{
if (!channel.IsMonitored) continue;
if (!_methodCache.TryGetValue(channel, out var entry)) continue;
sampleTasks.Add(Task.Run(async () =>
{
try
{
var (method, device) = entry;
// 动态分析方法签名准备反射参数
var parmsInfo = method.GetParameters();
object?[]? invokeArgs = parmsInfo.Length == 1 && parmsInfo[0].ParameterType == typeof(CancellationToken)
? new object[] { token }
: null;
if (method.Invoke(device, invokeArgs) is Task<string> task)
{
string raw = await task.ConfigureAwait(false);
if (double.TryParse(raw, out double value))
{
channel.Record(_sampleTime, value);
var entity = new MonitorValueEntity
{
MonitorName = channel.DisplayName,
MonitorValue = value,
CreateTime = DateTime.Now,
IsDel = 0
};
}
}
}
catch
{
// 某个硬件通道故障时不干扰其他通道
}
}, token));
}
// 2. 批量等待并发采样,加入 2 秒强制超时保护兜底
if (sampleTasks.Count > 0)
{
try
{
var delayTask = Task.Delay(TimeSpan.FromSeconds(2), token);
await Task.WhenAny(Task.WhenAll(sampleTasks), delayTask).ConfigureAwait(true);
}
catch (OperationCanceledException)
{
return;
}
}
// 3. 【回归UI线程】由于前面 ConfigureAwait(true),这里直接执行 OxyPlot 坐标滚动与刷新
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
double window = 200 * _sampleInterval;
xAxis.Minimum = Math.Max(0, _sampleTime - window);
xAxis.Maximum = _sampleTime + 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>();
_deviceManager = _scope.Resolve<DeviceManager>();
IsInitiated = true;
// 初始化全新的取消信号源
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
// 扫描可用监测项
DiscoverAvailableMethods();
// 启动采样定时器
_sampleTimer.Start();
}
}
#endregion
}
}