PLC轮询优化
This commit is contained in:
parent
9c20d9f905
commit
603eeda7df
@ -38,4 +38,54 @@
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PLC轮询配置
|
||||
/// </summary>
|
||||
public class PlcPollingSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// 单PLC最大并发数
|
||||
/// </summary>
|
||||
public int MaxConcurrentPerPlc { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 有数据请求时轮询间隔(秒)
|
||||
/// </summary>
|
||||
public double ActivePollInterval { get; set; } = 0.2;
|
||||
|
||||
/// <summary>
|
||||
/// 无数据请求时轮询间隔(秒)
|
||||
/// </summary>
|
||||
public double IdlePollInterval { get; set; } = 2.0;
|
||||
|
||||
/// <summary>
|
||||
/// 有数据后保持高频轮询的时长(秒)
|
||||
/// </summary>
|
||||
public int ActiveDuration { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 最大重试次数
|
||||
/// </summary>
|
||||
public int MaxRetryTimes { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 初始重试间隔(秒)
|
||||
/// </summary>
|
||||
public int InitialRetryInterval { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 配置刷新间隔(秒)
|
||||
/// </summary>
|
||||
public int ConfigRefreshInterval { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 全局最大并行度
|
||||
/// </summary>
|
||||
public int GlobalParallelDegree { get; set; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// 连接超时时间(秒)
|
||||
/// </summary>
|
||||
public int ConnectTimeoutSeconds { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,5 +423,52 @@ namespace RIZO.Admin.WebApi.PLC.Model
|
||||
[SugarColumn(ColumnName = "Screw7TightenTime")]
|
||||
public string ChipSN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品1SN
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product1SN")]
|
||||
public string Product1SN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品2SN
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product2SN")]
|
||||
public string Product2SN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品3SN
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product3SN")]
|
||||
public string Product3SN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品4SN
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product4SN")]
|
||||
public string Product4SN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品1结果- 1:OK 2:NG
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product1Result")]
|
||||
public string Product1Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品2结果- 1:OK 2:NG
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product2Result")]
|
||||
public string Product2Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品3结果- 1:OK 2:NG
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product3Result")]
|
||||
public string Product3Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品4结果- 1:OK 2:NG
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Product4Result")]
|
||||
public string Product4Result { get; set; }
|
||||
}
|
||||
}
|
||||
@ -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<PlcHostedService> _logger;
|
||||
private readonly PlcService _plcService;
|
||||
private readonly List<PlcConfig> _plcConfigs;
|
||||
private Timer _timer;
|
||||
private bool _isRunning;
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private readonly object _timerLock = new object();
|
||||
// 连接状态缓存:减少短时间内重复连接测试
|
||||
private readonly ConcurrentDictionary<string, PlcConnectionState> _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<string, SemaphoreSlim> _plcSemaphores = new();
|
||||
|
||||
private PlantWorkstationService plantWorkstationService = new PlantWorkstationService();
|
||||
// 2. 连接状态增强(分级+失败次数+最后请求时间)
|
||||
private readonly ConcurrentDictionary<string, PlcConnectionStatus> _connectionStatusCache = new();
|
||||
|
||||
// 3. 配置刷新
|
||||
private Timer _configRefreshTimer;
|
||||
private readonly SemaphoreSlim _configRefreshLock = new(1, 1);
|
||||
|
||||
// 基础配置(配置驱动+热更新)
|
||||
private readonly IOptionsMonitor<PlcPollingSettings> _pollingSettingsMonitor;
|
||||
private PlcPollingSettings _currentSettings;
|
||||
private SemaphoreSlim _globalSemaphore;
|
||||
private readonly SemaphoreSlim _timerAsyncLock = new(1, 1);
|
||||
|
||||
// PLC配置列表(支持动态刷新)
|
||||
private List<PlcConfig> _plcConfigs = new();
|
||||
private PlantWorkstationService _plantWorkstationService;
|
||||
|
||||
/// <summary>
|
||||
/// PLC 连接状态缓存对象
|
||||
/// 增强版PLC连接状态
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接状态分级
|
||||
/// </summary>
|
||||
private enum ConnectionLevel
|
||||
{
|
||||
Disconnected = 0, // 断开
|
||||
Weak = 1, // 弱连接(偶尔失败)
|
||||
Normal = 2 // 正常连接
|
||||
}
|
||||
|
||||
public PlcHostedService(
|
||||
ILogger<PlcHostedService> logger,
|
||||
PlcService plcService)
|
||||
PlcService plcService,
|
||||
IOptionsMonitor<PlcPollingSettings> 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<string, PlcConnectionState>();
|
||||
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<PlcConfig> initPlcConfigs(List<PlcConfig> result)
|
||||
/// <summary>
|
||||
/// 动态刷新PLC配置(支持热更新)
|
||||
/// </summary>
|
||||
private async Task RefreshPlcConfigsAsync()
|
||||
{
|
||||
var defaultResult = result ?? new List<PlcConfig>();
|
||||
if (!await _configRefreshLock.WaitAsync(0)) return;
|
||||
|
||||
try
|
||||
{
|
||||
List<PlcConfig> 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<PlcConfig>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写BackgroundService的ExecuteAsync(替代StartAsync)
|
||||
/// 重写BackgroundService的ExecuteAsync
|
||||
/// </summary>
|
||||
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为异步锁版本
|
||||
/// <summary>
|
||||
/// 优化版定时器回调(动态调整轮询频率)
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动时批量并行连接PLC
|
||||
/// 动态调整定时器轮询频率
|
||||
/// </summary>
|
||||
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, "调整轮询频率失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动时批量并行连接PLC(带指数退避重试)
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 并行轮询PLC数据(读取前先验证连接状态,修复传参/日志/数据处理问题)
|
||||
/// 优化版并行轮询PLC数据(IP隔离+动态频率+异常闭环)
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理PLC生产数据(示例方法:可根据业务扩展)
|
||||
/// 重新检查PLC连接状态
|
||||
/// </summary>
|
||||
private async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 失败时更新连接状态
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理PLC生产数据(可扩展)
|
||||
/// </summary>
|
||||
/// <param name="config">PLC配置</param>
|
||||
/// <param name="prodData">生产数据</param>
|
||||
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}");
|
||||
// 数据处理失败不影响轮询,仅记录日志
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写停止逻辑(更安全)
|
||||
/// 重写停止逻辑
|
||||
/// </summary>
|
||||
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后台监听服务已停止");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源释放(完整实现IDisposable)
|
||||
/// 完整释放资源
|
||||
/// </summary>
|
||||
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后台监听服务已释放所有资源");
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,55 @@ namespace RIZO.Admin.WebApi.PLC.Service
|
||||
{ "总结果", "DB1001.DBW2158" }, // Int - 一个托盘上传四次结果
|
||||
//{ "节拍时间", "DB1001.DBD2988" } // Real
|
||||
};
|
||||
// OP070-1 专属地址映射
|
||||
|
||||
// OP020-3 专属地址映射(热铆工位,DB1001)
|
||||
private readonly Dictionary<string, string> _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<string, (string Addr, int Len)> _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<string, string> _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<string, (string Addr, int Len)> _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<string, (string Addr, int Len)> _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<string, (string Addr, int Len)> _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
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 读取OP020-3数据(热铆工位)
|
||||
/// </summary>
|
||||
private async Task<PlcProductionData?> 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,符合可空类型设计
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// OP070-1数据读取
|
||||
/// </summary>
|
||||
private async Task<PlcProductionData> ReadOP070_1DataAsync(Plc plc, string ip,string workstationCode)
|
||||
|
||||
@ -34,10 +34,12 @@ builder.Services.AddControllers();
|
||||
// ===== 新增PLC服务注册 =====
|
||||
builder.Services.Configure<List<PlcConfig>>(builder.Configuration.GetSection("PlcConfigs"));
|
||||
builder.Services.Configure<GlobalPlcConfig>(builder.Configuration.GetSection("GlobalPlcConfig"));
|
||||
// 注册PLC轮询配置
|
||||
builder.Services.Configure<PlcPollingSettings>(builder.Configuration.GetSection("PlcPollingSettings"));
|
||||
builder.Services.AddSingleton<PlcService>();
|
||||
builder.Services.AddScoped<IPlcProductionDataService, PlcProductionDataService>();
|
||||
// 新增:注册PLC后台监听服务(项目启动自动执行)
|
||||
//builder.Services.AddHostedService<PlcHostedService>();
|
||||
builder.Services.AddHostedService<PlcHostedService>();
|
||||
// ==========================
|
||||
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user