You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1103 lines
42 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#region << 版 本 注 释 >>
/*--------------------------------------------------------------------
* 版权所有 (c) 2025 WenJY 保留所有权利。
* CLR版本4.0.30319.42000
* 机器名称Mr.Wen's MacBook Pro
* 命名空间Sln.Imm.Daemon.Business
* 唯一标识A7D0F481-367C-4C8F-9E27-416639212E62
*
* 创建者WenJY
* 电子邮箱:
* 创建时间2025-12-19 09:57:36
* 版本V1.0.0
* 描述:
*
*--------------------------------------------------------------------
* 修改人:
* 时间:
* 修改说明:
*
* 版本V1.0.0
*--------------------------------------------------------------------*/
#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;
public class DeviceDataCollector : IDisposable
{
private readonly SerilogHelper _serilog;
private readonly SemaphoreSlim _semaphore; // 并发控制:限制同时采集的设备数(避免资源耗尽)
private CancellationTokenSource _cts; // 取消令牌:用于终止所有采集任务
private readonly int _collectIntervalMs; // 循环采集间隔(毫秒)
private readonly int _collectAlarmMs; // 循环采集三色灯间隔(毫秒)
private bool _isCollecting; // 是否正在采集(防止重复启动)
private readonly BaseDeviceInfoCacheService _cacheService;
private readonly IBaseDeviceParamService _paramAlarmService;
private readonly IBaseService<BaseDeviceParamVal> _paramValService;
private readonly IBaseDeviceAlarmValService _alarmValService;
private readonly IBaseRecordShutDownService _shutDownValService;
// 在类级别添加这个字典来存储每个设备的上一次状态
private readonly ConcurrentDictionary<string, DeviceAlarmState> _deviceLastAlarmStates =
new ConcurrentDictionary<string, DeviceAlarmState>();
// 新增存储每个设备Alarm触发的状态
private readonly ConcurrentDictionary<string, AlarmTriggerState> _deviceAlarmTriggerStates =
new ConcurrentDictionary<string, AlarmTriggerState>();
// 新增:存储每个设备停机触发的状态
private readonly ConcurrentDictionary<string, ShutDownTriggerState> _deviceShutDownTriggerStates =
new ConcurrentDictionary<string, ShutDownTriggerState>();
private Dictionary<string, bool> _deviceAlarmStatus = new Dictionary<string, bool>();
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<BaseDeviceParamVal> 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 *10 ; //设备数据采集时间间隔
_isCollecting = false;
}
#region 私有类
/// <summary>
/// 设备报警状态结构
/// </summary>
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; // 标记是否为第一次采集
}
/// <summary>
/// Alarm触发状态结构
/// </summary>
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记录
}
/// <summary>
/// 停机触发状态结构
/// </summary>
private class ShutDownTriggerState
{
public bool LastStoppedValue { get; set; }
public DateTime LastTriggerTime { get; set; }
public bool IsShutDownActive { get; set; } // 标记当前是否有活跃的停机记录
}
#endregion
#region 循环采集
/// <summary>
/// 启动15设备并行循环采集
/// </summary>
/// <param name="devices">15台设备列表</param>
/// <param name="loopCount">循环次数(-1=无限)</param>
public async Task StartParallelLoopCollectAsync(int loopCount = -1)
{
if (_isCollecting)
{
Console.WriteLine($"[{DateTime.Now}] 并行采集已在运行,无需重复启动");
return;
}
_isCollecting = true;
int currentLoop = 0;
try
{
while (!_cts.Token.IsCancellationRequested)
{
// 达到指定循环次数退出
if (loopCount > 0 && currentLoop >= loopCount)
{
Console.WriteLine($"[{DateTime.Now}] 完成指定循环次数({loopCount}次),停止采集");
break;
}
currentLoop++;
Console.WriteLine($"\n========== 第 {currentLoop} 轮并行采集开始 [{DateTime.Now}] ==========");
// 记录本轮开始时间保证10秒间隔精准
var roundStartTime = DateTime.Now;
// 核心并行采集15台设备真正同时启动
var collectResult = await CollectDevicesInParallelAsync();
// 输出本轮结果
OutputCollectResult(collectResult);
// 计算耗时补足10秒间隔
var roundCost = (DateTime.Now - roundStartTime).TotalMilliseconds;
var waitMs = _collectIntervalMs - roundCost;
if (waitMs > 0)
{
Console.WriteLine($"\n第 {currentLoop} 轮采集完成(耗时{roundCost:F0}ms等待{waitMs:F0}ms后下一轮");
await Task.Delay((int)waitMs, _cts.Token);
}
else
{
Console.WriteLine($"\n第 {currentLoop} 轮采集超时(耗时{roundCost:F0}ms立即开始下一轮");
}
}
}
catch (OperationCanceledException)
{
Console.WriteLine($"[{DateTime.Now}] 并行采集已手动终止");
}
catch (Exception ex)
{
Console.WriteLine($"[{DateTime.Now}] 并行采集异常:{ex.Message}", ex);
}
finally
{
_isCollecting = false;
Console.WriteLine($"[{DateTime.Now}] 并行采集循环结束");
}
}
/// <summary>
/// 启动报警信息监控循环
/// </summary>
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}] 报警监控结束");
}
}
/// <summary>
/// 输出采集结果
/// </summary>
private void OutputCollectResult(Dictionary<string, object> collectResult)
{
Console.WriteLine($"\n[{DateTime.Now}] 本轮采集结果汇总:");
foreach (var kvp in collectResult)
{
var deviceAddress = kvp.Key;
var result = kvp.Value;
if (result is Exception ex)
{
Console.WriteLine($" {deviceAddress}:失败 - {ex.Message}");
}
else
{
Console.WriteLine($" {deviceAddress}:成功 - {JsonConvert.SerializeObject(result)}");
}
}
}
public async Task<Dictionary<string, object>> CollectDevicesInParallelAsync()
{
var deviceInfos = await _cacheService.GetValueAsync("BaseDeviceInfoCache");
if (deviceInfos == null || deviceInfos.Count == 0)
throw new ArgumentException("设备列表不能为空", nameof(deviceInfos));
var resultDict = new Dictionary<string, object>();
var taskList = new List<Task>();
foreach (var item in deviceInfos)
{
// 每个设备创建独立的采集任务
taskList.Add(CollectSingleDeviceParallelAsync(item, resultDict, _cts.Token));
}
// 等待所有任务完成(无论成功/失败)
await Task.WhenAll(taskList);
return resultDict;
}
/// <summary>
/// 采集三色灯报警:顺序循环采集每台设备,每台采集完成后等待 10 秒再采集下一台;一轮完成后间隔时间不变。
/// </summary>
public async Task<Dictionary<string, object>> 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<string, object>();
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;
}
/// <summary>
/// 顺序模式下单台设备报警采集(无信号量,由上层循环调用)。
/// </summary>
private async Task CollectOneDeviceAlarmAsync(
BaseDeviceInfo device,
Dictionary<string, object> resultDict,
CancellationToken cancellationToken)
{
IOpcService opcUa = null;
List<OpcNode> 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<OpcNode>();
return;
}
if (HasAlarmStateChanged(device, opcItemValues))
{
_serilog.Info($"{device.deviceName}三色灯状态改变");
try
{
SaveParam(device, opcItemValues, out List<BaseDeviceParamVal> 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<string, object> resultDict,
CancellationToken cancellationToken)
{
await _semaphore.WaitAsync(cancellationToken);
List<OpcNode> opcItemValues = null;
IOpcService opcUa = null;
try
{
if (device.deviceFacture.Contains("伊之密"))
{
opcUa =new OpcUaService();
}
else if(device.deviceFacture.Contains("老设备"))
{
opcUa =new OpcDaService();
}
else
{
return;
}
Console.WriteLine($"[{DateTime.Now}] {device.deviceName} - 开始连接并采集");
bool connectResult = await opcUa.ConnectAsync(device.networkAddress);
if (!connectResult)
{
resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 连接失败");
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 连接失败");
return;
}
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 连接成功");
// 2. 读取设备数据你封装的ReadParam方法
//Console.WriteLine($"[{DateTime.Now}] 开始读取设备 {device.deviceName} 数据");
_serilog.Info($"开始采集{device.deviceName}");
opcItemValues = await ReadParam(device, opcUa);
this.SaveParam(device, opcItemValues, out List<BaseDeviceParamVal> paramValues);
_serilog.Info($"{device.deviceName}数据采集完成:{JsonConvert.SerializeObject(opcItemValues)}");
//Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 数据读取完成");
// 3. 存储读取结果
resultDict[device.networkAddress] = opcItemValues;
}
catch (Exception ex)
{
// 捕获采集过程中的异常
resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 采集异常:{ex.Message}", ex);
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 采集异常:{ex.Message}");
}
finally
{
// 无论成功/失败,都断开设备连接(释放资源)
try
{
await SafeDisconnectAsync(opcUa, device.deviceName).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 断开连接失败:{ex.Message}");
}
// 释放信号量(允许下一个设备采集)
_semaphore.Release();
}
}
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
/// <summary>
/// 读取设备参数
/// </summary>
/// <param name="device"></param>
public async Task<List<OpcNode>> ReadParam(BaseDeviceInfo device,IOpcService opcUa)
{
try
{
if (device == null)
{
throw new ArgumentNullException($"设备信息不允许为空");
}
List<string> deviceParams = device.deviceParams.Select(x => x.paramAddr).ToList();
Stopwatch sw = Stopwatch.StartNew();
List<OpcNode> infos = await opcUa.ReadNodeAsync(deviceParams);
sw.Stop();
_serilog.Info($"opc读取耗时{sw.ElapsedMilliseconds.ToString()}");
return infos;
}
catch (Exception e)
{
throw new InvalidOperationException($"设备参数读取异常:{e.Message}");
}
}
/// <summary>
/// 读取设备报警
/// </summary>
/// <param name="device"></param>
/// <param name="opcUa"></param>
/// <returns></returns>
public async Task<List<OpcNode>> ReadAlarm(BaseDeviceInfo device, IOpcService opcUa)
{
try
{
if (device == null)
{
throw new ArgumentNullException($"设备信息不允许为空");
}
if (opcUa == null)
{
throw new ArgumentNullException($"OPC服务不允许为空");
}
List<string> deviceParams = new List<string>();
List<BaseDeviceParam> 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<OpcNode> infos = await opcUa.ReadNodeAsync(deviceParams);
return infos;
}
catch (Exception e)
{
throw new InvalidOperationException($"设备参数读取异常:{e.Message}", e);
}
}
/// <summary>
/// 保存设备参数值到数据库
/// </summary>
/// <param name="device">设备信息</param>
/// <param name="opcItemValues">OPC节点值列表</param>
/// <param name="paramValues">输出参数值DTO列表</param>
public void SaveParam(BaseDeviceInfo device, List<OpcNode> opcItemValues,
out List<BaseDeviceParamVal> paramValues)
{
paramValues = new List<BaseDeviceParamVal>();
try
{
foreach (OpcNode opcItem in opcItemValues)
{
BaseDeviceParamVal deviceParamVal = new BaseDeviceParamVal();
var paramInfo = device.deviceParams.Where(x => x.paramAddr == opcItem.NodeId).FirstOrDefault();
if (paramInfo != null)
{
deviceParamVal.paramCode = paramInfo.paramCode;
deviceParamVal.paramName = paramInfo.paramName;
}
deviceParamVal.deviceCode = device.deviceCode;
deviceParamVal.deviceId = device.objid;
if(opcItem.Value != null)
{
deviceParamVal.paramValue = opcItem.Value.ToString();
}
else
{
deviceParamVal.paramValue = "无";
}
deviceParamVal.paramType = opcItem.DataType;
deviceParamVal.collectTime = DateTime.Now;
deviceParamVal.recordTime = DateTime.Now;
paramValues.Add(deviceParamVal);
}
var isRes = _paramValService.Insert(paramValues);
if (isRes)
{
_serilog.Info(($"{device.deviceName} 设备参数保存成功"));
}
else
{
_serilog.Info(($"{device.deviceName} 设备参数保存失败"));
}
}catch (Exception e)
{
_serilog.Error($"DevicedataCollection报错", e);
throw new InvalidOperationException($"设备参数保存异常:{e.Message}");
}
}
/// <summary>
/// 检查报警状态是否有变化
/// </summary>
/// <param name="device"></param>
/// <param name="currentOpcItemValues"></param>
/// <returns></returns>
private bool HasAlarmStateChanged(BaseDeviceInfo device, List<OpcNode> 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; // 出现异常时,不保存
}
}
/// <summary>
/// 检查是否需要记录Alarm为true的情况新增逻辑
/// </summary>
/// <param name="device">设备信息</param>
/// <param name="currentOpcItemValues">当前采集的OPC点位值</param>
/// <returns>true表示需要记录Alarm为truefalse表示不需要记录</returns>
private int ShouldRecordAlarmTrue(BaseDeviceInfo device, List<OpcNode> 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; // 出现异常时,不操作
}
}
/// <summary>
/// 检查是否需要记录停机为true的情况
/// </summary>
/// <param name="device">设备信息</param>
/// <param name="currentOpcItemValues">当前采集的OPC点位值</param>
/// <returns>0-无操作1-插入2-更新</returns>
private int ShouldRecordShutDownTrue(BaseDeviceInfo device, List<OpcNode> 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; // 出现异常时,不操作
}
}
/// <summary>
/// 安全获取bool值
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// 终止所有采集任务
/// </summary>
public void CancelAllCollectTasks()
{
if (!_cts.IsCancellationRequested)
{
_cts.Cancel();
Console.WriteLine($"[{DateTime.Now}] 已触发采集任务终止");
}
}
/// <summary>
/// 计算两个时间之间的持续时间(分钟),四舍五入取整
/// </summary>
/// <param name="startTime">开始时间</param>
/// <param name="endTime">结束时间</param>
/// <returns>持续时间的总分钟数int类型四舍五入</returns>
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();
}
}