plc 异步锁连接

This commit is contained in:
gcw_MV9p2JJN 2026-01-25 09:45:29 +08:00
parent 42e18b5af1
commit ff1dd97316
12 changed files with 1215 additions and 5 deletions

View File

@ -0,0 +1,181 @@
using Microsoft.AspNetCore.Mvc;
using RIZO.Model.PLC;
using RIZO.Service.PLC.IService;
namespace RIZO.Admin.WebApi.Controllers.Plc
{
[ApiController]
[Route("api/[controller]")]
public class PlcDataController : ControllerBase
{
private readonly IPlcDataService _plcDataService;
private readonly ILogger<PlcDataController> _logger;
public PlcDataController(
IPlcDataService plcDataService,
ILogger<PlcDataController> logger)
{
_plcDataService = plcDataService;
_logger = logger;
}
/// <summary>
/// 获取所有工站数据概览
/// </summary>
[HttpGet]
public ActionResult<AllStationsDataDto> GetAllStationsData()
{
try
{
var allData = _plcDataService.GetAllStationData();
var connectedCount = allData.Count(s => s.Value.IsConnected);
var result = new AllStationsDataDto
{
Stations = allData.ToDictionary(
kvp => kvp.Key,
kvp => new StationDataDto
{
StationName = kvp.Value.StationName,
IsConnected = kvp.Value.IsConnected,
LastUpdateTime = kvp.Value.LastReadTime,
LastConnectTime = kvp.Value.LastConnectTime,
ReadFailureCount = kvp.Value.ReadFailureCount,
CurrentValues = kvp.Value.DataItems.ToDictionary(
di => di.Key,
di => di.Value.Value
)
}
),
ServerTime = DateTime.Now,
TotalStations = allData.Count,
ConnectedStations = connectedCount
};
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取所有工站数据失败");
return StatusCode(500, new { error = "获取数据失败" });
}
}
/// <summary>
/// 获取指定工站详细数据
/// </summary>
[HttpGet("{stationName}")]
public ActionResult<StationDataDto> GetStationData(string stationName)
{
try
{
var stationData = _plcDataService.GetStationData(stationName);
if (stationData == null)
return NotFound(new { error = $"未找到工位: {stationName}" });
var result = new StationDataDto
{
StationName = stationData.StationName,
IsConnected = stationData.IsConnected,
LastUpdateTime = stationData.LastReadTime,
LastConnectTime = stationData.LastConnectTime,
ReadFailureCount = stationData.ReadFailureCount,
CurrentValues = stationData.DataItems.ToDictionary(
di => di.Key,
di => di.Value.Value
)
};
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, $"获取工位 {stationName} 数据失败");
return StatusCode(500, new { error = "获取数据失败" });
}
}
/// <summary>
/// 获取指定数据项的详细信息
/// </summary>
[HttpGet("{stationName}/data/{dataItemName}")]
public ActionResult<object> GetDataItemDetail(string stationName, string dataItemName)
{
try
{
var stationData = _plcDataService.GetStationData(stationName);
if (stationData == null)
return NotFound(new { error = $"未找到工位: {stationName}" });
if (!stationData.DataItems.ContainsKey(dataItemName))
return NotFound(new { error = $"未找到数据项: {dataItemName}" });
var dataItem = stationData.DataItems[dataItemName];
return Ok(new
{
Name = dataItem.Name,
Value = dataItem.Value,
LastUpdateTime = dataItem.LastUpdateTime,
IsSuccess = dataItem.IsSuccess,
ErrorMessage = dataItem.ErrorMessage
});
}
catch (Exception ex)
{
_logger.LogError(ex, $"获取工位 {stationName} 数据项 {dataItemName} 失败");
return StatusCode(500, new { error = "获取数据失败" });
}
}
/// <summary>
/// 手动重连指定工站
/// </summary>
[HttpPost("{stationName}/reconnect")]
public async Task<IActionResult> ReconnectStation(string stationName)
{
try
{
var success = await _plcDataService.ReconnectStationAsync(stationName);
if (success)
return Ok(new { message = $"工位 {stationName} 重连成功" });
else
return BadRequest(new { error = $"工位 {stationName} 重连失败" });
}
catch (Exception ex)
{
_logger.LogError(ex, $"重连工位 {stationName} 失败");
return StatusCode(500, new { error = "重连失败" });
}
}
/// <summary>
/// 写入数据到PLC
/// </summary>
[HttpPost("{stationName}/write")]
public async Task<IActionResult> WriteData(string stationName, [FromBody] WriteDataRequest request)
{
try
{
if (string.IsNullOrEmpty(request.DataItemName))
return BadRequest(new { error = "数据项名称不能为空" });
var success = await _plcDataService.WriteDataAsync(stationName, request.DataItemName, request.Value);
if (success)
return Ok(new { message = "写入成功" });
else
return BadRequest(new { error = "写入失败" });
}
catch (Exception ex)
{
_logger.LogError(ex, $"写入工位 {stationName} 数据失败");
return StatusCode(500, new { error = "写入失败" });
}
}
}
public class WriteDataRequest
{
public string DataItemName { get; set; } = string.Empty;
public object Value { get; set; } = new();
}
}

