Commit 18c27196f1638f48d29f999e58c27c4e1313623d
1 parent
801a6b97
feat:【IoT 物联网】为 EMQX 协议添加 共享 的 Vertx
Showing
5 changed files
with
106 additions
and
44 deletions
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 |