using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RIZO.Model.PLC; using RIZO.Service.PLC.IService; using S7.Net; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace RIZO.Service.PLC { /// /// BackgroundService是一个抽象基类,用于创建长时间运行的后台任务 /// public class PlcDataService : BackgroundService, IPlcDataService { private readonly ILogger _logger; private readonly List _plcConfigs; //这行代码创建了一个线程安全的字典,专门用于存储和管理多个工站的数据。 初始化后值不能再改变(引用不能变,但对象内容可以变) private readonly ConcurrentDictionary _stationData = new(); //全局互斥锁 同一时间只能有一个线程/任务访问受保护的资源。 private readonly SemaphoreSlim _globalConnectionLock = new(1, 1); private CancellationTokenSource _shutdownTokenSource = new(); public event Action? OnDataUpdated; public event Action? OnError; public PlcDataService( ILogger logger, IOptions> plcConfigs) { _logger = logger; _plcConfigs = plcConfigs.Value; } /// /// 后台任务初始化的时候会被调用 /// /// /// /// 当应用程序停止时(如 Ctrl+C、Docker 停止、系统关机),stoppingToken会被触发,让后台服务有机会清理资源并正常退出 protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("PLC数据服务启动 - S7NetPlus v0.20.0"); _shutdownTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); var token = _shutdownTokenSource.Token; // 初始化所有工站连接 await InitializeAllStationsAsync(token); // 为每个工站创建独立的读取任务(相当于每个站一个线程) Task[] readTasks = _plcConfigs.Select(config => ReadStationDataLoop(config, token)).ToArray(); _logger.LogInformation($"启动了 {readTasks.Length} 个工站读取任务"); // 等待所有任务完成或取消 await Task.WhenAny(Task.WhenAll(readTasks), Task.Delay(Timeout.Infinite, token)); } #region 初始化代码块 /// /// // 初始化所有工站连接 /// /// /// private async Task InitializeAllStationsAsync(CancellationToken cancellationToken) { // 并行初始化每个工站 Func var initTasks = _plcConfigs.Select(async config => { try { await InitializeStationAsync(config, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, $"初始化工位 {config.StationName} 时发生异常"); } }); await Task.WhenAll(initTasks); } private async Task InitializeStationAsync(PlcConfig config, CancellationToken cancellationToken) { _logger.LogInformation($"正在初始化工位: {config.StationName} ({config.IpAddress})"); try { //异步锁 await _globalConnectionLock.WaitAsync(cancellationToken); try { // 创建PLC连接实例 using var plc = new Plc(CpuType.S71200, config.IpAddress, config.Rack, config.Slot); // 测试连接 (0.20.0 版本使用 OpenAsync/CloseAsync) var isConnected = await TestConnectionWithRetryAsync(plc, config, cancellationToken); if (!isConnected) { _logger.LogError($"无法连接到工位 {config.StationName} ({config.IpAddress})"); return; } // 工作站取到的数据 var stationData = new StationData { StationName = config.StationName, PlcConnection = plc, IsConnected = true, LastReadTime = DateTime.Now, ReadFailureCount = 0 }; // 初始化数据项 foreach (var itemConfig in config.DataItems) { stationData.DataItems[itemConfig.Name] = new PlcDataItem { Name = itemConfig.Name, Value = null, LastUpdateTime = DateTime.MinValue, IsSuccess = false, ErrorMessage = "尚未读取" }; } _stationData[config.StationName] = stationData; _logger.LogInformation($"✅ 工位 {config.StationName} 初始化成功,配置了 {config.DataItems.Count} 个数据项"); } finally { _globalConnectionLock.Release(); } } catch (Exception ex) { _logger.LogError(ex, $"❌ 初始化工位 {config.StationName} 失败"); } } /// /// 测试 PLC 连接(带重试机制和超时控制) /// 使用指数退避策略进行多次连接尝试,确保连接的可靠性 /// /// 要测试的 PLC 实例 /// PLC 配置信息(包含工位名称等信息) /// 取消令牌,用于支持外部取消操作 /// 最大重试次数,默认为 3 次 /// /// 连接测试成功返回 true,所有重试都失败返回 false /// private async Task TestConnectionWithRetryAsync(Plc plc, PlcConfig config, CancellationToken cancellationToken, int maxRetries = 3) { for (int attempt = 1; attempt <= maxRetries; attempt++) { try { _logger.LogDebug($"测试工位 {config.StationName} 连接 (尝试 {attempt}/{maxRetries})"); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(10)); // 0.20.0 版本的异步连接测试 await plc.OpenAsync().WaitAsync(timeoutCts.Token); bool isConnected = plc.IsConnected; // await plc.CloseAsync(); if (isConnected) { _logger.LogDebug($"✅ 工位 {config.StationName} 连接测试成功"); return true; } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { _logger.LogWarning(ex, $"工位 {config.StationName} 连接测试失败 (尝试 {attempt}/{maxRetries})"); if (attempt < maxRetries) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken); // 指数退避 } } } return false; } /// /// 持续读取指定工位的数据,具备连接监控、自动重连、错误恢复能力 /// 这是一个长期运行的后台任务,直到收到取消信号才停止 /// /// 某个工站plc配置 /// /// private async Task ReadStationDataLoop(PlcConfig config, CancellationToken cancellationToken) { _logger.LogInformation($"开始读取工位 {config.StationName} 数据 (间隔: {config.ReadIntervalMs}ms)"); // 获取之前工站数据存储对象 var stationData = _stationData[config.StationName]; // 连续失败计数器 var consecutiveFailures = 0; // 最大容忍连续失败次数 const int maxConsecutiveFailures = 5; while (!cancellationToken.IsCancellationRequested) { try { if (!stationData.IsConnected || stationData.PlcConnection == null) { _logger.LogWarning($"工位 {config.StationName} 连接断开,尝试重连..."); await ReconnectStationInternalAsync(config, stationData, cancellationToken); if (!stationData.IsConnected) { await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); continue; } } // 读取所有数据项 await ReadAllDataItemsAsync(config, stationData, cancellationToken); stationData.LastReadTime = DateTime.Now; // stationData.ReadFailureCount = 0; consecutiveFailures = 0; // 等待下次读取 await Task.Delay(config.ReadIntervalMs, cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { _logger.LogInformation($"工位 {config.StationName} 读取循环被取消"); break; } catch (Exception ex) { consecutiveFailures++; // stationData.ReadFailureCount++; stationData.IsConnected = false; _logger.LogError(ex, $"读取工位 {config.StationName} 数据时发生错误 (连续失败: {consecutiveFailures})"); OnError?.Invoke(config.StationName, "ReadLoop", ex); if (consecutiveFailures >= maxConsecutiveFailures) { _logger.LogError($"工位 {config.StationName} 连续失败 {maxConsecutiveFailures} 次,暂停读取"); await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); consecutiveFailures = 0; } else { await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); } } } // 清理资源 await CleanupStationAsync(config.StationName); _logger.LogInformation($" 工位 {config.StationName} 读取循环结束"); } private async Task CleanupStationAsync(string stationName) { try { if (_stationData.TryRemove(stationName, out var stationData)) { if (stationData.PlcConnection != null) { try { stationData.PlcConnection.Close(); } catch { } //try { stationData.PlcConnection.Dispose(); } catch { } } _logger.LogInformation($"工位 {stationName} 资源清理完成"); } } catch (Exception ex) { _logger.LogError(ex, $"清理工位 {stationName} 资源时发生错误"); } } private async Task ReadAllDataItemsAsync(PlcConfig config, StationData stationData, CancellationToken cancellationToken) { var readTasks = stationData.DataItems.Values.Select(dataItem => ReadDataItemAsync(config, stationData, dataItem, cancellationToken)).ToArray(); await Task.WhenAll(readTasks); } /// /// 读取单个数据项的值,更新状态并触发事件 /// /// /// /// /// /// private async Task ReadDataItemAsync(PlcConfig config, StationData stationData, PlcDataItem dataItem, CancellationToken cancellationToken) { try { if (stationData.PlcConnection == null || !stationData.PlcConnection.IsConnected) throw new InvalidOperationException("PLC连接未就绪"); var dataItemConfig = config.DataItems.FirstOrDefault(x => x.Name == dataItem.Name); if (dataItemConfig == null) return; // 确保连接打开 (0.20.0 异步操作) if (!stationData.PlcConnection.IsConnected) { await stationData.PlcConnection.OpenAsync().WaitAsync(cancellationToken); } // 0.20.0 版本的异步读取 //DataType用于指定要读取 PLC 中哪个内存区域的数据,而 VarType用于表示 PLC 中变量的具体数据类型 var result = await stationData.PlcConnection.ReadAsync( dataItemConfig.DataType, dataItemConfig.DB, dataItemConfig.StartByteAdr, dataItemConfig.VarType, // 这是第四个参数 dataItemConfig.VarType == VarType.String ? 255 : 1, // 这是第五个参数 - varCount cancellationToken: cancellationToken // 可选:如果你想在这里传递取消令牌 ).WaitAsync(TimeSpan.FromSeconds(5), cancellationToken); if (result != null) { var oldValue = dataItem.Value; dataItem.Value = result; dataItem.LastUpdateTime = DateTime.Now; dataItem.IsSuccess = true; dataItem.ErrorMessage = string.Empty; // 触发数据更新事件(只有值变化时才触发) if (!Equals(oldValue, result)) { OnDataUpdated?.Invoke(config.StationName, dataItem.Name, result); _logger.LogTrace($"[{config.StationName}] {dataItem.Name} = {result}"); } } } catch (Exception ex) { dataItem.IsSuccess = false; dataItem.ErrorMessage = ex.Message; OnError?.Invoke(config.StationName, dataItem.Name, ex); _logger.LogWarning(ex, $"读取工位 {config.StationName} 数据项 {dataItem.Name} 失败"); } } /// /// 内部重连方法:清理旧连接、创建新连接、验证连接可用性 /// 是连接故障恢复机制的核心实现,确保工位的持续可用性 /// private async Task ReconnectStationInternalAsync(PlcConfig config, StationData stationData, CancellationToken cancellationToken) { try { _logger.LogInformation($"尝试重新连接工位 {config.StationName}"); // 清理现有连接 if (stationData.PlcConnection != null) { try { stationData.PlcConnection.Close(); } catch { } // try { stationData.PlcConnection.Dispose(); } catch { } } // 创建新连接 var newPlc = new Plc(CpuType.S71200, config.IpAddress, config.Rack, config.Slot); var isConnected = await TestConnectionWithRetryAsync(newPlc, config, cancellationToken); if (isConnected) { stationData.PlcConnection = newPlc; stationData.IsConnected = true; stationData.LastConnectTime = DateTime.Now; stationData.ReadFailureCount = 0; _logger.LogInformation($" 工位 {config.StationName} 重新连接成功"); } else { newPlc.Close(); stationData.IsConnected = false; _logger.LogWarning($"❌ 工位 {config.StationName} 重新连接失败"); } } catch (Exception ex) { stationData.IsConnected = false; _logger.LogError(ex, $"工位 {config.StationName} 重连过程中出错"); } } #endregion #region 外部调用接口代码块 public async Task ReconnectStationAsync(string stationName) { if (!_stationData.ContainsKey(stationName)) return false; var config = _plcConfigs.FirstOrDefault(c => c.StationName == stationName); var stationData = _stationData[stationName]; await ReconnectStationInternalAsync(config!, stationData, CancellationToken.None); return stationData.IsConnected; } public StationData? GetStationData(string stationName) { _stationData.TryGetValue(stationName, out var data); return data; } // 获取所有工位的数据快照 public Dictionary GetAllStationData() { return _stationData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } public async Task WriteDataAsync(string stationName, string dataItemName, object value) { try { var stationData = GetStationData(stationName); if (stationData == null || !stationData.IsConnected || stationData.PlcConnection == null) return false; var config = _plcConfigs.FirstOrDefault(c => c.StationName == stationName); var dataItemConfig = config?.DataItems.FirstOrDefault(d => d.Name == dataItemName); if (config == null || dataItemConfig == null) return false; // 确保连接打开 if (!stationData.PlcConnection.IsConnected) { await stationData.PlcConnection.OpenAsync(); } // 0.20.0 版本的异步写入 await stationData.PlcConnection.WriteAsync( dataItemConfig.DataType, dataItemConfig.DB, dataItemConfig.StartByteAdr, value ); _logger.LogInformation($" 向工位 {stationName} 写入数据 {dataItemName} = {value}"); return true; } catch (Exception ex) { _logger.LogError(ex, $"写入工位 {stationName} 数据项 {dataItemName} 失败"); return false; } } public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation(" PLC数据服务停止中..."); // 取消所有操作 await _shutdownTokenSource.CancelAsync(); // 清理所有连接 var cleanupTasks = _stationData.Keys.Select(CleanupStationAsync); await Task.WhenAll(cleanupTasks); _stationData.Clear(); _shutdownTokenSource.Dispose(); await base.StopAsync(cancellationToken); _logger.LogInformation(" PLC数据服务已停止"); } public void Dispose() { StopAsync(CancellationToken.None).Wait(); _shutdownTokenSource?.Dispose(); _globalConnectionLock?.Dispose(); GC.SuppressFinalize(this); } #endregion } }