View File

@ -3,6 +3,7 @@ using Infrastructure.Converter;
using MDM;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options;
using NLog.Web;
using RIZO.Admin.WebApi.Extensions;
using RIZO.Admin.WebApi.PLC;
@ -13,8 +14,13 @@ using RIZO.Common.Cache;
using RIZO.Common.DynamicApiSimple.Extens;
using RIZO.Infrastructure.WebExtensions;
using RIZO.Mall;
using RIZO.Model.PLC;
using RIZO.Service.PLC;
using RIZO.Service.PLC.IService;
using RIZO.Service.PLCBackground;
using RIZO.ServiceCore.Signalr;
using RIZO.ServiceCore.SqlSugar;
using S7.Net;
using SqlSugar;
using System.Globalization;
using System.Text.Json;
@ -30,10 +36,10 @@ builder.Services.AddDynamicApi();
builder.Services.AddControllers();
// ===== 新增PLC服务注册 =====
builder.Services.Configure<List<PlcConfig>>(builder.Configuration.GetSection("PlcConfigs"));
builder.Services.Configure<GlobalPlcConfig>(builder.Configuration.GetSection("GlobalPlcConfig"));
builder.Services.AddSingleton<PlcService>();
builder.Services.AddScoped<IPlcProductionDataService, PlcProductionDataService>();
//builder.Services.Configure<List<PlcConfig>>(builder.Configuration.GetSection("PlcConfigs"));
//builder.Services.Configure<GlobalPlcConfig>(builder.Configuration.GetSection("GlobalPlcConfig"));
//builder.Services.AddSingleton<PlcService>();
//builder.Services.AddScoped<IPlcProductionDataService, PlcProductionDataService>();
// 新增注册PLC后台监听服务项目启动自动执行
builder.Services.AddHostedService<PlcHostedService>();
// ==========================
@ -103,6 +109,48 @@ builder.Services.AddSwaggerConfig();
builder.Services.AddLogo();
// 添加本地化服务
builder.Services.AddLocalization(options => options.ResourcesPath = "");
// 配置PLC设置
//builder.Services.Configure<List<RIZO.Model.PLC.PlcConfig>>(builder.Configuration.GetSection("PlcSettings:Stations"));
//// 注册PLC数据服务
//builder.Services.AddSingleton<IPlcDataService, PlcDataService>();
//builder.Services.AddHostedService(provider => provider.GetService<IPlcDataService>() as BackgroundService);
//builder.Services.Configure<List<PlcConnectionConfig>>(builder.Configuration.GetSection("PlcSettingss:Connections"));
//builder.Services.AddSingleton<RIZO.Service.PLC.PlcService2>(sp =>
//{
// var configs = sp.GetRequiredService<IOptions<List<PlcConnectionConfig>>>().Value;
// var logger = sp.GetRequiredService<ILogger<RIZO.Service.PLC.PlcService2>>();
// var service = new RIZO.Service.PLC.PlcService2(logger);
// foreach (var config in configs)
// {
// service.AddPlc(
// config.PlcId,
// ParseCpuType(config.CpuType),
// config.Ip,
// config.Rack,
// config.Slot
// );
// }
// // 后台启动连接
// _ = service.ConnectAllAsync();
// return service;
//});
//// CPU类型转换辅助方法
//static CpuType ParseCpuType(string type)
//{
// return Enum.Parse<CpuType>(type.Replace("-", "")); // "S71200" -> "S71200"
//}
// 配置服务
// 注册PLC服务单例模式
// 注册后台任务服务
builder.Services.AddHostedService<PlcPollingServiceOP72>();
// 在应用程序启动的最开始处调用
var app = builder.Build();
InternalApp.ServiceProvider = app.Services;

