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