change -添加三色灯采集线程

master
启龙 曹 3 weeks ago
parent 7d611f471c
commit 0e6bcc2346

@ -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<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;
}

@ -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<BaseDeviceParamVal> _paramValService;
public DeviceDataCollector(SerilogHelper serilogHelper,
BaseDeviceInfoCacheService cacheService,
IBaseService<BaseDeviceParamVal> paramValService, int maxConcurrentDevices = 15)
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 * 1;
_isCollecting = false;
_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>
@ -73,7 +148,7 @@ public class DeviceDataCollector : IDisposable
/// </summary>
/// <param name="devices">15台设备列表</param>
/// <param name="loopCount">循环次数(-1=无限)</param>
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}] 并行采集循环结束");
}
}
/// <summary>
/// 输出采集结果
/// </summary>
private void OutputCollectResult(Dictionary<string, object> collectResult)
/// <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)
@ -181,6 +304,178 @@ public class DeviceDataCollector : IDisposable
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)
@ -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
/// <summary>
/// 读取设备参数
/// </summary>
@ -265,8 +582,14 @@ public class DeviceDataCollector : IDisposable
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;
}
@ -276,6 +599,47 @@ public class DeviceDataCollector : IDisposable
}
}
/// <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>
@ -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}");
}
}
/// <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>
@ -335,7 +1072,30 @@ public class DeviceDataCollector : IDisposable
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();

@ -51,21 +51,12 @@ public class BaseDeviceInfoCacheService
/// <param name="key"></param>
/// <returns></returns>
public async Task<List<BaseDeviceInfo>> GetValueAsync(string key)
{
var cachedValue = await _fusionCache.GetOrDefaultAsync<List<BaseDeviceInfo>>(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<bool> SetValueAsync(string key, BaseDeviceInfo deviceInfo)

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SqlSugar;
namespace Models
{
/// <summary>
/// 设备报警记录
///</summary>
[SugarTable("DMS_RECORD_ALARM_INFO"), TenantAttribute("mes")]
public class BaseDeviceAlarmVal
{
/// <summary>
/// 备 注:主键标识
/// 默认值:
///</summary>
[SugarColumn(ColumnName="ALARM_ID" , OracleSequenceName = "PARAMRECORD_SEQ_ID", IsPrimaryKey = true) ]
public decimal ALARM_ID { get; set; }
/// <summary>
/// 备 注:设备台账id,关联dms_base_device_ledger的device_id
/// 默认值:
///</summary>
[SugarColumn(ColumnName="DEVICE_ID" ) ]
public decimal DEVICE_ID { get; set; }
/// <summary>
/// 备 注:报警开始时间
/// 默认值:
///</summary>
[SugarColumn(ColumnName="ALARM_BEGIN_TIME" ) ]
public DateTime? ALARM_BEGIN_TIME { get; set; }
/// <summary>
/// 备 注:报警结束时间
/// 默认值:
///</summary>
[SugarColumn(ColumnName="ALARM_END_TIME" ) ]
public DateTime? ALARM_END_TIME { get; set; }
/// <summary>
/// 备 注:报警持续时间ms
/// 默认值:
///</summary>
[SugarColumn(ColumnName="CONTINUE_TIME" ) ]
public decimal? CONTINUE_TIME { get; set; }
}
}

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SqlSugar;
namespace Models
{
/// <summary>
/// 停机记录
///</summary>
[SugarTable("DMS_RECORD_SHUT_DOWN")]
public class BaseRecordShutDown
{
/// <summary>
/// 备 注:主键标识;scada上报的记录
/// 默认值:
///</summary>
[SugarColumn(ColumnName="RECORD_SHUT_DOWN_ID" , OracleSequenceName = "PARAMRECORD_SEQ_ID", IsPrimaryKey = true) ]
public decimal RECORD_SHUT_DOWN_ID { get; set; }
/// <summary>
/// 备 注:设备ID关联prod_base_machine_info的machine_id
/// 默认值:
///</summary>
[SugarColumn(ColumnName="MACHINE_ID" ) ]
public decimal MACHINE_ID { get; set; }
/// <summary>
/// 备 注:停机类型ID关联dm_base_shut_type的shut_type_id
/// 默认值:
///</summary>
[SugarColumn(ColumnName="SHUT_TYPE_ID" ) ]
public decimal? SHUT_TYPE_ID { get; set; }
/// <summary>
/// 备 注:停机原因ID关联dms_base_shut_reason的shut_reason_id
/// 默认值:
///</summary>
[SugarColumn(ColumnName="SHUT_REASON_ID" ) ]
public decimal? SHUT_REASON_ID { get; set; }
/// <summary>
/// 备 注:停机开始时间
/// 默认值:
///</summary>
[SugarColumn(ColumnName="SHUT_BEGIN_TIME" ) ]
public DateTime? SHUT_BEGIN_TIME { get; set; }
/// <summary>
/// 备 注:停机结束时间
/// 默认值:
///</summary>
[SugarColumn(ColumnName="SHUT_END_TIME" ) ]
public DateTime? SHUT_END_TIME { get; set; }
/// <summary>
/// 备 注:停机时长(秒)
/// 默认值:
///</summary>
[SugarColumn(ColumnName="SHUT_TIME" ) ]
public decimal? SHUT_TIME { get; set; }
/// <summary>
/// 备 注:停机标识0未结束 1已结束
/// 默认值:
///</summary>
[SugarColumn(ColumnName="DOWNTIME_FLAG" ) ]
public string DOWNTIME_FLAG { get; set; } = null!;
/// <summary>
/// 备 注:停机原因
/// 默认值:
///</summary>
[SugarColumn(ColumnName="SHUT_REASON" ) ]
public string? SHUT_REASON { get; set; }
/// <summary>
/// 备 注:激活标识1是 0否
/// 默认值:
///</summary>
[SugarColumn(ColumnName="ACTIVE_FLAG" ) ]
public string ACTIVE_FLAG { get; set; } = null!;
}
}

@ -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; }
}
}

