Commit 1b4ac9fb2429ce6194a98d41356cfe77a7300126

Authored by YunaiV
1 parent 99bcd252

feat:【iot】MQTT 协议:1)增加 gateway 相关的单测

feat:【iot】统一几个协议的单测风格
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java
... ... @@ -87,7 +87,6 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
87 87 * 网关设备认证测试:获取网关设备 Token
88 88 */
89 89 @Test
90   - @SuppressWarnings("deprecation")
91 90 public void testAuth() throws Exception {
92 91 // 1.1 构建请求
93 92 String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java
... ... @@ -75,7 +75,6 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest {
75 75 * 子设备认证测试:获取子设备 Token
76 76 */
77 77 @Test
78   - @SuppressWarnings("deprecation")
79 78 public void testAuth() throws Exception {
80 79 // 1.1 构建请求
81 80 String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
... ... @@ -89,7 +89,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
89 89  
90 90 // 1. 构建认证信息
91 91 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
92   - log.info("[testConnect][认证信息: clientId={}, username={}, password={}]",
  92 + log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
93 93 authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
94 94  
95 95 // 2. 创建客户端并连接
... ... @@ -97,19 +97,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
97 97 client.connect(SERVER_PORT, SERVER_HOST)
98 98 .onComplete(ar -> {
99 99 if (ar.succeeded()) {
100   - log.info("[testConnect][连接成功,客户端 ID: {}]", client.clientId());
  100 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
101 101 // 断开连接
102 102 client.disconnect()
103 103 .onComplete(disconnectAr -> {
104 104 if (disconnectAr.succeeded()) {
105   - log.info("[testConnect][断开连接成功]");
  105 + log.info("[testAuth][断开连接成功]");
106 106 } else {
107   - log.error("[testConnect][断开连接失败]", disconnectAr.cause());
  107 + log.error("[testAuth][断开连接失败]", disconnectAr.cause());
108 108 }
109 109 latch.countDown();
110 110 });
111 111 } else {
112   - log.error("[testConnect][连接失败]", ar.cause());
  112 + log.error("[testAuth][连接失败]", ar.cause());
113 113 latch.countDown();
114 114 }
115 115 });
... ... @@ -117,7 +117,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
117 117 // 3. 等待测试完成
118 118 boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
119 119 if (!completed) {
120   - log.warn("[testConnect][测试超时]");
  120 + log.warn("[testAuth][测试超时]");
121 121 }
122 122 }
123 123  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  7 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
  13 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  15 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  16 +import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
  17 +import io.netty.handler.codec.mqtt.MqttQoS;
  18 +import io.vertx.core.Vertx;
  19 +import io.vertx.core.buffer.Buffer;
  20 +import io.vertx.mqtt.MqttClient;
  21 +import io.vertx.mqtt.MqttClientOptions;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.junit.jupiter.api.AfterAll;
  24 +import org.junit.jupiter.api.BeforeAll;
  25 +import org.junit.jupiter.api.Test;
  26 +
  27 +import java.util.Collections;
  28 +import java.util.List;
  29 +import java.util.Map;
  30 +import java.util.concurrent.CompletableFuture;
  31 +import java.util.concurrent.CountDownLatch;
  32 +import java.util.concurrent.TimeUnit;
  33 +
  34 +/**
  35 + * IoT 网关设备 MQTT 协议集成测试(手动测试)
  36 + *
  37 + * <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 MQTT 协议管理子设备拓扑关系
  38 + *
  39 + * <p>使用步骤:
  40 + * <ol>
  41 + * <li>启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)</li>
  42 + * <li>运行以下测试方法:
  43 + * <ul>
  44 + * <li>{@link #testAuth()} - 网关设备连接认证</li>
  45 + * <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
  46 + * <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
  47 + * <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
  48 + * <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
  49 + * <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
  50 + * </ul>
  51 + * </li>
  52 + * </ol>
  53 + *
  54 + * <p>注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
  55 + * 认证成功后同一连接上的后续请求无需再携带认证信息
  56 + *
  57 + * @author 芋道源码
  58 + */
  59 +@Slf4j
  60 +public class IotGatewayDeviceMqttProtocolIntegrationTest {
  61 +
  62 + private static final String SERVER_HOST = "127.0.0.1";
  63 + private static final int SERVER_PORT = 1883;
  64 + private static final int TIMEOUT_SECONDS = 10;
  65 +
  66 + // ===================== 编解码器(MQTT 使用 Alink 协议) =====================
  67 + private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
  68 +
  69 + // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
  70 + private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
  71 + private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
  72 + private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
  73 +
  74 + // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
  75 + private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
  76 + private static final String SUB_DEVICE_NAME = "chazuo-it";
  77 + private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
  78 +
  79 + // ===================== 全局共享 Vertx 实例 =====================
  80 + private static Vertx vertx;
  81 +
  82 + @BeforeAll
  83 + public static void setUp() {
  84 + vertx = Vertx.vertx();
  85 + }
  86 +
  87 + @AfterAll
  88 + public static void tearDown() {
  89 + if (vertx != null) {
  90 + vertx.close();
  91 + }
  92 + }
  93 +
  94 + // ===================== 连接认证测试 =====================
  95 +
  96 + /**
  97 + * 网关设备认证测试:获取网关设备 Token
  98 + */
  99 + @Test
  100 + public void testAuth() throws Exception {
  101 + CountDownLatch latch = new CountDownLatch(1);
  102 +
  103 + // 1. 构建认证信息
  104 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
  105 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
  106 + log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
  107 + authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
  108 +
  109 + // 2. 创建客户端并连接
  110 + MqttClient client = connect(authInfo);
  111 + client.connect(SERVER_PORT, SERVER_HOST)
  112 + .onComplete(ar -> {
  113 + if (ar.succeeded()) {
  114 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  115 + // 断开连接
  116 + client.disconnect()
  117 + .onComplete(disconnectAr -> {
  118 + if (disconnectAr.succeeded()) {
  119 + log.info("[testAuth][断开连接成功]");
  120 + } else {
  121 + log.error("[testAuth][断开连接失败]", disconnectAr.cause());
  122 + }
  123 + latch.countDown();
  124 + });
  125 + } else {
  126 + log.error("[testAuth][连接失败]", ar.cause());
  127 + latch.countDown();
  128 + }
  129 + });
  130 +
  131 + // 3. 等待测试完成
  132 + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  133 + if (!completed) {
  134 + log.warn("[testAuth][测试超时]");
  135 + }
  136 + }
  137 +
  138 + // ===================== 拓扑管理测试 =====================
  139 +
  140 + /**
  141 + * 添加子设备拓扑关系测试
  142 + * <p>
  143 + * 网关设备向平台上报需要绑定的子设备信息
  144 + */
  145 + @Test
  146 + public void testTopoAdd() throws Exception {
  147 + // 1. 连接并认证
  148 + MqttClient client = connectAndAuth();
  149 + log.info("[testTopoAdd][连接认证成功]");
  150 +
  151 + // 2.1 订阅 _reply 主题
  152 + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
  153 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  154 + subscribeReply(client, replyTopic);
  155 +
  156 + // 2.2 构建子设备认证信息
  157 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
  158 + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
  159 + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
  160 + .setClientId(subAuthInfo.getClientId())
  161 + .setUsername(subAuthInfo.getUsername())
  162 + .setPassword(subAuthInfo.getPassword());
  163 +
  164 + // 2.3 构建请求消息
  165 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
  166 + params.setSubDevices(Collections.singletonList(subDeviceAuth));
  167 + IotDeviceMessage request = IotDeviceMessage.of(
  168 + IdUtil.fastSimpleUUID(),
  169 + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
  170 + params,
  171 + null, null, null);
  172 +
  173 + // 3. 发布消息并等待响应
  174 + String topic = String.format("/sys/%s/%s/thing/topo/add",
  175 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  176 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  177 + log.info("[testTopoAdd][响应消息: {}]", response);
  178 +
  179 + // 4. 断开连接
  180 + disconnect(client);
  181 + }
  182 +
  183 + /**
  184 + * 删除子设备拓扑关系测试
  185 + * <p>
  186 + * 网关设备向平台上报需要解绑的子设备信息
  187 + */
  188 + @Test
  189 + public void testTopoDelete() throws Exception {
  190 + // 1. 连接并认证
  191 + MqttClient client = connectAndAuth();
  192 + log.info("[testTopoDelete][连接认证成功]");
  193 +
  194 + // 2.1 订阅 _reply 主题
  195 + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
  196 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  197 + subscribeReply(client, replyTopic);
  198 +
  199 + // 2.2 构建请求消息
  200 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
  201 + params.setSubDevices(Collections.singletonList(
  202 + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
  203 + IotDeviceMessage request = IotDeviceMessage.of(
  204 + IdUtil.fastSimpleUUID(),
  205 + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
  206 + params,
  207 + null, null, null);
  208 +
  209 + // 3. 发布消息并等待响应
  210 + String topic = String.format("/sys/%s/%s/thing/topo/delete",
  211 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  212 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  213 + log.info("[testTopoDelete][响应消息: {}]", response);
  214 +
  215 + // 4. 断开连接
  216 + disconnect(client);
  217 + }
  218 +
  219 + /**
  220 + * 获取子设备拓扑关系测试
  221 + * <p>
  222 + * 网关设备向平台查询已绑定的子设备列表
  223 + */
  224 + @Test
  225 + public void testTopoGet() throws Exception {
  226 + // 1. 连接并认证
  227 + MqttClient client = connectAndAuth();
  228 + log.info("[testTopoGet][连接认证成功]");
  229 +
  230 + // 2.1 订阅 _reply 主题
  231 + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
  232 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  233 + subscribeReply(client, replyTopic);
  234 +
  235 + // 2.2 构建请求消息
  236 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
  237 + IotDeviceMessage request = IotDeviceMessage.of(
  238 + IdUtil.fastSimpleUUID(),
  239 + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
  240 + params,
  241 + null, null, null);
  242 +
  243 + // 3. 发布消息并等待响应
  244 + String topic = String.format("/sys/%s/%s/thing/topo/get",
  245 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  246 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  247 + log.info("[testTopoGet][响应消息: {}]", response);
  248 +
  249 + // 4. 断开连接
  250 + disconnect(client);
  251 + }
  252 +
  253 + // ===================== 子设备注册测试 =====================
  254 +
  255 + /**
  256 + * 子设备动态注册测试
  257 + * <p>
  258 + * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
  259 + * <p>
  260 + * 注意:此接口需要网关认证
  261 + */
  262 + @Test
  263 + public void testSubDeviceRegister() throws Exception {
  264 + // 1. 连接并认证
  265 + MqttClient client = connectAndAuth();
  266 + log.info("[testSubDeviceRegister][连接认证成功]");
  267 +
  268 + // 2.1 订阅 _reply 主题
  269 + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
  270 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  271 + subscribeReply(client, replyTopic);
  272 +
  273 + // 2.2 构建请求消息
  274 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
  275 + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
  276 + subDevice.setDeviceName("mougezishebei-mqtt");
  277 + IotDeviceMessage request = IotDeviceMessage.of(
  278 + IdUtil.fastSimpleUUID(),
  279 + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
  280 + Collections.singletonList(subDevice),
  281 + null, null, null);
  282 +
  283 + // 3. 发布消息并等待响应
  284 + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
  285 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  286 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  287 + log.info("[testSubDeviceRegister][响应消息: {}]", response);
  288 +
  289 + // 4. 断开连接
  290 + disconnect(client);
  291 + }
  292 +
  293 + // ===================== 批量上报测试 =====================
  294 +
  295 + /**
  296 + * 批量上报属性测试(网关 + 子设备)
  297 + * <p>
  298 + * 网关设备批量上报自身属性、事件,以及子设备的属性、事件
  299 + */
  300 + @Test
  301 + public void testPropertyPackPost() throws Exception {
  302 + // 1. 连接并认证
  303 + MqttClient client = connectAndAuth();
  304 + log.info("[testPropertyPackPost][连接认证成功]");
  305 +
  306 + // 2.1 订阅 _reply 主题
  307 + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
  308 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  309 + subscribeReply(client, replyTopic);
  310 +
  311 + // 2.2 构建【网关设备】自身属性
  312 + Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
  313 + .put("temperature", 25.5)
  314 + .build();
  315 +
  316 + // 2.3 构建【网关设备】自身事件
  317 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  318 + gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
  319 + gatewayEvent.setTime(System.currentTimeMillis());
  320 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
  321 + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  322 + .put("statusReport", gatewayEvent)
  323 + .build();
  324 +
  325 + // 2.4 构建【网关子设备】属性
  326 + Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
  327 + .put("power", 100)
  328 + .build();
  329 +
  330 + // 2.5 构建【网关子设备】事件
  331 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
  332 + subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
  333 + subDeviceEvent.setTime(System.currentTimeMillis());
  334 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
  335 + .<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
  336 + .put("healthCheck", subDeviceEvent)
  337 + .build();
  338 +
  339 + // 2.6 构建子设备数据
  340 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
  341 + subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
  342 + subDeviceData.setProperties(subDeviceProperties);
  343 + subDeviceData.setEvents(subDeviceEvents);
  344 +
  345 + // 2.7 构建请求消息
  346 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
  347 + params.setProperties(gatewayProperties);
  348 + params.setEvents(gatewayEvents);
  349 + params.setSubDevices(List.of(subDeviceData));
  350 + IotDeviceMessage request = IotDeviceMessage.of(
  351 + IdUtil.fastSimpleUUID(),
  352 + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
  353 + params,
  354 + null, null, null);
  355 +
  356 + // 3. 发布消息并等待响应
  357 + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
  358 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  359 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  360 + log.info("[testPropertyPackPost][响应消息: {}]", response);
  361 +
  362 + // 4. 断开连接
  363 + disconnect(client);
  364 + }
  365 +
  366 + // ===================== 辅助方法 =====================
  367 +
  368 + /**
  369 + * 创建 MQTT 客户端
  370 + *
  371 + * @param authInfo 认证信息
  372 + * @return MQTT 客户端
  373 + */
  374 + private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  375 + MqttClientOptions options = new MqttClientOptions()
  376 + .setClientId(authInfo.getClientId())
  377 + .setUsername(authInfo.getUsername())
  378 + .setPassword(authInfo.getPassword())
  379 + .setCleanSession(true)
  380 + .setKeepAliveInterval(60);
  381 + return MqttClient.create(vertx, options);
  382 + }
  383 +
  384 + /**
  385 + * 连接并认证网关设备
  386 + *
  387 + * @return 已认证的 MQTT 客户端
  388 + */
  389 + private MqttClient connectAndAuth() throws Exception {
  390 + // 1. 创建客户端并连接
  391 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
  392 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
  393 + MqttClient client = connect(authInfo);
  394 +
  395 + // 2.1 连接
  396 + CompletableFuture<MqttClient> future = new CompletableFuture<>();
  397 + client.connect(SERVER_PORT, SERVER_HOST)
  398 + .onComplete(ar -> {
  399 + if (ar.succeeded()) {
  400 + future.complete(client);
  401 + } else {
  402 + future.completeExceptionally(ar.cause());
  403 + }
  404 + });
  405 + // 2.2 等待连接结果
  406 + return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  407 + }
  408 +
  409 + /**
  410 + * 订阅响应主题
  411 + *
  412 + * @param client MQTT 客户端
  413 + * @param replyTopic 响应主题
  414 + */
  415 + private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
  416 + // 1. 订阅响应主题
  417 + CompletableFuture<Void> future = new CompletableFuture<>();
  418 + client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
  419 + .onComplete(ar -> {
  420 + if (ar.succeeded()) {
  421 + log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
  422 + future.complete(null);
  423 + } else {
  424 + future.completeExceptionally(ar.cause());
  425 + }
  426 + });
  427 + // 2. 等待订阅结果
  428 + future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  429 + }
  430 +
  431 + /**
  432 + * 发布消息并等待响应
  433 + *
  434 + * @param client MQTT 客户端
  435 + * @param topic 发布主题
  436 + * @param request 请求消息
  437 + * @return 响应消息
  438 + */
  439 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  440 + // 1. 设置消息处理器,接收响应
  441 + CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  442 + client.publishHandler(message -> {
  443 + log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
  444 + message.topicName(), message.payload().toString());
  445 + IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
  446 + future.complete(response);
  447 + });
  448 +
  449 + // 2. 编码并发布消息
  450 + byte[] payload = CODEC.encode(request);
  451 + log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
  452 + CODEC.type(), topic, new String(payload));
  453 +
  454 + client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
  455 + .onComplete(ar -> {
  456 + if (ar.succeeded()) {
  457 + log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
  458 + } else {
  459 + log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
  460 + future.completeExceptionally(ar.cause());
  461 + }
  462 + });
  463 +
  464 + // 3. 等待响应(超时返回 null)
  465 + try {
  466 + return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  467 + } catch (Exception e) {
  468 + log.warn("[publishAndWaitReply][等待响应超时或失败]");
  469 + return null;
  470 + }
  471 + }
  472 +
  473 + /**
  474 + * 断开连接
  475 + *
  476 + * @param client MQTT 客户端
  477 + */
  478 + private void disconnect(MqttClient client) throws Exception {
  479 + // 1. 断开连接
  480 + CompletableFuture<Void> future = new CompletableFuture<>();
  481 + client.disconnect()
  482 + .onComplete(ar -> {
  483 + if (ar.succeeded()) {
  484 + log.info("[disconnect][断开连接成功]");
  485 + future.complete(null);
  486 + } else {
  487 + future.completeExceptionally(ar.cause());
  488 + }
  489 + });
  490 + // 2. 等待断开结果
  491 + future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  492 + }
  493 +
  494 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  7 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
  12 +import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
  13 +import io.netty.handler.codec.mqtt.MqttQoS;
  14 +import io.vertx.core.Vertx;
  15 +import io.vertx.core.buffer.Buffer;
  16 +import io.vertx.mqtt.MqttClient;
  17 +import io.vertx.mqtt.MqttClientOptions;
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.junit.jupiter.api.AfterAll;
  20 +import org.junit.jupiter.api.BeforeAll;
  21 +import org.junit.jupiter.api.Test;
  22 +
  23 +import java.util.concurrent.CompletableFuture;
  24 +import java.util.concurrent.CountDownLatch;
  25 +import java.util.concurrent.TimeUnit;
  26 +
  27 +/**
  28 + * IoT 网关子设备 MQTT 协议集成测试(手动测试)
  29 + *
  30 + * <p>测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
  31 + *
  32 + * <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
  33 + * <p>网关设备转发子设备请求时,使用子设备自己的认证信息连接。
  34 + *
  35 + * <p>使用步骤:
  36 + * <ol>
  37 + * <li>启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)</li>
  38 + * <li>确保子设备已通过 {@link IotGatewayDeviceMqttProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
  39 + * <li>运行以下测试方法:
  40 + * <ul>
  41 + * <li>{@link #testAuth()} - 子设备连接认证</li>
  42 + * <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
  43 + * <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
  44 + * </ul>
  45 + * </li>
  46 + * </ol>
  47 + *
  48 + * <p>注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
  49 + * 认证成功后同一连接上的后续请求无需再携带认证信息
  50 + *
  51 + * @author 芋道源码
  52 + */
  53 +@Slf4j
  54 +public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
  55 +
  56 + private static final String SERVER_HOST = "127.0.0.1";
  57 + private static final int SERVER_PORT = 1883;
  58 + private static final int TIMEOUT_SECONDS = 10;
  59 +
  60 + // ===================== 编解码器(MQTT 使用 Alink 协议) =====================
  61 + private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
  62 +
  63 + // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
  64 + private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
  65 + private static final String DEVICE_NAME = "chazuo-it";
  66 + private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
  67 +
  68 + // ===================== 全局共享 Vertx 实例 =====================
  69 + private static Vertx vertx;
  70 +
  71 + @BeforeAll
  72 + public static void setUp() {
  73 + vertx = Vertx.vertx();
  74 + }
  75 +
  76 + @AfterAll
  77 + public static void tearDown() {
  78 + if (vertx != null) {
  79 + vertx.close();
  80 + }
  81 + }
  82 +
  83 + // ===================== 连接认证测试 =====================
  84 +
  85 + /**
  86 + * 子设备认证测试:获取子设备 Token
  87 + */
  88 + @Test
  89 + public void testAuth() throws Exception {
  90 + CountDownLatch latch = new CountDownLatch(1);
  91 +
  92 + // 1. 构建认证信息
  93 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  94 + log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
  95 + authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
  96 +
  97 + // 2. 创建客户端并连接
  98 + MqttClient client = connect(authInfo);
  99 + client.connect(SERVER_PORT, SERVER_HOST)
  100 + .onComplete(ar -> {
  101 + if (ar.succeeded()) {
  102 + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
  103 + // 断开连接
  104 + client.disconnect()
  105 + .onComplete(disconnectAr -> {
  106 + if (disconnectAr.succeeded()) {
  107 + log.info("[testAuth][断开连接成功]");
  108 + } else {
  109 + log.error("[testAuth][断开连接失败]", disconnectAr.cause());
  110 + }
  111 + latch.countDown();
  112 + });
  113 + } else {
  114 + log.error("[testAuth][连接失败]", ar.cause());
  115 + latch.countDown();
  116 + }
  117 + });
  118 +
  119 + // 3. 等待测试完成
  120 + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  121 + if (!completed) {
  122 + log.warn("[testAuth][测试超时]");
  123 + }
  124 + }
  125 +
  126 + // ===================== 子设备属性上报测试 =====================
  127 +
  128 + /**
  129 + * 子设备属性上报测试
  130 + */
  131 + @Test
  132 + public void testPropertyPost() throws Exception {
  133 + // 1. 连接并认证
  134 + MqttClient client = connectAndAuth();
  135 + log.info("[testPropertyPost][连接认证成功]");
  136 + log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
  137 +
  138 + // 2. 订阅 _reply 主题
  139 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
  140 + subscribeReply(client, replyTopic);
  141 +
  142 + // 3. 构建属性上报消息
  143 + IotDeviceMessage request = IotDeviceMessage.of(
  144 + IdUtil.fastSimpleUUID(),
  145 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  146 + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
  147 + .put("power", 100)
  148 + .put("status", "online")
  149 + .put("temperature", 36.5)
  150 + .build()),
  151 + null, null, null);
  152 +
  153 + // 4. 发布消息并等待响应
  154 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
  155 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  156 + log.info("[testPropertyPost][响应消息: {}]", response);
  157 +
  158 + // 5. 断开连接
  159 + disconnect(client);
  160 + }
  161 +
  162 + // ===================== 子设备事件上报测试 =====================
  163 +
  164 + /**
  165 + * 子设备事件上报测试
  166 + */
  167 + @Test
  168 + public void testEventPost() throws Exception {
  169 + // 1. 连接并认证
  170 + MqttClient client = connectAndAuth();
  171 + log.info("[testEventPost][连接认证成功]");
  172 + log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
  173 +
  174 + // 2. 订阅 _reply 主题
  175 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
  176 + subscribeReply(client, replyTopic);
  177 +
  178 + // 3. 构建事件上报消息
  179 + IotDeviceMessage request = IotDeviceMessage.of(
  180 + IdUtil.fastSimpleUUID(),
  181 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  182 + IotDeviceEventPostReqDTO.of(
  183 + "alarm",
  184 + MapUtil.<String, Object>builder()
  185 + .put("level", "warning")
  186 + .put("message", "temperature too high")
  187 + .put("threshold", 40)
  188 + .put("current", 42)
  189 + .build(),
  190 + System.currentTimeMillis()),
  191 + null, null, null);
  192 +
  193 + // 4. 发布消息并等待响应
  194 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
  195 + IotDeviceMessage response = publishAndWaitReply(client, topic, request);
  196 + log.info("[testEventPost][响应消息: {}]", response);
  197 +
  198 + // 5. 断开连接
  199 + disconnect(client);
  200 + }
  201 +
  202 + // ===================== 辅助方法 =====================
  203 +
  204 + /**
  205 + * 创建 MQTT 客户端
  206 + *
  207 + * @param authInfo 认证信息
  208 + * @return MQTT 客户端
  209 + */
  210 + private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
  211 + MqttClientOptions options = new MqttClientOptions()
  212 + .setClientId(authInfo.getClientId())
  213 + .setUsername(authInfo.getUsername())
  214 + .setPassword(authInfo.getPassword())
  215 + .setCleanSession(true)
  216 + .setKeepAliveInterval(60);
  217 + return MqttClient.create(vertx, options);
  218 + }
  219 +
  220 + /**
  221 + * 连接并认证子设备
  222 + *
  223 + * @return 已认证的 MQTT 客户端
  224 + */
  225 + private MqttClient connectAndAuth() throws Exception {
  226 + // 1. 创建客户端并连接
  227 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  228 + MqttClient client = connect(authInfo);
  229 +
  230 + // 2.1 连接
  231 + CompletableFuture<MqttClient> future = new CompletableFuture<>();
  232 + client.connect(SERVER_PORT, SERVER_HOST)
  233 + .onComplete(ar -> {
  234 + if (ar.succeeded()) {
  235 + future.complete(client);
  236 + } else {
  237 + future.completeExceptionally(ar.cause());
  238 + }
  239 + });
  240 + // 2.2 等待连接结果
  241 + return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  242 + }
  243 +
  244 + /**
  245 + * 订阅响应主题
  246 + *
  247 + * @param client MQTT 客户端
  248 + * @param replyTopic 响应主题
  249 + */
  250 + private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
  251 + // 1. 订阅响应主题
  252 + CompletableFuture<Void> future = new CompletableFuture<>();
  253 + client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
  254 + .onComplete(ar -> {
  255 + if (ar.succeeded()) {
  256 + log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
  257 + future.complete(null);
  258 + } else {
  259 + future.completeExceptionally(ar.cause());
  260 + }
  261 + });
  262 + // 2. 等待订阅结果
  263 + future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  264 + }
  265 +
  266 + /**
  267 + * 发布消息并等待响应
  268 + *
  269 + * @param client MQTT 客户端
  270 + * @param topic 发布主题
  271 + * @param request 请求消息
  272 + * @return 响应消息
  273 + */
  274 + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
  275 + // 1. 设置消息处理器,接收响应
  276 + CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
  277 + client.publishHandler(message -> {
  278 + log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
  279 + message.topicName(), message.payload().toString());
  280 + IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
  281 + future.complete(response);
  282 + });
  283 +
  284 + // 2. 编码并发布消息
  285 + byte[] payload = CODEC.encode(request);
  286 + log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
  287 + CODEC.type(), topic, new String(payload));
  288 +
  289 + client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
  290 + .onComplete(ar -> {
  291 + if (ar.succeeded()) {
  292 + log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
  293 + } else {
  294 + log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
  295 + future.completeExceptionally(ar.cause());
  296 + }
  297 + });
  298 +
  299 + // 3. 等待响应(超时返回 null)
  300 + try {
  301 + return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  302 + } catch (Exception e) {
  303 + log.warn("[publishAndWaitReply][等待响应超时或失败]");
  304 + return null;
  305 + }
  306 + }
  307 +
  308 + /**
  309 + * 断开连接
  310 + *
  311 + * @param client MQTT 客户端
  312 + */
  313 + private void disconnect(MqttClient client) throws Exception {
  314 + // 1. 断开连接
  315 + CompletableFuture<Void> future = new CompletableFuture<>();
  316 + client.disconnect()
  317 + .onComplete(ar -> {
  318 + if (ar.succeeded()) {
  319 + log.info("[disconnect][断开连接成功]");
  320 + future.complete(null);
  321 + } else {
  322 + future.completeExceptionally(ar.cause());
  323 + }
  324 + });
  325 + // 2. 等待断开结果
  326 + future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  327 + }
  328 +
  329 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java
