Files
ADP/MonitorModule/ViewModels/MonitorViewModel.cs
2026-06-29 09:39:11 +08:00

589 lines
21 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 Service.Interface;
using System;
using System.Collections.Concurrent;
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
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; }
/// <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);
}
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; }
#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;
// 图表采样定时器 (100ms)
private readonly DispatcherTimer _sampleTimer;
private double _sampleTime;
private const double _sampleInterval = 0.1;
/// <summary>反射方法缓存Channel → (MethodInfo, DeviceInstance)</summary>
private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new();
// ==========================================
// 🟥 基于 Task.Run 批量入库的核心高并发结构
// ==========================================
/// <summary>高并发无锁无阻塞队列</summary>
private readonly ConcurrentQueue<MonitorValueEntity> _insertQueue = new();
/// <summary>常驻后台的数据库消费任务</summary>
private Task? _dbFlushTask;
/// <summary>定量触发阈值</summary>
private const int BulkInsertThreshold = 50;
/// <summary>防并发消费锁标志</summary>
private int _isFlushing = 0;
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
_monitorValueService = container.Resolve<IMonitorValueService>();
Plot = BuildEmptyPlot();
AddChannelCommand = new DelegateCommand(OnAddChannel);
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
ResetViewCommand = new DelegateCommand(OnResetView);
RefreshDataCommand = new DelegateCommand(OnRefreshData);
RefreshCommand = new DelegateCommand(OnExpand);
// 前端采样定时器保持 100ms
_sampleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_sampleInterval) };
_sampleTimer.Tick += OnSampleTick;
}
/// <summary>
/// 页面销毁与清理
/// </summary>
public void Dispose()
{
// 1. 率先掐断所有底层的异步信号(引发 Task.Delay 抛出异常安全退出常驻循环)
_monitorCTS?.Cancel();
_monitorCTS?.Dispose();
_monitorCTS = null;
// 2. 停掉前端 UI 定时器
_sampleTimer?.Stop();
_sampleTimer.Tick -= OnSampleTick;
// 3. 🟥 等待后台入库消费 Task 彻底结束,最大安全等待 1.5 秒
if (_dbFlushTask != null)
{
try
{
_dbFlushTask.Wait(TimeSpan.FromSeconds(1.5));
}
catch { /* 忽略退出时的线程取消异常 */ }
}
// 4. 释放容器作用域及清空集合
_scope?.Dispose();
Channels.Clear();
AvailableMethods.Clear();
_methodCache.Clear();
}
#region 🟥 Task.Run
/// <summary>
/// 开启常驻后台的数据库消费线程
/// </summary>
private void StartDbFlushWorker(CancellationToken token)
{
_dbFlushTask = Task.Run(async () =>
{
// 只要未触发取消信号,就一直在后台默默轮询
while (!token.IsCancellationRequested)
{
try
{
// 💡 核心策略一:定时兜底。每隔 1 秒强制检查一次队列。
// ConfigureAwait(false) 彻底丢弃 UI 上下文,拥抱线程池极致速度。
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);
}
/// <summary>
/// 纯粹的数据消费与批量 BulkCopy 入库
/// </summary>
private async Task DoFlushWorkAsync()
{
if (_insertQueue.IsEmpty) return;
// CAS 原子自增锁,确保同一时间只有一个线程在向 SqlSugar 投递这批数据
if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) return;
try
{
var listToInsert = new List<MonitorValueEntity>();
// 一口气掏空当前并发队列里的所有实体
while (_insertQueue.TryDequeue(out var entity))
{
listToInsert.Add(entity);
}
if (listToInsert.Count > 0)
{
// 调用你的 Service 层的 SqlSugar 批量写入
var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false);
if (!result.IsSuccess)
{
System.Diagnostics.Debug.WriteLine($"[DB Bulk Error] 批量入库失败: {result.Msg}");
}
}
}
finally
{
Interlocked.Exchange(ref _isFlushing, 0); // 释放锁
}
}
#endregion
#region 100ms
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))
{
// A. 更新本地图表数据缓存
channel.Record(_sampleTime, value);
// B. 封装实体
var entity = new MonitorValueEntity
{
MonitorName = channel.DisplayName,
MonitorValue = value,
CreateTime = DateTime.Now,
IsDel = 0
};
// C. 🟥 直接压入无锁队列,耗时极其微小,绝不卡硬件采集
_insertQueue.Enqueue(entity);
// D. 🟥 核心策略三定量触发。如果采集数据爆棚瞬间堆积超过50条不等1秒定时器直接开辟后台任务去写入
if (_insertQueue.Count >= BulkInsertThreshold)
{
_ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false));
}
}
}
}
catch
{
// 某个硬件通道故障时不干扰其他通道
}
}, token));
}
// 2. 批量等待并发采样,加入 2 秒强制超时保护兜底
if (sampleTasks.Count > 0)
{
try
{
var delayTask = Task.Delay(TimeSpan.FromSeconds(2), token);
await Task.WhenAny(Task.WhenAll(sampleTasks), delayTask);
}
catch (OperationCanceledException)
{
return;
}
}
// 3. 【回归 UI 线程】执行 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 Navigation
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();
// 🟥 1. 先把后台数据库消费专线拉起来
StartDbFlushWorker(_monitorCTS.Token);
// 2. 扫描可用硬件方法
DiscoverAvailableMethods();
// 3. 开启 100ms 硬件采集
_sampleTimer.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
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();
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
}
}