377 lines
14 KiB
C#
Raw Normal View History

2026-01-17 10:42:02 +08:00
// 统一引入所有必要命名空间
using Microsoft.Extensions.Options;
using Quartz;
using RIZO.Common;
using S7.Net;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace RIZO.Admin.WebApi.PLC
{
// 移除嵌套类直接定义顶级PlcService类并实现IDisposable
public class PlcService : IDisposable
{
// 标记是否已释放资源,避免重复释放
private bool _disposed = false;
// PLC配置参数从配置文件注入
private readonly List<PlcConfig> _plcConfigs;
private readonly GlobalPlcConfig _globalConfig;
/// <summary>
/// 构造函数依赖注入获取PLC配置
/// </summary>
/// <param name="plcConfigs">所有PLC的连接配置</param>
/// <param name="globalConfig">PLC全局超时配置</param>
public PlcService(IOptions<List<PlcConfig>> plcConfigs, IOptions<GlobalPlcConfig> globalConfig)
{
_plcConfigs = plcConfigs.Value;
_globalConfig = globalConfig.Value;
}
/// <summary>
/// 测试单个PLC的连接、读、写功能
/// </summary>
/// <param name="config">单个PLC的配置参数</param>
/// <returns>测试结果</returns>
public async Task<PlcTestResult> TestSinglePlcAsync(PlcConfig config)
{
var result = new PlcTestResult
{
PlcName = config.PlcName,
Ip = config.Ip
};
Plc plc = null;
try
{
// 1. 初始化PLC连接对象
plc = new Plc(CpuType.S71500, config.Ip, config.Rack, config.Slot);
plc.ReadTimeout = _globalConfig.ReadWriteTimeout;
plc.WriteTimeout = _globalConfig.ReadWriteTimeout;
// 异步打开连接S7.Net无原生异步用Task.Run包装
await Task.Run(() => plc.Open());
// 检查连接状态
if (plc.IsConnected)
{
result.ConnectSuccess = true;
result.ConnectMessage = "连接成功";
}
else
{
result.ConnectSuccess = false;
result.ConnectMessage = "连接失败PLC未返回连接状态";
return result;
}
// 2. 读取PLC数据
try
{
var readValue = await Task.Run(() => plc.Read(config.TestReadAddress));
result.ReadSuccess = true;
result.ReadValue = FormatValue(readValue);
result.ReadMessage = "读取成功";
}
catch (Exception ex)
{
result.ReadSuccess = false;
result.ReadMessage = $"读取失败:{ex.Message}";
}
// 3. 写入PLC数据
try
{
bool writeOk = await Task.Run(() => WritePlcValue(plc, config.TestWriteAddress, config.TestWriteValue));
result.WriteSuccess = writeOk;
result.WriteMessage = writeOk ? "写入成功" : "写入失败(值类型与地址不匹配)";
}
catch (Exception ex)
{
result.WriteSuccess = false;
result.WriteMessage = $"写入失败:{ex.Message}";
}
}
catch (Exception ex)
{
result.ConnectSuccess = false;
result.ConnectMessage = $"连接异常:{ex.Message}";
}
finally
{
// 确保PLC连接最终被释放
if (plc != null)
{
if (plc.IsConnected)
{
plc.Close();
}
Dispose(); // 释放Plc对象资源
}
}
return result;
}
/// <summary>
/// 批量测试配置文件中所有PLC的读写功能
/// </summary>
/// <returns>所有PLC的测试结果列表</returns>
/// <exception cref="Exception">未配置PLC参数时抛出</exception>
public async Task<List<PlcTestResult>> BatchTestAllPlcAsync()
{
if (_plcConfigs == null || _plcConfigs.Count == 0)
{
throw new Exception("未从配置文件加载到任何PLC参数请检查PlcConfigs配置");
}
// 并行测试所有PLC提高效率
var testTasks = _plcConfigs.Select(config => TestSinglePlcAsync(config)).ToList();
var results = await Task.WhenAll(testTasks);
return results.ToList();
}
/// <summary>
/// 单独读取指定PLC的某个地址数据
/// </summary>
/// <param name="ip">PLC的IP地址</param>
/// <param name="rack">机架号</param>
/// <param name="slot">槽位号</param>
/// <param name="address">读取地址如DB1.DBD0</param>
/// <returns>读取结果(成功状态、值、消息)</returns>
public async Task<(bool Success, string Value, string Message)> ReadPlcDataAsync(string ip, short rack, short slot, string address)
{
Plc plc = null;
try
{
plc = new Plc(CpuType.S71500, ip, rack, slot);
plc.ReadTimeout = _globalConfig.ReadWriteTimeout;
await Task.Run(() => plc.Open());
if (!plc.IsConnected)
{
return (false, "", "PLC连接失败请检查IP/机架号/槽位号是否正确");
}
var value = await Task.Run(() => plc.Read(address));
// 示例读取DB1.DBX0.0和DB1.DBX0.1
//var address1 = "DB1010.DBX0.0"; // 第一个位地址
//var address2 = "DB1010.DBX0.1"; // 第二个位地址
var address1 = "DB1010.DBW2"; // 第二个位地址
var address2 = "DB1010.DBW4"; // 第二个位地址
// 读取DBX0.0
var value1 = await Task.Run(() => plc.Read(address1));
bool writeOk = await Task.Run(() => WritePlcValue(plc, address1, "56"));
// 读取DBX0.1
var value2 = await Task.Run(() => plc.Read(address2));
return (true, FormatValue(value), "读取成功");
}
catch (Exception ex)
{
return (false, "", $"读取失败:{ex.Message}");
}
finally
{
// 释放PLC连接
if (plc != null)
{
if (plc.IsConnected) plc.Close();
Dispose();
}
}
}
/// <summary>
/// 单独写入数据到指定PLC的某个地址
/// </summary>
/// <param name="ip">PLC的IP地址</param>
/// <param name="rack">机架号</param>
/// <param name="slot">槽位号</param>
/// <param name="address">写入地址如DB1.DBD0</param>
/// <param name="valueStr">写入的值(字符串格式)</param>
/// <returns>写入结果(成功状态、消息)</returns>
public async Task<(bool Success, string Message)> WritePlcDataAsync(string ip, short rack, short slot, string address, string valueStr)
{
Plc plc = null;
try
{
plc = new Plc(CpuType.S71500, ip, rack, slot);
plc.WriteTimeout = _globalConfig.ReadWriteTimeout;
await Task.Run(() => plc.Open());
if (!plc.IsConnected)
{
return (false, "PLC连接失败请检查IP/机架号/槽位号是否正确");
}
bool writeOk = await Task.Run(() => WritePlcValue(plc, address, valueStr));
return (writeOk, writeOk ? "写入成功" : "写入失败(值类型与地址不匹配)");
}
catch (Exception ex)
{
return (false, $"写入失败:{ex.Message}");
}
finally
{
// 释放PLC连接
if (plc != null)
{
if (plc.IsConnected) plc.Close();
Dispose();
}
}
}
/// <summary>
/// 格式化PLC读取的值转为友好的字符串
/// </summary>
/// <param name="value">PLC读取的原始值</param>
/// <returns>格式化后的字符串</returns>
private string FormatValue(object value)
{
if (value == null) return "空值";
return value switch
{
float f => f.ToString("0.000"), // 浮点数保留3位小数
short s => s.ToString(), // 16位整数
byte b => b.ToString(), // 8位整数
bool b => b.ToString(), // 布尔值
int i => i.ToString(), // 32位整数
uint ui => ui.ToString(), // 无符号32位整数
_ => value.ToString() // 其他类型直接转字符串
};
}
/// <summary>
/// 根据地址类型转换值并写入PLC
/// </summary>
/// <param name="plc">PLC连接对象</param>
/// <param name="address">写入地址</param>
/// <param name="valueStr">写入的值(字符串)</param>
/// <returns>是否写入成功</returns>
private bool WritePlcValue(Plc plc, string address, string valueStr)
{
// 双字DBD/DD浮点数或32位整数
if (address.Contains("DBD") || address.Contains("DD"))
{
if (float.TryParse(valueStr, out float f))
{
plc.Write(address, f);
return true;
}
else if (int.TryParse(valueStr, out int i))
{
plc.Write(address, i);
return true;
}
}
// 字DBW/DW16位整数
else if (address.Contains("DBW") || address.Contains("DW"))
{
if (short.TryParse(valueStr, out short s))
{
plc.Write(address, s);
return true;
}
}
// 字节DBB/DB8位整数
else if (address.Contains("DBB") || address.Contains("DB"))
{
if (byte.TryParse(valueStr, out byte b))
{
plc.Write(address, b);
return true;
}
}
// 位DBX布尔值兼容0/1输入
else if (address.Contains("DBX"))
{
if (bool.TryParse(valueStr, out bool bl))
{
plc.Write(address, bl);
return true;
}
else if (valueStr == "0")
{
plc.Write(address, false);
return true;
}
else if (valueStr == "1")
{
plc.Write(address, true);
return true;
}
}
// 不匹配的类型
return false;
}
/// <summary>
/// 带超时的PLC连接测试
/// </summary>
public async Task<PlcTestResult> TestSinglePlcAsync(PlcConfig config, CancellationToken cancellationToken)
{
// 替换为实际的PLC连接逻辑如S7NetPlus
// 示例:
// using var plc = new S7Client();
// var result = plc.ConnectTo(config.Ip, config.Rack, config.Slot);
// return new PlcTestResult { ConnectSuccess = result == 0, ConnectMessage = result == 0 ? "成功" : "失败" };
await Task.Delay(100, cancellationToken);
return new PlcTestResult { ConnectSuccess = true, ConnectMessage = "连接正常" };
}
/// <summary>
/// 读取PLC数据
/// </summary>
public async Task<(bool success, object value, string message)> ReadPlcDataAsync(string ip, int rack, int slot, string address)
{
// 替换为实际的PLC读取逻辑
await Task.Delay(50);
return (true, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), "读取成功");
}
/// <summary>
/// 实现IDisposable接口释放资源
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 通知GC无需调用终结器
}
/// <summary>
/// 实际释放资源的逻辑(区分托管/非托管资源)
/// </summary>
/// <param name="disposing">是否释放托管资源</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed) return; // 避免重复释放
// 释放托管资源此处无长期持有的PLC连接若有可在此处理
if (disposing)
{
// 示例若有全局PLC连接对象可在此释放
// if (_globalPlc != null) _globalPlc.Dispose();
}
// 标记为已释放
_disposed = true;
}
/// <summary>
/// 终结器防忘记手动Dispose
/// </summary>
~PlcService()
{
Dispose(false);
}
}
}