// 统一引入所有必要命名空间 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 { /// /// PLC通信服务(封装连接、读写、生产数据采集等功能) /// public class PlcService : IDisposable { #region 字段与构造函数 // 标记是否已释放资源,避免重复释放 private bool _disposed = false; // PLC配置参数(从配置文件注入) private readonly List _plcConfigs; private readonly GlobalPlcConfig _globalConfig; private PlcProductionDataService plcProductionDataService = new PlcProductionDataService(); // PLC地址映射(严格匹配业务地址清单) private readonly Dictionary _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 _plcIntMap = new() { { "QualificationFlag", "DB1010.DBW224" }, { "ReworkFlag", "DB1010.DBW226" }, { "ProductionCycle", "DB1010.DBW228" }, { "AutoManual", "DB1010.DBW230" }, { "RunStatus", "DB1010.DBW232" } }; /// /// 构造函数(依赖注入获取PLC配置) /// /// 所有PLC的连接配置 /// PLC全局超时配置 public PlcService(IOptions> plcConfigs, IOptions globalConfig) { _plcConfigs = plcConfigs?.Value ?? throw new ArgumentNullException(nameof(plcConfigs), "PLC配置不能为空"); _globalConfig = globalConfig?.Value ?? throw new ArgumentNullException(nameof(globalConfig), "PLC全局配置不能为空"); } #endregion #region 核心业务方法 /// /// 读取PLC生产数据(严格匹配业务地址和解析规则) /// /// PLC IP地址 /// 机架号 /// 槽位号 /// PLC型号(默认S7-1500) /// 读取结果(状态+数据+消息) 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); } } /// /// 测试单个PLC的连接、读、写功能 /// /// 单个PLC的配置参数 /// 取消令牌 /// 测试结果 public async Task 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; } /// /// 批量测试配置文件中所有PLC的读写功能 /// /// 取消令牌 /// 所有PLC的测试结果列表 public async Task> 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(); } /// /// 单独读取指定PLC的某个地址数据 /// /// PLC的IP地址 /// 机架号 /// 槽位号 /// 读取地址(如DB1.DBD0) /// PLC型号 /// 读取结果(成功状态、值、消息) 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); } } /// /// 单独写入数据到指定PLC的某个地址 /// /// PLC的IP地址 /// 机架号 /// 槽位号 /// 写入地址(如DB1.DBD0) /// 写入的值(字符串格式) /// PLC型号 /// 写入结果(成功状态、消息) 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 私有辅助方法 /// /// 创建PLC客户端实例(统一配置超时) /// 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; } /// /// 异步打开PLC连接(封装同步方法为异步) /// private async Task OpenPlcConnectionAsync(Plc plc) { if (plc == null) return; await Task.Run(() => plc.Open()); } /// /// 释放PLC连接(安全关闭+资源释放) /// 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(); } } /// /// 按业务规则解析PLC字符串(支持中文GBK编码) /// private async Task 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; } } /// /// 修复版:异步读取PLC整数(DBW地址,16位短整型) /// 增强类型兼容、异常日志、地址校验 /// /// PLC客户端实例 /// 读取地址(如DB1010.DBW226) /// 解析后的整数值,读取失败返回0 private async Task 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; } } /// /// 格式化PLC读取的值,转为友好的字符串 /// 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() // 其他类型直接转字符串 }; } /// /// 根据地址类型转换值并写入PLC /// 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 资源释放 /// /// 实现IDisposable接口,释放资源 /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); // 通知GC无需调用终结器 } /// /// 实际释放资源的逻辑(区分托管/非托管资源) /// /// 是否释放托管资源 protected virtual void Dispose(bool disposing) { if (_disposed) return; // 避免重复释放 // 释放托管资源 if (disposing) { // 此处无长期持有的PLC连接,若有可在此统一释放 } // 标记为已释放 _disposed = true; } /// /// 终结器(防忘记手动Dispose) /// ~PlcService() { Dispose(false); } #endregion } }