From 0e6bcc23460a32981d0a01c6e15d6859129e1b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=AF=E9=BE=99=20=E6=9B=B9?= <1805857645@QQ.com> Date: Fri, 13 Mar 2026 16:38:25 +0800 Subject: [PATCH] =?UTF-8?q?change=20-=E6=B7=BB=E5=8A=A0=E4=B8=89=E8=89=B2?= =?UTF-8?q?=E7=81=AF=E9=87=87=E9=9B=86=E7=BA=BF=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceCollectionBusiness.cs | 8 + .../DeviceDataCollector.cs | 800 +++++++++++++++++- .../BaseDeviceInfoCacheService.cs | 21 +- .../dao/BaseDeviceAlarmVal.cs | 52 ++ .../dao/BaseRecordShutDown.cs | 87 ++ Sln.Imm.Daemon.Model/dto/AlarmStateRecord.cs | 16 + Sln.Imm.Daemon.Model/dto/OpcNode.cs | 2 +- Sln.Imm.Daemon.Opc/Impl/OpcDaService.cs | 48 +- Sln.Imm.Daemon.Opc/Impl/OpcUaService.cs | 131 ++- .../service/IBaseDeviceAlarmValService.cs | 16 + .../service/IBaseDeviceParamService.cs | 5 + .../service/IBaseRecordShutDownService.cs | 15 + .../Impl/BaseDeviceAlarmValServiceImpl.cs | 53 ++ .../Impl/BaseDeviceParamServiceImpl.cs | 38 +- .../Impl/BaseRecordShutDownServiceImpl.cs | 53 ++ Sln.Imm.Daemon/Program.cs | 17 +- Sln.Imm.Daemon/appsettings.json | 3 +- 17 files changed, 1295 insertions(+), 70 deletions(-) create mode 100644 Sln.Imm.Daemon.Model/dao/BaseDeviceAlarmVal.cs create mode 100644 Sln.Imm.Daemon.Model/dao/BaseRecordShutDown.cs create mode 100644 Sln.Imm.Daemon.Model/dto/AlarmStateRecord.cs create mode 100644 Sln.Imm.Daemon.Repository/service/IBaseDeviceAlarmValService.cs create mode 100644 Sln.Imm.Daemon.Repository/service/IBaseRecordShutDownService.cs create mode 100644 Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceAlarmValServiceImpl.cs create mode 100644 Sln.Imm.Daemon.Repository/service/Impl/BaseRecordShutDownServiceImpl.cs diff --git a/Sln.Imm.Daemon.Business/DeviceCollectionBusiness.cs b/Sln.Imm.Daemon.Business/DeviceCollectionBusiness.cs index aa6de33..9a2aefd 100644 --- a/Sln.Imm.Daemon.Business/DeviceCollectionBusiness.cs +++ b/Sln.Imm.Daemon.Business/DeviceCollectionBusiness.cs @@ -31,6 +31,7 @@ using Sln.Imm.Daemon.Opc; using Sln.Imm.Daemon.Opc.Impl; using Sln.Imm.Daemon.Repository.service.@base; using Sln.Imm.Daemon.Serilog; +using System.Diagnostics; namespace Sln.Imm.Daemon.Business; @@ -170,10 +171,17 @@ public class DeviceCollectionBusiness throw new ArgumentNullException($"设备信息不允许为空"); } + List deviceParams = device.deviceParams.Select(x => x.paramAddr).ToList(); + + Stopwatch sw = Stopwatch.StartNew(); List infos = await opcUa.ReadNodeAsync(deviceParams); + sw.Stop(); + + _serilog.Info($"opc读取耗时{sw.ElapsedMilliseconds.ToString()}"); + return infos; } diff --git a/Sln.Imm.Daemon.Business/DeviceDataCollector.cs b/Sln.Imm.Daemon.Business/DeviceDataCollector.cs index 7145712..ba463e5 100644 --- a/Sln.Imm.Daemon.Business/DeviceDataCollector.cs +++ b/Sln.Imm.Daemon.Business/DeviceDataCollector.cs @@ -23,14 +23,22 @@ #endregion << 版 本 注 释 >> +using Dm.util; +using Models; using Newtonsoft.Json; using Sln.Imm.Daemon.Cache; using Sln.Imm.Daemon.Model.dao; using Sln.Imm.Daemon.Model.dto; using Sln.Imm.Daemon.Opc; using Sln.Imm.Daemon.Opc.Impl; +using Sln.Imm.Daemon.Repository.service; using Sln.Imm.Daemon.Repository.service.@base; using Sln.Imm.Daemon.Serilog; +using SqlSugar; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Threading; namespace Sln.Imm.Daemon.Business; @@ -44,28 +52,95 @@ public class DeviceDataCollector : IDisposable private CancellationTokenSource _cts; // 取消令牌:用于终止所有采集任务 private readonly int _collectIntervalMs; // 循环采集间隔(毫秒) + + private readonly int _collectAlarmMs; // 循环采集三色灯间隔(毫秒) private bool _isCollecting; // 是否正在采集(防止重复启动) - private readonly BaseDeviceInfoCacheService _cacheService; + private readonly BaseDeviceInfoCacheService _cacheService; + + private readonly IBaseDeviceParamService _paramAlarmService; private readonly IBaseService _paramValService; - - public DeviceDataCollector(SerilogHelper serilogHelper, - BaseDeviceInfoCacheService cacheService, - IBaseService paramValService, int maxConcurrentDevices = 15) + + private readonly IBaseDeviceAlarmValService _alarmValService; + + private readonly IBaseRecordShutDownService _shutDownValService; + + // 在类级别添加这个字典来存储每个设备的上一次状态 + private readonly ConcurrentDictionary _deviceLastAlarmStates = + new ConcurrentDictionary(); + + // 新增:存储每个设备Alarm触发的状态 + private readonly ConcurrentDictionary _deviceAlarmTriggerStates = + new ConcurrentDictionary(); + + // 新增:存储每个设备停机触发的状态 + private readonly ConcurrentDictionary _deviceShutDownTriggerStates = + new ConcurrentDictionary(); + + private Dictionary _deviceAlarmStatus = new Dictionary(); + private static readonly TimeSpan DisconnectTimeout = TimeSpan.FromSeconds(5); + private const int AlarmDeviceIntervalMs = 5000; // 每台设备采集完成后,等待 5 秒再采集下一台 + private const int AlarmRoundIntervalMs = 10000; // 每轮采集完成后,固定等待 10 秒再开始下一轮 + + public DeviceDataCollector(SerilogHelper serilogHelper, + BaseDeviceInfoCacheService cacheService, + IBaseService paramValService, + IBaseDeviceParamService paramAlarmService, + IBaseDeviceAlarmValService alarmValService, + IBaseRecordShutDownService shutDownValService, + int maxConcurrentDevices = 15) { _serilog = serilogHelper; _cacheService = cacheService; _paramValService = paramValService; + _paramAlarmService = paramAlarmService; + _alarmValService = alarmValService; + _shutDownValService = shutDownValService; _semaphore = new SemaphoreSlim(maxConcurrentDevices, maxConcurrentDevices); _cts = new CancellationTokenSource(); - _collectIntervalMs = 1000 * 60 * 1; - _isCollecting = false; + _collectIntervalMs = 1000 * 60 *10 ; //设备数据采集时间间隔 + _isCollecting = false; } + #region 私有类 + /// + /// 设备报警状态结构 + /// + private class DeviceAlarmState + { + public bool LastRunning { get; set; } + public bool LastAlarm { get; set; } + public bool LastStopped { get; set; } + public DateTime LastUpdateTime { get; set; } + public bool IsFirstTime { get; set; } = true; // 标记是否为第一次采集 + } + + /// + /// Alarm触发状态结构 + /// + private class AlarmTriggerState + { + public bool LastAlarmValue { get; set; } + public long? AlarmRecordId { get; set; } // 存储插入的Alarm记录ID,用于后续更新 + public DateTime LastTriggerTime { get; set; } + public bool IsAlarmActive { get; set; } // 标记当前是否有活跃的Alarm记录 + } + + /// + /// 停机触发状态结构 + /// + private class ShutDownTriggerState + { + public bool LastStoppedValue { get; set; } + public DateTime LastTriggerTime { get; set; } + public bool IsShutDownActive { get; set; } // 标记当前是否有活跃的停机记录 + } + #endregion + #region 循环采集 /// @@ -73,7 +148,7 @@ public class DeviceDataCollector : IDisposable /// /// 15台设备列表 /// 循环次数(-1=无限) - public async Task StartParallelLoopCollectAsync(int loopCount = -1) + public async Task StartParallelLoopCollectAsync(int loopCount = -1) { if (_isCollecting) { @@ -136,11 +211,59 @@ public class DeviceDataCollector : IDisposable Console.WriteLine($"[{DateTime.Now}] 并行采集循环结束"); } } - - /// - /// 输出采集结果 - /// - private void OutputCollectResult(Dictionary collectResult) + + /// + /// 启动报警信息监控循环 + /// + public async Task StartAlarmMonitorLoopSimpleAsync() + { + try + { + Console.WriteLine($"[{DateTime.Now}] 报警监控开始(每轮完成后固定等待 {AlarmRoundIntervalMs / 1000} 秒)"); + + while (!_cts.Token.IsCancellationRequested) + { + // 记录本轮开始时间 + var startTime = DateTime.Now; + + try + { + // 执行报警采集 + await CollectDevicesAlarmAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[报警监控] 采集异常: {ex.Message}"); + _serilog.Error($"报警监控采集异常", ex); + } + + // 计算耗时 + var elapsedMs = (DateTime.Now - startTime).TotalMilliseconds; + + // 固定间隔:每轮采集结束后等待 30 秒再开始下一轮(不再做“精准补齐间隔”) + Console.WriteLine($"[报警监控] 本轮耗时 {elapsedMs:F0}ms,等待 {AlarmRoundIntervalMs}ms 后下一轮"); + await Task.Delay(AlarmRoundIntervalMs, _cts.Token); + } + } + catch (OperationCanceledException) + { + Console.WriteLine($"[{DateTime.Now}] 报警监控已停止"); + } + catch (Exception ex) + { + Console.WriteLine($"[{DateTime.Now}] 报警监控异常:{ex.Message}"); + _serilog.Error($"报警监控异常", ex); + } + finally + { + Console.WriteLine($"[{DateTime.Now}] 报警监控结束"); + } + } + + /// + /// 输出采集结果 + /// + private void OutputCollectResult(Dictionary collectResult) { Console.WriteLine($"\n[{DateTime.Now}] 本轮采集结果汇总:"); foreach (var kvp in collectResult) @@ -181,6 +304,178 @@ public class DeviceDataCollector : IDisposable return resultDict; } + + /// + /// 采集三色灯报警:顺序循环采集每台设备,每台采集完成后等待 10 秒再采集下一台;一轮完成后间隔时间不变。 + /// + public async Task> CollectDevicesAlarmAsync() + { + var deviceInfos = await _cacheService.GetValueAsync("BaseDeviceInfoCache"); + + if (deviceInfos == null || deviceInfos.Count == 0) + { + Console.WriteLine($"[报警监控] 无设备信息"); + throw new ArgumentException("设备列表不能为空", nameof(deviceInfos)); + } + + Console.WriteLine($"[报警监控] 开始采集 {deviceInfos.Count} 台设备(顺序采集,每台间隔 {AlarmDeviceIntervalMs / 1000} 秒)"); + var resultDict = new Dictionary(); + + for (var i = 0; i < deviceInfos.Count; i++) + { + if (_cts.Token.IsCancellationRequested) + break; + + var device = deviceInfos[i]; + await CollectOneDeviceAlarmAsync(device, resultDict, _cts.Token); + + // 每台设备采集完成后等待 10 秒再采集下一台(最后一台后不等待) + if (i < deviceInfos.Count - 1) + { + await Task.Delay(AlarmDeviceIntervalMs, _cts.Token); + } + } + + Console.WriteLine($"[报警监控] 本轮采集完成(共 {deviceInfos.Count} 台设备,结果数: {resultDict.Count})"); + return resultDict; + } + + /// + /// 顺序模式下单台设备报警采集(无信号量,由上层循环调用)。 + /// + private async Task CollectOneDeviceAlarmAsync( + BaseDeviceInfo device, + Dictionary resultDict, + CancellationToken cancellationToken) + { + IOpcService opcUa = null; + List opcItemValues = null; + + try + { + if (device.deviceFacture.Contains("伊之密")) + { + opcUa = new OpcUaService(); + } + else if (device.deviceFacture.Contains("老设备")) + { + opcUa = new OpcDaService(); + } + else + { + return; + } + + bool connectResult = await opcUa.ConnectAsync(device.networkAddress); + if (!connectResult) + { + Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} 连接失败"); + resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 连接失败"); + return; + } + + _serilog.Info($"报警信息开始采集{device.deviceName};"); + + opcItemValues = await ReadAlarm(device, opcUa); + + if (opcItemValues == null || opcItemValues.Count == 0) + { + Console.WriteLine($"报警信息 - [{DateTime.Now}] {device.deviceName} - 未读取到报警点位"); + resultDict[device.networkAddress] = opcItemValues ?? new List(); + return; + } + + if (HasAlarmStateChanged(device, opcItemValues)) + { + _serilog.Info($"{device.deviceName}三色灯状态改变"); + try + { + SaveParam(device, opcItemValues, out List paramValues); + } + catch (Exception ex) + { + _serilog.Error($"设备 {device.deviceName} 保存参数失败", ex); + } + } + //报警处理 + try + { + int should = ShouldRecordAlarmTrue(device, opcItemValues); + if (should == 1) + { + _serilog.Info($"{device.deviceName}报警触发"); + BaseDeviceAlarmVal paramVal = new BaseDeviceAlarmVal + { + DEVICE_ID = device.objid, + ALARM_BEGIN_TIME = DateTime.Now, + CONTINUE_TIME = 0 + }; + _alarmValService.Insert(paramVal); + } + else if (should == 2) + { + _serilog.Info($"{device.deviceName}报警恢复"); + _alarmValService.UpdateAlarmVal(device.objid); + } + } + catch (Exception ex) + { + _serilog.Error($"设备 {device.deviceName} 报警记录操作失败", ex); + } + + // 处理停机记录 + try + { + int shutDownShould = ShouldRecordShutDownTrue(device, opcItemValues); + if (shutDownShould == 1) + { + _serilog.Info($"{device.deviceName}停机触发"); + BaseRecordShutDown shutDownVal = new BaseRecordShutDown + { + MACHINE_ID = device.objid, + SHUT_TYPE_ID = 1, + SHUT_REASON_ID = 1, + SHUT_REASON = "R001", + SHUT_BEGIN_TIME = DateTime.Now, + SHUT_TIME = 0, + DOWNTIME_FLAG = "0", + ACTIVE_FLAG = "1" + }; + _shutDownValService.Insert(shutDownVal); + } + else if (shutDownShould == 2) + { + _serilog.Info($"{device.deviceName}停机恢复"); + _shutDownValService.UpdateShutDownVal(device.objid); + } + } + catch (Exception ex) + { + _serilog.Error($"设备 {device.deviceName} 停机记录操作失败", ex); + } + + resultDict[device.networkAddress] = opcItemValues; + } + catch (Exception ex) + { + _serilog.Error($"设备 {device.deviceName} 三色灯采集异常", ex); + resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 三色灯采集异常:{ex.Message}", ex); + } + finally + { + if (opcUa != null) + { + try + { + await SafeDisconnectAsync(opcUa, device.deviceName).ConfigureAwait(false); + } + catch (Exception ex) + { + _serilog.Error($"设备 {device.deviceName} 断开连接失败", ex); + } + } + } + } private async Task CollectSingleDeviceParallelAsync(BaseDeviceInfo device, Dictionary resultDict, CancellationToken cancellationToken) @@ -195,10 +490,14 @@ public class DeviceDataCollector : IDisposable { opcUa =new OpcUaService(); } - else + else if(device.deviceFacture.Contains("老设备")) { opcUa =new OpcDaService(); } + else + { + return; + } Console.WriteLine($"[{DateTime.Now}] {device.deviceName} - 开始连接并采集"); bool connectResult = await opcUa.ConnectAsync(device.networkAddress); @@ -235,8 +534,7 @@ public class DeviceDataCollector : IDisposable // 无论成功/失败,都断开设备连接(释放资源) try { - await opcUa.DisconnectAsync(); - Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 已断开连接"); + await SafeDisconnectAsync(opcUa, device.deviceName).ConfigureAwait(false); } catch (Exception ex) { @@ -248,8 +546,27 @@ public class DeviceDataCollector : IDisposable } } + private static async Task SafeDisconnectAsync(IOpcService opcUa, string deviceName) + { + if (opcUa == null) return; + + Console.WriteLine($"[{DateTime.Now}] 设备 {deviceName} 开始断开连接..."); + + var disconnectTask = opcUa.DisconnectAsync(); + var finished = await Task.WhenAny(disconnectTask, Task.Delay(DisconnectTimeout)).ConfigureAwait(false); + + if (finished != disconnectTask) + { + Console.WriteLine($"[{DateTime.Now}] 设备 {deviceName} 断开连接超时(>{DisconnectTimeout.TotalSeconds:F0}s),跳过等待以避免阻塞"); + return; + } + + await disconnectTask.ConfigureAwait(false); + Console.WriteLine($"[{DateTime.Now}] 设备 {deviceName} 已断开连接"); + } + #endregion - + /// /// 读取设备参数 /// @@ -265,8 +582,14 @@ public class DeviceDataCollector : IDisposable List deviceParams = device.deviceParams.Select(x => x.paramAddr).ToList(); + Stopwatch sw = Stopwatch.StartNew(); + List infos = await opcUa.ReadNodeAsync(deviceParams); + sw.Stop(); + + _serilog.Info($"opc读取耗时{sw.ElapsedMilliseconds.ToString()}"); + return infos; } @@ -276,6 +599,47 @@ public class DeviceDataCollector : IDisposable } } + /// + /// 读取设备报警 + /// + /// + /// + /// + public async Task> ReadAlarm(BaseDeviceInfo device, IOpcService opcUa) + { + try + { + if (device == null) + { + throw new ArgumentNullException($"设备信息不允许为空"); + } + if (opcUa == null) + { + throw new ArgumentNullException($"OPC服务不允许为空"); + } + List deviceParams = new List(); + List paramList = _paramAlarmService.GetDeviceAlarmParams(device); + if (paramList == null || paramList.Count == 0) + return null; + + foreach (BaseDeviceParam param in paramList) + { + if (string.IsNullOrEmpty(param?.paramAddr)) + continue; + deviceParams.Add(param.paramAddr); + } + if (deviceParams.Count == 0) + return null; + + List infos = await opcUa.ReadNodeAsync(deviceParams); + return infos; + } + catch (Exception e) + { + throw new InvalidOperationException($"设备参数读取异常:{e.Message}", e); + } + } + /// /// 保存设备参数值到数据库 /// @@ -300,7 +664,15 @@ public class DeviceDataCollector : IDisposable } deviceParamVal.deviceCode = device.deviceCode; deviceParamVal.deviceId = device.objid; - deviceParamVal.paramValue = opcItem.Value.ToString(); + if(opcItem.Value != null) + { + deviceParamVal.paramValue = opcItem.Value.ToString(); + } + else + { + deviceParamVal.paramValue = "无"; + } + deviceParamVal.paramType = opcItem.DataType; deviceParamVal.collectTime = DateTime.Now; deviceParamVal.recordTime = DateTime.Now; @@ -319,11 +691,376 @@ public class DeviceDataCollector : IDisposable } }catch (Exception e) { + _serilog.Error($"DevicedataCollection报错", e); throw new InvalidOperationException($"设备参数保存异常:{e.Message}"); } } - + + /// + /// 检查报警状态是否有变化 + /// + /// + /// + /// + private bool HasAlarmStateChanged(BaseDeviceInfo device, List currentOpcItemValues) + { + try + { + // 1. 从当前数据中提取三个关键报警点位 + OpcNode runningNode = null; + OpcNode alarmNode = null; + OpcNode stoppedNode = null; + + foreach (var node in currentOpcItemValues) + { + if (node.NodeId.Contains("Running", StringComparison.OrdinalIgnoreCase)) + runningNode = node; + else if (node.NodeId.Contains("Alarm", StringComparison.OrdinalIgnoreCase)) + alarmNode = node; + else if (node.NodeId.Contains("Stopped", StringComparison.OrdinalIgnoreCase)) + stoppedNode = node; + } + + // 如果三个点位没有全部找到,认为数据不完整,不进行处理 + if (runningNode == null || alarmNode == null || stoppedNode == null) + { + Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 未找到完整的报警点位(Running/Alarm/Stopped)"); + return false; + } + + // 2. 解析当前状态值 + bool currentRunning = GetBoolValue(runningNode.Value); + bool currentAlarm = GetBoolValue(alarmNode.Value); + bool currentStopped = GetBoolValue(stoppedNode.Value); + + // 3. 获取或创建该设备的上一次状态记录 + string deviceKey = device.objid.ToString(); // 使用设备ID作为字典键 + bool hasChanged = false; + + // 使用线程安全的方式获取或创建设备状态 + _deviceLastAlarmStates.AddOrUpdate(deviceKey, + // 如果设备第一次采集,创建新状态记录 + (key) => + { + // 第一次采集,创建初始状态记录 + var newState = new DeviceAlarmState + { + LastRunning = currentRunning, + LastAlarm = currentAlarm, + LastStopped = currentStopped, + LastUpdateTime = DateTime.Now, + IsFirstTime = false // 标记为已初始化 + }; + + Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 首次采集,创建初始状态记录"); + hasChanged = true; // 第一次采集总是保存 + return newState; + }, + // 如果设备已有状态记录,进行比较和更新 + (key, existingState) => + { + // 检查状态是否有变化 + bool runningChanged = existingState.LastRunning != currentRunning; + bool alarmChanged = existingState.LastAlarm != currentAlarm; + bool stoppedChanged = existingState.LastStopped != currentStopped; + + // 只要有一个状态发生变化,就认为有变化 + hasChanged = runningChanged || alarmChanged || stoppedChanged; + + if (hasChanged) + { + // 记录变化详情 + Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 状态变化: " + + $"R[{existingState.LastRunning}->{currentRunning}], " + + $"A[{existingState.LastAlarm}->{currentAlarm}], " + + $"S[{existingState.LastStopped}->{currentStopped}]"); + + // 更新为最新状态 + existingState.LastRunning = currentRunning; + existingState.LastAlarm = currentAlarm; + existingState.LastStopped = currentStopped; + existingState.LastUpdateTime = DateTime.Now; + } + else + { + Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 状态无变化: " + + $"R[{currentRunning}], A[{currentAlarm}], S[{currentStopped}]"); + } + + return existingState; + }); + + return hasChanged; + } + catch (Exception ex) + { + Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 检查状态变化异常: {ex.Message}"); + _serilog.Error($"设备 {device.deviceName} 检查状态变化异常", ex); + return false; // 出现异常时,不保存 + } + } + + /// + /// 检查是否需要记录Alarm为true的情况(新增逻辑) + /// + /// 设备信息 + /// 当前采集的OPC点位值 + /// true表示需要记录Alarm为true,false表示不需要记录 + private int ShouldRecordAlarmTrue(BaseDeviceInfo device, List currentOpcItemValues) + { + try + { + // 1. 从当前数据中提取Alarm点位 + OpcNode alarmNode = null; + + foreach (var node in currentOpcItemValues) + { + if (node.NodeId.Contains("Alarm", StringComparison.OrdinalIgnoreCase)) + { + alarmNode = node; + break; + } + } + + // 如果Alarm点位没有找到,不进行处理 + if (alarmNode == null) + { + Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 未找到Alarm点位"); + return 0; + } + + // 2. 解析当前Alarm状态值 + bool currentAlarm = GetBoolValue(alarmNode.Value); + + // 3. 获取设备当前的Alarm触发状态 + string deviceKey = device.objid.ToString(); + + // 使用线程安全的方式获取或创建设备Alarm触发状态 + int operationType = 0; // 0-无操作,1-插入,2-更新 + + _deviceAlarmTriggerStates.AddOrUpdate(deviceKey, + // 如果设备第一次检测到Alarm,创建新状态记录 + (key) => + { + // 第一次检测到Alarm,创建初始状态记录 + var newState = new AlarmTriggerState + { + LastAlarmValue = currentAlarm, + AlarmRecordId = null, + LastTriggerTime = DateTime.Now, + IsAlarmActive = false + }; + + // 如果是第一次采集且Alarm为true,需要插入记录 + if (currentAlarm) + { + Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到Alarm为true,需要插入记录"); + operationType = 1; // 需要插入记录 + newState.IsAlarmActive = true; // 标记有活跃的Alarm记录 + } + else + { + Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到Alarm为false,无需操作"); + } + + return newState; + }, + // 如果设备已有Alarm触发状态记录,进行比较 + (key, existingState) => + { + // 检查状态变化 + if (existingState.LastAlarmValue == false && currentAlarm == true) + { + // 1: Alarm从false变为true,需要插入记录 + operationType = 1; // 需要插入记录 + existingState.IsAlarmActive = true; // 标记有活跃的Alarm记录 + existingState.AlarmRecordId = null; // 重置记录ID,将在插入后设置 + } + else if (existingState.LastAlarmValue == true && currentAlarm == false) + { + // 2: Alarm从true变为false,需要更新记录 + if (existingState.IsAlarmActive) + { + operationType = 2; // 需要更新记录 + existingState.IsAlarmActive = false; // 标记Alarm记录已结束 + } + } + else if (existingState.LastAlarmValue == true && currentAlarm == true) + { + // 3.Alarm持续为true,忽略 + operationType = 0; + } + else // existingState.LastAlarmValue == false && currentAlarm == false + { + // 4.Alarm持续为false,忽略 + operationType = 0; + } + + // 更新状态 + existingState.LastAlarmValue = currentAlarm; + existingState.LastTriggerTime = DateTime.Now; + + return existingState; + }); + + return operationType; + } + catch (Exception ex) + { + Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 检查Alarm记录异常: {ex.Message}"); + _serilog.Error($"设备 {device.deviceName} 检查Alarm记录异常", ex); + return 0; // 出现异常时,不操作 + } + } + + /// + /// 检查是否需要记录停机为true的情况 + /// + /// 设备信息 + /// 当前采集的OPC点位值 + /// 0-无操作,1-插入,2-更新 + private int ShouldRecordShutDownTrue(BaseDeviceInfo device, List currentOpcItemValues) + { + try + { + // 1. 从当前数据中提取Stopped点位 + OpcNode stoppedNode = null; + + foreach (var node in currentOpcItemValues) + { + if (node.NodeId.Contains("Stopped", StringComparison.OrdinalIgnoreCase)) + { + stoppedNode = node; + break; + } + } + + // 如果Stopped点位没有找到,不进行处理 + if (stoppedNode == null) + { + Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 未找到Stopped点位"); + return 0; + } + + // 2. 解析当前停机状态值 + bool currentStopped = GetBoolValue(stoppedNode.Value); + + // 3. 获取设备当前的停机触发状态 + string deviceKey = device.objid.ToString(); + + // 使用线程安全的方式获取或创建设备停机触发状态 + int operationType = 0; // 0-无操作,1-插入,2-更新 + + _deviceShutDownTriggerStates.AddOrUpdate(deviceKey, + // 如果设备第一次检测到停机,创建新状态记录 + (key) => + { + // 第一次检测到停机,创建初始状态记录 + var newState = new ShutDownTriggerState + { + LastStoppedValue = currentStopped, + LastTriggerTime = DateTime.Now, + IsShutDownActive = false + }; + + // 如果是第一次采集且停机为true,需要插入记录 + if (currentStopped) + { + Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到停机为true,需要插入记录"); + operationType = 1; // 需要插入记录 + newState.IsShutDownActive = true; // 标记有活跃的停机记录 + } + else + { + Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到停机为false,无需操作"); + } + + return newState; + }, + // 如果设备已有停机触发状态记录,进行比较 + (key, existingState) => + { + // 检查状态变化 + if (existingState.LastStoppedValue == false && currentStopped == true) + { + // 1: 停机从false变为true,需要插入记录 + operationType = 1; // 需要插入记录 + existingState.IsShutDownActive = true; // 标记有活跃的停机记录 + } + else if (existingState.LastStoppedValue == true && currentStopped == false) + { + // 2: 停机从true变为false,需要更新记录 + if (existingState.IsShutDownActive) + { + operationType = 2; // 需要更新记录 + existingState.IsShutDownActive = false; // 标记停机记录已结束 + } + } + else if (existingState.LastStoppedValue == true && currentStopped == true) + { + // 3. 停机持续为true,忽略 + operationType = 0; + } + else // existingState.LastStoppedValue == false && currentStopped == false + { + // 4. 停机持续为false,忽略 + operationType = 0; + } + + // 更新状态 + existingState.LastStoppedValue = currentStopped; + existingState.LastTriggerTime = DateTime.Now; + + return existingState; + }); + + return operationType; + } + catch (Exception ex) + { + Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 检查停机记录异常: {ex.Message}"); + _serilog.Error($"设备 {device.deviceName} 检查停机记录异常", ex); + return 0; // 出现异常时,不操作 + } + } + + /// + /// 安全获取bool值 + /// + /// + /// + private bool GetBoolValue(object value) + { + if (value == null) return false; + + try + { + if (value is bool boolValue) + return boolValue; + + if (value is string stringValue) + { + if (bool.TryParse(stringValue, out bool result)) + return result; + + // 尝试其他常见表示方式 + if (stringValue == "1" || stringValue.Equals("true", StringComparison.OrdinalIgnoreCase)) + return true; + if (stringValue == "0" || stringValue.Equals("false", StringComparison.OrdinalIgnoreCase)) + return false; + } + + // 尝试转换 + return Convert.ToBoolean(value); + } + catch + { + return false; + } + } + + /// /// 终止所有采集任务 /// @@ -335,7 +1072,30 @@ public class DeviceDataCollector : IDisposable Console.WriteLine($"[{DateTime.Now}] 已触发采集任务终止"); } } - + + /// + /// 计算两个时间之间的持续时间(分钟),四舍五入取整 + /// + /// 开始时间 + /// 结束时间 + /// 持续时间的总分钟数(int类型,四舍五入) + public int CalculateDurationMinutesRounded(DateTime startTime, DateTime endTime) + { + // 验证参数有效性 + if (endTime < startTime) + { + // 这里可以根据需求处理,比如返回0或抛出异常 + return 0; + // 或者抛出异常:throw new ArgumentException("结束时间不能早于开始时间"); + } + + // 计算时间差 + TimeSpan duration = endTime - startTime; + + // 获取总分钟数(double类型),然后四舍五入转换为int + return (int)Math.Round(duration.TotalMinutes); + } + public void Dispose() { throw new NotImplementedException(); diff --git a/Sln.Imm.Daemon.Cache/BaseDeviceInfoCacheService.cs b/Sln.Imm.Daemon.Cache/BaseDeviceInfoCacheService.cs index c23f621..50e7fdb 100644 --- a/Sln.Imm.Daemon.Cache/BaseDeviceInfoCacheService.cs +++ b/Sln.Imm.Daemon.Cache/BaseDeviceInfoCacheService.cs @@ -51,21 +51,12 @@ public class BaseDeviceInfoCacheService /// /// public async Task> GetValueAsync(string key) - { - var cachedValue = await _fusionCache.GetOrDefaultAsync>(key).ConfigureAwait(false); - if (cachedValue != null) - { - _logger.Info($"通过Cache获取设备数据:{cachedValue.Count};条"); - return cachedValue; - } - else - { - var value = _service.GetDeviceInfosByNavigate(); - //将值存入缓存,设置过期时间等 - await _fusionCache.SetAsync(key, value, TimeSpan.FromSeconds(30)).ConfigureAwait(false); - _logger.Info($"通过ORM获取设备数据:{value.Count};条"); - return value; - } + { + var value = _service.GetDeviceInfosByNavigate(); + //将值存入缓存,设置过期时间等 + //await _fusionCache.SetAsync(key, value, TimeSpan.FromSeconds(30)).ConfigureAwait(false); + _logger.Info($"通过ORM获取设备数据:{value.Count};条"); + return value; } public async Task SetValueAsync(string key, BaseDeviceInfo deviceInfo) diff --git a/Sln.Imm.Daemon.Model/dao/BaseDeviceAlarmVal.cs b/Sln.Imm.Daemon.Model/dao/BaseDeviceAlarmVal.cs new file mode 100644 index 0000000..e68b79d --- /dev/null +++ b/Sln.Imm.Daemon.Model/dao/BaseDeviceAlarmVal.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SqlSugar; +namespace Models +{ + /// + /// 设备报警记录 + /// + [SugarTable("DMS_RECORD_ALARM_INFO"), TenantAttribute("mes")] + public class BaseDeviceAlarmVal + { + + /// + /// 备 注:主键标识 + /// 默认值: + /// + [SugarColumn(ColumnName="ALARM_ID" , OracleSequenceName = "PARAMRECORD_SEQ_ID", IsPrimaryKey = true) ] + public decimal ALARM_ID { get; set; } + + /// + /// 备 注:设备台账id,关联dms_base_device_ledger的device_id + /// 默认值: + /// + [SugarColumn(ColumnName="DEVICE_ID" ) ] + public decimal DEVICE_ID { get; set; } + + /// + /// 备 注:报警开始时间 + /// 默认值: + /// + [SugarColumn(ColumnName="ALARM_BEGIN_TIME" ) ] + public DateTime? ALARM_BEGIN_TIME { get; set; } + + /// + /// 备 注:报警结束时间 + /// 默认值: + /// + [SugarColumn(ColumnName="ALARM_END_TIME" ) ] + public DateTime? ALARM_END_TIME { get; set; } + + /// + /// 备 注:报警持续时间(ms) + /// 默认值: + /// + [SugarColumn(ColumnName="CONTINUE_TIME" ) ] + public decimal? CONTINUE_TIME { get; set; } + + + } + +} \ No newline at end of file diff --git a/Sln.Imm.Daemon.Model/dao/BaseRecordShutDown.cs b/Sln.Imm.Daemon.Model/dao/BaseRecordShutDown.cs new file mode 100644 index 0000000..c60c604 --- /dev/null +++ b/Sln.Imm.Daemon.Model/dao/BaseRecordShutDown.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SqlSugar; +namespace Models +{ + /// + /// 停机记录 + /// + [SugarTable("DMS_RECORD_SHUT_DOWN")] + public class BaseRecordShutDown + { + + /// + /// 备 注:主键标识;scada上报的记录 + /// 默认值: + /// + [SugarColumn(ColumnName="RECORD_SHUT_DOWN_ID" , OracleSequenceName = "PARAMRECORD_SEQ_ID", IsPrimaryKey = true) ] + public decimal RECORD_SHUT_DOWN_ID { get; set; } + + /// + /// 备 注:设备ID,关联prod_base_machine_info的machine_id + /// 默认值: + /// + [SugarColumn(ColumnName="MACHINE_ID" ) ] + public decimal MACHINE_ID { get; set; } + + /// + /// 备 注:停机类型ID,关联dm_base_shut_type的shut_type_id + /// 默认值: + /// + [SugarColumn(ColumnName="SHUT_TYPE_ID" ) ] + public decimal? SHUT_TYPE_ID { get; set; } + + /// + /// 备 注:停机原因ID,关联dms_base_shut_reason的shut_reason_id + /// 默认值: + /// + [SugarColumn(ColumnName="SHUT_REASON_ID" ) ] + public decimal? SHUT_REASON_ID { get; set; } + + /// + /// 备 注:停机开始时间 + /// 默认值: + /// + [SugarColumn(ColumnName="SHUT_BEGIN_TIME" ) ] + public DateTime? SHUT_BEGIN_TIME { get; set; } + + /// + /// 备 注:停机结束时间 + /// 默认值: + /// + [SugarColumn(ColumnName="SHUT_END_TIME" ) ] + public DateTime? SHUT_END_TIME { get; set; } + + /// + /// 备 注:停机时长(秒) + /// 默认值: + /// + [SugarColumn(ColumnName="SHUT_TIME" ) ] + public decimal? SHUT_TIME { get; set; } + + /// + /// 备 注:停机标识(0未结束 1已结束) + /// 默认值: + /// + [SugarColumn(ColumnName="DOWNTIME_FLAG" ) ] + public string DOWNTIME_FLAG { get; set; } = null!; + + /// + /// 备 注:停机原因 + /// 默认值: + /// + [SugarColumn(ColumnName="SHUT_REASON" ) ] + public string? SHUT_REASON { get; set; } + + /// + /// 备 注:激活标识(1是 0否) + /// 默认值: + /// + [SugarColumn(ColumnName="ACTIVE_FLAG" ) ] + public string ACTIVE_FLAG { get; set; } = null!; + + + } + +} \ No newline at end of file diff --git a/Sln.Imm.Daemon.Model/dto/AlarmStateRecord.cs b/Sln.Imm.Daemon.Model/dto/AlarmStateRecord.cs new file mode 100644 index 0000000..7c4a8e6 --- /dev/null +++ b/Sln.Imm.Daemon.Model/dto/AlarmStateRecord.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sln.Imm.Daemon.Model.dto +{ + public class AlarmStateRecord + { + public bool Running { get; set; } + public bool Alarm { get; set; } + public bool Stopped { get; set; } + public DateTime LastUpdateTime { get; set; } + } +} diff --git a/Sln.Imm.Daemon.Model/dto/OpcNode.cs b/Sln.Imm.Daemon.Model/dto/OpcNode.cs index 75dd21b..5b6c2a2 100644 --- a/Sln.Imm.Daemon.Model/dto/OpcNode.cs +++ b/Sln.Imm.Daemon.Model/dto/OpcNode.cs @@ -29,7 +29,7 @@ public class OpcNode { public string NodeId { get; set; } public string DisplayName { get; set; } - public object Value { get; set; } + public object Value { get; set; } = new object(); public DateTimeOffset SourceTimestamp { get; set; } public string DataType { get; set; } public string AccessLevel { get; set; } diff --git a/Sln.Imm.Daemon.Opc/Impl/OpcDaService.cs b/Sln.Imm.Daemon.Opc/Impl/OpcDaService.cs index 4496da4..3d46bdc 100644 --- a/Sln.Imm.Daemon.Opc/Impl/OpcDaService.cs +++ b/Sln.Imm.Daemon.Opc/Impl/OpcDaService.cs @@ -35,6 +35,7 @@ public class OpcDaService : IOpcService, IDisposable { private OpcDaServer _server; private bool _disposed = false; + private static readonly TimeSpan DefaultDisconnectTimeout = TimeSpan.FromSeconds(5); public OpcDaService() { @@ -64,11 +65,41 @@ public class OpcDaService : IOpcService, IDisposable public async Task DisconnectAsync() { - if (_server != null && _server.IsConnected) + var server = _server; + _server = null; + + if (server == null) + return; + + try { - _server.Disconnect(); - _server.Dispose(); - _server = null; + var disconnectTask = Task.Run(() => + { + try + { + if (server.IsConnected) + { + server.Disconnect(); + } + } + finally + { + server.Dispose(); + } + }); + + var finished = await Task.WhenAny(disconnectTask, Task.Delay(DefaultDisconnectTimeout)).ConfigureAwait(false); + if (finished != disconnectTask) + { + // 超时:避免上层采集被永久阻塞 + return; + } + + await disconnectTask.ConfigureAwait(false); + } + catch + { + // 断开失败不应影响上层循环 } } @@ -218,7 +249,14 @@ public class OpcDaService : IOpcService, IDisposable { if (!_disposed) { - DisconnectAsync().Wait(); + try + { + DisconnectAsync().GetAwaiter().GetResult(); + } + catch + { + // swallow + } _disposed = true; } } diff --git a/Sln.Imm.Daemon.Opc/Impl/OpcUaService.cs b/Sln.Imm.Daemon.Opc/Impl/OpcUaService.cs index a7dcb20..23ae5fc 100644 --- a/Sln.Imm.Daemon.Opc/Impl/OpcUaService.cs +++ b/Sln.Imm.Daemon.Opc/Impl/OpcUaService.cs @@ -26,6 +26,7 @@ using Opc.Ua; using Opc.Ua.Client; using Sln.Imm.Daemon.Model.dto; +using System.Threading; namespace Sln.Imm.Daemon.Opc.Impl; @@ -34,6 +35,11 @@ public class OpcUaService : IOpcService, IDisposable private ApplicationConfiguration _config; private Session _session; private bool _disposed = false; + private static readonly TimeSpan DefaultDisconnectTimeout = TimeSpan.FromSeconds(5); + // 全局限制:同时对 OPC UA 服务器建立的 Session 数量(避免 BadMaxConnectionsReached) + // 如果你的 OPC UA 服务器限制更低/更高,可以调整这个数(建议小于服务器 MaxConnections/MaxSessions) + private static readonly SemaphoreSlim UaConnectionSemaphore = new SemaphoreSlim(8, 8); + private bool _holdsUaConnectionSlot; public OpcUaService() { @@ -75,40 +81,119 @@ public class OpcUaService : IOpcService, IDisposable public async Task ConnectAsync(string serverUrl) { + // 先拿到“连接名额”,避免瞬时并发把服务器连接打满 + await UaConnectionSemaphore.WaitAsync().ConfigureAwait(false); + _holdsUaConnectionSlot = true; + try { - var endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, false); - var endpointConfiguration = EndpointConfiguration.Create(_config); - var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); + const int maxAttempts = 2; + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + var endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, false); + var endpointConfiguration = EndpointConfiguration.Create(_config); + var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); - _session = await Session.Create( - _config, - endpoint, - false, - false, - _config.ApplicationName, - 60000, - new UserIdentity(), - null); + _session = await Session.Create( + _config, + endpoint, + false, + false, + _config.ApplicationName, + 60000, + new UserIdentity(), + null).ConfigureAwait(false); - return _session != null && _session.Connected; + return _session != null && _session.Connected; + } + catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes.BadMaxConnectionsReached) + { + // 服务器连接数达到上限:做一次短暂退避重试 + if (attempt >= maxAttempts) + throw; + + await Task.Delay(500 * attempt).ConfigureAwait(false); + } + catch (Exception ex) when (ex.Message != null && ex.Message.Contains("BadMaxConnectionsReached", StringComparison.OrdinalIgnoreCase)) + { + if (attempt >= maxAttempts) + throw; + + await Task.Delay(500 * attempt).ConfigureAwait(false); + } + } + + return false; } catch (Exception ex) { + // 连接失败:释放名额 + ReleaseUaConnectionSlotIfHeld(); throw new InvalidOperationException($"连接到 OPC UA 服务器失败: {ex.Message}"); } } public async Task DisconnectAsync() { - if (_session != null && _session.Connected) + var session = _session; + _session = null; + + if (session == null) { - _session.Close(); - _session.Dispose(); - _session = null; + ReleaseUaConnectionSlotIfHeld(); + return; } - await Task.CompletedTask; + try + { + // 断开时可能会卡在 Close()/Dispose()(网络/通道问题),放到线程池并加超时保护 + var closeTask = Task.Run(() => + { + try + { + if (session.Connected) + { + // 使用带超时的 Close(毫秒) + session.Close((int)DefaultDisconnectTimeout.TotalMilliseconds); + } + } + finally + { + session.Dispose(); + } + }); + + var finished = await Task.WhenAny(closeTask, Task.Delay(DefaultDisconnectTimeout)).ConfigureAwait(false); + if (finished != closeTask) + { + // 超时:不阻塞调用方,避免采集线程被永久卡死 + // closeTask 仍可能在后台继续清理(或被底层卡住) + return; + } + + // 传播 closeTask 的异常(如果有) + await closeTask.ConfigureAwait(false); + } + catch + { + // 断开失败不应影响上层循环 + //(上层会记录日志/继续) + } + finally + { + ReleaseUaConnectionSlotIfHeld(); + } + } + + private void ReleaseUaConnectionSlotIfHeld() + { + if (_holdsUaConnectionSlot) + { + _holdsUaConnectionSlot = false; + try { UaConnectionSemaphore.Release(); } catch { /* ignore */ } + } } public async Task> ReadNodeAsync(List nodeId) @@ -256,7 +341,15 @@ public class OpcUaService : IOpcService, IDisposable { if (!_disposed) { - DisconnectAsync().Wait(); + try + { + // Dispose 里不要 .Wait() 无限阻塞;最多等待一次默认超时 + DisconnectAsync().GetAwaiter().GetResult(); + } + catch + { + // swallow + } _disposed = true; } } diff --git a/Sln.Imm.Daemon.Repository/service/IBaseDeviceAlarmValService.cs b/Sln.Imm.Daemon.Repository/service/IBaseDeviceAlarmValService.cs new file mode 100644 index 0000000..2842fc1 --- /dev/null +++ b/Sln.Imm.Daemon.Repository/service/IBaseDeviceAlarmValService.cs @@ -0,0 +1,16 @@ +using Models; +using Sln.Imm.Daemon.Model.dao; +using Sln.Imm.Daemon.Repository.service.@base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sln.Imm.Daemon.Repository.service +{ + public interface IBaseDeviceAlarmValService : IBaseService + { + bool UpdateAlarmVal(int id); + } +} diff --git a/Sln.Imm.Daemon.Repository/service/IBaseDeviceParamService.cs b/Sln.Imm.Daemon.Repository/service/IBaseDeviceParamService.cs index 70444ab..a751288 100644 --- a/Sln.Imm.Daemon.Repository/service/IBaseDeviceParamService.cs +++ b/Sln.Imm.Daemon.Repository/service/IBaseDeviceParamService.cs @@ -30,4 +30,9 @@ namespace Sln.Imm.Daemon.Repository.service; public interface IBaseDeviceParamService : IBaseService { + /// + /// 查出所有报警参数 + /// + /// + List GetDeviceAlarmParams(BaseDeviceInfo deviceInfo); } \ No newline at end of file diff --git a/Sln.Imm.Daemon.Repository/service/IBaseRecordShutDownService.cs b/Sln.Imm.Daemon.Repository/service/IBaseRecordShutDownService.cs new file mode 100644 index 0000000..cb6d569 --- /dev/null +++ b/Sln.Imm.Daemon.Repository/service/IBaseRecordShutDownService.cs @@ -0,0 +1,15 @@ +using Models; +using Sln.Imm.Daemon.Repository.service.@base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sln.Imm.Daemon.Repository.service +{ + public interface IBaseRecordShutDownService : IBaseService + { + bool UpdateShutDownVal(int id); + } +} diff --git a/Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceAlarmValServiceImpl.cs b/Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceAlarmValServiceImpl.cs new file mode 100644 index 0000000..e33311f --- /dev/null +++ b/Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceAlarmValServiceImpl.cs @@ -0,0 +1,53 @@ +using Models; +using Sln.Imm.Daemon.Model.dao; +using Sln.Imm.Daemon.Repository.service.@base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using System.Text; +using System.Threading.Tasks; + +namespace Sln.Imm.Daemon.Repository.service.Impl +{ + public class BaseDeviceAlarmValServiceImpl : BaseServiceImpl, IBaseDeviceAlarmValService + { + public BaseDeviceAlarmValServiceImpl(Repository rep) : base(rep) + { + } + + public bool UpdateAlarmVal(int id) + { + try + { + // 1. 根据设备ID和开始时间查找报警记录 + var alarmRecord = _rep.AsQueryable() + .Where(x => x.DEVICE_ID == id && + x.CONTINUE_TIME == 0) + .First(); + if (alarmRecord == null) + { + return false; + } + + DateTime endTime = DateTime.Now; + alarmRecord.ALARM_END_TIME = endTime; + if (alarmRecord.ALARM_BEGIN_TIME != null) + { + //2.计算时长 + DateTime startTime = (DateTime)alarmRecord.ALARM_BEGIN_TIME; + TimeSpan continueTime = endTime - startTime; + int totalMinutes = (int)Math.Round(continueTime.TotalMinutes); + alarmRecord.CONTINUE_TIME = totalMinutes; + } + + bool result = _rep.Update(alarmRecord); + return result; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceParamServiceImpl.cs b/Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceParamServiceImpl.cs index 9f48637..6207969 100644 --- a/Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceParamServiceImpl.cs +++ b/Sln.Imm.Daemon.Repository/service/Impl/BaseDeviceParamServiceImpl.cs @@ -25,12 +25,46 @@ using Sln.Imm.Daemon.Model.dao; using Sln.Imm.Daemon.Repository.service.@base; +using SqlSugar; namespace Sln.Imm.Daemon.Repository.service.Impl; -public class BaseDeviceParamServiceImpl : BaseServiceImpl, IBaseDeviceParamService -{ +public class BaseDeviceParamServiceImpl : BaseServiceImpl,IBaseDeviceParamService { + public BaseDeviceParamServiceImpl(Repository rep) : base(rep) { } + + /// + /// 查出所有报警参数(三色灯相关) + /// + /// 设备信息,为 null 或 deviceCode 为空时返回空列表 + /// 报警参数列表,无匹配或异常时返回空列表,保证调用方不因空指针中断 + public List GetDeviceAlarmParams(BaseDeviceInfo device) + { + try + { + if (device == null) + return new List(); + if (string.IsNullOrEmpty(device.deviceCode)) + return new List(); + + var ctx = _rep?.Context; + if (ctx == null) + return new List(); + + var list = ctx.Queryable() + .Where(x => x.deviceCode == device.deviceCode) + .Where(x => x.paramName != null && x.paramName.Contains("三色灯")) + .ToList(); + + return list ?? new List(); + } + catch (Exception) + { + // 任何异常(含 NRE、SqlSugar/DB 异常)均返回空列表,避免单台设备导致整轮采集停止;调用方会走“未读取到报警点位”并继续下一轮 + return new List(); + } + } + } \ No newline at end of file diff --git a/Sln.Imm.Daemon.Repository/service/Impl/BaseRecordShutDownServiceImpl.cs b/Sln.Imm.Daemon.Repository/service/Impl/BaseRecordShutDownServiceImpl.cs new file mode 100644 index 0000000..5a4995e --- /dev/null +++ b/Sln.Imm.Daemon.Repository/service/Impl/BaseRecordShutDownServiceImpl.cs @@ -0,0 +1,53 @@ +using Models; +using Sln.Imm.Daemon.Model.dao; +using Sln.Imm.Daemon.Repository.service.@base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sln.Imm.Daemon.Repository.service.Impl +{ + public class BaseRecordShutDownServiceImpl : BaseServiceImpl,IBaseRecordShutDownService + { + public BaseRecordShutDownServiceImpl(Repository rep) : base(rep) + { + } + + public bool UpdateShutDownVal(int id) + { + try + { + // 1. 根据设备ID和开始时间查找报警记录 + var shutDownRecord = _rep.AsQueryable() + .Where(x => x.MACHINE_ID == id && + x.SHUT_TIME == 0) + .First(); + if (shutDownRecord == null) + { + return false; + } + + DateTime endTime = DateTime.Now; + shutDownRecord.SHUT_END_TIME = endTime; + if (shutDownRecord.SHUT_BEGIN_TIME != null) + { + //2.计算时长 + DateTime startTime = (DateTime)shutDownRecord.SHUT_BEGIN_TIME; + TimeSpan continueTime = endTime - startTime; + int totalMinutes = (int)Math.Round(continueTime.TotalMinutes); + shutDownRecord.SHUT_TIME = totalMinutes; + shutDownRecord.DOWNTIME_FLAG = "1"; + } + + bool result = _rep.Update(shutDownRecord); + return result; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/Sln.Imm.Daemon/Program.cs b/Sln.Imm.Daemon/Program.cs index 8f1d050..79e6a65 100644 --- a/Sln.Imm.Daemon/Program.cs +++ b/Sln.Imm.Daemon/Program.cs @@ -1,4 +1,4 @@ -using System.Net.Sockets; +using System.Net.Sockets; using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -48,10 +48,16 @@ namespace Sln.Imm.Daemon }; var deviceCollectionBusiness = ServiceProvider.GetService(); + if (deviceCollectionBusiness == null) + { + log.Error("DeviceDataCollector 未注册,程序退出"); + return; + } - deviceCollectionBusiness?.StartParallelLoopCollectAsync(); - - + // 同时启动两个循环,并等待它们(任一退出前主进程不会结束) + var parallelTask = deviceCollectionBusiness.StartParallelLoopCollectAsync(); + var alarmTask = deviceCollectionBusiness.StartAlarmMonitorLoopSimpleAsync(); + await Task.WhenAll(parallelTask, alarmTask); //List opcs = ServiceProvider.GetService>(); @@ -75,9 +81,6 @@ namespace Sln.Imm.Daemon // Console.WriteLine($"断开设备连接"); // } //} - - - Task.Delay(-1).Wait(); } private static void ConfigureServices(IServiceCollection services) diff --git a/Sln.Imm.Daemon/appsettings.json b/Sln.Imm.Daemon/appsettings.json index cf4e0ed..8acadac 100644 --- a/Sln.Imm.Daemon/appsettings.json +++ b/Sln.Imm.Daemon/appsettings.json @@ -7,7 +7,8 @@ "dbType": 3, "isFlag": true, //"connStr": "server=127.0.0.1;Port=4000;Database=tao_iot;Uid=root;" - "connStr": "Data Source=1.13.177.47:1521/helowin;User ID=haiwei;Password=aucma;" + "connStr": "Data Source=1.13.177.47:1521/helowin;User ID=haiwei;Password=aucma;", + //"connStr": "Data Source=10.100.70.50:1521/NMES;User ID=haiwei;Password=Aucma#2026;" } ] }