... ... @@ -68,7 +68,7 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
68 68 // ===================== 认证测试 =====================
69 69  
70 70 /**
71   - * 认证测试:设备认证
  71 + * 认证测试:获取设备 Token
72 72 */
73 73 @Test
74 74 public void testAuth() throws Exception {
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java
... ... @@ -81,7 +81,7 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
81 81 // ===================== 认证测试 =====================
82 82  
83 83 /**
84   - * 网关设备认证测试
  84 + * 网关设备认证测试:获取网关设备 Token
85 85 */
86 86 @Test
87 87 public void testAuth() throws Exception {
... ... @@ -341,6 +341,9 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
341 341  
342 342 /**
343 343 * 执行网关设备认证
  344 + *
  345 + * @param socket TCP 连接
  346 + * @return 认证响应消息
344 347 */
345 348 private IotDeviceMessage authenticate(Socket socket) throws Exception {
346 349 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
... ... @@ -360,6 +363,10 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
360 363  
361 364 /**
362 365 * 发送 TCP 请求并接收响应
  366 + *
  367 + * @param socket TCP Socket
  368 + * @param payload 请求数据
  369 + * @return 响应数据
363 370 */
364 371 private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
365 372 // 1. 发送请求
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java
... ... @@ -69,7 +69,7 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
69 69 // ===================== 认证测试 =====================
70 70  
71 71 /**
72   - * 子设备认证测试
  72 + * 子设备认证测试:获取子设备 Token
73 73 */
74 74 @Test
75 75 public void testAuth() throws Exception {
... ... @@ -191,6 +191,9 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
191 191  
192 192 /**
193 193 * 执行子设备认证
  194 + *
  195 + * @param socket TCP 连接
  196 + * @return 认证响应消息
194 197 */
195 198 private IotDeviceMessage authenticate(Socket socket) throws Exception {
196 199 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
... ... @@ -209,6 +212,10 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
209 212  
210 213 /**
211 214 * 发送 TCP 请求并接收响应
  215 + *
  216 + * @param socket TCP Socket
  217 + * @param payload 请求数据
  218 + * @return 响应数据
212 219 */
213 220 private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
214 221 // 1. 发送请求
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
... ... @@ -6,6 +6,7 @@ import cn.hutool.core.util.IdUtil;
6 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
9 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
... ... @@ -106,6 +107,46 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
106 107 }
107 108 }
108 109  
  110 + // ===================== 动态注册测试 =====================
  111 +
  112 + /**
  113 + * 直连设备动态注册测试(一型一密)
  114 + * <p>
  115 + * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
  116 + * <p>
  117 + * 注意:此接口不需要认证
  118 + */
  119 + @Test
  120 + public void testDeviceRegister() throws Exception {
  121 + // 1.1 构建注册消息
  122 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
  123 + registerReqDTO.setProductKey(PRODUCT_KEY);
  124 + registerReqDTO.setDeviceName("test-udp-" + System.currentTimeMillis());
  125 + registerReqDTO.setProductSecret("test-product-secret");
  126 + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
  127 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
  128 + // 1.2 编码
  129 + byte[] payload = CODEC.encode(request);
  130 + log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
  131 + if (CODEC instanceof IotTcpBinaryDeviceMessageCodec) {
  132 + log.info("[testDeviceRegister][二进制数据包(HEX): {}]", HexUtil.encodeHexStr(payload));
  133 + }
  134 +
  135 + // 2.1 发送请求
  136 + try (DatagramSocket socket = new DatagramSocket()) {
  137 + socket.setSoTimeout(TIMEOUT_MS);
  138 + byte[] responseBytes = sendAndReceive(socket, payload);
  139 + // 2.2 解码响应
  140 + if (responseBytes != null) {
  141 + IotDeviceMessage response = CODEC.decode(responseBytes);
  142 + log.info("[testDeviceRegister][响应消息: {}]", response);
  143 + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
  144 + } else {
  145 + log.warn("[testDeviceRegister][未收到响应]");
  146 + }
  147 + }
  148 + }
  149 +
109 150 // ===================== 直连设备属性上报测试 =====================
110 151  
111 152 /**
... ...