377 lines
14 KiB
C#
377 lines
14 KiB
C#
// 统一引入所有必要命名空间
|
||
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/DW):16位整数
|
||
else if (address.Contains("DBW") || address.Contains("DW"))
|
||
{
|
||
if (short.TryParse(valueStr, out short s))
|
||
{
|
||
plc.Write(address, s);
|
||
return true;
|
||
}
|
||
}
|
||
// 字节(DBB/DB):8位整数
|
||
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);
|
||
}
|
||
}
|
||
} |