2025-12-23 09:17:36 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Modbus TCP 客户端帮助类(支持常用功能码) 客户端 = 主站
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class ModbusTcpClientHelper : IDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
private TcpClient _tcpClient;
|
|
|
|
|
|
private NetworkStream _networkStream;
|
|
|
|
|
|
private readonly object _lockObj = new object(); // 线程安全锁
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 连接Modbus TCP服务器 服务端 = 从站
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="ip">服务器IP</param>
|
|
|
|
|
|
/// <param name="port">端口(默认502)</param>
|
|
|
|
|
|
/// <param name="timeoutMs">超时时间(毫秒,默认3000)</param>
|
|
|
|
|
|
public void Connect(string ip, int port = 502, int timeoutMs = 3000)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_tcpClient != null && _tcpClient.Connected)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
lock (_lockObj)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-12-24 14:26:46 +08:00
|
|
|
|
Console.WriteLine($"连接Modbus TCP服务器已启动");
|
2025-12-23 09:17:36 +08:00
|
|
|
|
_tcpClient = new TcpClient();
|
|
|
|
|
|
_tcpClient.SendTimeout = timeoutMs;
|
|
|
|
|
|
_tcpClient.ReceiveTimeout = timeoutMs;
|
|
|
|
|
|
_tcpClient.Connect(ip, port);
|
|
|
|
|
|
_networkStream = _tcpClient.GetStream();
|
2025-12-24 14:26:46 +08:00
|
|
|
|
Console.WriteLine($"连接Modbus TCP服务器已成功");
|
2025-12-23 09:17:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2025-12-24 14:26:46 +08:00
|
|
|
|
Console.WriteLine($"连接Modbus TCP服务器失败: {ex.Message}");
|
2025-12-23 09:17:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 断开连接
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Disconnect()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_lockObj)
|
|
|
|
|
|
{
|
|
|
|
|
|
_networkStream?.Close();
|
|
|
|
|
|
_tcpClient?.Close();
|
|
|
|
|
|
_networkStream = null;
|
|
|
|
|
|
_tcpClient = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 读取保持寄存器(功能码0x03)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unitId">从站地址(单元ID)</param>
|
|
|
|
|
|
/// <param name="startAddress">起始地址(0-based)</param>
|
|
|
|
|
|
/// <param name="numberOfRegisters">寄存器数量</param>
|
|
|
|
|
|
/// <returns>寄存器值列表(每个寄存器2字节,高位在前)</returns>
|
|
|
|
|
|
public List<ushort> ReadHoldingRegisters(byte unitId, ushort startAddress, ushort numberOfRegisters)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = BuildReadRequest(unitId, 0x03, startAddress, numberOfRegisters);
|
|
|
|
|
|
var response = SendAndReceive(request);
|
|
|
|
|
|
return ParseReadRegistersResponse(response, numberOfRegisters);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 读取输入寄存器(功能码0x04)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public List<ushort> ReadInputRegisters(byte unitId, ushort startAddress, ushort numberOfRegisters)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = BuildReadRequest(unitId, 0x04, startAddress, numberOfRegisters);
|
|
|
|
|
|
var response = SendAndReceive(request);
|
|
|
|
|
|
return ParseReadRegistersResponse(response, numberOfRegisters);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 写入单个保持寄存器(功能码0x06)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unitId">从站地址</param>
|
|
|
|
|
|
/// <param name="address">寄存器地址(0-based)</param>
|
|
|
|
|
|
/// <param name="value">要写入的值(16位)</param>
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 写入多个保持寄存器(功能码0x10)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unitId">从站地址</param>
|
|
|
|
|
|
/// <param name="startAddress">起始地址(0-based)</param>
|
|
|
|
|
|
/// <param name="values">要写入的值列表(每个值16位)</param>
|
|
|
|
|
|
public void WriteMultipleRegisters(byte unitId, ushort startAddress, List<ushort> values)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = BuildWriteMultipleRequest(unitId, 0x10, startAddress, values);
|
|
|
|
|
|
var response = SendAndReceive(request);
|
|
|
|
|
|
ValidateWriteResponse(response, 0x10, startAddress, (ushort)values.Count);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 读取线圈状态(功能码0x01)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unitId">从站地址</param>
|
|
|
|
|
|
/// <param name="startAddress">起始地址(0-based)</param>
|
|
|
|
|
|
/// <param name="numberOfCoils">线圈数量(1-2000)</param>
|
|
|
|
|
|
/// <returns>线圈状态列表(true=ON,false=OFF)</returns>
|
|
|
|
|
|
public List<bool> ReadCoils(byte unitId, ushort startAddress, ushort numberOfCoils)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = BuildReadRequest(unitId, 0x01, startAddress, numberOfCoils);
|
|
|
|
|
|
var response = SendAndReceive(request);
|
|
|
|
|
|
return ParseReadCoilsResponse(response, numberOfCoils);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 写入单个线圈(功能码0x05)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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 私有方法(协议构建与解析)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构建读请求报文(功能码0x01、0x02、0x03、0x04通用)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private byte[] BuildReadRequest(byte unitId, byte functionCode, ushort startAddress, ushort quantity)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = new List<byte>
|
|
|
|
|
|
{
|
|
|
|
|
|
unitId, // 单元ID
|
|
|
|
|
|
functionCode // 功能码
|
|
|
|
|
|
};
|
|
|
|
|
|
request.AddRange(BitConverter.GetBytes(startAddress).Reverse()); // 起始地址(大端序)
|
|
|
|
|
|
request.AddRange(BitConverter.GetBytes(quantity).Reverse()); // 数量(大端序)
|
|
|
|
|
|
AddCrcOrLength(request); // 添加长度(Modbus TCP无CRC,用MBAP长度)
|
|
|
|
|
|
return request.ToArray();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构建写单个寄存器请求(功能码0x06)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private byte[] BuildWriteSingleRequest(byte unitId, byte functionCode, ushort address, ushort value)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = new List<byte>
|
|
|
|
|
|
{
|
|
|
|
|
|
unitId,
|
|
|
|
|
|
functionCode
|
|
|
|
|
|
};
|
|
|
|
|
|
request.AddRange(BitConverter.GetBytes(address).Reverse());
|
|
|
|
|
|
request.AddRange(BitConverter.GetBytes(value).Reverse());
|
|
|
|
|
|
AddCrcOrLength(request);
|
|
|
|
|
|
return request.ToArray();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构建写多个寄存器请求(功能码0x10)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private byte[] BuildWriteMultipleRequest(byte unitId, byte functionCode, ushort startAddress, List<ushort> values)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = new List<byte>
|
|
|
|
|
|
{
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构建写单个线圈请求(功能码0x05)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private byte[] BuildWriteSingleCoilRequest(byte unitId, byte functionCode, ushort address, bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
var request = new List<byte>
|
|
|
|
|
|
{
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 添加MBAP头的长度字段(Modbus TCP无CRC,用长度标识后续字节数)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void AddCrcOrLength(List<byte> data)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Modbus TCP格式:[事务ID(2)][协议ID(2)][长度(2)][单元ID(1)][PDU...]
|
|
|
|
|
|
// 此处简化:假设事务ID=0,协议ID=0,长度=单元ID+PDU长度(即data.Length)
|
|
|
|
|
|
var mbapHeader = new List<byte>
|
|
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 13:31:46 +08:00
|
|
|
|
|
2025-12-23 09:17:36 +08:00
|
|
|
|
private byte[] SendAndReceive(byte[] request)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_networkStream == null || !_tcpClient.Connected)
|
|
|
|
|
|
throw new InvalidOperationException("未连接到Modbus TCP服务器");
|
|
|
|
|
|
|
|
|
|
|
|
lock (_lockObj)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-12-23 13:31:46 +08:00
|
|
|
|
// 1. 清空流缓存(仅用DataAvailable+循环读取,不依赖Length)
|
|
|
|
|
|
if (_networkStream.DataAvailable)
|
|
|
|
|
|
{
|
|
|
|
|
|
byte[] discardBuffer = new byte[1024];
|
|
|
|
|
|
while (_networkStream.DataAvailable)
|
|
|
|
|
|
{
|
|
|
|
|
|
_networkStream.Read(discardBuffer, 0, discardBuffer.Length);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 发送请求
|
2025-12-23 09:17:36 +08:00
|
|
|
|
_networkStream.Write(request, 0, request.Length);
|
2025-12-23 13:31:46 +08:00
|
|
|
|
_networkStream.Flush();
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 读取MBAP头(6字节):循环读取直到获取完整6字节
|
|
|
|
|
|
byte[] headerBuffer = new byte[6];
|
|
|
|
|
|
int totalRead = 0;
|
|
|
|
|
|
DateTime startTime = DateTime.Now;
|
|
|
|
|
|
int timeoutMs = _tcpClient.ReceiveTimeout;
|
|
|
|
|
|
|
|
|
|
|
|
while (totalRead < 6)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 检查超时
|
|
|
|
|
|
if (DateTime.Now.Subtract(startTime).TotalMilliseconds > timeoutMs)
|
|
|
|
|
|
throw new TimeoutException();
|
|
|
|
|
|
|
|
|
|
|
|
int bytesRead = _networkStream.Read(headerBuffer, totalRead, 6 - totalRead);
|
|
|
|
|
|
if (bytesRead == 0)
|
|
|
|
|
|
break; // 连接关闭
|
|
|
|
|
|
totalRead += bytesRead;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (totalRead != 6)
|
|
|
|
|
|
throw new Exception($"接收MBAP头失败:预期6字节,实际读取{totalRead}字节");
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 解析长度字段
|
|
|
|
|
|
int length = (headerBuffer[4] << 8) | headerBuffer[5];
|
|
|
|
|
|
if (length <= 0 || length > 1024)
|
|
|
|
|
|
throw new Exception($"MBAP头长度字段无效:{length}");
|
2025-12-23 09:17:36 +08:00
|
|
|
|
|
2025-12-23 13:31:46 +08:00
|
|
|
|
// 5. 读取PDU数据
|
|
|
|
|
|
byte[] pduBuffer = new byte[length];
|
|
|
|
|
|
totalRead = 0;
|
|
|
|
|
|
startTime = DateTime.Now;
|
|
|
|
|
|
while (totalRead < length)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (DateTime.Now.Subtract(startTime).TotalMilliseconds > timeoutMs)
|
|
|
|
|
|
throw new TimeoutException();
|
2025-12-23 09:17:36 +08:00
|
|
|
|
|
2025-12-23 13:31:46 +08:00
|
|
|
|
int bytesRead = _networkStream.Read(pduBuffer, totalRead, length - totalRead);
|
|
|
|
|
|
if (bytesRead == 0)
|
|
|
|
|
|
break;
|
|
|
|
|
|
totalRead += bytesRead;
|
|
|
|
|
|
}
|
2025-12-23 09:17:36 +08:00
|
|
|
|
|
2025-12-23 13:31:46 +08:00
|
|
|
|
if (totalRead != length)
|
|
|
|
|
|
throw new Exception($"接收响应数据不完整:预期{length}字节,实际读取{totalRead}字节");
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 拼接响应
|
|
|
|
|
|
byte[] responseBuffer = new byte[6 + length];
|
|
|
|
|
|
Array.Copy(headerBuffer, 0, responseBuffer, 0, 6);
|
|
|
|
|
|
Array.Copy(pduBuffer, 0, responseBuffer, 6, length);
|
2025-12-23 09:17:36 +08:00
|
|
|
|
|
|
|
|
|
|
return responseBuffer;
|
|
|
|
|
|
}
|
2025-12-23 13:31:46 +08:00
|
|
|
|
catch (TimeoutException)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new Exception("接收响应超时");
|
|
|
|
|
|
}
|
2025-12-23 09:17:36 +08:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new Exception($"通信失败:{ex.Message}", ex);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 13:31:46 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 确保读取指定长度的字节(纯流式读取,无seek/长度操作)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="stream">网络流</param>
|
|
|
|
|
|
/// <param name="buffer">接收缓冲区</param>
|
|
|
|
|
|
/// <param name="offset">偏移量</param>
|
|
|
|
|
|
/// <param name="count">需要读取的字节数</param>
|
|
|
|
|
|
/// <param name="timeoutMs">超时时间(毫秒)</param>
|
|
|
|
|
|
/// <returns>实际读取的字节数</returns>
|
|
|
|
|
|
private int ReadExactly(NetworkStream stream, byte[] buffer, int offset, int count, int timeoutMs)
|
|
|
|
|
|
{
|
|
|
|
|
|
int totalRead = 0;
|
|
|
|
|
|
DateTime startTime = DateTime.Now;
|
|
|
|
|
|
|
|
|
|
|
|
while (totalRead < count)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 检查是否超时
|
|
|
|
|
|
if (DateTime.Now.Subtract(startTime).TotalMilliseconds > timeoutMs)
|
|
|
|
|
|
throw new TimeoutException();
|
|
|
|
|
|
|
|
|
|
|
|
// 读取剩余字节(NetworkStream.Read是非阻塞,可能只返回部分字节)
|
|
|
|
|
|
int bytesRead = stream.Read(buffer, offset + totalRead, count - totalRead);
|
|
|
|
|
|
if (bytesRead == 0)
|
|
|
|
|
|
break; // 流已关闭,无法继续读取
|
|
|
|
|
|
|
|
|
|
|
|
totalRead += bytesRead;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return totalRead;
|
|
|
|
|
|
}
|
2025-12-23 09:17:36 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 解析读寄存器响应(功能码0x03、0x04)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private List<ushort> 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<ushort>();
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 解析读线圈响应(功能码0x01)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private List<bool> 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<bool>();
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 验证写操作响应(确保从站正确接收)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-01-06 08:49:12 +08:00
|
|
|
|
|
|
|
|
|
|
// ===== 新增这两行 =====
|
|
|
|
|
|
public class ThreeColorLightModbus : ModbusTcpClientHelper { }
|
|
|
|
|
|
public class AlarmLightModbus : ModbusTcpClientHelper { }
|
2025-12-23 09:17:36 +08:00
|
|
|
|
}
|