Commit 53697b55c242ce246ba01a9ee85559c21aed0320
1 parent
8cf8af1f
【功能完善】IoT: 更新 EMQX 插件配置,添加 MQTT 连接参数,重构相关逻辑
Showing
11 changed files
with
161 additions
and
92 deletions
yudao-dependencies/pom.xml
| @@ -67,7 +67,7 @@ | @@ -67,7 +67,7 @@ | ||
| 67 | <netty.version>4.1.116.Final</netty.version> | 67 | <netty.version>4.1.116.Final</netty.version> |
| 68 | <mqtt.version>1.2.5</mqtt.version> | 68 | <mqtt.version>1.2.5</mqtt.version> |
| 69 | <pf4j-spring.version>0.9.0</pf4j-spring.version> | 69 | <pf4j-spring.version>0.9.0</pf4j-spring.version> |
| 70 | - <vertx.version>4.5.11</vertx.version> | 70 | + <vertx.version>4.5.13</vertx.version> |
| 71 | <!-- 三方云服务相关 --> | 71 | <!-- 三方云服务相关 --> |
| 72 | <commons-io.version>2.17.0</commons-io.version> | 72 | <commons-io.version>2.17.0</commons-io.version> |
| 73 | <commons-compress.version>1.27.1</commons-compress.version> | 73 | <commons-compress.version>1.27.1</commons-compress.version> |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java
| @@ -62,7 +62,7 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { | @@ -62,7 +62,7 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { | ||
| 62 | 62 | ||
| 63 | @Override | 63 | @Override |
| 64 | public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { | 64 | public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { |
| 65 | - Boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO); | 65 | + boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO); |
| 66 | return success(result); | 66 | return success(result); |
| 67 | } | 67 | } |
| 68 | 68 |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
| @@ -79,18 +79,6 @@ public class IotDeviceRespVO { | @@ -79,18 +79,6 @@ public class IotDeviceRespVO { | ||
| 79 | @ExcelProperty("设备密钥") | 79 | @ExcelProperty("设备密钥") |
| 80 | private String deviceSecret; | 80 | private String deviceSecret; |
| 81 | 81 | ||
| 82 | - @Schema(description = "MQTT 客户端 ID", example = "24602") | ||
| 83 | - @ExcelProperty("MQTT 客户端 ID") | ||
| 84 | - private String mqttClientId; | ||
| 85 | - | ||
| 86 | - @Schema(description = "MQTT 用户名", example = "芋艿") | ||
| 87 | - @ExcelProperty("MQTT 用户名") | ||
| 88 | - private String mqttUsername; | ||
| 89 | - | ||
| 90 | - @Schema(description = "MQTT 密码") | ||
| 91 | - @ExcelProperty("MQTT 密码") | ||
| 92 | - private String mqttPassword; | ||
| 93 | - | ||
| 94 | @Schema(description = "认证类型(如一机一密、动态注册)", example = "2") | 82 | @Schema(description = "认证类型(如一机一密、动态注册)", example = "2") |
| 95 | @ExcelProperty("认证类型(如一机一密、动态注册)") | 83 | @ExcelProperty("认证类型(如一机一密、动态注册)") |
| 96 | private String authType; | 84 | private String authType; |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java
| @@ -67,6 +67,6 @@ public interface IotDeviceUpstreamService { | @@ -67,6 +67,6 @@ public interface IotDeviceUpstreamService { | ||
| 67 | * | 67 | * |
| 68 | * @param authReqDTO Emqx 连接认证 DTO | 68 | * @param authReqDTO Emqx 连接认证 DTO |
| 69 | */ | 69 | */ |
| 70 | - Boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO); | 70 | + boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO); |
| 71 | 71 | ||
| 72 | } | 72 | } |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java
| @@ -174,7 +174,7 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | @@ -174,7 +174,7 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | ||
| 174 | } | 174 | } |
| 175 | 175 | ||
| 176 | private void registerDevice0(String productKey, String deviceName, Long gatewayId, | 176 | private void registerDevice0(String productKey, String deviceName, Long gatewayId, |
| 177 | - IotDeviceUpstreamAbstractReqDTO registerReqDTO) { | 177 | + IotDeviceUpstreamAbstractReqDTO registerReqDTO) { |
| 178 | // 1.1 注册设备 | 178 | // 1.1 注册设备 |
| 179 | IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); | 179 | IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); |
| 180 | boolean registerNew = device == null; | 180 | boolean registerNew = device == null; |
| @@ -280,16 +280,15 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | @@ -280,16 +280,15 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | ||
| 280 | sendDeviceMessage(message, device); | 280 | sendDeviceMessage(message, device); |
| 281 | } | 281 | } |
| 282 | 282 | ||
| 283 | - // TODO @haohao:建议返回 boolean; | ||
| 284 | @Override | 283 | @Override |
| 285 | - public Boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { | 284 | + public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { |
| 286 | log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO); | 285 | log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO); |
| 287 | // 1. 校验设备是否存在 | 286 | // 1. 校验设备是否存在 |
| 288 | // username 格式:${DeviceName}&${ProductKey} | 287 | // username 格式:${DeviceName}&${ProductKey} |
| 289 | String[] usernameParts = authReqDTO.getUsername().split("&"); | 288 | String[] usernameParts = authReqDTO.getUsername().split("&"); |
| 290 | if (usernameParts.length != 2) { | 289 | if (usernameParts.length != 2) { |
| 291 | log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]"); | 290 | log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]"); |
| 292 | - return Boolean.FALSE; | 291 | + return false; |
| 293 | } | 292 | } |
| 294 | String deviceName = usernameParts[0]; | 293 | String deviceName = usernameParts[0]; |
| 295 | String productKey = usernameParts[1]; | 294 | String productKey = usernameParts[1]; |
| @@ -298,19 +297,18 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | @@ -298,19 +297,18 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | ||
| 298 | if (device == null) { | 297 | if (device == null) { |
| 299 | log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", | 298 | log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", |
| 300 | productKey, deviceName); | 299 | productKey, deviceName); |
| 301 | - return Boolean.FALSE; | 300 | + return false; |
| 302 | } | 301 | } |
| 303 | // 2. 校验密码 | 302 | // 2. 校验密码 |
| 304 | String deviceSecret = device.getDeviceSecret(); | 303 | String deviceSecret = device.getDeviceSecret(); |
| 305 | String clientId = authReqDTO.getClientId(); | 304 | String clientId = authReqDTO.getClientId(); |
| 306 | MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId); | 305 | MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId); |
| 307 | - // TODO @haohao:notEquals,尽量不走取反逻辑哈 | ||
| 308 | - if (!StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) { | ||
| 309 | - log.error("[authenticateEmqxConnection][认证失败,密码不正确]"); | ||
| 310 | - return Boolean.FALSE; | 306 | + if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) { |
| 307 | + log.info("[authenticateEmqxConnection][认证成功]"); | ||
| 308 | + return true; | ||
| 311 | } | 309 | } |
| 312 | - log.info("[authenticateEmqxConnection][认证成功]"); | ||
| 313 | - return Boolean.TRUE; | 310 | + log.error("[authenticateEmqxConnection][认证失败,密码不正确]"); |
| 311 | + return false; | ||
| 314 | } | 312 | } |
| 315 | 313 | ||
| 316 | private void updateDeviceLastTime(IotDeviceDO device, IotDeviceUpstreamAbstractReqDTO reqDTO) { | 314 | private void updateDeviceLastTime(IotDeviceDO device, IotDeviceUpstreamAbstractReqDTO reqDTO) { |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java
| 1 | package cn.iocoder.yudao.module.iot.util; | 1 | package cn.iocoder.yudao.module.iot.util; |
| 2 | 2 | ||
| 3 | +import cn.hutool.crypto.digest.HMac; | ||
| 4 | +import cn.hutool.crypto.digest.HmacAlgorithm; | ||
| 5 | +import lombok.AllArgsConstructor; | ||
| 3 | import lombok.Getter; | 6 | import lombok.Getter; |
| 4 | 7 | ||
| 5 | -import javax.crypto.Mac; | ||
| 6 | -import javax.crypto.spec.SecretKeySpec; | ||
| 7 | import java.nio.charset.StandardCharsets; | 8 | import java.nio.charset.StandardCharsets; |
| 8 | 9 | ||
| 9 | /** | 10 | /** |
| @@ -13,10 +14,6 @@ import java.nio.charset.StandardCharsets; | @@ -13,10 +14,6 @@ import java.nio.charset.StandardCharsets; | ||
| 13 | */ | 14 | */ |
| 14 | public class MqttSignUtils { | 15 | public class MqttSignUtils { |
| 15 | 16 | ||
| 16 | - private static final String SIGN_METHOD = "hmacsha256"; | ||
| 17 | - | ||
| 18 | - // TODO @haohao:calculate 方法,可以融合么? | ||
| 19 | - | ||
| 20 | /** | 17 | /** |
| 21 | * 计算 MQTT 连接参数 | 18 | * 计算 MQTT 连接参数 |
| 22 | * | 19 | * |
| @@ -26,14 +23,7 @@ public class MqttSignUtils { | @@ -26,14 +23,7 @@ public class MqttSignUtils { | ||
| 26 | * @return 包含 clientId, username, password 的结果对象 | 23 | * @return 包含 clientId, username, password 的结果对象 |
| 27 | */ | 24 | */ |
| 28 | public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) { | 25 | public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) { |
| 29 | - String clientId = productKey + "." + deviceName; | ||
| 30 | - String username = deviceName + "&" + productKey; | ||
| 31 | - // 生成 password | ||
| 32 | - // TODO @haohao:signContent 和 signContentBuilder 风格保持统一的实现哈 | ||
| 33 | - String signContent = String.format("clientId%sdeviceName%sdeviceSecret%sproductKey%s", | ||
| 34 | - clientId, deviceName, deviceSecret, productKey); | ||
| 35 | - String password = sign(signContent, deviceSecret); | ||
| 36 | - return new MqttSignResult(clientId, username, password); | 26 | + return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName); |
| 37 | } | 27 | } |
| 38 | 28 | ||
| 39 | /** | 29 | /** |
| @@ -47,56 +37,31 @@ public class MqttSignUtils { | @@ -47,56 +37,31 @@ public class MqttSignUtils { | ||
| 47 | */ | 37 | */ |
| 48 | public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) { | 38 | public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) { |
| 49 | String username = deviceName + "&" + productKey; | 39 | String username = deviceName + "&" + productKey; |
| 50 | - String signContentBuilder = "clientId" + clientId + | ||
| 51 | - "deviceName" + deviceName + | ||
| 52 | - "deviceSecret" + deviceSecret + | ||
| 53 | - "productKey" + productKey; | 40 | + // 构建签名内容 |
| 41 | + StringBuilder signContentBuilder = new StringBuilder() | ||
| 42 | + .append("clientId").append(clientId) | ||
| 43 | + .append("deviceName").append(deviceName) | ||
| 44 | + .append("deviceSecret").append(deviceSecret) | ||
| 45 | + .append("productKey").append(productKey); | ||
| 54 | 46 | ||
| 55 | - String password = sign(signContentBuilder, deviceSecret); | 47 | + // 使用 HMac 计算签名 |
| 48 | + byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8); | ||
| 49 | + String signContent = signContentBuilder.toString(); | ||
| 50 | + HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key); | ||
| 51 | + String password = mac.digestHex(signContent); | ||
| 56 | 52 | ||
| 57 | return new MqttSignResult(clientId, username, password); | 53 | return new MqttSignResult(clientId, username, password); |
| 58 | } | 54 | } |
| 59 | 55 | ||
| 60 | - // TODO @haohao:hutool 貌似有工具类可以用哈。 | ||
| 61 | - private static String sign(String content, String key) { | ||
| 62 | - try { | ||
| 63 | - Mac mac = Mac.getInstance(SIGN_METHOD); | ||
| 64 | - mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), SIGN_METHOD)); | ||
| 65 | - byte[] signData = mac.doFinal(content.getBytes(StandardCharsets.UTF_8)); | ||
| 66 | - return bytesToHex(signData); | ||
| 67 | - } catch (Exception e) { | ||
| 68 | - throw new RuntimeException("Failed to sign content with HmacSHA256", e); | ||
| 69 | - } | ||
| 70 | - } | ||
| 71 | - | ||
| 72 | - private static String bytesToHex(byte[] bytes) { | ||
| 73 | - StringBuilder hexString = new StringBuilder(bytes.length * 2); | ||
| 74 | - for (byte b : bytes) { | ||
| 75 | - String hex = Integer.toHexString(0xFF & b); | ||
| 76 | - if (hex.length() == 1) { | ||
| 77 | - hexString.append('0'); | ||
| 78 | - } | ||
| 79 | - hexString.append(hex); | ||
| 80 | - } | ||
| 81 | - return hexString.toString(); | ||
| 82 | - } | ||
| 83 | - | ||
| 84 | /** | 56 | /** |
| 85 | * MQTT 签名结果类 | 57 | * MQTT 签名结果类 |
| 86 | */ | 58 | */ |
| 87 | @Getter | 59 | @Getter |
| 88 | - // TODO @haohao:可以用 lombok 哈 | 60 | + @AllArgsConstructor |
| 89 | public static class MqttSignResult { | 61 | public static class MqttSignResult { |
| 90 | - | ||
| 91 | private final String clientId; | 62 | private final String clientId; |
| 92 | private final String username; | 63 | private final String username; |
| 93 | private final String password; | 64 | private final String password; |
| 94 | - | ||
| 95 | - public MqttSignResult(String clientId, String username, String password) { | ||
| 96 | - this.clientId = clientId; | ||
| 97 | - this.username = username; | ||
| 98 | - this.password = password; | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | } | 65 | } |
| 66 | + | ||
| 102 | } | 67 | } |
| 103 | \ No newline at end of file | 68 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml
| @@ -160,5 +160,9 @@ | @@ -160,5 +160,9 @@ | ||
| 160 | <groupId>io.vertx</groupId> | 160 | <groupId>io.vertx</groupId> |
| 161 | <artifactId>vertx-web</artifactId> | 161 | <artifactId>vertx-web</artifactId> |
| 162 | </dependency> | 162 | </dependency> |
| 163 | + <dependency> | ||
| 164 | + <groupId>io.vertx</groupId> | ||
| 165 | + <artifactId>vertx-mqtt</artifactId> | ||
| 166 | + </dependency> | ||
| 163 | </dependencies> | 167 | </dependencies> |
| 164 | </project> | 168 | </project> |
| 165 | \ No newline at end of file | 169 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java
| @@ -17,22 +17,29 @@ public class IotPluginEmqxProperties { | @@ -17,22 +17,29 @@ public class IotPluginEmqxProperties { | ||
| 17 | /** | 17 | /** |
| 18 | * 服务主机 | 18 | * 服务主机 |
| 19 | */ | 19 | */ |
| 20 | - private String host; | ||
| 21 | - | 20 | + private String mqttHost; |
| 22 | /** | 21 | /** |
| 23 | * 服务端口 | 22 | * 服务端口 |
| 24 | */ | 23 | */ |
| 25 | - private int port; | 24 | + private int mqttPort; |
| 25 | + /** | ||
| 26 | + * 服务用户名 | ||
| 27 | + */ | ||
| 28 | + private String mqttUsername; | ||
| 26 | 29 | ||
| 27 | /** | 30 | /** |
| 31 | + * 服务密码 | ||
| 32 | + */ | ||
| 33 | + private String mqttPassword; | ||
| 34 | + /** | ||
| 28 | * 是否启用 SSL | 35 | * 是否启用 SSL |
| 29 | */ | 36 | */ |
| 30 | - private boolean ssl; | 37 | + private boolean mqttSsl; |
| 31 | 38 | ||
| 32 | /** | 39 | /** |
| 33 | * 订阅的主题 | 40 | * 订阅的主题 |
| 34 | */ | 41 | */ |
| 35 | - private String topics; | 42 | + private String mqttTopics; |
| 36 | 43 | ||
| 37 | /** | 44 | /** |
| 38 | * 认证端口 | 45 | * 认证端口 |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java
| @@ -7,8 +7,6 @@ import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamH | @@ -7,8 +7,6 @@ import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamH | ||
| 7 | /** | 7 | /** |
| 8 | * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类 | 8 | * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类 |
| 9 | * <p> | 9 | * <p> |
| 10 | - * 但是:由于设备通过 HTTP 短链接接入,导致其实无法下行指导给 device 设备,所以基本都是直接返回失败!!! | ||
| 11 | - * 类似 MQTT、WebSocket、TCP 插件,是可以实现下行指令的。 | ||
| 12 | * | 10 | * |
| 13 | * @author 芋道源码 | 11 | * @author 芋道源码 |
| 14 | */ | 12 | */ |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java
| 1 | package cn.iocoder.yudao.module.iot.plugin.emqx.upstream; | 1 | package cn.iocoder.yudao.module.iot.plugin.emqx.upstream; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.date.DateUtil; | ||
| 4 | +import cn.hutool.json.JSONObject; | ||
| 5 | +import cn.hutool.json.JSONUtil; | ||
| 3 | import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; | 6 | import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; |
| 7 | +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; | ||
| 4 | import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; | 8 | import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; |
| 5 | import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; | 9 | import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; |
| 10 | +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; | ||
| 6 | import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties; | 11 | import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties; |
| 7 | import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler; | 12 | import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler; |
| 8 | import io.vertx.core.Vertx; | 13 | import io.vertx.core.Vertx; |
| 9 | import io.vertx.core.http.HttpServer; | 14 | import io.vertx.core.http.HttpServer; |
| 10 | import io.vertx.ext.web.Router; | 15 | import io.vertx.ext.web.Router; |
| 11 | import io.vertx.ext.web.handler.BodyHandler; | 16 | import io.vertx.ext.web.handler.BodyHandler; |
| 17 | +import io.vertx.mqtt.MqttClient; | ||
| 18 | +import io.vertx.mqtt.MqttClientOptions; | ||
| 12 | import lombok.extern.slf4j.Slf4j; | 19 | import lombok.extern.slf4j.Slf4j; |
| 13 | 20 | ||
| 21 | +import java.time.LocalDateTime; | ||
| 22 | +import java.util.UUID; | ||
| 23 | + | ||
| 14 | /** | 24 | /** |
| 15 | * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 | 25 | * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 |
| 16 | * <p> | 26 | * <p> |
| 17 | - * 协议:HTTP | 27 | + * 协议:HTTP、MQTT |
| 18 | * | 28 | * |
| 19 | * @author haohao | 29 | * @author haohao |
| 20 | */ | 30 | */ |
| @@ -23,13 +33,16 @@ public class IotDeviceUpstreamServer { | @@ -23,13 +33,16 @@ public class IotDeviceUpstreamServer { | ||
| 23 | 33 | ||
| 24 | private final Vertx vertx; | 34 | private final Vertx vertx; |
| 25 | private final HttpServer server; | 35 | private final HttpServer server; |
| 36 | + private final MqttClient client; | ||
| 26 | private final IotPluginEmqxProperties emqxProperties; | 37 | private final IotPluginEmqxProperties emqxProperties; |
| 38 | + private final IotDeviceUpstreamApi deviceUpstreamApi; | ||
| 27 | 39 | ||
| 28 | public IotDeviceUpstreamServer(IotPluginCommonProperties commonProperties, | 40 | public IotDeviceUpstreamServer(IotPluginCommonProperties commonProperties, |
| 29 | IotPluginEmqxProperties emqxProperties, | 41 | IotPluginEmqxProperties emqxProperties, |
| 30 | IotDeviceUpstreamApi deviceUpstreamApi, | 42 | IotDeviceUpstreamApi deviceUpstreamApi, |
| 31 | IotDeviceDownstreamServer deviceDownstreamServer) { | 43 | IotDeviceDownstreamServer deviceDownstreamServer) { |
| 32 | this.emqxProperties = emqxProperties; | 44 | this.emqxProperties = emqxProperties; |
| 45 | + this.deviceUpstreamApi = deviceUpstreamApi; | ||
| 33 | // 创建 Vertx 实例 | 46 | // 创建 Vertx 实例 |
| 34 | this.vertx = Vertx.vertx(); | 47 | this.vertx = Vertx.vertx(); |
| 35 | // 创建 Router 实例 | 48 | // 创建 Router 实例 |
| @@ -39,18 +52,104 @@ public class IotDeviceUpstreamServer { | @@ -39,18 +52,104 @@ public class IotDeviceUpstreamServer { | ||
| 39 | .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi)); | 52 | .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi)); |
| 40 | // 创建 HttpServer 实例 | 53 | // 创建 HttpServer 实例 |
| 41 | this.server = vertx.createHttpServer().requestHandler(router); | 54 | this.server = vertx.createHttpServer().requestHandler(router); |
| 55 | + | ||
| 56 | + // 创建 MQTT 客户端 | ||
| 57 | + MqttClientOptions options = new MqttClientOptions() | ||
| 58 | + .setClientId("yudao-iot-server-" + UUID.randomUUID()) | ||
| 59 | + .setUsername(emqxProperties.getMqttUsername()) | ||
| 60 | + .setPassword(emqxProperties.getMqttPassword()) | ||
| 61 | + .setSsl(emqxProperties.isMqttSsl()); | ||
| 62 | + client = MqttClient.create(vertx, options); | ||
| 42 | } | 63 | } |
| 43 | 64 | ||
| 44 | /** | 65 | /** |
| 45 | - * 启动 HTTP 服务器 | 66 | + * 启动 HTTP 服务器、MQTT 客户端 |
| 46 | */ | 67 | */ |
| 47 | public void start() { | 68 | public void start() { |
| 69 | + // 1. 启动 HTTP 服务器 | ||
| 48 | log.info("[start][开始启动]"); | 70 | log.info("[start][开始启动]"); |
| 49 | server.listen(emqxProperties.getAuthPort()) | 71 | server.listen(emqxProperties.getAuthPort()) |
| 50 | .toCompletionStage() | 72 | .toCompletionStage() |
| 51 | .toCompletableFuture() | 73 | .toCompletableFuture() |
| 52 | .join(); | 74 | .join(); |
| 53 | - log.info("[start][启动完成,端口({})]", this.server.actualPort()); | 75 | + log.info("[start][HTTP服务器启动完成,端口({})]", this.server.actualPort()); |
| 76 | + | ||
| 77 | + // 2. 连接 MQTT Broker | ||
| 78 | + connectMqtt(); | ||
| 79 | + | ||
| 80 | + // 3. 添加 MQTT 断开重连监听器 | ||
| 81 | + client.closeHandler(v -> { | ||
| 82 | + log.warn("[closeHandler][MQTT 连接已断开,准备重连]"); | ||
| 83 | + // 等待 5 秒后重连,避免频繁重连 | ||
| 84 | + vertx.setTimer(5000, id -> { | ||
| 85 | + log.info("[closeHandler][开始重新连接 MQTT]"); | ||
| 86 | + connectMqtt(); | ||
| 87 | + }); | ||
| 88 | + }); | ||
| 89 | + | ||
| 90 | + // 4. 设置 MQTT 消息处理器 | ||
| 91 | + client.publishHandler(message -> { | ||
| 92 | + String topic = message.topicName(); | ||
| 93 | + String payload = message.payload().toString(); | ||
| 94 | + log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload); | ||
| 95 | + | ||
| 96 | + try { | ||
| 97 | + // 4.1 处理设备属性上报消息: /{productKey}/{deviceName}/event/property/post | ||
| 98 | + if (topic.contains("/event/property/post")) { | ||
| 99 | + // 4.2 解析消息内容 | ||
| 100 | + JSONObject jsonObject = JSONUtil.parseObj(payload); | ||
| 101 | + String requestId = jsonObject.getStr("id"); | ||
| 102 | + Long timestamp = jsonObject.getLong("timestamp"); | ||
| 103 | + | ||
| 104 | + // 4.3 从 topic 中解析设备标识 | ||
| 105 | + String[] topicParts = topic.split("/"); | ||
| 106 | + String productKey = topicParts[1]; | ||
| 107 | + String deviceName = topicParts[2]; | ||
| 108 | + | ||
| 109 | + // 4.4 构建设备属性上报请求对象 | ||
| 110 | + IotDevicePropertyReportReqDTO devicePropertyReportReqDTO = ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO() | ||
| 111 | + .setRequestId(requestId) | ||
| 112 | + .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) | ||
| 113 | + .setProductKey(productKey).setDeviceName(deviceName)) | ||
| 114 | + .setProperties(jsonObject.getJSONObject("params")); | ||
| 115 | + | ||
| 116 | + // 4.5 调用上游 API 处理设备上报数据 | ||
| 117 | + deviceUpstreamApi.reportDeviceProperty(devicePropertyReportReqDTO); | ||
| 118 | + log.info("[messageHandler][处理设备上行消息成功][topic: {}][devicePropertyReportReqDTO: {}]", | ||
| 119 | + topic, JSONUtil.toJsonStr(devicePropertyReportReqDTO)); | ||
| 120 | + } | ||
| 121 | + } catch (Exception e) { | ||
| 122 | + log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e); | ||
| 123 | + } | ||
| 124 | + }); | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + /** | ||
| 128 | + * 连接 MQTT Broker 并订阅主题 | ||
| 129 | + */ | ||
| 130 | + private void connectMqtt() { | ||
| 131 | + // 连接 MQTT Broker | ||
| 132 | + client.connect(emqxProperties.getMqttPort(), emqxProperties.getMqttHost()) | ||
| 133 | + .onSuccess(connAck -> { | ||
| 134 | + log.info("[connectMqtt][MQTT客户端连接成功]"); | ||
| 135 | + // 连接成功后订阅主题 | ||
| 136 | + String mqttTopics = emqxProperties.getMqttTopics(); | ||
| 137 | + String[] topics = mqttTopics.split(","); | ||
| 138 | + for (String topic : topics) { | ||
| 139 | + client.subscribe(topic, 1) | ||
| 140 | + .onSuccess(v -> log.info("[connectMqtt][成功订阅主题: {}]", topic)) | ||
| 141 | + .onFailure(err -> log.error("[connectMqtt][订阅主题失败: {}]", topic, err)); | ||
| 142 | + } | ||
| 143 | + log.info("[connectMqtt][开始订阅设备上行消息主题]"); | ||
| 144 | + }) | ||
| 145 | + .onFailure(err -> { | ||
| 146 | + log.error("[connectMqtt][连接 MQTT Broker 失败]", err); | ||
| 147 | + // 连接失败后,等待 5 秒重试 | ||
| 148 | + vertx.setTimer(5000, id -> { | ||
| 149 | + log.info("[connectMqtt][准备重新连接 MQTT]"); | ||
| 150 | + connectMqtt(); | ||
| 151 | + }); | ||
| 152 | + }); | ||
| 54 | } | 153 | } |
| 55 | 154 | ||
| 56 | /** | 155 | /** |
| @@ -67,6 +166,14 @@ public class IotDeviceUpstreamServer { | @@ -67,6 +166,14 @@ public class IotDeviceUpstreamServer { | ||
| 67 | .join(); | 166 | .join(); |
| 68 | } | 167 | } |
| 69 | 168 | ||
| 169 | + // 关闭 MQTT 客户端 | ||
| 170 | + if (client != null) { | ||
| 171 | + client.disconnect() | ||
| 172 | + .toCompletionStage() | ||
| 173 | + .toCompletableFuture() | ||
| 174 | + .join(); | ||
| 175 | + } | ||
| 176 | + | ||
| 70 | // 关闭 Vertx 实例 | 177 | // 关闭 Vertx 实例 |
| 71 | if (vertx != null) { | 178 | if (vertx != null) { |
| 72 | vertx.close() | 179 | vertx.close() |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml
| @@ -10,8 +10,10 @@ yudao: | @@ -10,8 +10,10 @@ yudao: | ||
| 10 | downstream-port: 8100 | 10 | downstream-port: 8100 |
| 11 | plugin-key: yudao-module-iot-plugin-emqx | 11 | plugin-key: yudao-module-iot-plugin-emqx |
| 12 | emqx: | 12 | emqx: |
| 13 | - host: 127.0.0.1 | ||
| 14 | - port: 1883 | ||
| 15 | - ssl: false | ||
| 16 | - topics: "/sys/#" | 13 | + mqtt-host: 127.0.0.1 |
| 14 | + mqtt-port: 1883 | ||
| 15 | + mqtt-ssl: false | ||
| 16 | + mqtt-username: yudao | ||
| 17 | + mqtt-password: yudao | ||
| 18 | + mqtt-topics: "/+/#" | ||
| 17 | auth-port: 8101 | 19 | auth-port: 8101 |