diff --git a/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs b/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs index c45e0a3..8a65cb1 100644 --- a/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs +++ b/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs @@ -38,4 +38,54 @@ } + /// + /// PLC轮询配置 + /// + public class PlcPollingSettings + { + /// + /// 单PLC最大并发数 + /// + public int MaxConcurrentPerPlc { get; set; } = 1; + + /// + /// 有数据请求时轮询间隔(秒) + /// + public double ActivePollInterval { get; set; } = 0.2; + + /// + /// 无数据请求时轮询间隔(秒) + /// + public double IdlePollInterval { get; set; } = 2.0; + + /// + /// 有数据后保持高频轮询的时长(秒) + /// + public int ActiveDuration { get; set; } = 10; + + /// + /// 最大重试次数 + /// + public int MaxRetryTimes { get; set; } = 3; + + /// + /// 初始重试间隔(秒) + /// + public int InitialRetryInterval { get; set; } = 1; + + /// + /// 配置刷新间隔(秒) + /// + public int ConfigRefreshInterval { get; set; } = 30; + + /// + /// 全局最大并行度 + /// + public int GlobalParallelDegree { get; set; } = 15; + + /// + /// 连接超时时间(秒) + /// + public int ConnectTimeoutSeconds { get; set; } = 1; + } } diff --git a/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs b/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs index 28161a7..4e1d870 100644 --- a/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs +++ b/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs @@ -423,5 +423,52 @@ namespace RIZO.Admin.WebApi.PLC.Model [SugarColumn(ColumnName = "Screw7TightenTime")] public string ChipSN { get; set; } + /// + /// 产品1SN + /// + [SugarColumn(ColumnName = "Product1SN")] + public string Product1SN { get; set; } + + /// + /// 产品2SN + /// + [SugarColumn(ColumnName = "Product2SN")] + public string Product2SN { get; set; } + + /// + /// 产品3SN + /// + [SugarColumn(ColumnName = "Product3SN")] + public string Product3SN { get; set; } + + /// + /// 产品4SN + /// + [SugarColumn(ColumnName = "Product4SN")] + public string Product4SN { get; set; } + + /// + /// 产品1结果- 1:OK 2:NG + /// + [SugarColumn(ColumnName = "Product1Result")] + public string Product1Result { get; set; } + + /// + /// 产品2结果- 1:OK 2:NG + /// + [SugarColumn(ColumnName = "Product2Result")] + public string Product2Result { get; set; } + + /// + /// 产品3结果- 1:OK 2:NG + /// + [SugarColumn(ColumnName = "Product3Result")] + public string Product3Result { get; set; } + + /// + /// 产品4结果- 1:OK 2:NG + /// + [SugarColumn(ColumnName = "Product4Result")] + public string Product4Result { get; set; } } } \ No newline at end of file diff --git a/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs b/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs index 7b037d6..9b96d96 100644 --- a/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs +++ b/RIZO.Admin.WebApi/PLC/Service/PlcHostedService.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RIZO.Admin.WebApi.PLC.Model; -using RIZO.Admin.WebApi.PLC.Service; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -17,127 +16,198 @@ namespace RIZO.Admin.WebApi.PLC.Service { private readonly ILogger _logger; private readonly PlcService _plcService; - private readonly List _plcConfigs; private Timer _timer; private bool _isRunning; - private readonly SemaphoreSlim _semaphore; private readonly object _timerLock = new object(); - // 连接状态缓存:减少短时间内重复连接测试 - private readonly ConcurrentDictionary _connectionStateCache; - // 可配置参数(建议放到配置文件中,通过IOptions注入) - private readonly int _parallelDegree = 15; // 并行度(20+PLC建议8-12) - private readonly double _pollingIntervalSeconds = 0.2; // 轮询间隔 - private readonly int _connectTimeoutSeconds = 1; // 单个PLC连接超时时间 - private readonly int _stateCacheExpireSeconds = 5; // 连接状态缓存有效期 + // 1. 按IP隔离信号量(避免单PLC故障阻塞所有) + private readonly ConcurrentDictionary _plcSemaphores = new(); - private PlantWorkstationService plantWorkstationService = new PlantWorkstationService(); + // 2. 连接状态增强(分级+失败次数+最后请求时间) + private readonly ConcurrentDictionary _connectionStatusCache = new(); + + // 3. 配置刷新 + private Timer _configRefreshTimer; + private readonly SemaphoreSlim _configRefreshLock = new(1, 1); + + // 基础配置(配置驱动+热更新) + private readonly IOptionsMonitor _pollingSettingsMonitor; + private PlcPollingSettings _currentSettings; + private SemaphoreSlim _globalSemaphore; + private readonly SemaphoreSlim _timerAsyncLock = new(1, 1); + + // PLC配置列表(支持动态刷新) + private List _plcConfigs = new(); + private PlantWorkstationService _plantWorkstationService; /// - /// PLC 连接状态缓存对象 + /// 增强版PLC连接状态 /// - private class PlcConnectionState + private class PlcConnectionStatus { public bool IsConnected { get; set; } public DateTime LastCheckTime { get; set; } + public int FailCount { get; set; } // 连续失败次数 + public DateTime LastRequestTime { get; set; } // 最后有数据请求的时间 + public ConnectionLevel Level { get; set; } = ConnectionLevel.Disconnected; + } + + /// + /// 连接状态分级 + /// + private enum ConnectionLevel + { + Disconnected = 0, // 断开 + Weak = 1, // 弱连接(偶尔失败) + Normal = 2 // 正常连接 } public PlcHostedService( ILogger logger, - PlcService plcService) + PlcService plcService, + IOptionsMonitor pollingSettingsMonitor = null) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _plcService = plcService ?? throw new ArgumentNullException(nameof(plcService)); + _plantWorkstationService = new PlantWorkstationService(); - //初始化plcConfigs - _plcConfigs = initPlcConfigs(_plcConfigs); + // 配置初始化(支持热更新,无配置时用默认值) + _pollingSettingsMonitor = pollingSettingsMonitor; + _currentSettings = pollingSettingsMonitor?.CurrentValue ?? new PlcPollingSettings(); - - // 初始化并行控制信号量 - _semaphore = new SemaphoreSlim(_parallelDegree, _parallelDegree); - // 初始化连接状态缓存 - _connectionStateCache = new ConcurrentDictionary(); - foreach (var config in _plcConfigs) + // 监听配置热更新 + if (_pollingSettingsMonitor != null) { - if (!string.IsNullOrWhiteSpace(config.Ip)) + _pollingSettingsMonitor.OnChange(newSettings => { - _connectionStateCache.TryAdd(config.Ip, new PlcConnectionState - { - IsConnected = false, - LastCheckTime = DateTime.MinValue - }); - } - else - { - _logger.LogWarning("发现空IP的PLC配置,已跳过缓存初始化"); - } + _currentSettings = newSettings; + _logger.LogInformation("PLC轮询配置已热更新,新配置:{Settings}", + Newtonsoft.Json.JsonConvert.SerializeObject(newSettings)); + // 配置变更后立即调整定时器频率 + AdjustTimerInterval(); + // 重新初始化全局信号量(并行度变更时生效) + _globalSemaphore?.Dispose(); + _globalSemaphore = new SemaphoreSlim( + _currentSettings.GlobalParallelDegree, + _currentSettings.GlobalParallelDegree); + }); } + + // 初始化全局信号量 + _globalSemaphore = new SemaphoreSlim( + _currentSettings.GlobalParallelDegree, + _currentSettings.GlobalParallelDegree); + + // 初始化PLC配置 + _ = RefreshPlcConfigsAsync(); } - private List initPlcConfigs(List result) + /// + /// 动态刷新PLC配置(支持热更新) + /// + private async Task RefreshPlcConfigsAsync() { - var defaultResult = result ?? new List(); + if (!await _configRefreshLock.WaitAsync(0)) return; + try { - List query = plantWorkstationService.Queryable() - .Where(it => it.Status == 1) - .Select(it => new PlcConfig - { - PlcName = it.WorkstationCode, - Ip = it.PlcIP, - Rack = (short)it.Rack, // 直接强制转换(it.Rack是int,非空) - Slot = (short)it.Slot // 同理,it.Slot是int,非空 - }) - .ToList(); - return query.Count > 0 ? query : defaultResult; + var newConfigs = new List(); + try + { + newConfigs = _plantWorkstationService.Queryable() + .Where(it => it.Status == 1) + .Select(it => new PlcConfig + { + PlcName = it.WorkstationCode, + Ip = it.PlcIP, + Rack = (short)it.Rack, // 空值兜底 + Slot = (short)it.Slot // 空值兜底 + }) + .Where(c => !string.IsNullOrWhiteSpace(c.Ip)) + .ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "刷新PLC配置异常,使用旧配置"); + return; + } + + // 更新配置并初始化状态缓存 + if (newConfigs.Any()) + { + _plcConfigs = newConfigs; + foreach (var config in _plcConfigs) + { + // 初始化IP隔离信号量(从配置读取并发数) + _plcSemaphores.TryAdd(config.Ip, new SemaphoreSlim( + _currentSettings.MaxConcurrentPerPlc, + _currentSettings.MaxConcurrentPerPlc)); + + // 初始化连接状态 + _connectionStatusCache.TryAdd(config.Ip, new PlcConnectionStatus + { + IsConnected = false, + LastCheckTime = DateTime.MinValue, + FailCount = 0, + LastRequestTime = DateTime.MinValue, + Level = ConnectionLevel.Disconnected + }); + } + _logger.LogInformation($"刷新PLC配置完成,当前有效配置数:{_plcConfigs.Count}"); + } } - catch (Exception ex) + finally { - Console.WriteLine($"初始化PLC配置异常:{ex.Message}"); - return defaultResult; + _configRefreshLock.Release(); } } /// - /// 重写BackgroundService的ExecuteAsync(替代StartAsync) + /// 重写BackgroundService的ExecuteAsync /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("PLC后台监听服务启动中..."); - //获取PLC配置 - if (!_plcConfigs.Any()) { - _logger.LogWarning("未配置PLC参数,跳过PLC自动连接"); + _logger.LogWarning("未配置有效PLC参数,跳过PLC自动连接"); return; } _isRunning = true; - // 1. 启动时并行测试所有PLC连接 + // 1. 启动时批量并行连接PLC await BatchConnectPlcAsync(stoppingToken); - // 2. 启动安全定时器(防重叠执行) + // 2. 启动配置刷新定时器(从配置读取间隔) + _configRefreshTimer = new Timer( + async _ => await RefreshPlcConfigsAsync(), + null, + TimeSpan.Zero, + TimeSpan.FromSeconds(_currentSettings.ConfigRefreshInterval)); + + // 3. 启动数据轮询定时器(初始使用空闲频率) _timer = new Timer( TimerCallback, null, TimeSpan.Zero, - TimeSpan.FromSeconds(_pollingIntervalSeconds)); + TimeSpan.FromSeconds(_currentSettings.IdlePollInterval)); - _logger.LogInformation($"PLC服务启动完成 | 并行度:{_parallelDegree} | 轮询间隔:{_pollingIntervalSeconds}s | 设备总数:{_plcConfigs.Count}"); + _logger.LogInformation($"PLC服务启动完成 | 全局并行度:{_currentSettings.GlobalParallelDegree} | 设备总数:{_plcConfigs.Count}"); // 等待停止信号 await Task.Delay(Timeout.Infinite, stoppingToken); - // 停止定时器 + // 停止所有定时器 _timer?.Change(Timeout.Infinite, 0); + _configRefreshTimer?.Change(Timeout.Infinite, 0); _logger.LogInformation("PLC后台监听服务已收到停止信号"); } - // 1. 定义异步锁(全局变量) - private readonly SemaphoreSlim _timerAsyncLock = new SemaphoreSlim(1, 1); - // 2. 重构TimerCallback为异步锁版本 + /// + /// 优化版定时器回调(动态调整轮询频率) + /// private async void TimerCallback(object state) { if (!_isRunning) @@ -146,9 +216,8 @@ namespace RIZO.Admin.WebApi.PLC.Service return; } - // 尝试获取异步锁(超时时间设为0,等价于TryEnter) - bool isLockAcquired = await _timerAsyncLock.WaitAsync(0); - if (!isLockAcquired) + // 异步锁防止重叠执行 + if (!await _timerAsyncLock.WaitAsync(0)) { _logger.LogDebug("前一轮PLC轮询未完成,跳过本次执行"); return; @@ -157,6 +226,9 @@ namespace RIZO.Admin.WebApi.PLC.Service try { await PollPlcDataAsync(); + + // 动态调整定时器频率(全局自适应) + AdjustTimerInterval(); } catch (Exception ex) { @@ -164,19 +236,46 @@ namespace RIZO.Admin.WebApi.PLC.Service } finally { - // 释放异步锁(无异常风险) _timerAsyncLock.Release(); } } /// - /// 启动时批量并行连接PLC + /// 动态调整定时器轮询频率 + /// + private void AdjustTimerInterval() + { + // 统计活跃时长内有数据请求的PLC数量 + var activePlcCount = _connectionStatusCache.Values + .Count(s => (DateTime.Now - s.LastRequestTime).TotalSeconds <= _currentSettings.ActiveDuration); + + // 有活跃PLC则使用高频,否则使用低频 + var newInterval = activePlcCount > 0 + ? TimeSpan.FromSeconds(_currentSettings.ActivePollInterval) + : TimeSpan.FromSeconds(_currentSettings.IdlePollInterval); + + // 仅当频率变化时更新定时器 + if (_timer != null) + { + try + { + _timer.Change(newInterval, newInterval); + _logger.LogDebug($"调整轮询频率:{newInterval.TotalSeconds}s(活跃PLC数:{activePlcCount})"); + } + catch (Exception ex) + { + _logger.LogError(ex, "调整轮询频率失败"); + } + } + } + + /// + /// 启动时批量并行连接PLC(带指数退避重试) /// private async Task BatchConnectPlcAsync(CancellationToken cancellationToken) { _logger.LogInformation("开始批量连接所有PLC..."); - // 过滤空IP配置,避免无效任务 var validConfigs = _plcConfigs.Where(c => !string.IsNullOrWhiteSpace(c.Ip)).ToList(); if (!validConfigs.Any()) { @@ -186,50 +285,58 @@ namespace RIZO.Admin.WebApi.PLC.Service var tasks = validConfigs.Select(async config => { - await _semaphore.WaitAsync(cancellationToken); + await _globalSemaphore.WaitAsync(cancellationToken); try { - // 带超时的连接测试(合并cancellationToken,支持外部停止) - using var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - timeoutTokenSource.CancelAfter(TimeSpan.FromSeconds(_connectTimeoutSeconds)); - - var result = await _plcService.TestSinglePlcAsync(config, timeoutTokenSource.Token); - - // 安全更新缓存(ConcurrentDictionary线程安全) - if (_connectionStateCache.TryGetValue(config.Ip, out var state)) + // 指数退避重试连接 + bool connectSuccess = false; + for (int retry = 0; retry < _currentSettings.MaxRetryTimes; retry++) { - state.IsConnected = result.ConnectSuccess; - state.LastCheckTime = DateTime.Now; + try + { + using var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutTokenSource.CancelAfter(TimeSpan.FromSeconds(_currentSettings.ConnectTimeoutSeconds)); + + var result = await _plcService.TestSinglePlcAsync(config, timeoutTokenSource.Token); + connectSuccess = result.ConnectSuccess; + + if (connectSuccess) break; + + // 指数退避等待(从配置读取初始间隔) + var waitTime = TimeSpan.FromSeconds(_currentSettings.InitialRetryInterval * Math.Pow(2, retry)); + await Task.Delay(waitTime, cancellationToken); + } + catch (OperationCanceledException) + { + _logger.LogWarning($"[{config.PlcName}] 连接重试{retry + 1}次超时 | IP:{config.Ip}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{config.PlcName}] 连接重试{retry + 1}次异常 | IP:{config.Ip}"); + } } - if (result.ConnectSuccess) + // 更新连接状态 + if (_connectionStatusCache.TryGetValue(config.Ip, out var status)) + { + status.IsConnected = connectSuccess; + status.LastCheckTime = DateTime.Now; + status.FailCount = connectSuccess ? 0 : status.FailCount + 1; + status.Level = connectSuccess ? ConnectionLevel.Normal : ConnectionLevel.Disconnected; + } + + if (connectSuccess) _logger.LogInformation($"[{config.PlcName}] 连接成功 | IP:{config.Ip}"); else - _logger.LogWarning($"[{config.PlcName}] 连接失败 | IP:{config.Ip} | 原因:{result.ConnectMessage}"); - } - catch (OperationCanceledException) - { - _logger.LogWarning($"[{config.PlcName}] 连接超时 | IP:{config.Ip} | 超时时间:{_connectTimeoutSeconds}s"); - // 超时标记为断开 - if (_connectionStateCache.TryGetValue(config.Ip, out var state)) - { - state.IsConnected = false; - state.LastCheckTime = DateTime.Now; - } + _logger.LogWarning($"[{config.PlcName}] 连接失败({_currentSettings.MaxRetryTimes}次重试) | IP:{config.Ip}"); } catch (Exception ex) { - _logger.LogError(ex, $"[{config.PlcName}] 连接异常 | IP:{config.Ip}"); - // 异常标记为断开 - if (_connectionStateCache.TryGetValue(config.Ip, out var state)) - { - state.IsConnected = false; - state.LastCheckTime = DateTime.Now; - } + _logger.LogError(ex, $"[{config.PlcName}] 批量连接异常 | IP:{config.Ip}"); } finally { - _semaphore.Release(); + _globalSemaphore.Release(); } }); @@ -237,12 +344,10 @@ namespace RIZO.Admin.WebApi.PLC.Service } /// - /// 并行轮询PLC数据(读取前先验证连接状态,修复传参/日志/数据处理问题) + /// 优化版并行轮询PLC数据(IP隔离+动态频率+异常闭环) /// private async Task PollPlcDataAsync() { - - // 过滤有效配置(非空IP) var validConfigs = _plcConfigs.Where(c => !string.IsNullOrWhiteSpace(c.Ip)).ToList(); if (!validConfigs.Any()) { @@ -251,58 +356,46 @@ namespace RIZO.Admin.WebApi.PLC.Service } // 统计本次轮询结果 - int successCount = 0; - int failCount = 0; - int skipCount = 0; + int successCount = 0, failCount = 0, skipCount = 0, activeCount = 0; var tasks = validConfigs.Select(async config => { - await _semaphore.WaitAsync(); + // 获取IP隔离信号量 + if (!_plcSemaphores.TryGetValue(config.Ip, out var plcSemaphore)) + { + _logger.LogWarning($"[{config.PlcName}] 未找到IP隔离信号量,跳过 | IP:{config.Ip}"); + Interlocked.Increment(ref skipCount); + return; + } + + await _globalSemaphore.WaitAsync(); + await plcSemaphore.WaitAsync(); + try { - // 1. 校验缓存是否存在 - if (!_connectionStateCache.TryGetValue(config.Ip, out var state)) + if (!_connectionStatusCache.TryGetValue(config.Ip, out var status)) { _logger.LogWarning($"[{config.PlcName}] 未找到连接状态缓存,跳过 | IP:{config.Ip}"); Interlocked.Increment(ref skipCount); return; } - // 2. 判断是否需要重新检查连接(缓存过期) - var needRecheck = (DateTime.Now - state.LastCheckTime).TotalSeconds > _stateCacheExpireSeconds; - bool isConnected = state.IsConnected; + // 1. 检查连接状态缓存是否过期 + bool needRecheck = (DateTime.Now - status.LastCheckTime).TotalSeconds > _currentSettings.ConfigRefreshInterval; + bool isConnected = status.IsConnected; - // 3. 缓存过期则重新测试连接 + // 2. 缓存过期则重新检查连接 if (needRecheck) { - using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(_connectTimeoutSeconds)); - try - { - var connectResult = await _plcService.TestSinglePlcAsync(config, timeoutTokenSource.Token); - isConnected = connectResult.ConnectSuccess; - // 更新缓存状态 - state.IsConnected = isConnected; - state.LastCheckTime = DateTime.Now; + isConnected = await RecheckPlcConnectionAsync(config, status); + } - if (!isConnected) - { - _logger.LogDebug($"[{config.PlcName}] 连接缓存过期,重新检测仍断开 | IP:{config.Ip}"); - } - } - catch (OperationCanceledException) - { - _logger.LogWarning($"[{config.PlcName}] 连接重检超时 | IP:{config.Ip} | 超时:{_connectTimeoutSeconds}s"); - isConnected = false; - state.IsConnected = false; - state.LastCheckTime = DateTime.Now; - } - catch (Exception ex) - { - _logger.LogError(ex, $"[{config.PlcName}] 连接重检异常 | IP:{config.Ip}"); - isConnected = false; - state.IsConnected = false; - state.LastCheckTime = DateTime.Now; - } + // 3. 连接断开且失败次数超阈值,触发告警 + if (!isConnected && status.FailCount >= _currentSettings.MaxRetryTimes) + { + _logger.LogError($"[{config.PlcName}] 连续{status.FailCount}次连接失败,触发告警 | IP:{config.Ip}"); + Interlocked.Increment(ref skipCount); + return; } // 4. 连接断开则跳过读取 @@ -313,111 +406,172 @@ namespace RIZO.Admin.WebApi.PLC.Service return; } - // 5. 读取PLC生产数据(核心修复:移除多余的TestReadAddress参数) + // 5. 读取PLC生产数据 var (success, prodData, message) = await _plcService.ReadProductionDataAsync( - config.Ip, config.PlcName, config.Rack, config.Slot); // 仅传3个必要参数 + config.Ip, config.PlcName, config.Rack, config.Slot); if (success) { - - // 数据处理逻辑(示例:可替换为入库/推MQ/存Redis等) - await ProcessPlcProductionDataAsync(config, prodData); - + // 更新最后请求时间(标记为活跃) + status.LastRequestTime = DateTime.Now; + status.Level = ConnectionLevel.Normal; + status.FailCount = 0; Interlocked.Increment(ref successCount); + + // 有有效数据则计数 + if (prodData != null) + { + Interlocked.Increment(ref activeCount); + await ProcessPlcProductionDataAsync(config, prodData); + } } else { _logger.LogWarning($"[{config.PlcName}] 生产数据读取失败 | IP:{config.Ip} | 原因:{message}"); - // 读取失败标记连接断开 - state.IsConnected = false; + // 读取失败更新状态 + status.FailCount++; + status.Level = status.FailCount <= 1 ? ConnectionLevel.Weak : ConnectionLevel.Disconnected; Interlocked.Increment(ref failCount); } } catch (OperationCanceledException) { _logger.LogWarning($"[{config.PlcName}] 轮询操作超时 | IP:{config.Ip}"); - if (_connectionStateCache.TryGetValue(config.Ip, out var state)) - { - state.IsConnected = false; - } + UpdateStatusOnFailure(config.Ip); Interlocked.Increment(ref failCount); } catch (Exception ex) { _logger.LogError(ex, $"[{config.PlcName}] 轮询异常 | IP:{config.Ip}"); - if (_connectionStateCache.TryGetValue(config.Ip, out var state)) - { - state.IsConnected = false; - } + UpdateStatusOnFailure(config.Ip); Interlocked.Increment(ref failCount); } finally { - _semaphore.Release(); + plcSemaphore.Release(); + _globalSemaphore.Release(); } }); - // 等待所有轮询任务完成 await Task.WhenAll(tasks); + + // 输出本轮轮询统计 + _logger.LogInformation($"PLC轮询完成 | 成功:{successCount} | 失败:{failCount} | 跳过:{skipCount} | 有数据:{activeCount}"); } /// - /// 处理PLC生产数据(示例方法:可根据业务扩展) + /// 重新检查PLC连接状态 + /// + private async Task RecheckPlcConnectionAsync(PlcConfig config, PlcConnectionStatus status) + { + try + { + using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(_currentSettings.ConnectTimeoutSeconds)); + var connectResult = await _plcService.TestSinglePlcAsync(config, timeoutTokenSource.Token); + + // 更新状态 + status.IsConnected = connectResult.ConnectSuccess; + status.LastCheckTime = DateTime.Now; + status.FailCount = connectResult.ConnectSuccess ? 0 : status.FailCount + 1; + status.Level = connectResult.ConnectSuccess ? ConnectionLevel.Normal : ConnectionLevel.Disconnected; + + if (!connectResult.ConnectSuccess) + { + _logger.LogDebug($"[{config.PlcName}] 连接缓存过期,重新检测仍断开 | IP:{config.Ip}"); + } + + return connectResult.ConnectSuccess; + } + catch (OperationCanceledException) + { + _logger.LogWarning($"[{config.PlcName}] 连接重检超时 | IP:{config.Ip}"); + status.IsConnected = false; + status.LastCheckTime = DateTime.Now; + status.FailCount++; + status.Level = ConnectionLevel.Disconnected; + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{config.PlcName}] 连接重检异常 | IP:{config.Ip}"); + status.IsConnected = false; + status.LastCheckTime = DateTime.Now; + status.FailCount++; + status.Level = ConnectionLevel.Disconnected; + return false; + } + } + + /// + /// 失败时更新连接状态 + /// + private void UpdateStatusOnFailure(string ip) + { + if (_connectionStatusCache.TryGetValue(ip, out var status)) + { + status.IsConnected = false; + status.FailCount++; + status.Level = status.FailCount <= 1 ? ConnectionLevel.Weak : ConnectionLevel.Disconnected; + } + } + + /// + /// 处理PLC生产数据(可扩展) /// - /// PLC配置 - /// 生产数据 private async Task ProcessPlcProductionDataAsync(PlcConfig config, PlcProductionData prodData) { try { - // 示例1:写入数据库(需注入仓储/服务) - // await _plcProductionDataRepository.AddAsync(prodData); - - // 示例2:推送至消息队列(如RabbitMQ/Kafka) - // await _messageQueueService.PublishAsync("plc_production_data", prodData); - - // 示例3:缓存至Redis - // await _redisCache.SetAsync($"plc:production:{config.Ip}", prodData, TimeSpan.FromMinutes(5)); - - await Task.CompletedTask; // 异步占位 + // 业务处理逻辑:入库/推MQ/缓存等 + // 示例:await _plcDataService.SaveProductionDataAsync(prodData); + await Task.CompletedTask; } catch (Exception ex) { _logger.LogError(ex, $"[{config.PlcName}] 生产数据处理失败 | IP:{config.Ip}"); - // 数据处理失败不影响轮询,仅记录日志 } } /// - /// 重写停止逻辑(更安全) + /// 重写停止逻辑 /// public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("PLC后台监听服务停止中..."); _isRunning = false; - // 停止定时器 + // 停止所有定时器 _timer?.Change(Timeout.Infinite, 0); + _configRefreshTimer?.Change(Timeout.Infinite, 0); // 等待当前轮询完成 - await Task.Delay(100, cancellationToken); + await Task.Delay(1000, cancellationToken); await base.StopAsync(cancellationToken); _logger.LogInformation("PLC后台监听服务已停止"); } /// - /// 资源释放(完整实现IDisposable) + /// 完整释放资源 /// public override void Dispose() { - // 停止定时器 + // 释放定时器 _timer?.Dispose(); - // 释放信号量 - _semaphore?.Dispose(); - // 调用基类释放 - base.Dispose(); + _configRefreshTimer?.Dispose(); + // 释放信号量 + _globalSemaphore?.Dispose(); + _timerAsyncLock?.Dispose(); + _configRefreshLock?.Dispose(); + + // 释放IP隔离信号量 + foreach (var semaphore in _plcSemaphores.Values) + { + semaphore.Dispose(); + } + + base.Dispose(); _logger.LogInformation("PLC后台监听服务已释放所有资源"); } } diff --git a/RIZO.Admin.WebApi/PLC/Service/PlcService.cs b/RIZO.Admin.WebApi/PLC/Service/PlcService.cs index f2cd783..ce1499e 100644 --- a/RIZO.Admin.WebApi/PLC/Service/PlcService.cs +++ b/RIZO.Admin.WebApi/PLC/Service/PlcService.cs @@ -65,7 +65,55 @@ namespace RIZO.Admin.WebApi.PLC.Service { "总结果", "DB1001.DBW2158" }, // Int - 一个托盘上传四次结果 //{ "节拍时间", "DB1001.DBD2988" } // Real }; - // OP070-1 专属地址映射 + + // OP020-3 专属地址映射(热铆工位,DB1001) + private readonly Dictionary _op020_3IntMap = new() + { + { "运行状态", "DB1001.DBW0" }, // Int - 1=空闲,2=运行中,3=故障 + { "设备模式", "DB1001.DBW2" }, // Int - 1=空模式;2=手动;4=初始化;8=自动;16=CycleStop + { "设备在线状态", "DB1001.DBW4" }, // Int - 1=离线,0=在线 + { "生产模式", "DB1001.DBW8" }, // Int - 1=正常模式;2=清线模式;4=返工模式;8=换型模式;16=预热模式 + }; + + // OP020-4 专属地址映射(pin压合&视觉检查工位,DB1001) + private readonly Dictionary _op020_4StringMap = new() + { + { "报警信息", ("DB1001.DBB58", 48) }, // Array[1..48] of Byte + { "产品型号", ("DB1001.DBB1000", 48) }, // String[48] + { "产品名称", ("DB1001.DBB1054", 48) }, // String[48] + { "产品1SN", ("DB1001.DBB2230", 40) }, // String[40] + { "产品2SN", ("DB1001.DBB2360", 40) }, // String[40] + { "产品3SN", ("DB1001.DBB2490", 40) }, // String[40] + { "产品4SN", ("DB1001.DBB2620", 40) } // String[40] + }; + + private readonly Dictionary _op020_4IntMap = new() + { + // 基础状态 + { "运行状态", "DB1001.DBW0" }, // Int - 1=空闲,2=运行中,3=故障 + { "设备模式", "DB1001.DBW2" }, // Int - 1=空模式;2=手动;4=初始化;8=自动;16=CycleStop + { "设备在线状态", "DB1001.DBW4" }, // Int - 1=离线,0=在线 + { "ByPass", "DB1001.DBW6" }, // Int - 1=ByPass,0=正常模式 + { "生产模式", "DB1001.DBW8" }, // Int - 1=正常模式;2=清线模式;4=返工模式;8=换型模式;16=预热模式 + + // 上传请求 + { "产品1上传请求", "DB1001.DBW2002" }, // Int + { "产品2上传请求", "DB1001.DBW2004" }, // Int + { "产品3上传请求", "DB1001.DBW2006" }, // Int + { "产品4上传请求", "DB1001.DBW2008" }, // Int + + // 托盘号 + { "托盘号", "DB1001.DBW2070" }, // Int + + // 产品结果 + { "产品1结果", "DB1001.DBW2274" }, // Int - 1:OK 2:NG + { "产品2结果", "DB1001.DBW2404" }, // Int - 1:OK 2:NG + { "产品3结果", "DB1001.DBW2534" }, // Int - 1:OK 2:NG + { "产品4结果", "DB1001.DBW2664" }, // Int - 1:OK 2:NG + + }; + + // OP070-1 专属地址映射 (点散热胶GF1500工位) OP070-2 点散热胶TC4060 OP070-3 点散热胶GF3500 private readonly Dictionary _op070_1StringMap = new() { //{ "报警信息", ("DB1011.DBB58", 48) }, // Array[1..48] of Byte @@ -94,7 +142,7 @@ namespace RIZO.Admin.WebApi.PLC.Service { "节拍时间", "DB1011.DBD2168" } // Real }; - // OP075 专属地址映射 + // OP075 专属地址映射 PWM折弯&装配 private readonly Dictionary _op075StringMap = new() { //{ "报警信息", ("DB1011.DBB58", 48) }, // Array[1..48] of Byte @@ -123,7 +171,7 @@ namespace RIZO.Admin.WebApi.PLC.Service { "节拍时间", "DB1011.DBD2284" } // Real }; - // OP080-1 专属地址映射 + // OP080-1 专属地址映射 PCBA组装&拧紧 private readonly Dictionary _op080_1StringMap = new() { //{ "报警信息", ("DB1016.DBB58", 48) }, // Array[1..48] of Byte @@ -365,6 +413,22 @@ namespace RIZO.Admin.WebApi.PLC.Service //查询请求??20260125 PM iSaveRequest = await ReadPlcIntAsync(plc, _op020_2IntMap["上传请求"]); } + else if (plcName == "OP020-3") + { + //保存请求??20260125 PM + iSaveRequest = 1; //每次轮询都会读 + } + else if (plcName == "OP020-4") + { + int iSaveRequest1 = await ReadPlcIntAsync(plc, _op020_4IntMap["产品1上传请求"]); + int iSaveRequest2 = await ReadPlcIntAsync(plc, _op020_4IntMap["产品2上传请求"]); + int iSaveRequest3 = await ReadPlcIntAsync(plc, _op020_4IntMap["产品3上传请求"]); + int iSaveRequest4 = await ReadPlcIntAsync(plc, _op020_4IntMap["产品4上传请求"]); + if (iSaveRequest1 == 1 || iSaveRequest2 == 1 || iSaveRequest3 == 1 || iSaveRequest4 == 1) + { + iSaveRequest = 1; + } + } else if (plcName == "OP070-1" || plcName == "OP070-2" || plcName == "OP070-3") { iQueryRequest = await ReadPlcIntAsync(plc, _op070_1IntMap["查询请求"]); @@ -417,9 +481,13 @@ namespace RIZO.Admin.WebApi.PLC.Service { prodData = await ReadOP020_2DataAsync(plc, ip, plcName); } - if (plcName == "OP020-3") + else if (plcName == "OP020-3") { - prodData = await ReadOP020_2DataAsync(plc, ip, plcName); + prodData = await ReadOP020_3DataAsync(plc, ip, plcName); + } + else if (plcName == "OP020-4") + { + } else if (plcName == "OP070-1" || plcName == "OP070-2" || plcName == "OP070-3") { @@ -570,6 +638,75 @@ namespace RIZO.Admin.WebApi.PLC.Service } } /// + /// 读取OP020-3数据(热铆工位) + /// + private async Task ReadOP020_3DataAsync(Plc plc, string ip, string workstationCode) + { + try + { + // 1. 批量并行读取所有字段(仅读取字典中存在的字段) + var intFields = await Task.Run(async () => ( + // Int字段(严格匹配_op020_3IntMap中的字段,无冗余) + RunStatus: await ReadPlcIntAsync(plc, _op020_3IntMap["运行状态"]), + MachineModel: await ReadPlcIntAsync(plc, _op020_3IntMap["设备模式"]), + OnlineStatus: await ReadPlcIntAsync(plc, _op020_3IntMap["设备在线状态"]), + ProduceModel: await ReadPlcIntAsync(plc, _op020_3IntMap["生产模式"]) + )); + + // 2. 解构字段 + int runStatus = intFields.RunStatus; + int machineModel = intFields.MachineModel; + int onlineStatus = intFields.OnlineStatus; + int produceModel = intFields.ProduceModel; + + // 3. 业务逻辑计算 + var reworkFlag = produceModel == 4 ? "1" : "0"; + // 生产模式描述(严格匹配OP020-3的定义) + string produceModelDesc = produceModel switch + { + 1 => "正常模式", + 2 => "清线模式", + 4 => "返工模式", + 8 => "换型模式", + 16 => "预热模式", + _ => $"未知({produceModel})" + }; + // 运行状态描述(1=空闲,2=运行中,3=故障) + string runStatusDesc = runStatus switch { 1 => "空闲", 2 => "运行中", 3 => "故障", _ => $"未知({runStatus})" }; + // 在线状态描述(1=离线,0=在线) + string onlineStatusDesc = onlineStatus == 1 ? "离线" : "在线"; + + // 调试日志(仅输出实际读取的字段) + Console.WriteLine($"OP020-3({ip})读取结果:运行状态={runStatusDesc},生产模式={produceModelDesc}"); + + // 4. 构建数据实体(无冗余字段,匹配读取结果) + return new PlcProductionData + { + // 基础字段 + PlcIp = ip.Trim(), + OccurTime = DateTime.Now, + LineCode = "line2", // 按实际产线调整 + WorkstationCode = workstationCode, + + // 状态字段(严格匹配读取结果) + ReworkFlag = reworkFlag, + Automanual = machineModel, + Runstatus = runStatus, + OnlineStatus = onlineStatusDesc, + ProduceModel = produceModelDesc, + + // 系统字段 + CreatedBy = "PLC", + CreatedTime = DateTime.Now, + }; + } + catch (Exception ex) + { + Console.WriteLine($"OP020-3({ip})数据读取异常:{ex.Message}"); + return null; // 异常时返回null,符合可空类型设计 + } + } + /// /// OP070-1数据读取 /// private async Task ReadOP070_1DataAsync(Plc plc, string ip,string workstationCode) diff --git a/RIZO.Admin.WebApi/Program.cs b/RIZO.Admin.WebApi/Program.cs index e24a091..91a5de4 100644 --- a/RIZO.Admin.WebApi/Program.cs +++ b/RIZO.Admin.WebApi/Program.cs @@ -34,10 +34,12 @@ builder.Services.AddControllers(); // ===== 新增PLC服务注册 ===== builder.Services.Configure>(builder.Configuration.GetSection("PlcConfigs")); builder.Services.Configure(builder.Configuration.GetSection("GlobalPlcConfig")); +// 注册PLC轮询配置 +builder.Services.Configure(builder.Configuration.GetSection("PlcPollingSettings")); builder.Services.AddSingleton(); builder.Services.AddScoped(); // 新增:注册PLC后台监听服务(项目启动自动执行) -//builder.Services.AddHostedService(); +builder.Services.AddHostedService(); // ========================== // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/RIZO.Admin.WebApi/appsettings.json b/RIZO.Admin.WebApi/appsettings.json index 741b8df..a5a379e 100644 --- a/RIZO.Admin.WebApi/appsettings.json +++ b/RIZO.Admin.WebApi/appsettings.json @@ -100,52 +100,16 @@ "ConnectTimeout": 5000, // 连接超时(毫秒) "ReadWriteTimeout": 5000 // 读写超时(毫秒) }, - - "PlcSettings": { - "Stations": [ - { - "StationName": "装配站1", - "IpAddress": "192.168.0.10", - "Rack": 0, - "Slot": 1, - "ReadIntervalMs": 500, - "DataItems": [ - { - "Name": "产品计数", - "Address": "DB1.DBD0", - "VarType": "Int", - "DB": 1, - "StartByteAdr": 0 - }, - { - "Name": "运行状态", - "Address": "DB1.DBX2.0", - "VarType": "Bit", - "DB": 1, - "StartByteAdr": 2 - } - ] - } - ] - - }, - "PlcSettingss": { - "Connections": [ - { - "PlcId": "PLC1", - "CpuType": "S71200", - "Ip": "192.168.1.10", - "Rack": 0, - "Slot": 1 - }, - { - "PlcId": "PLC2", - "CpuType": "S71500", - "Ip": "192.168.1.20", - "Rack": 0, - "Slot": 1 - } - ] + "PlcPollingSettings": { + "MaxConcurrentPerPlc": 1, + "ActivePollInterval": 0.2, + "IdlePollInterval": 2.0, + "ActiveDuration": 10, + "MaxRetryTimes": 3, + "InitialRetryInterval": 1, + "ConfigRefreshInterval": 30, + "GlobalParallelDegree": 15, + "ConnectTimeoutSeconds": 1 } }