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