Commit 99bcd252a336a9e349b44b61dc2489f3820f4344
1 parent
70135174
feat:【iot】MQTT 协议:1)增加 register 接口
feat:【iot】TCP/UDP 协议:统一 register 返回数据的格式
Showing
4 changed files
with
478 additions
and
240 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.map.MapUtil; | ||
| 4 | +import cn.hutool.core.util.ArrayUtil; | ||
| 3 | import cn.hutool.core.util.BooleanUtil; | 5 | import cn.hutool.core.util.BooleanUtil; |
| 4 | import cn.hutool.core.util.StrUtil; | 6 | import cn.hutool.core.util.StrUtil; |
| 5 | import cn.hutool.extra.spring.SpringUtil; | 7 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 8 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 9 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 7 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 10 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 11 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 9 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; | 12 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 13 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 14 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 11 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 15 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 12 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 16 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 17 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; | ||
| 18 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; | ||
| 13 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 19 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; | 20 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; |
| 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 21 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 16 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 22 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 23 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | ||
| 17 | import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | 24 | import io.netty.handler.codec.mqtt.MqttConnectReturnCode; |
| 18 | import io.netty.handler.codec.mqtt.MqttQoS; | 25 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 19 | import io.vertx.mqtt.MqttEndpoint; | 26 | import io.vertx.mqtt.MqttEndpoint; |
| @@ -21,6 +28,7 @@ import io.vertx.mqtt.MqttTopicSubscription; | @@ -21,6 +28,7 @@ import io.vertx.mqtt.MqttTopicSubscription; | ||
| 21 | import lombok.extern.slf4j.Slf4j; | 28 | import lombok.extern.slf4j.Slf4j; |
| 22 | 29 | ||
| 23 | import java.util.List; | 30 | import java.util.List; |
| 31 | +import java.util.Map; | ||
| 24 | 32 | ||
| 25 | /** | 33 | /** |
| 26 | * MQTT 上行消息处理器 | 34 | * MQTT 上行消息处理器 |
| @@ -30,6 +38,16 @@ import java.util.List; | @@ -30,6 +38,16 @@ import java.util.List; | ||
| 30 | @Slf4j | 38 | @Slf4j |
| 31 | public class IotMqttUpstreamHandler { | 39 | public class IotMqttUpstreamHandler { |
| 32 | 40 | ||
| 41 | + /** | ||
| 42 | + * 默认编解码类型(MQTT 使用 Alink 协议) | ||
| 43 | + */ | ||
| 44 | + private static final String DEFAULT_CODEC_TYPE = "Alink"; | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * register 请求的 topic 后缀 | ||
| 48 | + */ | ||
| 49 | + private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; | ||
| 50 | + | ||
| 33 | private final IotDeviceMessageService deviceMessageService; | 51 | private final IotDeviceMessageService deviceMessageService; |
| 34 | 52 | ||
| 35 | private final IotMqttConnectionManager connectionManager; | 53 | private final IotMqttConnectionManager connectionManager; |
| @@ -85,20 +103,28 @@ public class IotMqttUpstreamHandler { | @@ -85,20 +103,28 @@ public class IotMqttUpstreamHandler { | ||
| 85 | }); | 103 | }); |
| 86 | 104 | ||
| 87 | // 4. 设置消息处理器 | 105 | // 4. 设置消息处理器 |
| 88 | - endpoint.publishHandler(message -> { | 106 | + endpoint.publishHandler(mqttMessage -> { |
| 89 | try { | 107 | try { |
| 90 | - processMessage(clientId, message.topicName(), message.payload().getBytes()); | 108 | + // 4.1 根据 topic 判断是否为 register 请求 |
| 109 | + String topic = mqttMessage.topicName(); | ||
| 110 | + byte[] payload = mqttMessage.payload().getBytes(); | ||
| 111 | + if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) { | ||
| 112 | + // register 请求:使用默认编解码器处理(设备可能未注册) | ||
| 113 | + processRegisterMessage(clientId, topic, payload, endpoint); | ||
| 114 | + } else { | ||
| 115 | + // 业务请求:正常处理 | ||
| 116 | + processMessage(clientId, topic, payload); | ||
| 117 | + } | ||
| 91 | 118 | ||
| 92 | - // 根据 QoS 级别发送相应的确认消息 | ||
| 93 | - if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | 119 | + // 4.2 根据 QoS 级别发送相应的确认消息 |
| 120 | + if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) { | ||
| 94 | // QoS 1: 发送 PUBACK 确认 | 121 | // QoS 1: 发送 PUBACK 确认 |
| 95 | - endpoint.publishAcknowledge(message.messageId()); | ||
| 96 | - } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { | 122 | + endpoint.publishAcknowledge(mqttMessage.messageId()); |
| 123 | + } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) { | ||
| 97 | // QoS 2: 发送 PUBREC 确认 | 124 | // QoS 2: 发送 PUBREC 确认 |
| 98 | - endpoint.publishReceived(message.messageId()); | 125 | + endpoint.publishReceived(mqttMessage.messageId()); |
| 99 | } | 126 | } |
| 100 | // QoS 0 无需确认 | 127 | // QoS 0 无需确认 |
| 101 | - | ||
| 102 | } catch (Exception e) { | 128 | } catch (Exception e) { |
| 103 | log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", | 129 | log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", |
| 104 | clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); | 130 | clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); |
| @@ -161,10 +187,9 @@ public class IotMqttUpstreamHandler { | @@ -161,10 +187,9 @@ public class IotMqttUpstreamHandler { | ||
| 161 | return; | 187 | return; |
| 162 | } | 188 | } |
| 163 | 189 | ||
| 190 | + // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) | ||
| 164 | String productKey = topicParts[2]; | 191 | String productKey = topicParts[2]; |
| 165 | String deviceName = topicParts[3]; | 192 | String deviceName = topicParts[3]; |
| 166 | - | ||
| 167 | - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) | ||
| 168 | try { | 193 | try { |
| 169 | IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); | 194 | IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); |
| 170 | if (message == null) { | 195 | if (message == null) { |
| @@ -172,10 +197,9 @@ public class IotMqttUpstreamHandler { | @@ -172,10 +197,9 @@ public class IotMqttUpstreamHandler { | ||
| 172 | return; | 197 | return; |
| 173 | } | 198 | } |
| 174 | 199 | ||
| 200 | + // 4. 处理业务消息(认证已在连接时完成) | ||
| 175 | log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", | 201 | log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", |
| 176 | productKey, deviceName, message.getMethod()); | 202 | productKey, deviceName, message.getMethod()); |
| 177 | - | ||
| 178 | - // 4. 处理业务消息(认证已在连接时完成) | ||
| 179 | handleBusinessRequest(message, productKey, deviceName); | 203 | handleBusinessRequest(message, productKey, deviceName); |
| 180 | } catch (Exception e) { | 204 | } catch (Exception e) { |
| 181 | log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | 205 | log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", |
| @@ -247,6 +271,195 @@ public class IotMqttUpstreamHandler { | @@ -247,6 +271,195 @@ public class IotMqttUpstreamHandler { | ||
| 247 | } | 271 | } |
| 248 | 272 | ||
| 249 | /** | 273 | /** |
| 274 | + * 处理 register 消息(设备动态注册,使用默认编解码器) | ||
| 275 | + * | ||
| 276 | + * @param clientId 客户端 ID | ||
| 277 | + * @param topic 主题 | ||
| 278 | + * @param payload 消息内容 | ||
| 279 | + * @param endpoint MQTT 连接端点 | ||
| 280 | + */ | ||
| 281 | + private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) { | ||
| 282 | + // 1.1 基础检查 | ||
| 283 | + if (ArrayUtil.isEmpty(payload)) { | ||
| 284 | + return; | ||
| 285 | + } | ||
| 286 | + // 1.2 解析主题,获取 productKey 和 deviceName | ||
| 287 | + String[] topicParts = topic.split("/"); | ||
| 288 | + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | ||
| 289 | + log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic); | ||
| 290 | + return; | ||
| 291 | + } | ||
| 292 | + String productKey = topicParts[2]; | ||
| 293 | + String deviceName = topicParts[3]; | ||
| 294 | + | ||
| 295 | + // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) | ||
| 296 | + IotDeviceMessage message; | ||
| 297 | + try { | ||
| 298 | + message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); | ||
| 299 | + if (message == null) { | ||
| 300 | + log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | ||
| 301 | + return; | ||
| 302 | + } | ||
| 303 | + } catch (Exception e) { | ||
| 304 | + log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]", | ||
| 305 | + clientId, topic, e.getMessage(), e); | ||
| 306 | + return; | ||
| 307 | + } | ||
| 308 | + | ||
| 309 | + // 3. 处理设备动态注册请求 | ||
| 310 | + log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]", | ||
| 311 | + productKey, deviceName, message.getMethod()); | ||
| 312 | + try { | ||
| 313 | + handleRegisterRequest(message, productKey, deviceName, endpoint); | ||
| 314 | + } catch (Exception e) { | ||
| 315 | + log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | ||
| 316 | + clientId, topic, e.getMessage(), e); | ||
| 317 | + } | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + /** | ||
| 321 | + * 处理设备动态注册请求(一型一密,不需要 deviceSecret) | ||
| 322 | + * | ||
| 323 | + * @param message 消息信息 | ||
| 324 | + * @param productKey 产品 Key | ||
| 325 | + * @param deviceName 设备名称 | ||
| 326 | + * @param endpoint MQTT 连接端点 | ||
| 327 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a> | ||
| 328 | + */ | ||
| 329 | + private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) { | ||
| 330 | + String clientId = endpoint.clientIdentifier(); | ||
| 331 | + try { | ||
| 332 | + // 1. 解析注册参数 | ||
| 333 | + IotDeviceRegisterReqDTO registerParams = parseRegisterParams(message.getParams()); | ||
| 334 | + if (registerParams == null) { | ||
| 335 | + log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); | ||
| 336 | + sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整"); | ||
| 337 | + return; | ||
| 338 | + } | ||
| 339 | + | ||
| 340 | + // 2. 调用动态注册 API | ||
| 341 | + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(registerParams); | ||
| 342 | + if (result.isError()) { | ||
| 343 | + log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); | ||
| 344 | + sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg()); | ||
| 345 | + return; | ||
| 346 | + } | ||
| 347 | + | ||
| 348 | + // 3. 发送成功响应(包含 deviceSecret) | ||
| 349 | + sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData()); | ||
| 350 | + log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", | ||
| 351 | + registerParams.getDeviceName(), clientId); | ||
| 352 | + } catch (Exception e) { | ||
| 353 | + log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); | ||
| 354 | + sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常"); | ||
| 355 | + } | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + /** | ||
| 359 | + * 解析注册参数 | ||
| 360 | + * | ||
| 361 | + * @param params 参数对象(通常为 Map 类型) | ||
| 362 | + * @return 注册参数 DTO,解析失败时返回 null | ||
| 363 | + */ | ||
| 364 | + @SuppressWarnings("unchecked") | ||
| 365 | + private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { | ||
| 366 | + if (params == null) { | ||
| 367 | + return null; | ||
| 368 | + } | ||
| 369 | + | ||
| 370 | + try { | ||
| 371 | + // 参数默认为 Map 类型,直接转换 | ||
| 372 | + if (params instanceof Map) { | ||
| 373 | + Map<String, Object> paramMap = (Map<String, Object>) params; | ||
| 374 | + String productKey = MapUtil.getStr(paramMap, "productKey"); | ||
| 375 | + String deviceName = MapUtil.getStr(paramMap, "deviceName"); | ||
| 376 | + String productSecret = MapUtil.getStr(paramMap, "productSecret"); | ||
| 377 | + if (StrUtil.hasBlank(productKey, deviceName, productSecret)) { | ||
| 378 | + return null; | ||
| 379 | + } | ||
| 380 | + return new IotDeviceRegisterReqDTO() | ||
| 381 | + .setProductKey(productKey) | ||
| 382 | + .setDeviceName(deviceName) | ||
| 383 | + .setProductSecret(productSecret); | ||
| 384 | + } | ||
| 385 | + | ||
| 386 | + // 如果已经是目标类型,直接返回 | ||
| 387 | + if (params instanceof IotDeviceRegisterReqDTO) { | ||
| 388 | + return (IotDeviceRegisterReqDTO) params; | ||
| 389 | + } | ||
| 390 | + | ||
| 391 | + // 其他情况尝试 JSON 转换 | ||
| 392 | + String jsonStr = JsonUtils.toJsonString(params); | ||
| 393 | + return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class); | ||
| 394 | + } catch (Exception e) { | ||
| 395 | + log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); | ||
| 396 | + return null; | ||
| 397 | + } | ||
| 398 | + } | ||
| 399 | + | ||
| 400 | + /** | ||
| 401 | + * 发送注册成功响应(包含 deviceSecret) | ||
| 402 | + * | ||
| 403 | + * @param endpoint MQTT 连接端点 | ||
| 404 | + * @param productKey 产品 Key | ||
| 405 | + * @param deviceName 设备名称 | ||
| 406 | + * @param requestId 请求 ID | ||
| 407 | + * @param registerResp 注册响应 | ||
| 408 | + */ | ||
| 409 | + private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, | ||
| 410 | + String requestId, IotDeviceRegisterRespDTO registerResp) { | ||
| 411 | + try { | ||
| 412 | + // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) | ||
| 413 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | ||
| 414 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); | ||
| 415 | + | ||
| 416 | + // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | ||
| 417 | + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | ||
| 418 | + | ||
| 419 | + // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | ||
| 420 | + String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | ||
| 421 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | ||
| 422 | + endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | ||
| 423 | + MqttQoS.AT_LEAST_ONCE, false, false); | ||
| 424 | + log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic); | ||
| 425 | + } catch (Exception e) { | ||
| 426 | + log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]", | ||
| 427 | + endpoint.clientIdentifier(), e); | ||
| 428 | + } | ||
| 429 | + } | ||
| 430 | + | ||
| 431 | + /** | ||
| 432 | + * 发送注册错误响应 | ||
| 433 | + * | ||
| 434 | + * @param endpoint MQTT 连接端点 | ||
| 435 | + * @param productKey 产品 Key | ||
| 436 | + * @param deviceName 设备名称 | ||
| 437 | + * @param requestId 请求 ID | ||
| 438 | + * @param errorMessage 错误消息 | ||
| 439 | + */ | ||
| 440 | + private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, | ||
| 441 | + String requestId, String errorMessage) { | ||
| 442 | + try { | ||
| 443 | + // 1. 构建响应消息 | ||
| 444 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | ||
| 445 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage); | ||
| 446 | + | ||
| 447 | + // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) | ||
| 448 | + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | ||
| 449 | + | ||
| 450 | + // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) | ||
| 451 | + String replyTopic = IotMqttTopicUtils.buildTopicByMethod( | ||
| 452 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); | ||
| 453 | + endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), | ||
| 454 | + MqttQoS.AT_LEAST_ONCE, false, false); | ||
| 455 | + log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic); | ||
| 456 | + } catch (Exception e) { | ||
| 457 | + log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]", | ||
| 458 | + endpoint.clientIdentifier(), e); | ||
| 459 | + } | ||
| 460 | + } | ||
| 461 | + | ||
| 462 | + /** | ||
| 250 | * 处理业务请求 | 463 | * 处理业务请求 |
| 251 | */ | 464 | */ |
| 252 | private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { | 465 | private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
| @@ -502,15 +502,10 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | @@ -502,15 +502,10 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 502 | private void sendRegisterSuccessResponse(NetSocket socket, String requestId, | 502 | private void sendRegisterSuccessResponse(NetSocket socket, String requestId, |
| 503 | IotDeviceRegisterRespDTO registerResp, String codecType) { | 503 | IotDeviceRegisterRespDTO registerResp, String codecType) { |
| 504 | try { | 504 | try { |
| 505 | - // 构建响应数据 | ||
| 506 | - Object responseData = MapUtil.builder() | ||
| 507 | - .put("success", true) | ||
| 508 | - .put("deviceSecret", registerResp.getDeviceSecret()) | ||
| 509 | - .put("message", "注册成功") | ||
| 510 | - .build(); | 505 | + // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) |
| 511 | IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | 506 | IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, |
| 512 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), responseData, 0, "注册成功"); | ||
| 513 | - | 507 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); |
| 508 | + // 2. 发送响应 | ||
| 514 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | 509 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); |
| 515 | socket.write(Buffer.buffer(encodedData)); | 510 | socket.write(Buffer.buffer(encodedData)); |
| 516 | } catch (Exception e) { | 511 | } catch (Exception e) { |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java
| @@ -405,15 +405,10 @@ public class IotUdpUpstreamHandler { | @@ -405,15 +405,10 @@ public class IotUdpUpstreamHandler { | ||
| 405 | String requestId, IotDeviceRegisterRespDTO registerResp, | 405 | String requestId, IotDeviceRegisterRespDTO registerResp, |
| 406 | String codecType) { | 406 | String codecType) { |
| 407 | try { | 407 | try { |
| 408 | - // 构建响应数据 | ||
| 409 | - Object responseData = MapUtil.builder() | ||
| 410 | - .put("success", true) | ||
| 411 | - .put("deviceSecret", registerResp.getDeviceSecret()) | ||
| 412 | - .put("message", "注册成功") | ||
| 413 | - .build(); | 408 | + // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) |
| 414 | IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, | 409 | IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, |
| 415 | - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), responseData, 0, "注册成功"); | ||
| 416 | - // 发送响应 | 410 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); |
| 411 | + // 2. 发送响应 | ||
| 417 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); | 412 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); |
| 418 | socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { | 413 | socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { |
| 419 | if (result.failed()) { | 414 | if (result.failed()) { |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
| @@ -2,12 +2,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | @@ -2,12 +2,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.map.MapUtil; | 3 | import cn.hutool.core.map.MapUtil; |
| 4 | import cn.hutool.core.util.IdUtil; | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 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.auth.IotDeviceRegisterReqDTO; | ||
| 8 | import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; | 9 | 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.topic.property.IotDevicePropertyPostReqDTO; |
| 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 13 | +import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; | ||
| 11 | import io.netty.handler.codec.mqtt.MqttQoS; | 14 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 12 | import io.vertx.core.Vertx; | 15 | import io.vertx.core.Vertx; |
| 13 | import io.vertx.core.buffer.Buffer; | 16 | import io.vertx.core.buffer.Buffer; |
| @@ -18,6 +21,7 @@ import org.junit.jupiter.api.AfterAll; | @@ -18,6 +21,7 @@ import org.junit.jupiter.api.AfterAll; | ||
| 18 | import org.junit.jupiter.api.BeforeAll; | 21 | import org.junit.jupiter.api.BeforeAll; |
| 19 | import org.junit.jupiter.api.Test; | 22 | import org.junit.jupiter.api.Test; |
| 20 | 23 | ||
| 24 | +import java.util.concurrent.CompletableFuture; | ||
| 21 | import java.util.concurrent.CountDownLatch; | 25 | import java.util.concurrent.CountDownLatch; |
| 22 | import java.util.concurrent.TimeUnit; | 26 | import java.util.concurrent.TimeUnit; |
| 23 | 27 | ||
| @@ -31,9 +35,10 @@ import java.util.concurrent.TimeUnit; | @@ -31,9 +35,10 @@ import java.util.concurrent.TimeUnit; | ||
| 31 | * <li>启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)</li> | 35 | * <li>启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)</li> |
| 32 | * <li>运行以下测试方法: | 36 | * <li>运行以下测试方法: |
| 33 | * <ul> | 37 | * <ul> |
| 34 | - * <li>{@link #testConnect()} - 设备连接认证</li> | 38 | + * <li>{@link #testAuth()} - 设备连接认证</li> |
| 35 | * <li>{@link #testPropertyPost()} - 设备属性上报</li> | 39 | * <li>{@link #testPropertyPost()} - 设备属性上报</li> |
| 36 | * <li>{@link #testEventPost()} - 设备事件上报</li> | 40 | * <li>{@link #testEventPost()} - 设备事件上报</li> |
| 41 | + * <li>{@link #testSubscribe()} - 订阅下行消息</li> | ||
| 37 | * </ul> | 42 | * </ul> |
| 38 | * </li> | 43 | * </li> |
| 39 | * </ol> | 44 | * </ol> |
| @@ -50,6 +55,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -50,6 +55,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 50 | private static final int SERVER_PORT = 1883; | 55 | private static final int SERVER_PORT = 1883; |
| 51 | private static final int TIMEOUT_SECONDS = 10; | 56 | private static final int TIMEOUT_SECONDS = 10; |
| 52 | 57 | ||
| 58 | + // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== | ||
| 59 | + private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); | ||
| 60 | + | ||
| 53 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== | 61 | // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== |
| 54 | private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; | 62 | private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; |
| 55 | private static final String DEVICE_NAME = "small"; | 63 | private static final String DEVICE_NAME = "small"; |
| @@ -73,10 +81,10 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -73,10 +81,10 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 73 | // ===================== 连接认证测试 ===================== | 81 | // ===================== 连接认证测试 ===================== |
| 74 | 82 | ||
| 75 | /** | 83 | /** |
| 76 | - * 连接认证测试:设备通过 MQTT 协议连接平台 | 84 | + * 认证测试:获取设备 Token |
| 77 | */ | 85 | */ |
| 78 | @Test | 86 | @Test |
| 79 | - public void testConnect() throws Exception { | 87 | + public void testAuth() throws Exception { |
| 80 | CountDownLatch latch = new CountDownLatch(1); | 88 | CountDownLatch latch = new CountDownLatch(1); |
| 81 | 89 | ||
| 82 | // 1. 构建认证信息 | 90 | // 1. 构建认证信息 |
| @@ -84,16 +92,8 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -84,16 +92,8 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 84 | log.info("[testConnect][认证信息: clientId={}, username={}, password={}]", | 92 | log.info("[testConnect][认证信息: clientId={}, username={}, password={}]", |
| 85 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); | 93 | authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); |
| 86 | 94 | ||
| 87 | - // 2. 创建 MQTT 客户端配置 | ||
| 88 | - MqttClientOptions options = new MqttClientOptions() | ||
| 89 | - .setClientId(authInfo.getClientId()) | ||
| 90 | - .setUsername(authInfo.getUsername()) | ||
| 91 | - .setPassword(authInfo.getPassword()) | ||
| 92 | - .setCleanSession(true) | ||
| 93 | - .setKeepAliveInterval(60); | ||
| 94 | - | ||
| 95 | - // 3. 创建 MQTT 客户端并连接 | ||
| 96 | - MqttClient client = MqttClient.create(vertx, options); | 95 | + // 2. 创建客户端并连接 |
| 96 | + MqttClient client = connect(authInfo); | ||
| 97 | client.connect(SERVER_PORT, SERVER_HOST) | 97 | client.connect(SERVER_PORT, SERVER_HOST) |
| 98 | .onComplete(ar -> { | 98 | .onComplete(ar -> { |
| 99 | if (ar.succeeded()) { | 99 | if (ar.succeeded()) { |
| @@ -114,7 +114,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -114,7 +114,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 114 | } | 114 | } |
| 115 | }); | 115 | }); |
| 116 | 116 | ||
| 117 | - // 4. 等待测试完成 | 117 | + // 3. 等待测试完成 |
| 118 | boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 118 | boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 119 | if (!completed) { | 119 | if (!completed) { |
| 120 | log.warn("[testConnect][测试超时]"); | 120 | log.warn("[testConnect][测试超时]"); |
| @@ -128,86 +128,31 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -128,86 +128,31 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 128 | */ | 128 | */ |
| 129 | @Test | 129 | @Test |
| 130 | public void testPropertyPost() throws Exception { | 130 | public void testPropertyPost() throws Exception { |
| 131 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 132 | - | ||
| 133 | - // 1. 构建认证信息 | ||
| 134 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 135 | - | ||
| 136 | - // 2. 创建 MQTT 客户端配置 | ||
| 137 | - MqttClientOptions options = new MqttClientOptions() | ||
| 138 | - .setClientId(authInfo.getClientId()) | ||
| 139 | - .setUsername(authInfo.getUsername()) | ||
| 140 | - .setPassword(authInfo.getPassword()) | ||
| 141 | - .setCleanSession(true) | ||
| 142 | - .setKeepAliveInterval(60); | ||
| 143 | - | ||
| 144 | - // 3. 创建 MQTT 客户端并连接 | ||
| 145 | - MqttClient client = MqttClient.create(vertx, options); | ||
| 146 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 147 | - .onComplete(connectAr -> { | ||
| 148 | - if (connectAr.succeeded()) { | ||
| 149 | - log.info("[testPropertyPost][连接成功]"); | ||
| 150 | - | ||
| 151 | - // 4.1 设置消息处理器,接收 _reply 响应 | ||
| 152 | - client.publishHandler(message -> { | ||
| 153 | - log.info("[testPropertyPost][收到响应: topic={}, payload={}]", | ||
| 154 | - message.topicName(), message.payload().toString()); | ||
| 155 | - }); | ||
| 156 | - | ||
| 157 | - // 4.2 订阅 _reply 主题 | ||
| 158 | - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 159 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 160 | - .onComplete(subscribeAr -> { | ||
| 161 | - if (subscribeAr.succeeded()) { | ||
| 162 | - log.info("[testPropertyPost][订阅响应主题成功: {}]", replyTopic); | ||
| 163 | - | ||
| 164 | - // 5. 构建属性上报消息(Alink 协议格式) | ||
| 165 | - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 166 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | ||
| 167 | - .put("id", IdUtil.fastSimpleUUID()) | ||
| 168 | - .put("version", "1.0") | ||
| 169 | - .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) | ||
| 170 | - .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 171 | - .put("width", 1) | ||
| 172 | - .put("height", "2") | ||
| 173 | - .build())) | ||
| 174 | - .build()); | ||
| 175 | - log.info("[testPropertyPost][发送消息: topic={}, payload={}]", topic, payload); | ||
| 176 | - | ||
| 177 | - // 6. 发布消息 | ||
| 178 | - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | ||
| 179 | - .onComplete(publishAr -> { | ||
| 180 | - if (publishAr.succeeded()) { | ||
| 181 | - log.info("[testPropertyPost][消息发布成功,messageId={}]", publishAr.result()); | ||
| 182 | - } else { | ||
| 183 | - log.error("[testPropertyPost][消息发布失败]", publishAr.cause()); | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - // 等待一会儿接收响应 | ||
| 187 | - vertx.setTimer(2000, id -> { | ||
| 188 | - client.disconnect() | ||
| 189 | - .onComplete(disconnectAr -> { | ||
| 190 | - log.info("[testPropertyPost][断开连接]"); | ||
| 191 | - latch.countDown(); | ||
| 192 | - }); | ||
| 193 | - }); | ||
| 194 | - }); | ||
| 195 | - } else { | ||
| 196 | - log.error("[testPropertyPost][订阅响应主题失败]", subscribeAr.cause()); | ||
| 197 | - latch.countDown(); | ||
| 198 | - } | ||
| 199 | - }); | ||
| 200 | - } else { | ||
| 201 | - log.error("[testPropertyPost][连接失败]", connectAr.cause()); | ||
| 202 | - latch.countDown(); | ||
| 203 | - } | ||
| 204 | - }); | ||
| 205 | - | ||
| 206 | - // 7. 等待测试完成 | ||
| 207 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 208 | - if (!completed) { | ||
| 209 | - log.warn("[testPropertyPost][测试超时]"); | ||
| 210 | - } | 131 | + // 1. 连接并认证 |
| 132 | + MqttClient client = connectAndAuth(); | ||
| 133 | + log.info("[testPropertyPost][连接认证成功]"); | ||
| 134 | + | ||
| 135 | + // 2. 订阅 _reply 主题 | ||
| 136 | + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 137 | + subscribeReply(client, replyTopic); | ||
| 138 | + | ||
| 139 | + // 3. 构建属性上报消息 | ||
| 140 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 141 | + IdUtil.fastSimpleUUID(), | ||
| 142 | + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), | ||
| 143 | + IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder() | ||
| 144 | + .put("width", 1) | ||
| 145 | + .put("height", "2") | ||
| 146 | + .build()), | ||
| 147 | + null, null, null); | ||
| 148 | + | ||
| 149 | + // 4. 发布消息并等待响应 | ||
| 150 | + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 151 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 152 | + log.info("[testPropertyPost][响应消息: {}]", response); | ||
| 153 | + | ||
| 154 | + // 5. 断开连接 | ||
| 155 | + disconnect(client); | ||
| 211 | } | 156 | } |
| 212 | 157 | ||
| 213 | // ===================== 直连设备事件上报测试 ===================== | 158 | // ===================== 直连设备事件上报测试 ===================== |
| @@ -217,154 +162,244 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -217,154 +162,244 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 217 | */ | 162 | */ |
| 218 | @Test | 163 | @Test |
| 219 | public void testEventPost() throws Exception { | 164 | public void testEventPost() throws Exception { |
| 220 | - CountDownLatch latch = new CountDownLatch(1); | 165 | + // 1. 连接并认证 |
| 166 | + MqttClient client = connectAndAuth(); | ||
| 167 | + log.info("[testEventPost][连接认证成功]"); | ||
| 168 | + | ||
| 169 | + // 2. 订阅 _reply 主题 | ||
| 170 | + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 171 | + subscribeReply(client, replyTopic); | ||
| 172 | + | ||
| 173 | + // 3. 构建事件上报消息 | ||
| 174 | + IotDeviceMessage request = IotDeviceMessage.of( | ||
| 175 | + IdUtil.fastSimpleUUID(), | ||
| 176 | + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), | ||
| 177 | + IotDeviceEventPostReqDTO.of( | ||
| 178 | + "eat", | ||
| 179 | + MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 180 | + System.currentTimeMillis()), | ||
| 181 | + null, null, null); | ||
| 182 | + | ||
| 183 | + // 4. 发布消息并等待响应 | ||
| 184 | + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 185 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 186 | + log.info("[testEventPost][响应消息: {}]", response); | ||
| 187 | + | ||
| 188 | + // 5. 断开连接 | ||
| 189 | + disconnect(client); | ||
| 190 | + } | ||
| 221 | 191 | ||
| 222 | - // 1. 构建认证信息 | ||
| 223 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | 192 | + // ===================== 设备动态注册测试(一型一密) ===================== |
| 224 | 193 | ||
| 225 | - // 2. 创建 MQTT 客户端配置 | ||
| 226 | - MqttClientOptions options = new MqttClientOptions() | ||
| 227 | - .setClientId(authInfo.getClientId()) | ||
| 228 | - .setUsername(authInfo.getUsername()) | ||
| 229 | - .setPassword(authInfo.getPassword()) | ||
| 230 | - .setCleanSession(true) | ||
| 231 | - .setKeepAliveInterval(60); | 194 | + /** |
| 195 | + * 直连设备动态注册测试(一型一密) | ||
| 196 | + * <p> | ||
| 197 | + * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) | ||
| 198 | + * <p> | ||
| 199 | + * 注意:此接口不需要认证 | ||
| 200 | + */ | ||
| 201 | + @Test | ||
| 202 | + public void testDeviceRegister() throws Exception { | ||
| 203 | + // 1. 连接并认证(使用已有设备连接) | ||
| 204 | + MqttClient client = connectAndAuth(); | ||
| 205 | + log.info("[testDeviceRegister][连接认证成功]"); | ||
| 206 | + | ||
| 207 | + // 2.1 构建注册消息 | ||
| 208 | + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); | ||
| 209 | + registerReqDTO.setProductKey(PRODUCT_KEY); | ||
| 210 | + registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis()); | ||
| 211 | + registerReqDTO.setProductSecret("test-product-secret"); | ||
| 212 | + IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), | ||
| 213 | + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); | ||
| 214 | + // 2.2 订阅 _reply 主题 | ||
| 215 | + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", | ||
| 216 | + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 217 | + subscribeReply(client, replyTopic); | ||
| 218 | + | ||
| 219 | + // 3. 发布消息并等待响应 | ||
| 220 | + String topic = String.format("/sys/%s/%s/thing/auth/register", | ||
| 221 | + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); | ||
| 222 | + IotDeviceMessage response = publishAndWaitReply(client, topic, request); | ||
| 223 | + log.info("[testDeviceRegister][响应消息: {}]", response); | ||
| 224 | + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); | ||
| 225 | + | ||
| 226 | + // 4. 断开连接 | ||
| 227 | + disconnect(client); | ||
| 228 | + } | ||
| 232 | 229 | ||
| 233 | - // 3. 创建 MQTT 客户端并连接 | ||
| 234 | - MqttClient client = MqttClient.create(vertx, options); | ||
| 235 | - // TODO @AI:可以像 tcp 里面一样,有个复用么?auth 流程; | ||
| 236 | - client.connect(SERVER_PORT, SERVER_HOST) | ||
| 237 | - .onComplete(connectAr -> { | ||
| 238 | - if (connectAr.succeeded()) { | ||
| 239 | - log.info("[testEventPost][连接成功]"); | ||
| 240 | - | ||
| 241 | - // 4.1 设置消息处理器,接收 _reply 响应 | ||
| 242 | - client.publishHandler(message -> { | ||
| 243 | - log.info("[testEventPost][收到响应: topic={}, payload={}]", | ||
| 244 | - message.topicName(), message.payload().toString()); | ||
| 245 | - }); | 230 | + // ===================== 订阅下行消息测试 ===================== |
| 246 | 231 | ||
| 247 | - // 4.2 订阅 _reply 主题 | ||
| 248 | - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); | ||
| 249 | - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 250 | - .onComplete(subscribeAr -> { | ||
| 251 | - if (subscribeAr.succeeded()) { | ||
| 252 | - log.info("[testEventPost][订阅响应主题成功: {}]", replyTopic); | ||
| 253 | - | ||
| 254 | - // 5. 构建事件上报消息(Alink 协议格式) | ||
| 255 | - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); | ||
| 256 | - String payload = JsonUtils.toJsonString(MapUtil.builder() | ||
| 257 | - .put("id", IdUtil.fastSimpleUUID()) | ||
| 258 | - .put("version", "1.0") | ||
| 259 | - .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) | ||
| 260 | - .put("params", IotDeviceEventPostReqDTO.of( | ||
| 261 | - "eat", | ||
| 262 | - MapUtil.<String, Object>builder().put("rice", 3).build(), | ||
| 263 | - System.currentTimeMillis())) | ||
| 264 | - .build()); | ||
| 265 | - log.info("[testEventPost][发送消息: topic={}, payload={}]", topic, payload); | ||
| 266 | - | ||
| 267 | - // 6. 发布消息 | ||
| 268 | - client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | ||
| 269 | - .onComplete(publishAr -> { | ||
| 270 | - if (publishAr.succeeded()) { | ||
| 271 | - log.info("[testEventPost][消息发布成功,messageId={}]", publishAr.result()); | ||
| 272 | - } else { | ||
| 273 | - log.error("[testEventPost][消息发布失败]", publishAr.cause()); | ||
| 274 | - } | ||
| 275 | - | ||
| 276 | - // 等待一会儿接收响应 | ||
| 277 | - vertx.setTimer(2000, id -> { | ||
| 278 | - client.disconnect() | ||
| 279 | - .onComplete(disconnectAr -> { | ||
| 280 | - log.info("[testEventPost][断开连接]"); | ||
| 281 | - latch.countDown(); | ||
| 282 | - }); | ||
| 283 | - }); | ||
| 284 | - }); | ||
| 285 | - } else { | ||
| 286 | - log.error("[testEventPost][订阅响应主题失败]", subscribeAr.cause()); | 232 | + /** |
| 233 | + * 订阅下行消息测试:订阅服务端下发的消息 | ||
| 234 | + */ | ||
| 235 | + @Test | ||
| 236 | + public void testSubscribe() throws Exception { | ||
| 237 | + CountDownLatch latch = new CountDownLatch(1); | ||
| 238 | + | ||
| 239 | + // 1. 连接并认证 | ||
| 240 | + MqttClient client = connectAndAuth(); | ||
| 241 | + log.info("[testSubscribe][连接认证成功]"); | ||
| 242 | + | ||
| 243 | + // 2. 设置消息处理器 | ||
| 244 | + client.publishHandler(message -> { | ||
| 245 | + log.info("[testSubscribe][收到消息: topic={}, payload={}]", | ||
| 246 | + message.topicName(), message.payload().toString()); | ||
| 247 | + }); | ||
| 248 | + | ||
| 249 | + // 3. 订阅下行主题 | ||
| 250 | + String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | ||
| 251 | + log.info("[testSubscribe][订阅主题: {}]", topic); | ||
| 252 | + | ||
| 253 | + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 254 | + .onComplete(subscribeAr -> { | ||
| 255 | + if (subscribeAr.succeeded()) { | ||
| 256 | + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | ||
| 257 | + // 保持连接 30 秒等待消息 | ||
| 258 | + vertx.setTimer(30000, id -> { | ||
| 259 | + client.disconnect() | ||
| 260 | + .onComplete(disconnectAr -> { | ||
| 261 | + log.info("[testSubscribe][断开连接]"); | ||
| 287 | latch.countDown(); | 262 | latch.countDown(); |
| 288 | - } | ||
| 289 | - }); | 263 | + }); |
| 264 | + }); | ||
| 290 | } else { | 265 | } else { |
| 291 | - log.error("[testEventPost][连接失败]", connectAr.cause()); | 266 | + log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); |
| 292 | latch.countDown(); | 267 | latch.countDown(); |
| 293 | } | 268 | } |
| 294 | }); | 269 | }); |
| 295 | 270 | ||
| 296 | - // 7. 等待测试完成 | ||
| 297 | - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 271 | + // 4. 等待测试完成 |
| 272 | + boolean completed = latch.await(60, TimeUnit.SECONDS); | ||
| 298 | if (!completed) { | 273 | if (!completed) { |
| 299 | - log.warn("[testEventPost][测试超时]"); | 274 | + log.warn("[testSubscribe][测试超时]"); |
| 300 | } | 275 | } |
| 301 | } | 276 | } |
| 302 | 277 | ||
| 303 | - // ===================== 订阅下行消息测试 ===================== | 278 | + // ===================== 辅助方法 ===================== |
| 304 | 279 | ||
| 305 | /** | 280 | /** |
| 306 | - * 订阅下行消息测试:订阅服务端下发的消息 | 281 | + * 创建 MQTT 客户端 |
| 282 | + * | ||
| 283 | + * @param authInfo 认证信息 | ||
| 284 | + * @return MQTT 客户端 | ||
| 307 | */ | 285 | */ |
| 308 | - @Test | ||
| 309 | - public void testSubscribe() throws Exception { | ||
| 310 | - CountDownLatch latch = new CountDownLatch(1); | ||
| 311 | - | ||
| 312 | - // 1. 构建认证信息 | ||
| 313 | - IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 314 | - | ||
| 315 | - // 2. 创建 MQTT 客户端配置 | 286 | + private MqttClient connect(IotDeviceAuthReqDTO authInfo) { |
| 316 | MqttClientOptions options = new MqttClientOptions() | 287 | MqttClientOptions options = new MqttClientOptions() |
| 317 | .setClientId(authInfo.getClientId()) | 288 | .setClientId(authInfo.getClientId()) |
| 318 | .setUsername(authInfo.getUsername()) | 289 | .setUsername(authInfo.getUsername()) |
| 319 | .setPassword(authInfo.getPassword()) | 290 | .setPassword(authInfo.getPassword()) |
| 320 | .setCleanSession(true) | 291 | .setCleanSession(true) |
| 321 | .setKeepAliveInterval(60); | 292 | .setKeepAliveInterval(60); |
| 293 | + return MqttClient.create(vertx, options); | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + /** | ||
| 297 | + * 连接并认证设备 | ||
| 298 | + * | ||
| 299 | + * @return 已认证的 MQTT 客户端 | ||
| 300 | + */ | ||
| 301 | + private MqttClient connectAndAuth() throws Exception { | ||
| 302 | + // 1. 创建客户端并连接 | ||
| 303 | + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); | ||
| 304 | + MqttClient client = connect(authInfo); | ||
| 322 | 305 | ||
| 323 | - // 3. 创建 MQTT 客户端并连接 | ||
| 324 | - MqttClient client = MqttClient.create(vertx, options); | 306 | + // 2.1 连接 |
| 307 | + CompletableFuture<MqttClient> future = new CompletableFuture<>(); | ||
| 325 | client.connect(SERVER_PORT, SERVER_HOST) | 308 | client.connect(SERVER_PORT, SERVER_HOST) |
| 326 | - .onComplete(connectAr -> { | ||
| 327 | - if (connectAr.succeeded()) { | ||
| 328 | - log.info("[testSubscribe][连接成功]"); | ||
| 329 | - | ||
| 330 | - // 4. 设置消息处理器 | ||
| 331 | - client.publishHandler(message -> { | ||
| 332 | - log.info("[testSubscribe][收到消息: topic={}, payload={}]", | ||
| 333 | - message.topicName(), message.payload().toString()); | ||
| 334 | - }); | 309 | + .onComplete(ar -> { |
| 310 | + if (ar.succeeded()) { | ||
| 311 | + future.complete(client); | ||
| 312 | + } else { | ||
| 313 | + future.completeExceptionally(ar.cause()); | ||
| 314 | + } | ||
| 315 | + }); | ||
| 316 | + // 2.2 等待连接结果 | ||
| 317 | + return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 318 | + } | ||
| 335 | 319 | ||
| 336 | - // 5. 订阅下行主题 | ||
| 337 | - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | ||
| 338 | - log.info("[testSubscribe][订阅主题: {}]", topic); | ||
| 339 | - | ||
| 340 | - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 341 | - .onComplete(subscribeAr -> { | ||
| 342 | - if (subscribeAr.succeeded()) { | ||
| 343 | - log.info("[testSubscribe][订阅成功,等待下行消息... (按 Ctrl+C 结束)]"); | ||
| 344 | - // 保持连接 30 秒等待消息 | ||
| 345 | - vertx.setTimer(30000, id -> { | ||
| 346 | - client.disconnect() | ||
| 347 | - .onComplete(disconnectAr -> { | ||
| 348 | - log.info("[testSubscribe][断开连接]"); | ||
| 349 | - latch.countDown(); | ||
| 350 | - }); | ||
| 351 | - }); | ||
| 352 | - } else { | ||
| 353 | - log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); | ||
| 354 | - latch.countDown(); | ||
| 355 | - } | ||
| 356 | - }); | 320 | + /** |
| 321 | + * 订阅响应主题 | ||
| 322 | + * | ||
| 323 | + * @param client MQTT 客户端 | ||
| 324 | + * @param replyTopic 响应主题 | ||
| 325 | + */ | ||
| 326 | + private void subscribeReply(MqttClient client, String replyTopic) throws Exception { | ||
| 327 | + // 1. 订阅响应主题 | ||
| 328 | + CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 329 | + client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) | ||
| 330 | + .onComplete(ar -> { | ||
| 331 | + if (ar.succeeded()) { | ||
| 332 | + log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); | ||
| 333 | + future.complete(null); | ||
| 357 | } else { | 334 | } else { |
| 358 | - log.error("[testSubscribe][连接失败]", connectAr.cause()); | ||
| 359 | - latch.countDown(); | 335 | + future.completeExceptionally(ar.cause()); |
| 360 | } | 336 | } |
| 361 | }); | 337 | }); |
| 338 | + // 2. 等待订阅结果 | ||
| 339 | + future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 340 | + } | ||
| 362 | 341 | ||
| 363 | - // 6. 等待测试完成 | ||
| 364 | - boolean completed = latch.await(60, TimeUnit.SECONDS); | ||
| 365 | - if (!completed) { | ||
| 366 | - log.warn("[testSubscribe][测试超时]"); | 342 | + /** |
| 343 | + * 发布消息并等待响应 | ||
| 344 | + * | ||
| 345 | + * @param client MQTT 客户端 | ||
| 346 | + * @param topic 发布主题 | ||
| 347 | + * @param request 请求消息 | ||
| 348 | + * @return 响应消息 | ||
| 349 | + */ | ||
| 350 | + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { | ||
| 351 | + // 1. 设置消息处理器,接收响应 | ||
| 352 | + CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>(); | ||
| 353 | + client.publishHandler(message -> { | ||
| 354 | + log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", | ||
| 355 | + message.topicName(), message.payload().toString()); | ||
| 356 | + IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | ||
| 357 | + future.complete(response); | ||
| 358 | + }); | ||
| 359 | + | ||
| 360 | + // 2. 编码并发布消息 | ||
| 361 | + byte[] payload = CODEC.encode(request); | ||
| 362 | + log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", | ||
| 363 | + CODEC.type(), topic, new String(payload)); | ||
| 364 | + | ||
| 365 | + client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) | ||
| 366 | + .onComplete(ar -> { | ||
| 367 | + if (ar.succeeded()) { | ||
| 368 | + log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); | ||
| 369 | + } else { | ||
| 370 | + log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); | ||
| 371 | + future.completeExceptionally(ar.cause()); | ||
| 372 | + } | ||
| 373 | + }); | ||
| 374 | + | ||
| 375 | + // 3. 等待响应(超时返回 null) | ||
| 376 | + try { | ||
| 377 | + return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 378 | + } catch (Exception e) { | ||
| 379 | + log.warn("[publishAndWaitReply][等待响应超时或失败]"); | ||
| 380 | + return null; | ||
| 367 | } | 381 | } |
| 368 | } | 382 | } |
| 369 | 383 | ||
| 384 | + /** | ||
| 385 | + * 断开连接 | ||
| 386 | + * | ||
| 387 | + * @param client MQTT 客户端 | ||
| 388 | + */ | ||
| 389 | + private void disconnect(MqttClient client) throws Exception { | ||
| 390 | + // 1. 断开连接 | ||
| 391 | + CompletableFuture<Void> future = new CompletableFuture<>(); | ||
| 392 | + client.disconnect() | ||
| 393 | + .onComplete(ar -> { | ||
| 394 | + if (ar.succeeded()) { | ||
| 395 | + log.info("[disconnect][断开连接成功]"); | ||
| 396 | + future.complete(null); | ||
| 397 | + } else { | ||
| 398 | + future.completeExceptionally(ar.cause()); | ||
| 399 | + } | ||
| 400 | + }); | ||
| 401 | + // 2. 等待断开结果 | ||
| 402 | + future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | ||
| 403 | + } | ||
| 404 | + | ||
| 370 | } | 405 | } |