using System.Collections.Concurrent; using System.Diagnostics; using DMS.Application.DTOs; using DMS.Application.Events; using DMS.Application.Interfaces; using DMS.Application.Models; using DMS.Core.Enums; using DMS.Core.Events; using DMS.Core.Models; using DMS.Infrastructure.Interfaces.Services; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using DateTime = System.DateTime; namespace DMS.Infrastructure.Services.S7; /// /// 优化的S7后台服务,继承自BackgroundService,用于在后台高效地轮询S7 PLC设备数据。 /// public class OptimizedS7BackgroundService : BackgroundService { private readonly IAppCenterService _appCenterService; private readonly IAppStorageService _appStorageService; private readonly IEventService _eventService; private readonly IDataProcessingService _dataProcessingService; private readonly IS7ServiceManager _s7ServiceManager; private readonly ILogger _logger; private readonly SemaphoreSlim _reloadSemaphore = new SemaphoreSlim(0); // S7轮询一遍后的等待时间 private readonly int _s7PollOnceSleepTimeMs = 50; // 存储每个设备的变量按轮询间隔分组 private readonly ConcurrentDictionary>> _variablesByPollingInterval = new(); /// /// 构造函数,注入数据服务和数据处理服务。 /// public OptimizedS7BackgroundService( IAppCenterService appCenterService, IAppStorageService appStorageService, IEventService eventService, IDataProcessingService dataProcessingService, IS7ServiceManager s7ServiceManager, ILogger logger) { _appCenterService = appCenterService; _appStorageService = appStorageService; _eventService = eventService; _dataProcessingService = dataProcessingService; _s7ServiceManager = s7ServiceManager; _logger = logger; _eventService.OnLoadDataCompleted += OnLoadDataCompleted; } private void OnLoadDataCompleted(object? sender, DataLoadCompletedEventArgs e) { _reloadSemaphore.Release(); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("优化的S7后台服务正在启动。"); try { while (!stoppingToken.IsCancellationRequested) { await _reloadSemaphore.WaitAsync(stoppingToken); // Wait for a reload signal if (stoppingToken.IsCancellationRequested) { break; } if (_appStorageService.Devices.IsEmpty) { _logger.LogInformation("没有可用的S7设备,等待设备列表更新..."); continue; } var isLoaded = LoadVariables(); if (!isLoaded) { _logger.LogInformation("加载变量过程中发生了错误,停止后面的操作。"); continue; } await ConnectS7ServiceAsync(stoppingToken); _logger.LogInformation("优化的S7后台服务已启动。"); // 持续轮询,直到取消请求或需要重新加载 while (!stoppingToken.IsCancellationRequested && _reloadSemaphore.CurrentCount == 0) { await PollS7VariablesAsync(stoppingToken); await Task.Delay(_s7PollOnceSleepTimeMs, stoppingToken); } } } catch (OperationCanceledException) { _logger.LogInformation("优化的S7后台服务已停止。"); } catch (Exception e) { _logger.LogError(e, $"优化的S7后台服务运行中发生了错误:{e.Message}"); } finally { await DisconnectAllS7SessionsAsync(); } } /// /// 从数据库加载所有活动的 S7 变量,并按轮询级别进行分组。 /// private bool LoadVariables() { try { _variablesByPollingInterval.Clear(); _logger.LogInformation("开始加载S7变量...."); var s7Devices = _appStorageService .Devices.Values.Where(d => d.Protocol == ProtocolType.S7 && d.IsActive == true) .ToList(); foreach (var s7Device in s7Devices) { _s7ServiceManager.AddDevice(s7Device); // 查找设备中所有要轮询的变量 var variables = new List(); foreach (var variableTable in s7Device.VariableTables) { if (variableTable.IsActive && variableTable.Protocol == ProtocolType.S7) { variables.AddRange(variableTable.Variables.Where(v => v.IsActive)); } } _s7ServiceManager.UpdateVariables(s7Device.Id, variables); } _logger.LogInformation($"S7 变量加载成功,共加载S7设备:{s7Devices.Count}个"); return true; } catch (Exception e) { _logger.LogError(e, $"加载S7变量的过程中发生了错误:{e.Message}"); return false; } } /// /// 连接到 S7 服务器 /// private async Task ConnectS7ServiceAsync(CancellationToken stoppingToken) { var s7Devices = _appStorageService .Devices.Values.Where(d => d.Protocol == ProtocolType.S7 && d.IsActive == true) .ToList(); var deviceIds = s7Devices.Select(d => d.Id) .ToList(); await _s7ServiceManager.ConnectDevicesAsync(deviceIds, stoppingToken); _logger.LogInformation("已连接所有S7设备"); } /// /// 按轮询间隔轮询S7变量 /// private async Task PollS7VariablesAsync(CancellationToken stoppingToken) { try { var s7DeviceContexts = _s7ServiceManager.GetAllDeviceContexts(); foreach (var context in s7DeviceContexts) { if (stoppingToken.IsCancellationRequested) break; // 收集该设备所有需要轮询的变量 var variablesToPoll = context.Variables.Values.ToList(); if (variablesToPoll.Any()) { await PollVariablesForDeviceAsync(context, variablesToPoll, stoppingToken); } } } catch (Exception ex) { _logger.LogError(ex, $"按轮询间隔轮询S7变量时发生错误:{ex.Message}"); } } /// /// 轮询设备的变量 /// private async Task PollVariablesForDeviceAsync(S7DeviceContext context, List variables, CancellationToken stoppingToken) { if (!_appStorageService.Devices.TryGetValue(context.Device.Id, out var device)) { _logger.LogWarning($"轮询时没有找到设备ID:{context.Device.Id}"); return; } var s7Service = context.S7Service; if (s7Service == null || !s7Service.IsConnected) { _logger.LogWarning($"轮询时设备 {device.Name} 没有连接或服务不可用"); return; } try { var stopwatch = Stopwatch.StartNew(); // 按地址分组变量以进行批量读取 var addresses = variables.Where(v=>(DateTime.Now-v.UpdatedAt)>=TimeSpan.FromMilliseconds(v.PollingInterval)).Select(v => v.S7Address) .ToList(); if (!addresses.Any()) { return; } // 批量读取变量值 var readResults = await s7Service.ReadVariablesAsync(addresses); stopwatch.Stop(); _logger.LogDebug($"设备 {device.Name} 轮询 {variables.Count} 个变量耗时 {stopwatch.ElapsedMilliseconds} ms"); // 更新变量值并推送到处理队列 foreach (var variable in variables) { if (readResults.TryGetValue(variable.S7Address, out var value)) { // 将更新后的数据推入处理队列。 await _dataProcessingService.EnqueueAsync(new VariableContext(variable, value?.ToString())); } // else // { // _logger.LogWarning($"未能从设备 {device.Name} 读取变量 {variable.S7Address} 的值"); // } } } catch (Exception ex) { _logger.LogError(ex, $"轮询设备 {device.Name} 的变量时发生错误:{ex.Message}"); } } /// /// 断开所有 S7 会话。 /// private async Task DisconnectAllS7SessionsAsync() { _logger.LogInformation("正在断开所有 S7 会话..."); var deviceIds = _s7ServiceManager.GetMonitoredDeviceIds(); await _s7ServiceManager.DisconnectDevicesAsync(deviceIds); _logger.LogInformation("已断开所有 S7 会话"); } }