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 +6,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr
6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
  9 +import io.vertx.core.Vertx;
9 import lombok.extern.slf4j.Slf4j; 10 import lombok.extern.slf4j.Slf4j;
10 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11 import org.springframework.boot.context.properties.EnableConfigurationProperties; 12 import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -45,14 +46,21 @@ public class IotGatewayConfiguration { @@ -45,14 +46,21 @@ public class IotGatewayConfiguration {
45 @Slf4j 46 @Slf4j
46 public static class MqttProtocolConfiguration { 47 public static class MqttProtocolConfiguration {
47 48
  49 + @Bean(destroyMethod = "close")
  50 + public Vertx emqxVertx() {
  51 + return Vertx.vertx();
  52 + }
  53 +
48 @Bean 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 @Bean 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 @Bean 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,20 +28,20 @@ public class IotEmqxAuthEventProtocol {
28 28
29 private final String serverId; 29 private final String serverId;
30 30
31 - private Vertx vertx; 31 + private final Vertx vertx;
32 32
33 private HttpServer httpServer; 33 private HttpServer httpServer;
34 34
35 - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties) { 35 + public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
  36 + Vertx vertx) {
36 this.emqxProperties = emqxProperties; 37 this.emqxProperties = emqxProperties;
  38 + this.vertx = vertx;
37 this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); 39 this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
38 } 40 }
39 41
40 @PostConstruct 42 @PostConstruct
41 public void start() { 43 public void start() {
42 try { 44 try {
43 - // 创建 Vertx 实例  
44 - this.vertx = Vertx.vertx();  
45 startHttpServer(); 45 startHttpServer();
46 log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort()); 46 log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
47 } catch (Exception e) { 47 } catch (Exception e) {
@@ -53,17 +53,6 @@ public class IotEmqxAuthEventProtocol { @@ -53,17 +53,6 @@ public class IotEmqxAuthEventProtocol {
53 @PreDestroy 53 @PreDestroy
54 public void stop() { 54 public void stop() {
55 stopHttpServer(); 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 log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]"); 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,7 +10,6 @@ import io.vertx.mqtt.MqttClient;
10 import io.vertx.mqtt.MqttClientOptions; 10 import io.vertx.mqtt.MqttClientOptions;
11 import jakarta.annotation.PostConstruct; 11 import jakarta.annotation.PostConstruct;
12 import jakarta.annotation.PreDestroy; 12 import jakarta.annotation.PreDestroy;
13 -import jodd.util.ThreadUtil;  
14 import lombok.Getter; 13 import lombok.Getter;
15 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
16 15
@@ -41,9 +40,11 @@ public class IotEmqxUpstreamProtocol { @@ -41,9 +40,11 @@ public class IotEmqxUpstreamProtocol {
41 40
42 private IotEmqxUpstreamHandler upstreamHandler; 41 private IotEmqxUpstreamHandler upstreamHandler;
43 42
44 - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties) { 43 + public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
  44 + Vertx vertx) {
45 this.emqxProperties = emqxProperties; 45 this.emqxProperties = emqxProperties;
46 this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); 46 this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
  47 + this.vertx = vertx;
47 } 48 }
48 49
49 @PostConstruct 50 @PostConstruct
@@ -53,13 +54,10 @@ public class IotEmqxUpstreamProtocol { @@ -53,13 +54,10 @@ public class IotEmqxUpstreamProtocol {
53 } 54 }
54 55
55 try { 56 try {
56 - // 1. 初始化 Vertx 实例  
57 - this.vertx = Vertx.vertx();  
58 -  
59 - // 2. 启动 MQTT 客户端 57 + // 1. 启动 MQTT 客户端
60 startMqttClient(); 58 startMqttClient();
61 59
62 - // 3. 标记服务为运行状态 60 + // 2. 标记服务为运行状态
63 isRunning = true; 61 isRunning = true;
64 log.info("[start][IoT 网关 EMQX 协议启动成功]"); 62 log.info("[start][IoT 网关 EMQX 协议启动成功]");
65 } catch (Exception e) { 63 } catch (Exception e) {
@@ -67,10 +65,16 @@ public class IotEmqxUpstreamProtocol { @@ -67,10 +65,16 @@ public class IotEmqxUpstreamProtocol {
67 stop(); 65 stop();
68 66
69 // 异步关闭应用 67 // 异步关闭应用
70 - // TODO haohao:是不是不用 sleep 也行哈?  
71 Thread shutdownThread = new Thread(() -> { 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 System.exit(1); 78 System.exit(1);
75 }); 79 });
76 shutdownThread.setDaemon(true); 80 shutdownThread.setDaemon(true);
@@ -90,16 +94,7 @@ public class IotEmqxUpstreamProtocol { @@ -90,16 +94,7 @@ public class IotEmqxUpstreamProtocol {
90 // 1. 停止 MQTT 客户端 94 // 1. 停止 MQTT 客户端
91 stopMqttClient(); 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 isRunning = false; 98 isRunning = false;
104 log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); 99 log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
105 } 100 }
@@ -147,7 +142,7 @@ public class IotEmqxUpstreamProtocol { @@ -147,7 +142,7 @@ public class IotEmqxUpstreamProtocol {
147 142
148 // 2. 等待连接结果 143 // 2. 等待连接结果
149 try { 144 try {
150 - // TODO @haohao:想了下,timeout 可以不设置,全靠 mqttclient 的超时时间? 145 + // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制
151 boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); 146 boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
152 if (!awaitResult) { 147 if (!awaitResult) {
153 log.error("[connectMqttSync][等待连接结果超时]"); 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,7 +54,8 @@ public class IotEmqxDownstreamHandler {
54 return; 54 return;
55 } 55 }
56 // 2.2 构建载荷 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 // 2.3 发布消息 59 // 2.3 发布消息
59 protocol.publishMessage(topic, payload); 60 protocol.publishMessage(topic, payload);
60 } 61 }
@@ -78,20 +79,54 @@ public class IotEmqxDownstreamHandler { @@ -78,20 +79,54 @@ public class IotEmqxDownstreamHandler {
78 // 2. 根据消息方法和回复状态,构建 topic 79 // 2. 根据消息方法和回复状态,构建 topic
79 boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); 80 boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
80 81
81 - // TODO @芋艿:需要添加对应的 Topic,所以需要先判断消息方法类型  
82 - // TODO @haohao:基于 method,然后逆推对应的 topic,可以哇?约定好~  
83 - // 根据消息方法和回复状态构建对应的主题 82 + // 3. 根据消息方法类型构建对应的主题
84 switch (methodEnum) { 83 switch (methodEnum) {
85 case PROPERTY_POST: 84 case PROPERTY_POST:
  85 + // 属性上报:只支持回复消息(下行)
86 if (isReply) { 86 if (isReply) {
87 return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName); 87 return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);
88 } 88 }
89 break; 89 break;
  90 +
90 case PROPERTY_SET: 91 case PROPERTY_SET:
  92 + // 属性设置:只支持非回复消息(下行)
91 if (!isReply) { 93 if (!isReply) {
92 return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName); 94 return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);
93 } 95 }
94 break; 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 log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]", 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,4 +91,39 @@ public final class IotMqttTopicUtils {
91 return buildDeviceTopicPrefix(productKey, deviceName) + SERVICE_TOPIC_PREFIX + serviceIdentifier; 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 \ No newline at end of file 130 \ No newline at end of file