package cn.iocoder.yudao.module.iot.gateway.protocol.modbus;
|
|
import com.ghgande.j2mod.modbus.io.ModbusRTUTCPTransport;
|
import com.ghgande.j2mod.modbus.msg.*;
|
import com.ghgande.j2mod.modbus.procimg.*;
|
import com.ghgande.j2mod.modbus.slave.ModbusSlave;
|
import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;
|
|
import java.net.ServerSocket;
|
import java.net.Socket;
|
|
/**
|
* Modbus RTU over TCP 完整 Demo
|
*
|
* 架构:Master(主站)启动 TCP Server 监听 → Slave(从站)主动 TCP 连接上来
|
* 通信协议:RTU 帧格式(带 CRC)通过 TCP 传输,而非标准 MBAP 头
|
*
|
* 流程:
|
* 1. Master 启动 TCP ServerSocket 监听端口
|
* 2. Slave(从站模拟器)作为 TCP Client 连接到 Master
|
* 3. Master 通过 accept 得到的 Socket,使用 {@link ModbusRTUTCPTransport} 发送读写请求
|
*
|
* 实现说明:
|
* 因为 j2mod 的 ModbusSlave 只能以 TCP Server 模式运行(监听端口等待 Master 连接),
|
* 不支持"Slave 作为 TCP Client 主动连接 Master"的模式。
|
* 所以这里用一个 TCP 桥接(bridge)来模拟:
|
* - Slave 在本地内部端口启动(RTU over TCP 模式)
|
* - 一个桥接线程同时连接 Master Server 和 Slave 内部端口,做双向数据转发
|
* - Master 视角:看到的是 Slave 主动连上来
|
*
|
* 依赖:j2mod 3.2.1(pom.xml 中已声明)
|
*
|
* @author 芋道源码
|
*/
|
@Deprecated // 仅技术演示,非是必须的
|
public class ModbusRtuOverTcpDemo {
|
|
/**
|
* Master(主站)TCP Server 监听端口
|
*/
|
private static final int PORT = 5021;
|
/**
|
* Slave 内部端口(仅本地中转用,不对外暴露)
|
*/
|
private static final int SLAVE_INTERNAL_PORT = PORT + 100;
|
/**
|
* Modbus 从站地址
|
*/
|
private static final int SLAVE_ID = 1;
|
|
public static void main(String[] args) throws Exception {
|
// ===================== 第一步:Master 启动 TCP Server 监听 =====================
|
ServerSocket serverSocket = new ServerSocket(PORT);
|
System.out.println("===================================================");
|
System.out.println("[Master] TCP Server 已启动,监听端口: " + PORT);
|
System.out.println("[Master] 等待 Slave 连接...");
|
System.out.println("===================================================");
|
|
// ===================== 第二步:后台启动 Slave,它会主动连接 Master =====================
|
ModbusSlave slave = startSlaveInBackground();
|
|
// Master accept Slave 的连接
|
Socket slaveSocket = serverSocket.accept();
|
System.out.println("[Master] Slave 已连接: " + slaveSocket.getRemoteSocketAddress());
|
|
// ===================== 第三步:Master 通过 RTU over TCP 发送读写请求 =====================
|
// 使用 ModbusRTUTCPTransport 包装 Socket(RTU 帧 = SlaveID + 功能码 + 数据 + CRC,无 MBAP 头)
|
ModbusRTUTCPTransport transport = new ModbusRTUTCPTransport(slaveSocket);
|
|
try {
|
System.out.println("[Master] RTU over TCP 通道已建立\n");
|
|
// 1. 读操作演示:4 种功能码
|
demoReadCoils(transport); // 功能码 01:读线圈
|
demoReadDiscreteInputs(transport); // 功能码 02:读离散输入
|
demoReadHoldingRegisters(transport); // 功能码 03:读保持寄存器
|
demoReadInputRegisters(transport); // 功能码 04:读输入寄存器
|
|
// 2. 写操作演示 + 读回验证
|
demoWriteCoil(transport); // 功能码 05:写单个线圈
|
demoWriteRegister(transport); // 功能码 06:写单个保持寄存器
|
|
System.out.println("\n===================================================");
|
System.out.println("所有 RTU over TCP 读写操作执行成功!");
|
System.out.println("===================================================");
|
} finally {
|
// 清理资源
|
transport.close();
|
slaveSocket.close();
|
serverSocket.close();
|
slave.close();
|
System.out.println("[Master] 资源已关闭");
|
}
|
}
|
|
// ===================== Slave 设备模拟(作为 TCP Client 连接 Master) =====================
|
|
/**
|
* 在后台启动从站模拟器,并通过 TCP 桥接连到 Master Server
|
*
|
* @return ModbusSlave 实例(用于最后关闭资源)
|
*/
|
private static ModbusSlave startSlaveInBackground() throws Exception {
|
// 1. 创建进程映像,初始化寄存器数据
|
SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID);
|
// 1.1 线圈(Coil,功能码 01/05)- 可读写,地址 0~9
|
for (int i = 0; i < 10; i++) {
|
spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0));
|
}
|
// 1.2 离散输入(Discrete Input,功能码 02)- 只读,地址 0~9
|
for (int i = 0; i < 10; i++) {
|
spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0));
|
}
|
// 1.3 保持寄存器(Holding Register,功能码 03/06/16)- 可读写,地址 0~19
|
for (int i = 0; i < 20; i++) {
|
spi.addRegister(new SimpleRegister(i * 100));
|
}
|
// 1.4 输入寄存器(Input Register,功能码 04)- 只读,地址 0~19
|
for (int i = 0; i < 20; i++) {
|
spi.addInputRegister(new SimpleInputRegister(i * 10 + 1));
|
}
|
|
// 2. 启动 Slave(RTU over TCP 模式,在本地内部端口监听)
|
ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(SLAVE_INTERNAL_PORT, 5, true);
|
slave.addProcessImage(SLAVE_ID, spi);
|
slave.open();
|
System.out.println("[Slave] 从站模拟器已启动(内部端口: " + SLAVE_INTERNAL_PORT + ")");
|
|
// 3. 启动桥接线程:TCP Client 连接 Master Server,同时连接 Slave 内部端口,双向转发
|
Thread bridgeThread = new Thread(() -> {
|
try {
|
Socket toMaster = new Socket("127.0.0.1", PORT);
|
Socket toSlave = new Socket("127.0.0.1", SLAVE_INTERNAL_PORT);
|
System.out.println("[Bridge] 已建立桥接: Master(" + PORT + ") <-> Slave(" + SLAVE_INTERNAL_PORT + ")");
|
|
// 双向桥接:Master ↔ Bridge ↔ Slave
|
Thread forward = new Thread(() -> bridge(toMaster, toSlave), "bridge-master→slave");
|
Thread backward = new Thread(() -> bridge(toSlave, toMaster), "bridge-slave→master");
|
forward.setDaemon(true);
|
backward.setDaemon(true);
|
forward.start();
|
backward.start();
|
} catch (Exception e) {
|
e.printStackTrace();
|
}
|
}, "bridge-setup");
|
bridgeThread.setDaemon(true);
|
bridgeThread.start();
|
|
return slave;
|
}
|
|
/**
|
* TCP 双向桥接:从 src 读取数据,写入 dst
|
*/
|
private static void bridge(Socket src, Socket dst) {
|
try {
|
byte[] buf = new byte[1024];
|
var in = src.getInputStream();
|
var out = dst.getOutputStream();
|
int len;
|
while ((len = in.read(buf)) != -1) {
|
out.write(buf, 0, len);
|
out.flush();
|
}
|
} catch (Exception ignored) {
|
// 连接关闭时正常退出
|
}
|
}
|
|
// ===================== Master 读写操作 =====================
|
|
/**
|
* 发送请求并接收响应(通用方法)
|
*/
|
private static ModbusResponse sendRequest(ModbusRTUTCPTransport transport, ModbusRequest request) throws Exception {
|
request.setUnitID(SLAVE_ID);
|
transport.writeRequest(request);
|
return transport.readResponse();
|
}
|
|
/**
|
* 功能码 01:读线圈(Read Coils)
|
*/
|
private static void demoReadCoils(ModbusRTUTCPTransport transport) throws Exception {
|
ReadCoilsRequest request = new ReadCoilsRequest(0, 5);
|
ReadCoilsResponse response = (ReadCoilsResponse) sendRequest(transport, request);
|
|
StringBuilder sb = new StringBuilder("[功能码 01] 读线圈(0~4): ");
|
for (int i = 0; i < 5; i++) {
|
sb.append(response.getCoilStatus(i) ? "ON" : "OFF");
|
if (i < 4) {
|
sb.append(", ");
|
}
|
}
|
System.out.println(sb);
|
}
|
|
/**
|
* 功能码 02:读离散输入(Read Discrete Inputs)
|
*/
|
private static void demoReadDiscreteInputs(ModbusRTUTCPTransport transport) throws Exception {
|
ReadInputDiscretesRequest request = new ReadInputDiscretesRequest(0, 5);
|
ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) sendRequest(transport, request);
|
|
StringBuilder sb = new StringBuilder("[功能码 02] 读离散输入(0~4): ");
|
for (int i = 0; i < 5; i++) {
|
sb.append(response.getDiscreteStatus(i) ? "ON" : "OFF");
|
if (i < 4) {
|
sb.append(", ");
|
}
|
}
|
System.out.println(sb);
|
}
|
|
/**
|
* 功能码 03:读保持寄存器(Read Holding Registers)
|
*/
|
private static void demoReadHoldingRegisters(ModbusRTUTCPTransport transport) throws Exception {
|
ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(0, 5);
|
ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) sendRequest(transport, request);
|
|
StringBuilder sb = new StringBuilder("[功能码 03] 读保持寄存器(0~4): ");
|
for (int i = 0; i < response.getWordCount(); i++) {
|
sb.append(response.getRegisterValue(i));
|
if (i < response.getWordCount() - 1) {
|
sb.append(", ");
|
}
|
}
|
System.out.println(sb);
|
}
|
|
/**
|
* 功能码 04:读输入寄存器(Read Input Registers)
|
*/
|
private static void demoReadInputRegisters(ModbusRTUTCPTransport transport) throws Exception {
|
ReadInputRegistersRequest request = new ReadInputRegistersRequest(0, 5);
|
ReadInputRegistersResponse response = (ReadInputRegistersResponse) sendRequest(transport, request);
|
|
StringBuilder sb = new StringBuilder("[功能码 04] 读输入寄存器(0~4): ");
|
for (int i = 0; i < response.getWordCount(); i++) {
|
sb.append(response.getRegisterValue(i));
|
if (i < response.getWordCount() - 1) {
|
sb.append(", ");
|
}
|
}
|
System.out.println(sb);
|
}
|
|
/**
|
* 功能码 05:写单个线圈(Write Single Coil)+ 读回验证
|
*/
|
private static void demoWriteCoil(ModbusRTUTCPTransport transport) throws Exception {
|
int address = 0;
|
|
// 1. 先读取当前值
|
ReadCoilsRequest readReq = new ReadCoilsRequest(address, 1);
|
ReadCoilsResponse readResp = (ReadCoilsResponse) sendRequest(transport, readReq);
|
boolean beforeValue = readResp.getCoilStatus(0);
|
|
// 2. 写入相反的值
|
boolean writeValue = !beforeValue;
|
WriteCoilRequest writeReq = new WriteCoilRequest(address, writeValue);
|
sendRequest(transport, writeReq);
|
|
// 3. 读回验证
|
ReadCoilsResponse verifyResp = (ReadCoilsResponse) sendRequest(transport, readReq);
|
boolean afterValue = verifyResp.getCoilStatus(0);
|
|
System.out.println("[功能码 05] 写线圈: 地址=" + address
|
+ ", 写入前=" + (beforeValue ? "ON" : "OFF")
|
+ ", 写入值=" + (writeValue ? "ON" : "OFF")
|
+ ", 读回值=" + (afterValue ? "ON" : "OFF")
|
+ (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
|
}
|
|
/**
|
* 功能码 06:写单个保持寄存器(Write Single Register)+ 读回验证
|
*/
|
private static void demoWriteRegister(ModbusRTUTCPTransport transport) throws Exception {
|
int address = 0;
|
int writeValue = 12345;
|
|
// 1. 先读取当前值
|
ReadMultipleRegistersRequest readReq = new ReadMultipleRegistersRequest(address, 1);
|
ReadMultipleRegistersResponse readResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
|
int beforeValue = readResp.getRegisterValue(0);
|
|
// 2. 写入新值
|
WriteSingleRegisterRequest writeReq = new WriteSingleRegisterRequest(address, new SimpleRegister(writeValue));
|
sendRequest(transport, writeReq);
|
|
// 3. 读回验证
|
ReadMultipleRegistersResponse verifyResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
|
int afterValue = verifyResp.getRegisterValue(0);
|
|
System.out.println("[功能码 06] 写保持寄存器: 地址=" + address
|
+ ", 写入前=" + beforeValue
|
+ ", 写入值=" + writeValue
|
+ ", 读回值=" + afterValue
|
+ (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
|
}
|
|
}
|