612 lines
24 KiB
C#
612 lines
24 KiB
C#
// 统一引入所有必要命名空间
|
||
using Microsoft.Extensions.Options;
|
||
using RIZO.Admin.WebApi.PLC.Model;
|
||
using RIZO.Common;
|
||
using S7.Net;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace RIZO.Admin.WebApi.PLC.Service
|
||
{
|
||
/// <summary>
|
||
/// PLC通信服务(封装连接、读写、生产数据采集等功能)
|
||
/// </summary>
|
||
public class PlcService : IDisposable
|
||
{
|
||
#region 字段与构造函数
|
||
// 标记是否已释放资源,避免重复释放
|
||
private bool _disposed = false;
|
||
|
||
// PLC配置参数(从配置文件注入)
|
||
private readonly List<PlcConfig> _plcConfigs;
|
||
private readonly GlobalPlcConfig _globalConfig;
|
||
|
||
private PlcProductionDataService plcProductionDataService = new PlcProductionDataService();
|
||
|
||
// PLC地址映射(严格匹配业务地址清单)
|
||
private readonly Dictionary<string, (string Addr, int Len)> _plcStringMap = new()
|
||
{
|
||
{ "LineCode", ("DB1010.DBB50", 14) },
|
||
{ "IpStation", ("DB1010.DBB64", 16) },
|
||
{ "ProductCode", ("DB1010.DBB80", 16) },
|
||
{ "ProductName", ("DB1010.DBB94", 50) },
|
||
{ "PartCode", ("DB1010.DBB144", 16) },
|
||
{ "PartName", ("DB1010.DBB158", 28) },
|
||
{ "ProcessName", ("DB1010.DBB186", 12) },
|
||
{ "ParamName", ("DB1010.DBB198", 12) },
|
||
{ "ParamValue", ("DB1010.DBB210", 14) },
|
||
{ "Static16", ("DB1010.DBB238", 68) }
|
||
};
|
||
|
||
private readonly Dictionary<string, string> _plcIntMap = new()
|
||
{
|
||
{ "QualificationFlag", "DB1010.DBW224" },
|
||
{ "ReworkFlag", "DB1010.DBW226" },
|
||
{ "ProductionCycle", "DB1010.DBW228" },
|
||
{ "AutoManual", "DB1010.DBW230" },
|
||
{ "RunStatus", "DB1010.DBW232" }
|
||
};
|
||
|
||
/// <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 ?? throw new ArgumentNullException(nameof(plcConfigs), "PLC配置不能为空");
|
||
_globalConfig = globalConfig?.Value ?? throw new ArgumentNullException(nameof(globalConfig), "PLC全局配置不能为空");
|
||
}
|
||
#endregion
|
||
|
||
#region 核心业务方法
|
||
/// <summary>
|
||
/// 读取PLC生产数据(严格匹配业务地址和解析规则)
|
||
/// </summary>
|
||
/// <param name="ip">PLC IP地址</param>
|
||
/// <param name="rack">机架号</param>
|
||
/// <param name="slot">槽位号</param>
|
||
/// <param name="cpuType">PLC型号(默认S7-1500)</param>
|
||
/// <returns>读取结果(状态+数据+消息)</returns>
|
||
public async Task<(bool Success, PlcProductionData Data, string Message)> ReadProductionDataAsync(
|
||
string ip,
|
||
short rack,
|
||
short slot,
|
||
CpuType cpuType = CpuType.S71500)
|
||
{
|
||
// 参数校验
|
||
if (string.IsNullOrWhiteSpace(ip))
|
||
return (false, null, "PLC IP地址不能为空");
|
||
|
||
Plc plc = null;
|
||
try
|
||
{
|
||
// 初始化PLC连接
|
||
plc = CreatePlcClient(cpuType, ip, rack, slot);
|
||
await OpenPlcConnectionAsync(plc);
|
||
|
||
if (!plc.IsConnected)
|
||
return (false, null, "PLC连接失败");
|
||
|
||
// 构建生产数据实体
|
||
var prodData = new PlcProductionData
|
||
{
|
||
PlcIp = ip.Trim(),
|
||
OccurTime = DateTime.Now, // MES自配时间
|
||
|
||
// 字符串字段(按规则解析)
|
||
LineCode = await ReadPlcStringAsync(plc, _plcStringMap["LineCode"].Addr, _plcStringMap["LineCode"].Len),
|
||
ProductCode = await ReadPlcStringAsync(plc, _plcStringMap["ProductCode"].Addr, _plcStringMap["ProductCode"].Len),
|
||
ProductName = await ReadPlcStringAsync(plc, _plcStringMap["ProductName"].Addr, _plcStringMap["ProductName"].Len),
|
||
PartCode = await ReadPlcStringAsync(plc, _plcStringMap["PartCode"].Addr, _plcStringMap["PartCode"].Len),
|
||
PartName = await ReadPlcStringAsync(plc, _plcStringMap["PartName"].Addr, _plcStringMap["PartName"].Len),
|
||
ProcessName = await ReadPlcStringAsync(plc, _plcStringMap["ProcessName"].Addr, _plcStringMap["ProcessName"].Len),
|
||
ParamName = await ReadPlcStringAsync(plc, _plcStringMap["ParamName"].Addr, _plcStringMap["ParamName"].Len),
|
||
ParamValue = await ReadPlcStringAsync(plc, _plcStringMap["ParamValue"].Addr, _plcStringMap["ParamValue"].Len),
|
||
|
||
// 修复:改为异步读取整数,确保类型兼容
|
||
//合格标志(0默认,1合格,2不合格)
|
||
QualificationFlag = (await ReadPlcIntAsync(plc, _plcIntMap["QualificationFlag"])).ToString(),
|
||
//返工标志(0正常,1返工)
|
||
ReworkFlag = (await ReadPlcIntAsync(plc, _plcIntMap["ReworkFlag"])).ToString(),
|
||
//生产节拍秒
|
||
ProductionCycle = await ReadPlcIntAsync(plc, _plcIntMap["ProductionCycle"])
|
||
};
|
||
|
||
// 空值兜底(避免入库报错)
|
||
prodData.QualificationFlag ??= "0";
|
||
prodData.ReworkFlag ??= "0";
|
||
prodData.ProductionCycle ??= 0;
|
||
|
||
// 保存生产数据到MES数据库
|
||
plcProductionDataService.AddPlcProductionData(prodData);
|
||
return (true, prodData, "生产数据读取成功");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return (false, null, $"生产数据读取失败:{ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
ReleasePlcConnection(plc);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试单个PLC的连接、读、写功能
|
||
/// </summary>
|
||
/// <param name="config">单个PLC的配置参数</param>
|
||
/// <param name="cancellationToken">取消令牌</param>
|
||
/// <returns>测试结果</returns>
|
||
public async Task<PlcTestResult> TestSinglePlcAsync(PlcConfig config, CancellationToken cancellationToken = default)
|
||
{
|
||
// 参数校验
|
||
if (config == null)
|
||
throw new ArgumentNullException(nameof(config), "PLC测试配置不能为空");
|
||
if (string.IsNullOrWhiteSpace(config.Ip))
|
||
return new PlcTestResult { PlcName = config.PlcName, Ip = config.Ip, ConnectSuccess = false, ConnectMessage = "IP地址为空" };
|
||
|
||
var result = new PlcTestResult
|
||
{
|
||
PlcName = config.PlcName,
|
||
Ip = config.Ip
|
||
};
|
||
|
||
Plc plc = null;
|
||
try
|
||
{
|
||
// 初始化PLC客户端
|
||
plc = CreatePlcClient(CpuType.S71500, config.Ip, config.Rack, config.Slot);
|
||
|
||
// 带超时的连接测试
|
||
var connectTask = OpenPlcConnectionAsync(plc);
|
||
if (await Task.WhenAny(connectTask, Task.Delay(_globalConfig.ReadWriteTimeout, cancellationToken)) != connectTask)
|
||
{
|
||
result.ConnectSuccess = false;
|
||
result.ConnectMessage = $"连接超时({_globalConfig.ReadWriteTimeout}ms)";
|
||
return result;
|
||
}
|
||
await connectTask;
|
||
|
||
// 检查连接状态
|
||
if (plc.IsConnected)
|
||
{
|
||
result.ConnectSuccess = true;
|
||
result.ConnectMessage = "连接成功";
|
||
}
|
||
else
|
||
{
|
||
result.ConnectSuccess = false;
|
||
result.ConnectMessage = "连接失败(PLC未返回连接状态)";
|
||
return result;
|
||
}
|
||
|
||
string TestAddress = "DB1010.DBB238"; // 测试地址
|
||
|
||
// 读取测试
|
||
if (!string.IsNullOrWhiteSpace(TestAddress))
|
||
{
|
||
try
|
||
{
|
||
var readValue = await Task.Run(() => plc.Read(TestAddress), cancellationToken);
|
||
result.ReadSuccess = true;
|
||
result.ReadValue = FormatPlcValue(readValue);
|
||
result.ReadMessage = "读取成功";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.ReadSuccess = false;
|
||
result.ReadMessage = $"读取失败:{ex.Message}";
|
||
}
|
||
}
|
||
|
||
// 写入测试
|
||
if (!string.IsNullOrWhiteSpace(TestAddress) && !string.IsNullOrWhiteSpace(TestAddress))
|
||
{
|
||
try
|
||
{
|
||
bool writeOk = await Task.Run(() => WritePlcValue(plc, TestAddress, "1"), cancellationToken);
|
||
result.WriteSuccess = writeOk;
|
||
result.WriteMessage = writeOk ? "写入成功" : "写入失败(值类型与地址不匹配)";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.WriteSuccess = false;
|
||
result.WriteMessage = $"写入失败:{ex.Message}";
|
||
}
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
result.ConnectSuccess = false;
|
||
result.ConnectMessage = "测试被取消";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.ConnectSuccess = false;
|
||
result.ConnectMessage = $"连接异常:{ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
ReleasePlcConnection(plc);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量测试配置文件中所有PLC的读写功能
|
||
/// </summary>
|
||
/// <param name="cancellationToken">取消令牌</param>
|
||
/// <returns>所有PLC的测试结果列表</returns>
|
||
public async Task<List<PlcTestResult>> BatchTestAllPlcAsync(CancellationToken cancellationToken = default)
|
||
{
|
||
if (_plcConfigs == null || _plcConfigs.Count == 0)
|
||
throw new InvalidOperationException("未从配置文件加载到任何PLC参数,请检查PlcConfigs配置");
|
||
|
||
// 并行测试所有PLC(带最大并行度限制,避免资源耗尽)
|
||
var testTasks = _plcConfigs
|
||
.Select(config => TestSinglePlcAsync(config, cancellationToken))
|
||
.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>
|
||
/// <param name="cpuType">PLC型号</param>
|
||
/// <returns>读取结果(成功状态、值、消息)</returns>
|
||
public async Task<(bool Success, string Value, string Message)> ReadPlcDataAsync(
|
||
string ip,
|
||
short rack,
|
||
short slot,
|
||
string address,
|
||
CpuType cpuType = CpuType.S71500)
|
||
{
|
||
// 参数校验
|
||
if (string.IsNullOrWhiteSpace(ip))
|
||
return (false, "", "PLC IP地址不能为空");
|
||
if (string.IsNullOrWhiteSpace(address))
|
||
return (false, "", "读取地址不能为空");
|
||
|
||
Plc plc = null;
|
||
try
|
||
{
|
||
plc = CreatePlcClient(cpuType, ip, rack, slot);
|
||
await OpenPlcConnectionAsync(plc);
|
||
|
||
if (!plc.IsConnected)
|
||
return (false, "", "PLC连接失败,请检查IP/机架号/槽位号是否正确");
|
||
|
||
var value = await Task.Run(() => plc.Read(address));
|
||
return (true, FormatPlcValue(value), "读取成功");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return (false, "", $"读取失败:{ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
ReleasePlcConnection(plc);
|
||
}
|
||
}
|
||
|
||
/// <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>
|
||
/// <param name="cpuType">PLC型号</param>
|
||
/// <returns>写入结果(成功状态、消息)</returns>
|
||
public async Task<(bool Success, string Message)> WritePlcDataAsync(
|
||
string ip,
|
||
short rack,
|
||
short slot,
|
||
string address,
|
||
string valueStr,
|
||
CpuType cpuType = CpuType.S71500)
|
||
{
|
||
// 参数校验
|
||
if (string.IsNullOrWhiteSpace(ip))
|
||
return (false, "PLC IP地址不能为空");
|
||
if (string.IsNullOrWhiteSpace(address))
|
||
return (false, "写入地址不能为空");
|
||
if (valueStr == null) // 允许空字符串(如清空值)
|
||
return (false, "写入值不能为null");
|
||
|
||
Plc plc = null;
|
||
try
|
||
{
|
||
plc = CreatePlcClient(cpuType, ip, rack, slot);
|
||
await OpenPlcConnectionAsync(plc);
|
||
|
||
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
|
||
{
|
||
ReleasePlcConnection(plc);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region 私有辅助方法
|
||
/// <summary>
|
||
/// 创建PLC客户端实例(统一配置超时)
|
||
/// </summary>
|
||
private Plc CreatePlcClient(CpuType cpuType, string ip, short rack, short slot)
|
||
{
|
||
var plc = new Plc(cpuType, ip, rack, slot)
|
||
{
|
||
ReadTimeout = _globalConfig.ReadWriteTimeout,
|
||
WriteTimeout = _globalConfig.ReadWriteTimeout
|
||
};
|
||
return plc;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步打开PLC连接(封装同步方法为异步)
|
||
/// </summary>
|
||
private async Task OpenPlcConnectionAsync(Plc plc)
|
||
{
|
||
if (plc == null) return;
|
||
await Task.Run(() => plc.Open());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放PLC连接(安全关闭+资源释放)
|
||
/// </summary>
|
||
private void ReleasePlcConnection(Plc plc)
|
||
{
|
||
if (plc == null) return;
|
||
|
||
try
|
||
{
|
||
if (plc.IsConnected)
|
||
plc.Close();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 记录日志(此处简化为控制台输出,实际项目替换为日志框架)
|
||
Console.WriteLine($"PLC连接关闭失败:{ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
Dispose();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按业务规则解析PLC字符串(支持中文GBK编码)
|
||
/// </summary>
|
||
private async Task<string> ReadPlcStringAsync(Plc plc, string addr, int maxLen)
|
||
{
|
||
try
|
||
{
|
||
// 解析地址:DB1010.DBB50 → DB编号1010 + 起始字节50
|
||
var parts = addr.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||
if (parts.Length != 2)
|
||
return string.Empty;
|
||
|
||
if (!int.TryParse(parts[0].Replace("DB", ""), out int dbNum) || dbNum <= 0)
|
||
return string.Empty;
|
||
|
||
if (!int.TryParse(parts[1].Replace("DBB", ""), out int startByte) || startByte < 0)
|
||
return string.Empty;
|
||
|
||
// 读取字节数组
|
||
var bytes = await Task.Run(() => plc.ReadBytes(DataType.DataBlock, dbNum, startByte, maxLen));
|
||
if (bytes == null || bytes.Length < 3)
|
||
return string.Empty;
|
||
|
||
// 严格按规则解析:bytes[0]=总长度(备用)、bytes[1]=有效长度、bytes[2+]=内容
|
||
int validLen = Math.Clamp(bytes[1], 0, bytes.Length - 2);
|
||
if (validLen <= 0)
|
||
return string.Empty;
|
||
|
||
// 提取有效内容并转字符串(GBK编码支持中文)
|
||
var contentBytes = new byte[validLen];
|
||
Array.Copy(bytes, 2, contentBytes, 0, validLen);
|
||
return Encoding.GetEncoding("GBK").GetString(contentBytes).Trim('\0', ' ', '\t');
|
||
}
|
||
catch
|
||
{
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 修复版:异步读取PLC整数(DBW地址,16位短整型)
|
||
/// 增强类型兼容、异常日志、地址校验
|
||
/// </summary>
|
||
/// <param name="plc">PLC客户端实例</param>
|
||
/// <param name="addr">读取地址(如DB1010.DBW226)</param>
|
||
/// <returns>解析后的整数值,读取失败返回0</returns>
|
||
private async Task<int> ReadPlcIntAsync(Plc plc, string addr)
|
||
{
|
||
// 1. 地址和PLC实例有效性校验
|
||
if (plc == null)
|
||
{
|
||
Console.WriteLine($"PLC整数读取失败:PLC实例为空(地址:{addr})");
|
||
return 0;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(addr))
|
||
{
|
||
Console.WriteLine("PLC整数读取失败:读取地址为空");
|
||
return 0;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 2. 异步读取(避免同步阻塞,适配整体异步上下文)
|
||
var val = await Task.Run(() => plc.Read(addr));
|
||
|
||
// 调试日志:输出原始值和类型,便于排查问题
|
||
Console.WriteLine($"PLC地址[{addr}]原始读取值:{val ?? "null"},类型:{val?.GetType().Name ?? "未知"}");
|
||
|
||
// 3. 增强类型兼容(覆盖PLC可能返回的所有整数类型)
|
||
return val switch
|
||
{
|
||
short s => s, // 16位短整型(DBW默认类型)
|
||
int i => i, // 32位整型(兼容返回)
|
||
uint ui => (int)ui, // 无符号32位整型
|
||
byte b => b, // 8位字节型
|
||
ushort us => us, // 无符号16位整型
|
||
long l => (int)l, // 64位整型(截断为32位)
|
||
ulong ul => (int)ul, // 无符号64位整型(截断为32位)
|
||
float f => (int)f, // 浮点数转整型(兼容PLC数值存储)
|
||
double d => (int)d, // 双精度浮点数转整型
|
||
_ => 0 // 未知类型返回0
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 4. 输出详细异常日志,便于定位问题
|
||
Console.WriteLine($"PLC整数读取失败(地址:{addr}):{ex.Message},堆栈:{ex.StackTrace}");
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 格式化PLC读取的值,转为友好的字符串
|
||
/// </summary>
|
||
private string FormatPlcValue(object value)
|
||
{
|
||
if (value == null) return "空值";
|
||
|
||
return value switch
|
||
{
|
||
float f => f.ToString("0.000"), // 浮点数保留3位小数
|
||
double d => d.ToString("0.000"), // 补充双精度浮点数支持
|
||
short s => s.ToString(), // 16位整数
|
||
byte b => b.ToString(), // 8位整数
|
||
bool b => b.ToString(), // 布尔值
|
||
int i => i.ToString(), // 32位整数
|
||
uint ui => ui.ToString(), // 无符号32位整数
|
||
long l => l.ToString(), // 64位整数
|
||
ulong ul => ul.ToString(), // 无符号64位整数
|
||
_ => value.ToString() // 其他类型直接转字符串
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据地址类型转换值并写入PLC
|
||
/// </summary>
|
||
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;
|
||
}
|
||
#endregion
|
||
|
||
#region 资源释放
|
||
/// <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; // 避免重复释放
|
||
|
||
// 释放托管资源
|
||
if (disposing)
|
||
{
|
||
// 此处无长期持有的PLC连接,若有可在此统一释放
|
||
}
|
||
|
||
// 标记为已释放
|
||
_disposed = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 终结器(防忘记手动Dispose)
|
||
/// </summary>
|
||
~PlcService()
|
||
{
|
||
Dispose(false);
|
||
}
|
||
#endregion
|
||
}
|
||
} |