diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java index 76c853c..10bcca7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java +++ b/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 { * 网关设备认证测试:获取网关设备 Token */ @Test - @SuppressWarnings("deprecation") public void testAuth() throws Exception { // 1.1 构建请求 String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java index f6d4740..c10bb77 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java +++ b/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 { * 子设备认证测试:获取子设备 Token */ @Test - @SuppressWarnings("deprecation") public void testAuth() throws Exception { // 1.1 构建请求 String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index d1a3b4d..640572e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/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 { // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - log.info("[testConnect][认证信息: clientId={}, username={}, password={}]", + log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 @@ -97,19 +97,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { client.connect(SERVER_PORT, SERVER_HOST) .onComplete(ar -> { if (ar.succeeded()) { - log.info("[testConnect][连接成功,客户端 ID: {}]", client.clientId()); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); // 断开连接 client.disconnect() .onComplete(disconnectAr -> { if (disconnectAr.succeeded()) { - log.info("[testConnect][断开连接成功]"); + log.info("[testAuth][断开连接成功]"); } else { - log.error("[testConnect][断开连接失败]", disconnectAr.cause()); + log.error("[testAuth][断开连接失败]", disconnectAr.cause()); } latch.countDown(); }); } else { - log.error("[testConnect][连接失败]", ar.cause()); + log.error("[testAuth][连接失败]", ar.cause()); latch.countDown(); } }); @@ -117,7 +117,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { // 3. 等待测试完成 boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); if (!completed) { - log.warn("[testConnect][测试超时]"); + log.warn("[testAuth][测试超时]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java new file mode 100644 index 0000000..57ed6a7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java @@ -0,0 +1,494 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * IoT 网关设备 MQTT 协议集成测试(手动测试) + * + *
测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 MQTT 协议管理子设备拓扑关系 + * + *
使用步骤: + *
注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成, + * 认证成功后同一连接上的后续请求无需再携带认证信息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotGatewayDeviceMqttProtocolIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 1883; + private static final int TIMEOUT_SECONDS = 10; + + // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + + // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== + private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; + private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; + private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; + + // ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== + private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; + private static final String SUB_DEVICE_NAME = "chazuo-it"; + private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + + // ===================== 全局共享 Vertx 实例 ===================== + private static Vertx vertx; + + @BeforeAll + public static void setUp() { + vertx = Vertx.vertx(); + } + + @AfterAll + public static void tearDown() { + if (vertx != null) { + vertx.close(); + } + } + + // ===================== 连接认证测试 ===================== + + /** + * 网关设备认证测试:获取网关设备 Token + */ + @Test + public void testAuth() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + // 1. 构建认证信息 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); + log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", + authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); + + // 2. 创建客户端并连接 + MqttClient client = connect(authInfo); + client.connect(SERVER_PORT, SERVER_HOST) + .onComplete(ar -> { + if (ar.succeeded()) { + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + // 断开连接 + client.disconnect() + .onComplete(disconnectAr -> { + if (disconnectAr.succeeded()) { + log.info("[testAuth][断开连接成功]"); + } else { + log.error("[testAuth][断开连接失败]", disconnectAr.cause()); + } + latch.countDown(); + }); + } else { + log.error("[testAuth][连接失败]", ar.cause()); + latch.countDown(); + } + }); + + // 3. 等待测试完成 + boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!completed) { + log.warn("[testAuth][测试超时]"); + } + } + + // ===================== 拓扑管理测试 ===================== + + /** + * 添加子设备拓扑关系测试 + *
+ * 网关设备向平台上报需要绑定的子设备信息 + */ + @Test + public void testTopoAdd() throws Exception { + // 1. 连接并认证 + MqttClient client = connectAndAuth(); + log.info("[testTopoAdd][连接认证成功]"); + + // 2.1 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribeReply(client, replyTopic); + + // 2.2 构建子设备认证信息 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() + .setClientId(subAuthInfo.getClientId()) + .setUsername(subAuthInfo.getUsername()) + .setPassword(subAuthInfo.getPassword()); + + // 2.3 构建请求消息 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); + params.setSubDevices(Collections.singletonList(subDeviceAuth)); + IotDeviceMessage request = IotDeviceMessage.of( + IdUtil.fastSimpleUUID(), + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), + params, + null, null, null); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/add", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoAdd][响应消息: {}]", response); + + // 4. 断开连接 + disconnect(client); + } + + /** + * 删除子设备拓扑关系测试 + *
+ * 网关设备向平台上报需要解绑的子设备信息 + */ + @Test + public void testTopoDelete() throws Exception { + // 1. 连接并认证 + MqttClient client = connectAndAuth(); + log.info("[testTopoDelete][连接认证成功]"); + + // 2.1 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribeReply(client, replyTopic); + + // 2.2 构建请求消息 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); + params.setSubDevices(Collections.singletonList( + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); + IotDeviceMessage request = IotDeviceMessage.of( + IdUtil.fastSimpleUUID(), + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), + params, + null, null, null); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/delete", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoDelete][响应消息: {}]", response); + + // 4. 断开连接 + disconnect(client); + } + + /** + * 获取子设备拓扑关系测试 + *
+ * 网关设备向平台查询已绑定的子设备列表 + */ + @Test + public void testTopoGet() throws Exception { + // 1. 连接并认证 + MqttClient client = connectAndAuth(); + log.info("[testTopoGet][连接认证成功]"); + + // 2.1 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribeReply(client, replyTopic); + + // 2.2 构建请求消息 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); + IotDeviceMessage request = IotDeviceMessage.of( + IdUtil.fastSimpleUUID(), + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), + params, + null, null, null); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/get", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoGet][响应消息: {}]", response); + + // 4. 断开连接 + disconnect(client); + } + + // ===================== 子设备注册测试 ===================== + + /** + * 子设备动态注册测试 + *
+ * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret + *
+ * 注意:此接口需要网关认证 + */ + @Test + public void testSubDeviceRegister() throws Exception { + // 1. 连接并认证 + MqttClient client = connectAndAuth(); + log.info("[testSubDeviceRegister][连接认证成功]"); + + // 2.1 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribeReply(client, replyTopic); + + // 2.2 构建请求消息 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); + subDevice.setDeviceName("mougezishebei-mqtt"); + IotDeviceMessage request = IotDeviceMessage.of( + IdUtil.fastSimpleUUID(), + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), + Collections.singletonList(subDevice), + null, null, null); + + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); + + // 4. 断开连接 + disconnect(client); + } + + // ===================== 批量上报测试 ===================== + + /** + * 批量上报属性测试(网关 + 子设备) + *
+ * 网关设备批量上报自身属性、事件,以及子设备的属性、事件
+ */
+ @Test
+ public void testPropertyPackPost() throws Exception {
+ // 1. 连接并认证
+ MqttClient client = connectAndAuth();
+ log.info("[testPropertyPackPost][连接认证成功]");
+
+ // 2.1 订阅 _reply 主题
+ String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
+ GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
+ subscribeReply(client, replyTopic);
+
+ // 2.2 构建【网关设备】自身属性
+ Map 测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
+ *
+ * 重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。
+ * 网关设备转发子设备请求时,使用子设备自己的认证信息连接。
+ *
+ * 使用步骤:
+ * 注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
+ * 认证成功后同一连接上的后续请求无需再携带认证信息
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
+
+ private static final String SERVER_HOST = "127.0.0.1";
+ private static final int SERVER_PORT = 1883;
+ private static final int TIMEOUT_SECONDS = 10;
+
+ // ===================== 编解码器(MQTT 使用 Alink 协议) =====================
+ private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
+
+ // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
+ private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
+ private static final String DEVICE_NAME = "chazuo-it";
+ private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
+
+ // ===================== 全局共享 Vertx 实例 =====================
+ private static Vertx vertx;
+
+ @BeforeAll
+ public static void setUp() {
+ vertx = Vertx.vertx();
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ if (vertx != null) {
+ vertx.close();
+ }
+ }
+
+ // ===================== 连接认证测试 =====================
+
+ /**
+ * 子设备认证测试:获取子设备 Token
+ */
+ @Test
+ public void testAuth() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ // 1. 构建认证信息
+ IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
+ log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
+ authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
+
+ // 2. 创建客户端并连接
+ MqttClient client = connect(authInfo);
+ client.connect(SERVER_PORT, SERVER_HOST)
+ .onComplete(ar -> {
+ if (ar.succeeded()) {
+ log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
+ // 断开连接
+ client.disconnect()
+ .onComplete(disconnectAr -> {
+ if (disconnectAr.succeeded()) {
+ log.info("[testAuth][断开连接成功]");
+ } else {
+ log.error("[testAuth][断开连接失败]", disconnectAr.cause());
+ }
+ latch.countDown();
+ });
+ } else {
+ log.error("[testAuth][连接失败]", ar.cause());
+ latch.countDown();
+ }
+ });
+
+ // 3. 等待测试完成
+ boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (!completed) {
+ log.warn("[testAuth][测试超时]");
+ }
+ }
+
+ // ===================== 子设备属性上报测试 =====================
+
+ /**
+ * 子设备属性上报测试
+ */
+ @Test
+ public void testPropertyPost() throws Exception {
+ // 1. 连接并认证
+ MqttClient client = connectAndAuth();
+ log.info("[testPropertyPost][连接认证成功]");
+ log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
+
+ // 2. 订阅 _reply 主题
+ String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
+ subscribeReply(client, replyTopic);
+
+ // 3. 构建属性上报消息
+ IotDeviceMessage request = IotDeviceMessage.of(
+ IdUtil.fastSimpleUUID(),
+ IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
+ IotDevicePropertyPostReqDTO.of(MapUtil.
+ * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
+ *
+ * 注意:此接口不需要认证
+ */
+ @Test
+ public void testDeviceRegister() throws Exception {
+ // 1.1 构建注册消息
+ IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
+ registerReqDTO.setProductKey(PRODUCT_KEY);
+ registerReqDTO.setDeviceName("test-udp-" + System.currentTimeMillis());
+ registerReqDTO.setProductSecret("test-product-secret");
+ IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
+ IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
+ // 1.2 编码
+ byte[] payload = CODEC.encode(request);
+ log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
+ if (CODEC instanceof IotTcpBinaryDeviceMessageCodec) {
+ log.info("[testDeviceRegister][二进制数据包(HEX): {}]", HexUtil.encodeHexStr(payload));
+ }
+
+ // 2.1 发送请求
+ try (DatagramSocket socket = new DatagramSocket()) {
+ socket.setSoTimeout(TIMEOUT_MS);
+ byte[] responseBytes = sendAndReceive(socket, payload);
+ // 2.2 解码响应
+ if (responseBytes != null) {
+ IotDeviceMessage response = CODEC.decode(responseBytes);
+ log.info("[testDeviceRegister][响应消息: {}]", response);
+ log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
+ } else {
+ log.warn("[testDeviceRegister][未收到响应]");
+ }
+ }
+ }
+
// ===================== 直连设备属性上报测试 =====================
/**
+ *
+ *
+ *
+ *
+ *