2026-01-19 15:41:58 +08:00

643 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

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