From c38ac18a30c8a57f9a759209b7da2845bd506234 Mon Sep 17 00:00:00 2001 From: gcw_MV9p2JJN Date: Tue, 23 Dec 2025 09:17:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AD=A6=E7=81=AF=E9=80=9A=E8=AE=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Helper/ModbusTcpClientHelper.cs | 376 ++++++++++++++++++ ZR.Admin.WebApi/Program.cs | 10 + .../background/SocketBackgroundService.cs | 109 ++++- ZR.Service/Utils/MyAlarmLigth/LightUp.cs | 95 +++++ 4 files changed, 585 insertions(+), 5 deletions(-) create mode 100644 Infrastructure/Helper/ModbusTcpClientHelper.cs create mode 100644 ZR.Service/Utils/MyAlarmLigth/LightUp.cs diff --git a/Infrastructure/Helper/ModbusTcpClientHelper.cs b/Infrastructure/Helper/ModbusTcpClientHelper.cs new file mode 100644 index 00000000..2649dc39 --- /dev/null +++ b/Infrastructure/Helper/ModbusTcpClientHelper.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure.Helper +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Sockets; + using System.Threading.Tasks; + + /// + /// Modbus TCP 客户端帮助类(支持常用功能码) 客户端 = 主站 + /// + public class ModbusTcpClientHelper : IDisposable + { + private TcpClient _tcpClient; + private NetworkStream _networkStream; + private readonly object _lockObj = new object(); // 线程安全锁 + + /// + /// 连接Modbus TCP服务器 服务端 = 从站 + /// + /// 服务器IP + /// 端口(默认502) + /// 超时时间(毫秒,默认3000) + public void Connect(string ip, int port = 502, int timeoutMs = 3000) + { + if (_tcpClient != null && _tcpClient.Connected) + return; + + lock (_lockObj) + { + try + { + _tcpClient = new TcpClient(); + _tcpClient.SendTimeout = timeoutMs; + _tcpClient.ReceiveTimeout = timeoutMs; + _tcpClient.Connect(ip, port); + _networkStream = _tcpClient.GetStream(); + } + catch (Exception ex) + { + throw new Exception($"连接Modbus TCP服务器失败:{ex.Message}", ex); + } + } + } + + /// + /// 断开连接 + /// + public void Disconnect() + { + lock (_lockObj) + { + _networkStream?.Close(); + _tcpClient?.Close(); + _networkStream = null; + _tcpClient = null; + } + } + + /// + /// 读取保持寄存器(功能码0x03) + /// + /// 从站地址(单元ID) + /// 起始地址(0-based) + /// 寄存器数量 + /// 寄存器值列表(每个寄存器2字节,高位在前) + public List ReadHoldingRegisters(byte unitId, ushort startAddress, ushort numberOfRegisters) + { + var request = BuildReadRequest(unitId, 0x03, startAddress, numberOfRegisters); + var response = SendAndReceive(request); + return ParseReadRegistersResponse(response, numberOfRegisters); + } + + /// + /// 读取输入寄存器(功能码0x04) + /// + public List ReadInputRegisters(byte unitId, ushort startAddress, ushort numberOfRegisters) + { + var request = BuildReadRequest(unitId, 0x04, startAddress, numberOfRegisters); + var response = SendAndReceive(request); + return ParseReadRegistersResponse(response, numberOfRegisters); + } + + /// + /// 写入单个保持寄存器(功能码0x06) + /// + /// 从站地址 + /// 寄存器地址(0-based) + /// 要写入的值(16位) + public void WriteSingleRegister(byte unitId, ushort address, ushort value) + { + var request = BuildWriteSingleRequest(unitId, 0x06, address, value); + var response = SendAndReceive(request); + ValidateWriteResponse(response, 0x06, address, value); + } + + /// + /// 写入多个保持寄存器(功能码0x10) + /// + /// 从站地址 + /// 起始地址(0-based) + /// 要写入的值列表(每个值16位) + public void WriteMultipleRegisters(byte unitId, ushort startAddress, List values) + { + var request = BuildWriteMultipleRequest(unitId, 0x10, startAddress, values); + var response = SendAndReceive(request); + ValidateWriteResponse(response, 0x10, startAddress, (ushort)values.Count); + } + + /// + /// 读取线圈状态(功能码0x01) + /// + /// 从站地址 + /// 起始地址(0-based) + /// 线圈数量(1-2000) + /// 线圈状态列表(true=ON,false=OFF) + public List ReadCoils(byte unitId, ushort startAddress, ushort numberOfCoils) + { + var request = BuildReadRequest(unitId, 0x01, startAddress, numberOfCoils); + var response = SendAndReceive(request); + return ParseReadCoilsResponse(response, numberOfCoils); + } + + /// + /// 写入单个线圈(功能码0x05) + /// + public void WriteSingleCoil(byte unitId, ushort address, bool value) + { + var request = BuildWriteSingleCoilRequest(unitId, 0x05, address, value); + var response = SendAndReceive(request); + ValidateWriteResponse(response, 0x05, address, value ? (ushort)0xFF00 : (ushort)0x0000); + } + + + #region 私有方法(协议构建与解析) + + /// + /// 构建读请求报文(功能码0x01、0x02、0x03、0x04通用) + /// + private byte[] BuildReadRequest(byte unitId, byte functionCode, ushort startAddress, ushort quantity) + { + var request = new List + { + unitId, // 单元ID + functionCode // 功能码 + }; + request.AddRange(BitConverter.GetBytes(startAddress).Reverse()); // 起始地址(大端序) + request.AddRange(BitConverter.GetBytes(quantity).Reverse()); // 数量(大端序) + AddCrcOrLength(request); // 添加长度(Modbus TCP无CRC,用MBAP长度) + return request.ToArray(); + } + + /// + /// 构建写单个寄存器请求(功能码0x06) + /// + private byte[] BuildWriteSingleRequest(byte unitId, byte functionCode, ushort address, ushort value) + { + var request = new List + { + unitId, + functionCode + }; + request.AddRange(BitConverter.GetBytes(address).Reverse()); + request.AddRange(BitConverter.GetBytes(value).Reverse()); + AddCrcOrLength(request); + return request.ToArray(); + } + + /// + /// 构建写多个寄存器请求(功能码0x10) + /// + private byte[] BuildWriteMultipleRequest(byte unitId, byte functionCode, ushort startAddress, List values) + { + var request = new List + { + unitId, + functionCode, + (byte)(startAddress >> 8), // 起始地址高字节 + (byte)(startAddress & 0xFF),// 起始地址低字节 + (byte)(values.Count >> 8), // 数量高字节 + (byte)(values.Count & 0xFF),// 数量低字节 + (byte)(values.Count * 2) // 字节数(每个寄存器2字节) + }; + foreach (var value in values) + { + request.AddRange(BitConverter.GetBytes(value).Reverse()); // 寄存器值(大端序) + } + AddCrcOrLength(request); + return request.ToArray(); + } + + /// + /// 构建写单个线圈请求(功能码0x05) + /// + private byte[] BuildWriteSingleCoilRequest(byte unitId, byte functionCode, ushort address, bool value) + { + var request = new List + { + unitId, + functionCode + }; + request.AddRange(BitConverter.GetBytes(address).Reverse()); + request.AddRange(value ? new byte[] { 0xFF, 0x00 } : new byte[] { 0x00, 0x00 }); // ON=0xFF00,OFF=0x0000 + AddCrcOrLength(request); + return request.ToArray(); + } + + /// + /// 添加MBAP头的长度字段(Modbus TCP无CRC,用长度标识后续字节数) + /// + private void AddCrcOrLength(List data) + { + // Modbus TCP格式:[事务ID(2)][协议ID(2)][长度(2)][单元ID(1)][PDU...] + // 此处简化:假设事务ID=0,协议ID=0,长度=单元ID+PDU长度(即data.Length) + var mbapHeader = new List + { + 0x00, 0x00, // 事务ID=0 + 0x00, 0x00, // 协议ID=0(Modbus) + 0x00, (byte)(data.Count) // 长度=单元ID+PDU长度(data已包含单元ID+PDU) + }; + mbapHeader.AddRange(data); + data.Clear(); + data.AddRange(mbapHeader); + } + + /// + /// 发送请求并接收响应 + /// + private byte[] SendAndReceive(byte[] request) + { + if (_networkStream == null || !_tcpClient.Connected) + throw new InvalidOperationException("未连接到Modbus TCP服务器"); + + lock (_lockObj) + { + try + { + // 发送请求 + _networkStream.Write(request, 0, request.Length); + + // 接收响应(先读MBAP头的长度字段,再读剩余数据) + var headerBuffer = new byte[6]; // MBAP头前6字节(事务ID+协议ID+长度) + var bytesRead = _networkStream.Read(headerBuffer, 0, headerBuffer.Length); + if (bytesRead != 6) + throw new Exception("接收MBAP头失败"); + + // 解析长度字段(第5-6字节,大端序) + var length = (headerBuffer[4] << 8) | headerBuffer[5]; + var responseBuffer = new byte[length + 6]; // 总长度=MBAP头(6)+长度字段指定的字节数 + Array.Copy(headerBuffer, responseBuffer, 6); + + // 读剩余数据 + bytesRead = _networkStream.Read(responseBuffer, 6, length); + if (bytesRead != length) + throw new Exception("接收响应数据不完整"); + + return responseBuffer; + } + catch (Exception ex) + { + throw new Exception($"通信失败:{ex.Message}", ex); + } + } + } + + /// + /// 解析读寄存器响应(功能码0x03、0x04) + /// + private List ParseReadRegistersResponse(byte[] response, ushort expectedCount) + { + // 响应格式:[MBAP头(7)][单元ID(1)][功能码(1)][字节数(1)][数据(n)] + var dataStartIndex = 7; // MBAP头7字节(事务ID2+协议ID2+长度2+单元ID1)+ 功能码1?不,重新算: + // 实际响应结构:MBAP头(7字节) = [事务ID(2),协议ID(2),长度(2),单元ID(1)]?不,标准MBAP头是7字节: + // 正确的MBAP头:事务ID(2)、协议ID(2)、长度(2)、单元ID(1) → 共7字节。然后PDU是功能码(1)+数据。 + // 所以response的结构: + // 0-1: 事务ID + // 2-3: 协议ID + // 4-5: 长度(=单元ID+PDU长度) + // 6: 单元ID + // 7: 功能码 + // 8: 字节数(=n*2,n为寄存器数量) + // 9~: 数据(每个寄存器2字节,大端序) + + if (response[7] != 0x03 && response[7] != 0x04) // 检查功能码(成功时无异常码) + throw new Exception($"Modbus异常:功能码={response[7]:X2}"); + + var byteCount = response[8]; + if (byteCount != expectedCount * 2) + throw new Exception($"响应字节数与预期不符:实际={byteCount},预期={expectedCount * 2}"); + + var registers = new List(); + for (int i = 0; i < expectedCount; i++) + { + var index = 9 + i * 2; + var highByte = response[index]; + var lowByte = response[index + 1]; + registers.Add((ushort)((highByte << 8) | lowByte)); + } + return registers; + } + + /// + /// 解析读线圈响应(功能码0x01) + /// + private List ParseReadCoilsResponse(byte[] response, ushort expectedCount) + { + if (response[7] != 0x01) + throw new Exception($"Modbus异常:功能码={response[7]:X2}"); + + var byteCount = response[8]; + var expectedBytes = (expectedCount + 7) / 8; // 向上取整 + if (byteCount != expectedBytes) + throw new Exception($"响应字节数与预期不符:实际={byteCount},预期={expectedBytes}"); + + var coils = new List(); + for (int i = 0; i < expectedCount; i++) + { + var byteIndex = 9 + i / 8; + var bitIndex = i % 8; + var mask = (byte)(1 << bitIndex); + coils.Add((response[byteIndex] & mask) != 0); + } + return coils; + } + + /// + /// 验证写操作响应(确保从站正确接收) + /// + private void ValidateWriteResponse(byte[] response, byte functionCode, ushort address, ushort value) + { + if (response[7] != functionCode) // 功能码应与原请求一致(无异常) + throw new Exception($"写操作失败:响应功能码={response[7]:X2}"); + + // 解析响应中的地址和值(应与请求一致) + var respAddress = (ushort)((response[8] << 8) | response[9]); + var respValue = (ushort)((response[10] << 8) | response[11]); + if (respAddress != address || respValue != value) + throw new Exception($"写操作响应不匹配:地址={respAddress:X4}(预期={address:X4}),值={respValue:X4}(预期={value:X4})"); + } + + #endregion + + + #region IDisposable实现 + private bool _disposed = false; + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + Disconnect(); + } + _disposed = true; + } + } + + ~ModbusTcpClientHelper() + { + Dispose(false); + } + #endregion + } +} diff --git a/ZR.Admin.WebApi/Program.cs b/ZR.Admin.WebApi/Program.cs index 37757eb5..93755909 100644 --- a/ZR.Admin.WebApi/Program.cs +++ b/ZR.Admin.WebApi/Program.cs @@ -1,4 +1,5 @@ using AspNetCoreRateLimit; +using Infrastructure.Helper; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; @@ -124,6 +125,15 @@ builder.Services.AddSingleton(provider => return server; }); +builder.Services.AddSingleton(provider => +{ + var modbus = new ModbusTcpClientHelper(); + + modbus.Connect("192.168.2.22", 9999); // 你可以按需修改 IP 和端口 + + + return modbus; +}); var app = builder.Build(); InternalApp.ServiceProvider = app.Services; InternalApp.Configuration = builder.Configuration; diff --git a/ZR.Admin.WebApi/background/SocketBackgroundService.cs b/ZR.Admin.WebApi/background/SocketBackgroundService.cs index 74b95681..a8c22a26 100644 --- a/ZR.Admin.WebApi/background/SocketBackgroundService.cs +++ b/ZR.Admin.WebApi/background/SocketBackgroundService.cs @@ -1,6 +1,7 @@  using DOAN.ServiceCore.MyMatchPush; using Infrastructure; +using Infrastructure.Helper; using Microsoft.Extensions.Logging; namespace ZR.Admin.WebApi.background @@ -8,12 +9,14 @@ namespace ZR.Admin.WebApi.background public class SocketBackgroundService : BackgroundService { private readonly SocketGatewayServer _socketGateway; + private readonly ModbusTcpClientHelper _modbusClient; private readonly ILogger _logger; - public SocketBackgroundService(SocketGatewayServer socketGateway, ILogger logger) + public SocketBackgroundService(SocketGatewayServer socketGateway, ILogger logger, ModbusTcpClientHelper modbusClient) { _socketGateway = socketGateway; _logger = logger; + _modbusClient = modbusClient; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -22,15 +25,111 @@ namespace ZR.Admin.WebApi.background { // 等待服务完全启动 await Task.Delay(1000, stoppingToken); - + // 测试Socket推送功能 - //var result = Watchup.StartPush("测试Socket推送功能", _socketGateway); - //_logger.LogInformation($"Socket推送测试结果: {result}"); + var result = Watchup.StartPush("测试Socket推送功能", _socketGateway); + _logger.LogInformation($"Socket推送测试结果: {result}"); + TestLight(); } catch (Exception ex) { _logger.LogError(ex, "Socket推送测试失败"); } } + + private void TestLight() + { + // 2. 定义要读取的寄存器信息(根据您的表格) + const byte UNIT_ID = 0x01; // 从站地址,根据您的指令示例是 01 + ushort startAddress = 0x0000; // 起始地址,对应 40001 + ushort numberOfRegisters = 12; // 要读取的寄存器数量 (40001 到 40012) + + // 3. PLC 下发读指令并获取原始寄存器值列表 + Console.WriteLine($"正在读取从站 {UNIT_ID} 的寄存器 {startAddress} 开始的 {numberOfRegisters} 个寄存器..."); + List registerValues = _modbusClient.ReadHoldingRegisters(UNIT_ID, startAddress, numberOfRegisters); + + // 4. 解析警灯返回指令(即解析读取到的寄存器值) + if (registerValues.Count == numberOfRegisters) + { + Console.WriteLine("读取成功,解析数据:"); + + // 创建一个字典或列表来存储解析后的状态 + var lightStatus = new Dictionary + { + { "红色", registerValues[0] }, // 40001 -> index 0 + { "绿色", registerValues[1] }, // 40002 -> index 1 + { "黄色", registerValues[2] }, // 40003 -> index 2 + { "蓝色", registerValues[3] }, // 40004 -> index 3 + { "白色", registerValues[4] }, // 40005 -> index 4 + { "备用1", registerValues[5] }, // 40006 -> index 5 + { "备用2", registerValues[6] }, // 40007 -> index 6 + { "备用3", registerValues[7] }, // 40008 -> index 7 + { "备用4", registerValues[8] }, // 40009 -> index 8 + { "备用5", registerValues[9] }, // 40010 -> index 9 + { "蜂鸣器", registerValues[10] }, // 40011 -> index 10 + { "电池电量", registerValues[11] } // 40012 -> index 11 + }; + + // 打印解析结果 + foreach (var status in lightStatus) + { + Console.WriteLine($"{status.Key}: {status.Value}"); + } + + // --- 根据业务逻辑解读状态值 --- + // 示例:假设 0=灭/关, 1=闪烁, 2=常亮 (具体含义需查阅设备手册) + InterpretLightStatus(lightStatus); + } + } + /// + /// 根据业务逻辑解读灯的状态值(这是一个示例,具体值需参考设备手册) + /// + static void InterpretLightStatus(Dictionary status) + { + Console.WriteLine("\n--- 状态解读 (示例) ---"); + + // 红色 + switch (status["红色"]) + { + case 0: Console.WriteLine("红色: 灭"); break; + case 1: Console.WriteLine("红色: 0.1S 闪烁"); break; // 根据您之前的例子 + default: Console.WriteLine($"红色: 未知状态 ({status["红色"]})"); break; + } + + // 绿色 + switch (status["绿色"]) + { + case 0: Console.WriteLine("绿色: 灭"); break; + case 10: Console.WriteLine("绿色: 1S 闪烁"); break; // 根据您之前的例子 (10 * 0.1S = 1S) + default: Console.WriteLine($"绿色: 未知状态 ({status["绿色"]})"); break; + } + + // 黄色 + switch (status["黄色"]) + { + case 0: Console.WriteLine("黄色: 灭"); break; + case 99: Console.WriteLine("黄色: 常亮"); break; // 根据您之前的例子 + default: Console.WriteLine($"黄色: 未知状态 ({status["黄色"]})"); break; + } + + // 蓝、白 + Console.WriteLine($"蓝色: {(status["蓝色"] == 0 ? "灭" : $"未知状态({status["蓝色"]})")}"); + Console.WriteLine($"白色: {(status["白色"] == 0 ? "灭" : $"未知状态({status["白色"]})")}"); + + // 蜂鸣器 + switch (status["蜂鸣器"]) + { + case 0: Console.WriteLine("蜂鸣器: 静音"); break; + case 10: Console.WriteLine("蜂鸣器: 1S 蜂鸣"); break; // 根据您之前的例子 + default: Console.WriteLine($"蜂鸣器: 未知状态 ({status["蜂鸣器"]})"); break; + } + + // 电池电量 + Console.WriteLine($"电池电量: {status["电池电量"]}%"); // 假设寄存器值直接代表百分比 + } + + + + } + } -} diff --git a/ZR.Service/Utils/MyAlarmLigth/LightUp.cs b/ZR.Service/Utils/MyAlarmLigth/LightUp.cs new file mode 100644 index 00000000..e13515b1 --- /dev/null +++ b/ZR.Service/Utils/MyAlarmLigth/LightUp.cs @@ -0,0 +1,95 @@ +using Infrastructure.Helper; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace ZR.Service.Utils.MyAlarmLigth +{ + /// + /// 三色灯帮助类 + /// + public class LightUp + { + /// + /// 读警灯设备站号 + /// + /// 0 没有解析出来 + /// 其他 解析出来设备站号 + + public static ushort ReadAlarmlightNumber(ModbusTcpClientHelper _modbusClient) + { + + // 2. 定义要读取的寄存器信息(根据您的表格) + const byte UNIT_ID = 0x00; // 从站地址,根据您的指令示例是 01 + ushort startAddress = 0x0fa1; // 起始地址,对应 + ushort numberOfRegisters = 1; // 要读取的寄存器数量 (40001 到 40012) + + // 3. PLC 下发读指令并获取原始寄存器值列表 + Console.WriteLine($"正在读取从站{UNIT_ID}广播的寄存器 {startAddress} 开始的 {numberOfRegisters} 个寄存器..."); + List registerValues = _modbusClient.ReadHoldingRegisters(UNIT_ID, startAddress, numberOfRegisters); + + if (registerValues.Count == numberOfRegisters) + { + Console.WriteLine("读取成功,解析数据:"); + return registerValues[0]; + + } + return 0; + } + + + + /// + /// 向指定的警灯设备写入指令,控制警灯的状态为红色闪烁 且蜂鸣器响 + /// + /// + /// 警灯站号 + /// + /// + public static void WriteAlarmLightCommand_Red(ModbusTcpClientHelper _modbusClient, byte unitId ) + { + ushort startAddress = 0x0000; // 起始地址,对应 + ushort numberOfRegisters = 0x000B; // 要读取的寄存器数量 (40001 到 40012) + List commandValues= new List { 0x00001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,0x0000, 0x0000,0x000A }; // 红色闪烁 且蜂鸣器响 + // 发送写指令到警灯设备 + _modbusClient.WriteMultipleRegisters(unitId, startAddress, commandValues); + Console.WriteLine($"已向从站 {unitId} 的寄存器 {startAddress} 写入指令: {string.Join(", ", commandValues)}"); + } + /// + /// 向指定的警灯设备写入指令,控制警灯的状态为黄色常亮 且蜂鸣器响 + /// + /// + /// 警灯站号 + /// + /// + public static void WriteAlarmLightCommand_Yellow(ModbusTcpClientHelper _modbusClient, byte unitId) + { + ushort startAddress = 0x0000; // 起始地址,对应 + ushort numberOfRegisters = 0x000B; // 要读取的寄存器数量 (40001 到 40012) + List commandValues = new List { 0x0000, 0x0000, 0x0063, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x000A }; // 红色闪烁 且蜂鸣器响 + // 发送写指令到警灯设备 + _modbusClient.WriteMultipleRegisters(unitId, startAddress, commandValues); + Console.WriteLine($"已向从站 {unitId} 的寄存器 {startAddress} 写入指令: {string.Join(", ", commandValues)}"); + } + + /// + /// 向指定的警灯设备写入指令,控制警灯的状态为绿灯常亮 且蜂鸣器关闭 + /// + /// + /// 警灯站号 + /// + /// + public static void WriteAlarmLightCommand_Normal(ModbusTcpClientHelper _modbusClient, byte unitId) + { + ushort startAddress = 0x0000; // 起始地址,对应 + ushort numberOfRegisters = 0x000B; // 要读取的寄存器数量 (40001 到 40012) + List commandValues = new List { 0x0000, 0x0063, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }; // 红色闪烁 且蜂鸣器响 + // 发送写指令到警灯设备 + _modbusClient.WriteMultipleRegisters(unitId, startAddress, commandValues); + Console.WriteLine($"已向从站 {unitId} 的寄存器 {startAddress} 写入指令: {string.Join(", ", commandValues)}"); + } + } +}