@ -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; }

@ -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;
}
}

@ -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<bool> 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<List<OpcNode>> ReadNodeAsync(List<string> nodeId)
@ -256,7 +341,15 @@ public class OpcUaService : IOpcService, IDisposable
{
if (!_disposed)
{
DisconnectAsync().Wait();
try
{
// Dispose 里不要 .Wait() 无限阻塞;最多等待一次默认超时
DisconnectAsync().GetAwaiter().GetResult();
}
catch
{
// swallow
}
_disposed = true;
}
}

@ -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<BaseDeviceAlarmVal>
{
bool UpdateAlarmVal(int id);
}
}

@ -30,4 +30,9 @@ namespace Sln.Imm.Daemon.Repository.service;
public interface IBaseDeviceParamService : IBaseService<BaseDeviceParam>
{
/// <summary>
/// 查出所有报警参数
/// </summary>
/// <returns></returns>
List<BaseDeviceParam> GetDeviceAlarmParams(BaseDeviceInfo deviceInfo);
}

@ -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<BaseRecordShutDown>
{
bool UpdateShutDownVal(int id);
}
}

@ -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<BaseDeviceAlarmVal>, IBaseDeviceAlarmValService
{
public BaseDeviceAlarmValServiceImpl(Repository<BaseDeviceAlarmVal> 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;
}
}
}
}

@ -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<BaseDeviceParam>, IBaseDeviceParamService
{
public class BaseDeviceParamServiceImpl : BaseServiceImpl<BaseDeviceParam>,IBaseDeviceParamService {
public BaseDeviceParamServiceImpl(Repository<BaseDeviceParam> rep) : base(rep)
{
}
/// <summary>
/// 查出所有报警参数(三色灯相关)
/// </summary>
/// <param name="device">设备信息,为 null 或 deviceCode 为空时返回空列表</param>
/// <returns>报警参数列表,无匹配或异常时返回空列表,保证调用方不因空指针中断</returns>
public List<BaseDeviceParam> GetDeviceAlarmParams(BaseDeviceInfo device)
{
try
{
if (device == null)
return new List<BaseDeviceParam>();
if (string.IsNullOrEmpty(device.deviceCode))
return new List<BaseDeviceParam>();
var ctx = _rep?.Context;
if (ctx == null)
return new List<BaseDeviceParam>();
var list = ctx.Queryable<BaseDeviceParam>()
.Where(x => x.deviceCode == device.deviceCode)
.Where(x => x.paramName != null && x.paramName.Contains("三色灯"))
.ToList();
return list ?? new List<BaseDeviceParam>();
}
catch (Exception)
{
// 任何异常(含 NRE、SqlSugar/DB 异常)均返回空列表,避免单台设备导致整轮采集停止;调用方会走“未读取到报警点位”并继续下一轮
return new List<BaseDeviceParam>();
}
}
}

@ -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<BaseRecordShutDown>,IBaseRecordShutDownService
{
public BaseRecordShutDownServiceImpl(Repository<BaseRecordShutDown> 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;
}
}
}
}

@ -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<DeviceDataCollector>();
if (deviceCollectionBusiness == null)
{
log.Error("DeviceDataCollector 未注册,程序退出");
return;
}
deviceCollectionBusiness?.StartParallelLoopCollectAsync();
// 同时启动两个循环,并等待它们(任一退出前主进程不会结束)
var parallelTask = deviceCollectionBusiness.StartParallelLoopCollectAsync();
var alarmTask = deviceCollectionBusiness.StartAlarmMonitorLoopSimpleAsync();
await Task.WhenAll(parallelTask, alarmTask);
//List<IOpcService> opcs = ServiceProvider.GetService<List<IOpcService>>();
@ -75,9 +81,6 @@ namespace Sln.Imm.Daemon
// Console.WriteLine($"断开设备连接");
// }
//}
Task.Delay(-1).Wait();
}
private static void ConfigureServices(IServiceCollection services)

@ -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;"
}
]
}

Loading…
Cancel
Save