监控数据添加

This commit is contained in:
hsc
2026-06-26 16:07:22 +08:00
parent 4567f058b5
commit 571bdbf258
7 changed files with 222 additions and 76 deletions

View File

@@ -65,7 +65,7 @@ namespace ADP
//初始化数据库 //初始化数据库
//DatabaseConfig.SetTenant(10001); //DatabaseConfig.SetTenant(10001);
//DatabaseConfig.InitMySql("127.0.0.1",3306,"ADP","root","123456"); //DatabaseConfig.InitSqlite("127.0.0.1", 3306, "ADP", "root", "123456");
//DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true); //DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
//SqlSugarContext.InitDatabase(); //SqlSugarContext.InitDatabase();
//显示登录窗口 //显示登录窗口

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Model.Entity
{
public class MonitorValueEntity:BaseEntity
{
public string MonitorName;
public double MonitorValue;
}
}

View 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!;
}
}

View File

@@ -68,20 +68,6 @@ namespace MonitorModule.ViewModels
public void Record(double time, double rawValue) public void Record(double time, double rawValue)
{ {
double displayValue = 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)); DataPoints.Enqueue((time, rawValue, displayValue));

View File

@@ -1,16 +1,23 @@
using OxyPlot; using Common.Attributes;
using Model.Entity;
using Model.Models;
using OxyPlot;
using OxyPlot.Axes; using OxyPlot.Axes;
using OxyPlot.Legends; using OxyPlot.Legends;
using OxyPlot.Series; using OxyPlot.Series;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading; using System.Windows.Threading;
using UIShare.GlobalVariable; using UIShare.GlobalVariable;
using UIShare.PubEvent; using UIShare.PubEvent;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
using Prism.Mvvm;
using Common.Attributes;
namespace MonitorModule.ViewModels namespace MonitorModule.ViewModels
{ {
@@ -18,7 +25,7 @@ namespace MonitorModule.ViewModels
{ {
#region #region
public bool KeepAlive => true; public bool KeepAlive => true;
public ScopedContext _scopedContext { get; set; } public ScopedContext _scopedContext { get; set; } = null!;
public GlobalInfo _globalInfo { get; set; } public GlobalInfo _globalInfo { get; set; }
public string TestStatus public string TestStatus
@@ -66,8 +73,9 @@ namespace MonitorModule.ViewModels
#region #region
private bool IsInitiated = false; private bool IsInitiated = false;
private IScopedProvider _scope; private IScopedProvider? _scope;
private DeviceManager? _deviceManager; private DeviceManager? _deviceManager;
private CancellationTokenSource? _monitorCTS = new();
private static readonly OxyColor[] _palette = private static readonly OxyColor[] _palette =
{ {
@@ -85,9 +93,6 @@ namespace MonitorModule.ViewModels
/// <summary>反射方法缓存Channel → (MethodInfo, DeviceInstance)</summary> /// <summary>反射方法缓存Channel → (MethodInfo, DeviceInstance)</summary>
private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new(); private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new();
/// <summary>方法名前缀:匹配这些开头的公共方法被视为可监测项</summary>
private static readonly string[] _methodPrefixes = { "查询", "读取", "获取", "测量" };
#endregion #endregion
public MonitorViewModel(IContainerExtension container) : base(container) public MonitorViewModel(IContainerExtension container) : base(container)
@@ -106,8 +111,22 @@ namespace MonitorModule.ViewModels
public void Dispose() public void Dispose()
{ {
// 1. 率先掐断后台正在并行的所有硬件请求
_monitorCTS?.Cancel();
_monitorCTS?.Dispose();
_monitorCTS = null;
// 2. 停掉定时器,停止触发 UI 刷新
_sampleTimer?.Stop(); _sampleTimer?.Stop();
_sampleTimer.Tick -= OnSampleTick;
// 3. 释放容器作用域
_scope?.Dispose(); _scope?.Dispose();
// 4. 清理集合
Channels.Clear();
AvailableMethods.Clear();
_methodCache.Clear();
} }
#region PlotModel #region PlotModel
@@ -144,9 +163,6 @@ namespace MonitorModule.ViewModels
#endregion #endregion
#region #region
/// <summary>
/// 扫描 DeviceManager.DeviceMap 中所有设备,反射找出可监测的方法。
/// </summary>
private void DiscoverAvailableMethods() private void DiscoverAvailableMethods()
{ {
AvailableMethods.Clear(); AvailableMethods.Clear();
@@ -162,18 +178,15 @@ namespace MonitorModule.ViewModels
var device = kvp.Value; var device = kvp.Value;
var deviceType = device.GetType(); var deviceType = device.GetType();
// 核心优化:直接通过特性和参数过滤方法 // 改用特性过滤:仅扫描带有 [Monitorable] 特性且签名正确的方法
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance) var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => .Where(m =>
{ {
// 1. 核心过滤:必须包含 [Monitorable] 特性
var attribute = m.GetCustomAttribute<MonitorableAttribute>(); var attribute = m.GetCustomAttribute<MonitorableAttribute>();
if (attribute == null) return false; if (attribute == null) return false;
// 2. 返回类型校验(如果强制要求是 Task<string>
if (m.ReturnType != typeof(Task<string>)) return false; if (m.ReturnType != typeof(Task<string>)) return false;
// 3. 参数校验:只取无参或仅有 CancellationToken 参数的方法
var parms = m.GetParameters(); var parms = m.GetParameters();
bool validParams = parms.Length == 0 || bool validParams = parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
@@ -183,7 +196,6 @@ namespace MonitorModule.ViewModels
foreach (var method in methods) foreach (var method in methods)
{ {
// 如果你在特性里写了 Description这里可以拿出来赋给 DisplayName
var attr = method.GetCustomAttribute<MonitorableAttribute>(); var attr = method.GetCustomAttribute<MonitorableAttribute>();
string displayName = !string.IsNullOrEmpty(attr?.Description) string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}" ? $"{deviceName}.{attr.Description}"
@@ -193,7 +205,7 @@ namespace MonitorModule.ViewModels
{ {
DeviceName = deviceName, DeviceName = deviceName,
MethodName = method.Name, MethodName = method.Name,
DisplayName = displayName, // 优先使用特性的中文描述 DisplayName = displayName,
MethodInfo = method, MethodInfo = method,
Device = device Device = device
}; };
@@ -217,7 +229,6 @@ namespace MonitorModule.ViewModels
return; return;
} }
// 防止重复添加同一设备方法
if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName)) if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName))
{ {
StatusMessage = $"监测项 [{method.DisplayName}] 已存在"; StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
@@ -237,7 +248,6 @@ namespace MonitorModule.ViewModels
IsDisplayed = true IsDisplayed = true
}; };
// 创建 Series 并加入 Plot
channel.Series = new LineSeries channel.Series = new LineSeries
{ {
Title = channel.DisplayName, Title = channel.DisplayName,
@@ -246,16 +256,12 @@ namespace MonitorModule.ViewModels
}; };
Plot.Series.Add(channel.Series); Plot.Series.Add(channel.Series);
// 缓存反射信息
_methodCache[channel] = (method.MethodInfo, method.Device); _methodCache[channel] = (method.MethodInfo, method.Device);
// 订阅属性变化IsDisplayed 控制显示/隐藏MathExpression 控制数学变换
channel.PropertyChanged += (s, e) => channel.PropertyChanged += (s, e) =>
{ {
if (e.PropertyName == nameof(MonitorChannel.IsDisplayed)) if (e.PropertyName == nameof(MonitorChannel.IsDisplayed))
OnChannelDisplayChanged(channel); OnChannelDisplayChanged(channel);
else if (e.PropertyName == nameof(MonitorChannel.MathExpression))
OnChannelMathChanged(channel);
}; };
Channels.Add(channel); Channels.Add(channel);
@@ -307,15 +313,11 @@ namespace MonitorModule.ViewModels
} }
#endregion #endregion
#region / #region /
/// <summary>
/// 当 Channel.IsDisplayed 变化时调用:控制 Series 的创建/移除
/// </summary>
public void OnChannelDisplayChanged(MonitorChannel channel) public void OnChannelDisplayChanged(MonitorChannel channel)
{ {
if (channel.IsDisplayed) if (channel.IsDisplayed)
{ {
// 从 false → true创建 Series回填最近数据
if (channel.Series == null) if (channel.Series == null)
{ {
channel.Series = new LineSeries channel.Series = new LineSeries
@@ -325,7 +327,6 @@ namespace MonitorModule.ViewModels
StrokeThickness = 1.5 StrokeThickness = 1.5
}; };
// 回填最近 200 个数据点
var recent = channel.DataPoints.ToArray(); var recent = channel.DataPoints.ToArray();
var startIdx = Math.Max(0, recent.Length - 200); var startIdx = Math.Max(0, recent.Length - 200);
for (int i = startIdx; i < recent.Length; i++) for (int i = startIdx; i < recent.Length; i++)
@@ -339,7 +340,6 @@ namespace MonitorModule.ViewModels
} }
else else
{ {
// 从 true → false移除 Series数据继续记录
if (channel.Series != null) if (channel.Series != null)
{ {
Plot.Series.Remove(channel.Series); Plot.Series.Remove(channel.Series);
@@ -349,14 +349,10 @@ namespace MonitorModule.ViewModels
} }
} }
/// <summary>
/// 当 Channel.MathExpression 变化时调用:重新计算所有已记录数据点的 DisplayValue
/// </summary>
public void OnChannelMathChanged(MonitorChannel channel) public void OnChannelMathChanged(MonitorChannel channel)
{ {
if (channel.Series == null) return; if (channel.Series == null) return;
// 用新表达式重算 Series 中的点
var points = channel.DataPoints.ToArray(); var points = channel.DataPoints.ToArray();
channel.Series.Points.Clear(); channel.Series.Points.Clear();
var startIdx = Math.Max(0, points.Length - 200); var startIdx = Math.Max(0, points.Length - 200);
@@ -368,38 +364,73 @@ namespace MonitorModule.ViewModels
} }
#endregion #endregion
#region #region
private async void OnSampleTick(object? sender, EventArgs e) private async void OnSampleTick(object? sender, EventArgs e)
{ {
_sampleTime += _sampleInterval; _sampleTime += _sampleInterval;
if (Channels.Count == 0 || _methodCache.Count == 0) return; 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) foreach (var channel in Channels)
{ {
if (!channel.IsMonitored) continue; if (!channel.IsMonitored) continue;
if (!_methodCache.TryGetValue(channel, out var entry)) 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 try
{ {
var (method, device) = entry; var delayTask = Task.Delay(TimeSpan.FromSeconds(2), token);
// 调用设备方法获取返回值 await Task.WhenAny(Task.WhenAll(sampleTasks), delayTask).ConfigureAwait(true);
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 catch (OperationCanceledException)
{ {
// 设备读取失败时跳过该通道本次采样 return;
} }
} }
// 让 X 轴跟随最新数据滚动 // 3. 【回归UI线程】由于前面 ConfigureAwait(true),这里直接执行 OxyPlot 坐标滚动与刷新
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom); var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null) if (xAxis != null)
{ {
@@ -426,6 +457,9 @@ namespace MonitorModule.ViewModels
_deviceManager = _scope.Resolve<DeviceManager>(); _deviceManager = _scope.Resolve<DeviceManager>();
IsInitiated = true; IsInitiated = true;
// 初始化全新的取消信号源
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
// 扫描可用监测项 // 扫描可用监测项
DiscoverAvailableMethods(); DiscoverAvailableMethods();
@@ -435,16 +469,4 @@ namespace MonitorModule.ViewModels
} }
#endregion #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!;
}
} }

View 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);
}
}
}
}

View 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);
}
}