1439 lines
67 KiB
C#
1439 lines
67 KiB
C#
// 统一引入所有必要命名空间
|
||
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;
|
||
using SqlSugar;
|
||
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(); // 连接池
|
||
|
||
#region PLC地址块儿映射
|
||
// OP070-1 专属地址映射
|
||
private readonly Dictionary<string, (string Addr, int Len)> _op070_1StringMap = new()
|
||
{
|
||
//{ "报警信息", ("DB1011.DBB58", 48) }, // Array[1..48] of Byte
|
||
{ "订单名称", ("DB1011.DBB1000", 48) }, // String[48]
|
||
{ "产品名称", ("DB1011.DBB1054", 48) }, // String[48]
|
||
{ "产品型号", ("DB1011.DBB2006", 28) }, // String[28]
|
||
{ "SN_1", ("DB1011.DBB2100", 28) }, // String[28]
|
||
{ "SN_2", ("DB1011.DBB2134", 28) } // String[28]
|
||
};
|
||
|
||
private readonly Dictionary<string, string> _op070_1IntMap = new()
|
||
{
|
||
{ "运行状态", "DB1011.DBW0" }, // Int
|
||
{ "设备模式", "DB1011.DBW2" }, // Int
|
||
{ "设备在线状态", "DB1011.DBW4" }, // Int
|
||
{ "ByPass", "DB1011.DBW6" }, // Int
|
||
{ "生产模式", "DB1011.DBW8" }, // Int
|
||
{ "实际产量", "DB1011.DBD1104" }, // DInt
|
||
//{ "合格数量", "DB1011.DBD1108" }, // DInt
|
||
//{ "失败数量", "DB1011.DBD1112" }, // DInt
|
||
{ "查询请求", "DB1011.DBW2000" }, // Int
|
||
{ "保存请求", "DB1011.DBW2002" }, // Int
|
||
{ "托盘号", "DB1011.DBW2004" }, // Int
|
||
{ "相机结果", "DB1011.DBW2164" }, // Int
|
||
{ "站位结果", "DB1011.DBW2166" }, // Int
|
||
{ "节拍时间", "DB1011.DBD2168" } // Real
|
||
};
|
||
|
||
// OP075 专属地址映射
|
||
private readonly Dictionary<string, (string Addr, int Len)> _op075StringMap = new()
|
||
{
|
||
//{ "报警信息", ("DB1011.DBB58", 48) }, // Array[1..48] of Byte
|
||
{ "订单名称", ("DB1011.DBB1000", 48) }, // String[48]
|
||
{ "产品名称", ("DB1011.DBB1054", 48) }, // String[48]
|
||
{ "产品型号", ("DB1011.DBB2006", 28) }, // String[28]
|
||
{ "SN_1", ("DB1011.DBB2100", 28) }, // String[28]
|
||
{ "SN_2", ("DB1011.DBB2192", 28) }, // String[28]
|
||
{ "芯片SN", ("DB1011.DBB2222", 28) } // String[28]
|
||
};
|
||
|
||
private readonly Dictionary<string, string> _op075IntMap = new()
|
||
{
|
||
{ "运行状态", "DB1011.DBW0" }, // Int
|
||
{ "设备模式", "DB1011.DBW2" }, // Int
|
||
{ "设备在线状态", "DB1011.DBW4" }, // Int
|
||
{ "ByPass", "DB1011.DBW6" }, // Int
|
||
{ "生产模式", "DB1011.DBW8" }, // Int
|
||
{ "实际产量", "DB1011.DBD1104" }, // DInt
|
||
//{ "合格数量", "DB1011.DBD1108" }, // DInt
|
||
//{ "失败数量", "DB1011.DBD1112" }, // DInt
|
||
{ "查询请求", "DB1011.DBW2000" }, // Int
|
||
{ "保存请求", "DB1011.DBW2002" }, // Int
|
||
{ "托盘号", "DB1011.DBW2004" }, // Int
|
||
{ "站位结果", "DB1011.DBW2252" }, // Int
|
||
{ "节拍时间", "DB1011.DBD2284" } // Real
|
||
};
|
||
|
||
// OP080-1 专属地址映射
|
||
private readonly Dictionary<string, (string Addr, int Len)> _op080_1StringMap = new()
|
||
{
|
||
//{ "报警信息", ("DB1016.DBB58", 48) }, // Array[1..48] of Byte
|
||
{ "订单名称", ("DB1016.DBB1000", 48) }, // String[48]
|
||
{ "产品名称", ("DB1016.DBB1054", 48) }, // String[48]
|
||
{ "合装位机壳_SN", ("DB1016.DBB2100", 28) }, // String[28]
|
||
{ "合装位PCB_SN", ("DB1016.DBB2130", 28) }, // String[28]
|
||
{ "拧紧位机壳_SN", ("DB1016.DBB2904", 28) }, // String[28]
|
||
{ "拧紧位PCB_SN", ("DB1016.DBB3092", 28) }, // String[28]
|
||
};
|
||
|
||
private readonly Dictionary<string, string> _op080_1IntMap = new()
|
||
{
|
||
{ "运行状态", "DB1016.DBW0" }, // Int
|
||
{ "设备模式", "DB1016.DBW2" }, // Int
|
||
{ "设备在线状态", "DB1016.DBW4" }, // Int
|
||
{ "ByPass", "DB1016.DBW6" }, // Int
|
||
{ "生产模式", "DB1016.DBW8" }, // Int
|
||
{ "实际产量", "DB1016.DBD1104" }, // DInt
|
||
{ "合格数量", "DB1016.DBD1108" }, // DInt
|
||
{ "失败数量", "DB1016.DBD1112" }, // DInt
|
||
{ "合装工位查询请求", "DB1016.DBW2000" }, // Int
|
||
{ "合装结果保存请求", "DB1016.DBW2004" }, // Int
|
||
{ "拧紧结果保存请求", "DB1016.DBW2006" }, // Int
|
||
{ "合装位托盘号", "DB1016.DBW2100" }, // Int
|
||
{ "拧紧位托盘号", "DB1016.DBW3152" }, // Int
|
||
// 2号螺钉
|
||
{ "2号螺钉_结果", "DB1016.DBW3164" }, // Int
|
||
{ "2号螺钉_扭矩", "DB1016.DBD3166" }, // Real
|
||
{ "2号螺钉_深度", "DB1016.DBD3170" }, // Real
|
||
{ "2号螺钉_角度", "DB1016.DBD3174" }, // Real
|
||
{ "2号螺钉_拧紧时间", "DB1016.DBD3178" }, // Real
|
||
// 3号螺钉
|
||
{ "3号螺钉_结果", "DB1016.DBW3182" }, // Int
|
||
{ "3号螺钉_扭矩", "DB1016.DBD3184" }, // Real
|
||
{ "3号螺钉_深度", "DB1016.DBD3188" }, // Real
|
||
{ "3号螺钉_角度", "DB1016.DBD3192" }, // Real
|
||
{ "3号螺钉_拧紧时间", "DB1016.DBD3196" }, // Real
|
||
// 4号螺钉
|
||
{ "4号螺钉_结果", "DB1016.DBW3200" }, // Int
|
||
{ "4号螺钉_扭矩", "DB1016.DBD3202" }, // Real
|
||
{ "4号螺钉_深度", "DB1016.DBD3206" }, // Real
|
||
{ "4号螺钉_角度", "DB1016.DBD3210" }, // Real
|
||
{ "4号螺钉_拧紧时间", "DB1016.DBD3214" }, // Real
|
||
// 1号螺钉
|
||
{ "1号螺钉_结果", "DB1016.DBW3218" }, // Int
|
||
{ "1号螺钉_扭矩", "DB1016.DBD3220" }, // Real
|
||
{ "1号螺钉_深度", "DB1016.DBD3224" }, // Real
|
||
{ "1号螺钉_角度", "DB1016.DBD3228" }, // Real
|
||
{ "1号螺钉_拧紧时间", "DB1016.DBD3232" }, // Real
|
||
// 5号螺钉
|
||
{ "5号螺钉_结果", "DB1016.DBW3236" }, // Int
|
||
{ "5号螺钉_扭矩", "DB1016.DBD3238" }, // Real
|
||
{ "5号螺钉_深度", "DB1016.DBD3242" }, // Real
|
||
{ "5号螺钉_角度", "DB1016.DBD3246" }, // Real
|
||
{ "5号螺钉_拧紧时间", "DB1016.DBD3250" }, // Real
|
||
// 6号螺钉
|
||
{ "6号螺钉_结果", "DB1016.DBW3254" }, // Int
|
||
{ "6号螺钉_扭矩", "DB1016.DBD3256" }, // Real
|
||
{ "6号螺钉_深度", "DB1016.DBD3260" }, // Real
|
||
{ "6号螺钉_角度", "DB1016.DBD3264" }, // Real
|
||
{ "6号螺钉_拧紧时间", "DB1016.DBD3268" }, // Real
|
||
// 7号螺钉
|
||
{ "7号螺钉_结果", "DB1016.DBW3272" }, // Int
|
||
{ "7号螺钉_扭矩", "DB1016.DBD3274" }, // Real
|
||
{ "7号螺钉_深度", "DB1016.DBD3278" }, // Real
|
||
{ "7号螺钉_角度", "DB1016.DBD3282" }, // Real
|
||
{ "7号螺钉_拧紧时间", "DB1016.DBD3286" }, // Real
|
||
};
|
||
|
||
#endregion
|
||
/// <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生产数据(优化版:连接池+并发控制+OP070-1/OP080专属适配)
|
||
/// </summary>
|
||
/// <param name="ip">PLC IP地址</param>
|
||
/// <param name="plcName">PLC名称(支持OP070-1/OP080)</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,
|
||
string plcName,
|
||
short rack,
|
||
short slot,
|
||
CpuType cpuType = CpuType.S71500)
|
||
{
|
||
// 1. 强化参数校验(补充PLC名称校验+白名单控制)
|
||
if (string.IsNullOrWhiteSpace(ip))
|
||
return (false, null, "PLC IP地址不能为空");
|
||
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)
|
||
return (false, null, $"PLC槽位号{slot}无效(有效值0-4)");
|
||
|
||
Plc plc = null;
|
||
bool isConnReused = false;
|
||
var poolKey = $"{ip}_{rack}_{slot}_{cpuType}";
|
||
PlcProductionData prodData = null;
|
||
|
||
// 2. 并发控制(核心保留,防止端口/线程耗尽)
|
||
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);
|
||
bool isConnected = false;
|
||
// for循环替代while,逻辑更直观
|
||
try
|
||
{
|
||
await OpenPlcConnectionAsync(plc);
|
||
isConnected = plc.IsConnected;
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
|
||
if (!isConnected)
|
||
return (false, null, $"{plcName}连接失败(含2次重试)");
|
||
|
||
_plcConnPool.TryAdd(poolKey, (plc, DateTime.Now));
|
||
}
|
||
|
||
// 4. 多工位查询请求标志适配(核心:区分不同工位的寄存器地址)
|
||
int iQueryRequest = 0;
|
||
int iSaveRequest = 0;
|
||
string strSaveRequest = "";
|
||
if (plcName == "OP070-1" || plcName == "OP070-2" || plcName == "OP070-3")
|
||
{
|
||
iQueryRequest = await ReadPlcIntAsync(plc, _op070_1IntMap["查询请求"]);
|
||
iSaveRequest = await ReadPlcIntAsync(plc, _op070_1IntMap["保存请求"]);
|
||
}
|
||
else if (plcName == "OP075")
|
||
{
|
||
iQueryRequest = await ReadPlcIntAsync(plc, _op075IntMap["查询请求"]);
|
||
iSaveRequest = await ReadPlcIntAsync(plc, _op075IntMap["保存请求"]);
|
||
}
|
||
else if (plcName == "OP080-1")
|
||
{
|
||
iQueryRequest = await ReadPlcIntAsync(plc, _op080_1IntMap["合装工位查询请求"]);
|
||
//考虑一条数据更新两次,还是生成两条数据
|
||
int iSaveRequest1 = await ReadPlcIntAsync(plc, _op080_1IntMap["合装结果保存请求"]);
|
||
int iSaveRequest2 = await ReadPlcIntAsync(plc, _op080_1IntMap["拧紧结果保存请求"]);
|
||
if (iSaveRequest1 == 1)
|
||
{
|
||
iSaveRequest = 1;
|
||
strSaveRequest = "合装结果保存请求";
|
||
}
|
||
if (iSaveRequest2 == 1)
|
||
{
|
||
iSaveRequest = 1;
|
||
if (strSaveRequest.Length > 0)
|
||
{
|
||
strSaveRequest += ",拧紧结果保存请求";
|
||
}
|
||
else
|
||
{
|
||
strSaveRequest = "拧紧结果保存请求";
|
||
}
|
||
}
|
||
|
||
} // 无效请求直接返回(避免无谓资源消耗)
|
||
if (iQueryRequest != 1 && iSaveRequest != 1)
|
||
{
|
||
return (false, null, $"");
|
||
}
|
||
|
||
//给PLC返回数据
|
||
if (iQueryRequest == 1)
|
||
{
|
||
|
||
}
|
||
if (iSaveRequest == 1)
|
||
{
|
||
// 5. 多工位专属数据读取(预留扩展,逻辑隔离)
|
||
if (plcName == "OP070-1" || plcName == "OP070-2" || plcName == "OP070-3")
|
||
{
|
||
prodData = await ReadOP070_1DataAsync(plc, ip, plcName);
|
||
}
|
||
else if (plcName == "OP075")
|
||
{
|
||
prodData = await ReadOP075DataAsync(plc, ip, plcName);
|
||
}
|
||
else if (plcName == "OP080-1" || plcName == "OP080-2")
|
||
{
|
||
int iSaveRequest1 = await ReadPlcIntAsync(plc, _op080_1IntMap["合装结果保存请求"]);
|
||
int iSaveRequest2 = await ReadPlcIntAsync(plc, _op080_1IntMap["拧紧结果保存请求"]);
|
||
prodData = await ReadOP080_1DataAsync(plc, ip, plcName, strSaveRequest);
|
||
}
|
||
|
||
// 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($"{plcName}({ip})生产数据读取异常:{ex.Message}\n{ex.StackTrace}");
|
||
return (false, null, $"{plcName}生产数据读取失败:{ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
// 资源安全释放(核心:防止死锁/连接泄漏)
|
||
if (!isConnReused)
|
||
{
|
||
ReleasePlcConnection(plc);
|
||
}
|
||
_concurrencySemaphore.Release();
|
||
}
|
||
}
|
||
|
||
|
||
#region 工位专属读取方法
|
||
/// <summary>
|
||
/// OP070-1数据读取
|
||
/// </summary>
|
||
private async Task<PlcProductionData> ReadOP070_1DataAsync(Plc plc, string ip,string workstationCode)
|
||
{
|
||
// 1. 批量并行读取所有字段(沿用原有结构+增强型读取方法)
|
||
var (strFields, intFields, 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["订单名称"].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)
|
||
),
|
||
// 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 (productName,orderName, productModel, 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["保存请求"], "0");
|
||
}
|
||
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})读取结果:产品型号={productModel},运行状态={runStatusDesc},订单名称={orderName}");
|
||
|
||
// 5. 构建数据实体(增强空值处理)
|
||
return new PlcProductionData
|
||
{
|
||
PlcIp = ip.Trim(),
|
||
OccurTime = DateTime.Now,
|
||
LineCode = "line2",
|
||
WorkstationCode = workstationCode,
|
||
ProductModel = productModel ?? 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
|
||
};
|
||
}
|
||
/// <summary>
|
||
/// OP075数据读取
|
||
/// </summary>
|
||
private async Task<PlcProductionData> ReadOP075DataAsync(Plc plc, string ip, string workstationCode)
|
||
{
|
||
// 1. 批量并行读取所有字段(沿用原有结构+增强型读取方法)
|
||
var (strFields, intFields, realFields) = await Task.Run(async () => (
|
||
// 字符串字段(增强空值和异常处理)
|
||
(
|
||
//await ReadPlcStringAsync(plc, _op075StringMap["报警信息"].Addr, _op075StringMap["报警信息"].Len),
|
||
await ReadPlcStringAsync(plc, _op075StringMap["产品名称"].Addr, _op075StringMap["产品名称"].Len),
|
||
await ReadPlcStringAsync(plc, _op075StringMap["订单名称"].Addr, _op075StringMap["订单名称"].Len),
|
||
await ReadPlcStringAsync(plc, _op075StringMap["产品型号"].Addr, _op075StringMap["产品型号"].Len),
|
||
await ReadPlcStringAsync(plc, _op075StringMap["SN_1"].Addr, _op075StringMap["SN_1"].Len),
|
||
await ReadPlcStringAsync(plc, _op075StringMap["SN_2"].Addr, _op075StringMap["SN_2"].Len),
|
||
await ReadPlcStringAsync(plc, _op075StringMap["芯片SN"].Addr, _op075StringMap["芯片SN"].Len)
|
||
),
|
||
// Int字段(使用增强版ReadPlcIntAsync)
|
||
(
|
||
await ReadPlcIntAsync(plc, _op075IntMap["运行状态"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["设备模式"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["设备在线状态"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["ByPass"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["生产模式"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["查询请求"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["保存请求"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["托盘号"]),
|
||
await ReadPlcIntAsync(plc, _op075IntMap["站位结果"])
|
||
),
|
||
// DInt字段(增强版读取方法)
|
||
//(
|
||
// await ReadPlcDIntAsync(plc, _op075IntMap["实际产量"]),
|
||
// await ReadPlcDIntAsync(plc, _op075IntMap["合格数量"]),
|
||
// await ReadPlcDIntAsync(plc, _op075IntMap["失败数量"])
|
||
//),
|
||
// Real字段(增强版读取方法)
|
||
(
|
||
await ReadPlcRealAsync(plc, _op075IntMap["节拍时间"])
|
||
)
|
||
));
|
||
|
||
// 2. 解构字段(保持原有逻辑,空值兜底)
|
||
var (productName, orderName, productModel, sn1, sn2,chipsn) = strFields;
|
||
var (runStatus, machineModel, onlineStatus, byPass, produceModel, queryReq, saveReq, trayNo, stationResult) = intFields;
|
||
//var (actualOutput, qualifiedQty, failedQty) = dintFields;
|
||
float cycleTime = realFields;
|
||
|
||
// 3. 写入保存请求(异步+增强异常日志)
|
||
try
|
||
{
|
||
WritePlcValue(plc, _op075IntMap["保存请求"], "0");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"OP075({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() };
|
||
|
||
// 调试日志:输出关键读取结果
|
||
Console.WriteLine($"OP075({ip})读取结果:产品型号={productModel},运行状态={runStatusDesc},订单名称={orderName}");
|
||
|
||
// 5. 构建数据实体(增强空值处理)
|
||
return new PlcProductionData
|
||
{
|
||
PlcIp = ip.Trim(),
|
||
OccurTime = DateTime.Now,
|
||
LineCode = "line2",
|
||
WorkstationCode = workstationCode,
|
||
ProductModel = productModel ?? 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(),
|
||
ChipSN = chipsn,
|
||
//ActualOutput = actualOutput,
|
||
//QualifiedQuantity = qualifiedQty,
|
||
//FailedQuantity = failedQty,
|
||
ProductionCycle = (int?)cycleTime,
|
||
//AlarmMsg = alarmMsg ?? string.Empty,
|
||
CreatedBy = "PLC",
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
}
|
||
/// <summary>
|
||
/// 读取OP080-1数据
|
||
/// </summary>
|
||
/// <param name="plc"></param>
|
||
/// <param name="ip"></param>
|
||
/// <param name="workstationCode"></param>
|
||
/// <returns></returns>
|
||
private async Task<PlcProductionData> ReadOP080_1DataAsync(Plc plc, string ip, string workstationCode,string strSaveRequest)
|
||
{
|
||
// 1. 批量并行读取所有字段(沿用原有结构+增强型读取方法)
|
||
var (strFields, intFields, realFields) = await Task.Run(async () => (
|
||
// 字符串字段(增强空值和异常处理)
|
||
(
|
||
await ReadPlcStringAsync(plc, _op080_1StringMap["订单名称"].Addr, _op080_1StringMap["订单名称"].Len),
|
||
await ReadPlcStringAsync(plc, _op080_1StringMap["产品名称"].Addr, _op080_1StringMap["产品名称"].Len),
|
||
await ReadPlcStringAsync(plc, _op080_1StringMap["合装位机壳_SN"].Addr, _op080_1StringMap["合装位机壳_SN"].Len),
|
||
await ReadPlcStringAsync(plc, _op080_1StringMap["合装位PCB_SN"].Addr, _op080_1StringMap["合装位PCB_SN"].Len),
|
||
await ReadPlcStringAsync(plc, _op080_1StringMap["拧紧位机壳_SN"].Addr, _op080_1StringMap["拧紧位机壳_SN"].Len),
|
||
await ReadPlcStringAsync(plc, _op080_1StringMap["拧紧位PCB_SN"].Addr, _op080_1StringMap["拧紧位PCB_SN"].Len)
|
||
),
|
||
// Int/DInt字段(使用增强版ReadPlcIntAsync/ReadPlcDIntAsync)
|
||
(
|
||
// 基础状态字段
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["运行状态"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["设备模式"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["设备在线状态"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["ByPass"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["生产模式"]),
|
||
// 产量统计字段(DInt类型,需用ReadPlcDIntAsync)
|
||
//await ReadPlcDIntAsync(plc, _op080_1IntMap["实际产量"]),
|
||
//await ReadPlcDIntAsync(plc, _op080_1IntMap["合格数量"]),
|
||
//await ReadPlcDIntAsync(plc, _op080_1IntMap["失败数量"]),
|
||
// 请求/托盘字段
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["合装工位查询请求"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["合装结果保存请求"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["拧紧结果保存请求"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["合装位托盘号"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["拧紧位托盘号"]),
|
||
// 螺钉结果字段(Int类型)
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["1号螺钉_结果"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["2号螺钉_结果"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["3号螺钉_结果"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["4号螺钉_结果"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["5号螺钉_结果"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["6号螺钉_结果"]),
|
||
await ReadPlcIntAsync(plc, _op080_1IntMap["7号螺钉_结果"])
|
||
),
|
||
// Real字段(螺钉扭矩/深度/角度/时间,增强版读取方法)
|
||
(
|
||
// 1号螺钉Real字段
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["1号螺钉_扭矩"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["1号螺钉_深度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["1号螺钉_角度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["1号螺钉_拧紧时间"]),
|
||
// 2号螺钉Real字段
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["2号螺钉_扭矩"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["2号螺钉_深度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["2号螺钉_角度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["2号螺钉_拧紧时间"]),
|
||
// 3号螺钉Real字段
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["3号螺钉_扭矩"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["3号螺钉_深度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["3号螺钉_角度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["3号螺钉_拧紧时间"]),
|
||
// 4号螺钉Real字段
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["4号螺钉_扭矩"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["4号螺钉_深度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["4号螺钉_角度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["4号螺钉_拧紧时间"]),
|
||
// 5号螺钉Real字段
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["5号螺钉_扭矩"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["5号螺钉_深度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["5号螺钉_角度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["5号螺钉_拧紧时间"]),
|
||
// 6号螺钉Real字段
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["6号螺钉_扭矩"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["6号螺钉_深度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["6号螺钉_角度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["6号螺钉_拧紧时间"]),
|
||
// 7号螺钉Real字段
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["7号螺钉_扭矩"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["7号螺钉_深度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["7号螺钉_角度"]),
|
||
await ReadPlcRealAsync(plc, _op080_1IntMap["7号螺钉_拧紧时间"])
|
||
)
|
||
));
|
||
|
||
// 2. 解构字段(保持原有逻辑,空值兜底)
|
||
var (orderName, productName, hzMachineSN, hzPcbsn, njMachineSN, njPcbsn) = strFields;
|
||
|
||
var (runStatus, machineModel, onlineStatus, byPass, produceModel,
|
||
hzQueryReq, hzSaveReq, njSaveReq,
|
||
hzTrayNo, njTrayNo, screw1Result, screw2Result, screw3Result,
|
||
screw4Result, screw5Result, screw6Result, screw7Result) = intFields;
|
||
|
||
// 解构螺钉Real字段(按顺序对应)
|
||
var (
|
||
// 1号螺钉
|
||
screw1Torque, screw1Depth, screw1Angle, screw1Time,
|
||
// 2号螺钉
|
||
screw2Torque, screw2Depth, screw2Angle, screw2Time,
|
||
// 3号螺钉
|
||
screw3Torque, screw3Depth, screw3Angle, screw3Time,
|
||
// 4号螺钉
|
||
screw4Torque, screw4Depth, screw4Angle, screw4Time,
|
||
// 5号螺钉
|
||
screw5Torque, screw5Depth, screw5Angle, screw5Time,
|
||
// 6号螺钉
|
||
screw6Torque, screw6Depth, screw6Angle, screw6Time,
|
||
// 7号螺钉
|
||
screw7Torque, screw7Depth, screw7Angle, screw7Time
|
||
) = realFields;
|
||
|
||
// 3. 写入保存请求(异步+增强异常日志),收完数据给这个数据位置0
|
||
try
|
||
{
|
||
if (strSaveRequest.Contains("合装结果保存请求"))
|
||
{
|
||
WritePlcValue(plc, _op080_1IntMap["合装结果保存请求"], "0");
|
||
}
|
||
if (strSaveRequest.Contains("拧紧结果保存请求"))
|
||
{
|
||
WritePlcValue(plc, _op080_1IntMap["拧紧结果保存请求"], "0");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"OP080-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 ? "离线" : "在线";
|
||
|
||
// 计算整体工位合格标识(所有螺钉合格则工位合格)
|
||
var allScrewQualified = new[] { screw1Result, screw2Result, screw3Result, screw4Result, screw5Result, screw6Result, screw7Result }
|
||
.All(s => s == 1);
|
||
string qualificationFlag = allScrewQualified ? "1" : "0";
|
||
|
||
// 调试日志:输出关键读取结果
|
||
Console.WriteLine($"OP080-1({ip})读取结果:产品名称={productName},运行状态={runStatusDesc},订单名称={orderName},螺钉整体合格={allScrewQualified}");
|
||
|
||
// 5. 构建数据实体(严格匹配PlcProductionData字段定义)
|
||
var plcData = new PlcProductionData
|
||
{
|
||
// 基础字段
|
||
PlcIp = ip.Trim(),
|
||
OccurTime = DateTime.Now,
|
||
LineCode = "line2", // 可根据实际产线调整
|
||
WorkstationCode = workstationCode,
|
||
ProductName = productName ?? string.Empty,
|
||
ProductCode = njPcbsn ?? string.Empty, // 拧紧位PCB SN作为产品编码
|
||
|
||
// 合格/返工标志
|
||
QualificationFlag = qualificationFlag,
|
||
ReworkFlag = reworkFlag,
|
||
|
||
// 设备状态相关
|
||
Automanual = machineModel, // 设备模式:0-自动,1-手动
|
||
Runstatus = runStatus, // 运行状态:1=空闲,2=运行中,3=故障
|
||
OnlineStatus = onlineStatusDesc, // 设备在线状态1=离线,0=在线
|
||
ProduceModel = produceModelDesc, // 生产模式描述
|
||
|
||
// 托盘号相关(拆分合装/拧紧位托盘号)
|
||
TrayNo = $"{hzTrayNo}!{njTrayNo}", // 合并显示
|
||
AssemblyTrayNo = hzTrayNo.ToString(), // 合装位托盘号
|
||
TightenTrayNo = njTrayNo.ToString(), // 拧紧位托盘号
|
||
|
||
// SN相关字段
|
||
SN1 = hzMachineSN ?? string.Empty, // 合装位机壳SN
|
||
SN2 = njMachineSN ?? string.Empty, // 拧紧位机壳SN
|
||
AssemblyHousingSN = hzMachineSN ?? string.Empty, // 合装位机壳SN
|
||
AssemblyPCBSN = hzPcbsn ?? string.Empty, // 合装位PCB SN
|
||
TightenHousingSN = njMachineSN ?? string.Empty, // 拧紧位机壳SN
|
||
TightenPCBSN = njPcbsn ?? string.Empty, // 拧紧位PCB SN
|
||
|
||
// 螺钉相关字段(严格匹配实体类)
|
||
Screw1Result = screw1Result.ToString(),
|
||
Screw1Torque = screw1Torque.ToString("0.00"),
|
||
Screw1Depth = screw1Depth.ToString("0.00"),
|
||
Screw1Angle = screw1Angle.ToString("0.00"),
|
||
Screw1TightenTime = screw1Time.ToString("0.00"),
|
||
|
||
Screw2Result = screw2Result.ToString(),
|
||
Screw2Torque = screw2Torque.ToString("0.00"),
|
||
Screw2Depth = screw2Depth.ToString("0.00"),
|
||
Screw2Angle = screw2Angle.ToString("0.00"),
|
||
Screw2TightenTime = screw2Time.ToString("0.00"),
|
||
|
||
Screw3Result = screw3Result.ToString(),
|
||
Screw3Torque = screw3Torque.ToString("0.00"),
|
||
Screw3Depth = screw3Depth.ToString("0.00"),
|
||
Screw3Angle = screw3Angle.ToString("0.00"),
|
||
Screw3TightenTime = screw3Time.ToString("0.00"),
|
||
|
||
Screw4Result = screw4Result.ToString(),
|
||
Screw4Torque = screw4Torque.ToString("0.00"),
|
||
Screw4Depth = screw4Depth.ToString("0.00"),
|
||
Screw4Angle = screw4Angle.ToString("0.00"),
|
||
Screw4TightenTime = screw4Time.ToString("0.00"),
|
||
|
||
Screw5Result = screw5Result.ToString(),
|
||
Screw5Torque = screw5Torque.ToString("0.00"),
|
||
Screw5Depth = screw5Depth.ToString("0.00"),
|
||
Screw5Angle = screw5Angle.ToString("0.00"),
|
||
Screw5TightenTime = screw5Time.ToString("0.00"),
|
||
|
||
Screw6Result = screw6Result.ToString(),
|
||
Screw6Torque = screw6Torque.ToString("0.00"),
|
||
Screw6Depth = screw6Depth.ToString("0.00"),
|
||
Screw6Angle = screw6Angle.ToString("0.00"),
|
||
Screw6TightenTime = screw6Time.ToString("0.00"),
|
||
|
||
Screw7Result = screw7Result.ToString(),
|
||
Screw7Torque = screw7Torque.ToString("0.00"),
|
||
Screw7Depth = screw7Depth.ToString("0.00"),
|
||
Screw7Angle = screw7Angle.ToString("0.00"),
|
||
Screw7TightenTime = screw7Time.ToString("0.00"),
|
||
|
||
// 系统字段
|
||
CreatedBy = "PLC",
|
||
CreatedTime = DateTime.Now,
|
||
// 可选字段(无值时置空)
|
||
ParamName = string.Empty,
|
||
ParamValue = string.Empty,
|
||
ProductionCycle = null,
|
||
Remark = string.Empty,
|
||
UpdatedBy = string.Empty,
|
||
UpdatedTime = null,
|
||
ProductModel = string.Empty, // 无产品型号数据时置空
|
||
WorkstationName = string.Empty, // 无工站名称数据时置空
|
||
CameraResult = string.Empty // 无相机结果数据时置空
|
||
};
|
||
return plcData;
|
||
}
|
||
|
||
#endregion
|
||
|
||
// 可选:添加连接池清理方法(防止长期运行导致连接泄露)
|
||
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)
|
||
{
|
||
// 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 };
|
||
Plc plc = null;
|
||
|
||
try
|
||
{
|
||
// 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)
|
||
{
|
||
result.ConnectSuccess = false;
|
||
result.ConnectMessage = $"连接超时({_globalConfig.ReadWriteTimeout}ms)";
|
||
return result;
|
||
}
|
||
await connectTask;
|
||
|
||
// 4. 连接状态校验(精简分支)
|
||
if (!plc.IsConnected)
|
||
{
|
||
result.ConnectSuccess = false;
|
||
result.ConnectMessage = "连接失败(PLC未返回连接状态)";
|
||
return result;
|
||
}
|
||
result.ConnectSuccess = true;
|
||
result.ConnectMessage = "连接成功";
|
||
|
||
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 = "通用地址读取成功";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.ReadSuccess = false;
|
||
result.ReadMessage = $"通用地址读取失败:{ex.Message}";
|
||
}
|
||
|
||
// 写入测试
|
||
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}";
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
{
|
||
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>
|
||
// 优化后的OpenPlcConnectionAsync
|
||
private async Task<bool> OpenPlcConnectionAsync(Plc plc)
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
}
|
||
private async Task<int> 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<float> 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;
|
||
}
|
||
}
|
||
/// <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
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
} |