Commit 18c27196f1638f48d29f999e58c27c4e1313623d

Authored by haohao
1 parent 801a6b97

feat:【IoT 物联网】为 EMQX 协议添加 共享 的 Vertx

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
... ... @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
7 7 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
  9 +import io.vertx.core.Vertx;
9 10 import lombok.extern.slf4j.Slf4j;
10 11 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11 12 import org.springframework.boot.context.properties.EnableConfigurationProperties;
... ... @@ -45,14 +46,21 @@ public class IotGatewayConfiguration {
45 46 @Slf4j
46 47 public static class MqttProtocolConfiguration {
47 48  
  49 + @Bean(destroyMethod = "close")
  50 + public Vertx emqxVertx() {
  51 + return Vertx.vertx();
  52 + }
  53 +
48 54 @Bean
49   - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties) {
50   - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx());
  55 + public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
  56 + Vertx emqxVertx) {
  57 + return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
51 58 }
52 59  
53 60 @Bean
54   - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties) {
55   - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx());
  61 + public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
  62 + Vertx emqxVertx) {
  63 + return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
56 64 }
57 65  
58 66 @Bean
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java
... ... @@ -28,20 +28,20 @@ public class IotEmqxAuthEventProtocol {
28 28  
29 29 private final String serverId;
30 30  
31   - private Vertx vertx;
  31 + private final Vertx vertx;
32 32  
33 33 private HttpServer httpServer;
34 34  
35   - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties) {
  35 + public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
  36 + Vertx vertx) {
36 37 this.emqxProperties = emqxProperties;
  38 + this.vertx = vertx;
37 39 this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
38 40 }
39 41  
40 42 @PostConstruct
41 43 public void start() {
42 44 try {
43   - // 创建 Vertx 实例
44   - this.vertx = Vertx.vertx();
45 45 startHttpServer();
46 46 log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
47 47 } catch (Exception e) {
... ... @@ -53,17 +53,6 @@ public class IotEmqxAuthEventProtocol {
53 53 @PreDestroy
54 54 public void stop() {
55 55 stopHttpServer();
56   -
57   - // 关闭 Vertx 实例
58   - if (vertx != null) {
59   - try {
60   - vertx.close();
61   - log.debug("[stop][Vertx 实例已关闭]");
62   - } catch (Exception e) {
63   - log.warn("[stop][关闭 Vertx 实例失败]", e);
64   - }
65   - }
66   -
67 56 log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]");
68 57 }
69 58  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java
... ... @@ -10,7 +10,6 @@ import io.vertx.mqtt.MqttClient;
10 10 import io.vertx.mqtt.MqttClientOptions;
11 11 import jakarta.annotation.PostConstruct;
12 12 import jakarta.annotation.PreDestroy;
13   -import jodd.util.ThreadUtil;
14 13 import lombok.Getter;
15 14 import lombok.extern.slf4j.Slf4j;
16 15  
... ... @@ -41,9 +40,11 @@ public class IotEmqxUpstreamProtocol {
41 40  
42 41 private IotEmqxUpstreamHandler upstreamHandler;
43 42  
44   - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties) {
  43 + public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
  44 + Vertx vertx) {
45 45 this.emqxProperties = emqxProperties;
46 46 this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
  47 + this.vertx = vertx;
47 48 }
48 49  
49 50 @PostConstruct
... ... @@ -53,13 +54,10 @@ public class IotEmqxUpstreamProtocol {
53 54 }
54 55  
55 56 try {
56   - // 1. 初始化 Vertx 实例
57   - this.vertx = Vertx.vertx();
58   -
59   - // 2. 启动 MQTT 客户端
  57 + // 1. 启动 MQTT 客户端
60 58 startMqttClient();
61 59  
62   - // 3. 标记服务为运行状态
  60 + // 2. 标记服务为运行状态
63 61 isRunning = true;
64 62 log.info("[start][IoT 网关 EMQX 协议启动成功]");
65 63 } catch (Exception e) {
... ... @@ -67,10 +65,16 @@ public class IotEmqxUpstreamProtocol {
67 65 stop();
68 66  
69 67 // 异步关闭应用
70   - // TODO haohao:是不是不用 sleep 也行哈?
71 68 Thread shutdownThread = new Thread(() -> {
72   - ThreadUtil.sleep(1000);
73   - log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
  69 + try {
  70 + // 确保日志输出完成,使用更优雅的方式
  71 + log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
  72 + // 等待日志输出完成
  73 + Thread.sleep(1000);
  74 + } catch (InterruptedException ie) {
  75 + Thread.currentThread().interrupt();
  76 + log.warn("[start][应用关闭被中断]");
  77 + }
74 78 System.exit(1);
75 79 });
76 80 shutdownThread.setDaemon(true);
... ... @@ -90,16 +94,7 @@ public class IotEmqxUpstreamProtocol {
90 94 // 1. 停止 MQTT 客户端
91 95 stopMqttClient();
92 96  
93   - // 2. 关闭 Vertx 实例
94   - if (vertx != null) {
95   - try {
96   - vertx.close();
97   - } catch (Exception e) {
98   - log.warn("[stop][关闭 Vertx 实例失败]", e);
99   - }
100   - }
101   -
102   - // 3. 标记服务为停止状态
  97 + // 2. 标记服务为停止状态
103 98 isRunning = false;
104 99 log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
105 100 }
... ... @@ -147,7 +142,7 @@ public class IotEmqxUpstreamProtocol {
147 142  
148 143 // 2. 等待连接结果
149 144 try {
150   - // TODO @haohao:想了下,timeout 可以不设置,全靠 mqttclient 的超时时间?
  145 + // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制
151 146 boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
152 147 if (!awaitResult) {
153 148 log.error("[connectMqttSync][等待连接结果超时]");
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java
... ... @@ -54,7 +54,8 @@ public class IotEmqxDownstreamHandler {
54 54 return;
55 55 }
56 56 // 2.2 构建载荷
57   - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  57 + byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
  58 + deviceInfo.getDeviceName());
58 59 // 2.3 发布消息
59 60 protocol.publishMessage(topic, payload);
60 61 }
... ... @@ -78,20 +79,54 @@ public class IotEmqxDownstreamHandler {
78 79 // 2. 根据消息方法和回复状态,构建 topic
79 80 boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
80 81  
81   - // TODO @芋艿:需要添加对应的 Topic,所以需要先判断消息方法类型
82   - // TODO @haohao:基于 method,然后逆推对应的 topic,可以哇?约定好~
83   - // 根据消息方法和回复状态构建对应的主题
  82 + // 3. 根据消息方法类型构建对应的主题
84 83 switch (methodEnum) {
85 84 case PROPERTY_POST:
  85 + // 属性上报:只支持回复消息(下行)
86 86 if (isReply) {
87 87 return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);
88 88 }
89 89 break;
  90 +
90 91 case PROPERTY_SET:
  92 + // 属性设置:只支持非回复消息(下行)
91 93 if (!isReply) {
92 94 return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);
93 95 }
94 96 break;
  97 +
  98 + case EVENT_POST:
  99 + // 事件上报:只支持回复消息(下行)
  100 + if (isReply) {
  101 + String identifier = IotDeviceMessageUtils.getIdentifier(message);
  102 + if (StrUtil.isNotBlank(identifier)) {
  103 + return IotMqttTopicUtils.buildEventPostReplyTopic(productKey, deviceName, identifier);
  104 + }
  105 + }
  106 + break;
  107 +
  108 + case SERVICE_INVOKE:
  109 + // 服务调用:支持请求和回复
  110 + String serviceIdentifier = IotDeviceMessageUtils.getIdentifier(message);
  111 + if (StrUtil.isNotBlank(serviceIdentifier)) {
  112 + if (isReply) {
  113 + return IotMqttTopicUtils.buildServiceReplyTopic(productKey, deviceName, serviceIdentifier);
  114 + } else {
  115 + return IotMqttTopicUtils.buildServiceTopic(productKey, deviceName, serviceIdentifier);
  116 + }
  117 + }
  118 + break;
  119 +
  120 + case CONFIG_PUSH:
  121 + // 配置推送:平台向设备推送配置(下行请求),设备回复确认(上行回复)
  122 + if (!isReply) {
  123 + return IotMqttTopicUtils.buildConfigPushTopic(productKey, deviceName);
  124 + }
  125 + break;
  126 +
  127 + default:
  128 + log.warn("[buildTopicByMethod][未处理的消息方法: {}]", methodEnum);
  129 + break;
95 130 }
96 131  
97 132 log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]",
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
... ... @@ -91,4 +91,39 @@ public final class IotMqttTopicUtils {
91 91 return buildDeviceTopicPrefix(productKey, deviceName) + SERVICE_TOPIC_PREFIX + serviceIdentifier;
92 92 }
93 93  
  94 + /**
  95 + * 构建设备服务调用回复主题
  96 + *
  97 + * @param productKey 产品 Key
  98 + * @param deviceName 设备名称
  99 + * @param serviceIdentifier 服务标识符
  100 + * @return 完整的主题路径
  101 + */
  102 + public static String buildServiceReplyTopic(String productKey, String deviceName, String serviceIdentifier) {
  103 + return buildDeviceTopicPrefix(productKey, deviceName) + SERVICE_TOPIC_PREFIX + serviceIdentifier + "_reply";
  104 + }
  105 +
  106 + /**
  107 + * 构建设备事件上报回复主题
  108 + *
  109 + * @param productKey 产品 Key
  110 + * @param deviceName 设备名称
  111 + * @param eventIdentifier 事件标识符
  112 + * @return 完整的主题路径
  113 + */
  114 + public static String buildEventPostReplyTopic(String productKey, String deviceName, String eventIdentifier) {
  115 + return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/event/" + eventIdentifier + "_reply";
  116 + }
  117 +
  118 + /**
  119 + * 构建设备配置推送主题
  120 + *
  121 + * @param productKey 产品 Key
  122 + * @param deviceName 设备名称
  123 + * @return 完整的主题路径
  124 + */
  125 + public static String buildConfigPushTopic(String productKey, String deviceName) {
  126 + return buildDeviceTopicPrefix(productKey, deviceName) + "/thing/config/push";
  127 + }
  128 +
94 129 }
95 130 \ No newline at end of file
... ...