2026-06-26 20b96473f2520590a0dca6b775b81e3ea06a77a0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
 
import cn.hutool.core.util.HexUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetClientOptions;
import io.vertx.core.net.NetSocket;
import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
 
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
/**
 * IoT Modbus TCP Server 协议集成测试 — MODBUS_RTU 帧格式(手动测试)
 *
 * <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信
 *
 * <p>使用步骤:
 * <ol>
 *     <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)</li>
 *     <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)</li>
 *     <li>运行以下测试方法:
 *         <ul>
 *             <li>{@link #testAuth()} - 自定义功能码认证</li>
 *             <li>{@link #testPollingResponse()} - 轮询响应</li>
 *             <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li>
 *         </ul>
 *     </li>
 * </ol>
 *
 * @author 芋道源码
 */
@Slf4j
@Disabled
public class IotModbusTcpServerRtuIntegrationTest {
 
    private static final String SERVER_HOST = "127.0.0.1";
    private static final int SERVER_PORT = 503;
    private static final int TIMEOUT_MS = 5000;
 
    private static final int CUSTOM_FC = 65;
    private static final int SLAVE_ID = 1;
 
    private static Vertx vertx;
    private static NetClient netClient;
 
    // ===================== 编解码器 =====================
 
    private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC);
    private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC);
 
    // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
 
    private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo";
    private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_rtu";
    private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e";
 
    @BeforeAll
    static void setUp() {
        vertx = Vertx.vertx();
        NetClientOptions options = new NetClientOptions()
                .setConnectTimeout(TIMEOUT_MS)
                .setIdleTimeout(TIMEOUT_MS);
        netClient = vertx.createNetClient(options);
    }
 
    @AfterAll
    static void tearDown() {
        if (netClient != null) {
            netClient.close();
        }
        if (vertx != null) {
            vertx.close();
        }
    }
 
    // ===================== 认证测试 =====================
 
    /**
     * 认证测试:发送自定义功能码 FC65 认证帧(RTU 格式),验证认证成功响应
     */
    @Test
    public void testAuth() throws Exception {
        NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
        try {
            // 1. 构造并发送认证帧
            IotModbusFrame response = authenticate(socket);
 
            // 2. 验证响应
            log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
                    response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
            JSONObject json = JSONUtil.parseObj(response.getCustomData());
            assertEquals(0, json.getInt("code"));
            log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
        } finally {
            socket.close();
        }
    }
 
    // ===================== 轮询响应测试 =====================
 
    /**
     * 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回
     */
    @Test
    public void testPollingResponse() throws Exception {
        NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
        try {
            // 1. 先认证
            IotModbusFrame authResponse = authenticate(socket);
            log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
            JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData());
            assertEquals(0, authJson.getInt("code"));
 
            // 2. 设置持续监听:每收到一个读请求,自动回复
            log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
            CompletableFuture<Void> done = new CompletableFuture<>();
            // 注意:使用 requestMode=true,因为设备端收到的是网关下发的读请求(非响应)
            RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> {
                log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]",
                        frame.getSlaveId(), frame.getFunctionCode());
                // 解析读请求中的起始地址和数量
                byte[] pdu = frame.getPdu();
                int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF);
                int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF);
                log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity);
 
                // 构造读响应帧(模拟寄存器数据,RTU 格式)
                int[] registerValues = new int[quantity];
                for (int i = 0; i < quantity; i++) {
                    registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
                }
                byte[] responseData = buildReadResponse(frame.getSlaveId(),
                        frame.getFunctionCode(), registerValues);
                socket.write(Buffer.buffer(responseData));
                log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
            }, true);
            socket.handler(parser);
 
            // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求
            Thread.sleep(200000);
        } finally {
            socket.close();
        }
    }
 
    // ===================== 属性设置测试 =====================
 
    /**
     * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求(RTU 格式)
     * <p>
     * 注意:需手动在平台触发 property.set
     */
    @Test
    public void testPropertySetWrite() throws Exception {
        NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
        try {
            // 1. 先认证
            IotModbusFrame authResponse = authenticate(socket);
            log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData());
 
            // 2. 等待网关下发写请求(需手动在平台触发 property.set)
            log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]");
            IotModbusFrame writeRequest = waitForRequest(socket);
            log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]",
                    writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
                    HexUtil.encodeHexStr(writeRequest.getPdu()));
        } finally {
            socket.close();
        }
    }
 
    // ===================== 辅助方法 =====================
 
    /**
     * 建立 TCP 连接
     */
    private CompletableFuture<NetSocket> connect() {
        CompletableFuture<NetSocket> future = new CompletableFuture<>();
        netClient.connect(SERVER_PORT, SERVER_HOST)
                .onSuccess(future::complete)
                .onFailure(future::completeExceptionally);
        return future;
    }
 
    /**
     * 执行认证并返回响应帧
     */
    private IotModbusFrame authenticate(NetSocket socket) throws Exception {
        IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
        authInfo.setClientId("");
        byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
        return sendAndReceive(socket, authFrame);
    }
 
    /**
     * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码)
     */
    private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
        CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
        // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame)
        RecordParser parser = FRAME_DECODER.createRecordParser(
                (frame, frameFormat) -> {
                    try {
                        log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat);
                        responseFuture.complete(frame);
                    } catch (Exception e) {
                        responseFuture.completeExceptionally(e);
                    }
                });
        socket.handler(parser);
 
        // 发送请求
        log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length);
        socket.write(Buffer.buffer(frameData));
 
        // 等待响应
        return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
    }
 
    /**
     * 等待接收网关下发的请求帧(不发送,只等待接收)
     */
    private IotModbusFrame waitForRequest(NetSocket socket) throws Exception {
        CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>();
        // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame)
        RecordParser parser = FRAME_DECODER.createRecordParser(
                (frame, frameFormat) -> {
                    try {
                        log.info("[waitForRequest][检测到帧格式: {}]", frameFormat);
                        requestFuture.complete(frame);
                    } catch (Exception e) {
                        requestFuture.completeExceptionally(e);
                    }
                });
        socket.handler(parser);
 
        // 等待(超时 30 秒,因为轮询间隔可能比较长)
        return requestFuture.get(30000, TimeUnit.MILLISECONDS);
    }
 
    /**
     * 构造认证帧(MODBUS_RTU 格式)
     * <p>
     * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
     * <p>
     * RTU 帧格式:[SlaveId(1)] [FC=0x41(1)] [ByteCount(1)] [JSON(N)] [CRC16(2)]
     */
    private byte[] buildAuthFrame(String clientId, String username, String password) {
        JSONObject params = new JSONObject();
        params.set("clientId", clientId);
        params.set("username", username);
        params.set("password", password);
        JSONObject json = new JSONObject();
        json.set("method", "auth");
        json.set("params", params);
        return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(),
                IotModbusFrameFormatEnum.MODBUS_RTU, 0);
    }
 
    /**
     * 构造 FC03/FC01-04 读响应帧(MODBUS_RTU 格式)
     * <p>
     * RTU 帧格式:[SlaveId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] [CRC16(2)]
     */
    private byte[] buildReadResponse(int slaveId, int functionCode, int[] registerValues) {
        int byteCount = registerValues.length * 2;
        // 帧长度:SlaveId(1) + FC(1) + ByteCount(1) + Data(N*2) + CRC(2)
        int totalLength = 1 + 1 + 1 + byteCount + 2;
        byte[] frame = new byte[totalLength];
        frame[0] = (byte) slaveId;
        frame[1] = (byte) functionCode;
        frame[2] = (byte) byteCount;
        for (int i = 0; i < registerValues.length; i++) {
            frame[3 + i * 2] = (byte) ((registerValues[i] >> 8) & 0xFF);
            frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF);
        }
        // 计算 CRC16
        int crc = IotModbusCommonUtils.calculateCrc16(frame, totalLength - 2);
        frame[totalLength - 2] = (byte) (crc & 0xFF);        // CRC Low
        frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
        return frame;
    }
 
}