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#

#region << 版 本 注 释 >>
/*--------------------------------------------------------------------
* (c) 2025 WenJY
* CLR4.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();
}
}