View File

@ -27,7 +27,6 @@
<ItemGroup>
<PackageReference Include="Lazy.Captcha.Core" Version="2.0.9" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="S7netplus" Version="0.20.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.12" />
<PackageReference Include="NLog.Web.AspNetCore" Version="6.0.4" />

View File

@ -99,6 +99,53 @@
"GlobalConfig": {
"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
}
]
}
}

View File

@ -0,0 +1,76 @@
using S7.Net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RIZO.Model.PLC
{
public class PlcConfig
{
/// <summary>
/// 工站名称/工位标识符
/// </summary>
public string StationName { get; set; }
/// <summary>
/// PLC设备的IP地址
/// </summary>
public string IpAddress { get; set; }
/// <summary>
/// S7协议中标识CPU所在的物理机架
/// </summary>
public short Rack { get; set; } = 0;
/// <summary>
/// S7协议中标识CPU在机架中的具体插槽
/// </summary>
public short Slot { get; set; } = 1;
/// <summary>
/// 数据读取间隔时间(毫秒)
/// </summary>
public int ReadIntervalMs { get; set; } = 1000;
/// <summary>
/// 定义要从PLC读取的具体数据点和属性
/// </summary>
public List<DataItemConfig> DataItems { get; set; } = new();
}
/// <summary>
/// 定义要从PLC读取的具体数据点和属性
/// </summary>
public class DataItemConfig
{
/// <summary>
/// 数据项的逻辑名称/别名 温度
/// </summary>
public string Name { get; set; }
/// <summary>
/// 地址 "DB1.DBD0"- DB块1双字Double Word起始字节0
/// </summary>
public string Address { get; set; }
public S7.Net.DataType DataType { get; set; }
/// <summary>
/// 含义: S7.Net库中定义的变量数据类型
/// 作用: 告诉库如何解析从PLC读取的原始字节数据
/// </summary>
public VarType VarType { get; set; }
/// <summary>
/// 指定数据存储在哪个DB块中
/// </summary>
public int DB { get; set; }
/// <summary>
/// 数据在DB块中的起始字节地址
/// </summary>
public int StartByteAdr { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RIZO.Model.PLC
{
public class PlcConnectionConfig
{
public string PlcId { get; set; }
public string CpuType { get; set; } // S71200/S71500
public string Ip { get; set; }
public int Rack { get; set; }
public int Slot { get; set; }
}
}

View File

@ -0,0 +1,116 @@
using S7.Net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RIZO.Model.PLC
{
/// <summary>
/// 单个数据点模型
/// </summary>
public class PlcDataItem
{
/// <summary>
/// 数据项的唯一标识符 "产品计数", "温度值", "电机状态"
/// </summary>
public string Name { get; set; }
/// <summary>
/// 存储PLC返回的原始数据类型根据实际VarType动态变化
/// </summary>
public object Value { get; set; }
/// <summary>
/// 最后一次成功读取数据的时间戳
/// </summary>
public DateTime LastUpdateTime { get; set; }
/// <summary>
/// 最近一次读取操作是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 读取失败时保存的错误信息
/// </summary>
public string ErrorMessage { get; set; }
}
/// <summary>
/// 工站完整数据模型
/// </summary>
public class StationData
{
/// <summary>
/// 工站的唯一标识符
/// </summary>
public string StationName { get; set; }
/// <summary>
/// 该工站所有数据项的集合
/// </summary>
public Dictionary<string, PlcDataItem> DataItems { get; set; } = new();
/// <summary>
/// S7.Net库的PLC连接实例
/// </summary>
public Plc PlcConnection { get; set; }
/// <summary>
/// 当前PLC连接状态
/// </summary>
public bool IsConnected { get; set; }
/// <summary>
/// 最后一次成功读取所有数据项的时间
/// </summary>
public DateTime LastReadTime { get; set; }
public DateTime LastConnectTime { get; set; }
public int ReadFailureCount { get; set; }
}
// API响应DTO
public class StationDataDto
{
/// <summary>
/// 工站名称(直接传递)
/// </summary>
public string StationName { get; set; }
/// <summary>
/// 仅包含数据值的简化字典
///{
///"产品计数": { Value: 100, IsSuccess: true, LastUpdateTime: ..., ErrorMessage: "" },
///"温度值": { Value: 25.6f, IsSuccess: true, LastUpdateTime: ..., ErrorMessage: "" }
///}
/// </summary>
public Dictionary<string, object> CurrentValues { get; set; } = new();
/// <summary>
/// 工站连接状态
/// </summary>
public bool IsConnected { get; set; }
public DateTime LastUpdateTime { get; set; }
public DateTime LastReadTime { get; set; }
public DateTime LastConnectTime { get; set; }
public int ReadFailureCount { get; set; }
}
/// <summary>
/// 所有工站数据的集合
/// </summary>
public class AllStationsDataDto
{
public Dictionary<string, StationDataDto> Stations { get; set; } = new();
public DateTime ServerTime { get; set; } = DateTime.Now;
public int TotalStations { get; set; }
public int ConnectedStations { get; set; }
}
}

View File

@ -6,6 +6,10 @@
<NoWarn>1701;1702;1591;1570</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Remove="PLC\PlcConfig.cs~RF12c5243.TMP" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MiniExcel" Version="1.41.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

View File

@ -0,0 +1,22 @@
using RIZO.Model.PLC;
using S7.Net;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace RIZO.Service.PLC.IService
{
public interface IPlcDataService : IDisposable
{
StationData? GetStationData(string stationName);
Dictionary<string, StationData> GetAllStationData();
Task<bool> WriteDataAsync(string stationName, string dataItemName, object value);
Task<bool> ReconnectStationAsync(string stationName);
event Action<string, string, object>? OnDataUpdated;
event Action<string, string, Exception>? OnError;
}
}

View File

@ -0,0 +1,508 @@
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
{
/// <summary>
/// BackgroundService是一个抽象基类用于创建长时间运行的后台任务
/// </summary>
public class PlcDataService : BackgroundService, IPlcDataService
{
private readonly ILogger<PlcDataService> _logger;
private readonly List<PlcConfig> _plcConfigs;
//这行代码创建了一个线程安全的字典,专门用于存储和管理多个工站的数据。 初始化后值不能再改变(引用不能变,但对象内容可以变)
private readonly ConcurrentDictionary<string, StationData> _stationData = new();
//全局互斥锁 同一时间只能有一个线程/任务访问受保护的资源。
private readonly SemaphoreSlim _globalConnectionLock = new(1, 1);
private CancellationTokenSource _shutdownTokenSource = new();
public event Action<string, string, object>? OnDataUpdated;
public event Action<string, string, Exception>? OnError;
public PlcDataService(
ILogger<PlcDataService> logger,
IOptions<List<PlcConfig>> plcConfigs)
{
_logger = logger;
_plcConfigs = plcConfigs.Value;
}
/// <summary>
/// 后台任务初始化的时候会被调用
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
/// 当应用程序停止时(如 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
/// <summary>
/// // 初始化所有工站连接
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task InitializeAllStationsAsync(CancellationToken cancellationToken)
{
// 并行初始化每个工站 Func<PlcConfig, Task>
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} 失败");
}
}
/// <summary>
/// 测试 PLC 连接(带重试机制和超时控制)
/// 使用指数退避策略进行多次连接尝试,确保连接的可靠性
/// </summary>
/// <param name="plc">要测试的 PLC 实例</param>
/// <param name="config">PLC 配置信息(包含工位名称等信息)</param>
/// <param name="cancellationToken">取消令牌,用于支持外部取消操作</param>
/// <param name="maxRetries">最大重试次数,默认为 3 次</param>
/// <returns>
/// 连接测试成功返回 true所有重试都失败返回 false
/// </returns>
private async Task<bool> 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;
}
/// <summary>
/// 持续读取指定工位的数据,具备连接监控、自动重连、错误恢复能力
/// 这是一个长期运行的后台任务,直到收到取消信号才停止
/// </summary>
/// <param name="config">某个工站plc配置</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
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);
}
/// <summary>
/// 读取单个数据项的值,更新状态并触发事件
/// </summary>
/// <param name="config"></param>
/// <param name="stationData"></param>
/// <param name="dataItem"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
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} 失败");
}
}
/// <summary>
/// 内部重连方法:清理旧连接、创建新连接、验证连接可用性
/// 是连接故障恢复机制的核心实现,确保工位的持续可用性
/// </summary>
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<bool> 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<string, StationData> GetAllStationData()
{
return _stationData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
public async Task<bool> 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
}
}

