plc 异步锁连接
This commit is contained in:
parent
42e18b5af1
commit
ff1dd97316
181
RIZO.Admin.WebApi/Controllers/Plc/PlcDataController.cs
Normal file
181
RIZO.Admin.WebApi/Controllers/Plc/PlcDataController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
76
RIZO.Model/PLC/PlcConfig.cs
Normal file
76
RIZO.Model/PLC/PlcConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
17
RIZO.Model/PLC/PlcConnectionConfig.cs
Normal file
17
RIZO.Model/PLC/PlcConnectionConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
116
RIZO.Model/PLC/PlcDataItem.cs
Normal file
116
RIZO.Model/PLC/PlcDataItem.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
22
RIZO.Service/PLC/IService/IPlcDataService.cs
Normal file
22
RIZO.Service/PLC/IService/IPlcDataService.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
508
RIZO.Service/PLC/PlcDataService.cs
Normal file
508
RIZO.Service/PLC/PlcDataService.cs
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
129
RIZO.Service/PLC/PlcService.cs
Normal file
129
RIZO.Service/PLC/PlcService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
RIZO.Service/PLCBackground/PlcPollingServiceOP72.cs
Normal file
63
RIZO.Service/PLCBackground/PlcPollingServiceOP72.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user