shgx_tz_mom/Infrastructure/Helper/ModbusTcpClientHelper.cs
2026-01-06 08:49:12 +08:00

459 lines
18 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 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
{
Console.WriteLine($"连接Modbus TCP服务器已启动");
_tcpClient = new TcpClient();
_tcpClient.SendTimeout = timeoutMs;
_tcpClient.ReceiveTimeout = timeoutMs;
_tcpClient.Connect(ip, port);
_networkStream = _tcpClient.GetStream();
Console.WriteLine($"连接Modbus TCP服务器已成功");
}
catch (Exception ex)
{
Console.WriteLine($"连接Modbus TCP服务器失败 {ex.Message}");
}
}
}
/// <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=ONfalse=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=0xFF00OFF=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=0Modbus
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
{
// 1. 清空流缓存仅用DataAvailable+循环读取不依赖Length
if (_networkStream.DataAvailable)
{
byte[] discardBuffer = new byte[1024];
while (_networkStream.DataAvailable)
{
_networkStream.Read(discardBuffer, 0, discardBuffer.Length);
}
}
// 2. 发送请求
_networkStream.Write(request, 0, request.Length);
_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}");
// 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();
int bytesRead = _networkStream.Read(pduBuffer, totalRead, length - totalRead);
if (bytesRead == 0)
break;
totalRead += bytesRead;
}
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);
return responseBuffer;
}
catch (TimeoutException)
{
throw new Exception("接收响应超时");
}
catch (Exception ex)
{
throw new Exception($"通信失败:{ex.Message}", ex);
}
}
}
/// <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;
}
/// <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*2n为寄存器数量
// 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
}
// ===== 新增这两行 =====
public class ThreeColorLightModbus : ModbusTcpClientHelper { }
public class AlarmLightModbus : ModbusTcpClientHelper { }
}