View File

@ -0,0 +1,129 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using S7.Net; // 确保引用 S7.Net 库
namespace RIZO.Service.PLC
{
public class PlcService : IDisposable
{
private Plc _plc;
private readonly string _ipAddress;
private readonly CpuType _cpuType;
private readonly short _rack;
private readonly short _slot;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private bool _disposed;
public PlcService(string ipAddress, CpuType cpuType = CpuType.S71200, short rack = 0, short slot = 1)
{
_ipAddress = ipAddress;
_cpuType = cpuType;
_rack = rack;
_slot = slot;
}
private async Task ConnectAsync(CancellationToken cancellationToken = default)
{
int maxRetries = 3;
int retryDelayMs = 2000;
int attempt = 0;
while (attempt < maxRetries)
{
attempt++;
try
{
_plc = new Plc(_cpuType, _ipAddress, _rack, _slot);
await Task.Run(() => _plc.Open(), cancellationToken); // 在线程池中执行同步Open
Console.WriteLine($"成功连接到PLC: {_ipAddress} (尝试{attempt}/{maxRetries})");
return;
}
catch (Exception ex)
{
Console.WriteLine($"连接尝试 {attempt}/{maxRetries} 失败: {ex.Message}");
if (attempt < maxRetries)
{
Console.WriteLine($"{retryDelayMs / 1000}秒后重试...");
await Task.Delay(retryDelayMs, cancellationToken);
}
else
{
Console.WriteLine("无法建立PLC连接请检查网络和设备状态");
_plc?.Close();
_plc = null;
}
}
}
}
public async Task<object> ReadAsync(string address, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
if (_plc == null || !_plc.IsConnected)
{
await ConnectAsync(cancellationToken);
if (_plc == null || !_plc.IsConnected)
throw new InvalidOperationException("PLC未连接");
}
return await Task.Run(() => _plc.Read(address), cancellationToken);
}
catch (Exception ex)
{
Console.WriteLine($"Read error: {ex.Message}");
return null;
}
finally
{
_semaphore.Release();
}
}
public async Task WriteAsync(string address, object value, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
if (_plc == null || !_plc.IsConnected)
{
await ConnectAsync(cancellationToken);
if (_plc == null || !_plc.IsConnected)
throw new InvalidOperationException("PLC未连接");
}
await Task.Run(() => _plc.Write(address, value), cancellationToken);
}
catch (Exception ex)
{
Console.WriteLine($"Write error: {ex.Message}");
}
finally
{
_semaphore.Release();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_semaphore.Dispose();
_plc?.Close();
(_plc as IDisposable)?.Dispose();
}
_disposed = true;
}
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RIZO.Service.PLC;
using S7.Net;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace RIZO.Service.PLCBackground
{
public class PlcPollingServiceOP72 : BackgroundService
{
private readonly ILogger<PlcPollingServiceOP72> _logger;
private readonly PlcService _plcService;
private readonly TimeSpan _pollingInterval = TimeSpan.FromSeconds(5);
public PlcPollingServiceOP72(ILogger<PlcPollingServiceOP72> logger)
{
_logger = logger;
// 配置PLC连接参数根据实际设备修改
_plcService = new PlcService("192.168.0.1", CpuType.S71500);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("PLC Polling Service started");
// 不断轮询PLC数据
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 示例读取DB块数据
var data1 = _plcService.Read("DB1.DBW0"); // 读取字
var data2 = _plcService.Read("DB1.DBD4"); // 读取双字
var flag = _plcService.Read("M10.0"); // 读取标志位
_logger.LogInformation($"DB1.DBW0: {data1}, DB1.DBD4: {data2}, M10.0: {flag}");
// 示例写入数据到PLC
_plcService.Write("DB1.DBW10", (short)123); // 写入字
_plcService.Write("DB1.DBD20", 45.67d); // 写入浮点数
await Task.Delay(_pollingInterval, stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "PLC polling error");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // 错误后延迟重试
}
}
_plcService.Dispose();
_logger.LogInformation("PLC Polling Service stopped");
}
}
}