Files
ADP/MonitorModule/ViewModels/MonitorViewModel.cs
2026-07-06 10:21:10 +08:00

546 lines
20 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.Diagnostics;
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;
/// <summary>广播器(由 UIShare 层注入,每 Scope 一个实例)</summary>
private HardwareDataBroadcaster? _broadcaster;
/// <summary>用于 OxyPlot X 轴相对秒数</summary>
private readonly Stopwatch _stopwatch = new();
/// <summary>OxyPlot 刷新定时器(仅负责滚动 X 轴 + InvalidatePlot</summary>
private readonly DispatcherTimer _plotRefreshTimer;
/// <summary>事件订阅令牌,用于 Dispose 时取消订阅</summary>
private SubscriptionToken? _subscriptionToken;
// ==========================================
// 批量入库核心结构
// ==========================================
private readonly ConcurrentQueue<MonitorValueEntity> _insertQueue = new();
private Task? _dbFlushTask;
private const int BulkInsertThreshold = 50;
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);
// OxyPlot 刷新定时器1000ms只负责视觉更新
_plotRefreshTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromMilliseconds(1000)
};
_plotRefreshTimer.Tick += OnPlotRefreshTick;
}
public void Dispose()
{
_monitorCTS?.Cancel();
_monitorCTS?.Dispose();
_monitorCTS = null;
_plotRefreshTimer.Stop();
_plotRefreshTimer.Tick -= OnPlotRefreshTick;
_stopwatch.Stop();
// 取消 EventAggregator 订阅
if (_subscriptionToken != null)
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Unsubscribe(_subscriptionToken);
if (_dbFlushTask != null)
{
try { _dbFlushTask.Wait(TimeSpan.FromSeconds(1.5)); } catch { }
}
// 3. 释放容器作用域
_scope?.Dispose();
Channels.Clear();
AvailableMethods.Clear();
}
#region 广
/// <summary>
/// 由 HardwareDataBroadcaster 通过 EventAggregator 广播触发。
/// 已通过 ThreadOption.UIThread 确保在 UI 线程执行。
/// 通过 HardwareFingerprint + MethodName 匹配监测通道。
/// </summary>
private void OnHardwareDataReceived(HardwareReportArgs args)
{
// 仅处理属于当前 Scope 的事件
if (string.IsNullOrEmpty(TestStatus) || args.Scope != TestStatus) return;
// 以硬件指纹 + 方法名唯一匹配通道(与逻辑名解耦)
var channel = Channels.FirstOrDefault(c =>
c.Fingerprint == args.HardwareFingerprint &&
c.MethodName == args.MethodName &&
c.IsMonitored);
if (channel == null) return;
double time = _stopwatch.Elapsed.TotalSeconds;
// 记录数据点线程安全Record 内部 ConcurrentQueue + Series 操作)
channel.Record(time, args.Value);
// 入队数据库批量写入
// CreateTime 使用 args.Time采样触发时刻而不是 DateTime.Now设备响应到达时刻
// 这样即使设备响应有延迟,数据库里的时间戳仍然按设定采样间隔分布。
var entity = new MonitorValueEntity
{
MonitorName = channel.DisplayName,
MonitorValue = args.Value,
Scope = args.Scope,
CreateTime = args.Time,
IsDel = 0
};
_insertQueue.Enqueue(entity);
if (_insertQueue.Count >= BulkInsertThreshold)
{
_ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false));
}
}
#endregion
#region OxyPlot
private void OnPlotRefreshTick(object? sender, EventArgs e)
{
if (!_stopwatch.IsRunning) return;
double elapsed = _stopwatch.Elapsed.TotalSeconds;
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
double window = 10000 * 0.1; // 20s 视窗
xAxis.Minimum = Math.Max(0, elapsed - window);
xAxis.Maximum = elapsed + 0.5;
}
Plot.InvalidatePlot(true);
}
#endregion
#region
private void StartDbFlushWorker(CancellationToken token)
{
_dbFlushTask = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
try
{
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);
}
private async Task DoFlushWorkAsync()
{
if (_insertQueue.IsEmpty) return;
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)
{
var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false);
}
}
finally { Interlocked.Exchange(ref _isFlushing, 0); }
}
#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>();
_broadcaster = _scope.Resolve<HardwareDataBroadcaster>();
IsInitiated = true;
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
// 1. 启动后台数据库消费
StartDbFlushWorker(_monitorCTS.Token);
// 2. 扫描可监测方法(供 UI 选择)
DiscoverAvailableMethods();
// 3. 启动广播器Discover + Start
_broadcaster.Discover();
_broadcaster.Start();
// 4. 订阅 HardwareDataReportedEventUI 线程回调)
_subscriptionToken = _eventAggregator.GetEvent<HardwareDataReportedEvent>()
.Subscribe(OnHardwareDataReceived, ThreadOption.UIThread);
// 5. 启动 OxyPlot 视觉刷新定时器
_stopwatch.Start();
_plotRefreshTimer.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 UI
/// <summary>
/// 通过 GlobalInfo.HardwarePool 构建 设备实例引用→硬件指纹 的反向查找表。
/// </summary>
private Dictionary<object, string> BuildInstanceToFingerprint()
{
var map = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
foreach (var poolEntry in _globalInfo.HardwarePool)
{
var lazy = poolEntry.Value;
if (lazy.IsValueCreated && lazy.Value != null)
map[lazy.Value] = poolEntry.Key;
}
return map;
}
private void DiscoverAvailableMethods()
{
AvailableMethods.Clear();
if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0)
{
StatusMessage = "当前工位无可用设备";
return;
}
var instanceToFp = BuildInstanceToFingerprint();
foreach (var kvp in _deviceManager.DeviceMap)
{
string deviceName = kvp.Key;
var device = kvp.Value;
var deviceType = device.GetType();
// 反查该实例的硬件指纹
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
continue;
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
});
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<MonitorableAttribute>();
string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}";
AvailableMethods.Add(new AvailableMethodItem
{
DeviceName = deviceName,
Fingerprint = fingerprint,
MethodName = method.Name,
DisplayName = displayName,
MethodInfo = method,
Device = device
});
}
}
StatusMessage = AvailableMethods.Count > 0
? $"发现 {AvailableMethods.Count} 个可监测项"
: "未发现可监测的设备方法";
}
#endregion
#region
private void OnAddChannel()
{
var method = SelectedAvailableMethod;
if (method == null)
{
StatusMessage = "请先在列表中选择一个监测项";
return;
}
// 以指纹+方法名去重,防止同一物理设备重复添加
if (Channels.Any(c => c.Fingerprint == method.Fingerprint && c.MethodName == method.MethodName))
{
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
return;
}
var color = _palette[_colorIndex % _palette.Length];
_colorIndex++;
var channel = new MonitorChannel
{
DeviceName = method.DeviceName,
Fingerprint = method.Fingerprint,
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);
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);
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 - 10000);
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 - 10000);
for (int i = startIdx; i < points.Length; i++)
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
Plot.InvalidatePlot(true);
}
#endregion
}
}