diff --git a/ADP/App.xaml.cs b/ADP/App.xaml.cs
index ad332b4..65b3d0a 100644
--- a/ADP/App.xaml.cs
+++ b/ADP/App.xaml.cs
@@ -65,7 +65,7 @@ namespace ADP
//初始化数据库
//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);
//SqlSugarContext.InitDatabase();
//显示登录窗口
diff --git a/Model/Entity/MonitorValueEntity.cs b/Model/Entity/MonitorValueEntity.cs
new file mode 100644
index 0000000..16ac6a7
--- /dev/null
+++ b/Model/Entity/MonitorValueEntity.cs
@@ -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;
+ }
+}
diff --git a/Model/Models/AvailableMethodItem.cs b/Model/Models/AvailableMethodItem.cs
new file mode 100644
index 0000000..edb4671
--- /dev/null
+++ b/Model/Models/AvailableMethodItem.cs
@@ -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
+{
+ ///
+ /// 可用设备方法项(供用户选择添加为监测通道)
+ ///
+ 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!;
+ }
+}
diff --git a/MonitorModule/ViewModels/MonitorChannel.cs b/MonitorModule/ViewModels/MonitorChannel.cs
index d2038ce..3887764 100644
--- a/MonitorModule/ViewModels/MonitorChannel.cs
+++ b/MonitorModule/ViewModels/MonitorChannel.cs
@@ -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));
diff --git a/MonitorModule/ViewModels/MonitorViewModel.cs b/MonitorModule/ViewModels/MonitorViewModel.cs
index 10a26f1..0f033d1 100644
--- a/MonitorModule/ViewModels/MonitorViewModel.cs
+++ b/MonitorModule/ViewModels/MonitorViewModel.cs
@@ -1,16 +1,23 @@
-using OxyPlot;
+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;
-using Prism.Mvvm;
-using Common.Attributes;
+
namespace MonitorModule.ViewModels
{
@@ -18,7 +25,7 @@ namespace MonitorModule.ViewModels
{
#region 属性
public bool KeepAlive => true;
- public ScopedContext _scopedContext { get; set; }
+ public ScopedContext _scopedContext { get; set; } = null!;
public GlobalInfo _globalInfo { get; set; }
public string TestStatus
@@ -66,8 +73,9 @@ namespace MonitorModule.ViewModels
#region 私有字段
private bool IsInitiated = false;
- private IScopedProvider _scope;
+ private IScopedProvider? _scope;
private DeviceManager? _deviceManager;
+ private CancellationTokenSource? _monitorCTS = new();
private static readonly OxyColor[] _palette =
{
@@ -85,9 +93,6 @@ namespace MonitorModule.ViewModels
/// 反射方法缓存:Channel → (MethodInfo, DeviceInstance)
private readonly Dictionary _methodCache = new();
-
- /// 方法名前缀:匹配这些开头的公共方法被视为可监测项
- private static readonly string[] _methodPrefixes = { "查询", "读取", "获取", "测量" };
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
@@ -106,8 +111,22 @@ namespace MonitorModule.ViewModels
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 构建
@@ -144,9 +163,6 @@ namespace MonitorModule.ViewModels
#endregion
#region 设备方法发现
- ///
- /// 扫描 DeviceManager.DeviceMap 中所有设备,反射找出可监测的方法。
- ///
private void DiscoverAvailableMethods()
{
AvailableMethods.Clear();
@@ -162,18 +178,15 @@ namespace MonitorModule.ViewModels
var device = kvp.Value;
var deviceType = device.GetType();
- // 核心优化:直接通过特性和参数过滤方法
+ // 改用特性过滤:仅扫描带有 [Monitorable] 特性且签名正确的方法
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
- // 1. 核心过滤:必须包含 [Monitorable] 特性
var attribute = m.GetCustomAttribute();
if (attribute == null) return false;
- // 2. 返回类型校验(如果强制要求是 Task)
if (m.ReturnType != typeof(Task)) return false;
- // 3. 参数校验:只取无参或仅有 CancellationToken 参数的方法
var parms = m.GetParameters();
bool validParams = parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
@@ -183,7 +196,6 @@ namespace MonitorModule.ViewModels
foreach (var method in methods)
{
- // 如果你在特性里写了 Description,这里可以拿出来赋给 DisplayName
var attr = method.GetCustomAttribute();
string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}"
@@ -193,7 +205,7 @@ namespace MonitorModule.ViewModels
{
DeviceName = deviceName,
MethodName = method.Name,
- DisplayName = displayName, // 优先使用特性的中文描述
+ DisplayName = displayName,
MethodInfo = method,
Device = device
};
@@ -217,7 +229,6 @@ namespace MonitorModule.ViewModels
return;
}
- // 防止重复添加同一设备方法
if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName))
{
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
@@ -237,7 +248,6 @@ namespace MonitorModule.ViewModels
IsDisplayed = true
};
- // 创建 Series 并加入 Plot
channel.Series = new LineSeries
{
Title = channel.DisplayName,
@@ -246,16 +256,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);
@@ -307,15 +313,11 @@ namespace MonitorModule.ViewModels
}
#endregion
- #region 显示/隐藏切换
- ///
- /// 当 Channel.IsDisplayed 变化时调用:控制 Series 的创建/移除
- ///
+ #region 显示/隐藏切换与数学变换
public void OnChannelDisplayChanged(MonitorChannel channel)
{
if (channel.IsDisplayed)
{
- // 从 false → true:创建 Series,回填最近数据
if (channel.Series == null)
{
channel.Series = new LineSeries
@@ -325,7 +327,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 +340,6 @@ namespace MonitorModule.ViewModels
}
else
{
- // 从 true → false:移除 Series,数据继续记录
if (channel.Series != null)
{
Plot.Series.Remove(channel.Series);
@@ -349,14 +349,10 @@ namespace MonitorModule.ViewModels
}
}
- ///
- /// 当 Channel.MathExpression 变化时调用:重新计算所有已记录数据点的 DisplayValue
- ///
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);
@@ -368,38 +364,73 @@ namespace MonitorModule.ViewModels
}
#endregion
- #region 采样定时器
+ #region 采样定时器与高并发处理
private async void OnSampleTick(object? sender, EventArgs e)
{
_sampleTime += _sampleInterval;
if (Channels.Count == 0 || _methodCache.Count == 0) return;
+ var sampleTasks = new List();
+ 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 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 (method, device) = entry;
- // 调用设备方法获取返回值
- var task = (Task)method.Invoke(device, null)!;
- var raw = await task.ConfigureAwait(true);
-
- // 尝试解析为 double
- if (double.TryParse(raw, out double value))
- {
- channel.Record(_sampleTime, value);
- }
+ var delayTask = Task.Delay(TimeSpan.FromSeconds(2), token);
+ await Task.WhenAny(Task.WhenAll(sampleTasks), delayTask).ConfigureAwait(true);
}
- catch
+ catch (OperationCanceledException)
{
- // 设备读取失败时跳过该通道本次采样
+ return;
}
}
- // 让 X 轴跟随最新数据滚动
+ // 3. 【回归UI线程】由于前面 ConfigureAwait(true),这里直接执行 OxyPlot 坐标滚动与刷新
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
@@ -426,6 +457,9 @@ namespace MonitorModule.ViewModels
_deviceManager = _scope.Resolve();
IsInitiated = true;
+ // 初始化全新的取消信号源
+ if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
+
// 扫描可用监测项
DiscoverAvailableMethods();
@@ -435,16 +469,4 @@ namespace MonitorModule.ViewModels
}
#endregion
}
-
- ///
- /// 可用设备方法项(供用户选择添加为监测通道)
- ///
- 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!;
- }
-}
+}
\ No newline at end of file
diff --git a/Service/Implement/MonitorValueService.cs b/Service/Implement/MonitorValueService.cs
new file mode 100644
index 0000000..ed63919
--- /dev/null
+++ b/Service/Implement/MonitorValueService.cs
@@ -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
+{
+ ///
+ /// 监测数值记录服务实现
+ ///
+ public class MonitorValueService : BaseService, IMonitorValueService
+ {
+ // 继承父类构造函数,注入 SqlSugar 仓储
+ public MonitorValueService(SqlSugarRepository repository) : base(repository)
+ {
+ }
+
+ ///
+ /// 批量插入监测数据
+ ///
+ public async Task> InsertRangeAsync(List entities)
+ {
+ if (entities == null || entities.Count == 0)
+ return Result.Success(true);
+
+ try
+ {
+ // 使用 SqlSugar 的批量插入,底层会转换为 BulkCopy 或是批量 SQL,速度极快
+ var result = await _repository.Context.Insertable(entities).ExecuteCommandAsync();
+ return Result.Success(result > 0);
+ }
+ catch (Exception ex)
+ {
+ return Result.Error("批量插入监测数据失败", ex);
+ }
+ }
+
+ ///
+ /// 根据监测通道名称,分页查询历史记录
+ ///
+ public async Task>> GetPagedByNameAsync(string monitorName, int pageIndex, int pageSize, RefAsync 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>.Success(list);
+ }
+ catch (Exception ex)
+ {
+ return Result>.Error($"根据名称 [{monitorName}] 分页查询数据失败", ex);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Service/Interface/IMonitorValueService.cs b/Service/Interface/IMonitorValueService.cs
new file mode 100644
index 0000000..6e3dc27
--- /dev/null
+++ b/Service/Interface/IMonitorValueService.cs
@@ -0,0 +1,32 @@
+using Model;
+using Model.Entity;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Service.Interface
+{
+ ///
+ /// 监测数值记录服务接口
+ ///
+ public interface IMonitorValueService : IBaseService
+ {
+ ///
+ /// 批量插入监测数据(工控高频采样推荐使用,性能远高于单条循环插入)
+ ///
+ /// 实体集合
+ /// 返回操作是否成功的 Result
+ Task> InsertRangeAsync(List entities);
+
+ ///
+ /// 根据监测通道名称,分页查询历史记录
+ ///
+ /// 通道名称(例如:台架1.读取主轴温度)
+ /// 页码
+ /// 每页大小
+ /// 总条数输出
+ /// 分页数据结果
+ Task>> GetPagedByNameAsync(string monitorName, int pageIndex, int pageSize, RefAsync total);
+ }
+}
\ No newline at end of file