From 96abcd22326c532d006e04556aab43919b3b1fb0 Mon Sep 17 00:00:00 2001 From: quowingwang Date: Fri, 23 Jan 2026 14:50:37 +0800 Subject: [PATCH] =?UTF-8?q?PLC=E6=95=B0=E6=8D=AE=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PLC/Model/Dto/PlcProductionDataDto.cs | 16 +- RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs | 10 + .../PLC/Model/PlcProductionData.cs | 67 +- .../PLC/Service/PlcProductionDataService.cs | 16 - RIZO.Admin.WebApi/PLC/Service/PlcService.cs | 767 +++++++++++++----- RIZO.Service/MES/alarm/AlarmInfoService.cs | 1 + 6 files changed, 622 insertions(+), 255 deletions(-) diff --git a/RIZO.Admin.WebApi/PLC/Model/Dto/PlcProductionDataDto.cs b/RIZO.Admin.WebApi/PLC/Model/Dto/PlcProductionDataDto.cs index b0355f4..5a9d454 100644 --- a/RIZO.Admin.WebApi/PLC/Model/Dto/PlcProductionDataDto.cs +++ b/RIZO.Admin.WebApi/PLC/Model/Dto/PlcProductionDataDto.cs @@ -27,18 +27,16 @@ namespace RIZO.Admin.WebApi.PLC.Model.Dto public int Id { get; set; } public string? LineCode { get; set; } - + public string? WorkstationCode { get; set; } public string? PlcIp { get; set; } public string? ProductCode { get; set; } public string? ProductName { get; set; } - public string? PartCode { get; set; } + public string? ProductModel { get; set; } - public string? PartName { get; set; } - - public string? ProcessName { get; set; } + public string? WorkstationName { get; set; } public string? ParamName { get; set; } @@ -53,9 +51,17 @@ namespace RIZO.Admin.WebApi.PLC.Model.Dto public int? ProductionCycle { get; set; } public int? AutoManual { get; set; } public int? RunStatus { get; set; } + public string? TrayNo { get; set; } + public string? ProduceModel { get; set; } + + public string? OnlineStatus { get; set; } public string? Remark { get; set; } + public string? SN1 { get; set; } + public string? SN2 { get; set; } + public string? CameraResult { get; set; } + public string? CreatedBy { get; set; } public DateTime? CreatedTime { get; set; } diff --git a/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs b/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs index a0281bb..c45e0a3 100644 --- a/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs +++ b/RIZO.Admin.WebApi/PLC/Model/PlcConfig.cs @@ -26,6 +26,16 @@ public string ReadMessage { get; set; } public bool WriteSuccess { get; set; } public string WriteMessage { get; set; } + + //设备运行状态 + public string RunStatus { get; set; } + //设备模式 + public string MachineModel { get; set; } + //设备在线模式 + public string OnLineStatus { get; set; } + //设备生产模式 + public string ProduceModel { get; set; } + } } diff --git a/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs b/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs index bbb5234..2dc8ee0 100644 --- a/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs +++ b/RIZO.Admin.WebApi/PLC/Model/PlcProductionData.cs @@ -21,6 +21,18 @@ namespace RIZO.Admin.WebApi.PLC.Model [SugarColumn(ColumnName = "line_code")] public string LineCode { get; set; } + /// + /// 工站编码 + /// + [SugarColumn(ColumnName = "workstationCode")] + public string WorkstationCode { get; set; } + + /// + /// 工站名称 + /// + [SugarColumn(ColumnName = "workstationName")] + public string WorkstationName { get; set; } + /// /// PLC IP地址 /// @@ -40,22 +52,10 @@ namespace RIZO.Admin.WebApi.PLC.Model public string ProductName { get; set; } /// - /// 零件编码 + /// 产品型号 /// - [SugarColumn(ColumnName = "part_code")] - public string PartCode { get; set; } - - /// - /// 零件名称 - /// - [SugarColumn(ColumnName = "part_name")] - public string PartName { get; set; } - - /// - /// 工序名称 - /// - [SugarColumn(ColumnName = "process_name")] - public string ProcessName { get; set; } + [SugarColumn(ColumnName = "product_model")] + public string ProductModel { get; set; } /// /// 参数名称 @@ -100,11 +100,28 @@ namespace RIZO.Admin.WebApi.PLC.Model public int? AutoManual { get; set; } /// - /// 运行状态:1正常,2异常 + /// 运行状态:1=空闲,2=运行中,3=故障; /// [SugarColumn(ColumnName = "runstatus")] public int? RunStatus { get; set; } + /// + /// 托盘号 + /// + [SugarColumn(ColumnName = "trayNo")] + public string TrayNo { get; set; } + + /// + /// 生产模式 + /// + [SugarColumn(ColumnName = "produceModel")] + public string ProduceModel { get; set; } + + /// + /// 设备在线状态1=离线,0=在线 + /// + [SugarColumn(ColumnName = "onlineStatus")] + public string OnlineStatus { get; set; } /// /// 备注 /// @@ -134,5 +151,23 @@ namespace RIZO.Admin.WebApi.PLC.Model [SugarColumn(ColumnName = "uPDATED_TIME")] public DateTime? UpdatedTime { get; set; } + /// + /// 条码查询 + /// + [SugarColumn(ColumnName = "SN_1")] + public string SN1 { get; set; } + + /// + /// 结果上传条码 + /// + [SugarColumn(ColumnName = "SN_2")] + public string SN2 { get; set; } + + /// + /// 相机结果1,OK,2,NG + /// + [SugarColumn(ColumnName = "cameraResult")] + public string CameraResult { get; set; } + } } \ No newline at end of file diff --git a/RIZO.Admin.WebApi/PLC/Service/PlcProductionDataService.cs b/RIZO.Admin.WebApi/PLC/Service/PlcProductionDataService.cs index 3026bf8..fe74401 100644 --- a/RIZO.Admin.WebApi/PLC/Service/PlcProductionDataService.cs +++ b/RIZO.Admin.WebApi/PLC/Service/PlcProductionDataService.cs @@ -33,14 +33,6 @@ namespace RIZO.Admin.WebApi.PLC.Service { predicate = predicate.And(x => x.ProductCode == parm.ProductCode); } - if (!string.IsNullOrEmpty(parm.PartName)) - { - predicate = predicate.And(x => x.PartName == parm.PartName); - } - if (!string.IsNullOrEmpty(parm.PartCode)) - { - predicate = predicate.And(x => x.PartCode == parm.PartCode); - } if (!string.IsNullOrEmpty(parm.ReworkFlag)) { predicate = predicate.And(x => x.ReworkFlag == parm.ReworkFlag); @@ -78,14 +70,6 @@ namespace RIZO.Admin.WebApi.PLC.Service { predicate = predicate.And(x => x.ProductCode == parm.ProductCode); } - if (!string.IsNullOrEmpty(parm.PartName)) - { - predicate = predicate.And(x => x.PartName == parm.PartName); - } - if (!string.IsNullOrEmpty(parm.PartCode)) - { - predicate = predicate.And(x => x.PartCode == parm.PartCode); - } if (!string.IsNullOrEmpty(parm.ReworkFlag)) { predicate = predicate.And(x => x.ReworkFlag == parm.ReworkFlag); diff --git a/RIZO.Admin.WebApi/PLC/Service/PlcService.cs b/RIZO.Admin.WebApi/PLC/Service/PlcService.cs index 590d373..8f3c2f9 100644 --- a/RIZO.Admin.WebApi/PLC/Service/PlcService.cs +++ b/RIZO.Admin.WebApi/PLC/Service/PlcService.cs @@ -1,6 +1,8 @@ // 统一引入所有必要命名空间 using MDM.Services.Plant; using Microsoft.Extensions.Options; +using NPOI.SS.Formula.Functions; +using Quartz; using RIZO.Admin.WebApi.PLC.Model; using RIZO.Common; using S7.Net; @@ -62,28 +64,34 @@ namespace RIZO.Admin.WebApi.PLC.Service #region PLC地址块儿映射 // OP070-1 专属地址映射 - private readonly Dictionary _op070_1IntMap = new Dictionary + private readonly Dictionary _op070_1StringMap = new() { - { "运行状态", "DB1001.DBW0" }, // 运行状态 - { "设备模式", "DB1001.DBW2" }, // 设备模式 - { "设备在线状态", "DB1001.DBW4" },// 设备在线状态 - { "ByPass", "DB1001.DBW6" }, // ByPass - { "生产模式", "DB1001.DBW8" }, // 生产模式 - { "查询请求", "DB1001.DBW2000" }, // 查询请求 - { "保存请求", "DB1001.DBW2002" }, // 保存请求 - { "托盘号", "DB1001.DBW2004" }, // 托盘号 - { "相机结果", "DB1001.DBW2164" }, // 相机结果 - { "站位结果", "DB1001.DBW2166" } // 站位结果 + { "报警信息", ("DB1001.DBB58", 48) }, // Array[1..48] of Byte + { "产品型号_48", ("DB1001.DBB1000", 48) }, // String[48] + { "产品名称", ("DB1001.DBB1054", 48) }, // String[48] + { "产品型号_28", ("DB1001.DBB2006", 28) }, // String[28] + { "SN_1", ("DB1001.DBB2100", 28) }, // String[28] + { "SN_2", ("DB1001.DBB2134", 28) } // String[28] }; - private readonly Dictionary _op070_1StringMap = new Dictionary + private readonly Dictionary _op070_1IntMap = new() { - { "报警信息", ("DB1001.DBB58", 48) }, // Array[1..48] of Byte - { "产品名称", ("DB1001.DBB1054", 48) }, // String[48] - { "产品型号", ("DB1001.DBB2006", 28) }, // String[28] - { "SN_1", ("DB1001.DBB2100", 28) }, // 条码查询 - { "SN_2", ("DB1001.DBB2134", 28) } // 结果上传条码 + { "运行状态", "DB1001.DBW0" }, // Int + { "设备模式", "DB1001.DBW2" }, // Int + { "设备在线状态", "DB1001.DBW4" }, // Int + { "ByPass", "DB1001.DBW6" }, // Int + { "生产模式", "DB1001.DBW8" }, // Int + { "实际产量", "DB1001.DBD1104" }, // DInt + { "合格数量", "DB1001.DBD1108" }, // DInt + { "失败数量", "DB1001.DBD1112" }, // DInt + { "查询请求", "DB1001.DBW2000" }, // Int + { "保存请求", "DB1001.DBW2002" }, // Int + { "托盘号", "DB1001.DBW2004" }, // Int + { "相机结果", "DB1001.DBW2164" }, // Int + { "站位结果", "DB1001.DBW2166" }, // Int + { "节拍时间", "DB1001.DBD2168" } // Real }; + #endregion /// /// 构造函数(依赖注入获取PLC配置) @@ -99,83 +107,6 @@ namespace RIZO.Admin.WebApi.PLC.Service #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(), - // //设备自动手动: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); - // } - //} - - /// /// 读取PLC生产数据(优化版:连接池+并发控制+批量读取+异常防护) @@ -185,6 +116,173 @@ namespace RIZO.Admin.WebApi.PLC.Service /// 槽位号 /// PLC型号(默认S7-1500) /// 读取结果(状态+数据+消息) + //public async Task<(bool Success, PlcProductionData Data, string Message)> ReadProductionDataAsync( + // string ip, + // string plcName, + // 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查询请求标志未设置,跳过生产数据读取"); + // } + // if (plcName == "OP070-1") + // { + + + // } + // // 4. 批量并行读取(核心优化:替代串行await,提升效率30%+) + // // 4.1 字符串字段批量读取 + // var stringTasks = new Dictionary> + // { + // { "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> + // { + // { "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 allTasks = stringTasks.Values.Cast() + // .Concat(intTasks.Values.Cast()); + // 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(); // 防止死锁的关键 + // } + //} + + /// + /// 读取PLC生产数据(优化版:连接池+并发控制+OP070-1/OP080专属适配) + /// + /// PLC IP地址 + /// PLC名称(支持OP070-1/OP080) + /// 机架号 + /// 槽位号 + /// PLC型号(默认S7-1500) + /// 读取结果(状态+数据+消息) public async Task<(bool Success, PlcProductionData Data, string Message)> ReadProductionDataAsync( string ip, string plcName, @@ -192,157 +290,311 @@ namespace RIZO.Admin.WebApi.PLC.Service short slot, CpuType cpuType = CpuType.S71500) { - // 1. 强化参数校验(源头避免无效请求) + // 1. 强化参数校验(补充PLC名称校验+白名单控制) if (string.IsNullOrWhiteSpace(ip)) return (false, null, "PLC IP地址不能为空"); - if (rack < 0 || rack > 10) // 工业场景机架号常规范围 + if (string.IsNullOrWhiteSpace(plcName)) + return (false, null, "PLC名称不能为空"); + if (rack < 0 || rack > 10) return (false, null, $"PLC机架号{rack}无效(有效值0-10)"); - if (slot < 0 || slot > 4) // 工业场景槽位号常规范围 + if (slot < 0 || slot > 4) return (false, null, $"PLC槽位号{slot}无效(有效值0-4)"); - Plc plc = null; - bool isConnReused = false; // 标记是否复用连接 - var poolKey = $"{ip}_{rack}_{slot}_{cpuType}"; // 连接池唯一标识 + // 仅放行指定工位,防止非法请求 + var supportedPlcs = new HashSet { "OP070-1", "OP080" }; + if (!supportedPlcs.Contains(plcName)) + return (false, null, $"仅支持OP070-1/OP080,当前工位:{plcName}"); - // 2. 并发控制(防止50台PLC同时访问导致端口/线程耗尽) + Plc plc = null; + bool isConnReused = false; + var poolKey = $"{ip}_{rack}_{slot}_{cpuType}"; + PlcProductionData prodData = null; + + // 2. 并发控制(核心保留,防止端口/线程耗尽) await _concurrencySemaphore.WaitAsync(); try { - // 3. 连接池复用(核心优化:避免高频创建/销毁连接) + // 3. 连接池复用+精简重试逻辑(提升可读性) if (_plcConnPool.TryGetValue(poolKey, out var poolItem) && poolItem.Client.IsConnected) { plc = poolItem.Client; - _plcConnPool.TryUpdate(poolKey, (plc, DateTime.Now), poolItem); // 更新最后使用时间 + _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) + // for循环替代while,逻辑更直观 + for (int retry = 0; retry < 2; retry++) { try { await OpenPlcConnectionAsync(plc); isConnected = plc.IsConnected; + if (isConnected) break; } catch { - if (retryCount > 0) await Task.Delay(100); // 重试间隔 + if (retry < 1) await Task.Delay(100); // 仅第一次失败后重试 } } if (!isConnected) - return (false, null, "PLC连接失败(含2次重试)"); + return (false, null, $"{plcName}连接失败(含2次重试)"); - // 新连接加入池 _plcConnPool.TryAdd(poolKey, (plc, DateTime.Now)); } - //先读取查询请求标志,判断是否需要读取生产数据 - int iQueryRequest = await ReadPlcIntAsync(plc, _plcIntMap["QueryRequest"]); - if (iQueryRequest != 1) + // 4. 多工位查询请求标志适配(核心:区分不同工位的寄存器地址) + int iQueryRequest = 0; + int iSaveRequest = 0; + if (plcName == "OP070-1" || plcName == "OP070-2" || plcName == "OP070-3" || plcName == "OP075") { - return (false, null, "PLC查询请求标志未设置,跳过生产数据读取"); + iQueryRequest = await ReadPlcIntAsync(plc, _op070_1IntMap["查询请求"]); + iSaveRequest = await ReadPlcIntAsync(plc, _op070_1IntMap["保存请求"]); } + // 无效请求直接返回(避免无谓资源消耗) + if (iQueryRequest != 1 && iSaveRequest != 1) + { + return (false, null, $""); + } + + if (iQueryRequest == 1) + { + + } + // 5. 多工位专属数据读取(预留扩展,逻辑隔离) if (plcName == "OP070-1") - { - - - } - // 4. 批量并行读取(核心优化:替代串行await,提升效率30%+) - // 4.1 字符串字段批量读取 - var stringTasks = new Dictionary> { - { "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> - { - { "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 allTasks = stringTasks.Values.Cast() - .Concat(intTasks.Values.Cast()); - 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 (iSaveRequest == 1) { - if (t.IsFaulted) - { - // 记录保存失败日志(建议替换为ILogger) - Console.WriteLine($"PLC({ip})数据保存失败:{t.Exception?.InnerException?.Message}"); - } - }); + prodData = await ReadOP070_1DataAsync(plc, ip, plcName); + } + } + else if (plcName == "OP080") + { + //prodData = await ReadOP080DataAsync(plc, ip); + } - var successMsg = isConnReused ? "生产数据读取成功(复用连接)" : "生产数据读取成功(新建连接)"; + // 6. 统一空值兜底(避免空引用) + if (prodData != null) + { + prodData.QualificationFlag ??= "0"; + prodData.ReworkFlag ??= "0"; + prodData.ProductionCycle ??= 0; + + // 7. 异步保存数据(日志补充PLC名称,便于定位) + _ = Task.Run(() => _plcProductionDataService.AddPlcProductionData(prodData)) + .ContinueWith(t => + { + if (t.IsFaulted) + { + Console.WriteLine($"{plcName}({ip})数据保存失败:{t.Exception?.InnerException?.Message}"); + } + }); + } + + // 8. 个性化返回消息(区分工位和连接类型) + var successMsg = isConnReused + ? $"{plcName}生产数据读取成功(复用连接)" + : $"{plcName}生产数据读取成功(新建连接)"; return (true, prodData, successMsg); } catch (Exception ex) { - // 精细化异常日志(便于定位具体PLC故障) - Console.WriteLine($"PLC({ip})生产数据读取异常:{ex.Message}\n{ex.StackTrace}"); - return (false, null, $"生产数据读取失败:{ex.Message}"); + // 精细化异常日志(包含PLC名称,快速定位故障) + Console.WriteLine($"{plcName}({ip})生产数据读取异常:{ex.Message}\n{ex.StackTrace}"); + return (false, null, $"{plcName}生产数据读取失败:{ex.Message}"); } finally { - // 7. 资源安全释放(核心:复用连接不释放,仅新建连接释放;必须释放并发信号量) + // 资源安全释放(核心:防止死锁/连接泄漏) if (!isConnReused) { ReleasePlcConnection(plc); } - _concurrencySemaphore.Release(); // 防止死锁的关键 + _concurrencySemaphore.Release(); } } + #region 工位专属读取方法(扩展友好,逻辑隔离) + /// + /// OP070-1专属数据读取(精简版:高效+易读) + /// + //private async Task ReadOP070_1DataAsync(Plc plc, string ip) + //{ + // // 1. 批量并行读取字符串字段 + // // 合并并行读取 + // var (strFields, intFields) = await Task.Run(async () => ( + // // 字符串字段 + // ( + // await ReadPlcStringAsync(plc, _op070_1StringMap["报警信息"].Addr, _op070_1StringMap["报警信息"].Len), + // await ReadPlcStringAsync(plc, _op070_1StringMap["产品名称"].Addr, _op070_1StringMap["产品名称"].Len), + // await ReadPlcStringAsync(plc, _op070_1StringMap["产品型号"].Addr, _op070_1StringMap["产品型号"].Len), + // await ReadPlcStringAsync(plc, _op070_1StringMap["SN_1"].Addr, _op070_1StringMap["SN_1"].Len), + // await ReadPlcStringAsync(plc, _op070_1StringMap["SN_2"].Addr, _op070_1StringMap["SN_2"].Len) + // ), + // // 整数字段 + // ( + // await ReadPlcIntAsync(plc, _op070_1IntMap["运行状态"]), + // await ReadPlcIntAsync(plc, _op070_1IntMap["设备模式"]), + // await ReadPlcIntAsync(plc, _op070_1IntMap["设备在线状态"]), + // await ReadPlcIntAsync(plc, _op070_1IntMap["ByPass"]), + // await ReadPlcIntAsync(plc, _op070_1IntMap["生产模式"]), + // await ReadPlcIntAsync(plc, _op070_1IntMap["托盘号"]), + // await ReadPlcIntAsync(plc, _op070_1IntMap["站位结果"]) + // ) + // )); + + // // 2.解构字段(保持原有逻辑) + // var (alarmMsg, productName, product_model, sn1, product_code) = strFields; + // var (runStatus, machineModel, onlineStatus, byPass, produceModel, trayNo, stationResult) = intFields; + + // // 3. 写入保存请求(极简异常防护,不影响主流程) + // try { WritePlcValue(plc, _op070_1IntMap["保存请求"], "1"); } + // catch (Exception ex) { Console.WriteLine($"OP070-1({ip})写保存请求失败:{ex.Message}"); } + + // // 4. 极简条件计算(一行搞定,易读高效) + // var reworkFlag = produceModel == 4 ? "1" : "0"; // 返工标志 + // string produceModelDesc = produceModel switch // 生产模式映射 + // { + // 1 => "正常模式", + // 2 => "清线模式", + // 4 => "返工模式", + // 8 => "换型模式", + // 16 => "预热模式", + // _ => $"未知({produceModel})" + // }; + + // // 5. 构建数据实体(极简空值兜底,逻辑清晰) + // return new PlcProductionData + // { + // //加一个产品型号 + // PlcIp = ip.Trim(), + // OccurTime = DateTime.Now, + // LineCode = "line2", + // WorkstationCode = "OP070-1", + // ProductModel = product_model ?? "", + // ProductName = productName ?? "", + // ProductCode = product_code, + // QualificationFlag = stationResult.ToString(), + // ReworkFlag = reworkFlag, + // AutoManual = machineModel, + // RunStatus = runStatus, + // OnlineStatus = onlineStatus.ToString(), + // TrayNo = trayNo.ToString(), + // ProduceModel = produceModelDesc, + // CreatedBy = "PLC", + // CreatedTime = DateTime.Now + // }; + //} + + /// + /// 优化版OP070-1数据读取(集成增强型PLC读写方法) + /// + private async Task ReadOP070_1DataAsync(Plc plc, string ip,string workstationCode) + { + // 1. 批量并行读取所有字段(沿用原有结构+增强型读取方法) + var (strFields, intFields, dintFields, realFields) = await Task.Run(async () => ( + // 字符串字段(增强空值和异常处理) + ( + await ReadPlcStringAsync(plc, _op070_1StringMap["报警信息"].Addr, _op070_1StringMap["报警信息"].Len), + await ReadPlcStringAsync(plc, _op070_1StringMap["产品名称"].Addr, _op070_1StringMap["产品名称"].Len), + await ReadPlcStringAsync(plc, _op070_1StringMap["产品型号_48"].Addr, _op070_1StringMap["产品型号_48"].Len), + await ReadPlcStringAsync(plc, _op070_1StringMap["产品型号_28"].Addr, _op070_1StringMap["产品型号_28"].Len), + await ReadPlcStringAsync(plc, _op070_1StringMap["SN_1"].Addr, _op070_1StringMap["SN_1"].Len), + await ReadPlcStringAsync(plc, _op070_1StringMap["SN_2"].Addr, _op070_1StringMap["SN_2"].Len) + ), + // Int字段(使用增强版ReadPlcIntAsync) + ( + await ReadPlcIntAsync(plc, _op070_1IntMap["运行状态"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["设备模式"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["设备在线状态"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["ByPass"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["生产模式"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["查询请求"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["保存请求"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["托盘号"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["相机结果"]), + await ReadPlcIntAsync(plc, _op070_1IntMap["站位结果"]) + ), + // DInt字段(增强版读取方法) + ( + await ReadPlcDIntAsync(plc, _op070_1IntMap["实际产量"]), + await ReadPlcDIntAsync(plc, _op070_1IntMap["合格数量"]), + await ReadPlcDIntAsync(plc, _op070_1IntMap["失败数量"]) + ), + // Real字段(增强版读取方法) + ( + await ReadPlcRealAsync(plc, _op070_1IntMap["节拍时间"]) + ) + )); + + // 2. 解构字段(保持原有逻辑,空值兜底) + var (alarmMsg, productName, productModel48, productModel28, sn1, sn2) = strFields; + var (runStatus, machineModel, onlineStatus, byPass, produceModel, queryReq, saveReq, trayNo, cameraResult, stationResult) = intFields; + var (actualOutput, qualifiedQty, failedQty) = dintFields; + float cycleTime = realFields; + + // 3. 写入保存请求(异步+增强异常日志) + try { WritePlcValue(plc, _op070_1IntMap["保存请求"], "1"); } + catch (Exception ex) { Console.WriteLine($"OP070-1({ip})写保存请求失败:{ex.Message}"); } + + // 4. 极简条件计算(增强类型安全) + var reworkFlag = produceModel == 4 ? "1" : "0"; + string produceModelDesc = produceModel switch + { + 1 => "正常模式", + 2 => "清线模式", + 4 => "返工模式", + 8 => "换型模式", + 16 => "预热模式", + _ => $"未知({produceModel})" + }; + string runStatusDesc = runStatus switch { 1 => "空闲", 2 => "运行中", 3 => "故障", _ => $"未知({runStatus})" }; + string onlineStatusDesc = onlineStatus == 1 ? "离线" : "在线"; + //string byPassDesc = byPass == 1 ? "旁路模式" : "正常模式"; + string qualificationFlag = stationResult switch { 1 => "1", 2 => "0", _ => stationResult.ToString() }; + string cameraResultDesc = cameraResult switch { 1 => "OK", 2 => "NG", _ => $"未知({cameraResult})" }; + + // 调试日志:输出关键读取结果 + Console.WriteLine($"OP070-1({ip})读取结果:产品型号={productModel28 ?? productModel48},运行状态={runStatusDesc},产量={actualOutput}"); + + // 5. 构建数据实体(增强空值处理) + return new PlcProductionData + { + PlcIp = ip.Trim(), + OccurTime = DateTime.Now, + LineCode = "line2", + WorkstationCode = workstationCode, + ProductModel = productModel28 ?? productModel48 ?? string.Empty, + ProductName = productName ?? string.Empty, + ProductCode = sn2 ?? string.Empty, + SN1 = sn1 ?? string.Empty, + SN2 = sn2 ?? string.Empty, + QualificationFlag = qualificationFlag, + ReworkFlag = reworkFlag, + AutoManual = machineModel, + RunStatus = runStatus, + OnlineStatus = onlineStatusDesc, + //ByPass = byPassDesc, + ProduceModel = produceModelDesc, + TrayNo = trayNo.ToString(), + CameraResult = cameraResultDesc, + //ActualOutput = actualOutput, + //QualifiedQuantity = qualifiedQty, + //FailedQuantity = failedQty, + ProductionCycle = (int?)cycleTime, + //AlarmMsg = alarmMsg ?? string.Empty, + CreatedBy = "PLC", + CreatedTime = DateTime.Now + }; + } + + #endregion + // 可选:添加连接池清理方法(防止长期运行导致连接泄露) private void CleanupExpiredConnections() { @@ -394,25 +646,21 @@ namespace RIZO.Admin.WebApi.PLC.Service /// 测试结果 public async Task TestSinglePlcAsync(PlcConfig config, CancellationToken cancellationToken = default) { - // 参数校验 + // 1. 极简参数校验(提前返回,减少嵌套) 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 - }; - + var result = new PlcTestResult { PlcName = config.PlcName, Ip = config.Ip }; Plc plc = null; + try { - // 初始化PLC客户端 + // 2. 初始化PLC客户端 plc = CreatePlcClient(CpuType.S71500, config.Ip, config.Rack, config.Slot); - // 带超时的连接测试 + // 3. 带超时的连接测试(精简逻辑) var connectTask = OpenPlcConnectionAsync(plc); if (await Task.WhenAny(connectTask, Task.Delay(_globalConfig.ReadWriteTimeout, cancellationToken)) != connectTask) { @@ -422,41 +670,34 @@ namespace RIZO.Admin.WebApi.PLC.Service } await connectTask; - // 检查连接状态 - if (plc.IsConnected) - { - result.ConnectSuccess = true; - result.ConnectMessage = "连接成功"; - } - else + // 4. 连接状态校验(精简分支) + if (!plc.IsConnected) { result.ConnectSuccess = false; result.ConnectMessage = "连接失败(PLC未返回连接状态)"; return result; } + result.ConnectSuccess = true; + result.ConnectMessage = "连接成功"; - string TestAddress = "DB1010.DBB238"; // 测试地址 - - // 读取测试 + const 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 = "读取成功"; + result.ReadMessage = "通用地址读取成功"; } catch (Exception ex) { result.ReadSuccess = false; - result.ReadMessage = $"读取失败:{ex.Message}"; + result.ReadMessage = $"通用地址读取失败:{ex.Message}"; } - } - // 写入测试 - if (!string.IsNullOrWhiteSpace(TestAddress) && !string.IsNullOrWhiteSpace(TestAddress)) - { + // 写入测试 try { bool writeOk = await Task.Run(() => WritePlcValue(plc, TestAddress, "1"), cancellationToken); @@ -469,6 +710,30 @@ namespace RIZO.Admin.WebApi.PLC.Service result.WriteMessage = $"写入失败:{ex.Message}"; } } + + // 6. OP070-1专属状态读取(删除双层判断+内联转换逻辑) + if (config.PlcName == "OP070-1") + { + try + { + int runStatus = await ReadPlcIntAsync(plc, _op070_1IntMap["运行状态"]); + int machineModel = await ReadPlcIntAsync(plc, _op070_1IntMap["设备模式"]); + int onlineStatus = await ReadPlcIntAsync(plc, _op070_1IntMap["设备在线状态"]); + int produceModel = await ReadPlcIntAsync(plc, _op070_1IntMap["生产模式"]); + + result.RunStatus = runStatus switch { 1 => "空闲", 2 => "运行中", 3 => "故障", _ => $"未知({runStatus})" }; + result.MachineModel = machineModel switch { 1 => "空模式", 2 => "手动", 4 => "初始化", 8 => "自动", 16 => "CycleStop", _ => $"未知({machineModel})" }; + result.OnLineStatus = onlineStatus == 1 ? "离线" : (onlineStatus == 0 ? "在线" : $"未知({onlineStatus})"); + result.ProduceModel = produceModel switch { 1 => "正常模式", 2 => "清线模式", 4 => "返工模式", 8 => "换型模式", 16 => "预热模式", _ => $"未知({produceModel})" }; + + result.ReadMessage = $"{result.ReadMessage}; OP070-1状态读取成功"; + } + catch (Exception ex) + { + result.RunStatus = result.MachineModel = result.OnLineStatus = result.ProduceModel = "读取失败"; + result.ReadMessage = $"{result.ReadMessage}; OP070-1状态读取失败:{ex.Message}"; + } + } } catch (OperationCanceledException) { @@ -617,10 +882,25 @@ namespace RIZO.Admin.WebApi.PLC.Service /// /// 异步打开PLC连接(封装同步方法为异步) /// - private async Task OpenPlcConnectionAsync(Plc plc) + // 优化后的OpenPlcConnectionAsync + private async Task OpenPlcConnectionAsync(Plc plc) { - if (plc == null) return; - await Task.Run(() => plc.Open()); + if (plc == null) return false; + try + { + // 带超时的连接操作 + var openTask = Task.Run(() => plc.Open()); + if (await Task.WhenAny(openTask, Task.Delay(_globalConfig.ReadWriteTimeout)) != openTask) + { + return false; + } + await openTask; + return plc.IsConnected; + } + catch + { + return false; + } } /// @@ -736,7 +1016,58 @@ namespace RIZO.Admin.WebApi.PLC.Service return 0; } } + private async Task ReadPlcDIntAsync(Plc plc, string addr) + { + if (plc == null || string.IsNullOrWhiteSpace(addr)) + { + Console.WriteLine($"DInt读取失败:参数无效(地址:{addr})"); + return 0; + } + try + { + var val = await Task.Run(() => plc.Read(addr)); + Console.WriteLine($"DInt地址[{addr}] 原始值:{val},类型:{val?.GetType().Name}"); + return val switch + { + int i => i, + long l => (int)l, + uint ui => (int)ui, + ulong ul => (int)ul, + _ => 0 + }; + } + catch (Exception ex) + { + Console.WriteLine($"DInt读取失败(地址:{addr}):{ex.Message}"); + return 0; + } + } + private async Task ReadPlcRealAsync(Plc plc, string addr) + { + if (plc == null || string.IsNullOrWhiteSpace(addr)) + { + Console.WriteLine($"Real读取失败:参数无效(地址:{addr})"); + return 0.0f; + } + try + { + var val = await Task.Run(() => plc.Read(addr)); + Console.WriteLine($"Real地址[{addr}] 原始值:{val},类型:{val?.GetType().Name}"); + return val switch + { + float f => f, + double d => (float)d, + int i => i, + _ => 0.0f + }; + } + catch (Exception ex) + { + Console.WriteLine($"Real读取失败(地址:{addr}):{ex.Message}"); + return 0.0f; + } + } /// /// 格式化PLC读取的值,转为友好的字符串 /// diff --git a/RIZO.Service/MES/alarm/AlarmInfoService.cs b/RIZO.Service/MES/alarm/AlarmInfoService.cs index 61f0450..ce93c72 100644 --- a/RIZO.Service/MES/alarm/AlarmInfoService.cs +++ b/RIZO.Service/MES/alarm/AlarmInfoService.cs @@ -50,6 +50,7 @@ namespace RIZO.Service.MES.alarm /// /// public AlarmInfo AddAlarmInfo(AlarmInfo model) + { return Insertable(model).ExecuteReturnEntity(); }