472 lines
17 KiB
C#
472 lines
17 KiB
C#
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
|
||
}
|
||
} |