监控数据添加
This commit is contained in:
@@ -64,10 +64,10 @@ namespace ADP
|
||||
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
|
||||
|
||||
//初始化数据库
|
||||
//DatabaseConfig.SetTenant(10001);
|
||||
//DatabaseConfig.InitMySql("127.0.0.1",3306,"ADP","root","123456");
|
||||
//DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
||||
//SqlSugarContext.InitDatabase();
|
||||
DatabaseConfig.SetTenant(10001);
|
||||
DatabaseConfig.InitSqlite();
|
||||
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
||||
SqlSugarContext.InitDatabase();
|
||||
//显示登录窗口
|
||||
var login = Container.Resolve<LoginModuleView>();
|
||||
var re = Container.Resolve<IRegionManager>();
|
||||
@@ -100,6 +100,8 @@ namespace ADP
|
||||
containerRegistry.RegisterInstance<INotificationManager>(NotificationManager);
|
||||
// 注册仓储
|
||||
containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>));
|
||||
//注册服务
|
||||
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
|
||||
}
|
||||
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
||||
protected override IModuleCatalog CreateModuleCatalog()
|
||||
|
||||
17
Model/Entity/MonitorValueEntity.cs
Normal file
17
Model/Entity/MonitorValueEntity.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Model.Entity
|
||||
{
|
||||
public class MonitorValueEntity:BaseEntity
|
||||
{
|
||||
[SugarColumn(ColumnName = "MonitorName")]
|
||||
public string MonitorName { get; set; }
|
||||
[SugarColumn(ColumnName = "MonitorValue")]
|
||||
public double MonitorValue { get; set; }
|
||||
}
|
||||
}
|
||||
21
Model/Models/AvailableMethodItem.cs
Normal file
21
Model/Models/AvailableMethodItem.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Model.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 可用设备方法项(供用户选择添加为监测通道)
|
||||
/// </summary>
|
||||
public class AvailableMethodItem
|
||||
{
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public string MethodName { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public MethodInfo MethodInfo { get; set; } = null!;
|
||||
public object Device { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
18
Model/Models/HardwareReportArgs.cs
Normal file
18
Model/Models/HardwareReportArgs.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Model.Models
|
||||
{
|
||||
public class HardwareReportArgs
|
||||
{
|
||||
public string HardwareFingerprint { get; set; } = string.Empty;
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
public double Value { get; set; }
|
||||
|
||||
public DateTime Time { get; set; } = DateTime.Now;
|
||||
}
|
||||
}
|
||||
@@ -68,20 +68,6 @@ namespace MonitorModule.ViewModels
|
||||
public void Record(double time, double rawValue)
|
||||
{
|
||||
double displayValue = rawValue;
|
||||
if (!string.IsNullOrWhiteSpace(MathExpression))
|
||||
{
|
||||
try
|
||||
{
|
||||
var expr = new NCalc.Expression(MathExpression);
|
||||
expr.Parameters["x"] = rawValue;
|
||||
var result = expr.Evaluate();
|
||||
displayValue = Convert.ToDouble(result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
displayValue = rawValue;
|
||||
}
|
||||
}
|
||||
|
||||
DataPoints.Enqueue((time, rawValue, displayValue));
|
||||
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
using OxyPlot;
|
||||
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;
|
||||
using Prism.Mvvm;
|
||||
using Common.Attributes;
|
||||
|
||||
namespace MonitorModule.ViewModels
|
||||
{
|
||||
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||
{
|
||||
#region 属性
|
||||
private IMonitorValueService _monitorValueService;
|
||||
public bool KeepAlive => true;
|
||||
public ScopedContext _scopedContext { get; set; }
|
||||
public ScopedContext _scopedContext { get; set; } = null!;
|
||||
public GlobalInfo _globalInfo { get; set; }
|
||||
|
||||
private string _testStatus = string.Empty;
|
||||
public string TestStatus
|
||||
{
|
||||
get => _testStatus;
|
||||
@@ -49,6 +59,7 @@ namespace MonitorModule.ViewModels
|
||||
set => SetProperty(ref _selectedAvailableMethod, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = "图表已就绪,暂无监测项";
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
@@ -64,11 +75,13 @@ namespace MonitorModule.ViewModels
|
||||
public ICommand RefreshCommand { get; }
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
#region 私有字段与并发缓存
|
||||
private bool IsInitiated = false;
|
||||
private IScopedProvider _scope;
|
||||
private IScopedProvider? _scope;
|
||||
private DeviceManager? _deviceManager;
|
||||
private CancellationTokenSource? _monitorCTS = new();
|
||||
|
||||
// 颜色调色盘
|
||||
private static readonly OxyColor[] _palette =
|
||||
{
|
||||
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
|
||||
@@ -77,39 +90,271 @@ namespace MonitorModule.ViewModels
|
||||
};
|
||||
private int _colorIndex;
|
||||
|
||||
// 图表采样定时器 (100ms)
|
||||
private readonly DispatcherTimer _sampleTimer;
|
||||
private double _sampleTime;
|
||||
private const double _sampleInterval = 0.1; // 100ms
|
||||
private string _testStatus = string.Empty;
|
||||
private string _statusMessage = "图表已就绪,暂无监测项";
|
||||
private const double _sampleInterval = 0.1;
|
||||
|
||||
/// <summary>反射方法缓存:Channel → (MethodInfo, DeviceInstance)</summary>
|
||||
private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new();
|
||||
|
||||
/// <summary>方法名前缀:匹配这些开头的公共方法被视为可监测项</summary>
|
||||
private static readonly string[] _methodPrefixes = { "查询", "读取", "获取", "测量" };
|
||||
// ==========================================
|
||||
// 🟥 基于 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()
|
||||
{
|
||||
@@ -144,9 +389,6 @@ namespace MonitorModule.ViewModels
|
||||
#endregion
|
||||
|
||||
#region 设备方法发现
|
||||
/// <summary>
|
||||
/// 扫描 DeviceManager.DeviceMap 中所有设备,反射找出可监测的方法。
|
||||
/// </summary>
|
||||
private void DiscoverAvailableMethods()
|
||||
{
|
||||
AvailableMethods.Clear();
|
||||
@@ -162,18 +404,13 @@ namespace MonitorModule.ViewModels
|
||||
var device = kvp.Value;
|
||||
var deviceType = device.GetType();
|
||||
|
||||
// 核心优化:直接通过特性和参数过滤方法
|
||||
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m =>
|
||||
{
|
||||
// 1. 核心过滤:必须包含 [Monitorable] 特性
|
||||
var attribute = m.GetCustomAttribute<MonitorableAttribute>();
|
||||
if (attribute == null) return false;
|
||||
|
||||
// 2. 返回类型校验(如果强制要求是 Task<string>)
|
||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
||||
|
||||
// 3. 参数校验:只取无参或仅有 CancellationToken 参数的方法
|
||||
var parms = m.GetParameters();
|
||||
bool validParams = parms.Length == 0 ||
|
||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||
@@ -183,7 +420,6 @@ namespace MonitorModule.ViewModels
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
// 如果你在特性里写了 Description,这里可以拿出来赋给 DisplayName
|
||||
var attr = method.GetCustomAttribute<MonitorableAttribute>();
|
||||
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
||||
? $"{deviceName}.{attr.Description}"
|
||||
@@ -193,7 +429,7 @@ namespace MonitorModule.ViewModels
|
||||
{
|
||||
DeviceName = deviceName,
|
||||
MethodName = method.Name,
|
||||
DisplayName = displayName, // 优先使用特性的中文描述
|
||||
DisplayName = displayName,
|
||||
MethodInfo = method,
|
||||
Device = device
|
||||
};
|
||||
@@ -217,7 +453,6 @@ namespace MonitorModule.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复添加同一设备方法
|
||||
if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName))
|
||||
{
|
||||
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
|
||||
@@ -237,7 +472,6 @@ namespace MonitorModule.ViewModels
|
||||
IsDisplayed = true
|
||||
};
|
||||
|
||||
// 创建 Series 并加入 Plot
|
||||
channel.Series = new LineSeries
|
||||
{
|
||||
Title = channel.DisplayName,
|
||||
@@ -246,16 +480,12 @@ namespace MonitorModule.ViewModels
|
||||
};
|
||||
Plot.Series.Add(channel.Series);
|
||||
|
||||
// 缓存反射信息
|
||||
_methodCache[channel] = (method.MethodInfo, method.Device);
|
||||
|
||||
// 订阅属性变化:IsDisplayed 控制显示/隐藏,MathExpression 控制数学变换
|
||||
channel.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MonitorChannel.IsDisplayed))
|
||||
OnChannelDisplayChanged(channel);
|
||||
else if (e.PropertyName == nameof(MonitorChannel.MathExpression))
|
||||
OnChannelMathChanged(channel);
|
||||
};
|
||||
|
||||
Channels.Add(channel);
|
||||
@@ -274,10 +504,8 @@ namespace MonitorModule.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.Series != null)
|
||||
{
|
||||
Plot.Series.Remove(target.Series);
|
||||
}
|
||||
if (target.Series != null) Plot.Series.Remove(target.Series);
|
||||
|
||||
_methodCache.Remove(target);
|
||||
Channels.Remove(target);
|
||||
SelectedChannel = Channels.LastOrDefault();
|
||||
@@ -307,15 +535,11 @@ namespace MonitorModule.ViewModels
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 显示/隐藏切换
|
||||
/// <summary>
|
||||
/// 当 Channel.IsDisplayed 变化时调用:控制 Series 的创建/移除
|
||||
/// </summary>
|
||||
#region 显示/隐藏切换与数学变换
|
||||
public void OnChannelDisplayChanged(MonitorChannel channel)
|
||||
{
|
||||
if (channel.IsDisplayed)
|
||||
{
|
||||
// 从 false → true:创建 Series,回填最近数据
|
||||
if (channel.Series == null)
|
||||
{
|
||||
channel.Series = new LineSeries
|
||||
@@ -325,7 +549,6 @@ namespace MonitorModule.ViewModels
|
||||
StrokeThickness = 1.5
|
||||
};
|
||||
|
||||
// 回填最近 200 个数据点
|
||||
var recent = channel.DataPoints.ToArray();
|
||||
var startIdx = Math.Max(0, recent.Length - 200);
|
||||
for (int i = startIdx; i < recent.Length; i++)
|
||||
@@ -339,7 +562,6 @@ namespace MonitorModule.ViewModels
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从 true → false:移除 Series,数据继续记录
|
||||
if (channel.Series != null)
|
||||
{
|
||||
Plot.Series.Remove(channel.Series);
|
||||
@@ -349,14 +571,10 @@ namespace MonitorModule.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 Channel.MathExpression 变化时调用:重新计算所有已记录数据点的 DisplayValue
|
||||
/// </summary>
|
||||
public void OnChannelMathChanged(MonitorChannel channel)
|
||||
{
|
||||
if (channel.Series == null) return;
|
||||
|
||||
// 用新表达式重算 Series 中的点
|
||||
var points = channel.DataPoints.ToArray();
|
||||
channel.Series.Points.Clear();
|
||||
var startIdx = Math.Max(0, points.Length - 200);
|
||||
@@ -367,84 +585,5 @@ namespace MonitorModule.ViewModels
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 采样定时器
|
||||
private async void OnSampleTick(object? sender, EventArgs e)
|
||||
{
|
||||
_sampleTime += _sampleInterval;
|
||||
|
||||
if (Channels.Count == 0 || _methodCache.Count == 0) return;
|
||||
|
||||
foreach (var channel in Channels)
|
||||
{
|
||||
if (!channel.IsMonitored) continue;
|
||||
if (!_methodCache.TryGetValue(channel, out var entry)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var (method, device) = entry;
|
||||
// 调用设备方法获取返回值
|
||||
var task = (Task<string>)method.Invoke(device, null)!;
|
||||
var raw = await task.ConfigureAwait(true);
|
||||
|
||||
// 尝试解析为 double
|
||||
if (double.TryParse(raw, out double value))
|
||||
{
|
||||
channel.Record(_sampleTime, value);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 设备读取失败时跳过该通道本次采样
|
||||
}
|
||||
}
|
||||
|
||||
// 让 X 轴跟随最新数据滚动
|
||||
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;
|
||||
|
||||
// 扫描可用监测项
|
||||
DiscoverAvailableMethods();
|
||||
|
||||
// 启动采样定时器
|
||||
_sampleTimer.Start();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可用设备方法项(供用户选择添加为监测通道)
|
||||
/// </summary>
|
||||
public class AvailableMethodItem
|
||||
{
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public string MethodName { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public MethodInfo MethodInfo { get; set; } = null!;
|
||||
public object Device { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ namespace ORM
|
||||
|
||||
// 拼接数据库文件路径
|
||||
string DBPath = Path.Combine(folder, "SQL.db");
|
||||
DbConnectionString = $"Data Source={DBPath};Version=3;";
|
||||
DbConnectionString = $"Data Source={DBPath};";
|
||||
}
|
||||
public static void InitMySql(string Server, int Port, string Database, string Uid,string Pwd)
|
||||
{
|
||||
|
||||
71
Service/Implement/MonitorValueService.cs
Normal file
71
Service/Implement/MonitorValueService.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Model;
|
||||
using Model.Entity;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 监测数值记录服务实现
|
||||
/// </summary>
|
||||
public class MonitorValueService : BaseService<MonitorValueEntity>, IMonitorValueService
|
||||
{
|
||||
// 继承父类构造函数,注入 SqlSugar 仓储
|
||||
public MonitorValueService(SqlSugarRepository<MonitorValueEntity> repository) : base(repository)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量插入监测数据
|
||||
/// </summary>
|
||||
public async Task<Result<bool>> InsertRangeAsync(List<MonitorValueEntity> entities)
|
||||
{
|
||||
if (entities == null || entities.Count == 0)
|
||||
return Result<bool>.Success(true);
|
||||
|
||||
try
|
||||
{
|
||||
// 使用 SqlSugar 的批量插入,底层会转换为 BulkCopy 或是批量 SQL,速度极快
|
||||
var result = await _repository.Context.Insertable(entities).ExecuteCommandAsync();
|
||||
return Result<bool>.Success(result > 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Error("批量插入监测数据失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据监测通道名称,分页查询历史记录
|
||||
/// </summary>
|
||||
public async Task<Result<List<MonitorValueEntity>>> GetPagedByNameAsync(string monitorName, int pageIndex, int pageSize, RefAsync<int> total)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = _repository.Entities;
|
||||
|
||||
// 如果传了名字则按名字过滤
|
||||
if (!string.IsNullOrEmpty(monitorName))
|
||||
{
|
||||
query = query.Where(x => x.MonitorName == monitorName);
|
||||
}
|
||||
|
||||
var list = await query
|
||||
.OrderByDescending(d => d.CreateTime) // 监测项通常优先看最新的数据
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
|
||||
// 计算总页数
|
||||
total.Value = (int)Math.Ceiling((double)total.Value / pageSize);
|
||||
return Result<List<MonitorValueEntity>>.Success(list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<MonitorValueEntity>>.Error($"根据名称 [{monitorName}] 分页查询数据失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Service/Interface/IMonitorValueService.cs
Normal file
32
Service/Interface/IMonitorValueService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Model;
|
||||
using Model.Entity;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Interface
|
||||
{
|
||||
/// <summary>
|
||||
/// 监测数值记录服务接口
|
||||
/// </summary>
|
||||
public interface IMonitorValueService : IBaseService<MonitorValueEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// 批量插入监测数据(工控高频采样推荐使用,性能远高于单条循环插入)
|
||||
/// </summary>
|
||||
/// <param name="entities">实体集合</param>
|
||||
/// <returns>返回操作是否成功的 Result</returns>
|
||||
Task<Result<bool>> InsertRangeAsync(List<MonitorValueEntity> entities);
|
||||
|
||||
/// <summary>
|
||||
/// 根据监测通道名称,分页查询历史记录
|
||||
/// </summary>
|
||||
/// <param name="monitorName">通道名称(例如:台架1.读取主轴温度)</param>
|
||||
/// <param name="pageIndex">页码</param>
|
||||
/// <param name="pageSize">每页大小</param>
|
||||
/// <param name="total">总条数输出</param>
|
||||
/// <returns>分页数据结果</returns>
|
||||
Task<Result<List<MonitorValueEntity>>> GetPagedByNameAsync(string monitorName, int pageIndex, int pageSize, RefAsync<int> total);
|
||||
}
|
||||
}
|
||||
13
UIShare/PubEvent/HardwareDataReportedEvent.cs
Normal file
13
UIShare/PubEvent/HardwareDataReportedEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class HardwareDataReportedEvent : PubSubEvent<HardwareReportArgs>
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user