2026-01-20 14:55:21 +08:00

853 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 统一引入所有必要命名空间
using MDM.Services.Plant;
using Microsoft.Extensions.Options;
using RIZO.Admin.WebApi.PLC.Model;
using RIZO.Common;
using S7.Net;
using System;
using System.Collections.Concurrent;
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();
private PlantWorkstationService _plantWorkstationService = new PlantWorkstationService();
// 先在类中添加2个核心优化字段支撑高频访问
private readonly SemaphoreSlim _concurrencySemaphore = new SemaphoreSlim(15, 50); // 限制20并发适配50台PLC
private readonly ConcurrentDictionary<string, (Plc Client, DateTime LastUsedTime)> _plcConnPool = new(); // 连接池
// 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" },
{ "QueryRequest", "DB1001.DBW2001" }
};
/// <summary>
/// 构造函数依赖注入获取PLC配置
/// </summary>
/// <param name="plcConfigs">所有PLC的连接配置</param>
/// <param name="globalConfig">PLC全局超时配置</param>
public PlcService(IOptions<GlobalPlcConfig> globalConfig)
{
_globalConfig = globalConfig?.Value ?? throw new ArgumentNullException(nameof(globalConfig), "PLC全局配置不能为空");
//初始化plcConfigs
_plcConfigs = initPlcConfigs(_plcConfigs);
}
#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(),
// //设备自动手动0-自动1-手动
// AutoManual = (await ReadPlcIntAsync(plc, _plcIntMap["AutoManual"])),
// //运行状态:1正常2异常
// RunStatus = (await ReadPlcIntAsync(plc, _plcIntMap["RunStatus"])),
// //生产节拍秒
// 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="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)
{
// 1. 强化参数校验(源头避免无效请求)
if (string.IsNullOrWhiteSpace(ip))
return (false, null, "PLC IP地址不能为空");
if (rack < 0 || rack > 10) // 工业场景机架号常规范围
return (false, null, $"PLC机架号{rack}无效有效值0-10");
if (slot < 0 || slot > 4) // 工业场景槽位号常规范围
return (false, null, $"PLC槽位号{slot}无效有效值0-4");
Plc plc = null;
bool isConnReused = false; // 标记是否复用连接
var poolKey = $"{ip}_{rack}_{slot}_{cpuType}"; // 连接池唯一标识
// 2. 并发控制防止50台PLC同时访问导致端口/线程耗尽)
await _concurrencySemaphore.WaitAsync();
try
{
// 3. 连接池复用(核心优化:避免高频创建/销毁连接)
if (_plcConnPool.TryGetValue(poolKey, out var poolItem) && poolItem.Client.IsConnected)
{
plc = poolItem.Client;
_plcConnPool.TryUpdate(poolKey, (plc, DateTime.Now), poolItem); // 更新最后使用时间
isConnReused = true;
}
else
{
// 初始化新连接(保留你的原始逻辑,增加重试)
plc = CreatePlcClient(cpuType, ip, rack, slot);
// 连接重试(应对临时网络波动)
int retryCount = 2;
bool isConnected = false;
while (retryCount-- > 0 && !isConnected)
{
try
{
await OpenPlcConnectionAsync(plc);
isConnected = plc.IsConnected;
}
catch
{
if (retryCount > 0) await Task.Delay(100); // 重试间隔
}
}
if (!isConnected)
return (false, null, "PLC连接失败含2次重试");
// 新连接加入池
_plcConnPool.TryAdd(poolKey, (plc, DateTime.Now));
}
//先读取查询请求标志,判断是否需要读取生产数据
int iQueryRequest = await ReadPlcIntAsync(plc, _plcIntMap["QueryRequest"]);
if (iQueryRequest != 1)
{
return (false, null, "PLC查询请求标志未设置跳过生产数据读取");
}
// 4. 批量并行读取核心优化替代串行await提升效率30%+
// 4.1 字符串字段批量读取
var stringTasks = new Dictionary<string, Task<string>>
{
{ "LineCode", ReadPlcStringAsync(plc, _plcStringMap["LineCode"].Addr, _plcStringMap["LineCode"].Len) },
{ "ProductCode", ReadPlcStringAsync(plc, _plcStringMap["ProductCode"].Addr, _plcStringMap["ProductCode"].Len) },
{ "ProductName", ReadPlcStringAsync(plc, _plcStringMap["ProductName"].Addr, _plcStringMap["ProductName"].Len) },
{ "PartCode", ReadPlcStringAsync(plc, _plcStringMap["PartCode"].Addr, _plcStringMap["PartCode"].Len) },
{ "PartName", ReadPlcStringAsync(plc, _plcStringMap["PartName"].Addr, _plcStringMap["PartName"].Len) },
{ "ProcessName", ReadPlcStringAsync(plc, _plcStringMap["ProcessName"].Addr, _plcStringMap["ProcessName"].Len) },
{ "ParamName", ReadPlcStringAsync(plc, _plcStringMap["ParamName"].Addr, _plcStringMap["ParamName"].Len) },
{ "ParamValue", ReadPlcStringAsync(plc, _plcStringMap["ParamValue"].Addr, _plcStringMap["ParamValue"].Len) }
};
// 4.2 整数字段批量读取
var intTasks = new Dictionary<string, Task<int>>
{
{ "QualificationFlag", ReadPlcIntAsync(plc, _plcIntMap["QualificationFlag"]) },
{ "ReworkFlag", ReadPlcIntAsync(plc, _plcIntMap["ReworkFlag"]) },
{ "AutoManual", ReadPlcIntAsync(plc, _plcIntMap["AutoManual"]) },
{ "RunStatus", ReadPlcIntAsync(plc, _plcIntMap["RunStatus"]) },
{ "ProductionCycle", ReadPlcIntAsync(plc, _plcIntMap["ProductionCycle"]) }
};
// 等待所有读取完成(并行执行,减少等待时间)
IEnumerable<Task> allTasks = stringTasks.Values.Cast<Task>()
.Concat(intTasks.Values.Cast<Task>());
await Task.WhenAll(allTasks);
// 5. 构建数据实体(空值安全处理,单个字段失败不影响整体)
var prodData = new PlcProductionData
{
PlcIp = ip.Trim(),
OccurTime = DateTime.Now,
// 字符串字段(空值兜底)
LineCode = await stringTasks["LineCode"] ?? string.Empty,
ProductCode = await stringTasks["ProductCode"] ?? string.Empty,
ProductName = await stringTasks["ProductName"] ?? string.Empty,
PartCode = await stringTasks["PartCode"] ?? string.Empty,
PartName = await stringTasks["PartName"] ?? string.Empty,
ProcessName = await stringTasks["ProcessName"] ?? string.Empty,
ParamName = await stringTasks["ParamName"] ?? string.Empty,
ParamValue = await stringTasks["ParamValue"] ?? string.Empty,
// 整数字段读取失败返回0不中断流程
QualificationFlag = (await intTasks["QualificationFlag"]).ToString(),
ReworkFlag = (await intTasks["ReworkFlag"]).ToString(),
AutoManual = await intTasks["AutoManual"],
RunStatus = await intTasks["RunStatus"],
ProductionCycle = await intTasks["ProductionCycle"]
};
// 空值兜底(保留你的原始逻辑)
prodData.QualificationFlag ??= "0";
prodData.ReworkFlag ??= "0";
prodData.ProductionCycle ??= 0;
// 6. 异步保存数据(非阻塞,提升响应速度)
_ = Task.Run(() => _plcProductionDataService.AddPlcProductionData(prodData))
.ContinueWith(t =>
{
if (t.IsFaulted)
{
// 记录保存失败日志建议替换为ILogger
Console.WriteLine($"PLC({ip})数据保存失败:{t.Exception?.InnerException?.Message}");
}
});
var successMsg = isConnReused ? "生产数据读取成功(复用连接)" : "生产数据读取成功(新建连接)";
return (true, prodData, successMsg);
}
catch (Exception ex)
{
// 精细化异常日志便于定位具体PLC故障
Console.WriteLine($"PLC({ip})生产数据读取异常:{ex.Message}\n{ex.StackTrace}");
return (false, null, $"生产数据读取失败:{ex.Message}");
}
finally
{
// 7. 资源安全释放(核心:复用连接不释放,仅新建连接释放;必须释放并发信号量)
if (!isConnReused)
{
ReleasePlcConnection(plc);
}
_concurrencySemaphore.Release(); // 防止死锁的关键
}
}
// 可选:添加连接池清理方法(防止长期运行导致连接泄露)
private void CleanupExpiredConnections()
{
var expiredTime = DateTime.Now.AddMinutes(-5); // 5分钟未使用的连接清理
// 先复制所有Key到列表避免遍历中修改原集合
var allKeys = _plcConnPool.Keys.ToList();
foreach (var key in allKeys)
{
if (!_plcConnPool.TryGetValue(key, out var poolItem))
continue;
if (poolItem.LastUsedTime < expiredTime)
{
if (_plcConnPool.TryRemove(key, out var removedItem))
{
try
{
if (removedItem.Client != null && removedItem.Client.IsConnected)
removedItem.Client.Close();
if (removedItem.Client is IDisposable disposable)
disposable.Dispose();
// 可选置空引用帮助GC回收
removedItem.Client = null;
}
catch
{
// 静默释放,避免清理失败影响主流程
}
}
}
}
}
// 在类的构造函数中启动定时清理(可选)
// public PlcService(IOptions<GlobalPlcConfig> globalConfig)
// {
// _globalConfig = globalConfig?.Value ?? throw new ArgumentNullException(nameof(globalConfig));
// _plcConfigs = initPlcConfigs(_plcConfigs);
// // 每30秒清理一次过期连接
// new Timer(_ => CleanupExpiredConnections(), null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
// }
/// <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();
// 正确释放PLC实例适配S7.Net显式实现IDisposable
if (plc is IDisposable disposable)
disposable.Dispose();
}
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/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;
}
#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
private List<PlcConfig> initPlcConfigs(List<PlcConfig> result)
{
var defaultResult = result ?? new List<PlcConfig>();
try
{
List<PlcConfig> query = _plantWorkstationService.Queryable()
.Where(it => it.Status == 1)
.Select(it => new PlcConfig
{
PlcName = it.WorkstationCode,
Ip = it.PlcIP,
Rack = (short)it.Rack, // 直接强制转换it.Rack是int非空
Slot = (short)it.Slot // 同理it.Slot是int非空
})
.ToList();
return query.Count > 0 ? query : defaultResult;
}
catch (Exception ex)
{
Console.WriteLine($"初始化PLC配置异常{ex.Message}");
return defaultResult;
}
}
}
}