【同步】BOOT 和 CLOUD 的功能
This commit is contained in:
@@ -75,6 +75,8 @@
|
||||
<netty.version>4.2.9.Final</netty.version>
|
||||
<mqtt.version>1.2.5</mqtt.version>
|
||||
<vertx.version>4.5.22</vertx.version>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<californium.version>3.12.0</californium.version>
|
||||
<!-- 三方云服务相关 -->
|
||||
<awssdk.version>2.40.15</awssdk.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
@@ -348,7 +350,6 @@
|
||||
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
@@ -611,6 +612,50 @@
|
||||
<version>${reflections.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.x -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-core</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MQTT -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>${mqtt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OkHttp -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- CoAP - Eclipse Californium -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.californium</groupId>
|
||||
<artifactId>californium-core</artifactId>
|
||||
<version>${californium.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
@@ -618,6 +663,24 @@
|
||||
<version>${awssdk.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
|
||||
<version>${justauth.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xkcoding.justauth</groupId>
|
||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||
<version>${justauth-starter.version}</version>
|
||||
<exclusions>
|
||||
<!-- 移除,避免和项目里的 hutool-all 冲突 -->
|
||||
<exclusion>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
@@ -646,17 +709,6 @@
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
|
||||
<version>${justauth.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xkcoding.justauth</groupId>
|
||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||
<version>${justauth-starter.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 积木报表-->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.jimureport</groupId>
|
||||
@@ -678,30 +730,6 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.x -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-core</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MQTT -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>${mqtt.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -65,4 +66,47 @@ public class MapUtils {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中获取 BigDecimal 值
|
||||
*
|
||||
* @param map Map 数据源
|
||||
* @param key 键名
|
||||
* @return BigDecimal 值,解析失败或值为 null 时返回 null
|
||||
*/
|
||||
public static BigDecimal getBigDecimal(Map<String, ?> map, String key) {
|
||||
return getBigDecimal(map, key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中获取 BigDecimal 值
|
||||
*
|
||||
* @param map Map 数据源
|
||||
* @param key 键名
|
||||
* @param defaultValue 默认值
|
||||
* @return BigDecimal 值,解析失败或值为 null 时返回默认值
|
||||
*/
|
||||
public static BigDecimal getBigDecimal(Map<String, ?> map, String key, BigDecimal defaultValue) {
|
||||
if (map == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof BigDecimal) {
|
||||
return (BigDecimal) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return BigDecimal.valueOf(((Number) value).doubleValue());
|
||||
}
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
return new BigDecimal((String) value);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -229,4 +229,53 @@ public class JsonUtils {
|
||||
return JSONUtil.isTypeJSONObject(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Object 转换为目标类型
|
||||
* <p>
|
||||
* 避免先转 jsonString 再 parseObject 的性能损耗
|
||||
*
|
||||
* @param obj 源对象(可以是 Map、POJO 等)
|
||||
* @param clazz 目标类型
|
||||
* @return 转换后的对象
|
||||
*/
|
||||
public static <T> T convertObject(Object obj, Class<T> clazz) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
if (clazz.isInstance(obj)) {
|
||||
return clazz.cast(obj);
|
||||
}
|
||||
return objectMapper.convertValue(obj, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Object 转换为目标类型(支持泛型)
|
||||
*
|
||||
* @param obj 源对象
|
||||
* @param typeReference 目标类型引用
|
||||
* @return 转换后的对象
|
||||
*/
|
||||
public static <T> T convertObject(Object obj, TypeReference<T> typeReference) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.convertValue(obj, typeReference);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Object 转换为 List 类型
|
||||
* <p>
|
||||
* 避免先转 jsonString 再 parseArray 的性能损耗
|
||||
*
|
||||
* @param obj 源对象(可以是 List、数组等)
|
||||
* @param clazz 目标元素类型
|
||||
* @return 转换后的 List
|
||||
*/
|
||||
public static <T> List<T> convertList(Object obj, Class<T> clazz) {
|
||||
if (obj == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.function.Consumer;
|
||||
* <p>
|
||||
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
||||
* 2. SFunction<S, ?> column + <S> 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
*/
|
||||
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||
@@ -122,6 +123,12 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> MPJLambdaWrapperX<T> orderByAsc(SFunction<X, ?> column) {
|
||||
super.orderByAsc(true, column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MPJLambdaWrapperX<T> last(String lastSql) {
|
||||
super.last(lastSql);
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.AsyncListenableTaskExecutor;
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.List;
|
||||
@@ -30,12 +30,12 @@ public class BpmFlowableConfiguration {
|
||||
|
||||
/**
|
||||
* 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
|
||||
*
|
||||
* <p>
|
||||
* 如果不创建,会导致项目启动时,Flowable 报错的问题
|
||||
*/
|
||||
@Bean(name = "applicationTaskExecutor")
|
||||
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
|
||||
public AsyncListenableTaskExecutor taskExecutor() {
|
||||
public AsyncTaskExecutor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(8);
|
||||
executor.setMaxPoolSize(8);
|
||||
|
||||
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
|
||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
|
||||
import lombok.Setter;
|
||||
import org.flowable.bpmn.model.*;
|
||||
import org.flowable.common.engine.api.delegate.Expression;
|
||||
import org.flowable.engine.delegate.DelegateExecution;
|
||||
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
|
||||
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
|
||||
@@ -90,4 +91,21 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
|
||||
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
|
||||
}
|
||||
|
||||
// ========== 屏蔽解析器覆写 ==========
|
||||
|
||||
@Override
|
||||
public void setCollectionExpression(Expression collectionExpression) {
|
||||
// 保持自定义变量名,忽略解析器写入的 collection 表达式
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCollectionVariable(String collectionVariable) {
|
||||
// 保持自定义变量名,忽略解析器写入的 collection 变量名
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCollectionElementVariable(String collectionElementVariable) {
|
||||
// 保持自定义变量名,忽略解析器写入的单元素变量名
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public class ApiAccessLogRespVO {
|
||||
|
||||
@Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@ExcelProperty(value = "操作分类", converter = DictConvert.class)
|
||||
@DictFormat(cn.iocoder.yudao.module.infra.enums.DictTypeConstants.OPERATE_TYPE)
|
||||
@DictFormat(DictTypeConstants.OPERATE_TYPE)
|
||||
private Integer operateType;
|
||||
|
||||
@Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
|
||||
@@ -26,13 +26,26 @@ public interface ErrorCodeConstants {
|
||||
// ========== 设备 1-050-003-000 ============
|
||||
ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
|
||||
ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
|
||||
ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除");
|
||||
ErrorCode DEVICE_GATEWAY_HAS_SUB = new ErrorCode(1_050_003_002, "网关设备存在已绑定的子设备,不允许删除");
|
||||
ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在");
|
||||
ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在");
|
||||
ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备");
|
||||
ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!");
|
||||
ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关");
|
||||
ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
|
||||
ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
|
||||
ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑");
|
||||
// 拓扑管理相关错误码 1-050-003-100
|
||||
ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效");
|
||||
ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
|
||||
ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
|
||||
ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
|
||||
// 设备注册相关错误码 1-050-003-200
|
||||
ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
|
||||
ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
|
||||
ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
|
||||
ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
|
||||
ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
|
||||
|
||||
// ========== 产品分类 1-050-004-000 ==========
|
||||
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.enums.device;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
// TODO @芋艿:需要添加对应的 DTO,以及上下行的链路,网关、网关服务、设备等
|
||||
/**
|
||||
* IoT 设备消息标识符枚举
|
||||
*/
|
||||
@Deprecated
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum IotDeviceMessageIdentifierEnum {
|
||||
|
||||
PROPERTY_GET("get"), // 下行
|
||||
PROPERTY_SET("set"), // 下行
|
||||
PROPERTY_REPORT("report"), // 上行
|
||||
|
||||
STATE_ONLINE("online"), // 上行
|
||||
STATE_OFFLINE("offline"), // 上行
|
||||
|
||||
CONFIG_GET("get"), // 上行 TODO 芋艿:【讨论】暂时没有上行的场景
|
||||
CONFIG_SET("set"), // 下行
|
||||
|
||||
SERVICE_INVOKE("${identifier}"), // 下行
|
||||
SERVICE_REPLY_SUFFIX("_reply"), // 芋艿:TODO 芋艿:【讨论】上行 or 下行
|
||||
|
||||
OTA_UPGRADE("upgrade"), // 下行
|
||||
OTA_PULL("pull"), // 上行
|
||||
OTA_PROGRESS("progress"), // 上行
|
||||
OTA_REPORT("report"), // 上行
|
||||
|
||||
REGISTER_REGISTER("register"), // 上行
|
||||
REGISTER_REGISTER_SUB("register_sub"), // 上行
|
||||
REGISTER_UNREGISTER_SUB("unregister_sub"), // 下行
|
||||
|
||||
TOPOLOGY_ADD("topology_add"), // 下行;
|
||||
;
|
||||
|
||||
/**
|
||||
* 标志符
|
||||
*/
|
||||
private final String identifier;
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.enums.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT 设备消息类型枚举
|
||||
*/
|
||||
@Deprecated
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum IotDeviceMessageTypeEnum implements ArrayValuable<String> {
|
||||
|
||||
STATE("state"), // 设备状态
|
||||
PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
|
||||
EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
|
||||
SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
|
||||
CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置
|
||||
OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级
|
||||
REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册
|
||||
TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);
|
||||
|
||||
/**
|
||||
* 属性
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
@Override
|
||||
public String[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.enums.product;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT 定位方式枚举类
|
||||
*
|
||||
* @author alwayssuper
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum IotLocationTypeEnum implements ArrayValuable<Integer> {
|
||||
|
||||
IP(1, "IP 定位"),
|
||||
DEVICE(2, "设备上报"),
|
||||
MANUAL(3, "手动定位");
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotLocationTypeEnum::getType).toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer type;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备通用 API
|
||||
@@ -28,4 +34,20 @@ public interface IotDeviceCommonApi {
|
||||
*/
|
||||
CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO);
|
||||
|
||||
/**
|
||||
* 直连/网关设备动态注册(一型一密)
|
||||
*
|
||||
* @param reqDTO 动态注册请求
|
||||
* @return 注册结果(包含 DeviceSecret)
|
||||
*/
|
||||
CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 网关子设备动态注册(网关代理转发)
|
||||
*
|
||||
* @param reqDTO 子设备注册请求(包含网关标识和子设备列表)
|
||||
* @return 注册结果列表
|
||||
*/
|
||||
CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package cn.iocoder.yudao.module.iot.core.biz.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 设备认证 Request DTO
|
||||
@@ -9,6 +11,8 @@ import lombok.Data;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceAuthReqDTO {
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.module.iot.core.biz.dto;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 子设备动态注册 Request DTO
|
||||
* <p>
|
||||
* 额外包含了网关设备的标识信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class IotSubDeviceRegisterFullReqDTO {
|
||||
|
||||
/**
|
||||
* 网关设备 ProductKey
|
||||
*/
|
||||
@NotEmpty(message = "网关产品标识不能为空")
|
||||
private String gatewayProductKey;
|
||||
|
||||
/**
|
||||
* 网关设备 DeviceName
|
||||
*/
|
||||
@NotEmpty(message = "网关设备名称不能为空")
|
||||
private String gatewayDeviceName;
|
||||
|
||||
/**
|
||||
* 子设备注册列表
|
||||
*/
|
||||
@NotNull(message = "子设备注册列表不能为空")
|
||||
private List<IotSubDeviceRegisterReqDTO> subDevices;
|
||||
|
||||
}
|
||||
@@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
|
||||
|
||||
// TODO 芋艿:要不要加个 ping 消息;
|
||||
|
||||
// ========== 拓扑管理 ==========
|
||||
// 可参考:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships
|
||||
|
||||
TOPO_ADD("thing.topo.add", "添加拓扑关系", true),
|
||||
TOPO_DELETE("thing.topo.delete", "删除拓扑关系", true),
|
||||
TOPO_GET("thing.topo.get", "获取拓扑关系", true),
|
||||
TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
|
||||
|
||||
// ========== 设备注册 ==========
|
||||
// 可参考:https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
|
||||
|
||||
DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
|
||||
SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
|
||||
|
||||
// ========== 设备属性 ==========
|
||||
// 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
|
||||
|
||||
PROPERTY_POST("thing.property.post", "属性上报", true),
|
||||
PROPERTY_SET("thing.property.set", "属性设置", false),
|
||||
|
||||
PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
|
||||
|
||||
// ========== 设备事件 ==========
|
||||
// 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
|
||||
|
||||
@@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
|
||||
|
||||
OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
|
||||
OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
|
||||
|
||||
;
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.core.enums;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT 设备消息类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum IotDeviceMessageTypeEnum implements ArrayValuable<String> {
|
||||
|
||||
STATE("state"), // 设备状态
|
||||
// PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
|
||||
EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
|
||||
SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
|
||||
CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置
|
||||
OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级
|
||||
REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册
|
||||
TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑
|
||||
|
||||
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);
|
||||
|
||||
/**
|
||||
* 属性
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
@Override
|
||||
public String[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 设备标识
|
||||
*
|
||||
* 用于标识一个设备的基本信息(productKey + deviceName)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceIdentity {
|
||||
|
||||
/**
|
||||
* 产品标识
|
||||
*/
|
||||
@NotEmpty(message = "产品标识不能为空")
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
@NotEmpty(message = "设备名称不能为空")
|
||||
private String deviceName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 设备动态注册 Request DTO
|
||||
* <p>
|
||||
* 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotDeviceRegisterReqDTO {
|
||||
|
||||
/**
|
||||
* 产品标识
|
||||
*/
|
||||
@NotEmpty(message = "产品标识不能为空")
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
@NotEmpty(message = "设备名称不能为空")
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 产品密钥
|
||||
*/
|
||||
@NotEmpty(message = "产品密钥不能为空")
|
||||
private String productSecret;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 设备动态注册 Response DTO
|
||||
* <p>
|
||||
* 用于直连设备/网关的一型一密动态注册响应
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceRegisterRespDTO {
|
||||
|
||||
/**
|
||||
* 产品标识
|
||||
*/
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 设备密钥
|
||||
*/
|
||||
private String deviceSecret;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 子设备动态注册 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.auth.register.sub 消息的 params 数组元素
|
||||
*
|
||||
* 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotSubDeviceRegisterReqDTO {
|
||||
|
||||
/**
|
||||
* 子设备 ProductKey
|
||||
*/
|
||||
@NotEmpty(message = "产品标识不能为空")
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 子设备 DeviceName
|
||||
*/
|
||||
@NotEmpty(message = "设备名称不能为空")
|
||||
private String deviceName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.auth;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 子设备动态注册 Response DTO
|
||||
* <p>
|
||||
* 用于 thing.auth.register.sub 响应的设备信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotSubDeviceRegisterRespDTO {
|
||||
|
||||
/**
|
||||
* 子设备 ProductKey
|
||||
*/
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 子设备 DeviceName
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 分配的 DeviceSecret
|
||||
*/
|
||||
private String deviceSecret;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.event;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 设备事件上报 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.event.post 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotDeviceEventPostReqDTO {
|
||||
|
||||
/**
|
||||
* 事件标识符
|
||||
*/
|
||||
private String identifier;
|
||||
|
||||
/**
|
||||
* 事件输出参数
|
||||
*/
|
||||
private Object value;
|
||||
|
||||
/**
|
||||
* 上报时间(毫秒时间戳,可选)
|
||||
*/
|
||||
private Long time;
|
||||
|
||||
/**
|
||||
* 创建事件上报 DTO
|
||||
*
|
||||
* @param identifier 事件标识符
|
||||
* @param value 事件值
|
||||
* @return DTO 对象
|
||||
*/
|
||||
public static IotDeviceEventPostReqDTO of(String identifier, Object value) {
|
||||
return of(identifier, value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建事件上报 DTO(带时间)
|
||||
*
|
||||
* @param identifier 事件标识符
|
||||
* @param value 事件值
|
||||
* @param time 上报时间
|
||||
* @return DTO 对象
|
||||
*/
|
||||
public static IotDeviceEventPostReqDTO of(String identifier, Object value, Long time) {
|
||||
return new IotDeviceEventPostReqDTO().setIdentifier(identifier).setValue(value).setTime(time);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* IoT Topic 消息体 DTO 定义
|
||||
* <p>
|
||||
* 定义设备与平台通信的消息体结构,遵循(参考)阿里云 Alink 协议规范
|
||||
*
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/alink-protocol-1">阿里云 Alink 协议</a>
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.core.topic;
|
||||
@@ -0,0 +1,88 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.property;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 设备属性批量上报 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.event.property.pack.post 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotDevicePropertyPackPostReqDTO {
|
||||
|
||||
/**
|
||||
* 网关自身属性
|
||||
* <p>
|
||||
* key: 属性标识符
|
||||
* value: 属性值
|
||||
*/
|
||||
private Map<String, Object> properties;
|
||||
|
||||
/**
|
||||
* 网关自身事件
|
||||
* <p>
|
||||
* key: 事件标识符
|
||||
* value: 事件值对象(包含 value 和 time)
|
||||
*/
|
||||
private Map<String, EventValue> events;
|
||||
|
||||
/**
|
||||
* 子设备数据列表
|
||||
*/
|
||||
private List<SubDeviceData> subDevices;
|
||||
|
||||
/**
|
||||
* 事件值对象
|
||||
*/
|
||||
@Data
|
||||
public static class EventValue {
|
||||
|
||||
/**
|
||||
* 事件参数
|
||||
*/
|
||||
private Object value;
|
||||
|
||||
/**
|
||||
* 上报时间(毫秒时间戳)
|
||||
*/
|
||||
private Long time;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 子设备数据
|
||||
*/
|
||||
@Data
|
||||
public static class SubDeviceData {
|
||||
|
||||
/**
|
||||
* 子设备标识
|
||||
*/
|
||||
private IotDeviceIdentity identity;
|
||||
|
||||
/**
|
||||
* 子设备属性
|
||||
* <p>
|
||||
* key: 属性标识符
|
||||
* value: 属性值
|
||||
*/
|
||||
private Map<String, Object> properties;
|
||||
|
||||
/**
|
||||
* 子设备事件
|
||||
* <p>
|
||||
* key: 事件标识符
|
||||
* value: 事件值对象(包含 value 和 time)
|
||||
*/
|
||||
private Map<String, EventValue> events;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.property;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 设备属性上报 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.property.post 消息的 params 参数
|
||||
* <p>
|
||||
* 本质是一个 Map,key 为属性标识符,value 为属性值
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-attributes">阿里云 - 设备上报属性</a>
|
||||
*/
|
||||
public class IotDevicePropertyPostReqDTO extends HashMap<String, Object> {
|
||||
|
||||
public IotDevicePropertyPostReqDTO() {
|
||||
super();
|
||||
}
|
||||
|
||||
public IotDevicePropertyPostReqDTO(Map<String, Object> properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建属性上报 DTO
|
||||
*
|
||||
* @param properties 属性数据
|
||||
* @return DTO 对象
|
||||
*/
|
||||
public static IotDevicePropertyPostReqDTO of(Map<String, Object> properties) {
|
||||
return new IotDevicePropertyPostReqDTO(properties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备拓扑添加 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.add 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotDeviceTopoAddReqDTO {
|
||||
|
||||
/**
|
||||
* 子设备认证信息列表
|
||||
* <p>
|
||||
* 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password
|
||||
*/
|
||||
@NotEmpty(message = "子设备认证信息列表不能为空")
|
||||
private List<IotDeviceAuthReqDTO> subDevices;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备拓扑关系变更通知 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.change 下行消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotDeviceTopoChangeReqDTO {
|
||||
|
||||
public static final Integer STATUS_CREATE = 0;
|
||||
public static final Integer STATUS_DELETE = 1;
|
||||
|
||||
/**
|
||||
* 拓扑关系状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 子设备列表
|
||||
*/
|
||||
private List<IotDeviceIdentity> subList;
|
||||
|
||||
public static IotDeviceTopoChangeReqDTO ofCreate(List<IotDeviceIdentity> subList) {
|
||||
return new IotDeviceTopoChangeReqDTO(STATUS_CREATE, subList);
|
||||
}
|
||||
|
||||
public static IotDeviceTopoChangeReqDTO ofDelete(List<IotDeviceIdentity> subList) {
|
||||
return new IotDeviceTopoChangeReqDTO(STATUS_DELETE, subList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备拓扑删除 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.delete 消息的 params 参数
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotDeviceTopoDeleteReqDTO {
|
||||
|
||||
/**
|
||||
* 子设备标识列表
|
||||
*/
|
||||
@Valid
|
||||
@NotEmpty(message = "子设备标识列表不能为空")
|
||||
private List<IotDeviceIdentity> subDevices;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IoT 设备拓扑关系获取 Request DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotDeviceTopoGetReqDTO {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.iot.core.topic.topo;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 设备拓扑关系获取 Response DTO
|
||||
* <p>
|
||||
* 用于 thing.topo.get 响应
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
|
||||
*/
|
||||
@Data
|
||||
public class IotDeviceTopoGetRespDTO {
|
||||
|
||||
/**
|
||||
* 子设备列表
|
||||
*/
|
||||
private List<IotDeviceIdentity> subDevices;
|
||||
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.module.iot.core.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.crypto.digest.HmacAlgorithm;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
|
||||
/**
|
||||
* IoT 设备【认证】的工具类,参考阿里云
|
||||
@@ -13,73 +13,40 @@ import lombok.NoArgsConstructor;
|
||||
*/
|
||||
public class IotDeviceAuthUtils {
|
||||
|
||||
/**
|
||||
* 认证信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AuthInfo {
|
||||
|
||||
/**
|
||||
* 客户端 ID
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备信息
|
||||
*/
|
||||
@Data
|
||||
public static class DeviceInfo {
|
||||
|
||||
private String productKey;
|
||||
|
||||
private String deviceName;
|
||||
|
||||
}
|
||||
|
||||
public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) {
|
||||
public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) {
|
||||
String clientId = buildClientId(productKey, deviceName);
|
||||
String username = buildUsername(productKey, deviceName);
|
||||
String content = "clientId" + clientId +
|
||||
"deviceName" + deviceName +
|
||||
"deviceSecret" + deviceSecret +
|
||||
"productKey" + productKey;
|
||||
String password = buildPassword(deviceSecret, content);
|
||||
return new AuthInfo(clientId, username, password);
|
||||
String password = buildPassword(deviceSecret,
|
||||
buildContent(clientId, productKey, deviceName, deviceSecret));
|
||||
return new IotDeviceAuthReqDTO(clientId, username, password);
|
||||
}
|
||||
|
||||
private static String buildClientId(String productKey, String deviceName) {
|
||||
public static String buildClientId(String productKey, String deviceName) {
|
||||
return String.format("%s.%s", productKey, deviceName);
|
||||
}
|
||||
|
||||
private static String buildUsername(String productKey, String deviceName) {
|
||||
public static String buildUsername(String productKey, String deviceName) {
|
||||
return String.format("%s&%s", deviceName, productKey);
|
||||
}
|
||||
|
||||
private static String buildPassword(String deviceSecret, String content) {
|
||||
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes())
|
||||
public static String buildPassword(String deviceSecret, String content) {
|
||||
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(deviceSecret))
|
||||
.digestHex(content);
|
||||
}
|
||||
|
||||
public static DeviceInfo parseUsername(String username) {
|
||||
private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) {
|
||||
return "clientId" + clientId +
|
||||
"deviceName" + deviceName +
|
||||
"deviceSecret" + deviceSecret +
|
||||
"productKey" + productKey;
|
||||
}
|
||||
|
||||
public static IotDeviceIdentity parseUsername(String username) {
|
||||
String[] usernameParts = username.split("&");
|
||||
if (usernameParts.length != 2) {
|
||||
return null;
|
||||
}
|
||||
return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]);
|
||||
return new IotDeviceIdentity(usernameParts[1], usernameParts[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class IotDeviceMessageUtils {
|
||||
|
||||
/**
|
||||
* 判断消息中是否包含指定的标识符
|
||||
*
|
||||
* <p>
|
||||
* 对于不同消息类型的处理:
|
||||
* - EVENT_POST/SERVICE_INVOKE:检查 params.identifier 是否匹配
|
||||
* - STATE_UPDATE:检查 params.state 是否匹配
|
||||
@@ -99,6 +99,17 @@ public class IotDeviceMessageUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断消息中是否不包含指定的标识符
|
||||
*
|
||||
* @param message 消息
|
||||
* @param identifier 要检查的标识符
|
||||
* @return 是否不包含
|
||||
*/
|
||||
public static boolean notContainsIdentifier(IotDeviceMessage message, String identifier) {
|
||||
return !containsIdentifier(message, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 params 解析为 Map
|
||||
*
|
||||
@@ -144,20 +155,19 @@ public class IotDeviceMessageUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 策略1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
|
||||
// 策略 1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
|
||||
if (!(params instanceof Map)) {
|
||||
return params;
|
||||
}
|
||||
|
||||
// 策略 2:直接通过标识符获取属性值
|
||||
Map<String, Object> paramsMap = (Map<String, Object>) params;
|
||||
|
||||
// 策略2:直接通过标识符获取属性值
|
||||
Object directValue = paramsMap.get(identifier);
|
||||
if (directValue != null) {
|
||||
return directValue;
|
||||
}
|
||||
|
||||
// 策略3:从 properties 字段中获取(适用于标准属性上报消息)
|
||||
// 策略 3:从 properties 字段中获取(适用于标准属性上报消息)
|
||||
Object properties = paramsMap.get("properties");
|
||||
if (properties instanceof Map) {
|
||||
Map<String, Object> propertiesMap = (Map<String, Object>) properties;
|
||||
@@ -167,7 +177,7 @@ public class IotDeviceMessageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// 策略4:从 data 字段中获取(适用于某些消息格式)
|
||||
// 策略 4:从 data 字段中获取(适用于某些消息格式)
|
||||
Object data = paramsMap.get("data");
|
||||
if (data instanceof Map) {
|
||||
Map<String, Object> dataMap = (Map<String, Object>) data;
|
||||
@@ -177,13 +187,13 @@ public class IotDeviceMessageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// 策略5:从 value 字段中获取(适用于单值消息)
|
||||
// 策略 5:从 value 字段中获取(适用于单值消息)
|
||||
Object value = paramsMap.get("value");
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 策略6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
|
||||
// 策略 6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
|
||||
if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
|
||||
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
|
||||
if (!"identifier".equals(entry.getKey())) {
|
||||
@@ -196,6 +206,43 @@ public class IotDeviceMessageUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务调用消息中提取输入参数
|
||||
* <p>
|
||||
* 服务调用消息的 params 结构通常为:
|
||||
* {
|
||||
* "identifier": "serviceIdentifier",
|
||||
* "inputData": { ... } 或 "inputParams": { ... }
|
||||
* }
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @return 输入参数 Map,如果未找到则返回 null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> extractServiceInputParams(IotDeviceMessage message) {
|
||||
// 1. 参数校验
|
||||
Object params = message.getParams();
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(params instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> paramsMap = (Map<String, Object>) params;
|
||||
|
||||
// 尝试从 inputData 字段获取
|
||||
Object inputData = paramsMap.get("inputData");
|
||||
if (inputData instanceof Map) {
|
||||
return (Map<String, Object>) inputData;
|
||||
}
|
||||
// 尝试从 inputParams 字段获取
|
||||
Object inputParams = paramsMap.get("inputParams");
|
||||
if (inputParams instanceof Map) {
|
||||
return (Map<String, Object>) inputParams;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== Topic 相关 ==========
|
||||
|
||||
public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package cn.iocoder.yudao.module.iot.core.util;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link IotDeviceMessageUtils} 的单元测试
|
||||
@@ -138,4 +138,72 @@ public class IotDeviceMessageUtilsTest {
|
||||
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
|
||||
assertEquals(25.5, result); // 应该返回直接标识符的值
|
||||
}
|
||||
|
||||
// ========== notContainsIdentifier 测试 ==========
|
||||
|
||||
/**
|
||||
* 测试 notContainsIdentifier 与 containsIdentifier 的互补性
|
||||
* **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
|
||||
* **Validates: Requirements 4.1**
|
||||
*/
|
||||
@Test
|
||||
public void testNotContainsIdentifier_complementary_whenContains() {
|
||||
// 准备参数:消息包含指定标识符
|
||||
IotDeviceMessage message = new IotDeviceMessage();
|
||||
message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("temperature", 25);
|
||||
message.setParams(params);
|
||||
String identifier = "temperature";
|
||||
|
||||
// 调用 & 断言:验证互补性
|
||||
boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
|
||||
boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
|
||||
assertTrue(containsResult);
|
||||
assertFalse(notContainsResult);
|
||||
assertEquals(!containsResult, notContainsResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 notContainsIdentifier 与 containsIdentifier 的互补性
|
||||
* **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
|
||||
* **Validates: Requirements 4.1**
|
||||
*/
|
||||
@Test
|
||||
public void testNotContainsIdentifier_complementary_whenNotContains() {
|
||||
// 准备参数:消息不包含指定标识符
|
||||
IotDeviceMessage message = new IotDeviceMessage();
|
||||
message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("temperature", 25);
|
||||
message.setParams(params);
|
||||
String identifier = "humidity";
|
||||
|
||||
// 调用 & 断言:验证互补性
|
||||
boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
|
||||
boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
|
||||
assertFalse(containsResult);
|
||||
assertTrue(notContainsResult);
|
||||
assertEquals(!containsResult, notContainsResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 notContainsIdentifier 与 containsIdentifier 的互补性 - 空参数场景
|
||||
* **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
|
||||
* **Validates: Requirements 4.1**
|
||||
*/
|
||||
@Test
|
||||
public void testNotContainsIdentifier_complementary_nullParams() {
|
||||
// 准备参数:params 为 null
|
||||
IotDeviceMessage message = new IotDeviceMessage();
|
||||
message.setParams(null);
|
||||
String identifier = "temperature";
|
||||
|
||||
// 调用 & 断言:验证互补性
|
||||
boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
|
||||
boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
|
||||
assertFalse(containsResult);
|
||||
assertTrue(notContainsResult);
|
||||
assertEquals(!containsResult, notContainsResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- CoAP 相关 - Eclipse Californium -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.californium</groupId>
|
||||
<artifactId>californium-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
|
||||
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.codec;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
|
||||
/**
|
||||
* {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器
|
||||
* {@link IotDeviceMessage} 的编解码器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
|
||||
|
||||
private static final String TYPE = "Alink";
|
||||
public static final String TYPE = "Alink";
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* TCP 二进制格式 {@link IotDeviceMessage} 编解码器
|
||||
* TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器
|
||||
* <p>
|
||||
* 二进制协议格式(所有数值使用大端序):
|
||||
*
|
||||
|
||||
@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* TCP JSON 格式 {@link IotDeviceMessage} 编解码器
|
||||
* TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
|
||||
*
|
||||
* 采用纯 JSON 格式传输,格式如下:
|
||||
* {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.config;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
|
||||
@@ -10,13 +12,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
@@ -40,9 +44,15 @@ public class IotGatewayConfiguration {
|
||||
@Slf4j
|
||||
public static class HttpProtocolConfiguration {
|
||||
|
||||
@Bean(name = "httpVertx", destroyMethod = "close")
|
||||
public Vertx httpVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) {
|
||||
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp());
|
||||
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
@Qualifier("httpVertx") Vertx httpVertx) {
|
||||
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -110,11 +120,9 @@ public class IotGatewayConfiguration {
|
||||
@Bean
|
||||
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
|
||||
IotDeviceMessageService messageService,
|
||||
IotDeviceService deviceService,
|
||||
IotTcpConnectionManager connectionManager,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager,
|
||||
messageBus);
|
||||
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,39 +165,88 @@ public class IotGatewayConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT WebSocket 协议配置类
|
||||
* IoT 网关 UDP 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt-ws", name = "enabled", havingValue = "true")
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class MqttWsProtocolConfiguration {
|
||||
public static class UdpProtocolConfiguration {
|
||||
|
||||
@Bean(name = "mqttWsVertx", destroyMethod = "close")
|
||||
public Vertx mqttWsVertx() {
|
||||
@Bean(name = "udpVertx", destroyMethod = "close")
|
||||
public Vertx udpVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotMqttWsUpstreamProtocol iotMqttWsUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
IotDeviceMessageService messageService,
|
||||
IotMqttWsConnectionManager connectionManager,
|
||||
@Qualifier("mqttWsVertx") Vertx mqttWsVertx) {
|
||||
return new IotMqttWsUpstreamProtocol(gatewayProperties.getProtocol().getMqttWs(),
|
||||
messageService, connectionManager, mqttWsVertx);
|
||||
public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
IotDeviceService deviceService,
|
||||
IotDeviceMessageService messageService,
|
||||
IotUdpSessionManager sessionManager,
|
||||
@Qualifier("udpVertx") Vertx udpVertx) {
|
||||
return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
|
||||
deviceService, messageService, sessionManager, udpVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotMqttWsDownstreamHandler iotMqttWsDownstreamHandler(IotDeviceMessageService messageService,
|
||||
IotDeviceService deviceService,
|
||||
IotMqttWsConnectionManager connectionManager) {
|
||||
return new IotMqttWsDownstreamHandler(messageService, deviceService, connectionManager);
|
||||
public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
|
||||
IotDeviceMessageService messageService,
|
||||
IotUdpSessionManager sessionManager,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class CoapProtocolConfiguration {
|
||||
|
||||
@Bean
|
||||
public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
|
||||
return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotMqttWsDownstreamSubscriber iotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol mqttWsUpstreamProtocol,
|
||||
IotMqttWsDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotMqttWsDownstreamSubscriber(mqttWsUpstreamProtocol, downstreamHandler, messageBus);
|
||||
public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* IoT 网关 WebSocket 协议配置类
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public static class WebSocketProtocolConfiguration {
|
||||
|
||||
@Bean(name = "websocketVertx", destroyMethod = "close")
|
||||
public Vertx websocketVertx() {
|
||||
return Vertx.vertx();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
|
||||
IotDeviceService deviceService,
|
||||
IotDeviceMessageService messageService,
|
||||
IotWebSocketConnectionManager connectionManager,
|
||||
@Qualifier("websocketVertx") Vertx websocketVertx) {
|
||||
return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
|
||||
deviceService, messageService, connectionManager, websocketVertx);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
|
||||
IotDeviceMessageService messageService,
|
||||
IotWebSocketConnectionManager connectionManager,
|
||||
IotMessageBus messageBus) {
|
||||
return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -93,6 +93,21 @@ public class IotGatewayProperties {
|
||||
*/
|
||||
private MqttWsProperties mqttWs;
|
||||
|
||||
/**
|
||||
* UDP 组件配置
|
||||
*/
|
||||
private UdpProperties udp;
|
||||
|
||||
/**
|
||||
* CoAP 组件配置
|
||||
*/
|
||||
private CoapProperties coap;
|
||||
|
||||
/**
|
||||
* WebSocket 组件配置
|
||||
*/
|
||||
private WebSocketProperties websocket;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -503,4 +518,129 @@ public class IotGatewayProperties {
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UdpProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 服务端口(默认 8093)
|
||||
*/
|
||||
private Integer port = 8093;
|
||||
|
||||
/**
|
||||
* 接收缓冲区大小(默认 64KB)
|
||||
*/
|
||||
private Integer receiveBufferSize = 65536;
|
||||
|
||||
/**
|
||||
* 发送缓冲区大小(默认 64KB)
|
||||
*/
|
||||
private Integer sendBufferSize = 65536;
|
||||
|
||||
/**
|
||||
* 会话超时时间(毫秒,默认 60 秒)
|
||||
* <p>
|
||||
* 用于清理不活跃的设备地址映射
|
||||
*/
|
||||
private Long sessionTimeoutMs = 60000L;
|
||||
|
||||
/**
|
||||
* 会话清理间隔(毫秒,默认 30 秒)
|
||||
*/
|
||||
private Long sessionCleanIntervalMs = 30000L;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CoapProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 服务端口(CoAP 默认端口 5683)
|
||||
*/
|
||||
@NotNull(message = "服务端口不能为空")
|
||||
private Integer port = 5683;
|
||||
|
||||
/**
|
||||
* 最大消息大小(字节)
|
||||
*/
|
||||
@NotNull(message = "最大消息大小不能为空")
|
||||
private Integer maxMessageSize = 1024;
|
||||
|
||||
/**
|
||||
* ACK 超时时间(毫秒)
|
||||
*/
|
||||
@NotNull(message = "ACK 超时时间不能为空")
|
||||
private Integer ackTimeout = 2000;
|
||||
|
||||
/**
|
||||
* 最大重传次数
|
||||
*/
|
||||
@NotNull(message = "最大重传次数不能为空")
|
||||
private Integer maxRetransmit = 4;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class WebSocketProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 服务器端口(默认:8094)
|
||||
*/
|
||||
private Integer port = 8094;
|
||||
|
||||
/**
|
||||
* WebSocket 路径(默认:/ws)
|
||||
*/
|
||||
@NotEmpty(message = "WebSocket 路径不能为空")
|
||||
private String path = "/ws";
|
||||
|
||||
/**
|
||||
* 最大消息大小(字节,默认 64KB)
|
||||
*/
|
||||
private Integer maxMessageSize = 65536;
|
||||
|
||||
/**
|
||||
* 最大帧大小(字节,默认 64KB)
|
||||
*/
|
||||
private Integer maxFrameSize = 65536;
|
||||
|
||||
/**
|
||||
* 空闲超时时间(秒,默认 60)
|
||||
*/
|
||||
private Integer idleTimeoutSeconds = 60;
|
||||
|
||||
/**
|
||||
* 是否启用 SSL(wss://)
|
||||
*/
|
||||
private Boolean sslEnabled = false;
|
||||
|
||||
/**
|
||||
* SSL 证书路径
|
||||
*/
|
||||
private String sslCertPath;
|
||||
|
||||
/**
|
||||
* SSL 私钥路径
|
||||
*/
|
||||
private String sslKeyPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 订阅者:接收下行给设备的消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class IotCoapDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotCoapUpstreamProtocol protocol;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
messageBus.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
// 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
|
||||
log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.*;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapResource;
|
||||
import org.eclipse.californium.core.CoapServer;
|
||||
import org.eclipse.californium.core.config.CoapConfig;
|
||||
import org.eclipse.californium.elements.config.Configuration;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议:接收设备上行消息
|
||||
*
|
||||
* 基于 Eclipse Californium 实现,支持:
|
||||
* 1. 认证:POST /auth
|
||||
* 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
|
||||
* 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.CoapProperties coapProperties;
|
||||
|
||||
private CoapServer coapServer;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
|
||||
this.coapProperties = coapProperties;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
try {
|
||||
// 1.1 创建网络配置(Californium 3.x API)
|
||||
Configuration config = Configuration.createStandardWithoutFile();
|
||||
config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
|
||||
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
|
||||
config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
|
||||
config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
|
||||
// 1.2 创建 CoAP 服务器
|
||||
coapServer = new CoapServer(config);
|
||||
|
||||
// 2.1 添加 /auth 认证资源
|
||||
IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
|
||||
IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
|
||||
coapServer.add(authResource);
|
||||
// 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
|
||||
IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
|
||||
IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
|
||||
authResource.add(new CoapResource("register") {{
|
||||
add(registerResource);
|
||||
}});
|
||||
// 2.3 添加 /topic 根资源(用于上行消息)
|
||||
IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
|
||||
IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
|
||||
coapServer.add(topicResource);
|
||||
|
||||
// 3. 启动服务器
|
||||
coapServer.start();
|
||||
log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (coapServer != null) {
|
||||
try {
|
||||
coapServer.stop();
|
||||
log.info("[stop][IoT 网关 CoAP 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* CoAP 协议实现包
|
||||
* <p>
|
||||
* 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能
|
||||
* <p>
|
||||
* URI 路径:
|
||||
* - 认证:POST /auth
|
||||
* - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
|
||||
* - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
|
||||
* <p>
|
||||
* Token 通过 CoAP Option 2088 携带
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
|
||||
@@ -0,0 +1,117 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议的【认证】处理器
|
||||
*
|
||||
* 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler}
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapAuthHandler {
|
||||
|
||||
private final IotDeviceTokenService deviceTokenService;
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
public IotCoapAuthHandler() {
|
||||
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理认证请求
|
||||
*
|
||||
* @param exchange CoAP 交换对象
|
||||
* @param protocol 协议对象
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
|
||||
try {
|
||||
// 1.1 解析请求体
|
||||
byte[] payload = exchange.getRequestPayload();
|
||||
if (payload == null || payload.length == 0) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
|
||||
return;
|
||||
}
|
||||
Map<String, Object> body;
|
||||
try {
|
||||
body = JsonUtils.parseObject(new String(payload), Map.class);
|
||||
} catch (Exception e) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
|
||||
return;
|
||||
}
|
||||
// 1.2 解析参数
|
||||
String clientId = MapUtil.getStr(body, "clientId");
|
||||
if (StrUtil.isEmpty(clientId)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空");
|
||||
return;
|
||||
}
|
||||
String username = MapUtil.getStr(body, "username");
|
||||
if (StrUtil.isEmpty(username)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空");
|
||||
return;
|
||||
}
|
||||
String password = MapUtil.getStr(body, "password");
|
||||
if (StrUtil.isEmpty(password)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 执行认证
|
||||
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
|
||||
.setClientId(clientId).setUsername(username).setPassword(password));
|
||||
if (result.isError()) {
|
||||
log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg());
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg());
|
||||
return;
|
||||
}
|
||||
if (!BooleanUtil.isTrue(result.getData())) {
|
||||
log.warn("[handle][认证失败,clientId: {}]", clientId);
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败");
|
||||
return;
|
||||
}
|
||||
// 2.2 生成 Token
|
||||
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
|
||||
Assert.notNull(deviceInfo, "设备信息不能为空");
|
||||
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
Assert.notBlank(token, "生成 token 不能为空");
|
||||
|
||||
// 3. 执行上线
|
||||
IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
|
||||
deviceMessageService.sendDeviceMessage(message,
|
||||
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
|
||||
|
||||
// 4. 返回成功响应
|
||||
log.info("[handle][认证成功,productKey: {}, deviceName: {}]",
|
||||
deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token));
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][认证处理异常]", e);
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapResource;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议的认证资源(/auth)
|
||||
*
|
||||
* 设备通过此资源进行认证,获取 Token
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapAuthResource extends CoapResource {
|
||||
|
||||
public static final String PATH = "auth";
|
||||
|
||||
private final IotCoapUpstreamProtocol protocol;
|
||||
private final IotCoapAuthHandler authHandler;
|
||||
|
||||
public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
|
||||
IotCoapAuthHandler authHandler) {
|
||||
super(PATH);
|
||||
this.protocol = protocol;
|
||||
this.authHandler = authHandler;
|
||||
log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePOST(CoapExchange exchange) {
|
||||
log.debug("[handlePOST][收到 /auth POST 请求]");
|
||||
authHandler.handle(exchange, protocol);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议的【设备动态注册】处理器
|
||||
* <p>
|
||||
* 用于直连设备/网关的一型一密动态注册,不需要认证
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
* @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapRegisterHandler {
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
public IotCoapRegisterHandler() {
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备动态注册请求
|
||||
*
|
||||
* @param exchange CoAP 交换对象
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handle(CoapExchange exchange) {
|
||||
try {
|
||||
// 1.1 解析请求体
|
||||
byte[] payload = exchange.getRequestPayload();
|
||||
if (payload == null || payload.length == 0) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
|
||||
return;
|
||||
}
|
||||
Map<String, Object> body;
|
||||
try {
|
||||
body = JsonUtils.parseObject(new String(payload), Map.class);
|
||||
} catch (Exception e) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.2 解析参数
|
||||
String productKey = MapUtil.getStr(body, "productKey");
|
||||
if (StrUtil.isEmpty(productKey)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
|
||||
return;
|
||||
}
|
||||
String deviceName = MapUtil.getStr(body, "deviceName");
|
||||
if (StrUtil.isEmpty(deviceName)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
|
||||
return;
|
||||
}
|
||||
String productSecret = MapUtil.getStr(body, "productSecret");
|
||||
if (StrUtil.isEmpty(productSecret)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用动态注册
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(productKey)
|
||||
.setDeviceName(deviceName)
|
||||
.setProductSecret(productSecret);
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
|
||||
if (result.isError()) {
|
||||
log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]",
|
||||
productKey, deviceName, result.getMsg());
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,
|
||||
"设备动态注册失败:" + result.getMsg());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 返回成功响应
|
||||
log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName);
|
||||
IotCoapUtils.respondSuccess(exchange, result.getData());
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][设备动态注册处理异常]", e);
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapResource;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议的设备动态注册资源(/auth/register/device)
|
||||
* <p>
|
||||
* 用于直连设备/网关的一型一密动态注册,不需要认证
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapRegisterResource extends CoapResource {
|
||||
|
||||
public static final String PATH = "device";
|
||||
|
||||
private final IotCoapRegisterHandler registerHandler;
|
||||
|
||||
public IotCoapRegisterResource(IotCoapRegisterHandler registerHandler) {
|
||||
super(PATH);
|
||||
this.registerHandler = registerHandler;
|
||||
log.info("[IotCoapRegisterResource][创建 CoAP 设备动态注册资源: /auth/register/{}]", PATH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePOST(CoapExchange exchange) {
|
||||
log.debug("[handlePOST][收到设备动态注册请求]");
|
||||
registerHandler.handle(exchange);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.text.StrPool;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议的【上行】处理器
|
||||
*
|
||||
* 处理设备通过 CoAP 协议发送的上行消息,包括:
|
||||
* 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
|
||||
* 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
|
||||
*
|
||||
* Token 通过自定义 CoAP Option 2088 携带
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapUpstreamHandler {
|
||||
|
||||
private final IotDeviceTokenService deviceTokenService;
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
public IotCoapUpstreamHandler() {
|
||||
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
|
||||
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 CoAP 请求
|
||||
*
|
||||
* @param exchange CoAP 交换对象
|
||||
* @param protocol 协议对象
|
||||
*/
|
||||
public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
|
||||
try {
|
||||
// 1. 解析通用参数
|
||||
List<String> uriPath = exchange.getRequestOptions().getUriPath();
|
||||
String productKey = CollUtil.get(uriPath, 2);
|
||||
String deviceName = CollUtil.get(uriPath, 3);
|
||||
byte[] payload = exchange.getRequestPayload();
|
||||
if (StrUtil.isEmpty(productKey)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isEmpty(deviceName)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
|
||||
return;
|
||||
}
|
||||
if (ArrayUtil.isEmpty(payload)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 认证:从自定义 Option 获取 token
|
||||
String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
|
||||
if (StrUtil.isEmpty(token)) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
|
||||
return;
|
||||
}
|
||||
// 验证 token
|
||||
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
|
||||
if (deviceInfo == null) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
|
||||
return;
|
||||
}
|
||||
// 验证设备信息匹配
|
||||
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|
||||
|| ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 解析 method:deviceName 后面的路径,用 . 拼接
|
||||
// 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
|
||||
String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
|
||||
|
||||
// 2.2 解码消息
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
|
||||
if (ObjUtil.notEqual(method, message.getMethod())) {
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配");
|
||||
return;
|
||||
}
|
||||
// 2.3 发送消息到消息总线
|
||||
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
|
||||
|
||||
// 3. 返回成功响应
|
||||
IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][CoAP 请求处理异常]", e);
|
||||
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapResource;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
import org.eclipse.californium.core.server.resources.Resource;
|
||||
|
||||
/**
|
||||
* IoT 网关 CoAP 协议的【上行】Topic 资源
|
||||
*
|
||||
* 支持任意深度的路径匹配:
|
||||
* - /topic/sys/{productKey}/{deviceName}/thing/property/post
|
||||
* - /topic/sys/{productKey}/{deviceName}/thing/event/{eventId}/post
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotCoapUpstreamTopicResource extends CoapResource {
|
||||
|
||||
public static final String PATH = "topic";
|
||||
|
||||
private final IotCoapUpstreamProtocol protocol;
|
||||
private final IotCoapUpstreamHandler upstreamHandler;
|
||||
|
||||
/**
|
||||
* 创建根资源(/topic)
|
||||
*/
|
||||
public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
|
||||
IotCoapUpstreamHandler upstreamHandler) {
|
||||
this(PATH, protocol, upstreamHandler);
|
||||
log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建子资源(动态路径)
|
||||
*/
|
||||
private IotCoapUpstreamTopicResource(String name,
|
||||
IotCoapUpstreamProtocol protocol,
|
||||
IotCoapUpstreamHandler upstreamHandler) {
|
||||
super(name);
|
||||
this.protocol = protocol;
|
||||
this.upstreamHandler = upstreamHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getChild(String name) {
|
||||
// 递归创建动态子资源,支持任意深度路径
|
||||
return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleGET(CoapExchange exchange) {
|
||||
upstreamHandler.handle(exchange, protocol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePOST(CoapExchange exchange) {
|
||||
upstreamHandler.handle(exchange, protocol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePUT(CoapExchange exchange) {
|
||||
upstreamHandler.handle(exchange, protocol);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import org.eclipse.californium.core.coap.CoAP;
|
||||
import org.eclipse.californium.core.coap.MediaTypeRegistry;
|
||||
import org.eclipse.californium.core.coap.Option;
|
||||
import org.eclipse.californium.core.server.resources.CoapExchange;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* IoT CoAP 协议工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class IotCoapUtils {
|
||||
|
||||
/**
|
||||
* 自定义 CoAP Option 编号,用于携带 Token
|
||||
* <p>
|
||||
* CoAP Option 范围 2048-65535 属于实验/自定义范围
|
||||
*/
|
||||
public static final int OPTION_TOKEN = 2088;
|
||||
|
||||
/**
|
||||
* 返回成功响应
|
||||
*
|
||||
* @param exchange CoAP 交换对象
|
||||
* @param data 响应数据
|
||||
*/
|
||||
public static void respondSuccess(CoapExchange exchange, Object data) {
|
||||
CommonResult<Object> result = CommonResult.success(data);
|
||||
String json = JsonUtils.toJsonString(result);
|
||||
exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误响应
|
||||
*
|
||||
* @param exchange CoAP 交换对象
|
||||
* @param code CoAP 响应码
|
||||
* @param message 错误消息
|
||||
*/
|
||||
public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {
|
||||
int errorCode = mapCoapCodeToErrorCode(code);
|
||||
CommonResult<Object> result = CommonResult.error(errorCode, message);
|
||||
String json = JsonUtils.toJsonString(result);
|
||||
exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从自定义 CoAP Option 中获取 Token
|
||||
*
|
||||
* @param exchange CoAP 交换对象
|
||||
* @param optionNumber Option 编号
|
||||
* @return Token 值,如果不存在则返回 null
|
||||
*/
|
||||
public static String getTokenFromOption(CoapExchange exchange, int optionNumber) {
|
||||
Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
|
||||
o -> o.getNumber() == optionNumber);
|
||||
return option != null ? new String(option.getValue()) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 CoAP 响应码映射到业务错误码
|
||||
*
|
||||
* @param code CoAP 响应码
|
||||
* @return 业务错误码
|
||||
*/
|
||||
public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {
|
||||
if (code == CoAP.ResponseCode.BAD_REQUEST) {
|
||||
return BAD_REQUEST.getCode();
|
||||
} else if (code == CoAP.ResponseCode.UNAUTHORIZED) {
|
||||
return UNAUTHORIZED.getCode();
|
||||
} else if (code == CoAP.ResponseCode.FORBIDDEN) {
|
||||
return FORBIDDEN.getCode();
|
||||
} else {
|
||||
return INTERNAL_SERVER_ERROR.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
@@ -103,7 +104,7 @@ public class IotEmqxAuthEventHandler {
|
||||
JsonObject body = null;
|
||||
try {
|
||||
// 1. 解析请求体
|
||||
body = parseRequestBody(context);
|
||||
body = parseEventRequestBody(context);
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +153,9 @@ public class IotEmqxAuthEventHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析请求体
|
||||
* 解析认证接口请求体
|
||||
* <p>
|
||||
* 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
|
||||
*
|
||||
* @param context 路由上下文
|
||||
* @return 请求体JSON对象,解析失败时返回null
|
||||
@@ -173,6 +176,30 @@ public class IotEmqxAuthEventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析事件接口请求体
|
||||
* <p>
|
||||
* 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范)
|
||||
*
|
||||
* @param context 路由上下文
|
||||
* @return 请求体JSON对象,解析失败时返回null
|
||||
*/
|
||||
private JsonObject parseEventRequestBody(RoutingContext context) {
|
||||
try {
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
if (body == null) {
|
||||
log.info("[parseEventRequestBody][请求体为空]");
|
||||
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
|
||||
return null;
|
||||
}
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
log.error("[parseEventRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
|
||||
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行设备认证
|
||||
*
|
||||
@@ -201,7 +228,7 @@ public class IotEmqxAuthEventHandler {
|
||||
*/
|
||||
private void handleDeviceStateChange(String username, boolean online) {
|
||||
// 1. 解析设备信息
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
if (deviceInfo == null) {
|
||||
log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username);
|
||||
return;
|
||||
|
||||
@@ -3,8 +3,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.core.http.HttpServerOptions;
|
||||
@@ -22,31 +23,36 @@ import lombok.extern.slf4j.Slf4j;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotHttpUpstreamProtocol extends AbstractVerticle {
|
||||
public class IotHttpUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.HttpProperties httpProperties;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
private HttpServer httpServer;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties) {
|
||||
public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) {
|
||||
this.httpProperties = httpProperties;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// 创建路由
|
||||
Vertx vertx = Vertx.vertx();
|
||||
Router router = Router.router(vertx);
|
||||
router.route().handler(BodyHandler.create());
|
||||
|
||||
// 创建处理器,添加路由处理器
|
||||
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
|
||||
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
|
||||
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
|
||||
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
|
||||
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
|
||||
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
|
||||
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
|
||||
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
|
||||
|
||||
@@ -70,7 +76,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (httpServer != null) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* HTTP 协议实现包
|
||||
* <p>
|
||||
* 提供基于 Vert.x HTTP Server 的 IoT 设备连接和消息处理功能
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
@@ -7,7 +7,8 @@ import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.http.HttpHeaders;
|
||||
@@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
|
||||
private void beforeHandle(RoutingContext context) {
|
||||
// 如果不需要认证,则不走前置处理
|
||||
String path = context.request().path();
|
||||
if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) {
|
||||
if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
|
||||
}
|
||||
|
||||
// 校验 token
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token);
|
||||
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
|
||||
Assert.notNull(deviceInfo, "设备信息不能为空");
|
||||
// 校验设备信息是否匹配
|
||||
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|
||||
|
||||
@@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
@@ -51,6 +51,9 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
|
||||
public CommonResult<Object> handle0(RoutingContext context) {
|
||||
// 1. 解析参数
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
if (body == null) {
|
||||
throw invalidParamException("请求体不能为空");
|
||||
}
|
||||
String clientId = body.getString("clientId");
|
||||
if (StrUtil.isEmpty(clientId)) {
|
||||
throw invalidParamException("clientId 不能为空");
|
||||
@@ -72,7 +75,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
|
||||
throw exception(DEVICE_AUTH_FAIL);
|
||||
}
|
||||
// 2.2 生成 Token
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username);
|
||||
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
|
||||
Assert.notNull(deviceInfo, "设备信息不能为空");
|
||||
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
Assert.notBlank(token, "生成 token 不能为空位");
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议的【设备动态注册】处理器
|
||||
* <p>
|
||||
* 用于直连设备/网关的一型一密动态注册,不需要认证
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
|
||||
|
||||
public static final String PATH = "/auth/register/device";
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
public IotHttpRegisterHandler() {
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Object> handle0(RoutingContext context) {
|
||||
// 1. 解析参数
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
if (body == null) {
|
||||
throw invalidParamException("请求体不能为空");
|
||||
}
|
||||
String productKey = body.getString("productKey");
|
||||
if (StrUtil.isEmpty(productKey)) {
|
||||
throw invalidParamException("productKey 不能为空");
|
||||
}
|
||||
String deviceName = body.getString("deviceName");
|
||||
if (StrUtil.isEmpty(deviceName)) {
|
||||
throw invalidParamException("deviceName 不能为空");
|
||||
}
|
||||
String productSecret = body.getString("productSecret");
|
||||
if (StrUtil.isEmpty(productSecret)) {
|
||||
throw invalidParamException("productSecret 不能为空");
|
||||
}
|
||||
|
||||
// 2. 调用动态注册
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
|
||||
result.checkError();
|
||||
|
||||
// 3. 返回结果
|
||||
return success(result.getData());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议的【子设备动态注册】处理器
|
||||
* <p>
|
||||
* 用于子设备的动态注册,需要网关认证
|
||||
*
|
||||
* @author 芋道源码
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
|
||||
*/
|
||||
public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
|
||||
|
||||
/**
|
||||
* 路径:/auth/register/sub-device/:productKey/:deviceName
|
||||
* <p>
|
||||
* productKey 和 deviceName 是网关设备的标识
|
||||
*/
|
||||
public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName";
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
public IotHttpRegisterSubHandler() {
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Object> handle0(RoutingContext context) {
|
||||
// 1. 解析通用参数
|
||||
String productKey = context.pathParam("productKey");
|
||||
String deviceName = context.pathParam("deviceName");
|
||||
|
||||
// 2. 解析子设备列表
|
||||
JsonObject body = context.body().asJsonObject();
|
||||
if (body == null) {
|
||||
throw invalidParamException("请求体不能为空");
|
||||
}
|
||||
if (body.getJsonArray("params") == null) {
|
||||
throw invalidParamException("params 不能为空");
|
||||
}
|
||||
List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
|
||||
body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
|
||||
|
||||
// 3. 调用子设备动态注册
|
||||
IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
|
||||
.setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
|
||||
CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
|
||||
result.checkError();
|
||||
|
||||
// 4. 返回结果
|
||||
return success(result.getData());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import io.vertx.ext.web.RoutingContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
/**
|
||||
* IoT 网关 HTTP 协议的【上行】处理器
|
||||
*
|
||||
@@ -40,6 +42,9 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
|
||||
String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
|
||||
|
||||
// 2.1 解析消息
|
||||
if (context.body().buffer() == null) {
|
||||
throw invalidParamException("请求体不能为空");
|
||||
}
|
||||
byte[] bytes = context.body().buffer().getBytes();
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes,
|
||||
productKey, deviceName);
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -87,9 +88,9 @@ public class IotMqttConnectionManager {
|
||||
connectionMap.remove(oldEndpoint);
|
||||
}
|
||||
|
||||
// 注册新连接
|
||||
connectionMap.put(endpoint, connectionInfo);
|
||||
deviceEndpointMap.put(deviceId, endpoint);
|
||||
|
||||
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
|
||||
deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
|
||||
}
|
||||
@@ -101,13 +102,12 @@ public class IotMqttConnectionManager {
|
||||
*/
|
||||
public void unregisterConnection(MqttEndpoint endpoint) {
|
||||
ConnectionInfo connectionInfo = connectionMap.remove(endpoint);
|
||||
if (connectionInfo != null) {
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
deviceEndpointMap.remove(deviceId);
|
||||
|
||||
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId,
|
||||
getEndpointAddress(endpoint));
|
||||
if (connectionInfo == null) {
|
||||
return;
|
||||
}
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
deviceEndpointMap.remove(deviceId);
|
||||
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, getEndpointAddress(endpoint));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +123,7 @@ public class IotMqttConnectionManager {
|
||||
* @param deviceId 设备 ID
|
||||
* @return 连接信息
|
||||
*/
|
||||
public IotMqttConnectionManager.ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
|
||||
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
|
||||
// 通过设备 ID 获取连接端点
|
||||
MqttEndpoint endpoint = getDeviceEndpoint(deviceId);
|
||||
if (endpoint == null) {
|
||||
@@ -166,7 +166,7 @@ public class IotMqttConnectionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
endpoint.publish(topic, io.vertx.core.buffer.Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain);
|
||||
endpoint.publish(topic, Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain);
|
||||
log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.mqtt.MqttEndpoint;
|
||||
@@ -20,6 +28,7 @@ import io.vertx.mqtt.MqttTopicSubscription;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MQTT 上行消息处理器
|
||||
@@ -29,6 +38,16 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public class IotMqttUpstreamHandler {
|
||||
|
||||
/**
|
||||
* 默认编解码类型(MQTT 使用 Alink 协议)
|
||||
*/
|
||||
private static final String DEFAULT_CODEC_TYPE = "Alink";
|
||||
|
||||
/**
|
||||
* register 请求的 topic 后缀
|
||||
*/
|
||||
private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotMqttConnectionManager connectionManager;
|
||||
@@ -84,20 +103,28 @@ public class IotMqttUpstreamHandler {
|
||||
});
|
||||
|
||||
// 4. 设置消息处理器
|
||||
endpoint.publishHandler(message -> {
|
||||
endpoint.publishHandler(mqttMessage -> {
|
||||
try {
|
||||
processMessage(clientId, message.topicName(), message.payload().getBytes());
|
||||
// 4.1 根据 topic 判断是否为 register 请求
|
||||
String topic = mqttMessage.topicName();
|
||||
byte[] payload = mqttMessage.payload().getBytes();
|
||||
if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) {
|
||||
// register 请求:使用默认编解码器处理(设备可能未注册)
|
||||
processRegisterMessage(clientId, topic, payload, endpoint);
|
||||
} else {
|
||||
// 业务请求:正常处理
|
||||
processMessage(clientId, topic, payload);
|
||||
}
|
||||
|
||||
// 根据 QoS 级别发送相应的确认消息
|
||||
if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
|
||||
// 4.2 根据 QoS 级别发送相应的确认消息
|
||||
if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
|
||||
// QoS 1: 发送 PUBACK 确认
|
||||
endpoint.publishAcknowledge(message.messageId());
|
||||
} else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
|
||||
endpoint.publishAcknowledge(mqttMessage.messageId());
|
||||
} else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) {
|
||||
// QoS 2: 发送 PUBREC 确认
|
||||
endpoint.publishReceived(message.messageId());
|
||||
endpoint.publishReceived(mqttMessage.messageId());
|
||||
}
|
||||
// QoS 0 无需确认
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
|
||||
clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
|
||||
@@ -160,10 +187,9 @@ public class IotMqttUpstreamHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
|
||||
// 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
|
||||
try {
|
||||
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
|
||||
if (message == null) {
|
||||
@@ -171,10 +197,9 @@ public class IotMqttUpstreamHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 处理业务消息(认证已在连接时完成)
|
||||
log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
|
||||
productKey, deviceName, message.getMethod());
|
||||
|
||||
// 4. 处理业务消息(认证已在连接时完成)
|
||||
handleBusinessRequest(message, productKey, deviceName);
|
||||
} catch (Exception e) {
|
||||
log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
@@ -214,7 +239,7 @@ public class IotMqttUpstreamHandler {
|
||||
}
|
||||
|
||||
// 4. 获取设备信息
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
if (deviceInfo == null) {
|
||||
log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
|
||||
return false;
|
||||
@@ -245,6 +270,186 @@ public class IotMqttUpstreamHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 register 消息(设备动态注册,使用默认编解码器)
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
* @param endpoint MQTT 连接端点
|
||||
*/
|
||||
private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) {
|
||||
// 1.1 基础检查
|
||||
if (ArrayUtil.isEmpty(payload)) {
|
||||
return;
|
||||
}
|
||||
// 1.2 解析主题,获取 productKey 和 deviceName
|
||||
String[] topicParts = topic.split("/");
|
||||
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
|
||||
log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic);
|
||||
return;
|
||||
}
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
|
||||
// 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
|
||||
IotDeviceMessage message;
|
||||
try {
|
||||
message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
|
||||
if (message == null) {
|
||||
log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 处理设备动态注册请求
|
||||
log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]",
|
||||
productKey, deviceName, message.getMethod());
|
||||
try {
|
||||
handleRegisterRequest(message, productKey, deviceName, endpoint);
|
||||
} catch (Exception e) {
|
||||
log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
|
||||
clientId, topic, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备动态注册请求(一型一密,不需要 deviceSecret)
|
||||
*
|
||||
* @param message 消息信息
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) {
|
||||
String clientId = endpoint.clientIdentifier();
|
||||
try {
|
||||
// 1. 解析注册参数
|
||||
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
|
||||
if (params == null) {
|
||||
log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
|
||||
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用动态注册 API
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
if (result.isError()) {
|
||||
log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
|
||||
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 发送成功响应(包含 deviceSecret)
|
||||
sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData());
|
||||
log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
|
||||
params.getDeviceName(), clientId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
|
||||
sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析注册参数
|
||||
*
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 注册参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "DuplicatedCode"})
|
||||
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) params;
|
||||
return new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
|
||||
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
|
||||
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
|
||||
}
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceRegisterReqDTO) {
|
||||
return (IotDeviceRegisterReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册成功响应(包含 deviceSecret)
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param requestId 请求 ID
|
||||
* @param registerResp 注册响应
|
||||
*/
|
||||
private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
|
||||
String requestId, IotDeviceRegisterRespDTO registerResp) {
|
||||
try {
|
||||
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
|
||||
|
||||
// 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
|
||||
|
||||
// 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
|
||||
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
|
||||
endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
|
||||
MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]",
|
||||
endpoint.clientIdentifier(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册错误响应
|
||||
*
|
||||
* @param endpoint MQTT 连接端点
|
||||
* @param productKey 产品 Key
|
||||
* @param deviceName 设备名称
|
||||
* @param requestId 请求 ID
|
||||
* @param errorMessage 错误消息
|
||||
*/
|
||||
private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
|
||||
String requestId, String errorMessage) {
|
||||
try {
|
||||
// 1. 构建响应消息
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage);
|
||||
|
||||
// 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
|
||||
|
||||
// 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
|
||||
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
|
||||
endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
|
||||
MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]",
|
||||
endpoint.clientIdentifier(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务请求
|
||||
*/
|
||||
@@ -257,9 +462,7 @@ public class IotMqttUpstreamHandler {
|
||||
/**
|
||||
* 注册连接
|
||||
*/
|
||||
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device,
|
||||
String clientId) {
|
||||
|
||||
private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
|
||||
IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
|
||||
.setDeviceId(device.getId())
|
||||
.setProductKey(device.getProductKey())
|
||||
@@ -267,7 +470,6 @@ public class IotMqttUpstreamHandler {
|
||||
.setClientId(clientId)
|
||||
.setAuthenticated(true)
|
||||
.setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
|
||||
|
||||
connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
|
||||
}
|
||||
|
||||
@@ -296,15 +498,13 @@ public class IotMqttUpstreamHandler {
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), serverId);
|
||||
log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]",
|
||||
connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
|
||||
log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
|
||||
}
|
||||
|
||||
// 注销连接
|
||||
connectionManager.unregisterConnection(endpoint);
|
||||
} catch (Exception e) {
|
||||
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",
|
||||
endpoint.clientIdentifier(), e.getMessage());
|
||||
log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT MQTT WebSocket 下行消息订阅器
|
||||
* <p>
|
||||
* 订阅消息总线的设备下行消息,并通过 WebSocket 发送到设备
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttWsDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotMqttWsUpstreamProtocol upstreamProtocol;
|
||||
private final IotMqttWsDownstreamHandler downstreamHandler;
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
public IotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol upstreamProtocol,
|
||||
IotMqttWsDownstreamHandler downstreamHandler,
|
||||
IotMessageBus messageBus) {
|
||||
this.upstreamProtocol = upstreamProtocol;
|
||||
this.downstreamHandler = downstreamHandler;
|
||||
this.messageBus = messageBus;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
messageBus.register(this);
|
||||
log.info("[init][MQTT WebSocket 下行消息订阅器已启动,topic: {}]", getTopic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
log.debug("[onMessage][收到下行消息,deviceId: {},method: {}]",
|
||||
message.getDeviceId(), message.getMethod());
|
||||
try {
|
||||
// 1. 校验
|
||||
String method = message.getMethod();
|
||||
if (StrUtil.isBlank(method)) {
|
||||
log.warn("[onMessage][消息方法为空,deviceId: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 委托给下行处理器处理业务逻辑
|
||||
boolean success = downstreamHandler.handleDownstreamMessage(message);
|
||||
if (success) {
|
||||
log.debug("[onMessage][下行消息处理成功,deviceId: {},method: {}]",
|
||||
message.getDeviceId(), message.getMethod());
|
||||
} else {
|
||||
log.warn("[onMessage][下行消息处理失败,deviceId: {},method: {}]",
|
||||
message.getDeviceId(), message.getMethod());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[onMessage][处理下行消息失败,deviceId: {},method: {}]",
|
||||
message.getDeviceId(), message.getMethod(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.core.http.HttpServerOptions;
|
||||
import io.vertx.core.http.ServerWebSocket;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 MQTT WebSocket 协议:接收设备上行消息
|
||||
* <p>
|
||||
* 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持:
|
||||
* - 标准 MQTT 3.1.1 协议
|
||||
* - WebSocket 协议升级
|
||||
* - SSL/TLS 加密(wss://)
|
||||
* - 设备认证与连接管理
|
||||
* - QoS 0/1/2 消息质量保证
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttWsUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.MqttWsProperties mqttWsProperties;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotMqttWsConnectionManager connectionManager;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
private HttpServer httpServer;
|
||||
|
||||
public IotMqttWsUpstreamProtocol(IotGatewayProperties.MqttWsProperties mqttWsProperties,
|
||||
IotDeviceMessageService messageService,
|
||||
IotMqttWsConnectionManager connectionManager,
|
||||
Vertx vertx) {
|
||||
this.mqttWsProperties = mqttWsProperties;
|
||||
this.messageService = messageService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(mqttWsProperties.getPort());
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// 创建 HTTP 服务器选项
|
||||
HttpServerOptions options = new HttpServerOptions()
|
||||
.setPort(mqttWsProperties.getPort())
|
||||
.setIdleTimeout(mqttWsProperties.getKeepAliveTimeoutSeconds())
|
||||
.setMaxWebSocketFrameSize(mqttWsProperties.getMaxFrameSize())
|
||||
.setMaxWebSocketMessageSize(mqttWsProperties.getMaxMessageSize())
|
||||
// 配置 WebSocket 子协议支持
|
||||
.addWebSocketSubProtocol(mqttWsProperties.getSubProtocol());
|
||||
|
||||
// 配置 SSL(如果启用)
|
||||
if (Boolean.TRUE.equals(mqttWsProperties.getSslEnabled())) {
|
||||
options.setSsl(true)
|
||||
.setKeyCertOptions(mqttWsProperties.getSslOptions().getKeyCertOptions())
|
||||
.setTrustOptions(mqttWsProperties.getSslOptions().getTrustOptions());
|
||||
log.info("[start][MQTT WebSocket 已启用 SSL/TLS (wss://)]");
|
||||
}
|
||||
|
||||
// 创建 HTTP 服务器
|
||||
httpServer = vertx.createHttpServer(options);
|
||||
|
||||
// 设置 WebSocket 处理器
|
||||
httpServer.webSocketHandler(this::handleWebSocketConnection);
|
||||
|
||||
// 启动服务器
|
||||
try {
|
||||
httpServer.listen().result();
|
||||
log.info("[start][IoT 网关 MQTT WebSocket 协议启动成功,端口: {},路径: {},支持子协议: {}]",
|
||||
mqttWsProperties.getPort(), mqttWsProperties.getPath(),
|
||||
"mqtt, mqttv3.1, " + mqttWsProperties.getSubProtocol());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 MQTT WebSocket 协议启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (httpServer != null) {
|
||||
try {
|
||||
// 关闭所有连接
|
||||
connectionManager.closeAllConnections();
|
||||
|
||||
// 关闭服务器
|
||||
httpServer.close().result();
|
||||
log.info("[stop][IoT 网关 MQTT WebSocket 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 MQTT WebSocket 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WebSocket 连接请求
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
*/
|
||||
private void handleWebSocketConnection(ServerWebSocket socket) {
|
||||
String path = socket.path();
|
||||
String subProtocol = socket.subProtocol();
|
||||
|
||||
log.info("[handleWebSocketConnection][收到 WebSocket 连接请求,path: {},subProtocol: {},remoteAddress: {}]",
|
||||
path, subProtocol, socket.remoteAddress());
|
||||
|
||||
// 验证路径
|
||||
if (!mqttWsProperties.getPath().equals(path)) {
|
||||
log.warn("[handleWebSocketConnection][WebSocket 路径不匹配,拒绝连接,path: {},期望: {}]",
|
||||
path, mqttWsProperties.getPath());
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证子协议
|
||||
// Vert.x 已经自动进行了子协议协商,这里只需要验证是否为 MQTT 相关协议
|
||||
if (subProtocol != null && !subProtocol.startsWith("mqtt")) {
|
||||
log.warn("[handleWebSocketConnection][WebSocket 子协议不支持,拒绝连接,subProtocol: {}]", subProtocol);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[handleWebSocketConnection][WebSocket 连接已接受,remoteAddress: {},subProtocol: {}]",
|
||||
socket.remoteAddress(), subProtocol);
|
||||
|
||||
// 创建处理器并处理连接
|
||||
IotMqttWsUpstreamHandler handler = new IotMqttWsUpstreamHandler(
|
||||
this, messageService, connectionManager);
|
||||
handler.handle(socket);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import io.vertx.core.http.ServerWebSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* IoT MQTT WebSocket 连接管理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IotMqttWsConnectionManager {
|
||||
|
||||
/**
|
||||
* 存储设备连接
|
||||
* Key: 设备标识(deviceKey)
|
||||
* Value: WebSocket 连接
|
||||
*/
|
||||
private final Map<String, ServerWebSocket> connections = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 存储设备标识与 Socket ID 的映射
|
||||
* Key: 设备标识(deviceKey)
|
||||
* Value: Socket ID(UUID)
|
||||
*/
|
||||
private final Map<String, String> deviceKeyToSocketId = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 存储 Socket ID 与设备标识的映射
|
||||
* Key: Socket ID(UUID)
|
||||
* Value: 设备标识(deviceKey)
|
||||
*/
|
||||
private final Map<String, String> socketIdToDeviceKey = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 存储设备订阅的主题
|
||||
* Key: 设备标识(deviceKey)
|
||||
* Value: 订阅的主题集合
|
||||
*/
|
||||
private final Map<String, Set<String>> deviceSubscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 添加连接
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
* @param socket WebSocket 连接
|
||||
* @param socketId Socket ID(UUID)
|
||||
*/
|
||||
public void addConnection(String deviceKey, ServerWebSocket socket, String socketId) {
|
||||
connections.put(deviceKey, socket);
|
||||
deviceKeyToSocketId.put(deviceKey, socketId);
|
||||
socketIdToDeviceKey.put(socketId, deviceKey);
|
||||
log.info("[addConnection][设备连接已添加,deviceKey: {},socketId: {},当前连接数: {}]",
|
||||
deviceKey, socketId, connections.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除连接
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
*/
|
||||
public void removeConnection(String deviceKey) {
|
||||
ServerWebSocket socket = connections.remove(deviceKey);
|
||||
String socketId = deviceKeyToSocketId.remove(deviceKey);
|
||||
if (socketId != null) {
|
||||
socketIdToDeviceKey.remove(socketId);
|
||||
}
|
||||
if (socket != null) {
|
||||
log.info("[removeConnection][设备连接已移除,deviceKey: {},socketId: {},当前连接数: {}]",
|
||||
deviceKey, socketId, connections.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Socket ID 移除连接
|
||||
*
|
||||
* @param socketId WebSocket 文本框架 ID
|
||||
*/
|
||||
public void removeConnectionBySocketId(String socketId) {
|
||||
String deviceKey = socketIdToDeviceKey.remove(socketId);
|
||||
if (deviceKey != null) {
|
||||
connections.remove(deviceKey);
|
||||
log.info("[removeConnectionBySocketId][设备连接已移除,socketId: {},deviceKey: {},当前连接数: {}]",
|
||||
socketId, deviceKey, connections.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
* @return WebSocket 连接
|
||||
*/
|
||||
public ServerWebSocket getConnection(String deviceKey) {
|
||||
return connections.get(deviceKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Socket ID 获取设备标识
|
||||
*
|
||||
* @param socketId WebSocket 文本框架 ID
|
||||
* @return 设备标识
|
||||
*/
|
||||
public String getDeviceKeyBySocketId(String socketId) {
|
||||
return socketIdToDeviceKey.get(socketId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否在线
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
* @return 是否在线
|
||||
*/
|
||||
public boolean isOnline(String deviceKey) {
|
||||
return connections.containsKey(deviceKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前连接数
|
||||
*
|
||||
* @return 连接数
|
||||
*/
|
||||
public int getConnectionCount() {
|
||||
return connections.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有连接
|
||||
*/
|
||||
public void closeAllConnections() {
|
||||
connections.forEach((deviceKey, socket) -> {
|
||||
try {
|
||||
socket.close();
|
||||
log.info("[closeAllConnections][关闭设备连接,deviceKey: {}]", deviceKey);
|
||||
} catch (Exception e) {
|
||||
log.error("[closeAllConnections][关闭设备连接失败,deviceKey: {}]", deviceKey, e);
|
||||
}
|
||||
});
|
||||
connections.clear();
|
||||
deviceKeyToSocketId.clear();
|
||||
socketIdToDeviceKey.clear();
|
||||
deviceSubscriptions.clear();
|
||||
log.info("[closeAllConnections][所有连接已关闭]");
|
||||
}
|
||||
|
||||
// ==================== 订阅管理方法 ====================
|
||||
|
||||
/**
|
||||
* 添加订阅
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
* @param topic 订阅主题
|
||||
*/
|
||||
public void addSubscription(String deviceKey, String topic) {
|
||||
deviceSubscriptions.computeIfAbsent(deviceKey, k -> new CopyOnWriteArraySet<>()).add(topic);
|
||||
log.debug("[addSubscription][设备订阅主题,deviceKey: {},topic: {}]", deviceKey, topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除订阅
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
* @param topic 订阅主题
|
||||
*/
|
||||
public void removeSubscription(String deviceKey, String topic) {
|
||||
Set<String> topics = deviceSubscriptions.get(deviceKey);
|
||||
if (topics != null) {
|
||||
topics.remove(topic);
|
||||
log.debug("[removeSubscription][设备取消订阅,deviceKey: {},topic: {}]", deviceKey, topic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否订阅了指定主题
|
||||
* 支持 MQTT 通配符匹配(+ 和 #)
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
* @param topic 发布主题
|
||||
* @return 是否匹配
|
||||
*/
|
||||
public boolean isSubscribed(String deviceKey, String topic) {
|
||||
Set<String> subscriptions = deviceSubscriptions.get(deviceKey);
|
||||
if (CollUtil.isEmpty(subscriptions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有匹配的订阅
|
||||
for (String subscription : subscriptions) {
|
||||
if (topicMatches(subscription, topic)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备的所有订阅
|
||||
*
|
||||
* @param deviceKey 设备标识
|
||||
* @return 订阅主题集合
|
||||
*/
|
||||
public Set<String> getSubscriptions(String deviceKey) {
|
||||
return deviceSubscriptions.get(deviceKey);
|
||||
}
|
||||
|
||||
// TODO @haohao:这个方法,是不是也可以考虑抽到 IotMqttTopicUtils 里面去哈;感觉更简洁一点?
|
||||
/**
|
||||
* MQTT 主题匹配
|
||||
* 支持通配符:
|
||||
* - +:匹配单层主题
|
||||
* - #:匹配多层主题(必须在末尾)
|
||||
*
|
||||
* @param subscription 订阅主题(可能包含通配符)
|
||||
* @param topic 发布主题(不包含通配符)
|
||||
* @return 是否匹配
|
||||
*/
|
||||
private boolean topicMatches(String subscription, String topic) {
|
||||
// 完全匹配
|
||||
if (subscription.equals(topic)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 不包含通配符
|
||||
// TODO @haohao:这里要不要枚举下哈;+ #
|
||||
if (!subscription.contains("+") && !subscription.contains("#")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] subscriptionParts = subscription.split("/");
|
||||
String[] topicParts = topic.split("/");
|
||||
int i = 0;
|
||||
for (; i < subscriptionParts.length && i < topicParts.length; i++) {
|
||||
String subPart = subscriptionParts[i];
|
||||
String topicPart = topicParts[i];
|
||||
|
||||
// # 匹配剩余所有层级,且必须在末尾
|
||||
if (subPart.equals("#")) {
|
||||
return i == subscriptionParts.length - 1;
|
||||
}
|
||||
|
||||
// 不是通配符且不匹配
|
||||
if (!subPart.equals("+") && !subPart.equals(topicPart)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否都匹配完
|
||||
return i == subscriptionParts.length && i == topicParts.length;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* IoT 网关 MQTT WebSocket 协议实现
|
||||
* <p>
|
||||
* 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持:
|
||||
* - 标准 MQTT 3.1.1 协议
|
||||
* - WebSocket 协议升级
|
||||
* - SSL/TLS 加密(wss://)
|
||||
* - 设备认证与连接管理
|
||||
* - QoS 0/1/2 消息质量保证
|
||||
* - 双向消息通信(上行/下行)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.http.ServerWebSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* IoT MQTT WebSocket 下行消息处理器
|
||||
* <p>
|
||||
* 处理从消息总线发送到设备的消息,包括:
|
||||
* - 属性设置
|
||||
* - 服务调用
|
||||
* - 事件通知
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttWsDownstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotMqttWsConnectionManager connectionManager;
|
||||
|
||||
/**
|
||||
* 消息 ID 生成器(用于发布消息)
|
||||
*/
|
||||
private final AtomicInteger messageIdGenerator = new AtomicInteger(1);
|
||||
|
||||
public IotMqttWsDownstreamHandler(IotDeviceMessageService deviceMessageService,
|
||||
IotDeviceService deviceService,
|
||||
IotMqttWsConnectionManager connectionManager) {
|
||||
this.deviceMessageService = deviceMessageService;
|
||||
this.deviceService = deviceService;
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @return 是否处理成功
|
||||
*/
|
||||
public boolean handleDownstreamMessage(IotDeviceMessage message) {
|
||||
try {
|
||||
// 1. 基础校验
|
||||
if (message == null || message.getDeviceId() == null) {
|
||||
log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 获取设备信息
|
||||
IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
|
||||
if (deviceInfo == null) {
|
||||
log.warn("[handleDownstreamMessage][设备不存在,设备 ID:{}]", message.getDeviceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 构建设备标识
|
||||
String deviceKey = deviceInfo.getProductKey() + ":" + deviceInfo.getDeviceName();
|
||||
|
||||
// 4. 检查设备是否在线
|
||||
if (!connectionManager.isOnline(deviceKey)) {
|
||||
log.warn("[handleDownstreamMessage][设备离线,无法发送消息,deviceKey: {}]", deviceKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 构建主题
|
||||
String topic = buildDownstreamTopic(message, deviceInfo);
|
||||
if (StrUtil.isBlank(topic)) {
|
||||
log.warn("[handleDownstreamMessage][主题构建失败,设备 ID:{},方法:{}]",
|
||||
message.getDeviceId(), message.getMethod());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 6. 检查设备是否订阅了该主题
|
||||
if (!connectionManager.isSubscribed(deviceKey, topic)) {
|
||||
log.warn("[handleDownstreamMessage][设备未订阅该主题,deviceKey: {},topic: {}]", deviceKey, topic);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 8. 编码消息
|
||||
byte[] payload = deviceMessageService.encodeDeviceMessage(message,
|
||||
deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
if (payload == null || payload.length == 0) {
|
||||
log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 9. 发送消息到设备
|
||||
return sendMessageToDevice(deviceKey, topic, payload, 1);
|
||||
} catch (Exception e) {
|
||||
if (message != null) {
|
||||
log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]",
|
||||
message.getDeviceId(), e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建下行消息主题
|
||||
*
|
||||
* @param message 设备消息
|
||||
* @param deviceInfo 设备信息
|
||||
* @return 主题
|
||||
*/
|
||||
private String buildDownstreamTopic(IotDeviceMessage message, IotDeviceRespDTO deviceInfo) {
|
||||
String method = message.getMethod();
|
||||
if (StrUtil.isBlank(method)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用工具类构建主题,支持回复消息处理
|
||||
boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
|
||||
return IotMqttTopicUtils.buildTopicByMethod(method, deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName(), isReply);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到设备
|
||||
*
|
||||
* @param deviceKey 设备标识(productKey:deviceName)
|
||||
* @param topic 主题
|
||||
* @param payload 消息内容
|
||||
* @param qos QoS 级别
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
private boolean sendMessageToDevice(String deviceKey, String topic, byte[] payload, int qos) {
|
||||
// 获取设备连接
|
||||
ServerWebSocket socket = connectionManager.getConnection(deviceKey);
|
||||
if (socket == null) {
|
||||
log.warn("[sendMessageToDevice][设备未连接,deviceKey: {}]", deviceKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
int messageId = qos > 0 ? generateMessageId() : 0;
|
||||
|
||||
// 手动编码 MQTT PUBLISH 消息
|
||||
io.netty.buffer.ByteBuf byteBuf = io.netty.buffer.Unpooled.buffer();
|
||||
|
||||
// 固定头:消息类型(PUBLISH=3) + DUP(0) + QoS + RETAIN
|
||||
int fixedHeaderByte1 = 0x30 | (qos << 1); // PUBLISH类型
|
||||
byteBuf.writeByte(fixedHeaderByte1);
|
||||
|
||||
// 计算剩余长度
|
||||
int topicLength = topic.getBytes().length;
|
||||
int remainingLength = 2 + topicLength + (qos > 0 ? 2 : 0) + payload.length;
|
||||
|
||||
// 写入剩余长度(简化版本,假设小于 128 字节)
|
||||
if (remainingLength < 128) {
|
||||
byteBuf.writeByte(remainingLength);
|
||||
} else {
|
||||
// 处理大于 127 的情况
|
||||
int x = remainingLength;
|
||||
do {
|
||||
int encodedByte = x % 128;
|
||||
x = x / 128;
|
||||
if (x > 0) {
|
||||
encodedByte = encodedByte | 128;
|
||||
}
|
||||
byteBuf.writeByte(encodedByte);
|
||||
} while (x > 0);
|
||||
}
|
||||
|
||||
// 可变头:主题名称
|
||||
byteBuf.writeShort(topicLength);
|
||||
byteBuf.writeBytes(topic.getBytes());
|
||||
|
||||
// 可变头:消息 ID(仅 QoS > 0 时)
|
||||
if (qos > 0) {
|
||||
byteBuf.writeShort(messageId);
|
||||
}
|
||||
|
||||
// 有效载荷
|
||||
byteBuf.writeBytes(payload);
|
||||
|
||||
// 发送
|
||||
byte[] bytes = new byte[byteBuf.readableBytes()];
|
||||
byteBuf.readBytes(bytes);
|
||||
byteBuf.release();
|
||||
socket.writeBinaryMessage(Buffer.buffer(bytes));
|
||||
|
||||
log.info("[sendMessageToDevice][消息已发送到设备,deviceKey: {},topic: {},qos: {},messageId: {}]",
|
||||
deviceKey, topic, qos, messageId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[sendMessageToDevice][发送消息到设备失败,deviceKey: {},topic: {}]", deviceKey, topic, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成消息 ID
|
||||
*
|
||||
* @return 消息 ID
|
||||
*/
|
||||
private int generateMessageId() {
|
||||
int id = messageIdGenerator.getAndIncrement();
|
||||
// MQTT 消息 ID 范围是 1-65535
|
||||
// TODO @haohao:并发可能有问题;
|
||||
if (id > 65535) {
|
||||
messageIdGenerator.set(1);
|
||||
return 1;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,753 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router;
|
||||
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.mqtt.*;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.http.ServerWebSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* IoT MQTT WebSocket 上行消息处理器
|
||||
* <p>
|
||||
* 处理来自设备的 MQTT 消息,包括:
|
||||
* - CONNECT:设备连接认证
|
||||
* - PUBLISH:设备发布消息
|
||||
* - SUBSCRIBE:设备订阅主题
|
||||
* - UNSUBSCRIBE:设备取消订阅
|
||||
* - PINGREQ:心跳请求
|
||||
* - DISCONNECT:设备断开连接
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotMqttWsUpstreamHandler {
|
||||
|
||||
private final IotMqttWsUpstreamProtocol upstreamProtocol;
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotMqttWsConnectionManager connectionManager;
|
||||
|
||||
/**
|
||||
* 存储 WebSocket 连接到 Socket ID 的映射
|
||||
* Key: WebSocket 对象
|
||||
* Value: Socket ID(UUID)
|
||||
*/
|
||||
private final ConcurrentHashMap<ServerWebSocket, String> socketIdMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 存储 Socket ID 对应的设备信息
|
||||
* Key: Socket ID(UUID)
|
||||
* Value: 设备信息
|
||||
*/
|
||||
private final ConcurrentHashMap<String, IotDeviceRespDTO> socketDeviceMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 存储设备的消息 ID 生成器(用于 QoS > 0 的消息)
|
||||
*/
|
||||
private final ConcurrentHashMap<String, AtomicInteger> deviceMessageIdMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* MQTT 解码通道(用于解析 WebSocket 中的 MQTT 二进制消息)
|
||||
*/
|
||||
private final ThreadLocal<EmbeddedChannel> decoderChannelThreadLocal = ThreadLocal
|
||||
.withInitial(() -> new EmbeddedChannel(new MqttDecoder()));
|
||||
|
||||
/**
|
||||
* MQTT 编码通道(用于编码 MQTT 响应消息)
|
||||
*/
|
||||
private final ThreadLocal<EmbeddedChannel> encoderChannelThreadLocal = ThreadLocal
|
||||
.withInitial(() -> new EmbeddedChannel(MqttEncoder.INSTANCE));
|
||||
|
||||
public IotMqttWsUpstreamHandler(IotMqttWsUpstreamProtocol upstreamProtocol,
|
||||
IotDeviceMessageService messageService,
|
||||
IotMqttWsConnectionManager connectionManager) {
|
||||
this.upstreamProtocol = upstreamProtocol;
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.messageService = messageService;
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WebSocket 连接
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
*/
|
||||
public void handle(ServerWebSocket socket) {
|
||||
// 生成唯一的 Socket ID(因为 MQTT 使用二进制协议,textHandlerID() 会返回 null)
|
||||
String socketId = IdUtil.simpleUUID();
|
||||
socketIdMap.put(socket, socketId);
|
||||
|
||||
log.info("[handle][WebSocket 连接建立,socketId: {},remoteAddress: {}]",
|
||||
socketId, socket.remoteAddress());
|
||||
|
||||
// 设置二进制数据处理器
|
||||
socket.binaryMessageHandler(buffer -> {
|
||||
try {
|
||||
handleMqttMessage(socket, buffer);
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][处理 MQTT 消息异常,socketId: {}]", socketId, e);
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
|
||||
// 设置关闭处理器
|
||||
socket.closeHandler(v -> {
|
||||
socketIdMap.remove(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.remove(socketId);
|
||||
if (device != null) {
|
||||
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
|
||||
connectionManager.removeConnection(deviceKey);
|
||||
deviceMessageIdMap.remove(deviceKey);
|
||||
// 发送设备离线消息
|
||||
sendOfflineMessage(device);
|
||||
log.info("[handle][WebSocket 连接关闭,deviceKey: {},socketId: {}]", deviceKey, socketId);
|
||||
}
|
||||
});
|
||||
|
||||
// 设置异常处理器
|
||||
socket.exceptionHandler(e -> {
|
||||
log.error("[handle][WebSocket 连接异常,socketId: {}]", socketId, e);
|
||||
socketIdMap.remove(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.remove(socketId);
|
||||
if (device != null) {
|
||||
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
|
||||
connectionManager.removeConnection(deviceKey);
|
||||
deviceMessageIdMap.remove(deviceKey);
|
||||
}
|
||||
socket.close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MQTT 消息
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
* @param buffer 消息缓冲区
|
||||
*/
|
||||
private void handleMqttMessage(ServerWebSocket socket, Buffer buffer) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer.getBytes());
|
||||
|
||||
try {
|
||||
// 使用 EmbeddedChannel 解码 MQTT 消息
|
||||
EmbeddedChannel decoderChannel = decoderChannelThreadLocal.get();
|
||||
decoderChannel.writeInbound(byteBuf.retain());
|
||||
|
||||
// 读取解码后的消息
|
||||
MqttMessage mqttMessage = decoderChannel.readInbound();
|
||||
if (mqttMessage == null) {
|
||||
log.warn("[handleMqttMessage][MQTT 消息解码失败,socketId: {}]", socketId);
|
||||
return;
|
||||
}
|
||||
|
||||
MqttMessageType messageType = mqttMessage.fixedHeader().messageType();
|
||||
log.debug("[handleMqttMessage][收到 MQTT 消息,类型: {},socketId: {}]", messageType, socketId);
|
||||
|
||||
// 根据消息类型分发处理
|
||||
switch (messageType) {
|
||||
case CONNECT:
|
||||
handleConnect(socket, (MqttConnectMessage) mqttMessage);
|
||||
break;
|
||||
case PUBLISH:
|
||||
handlePublish(socket, (MqttPublishMessage) mqttMessage);
|
||||
break;
|
||||
case PUBACK:
|
||||
handlePubAck(socket, mqttMessage);
|
||||
break;
|
||||
case PUBREC:
|
||||
handlePubRec(socket, mqttMessage);
|
||||
break;
|
||||
case PUBREL:
|
||||
handlePubRel(socket, mqttMessage);
|
||||
break;
|
||||
case PUBCOMP:
|
||||
handlePubComp(socket, mqttMessage);
|
||||
break;
|
||||
case SUBSCRIBE:
|
||||
handleSubscribe(socket, (MqttSubscribeMessage) mqttMessage);
|
||||
break;
|
||||
case UNSUBSCRIBE:
|
||||
handleUnsubscribe(socket, (MqttUnsubscribeMessage) mqttMessage);
|
||||
break;
|
||||
case PINGREQ:
|
||||
handlePingReq(socket);
|
||||
break;
|
||||
case DISCONNECT:
|
||||
handleDisconnect(socket);
|
||||
break;
|
||||
default:
|
||||
log.warn("[handleMqttMessage][不支持的消息类型: {},socketId: {}]", messageType, socketId);
|
||||
}
|
||||
} catch (DecoderException e) {
|
||||
log.error("[handleMqttMessage][MQTT 消息解码异常,socketId: {}]", socketId, e);
|
||||
socket.close();
|
||||
} catch (Exception e) {
|
||||
log.error("[handleMqttMessage][处理 MQTT 消息失败,socketId: {}]", socketId, e);
|
||||
socket.close();
|
||||
} finally {
|
||||
byteBuf.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 CONNECT 消息(设备认证)
|
||||
*/
|
||||
private void handleConnect(ServerWebSocket socket, MqttConnectMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
try {
|
||||
// 1. 解析 CONNECT 消息
|
||||
MqttConnectPayload payload = message.payload();
|
||||
String clientId = payload.clientIdentifier();
|
||||
String username = payload.userName();
|
||||
String password = payload.passwordInBytes() != null
|
||||
? new String(payload.passwordInBytes(), StandardCharsets.UTF_8)
|
||||
: null;
|
||||
|
||||
log.info("[handleConnect][收到 CONNECT 消息,clientId: {},username: {},socketId: {}]",
|
||||
clientId, username, socketId);
|
||||
|
||||
// 2. 设备认证
|
||||
IotDeviceRespDTO device = authenticateDevice(clientId, username, password);
|
||||
if (device == null) {
|
||||
log.warn("[handleConnect][设备认证失败,clientId: {},socketId: {}]", clientId, socketId);
|
||||
sendConnAck(socket, MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 保存设备信息
|
||||
socketDeviceMap.put(socketId, device);
|
||||
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
|
||||
connectionManager.addConnection(deviceKey, socket, socketId);
|
||||
deviceMessageIdMap.put(deviceKey, new AtomicInteger(1));
|
||||
|
||||
log.info("[handleConnect][设备认证成功,deviceId: {},deviceKey: {},socketId: {}]",
|
||||
device.getId(), deviceKey, socketId);
|
||||
|
||||
// 4. 发送 CONNACK
|
||||
sendConnAck(socket, MqttConnectReturnCode.CONNECTION_ACCEPTED);
|
||||
|
||||
// 5. 发送设备上线消息
|
||||
sendOnlineMessage(device);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleConnect][处理 CONNECT 消息失败,socketId: {}]", socketId, e);
|
||||
sendConnAck(socket, MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE);
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PUBLISH 消息(设备发布消息)
|
||||
*/
|
||||
private void handlePublish(ServerWebSocket socket, MqttPublishMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
|
||||
if (device == null) {
|
||||
log.warn("[handlePublish][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 解析 PUBLISH 消息
|
||||
MqttFixedHeader fixedHeader = message.fixedHeader();
|
||||
MqttPublishVariableHeader variableHeader = message.variableHeader();
|
||||
ByteBuf payload = message.payload();
|
||||
|
||||
String topic = variableHeader.topicName();
|
||||
int messageId = variableHeader.packetId();
|
||||
MqttQoS qos = fixedHeader.qosLevel();
|
||||
|
||||
log.debug("[handlePublish][收到 PUBLISH 消息,topic: {},messageId: {},QoS: {},deviceId: {}]",
|
||||
topic, messageId, qos, device.getId());
|
||||
|
||||
// 2. 读取 payload
|
||||
byte[] payloadBytes = new byte[payload.readableBytes()];
|
||||
payload.readBytes(payloadBytes);
|
||||
|
||||
// 3. 解码并发送消息
|
||||
IotDeviceMessage deviceMessage = messageService.decodeDeviceMessage(payloadBytes,
|
||||
device.getProductKey(), device.getDeviceName());
|
||||
if (deviceMessage != null) {
|
||||
deviceMessage.setServerId(upstreamProtocol.getServerId());
|
||||
messageService.sendDeviceMessage(deviceMessage, device.getProductKey(),
|
||||
device.getDeviceName(), upstreamProtocol.getServerId());
|
||||
log.info("[handlePublish][设备消息已发送,method: {},deviceId: {}]",
|
||||
deviceMessage.getMethod(), device.getId());
|
||||
}
|
||||
|
||||
// 4. 根据 QoS 级别发送相应的确认消息
|
||||
if (qos == MqttQoS.AT_LEAST_ONCE) {
|
||||
// QoS 1:发送 PUBACK
|
||||
sendPubAck(socket, messageId);
|
||||
} else if (qos == MqttQoS.EXACTLY_ONCE) {
|
||||
// QoS 2:发送 PUBREC
|
||||
sendPubRec(socket, messageId);
|
||||
}
|
||||
// QoS 0 无需确认
|
||||
} catch (Exception e) {
|
||||
log.error("[handlePublish][处理 PUBLISH 消息失败,deviceId: {}]", device.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PUBACK 消息(QoS 1 确认)
|
||||
*/
|
||||
private void handlePubAck(ServerWebSocket socket, MqttMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
if (device == null) {
|
||||
log.warn("[handlePubAck][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
|
||||
log.debug("[handlePubAck][收到 PUBACK,messageId: {},deviceId: {}]", messageId, device.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PUBREC 消息(QoS 2 第一步确认)
|
||||
*/
|
||||
private void handlePubRec(ServerWebSocket socket, MqttMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
if (device == null) {
|
||||
log.warn("[handlePubRec][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
|
||||
log.debug("[handlePubRec][收到 PUBREC,messageId: {},deviceId: {}]", messageId, device.getId());
|
||||
// 发送 PUBREL
|
||||
sendPubRel(socket, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PUBREL 消息(QoS 2 第二步)
|
||||
*/
|
||||
private void handlePubRel(ServerWebSocket socket, MqttMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
|
||||
if (device == null) {
|
||||
log.warn("[handlePubRel][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
|
||||
log.debug("[handlePubRel][收到 PUBREL,messageId: {},deviceId: {}]", messageId, device.getId());
|
||||
// 发送 PUBCOMP
|
||||
sendPubComp(socket, messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PUBCOMP 消息(QoS 2 完成确认)
|
||||
*/
|
||||
private void handlePubComp(ServerWebSocket socket, MqttMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
|
||||
if (device == null) {
|
||||
log.warn("[handlePubComp][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int messageId = ((MqttMessageIdVariableHeader) message.variableHeader()).messageId();
|
||||
log.debug("[handlePubComp][收到 PUBCOMP,messageId: {},deviceId: {}]", messageId, device.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SUBSCRIBE 消息(设备订阅主题)
|
||||
*/
|
||||
private void handleSubscribe(ServerWebSocket socket, MqttSubscribeMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
if (device == null) {
|
||||
log.warn("[handleSubscribe][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 解析 SUBSCRIBE 消息
|
||||
int messageId = message.variableHeader().messageId();
|
||||
MqttSubscribePayload payload = message.payload();
|
||||
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
|
||||
|
||||
log.info("[handleSubscribe][设备订阅请求,deviceKey: {},messageId: {},主题数量: {}]",
|
||||
deviceKey, messageId, payload.topicSubscriptions().size());
|
||||
|
||||
// 2. 构建 QoS 列表并记录订阅信息
|
||||
int[] grantedQosList = new int[payload.topicSubscriptions().size()];
|
||||
for (int i = 0; i < payload.topicSubscriptions().size(); i++) {
|
||||
MqttTopicSubscription subscription = payload.topicSubscriptions().get(i);
|
||||
String topic = subscription.topicFilter();
|
||||
grantedQosList[i] = subscription.qualityOfService().value();
|
||||
|
||||
// 记录订阅信息到连接管理器
|
||||
connectionManager.addSubscription(deviceKey, topic);
|
||||
|
||||
log.info("[handleSubscribe][订阅主题: {},QoS: {},deviceKey: {}]",
|
||||
topic, subscription.qualityOfService(), deviceKey);
|
||||
}
|
||||
|
||||
// 3. 发送 SUBACK
|
||||
sendSubAck(socket, messageId, grantedQosList);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleSubscribe][处理 SUBSCRIBE 消息失败,deviceId: {}]", device.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 UNSUBSCRIBE 消息(设备取消订阅)
|
||||
*/
|
||||
private void handleUnsubscribe(ServerWebSocket socket, MqttUnsubscribeMessage message) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
if (device == null) {
|
||||
log.warn("[handleUnsubscribe][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 解析 UNSUBSCRIBE 消息
|
||||
int messageId = message.variableHeader().messageId();
|
||||
MqttUnsubscribePayload payload = message.payload();
|
||||
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
|
||||
|
||||
log.info("[handleUnsubscribe][设备取消订阅,deviceKey: {},messageId: {},主题数量: {}]",
|
||||
deviceKey, messageId, payload.topics().size());
|
||||
|
||||
// 2. 移除订阅信息
|
||||
for (String topic : payload.topics()) {
|
||||
connectionManager.removeSubscription(deviceKey, topic);
|
||||
log.info("[handleUnsubscribe][取消订阅主题: {},deviceKey: {}]", topic, deviceKey);
|
||||
}
|
||||
|
||||
// 3. 发送 UNSUBACK
|
||||
sendUnsubAck(socket, messageId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleUnsubscribe][处理 UNSUBSCRIBE 消息失败,deviceId: {}]", device.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PINGREQ 消息(心跳请求)
|
||||
*/
|
||||
private void handlePingReq(ServerWebSocket socket) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.get(socketId);
|
||||
if (device == null) {
|
||||
log.warn("[handlePingReq][设备未认证,socketId: {}]", socketId);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("[handlePingReq][收到心跳请求,deviceId: {}]", device.getId());
|
||||
// 发送 PINGRESP
|
||||
sendPingResp(socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 DISCONNECT 消息(设备断开连接)
|
||||
*/
|
||||
private void handleDisconnect(ServerWebSocket socket) {
|
||||
String socketId = socketIdMap.get(socket);
|
||||
IotDeviceRespDTO device = socketDeviceMap.remove(socketId);
|
||||
if (device != null) {
|
||||
String deviceKey = device.getProductKey() + ":" + device.getDeviceName();
|
||||
connectionManager.removeConnection(deviceKey);
|
||||
deviceMessageIdMap.remove(deviceKey);
|
||||
sendOfflineMessage(device);
|
||||
log.info("[handleDisconnect][设备主动断开连接,deviceKey: {}]", deviceKey);
|
||||
}
|
||||
|
||||
socket.close();
|
||||
}
|
||||
|
||||
// ==================== 设备认证和状态相关方法 ====================
|
||||
|
||||
/**
|
||||
* 设备认证
|
||||
*/
|
||||
private IotDeviceRespDTO authenticateDevice(String clientId, String username, String password) {
|
||||
try {
|
||||
// 1. 参数校验
|
||||
if (StrUtil.hasEmpty(clientId, username, password)) {
|
||||
log.warn("[authenticateDevice][认证参数不完整,clientId: {},username: {}]", clientId, username);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 构建认证参数并调用 API
|
||||
IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
|
||||
.setClientId(clientId)
|
||||
.setUsername(username)
|
||||
.setPassword(password);
|
||||
|
||||
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
|
||||
if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) {
|
||||
log.warn("[authenticateDevice][设备认证失败,clientId: {}]", clientId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 获取设备信息
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
|
||||
if (deviceInfo == null) {
|
||||
log.warn("[authenticateDevice][用户名格式不正确,username: {}]", username);
|
||||
return null;
|
||||
}
|
||||
|
||||
IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO()
|
||||
.setProductKey(deviceInfo.getProductKey())
|
||||
.setDeviceName(deviceInfo.getDeviceName());
|
||||
|
||||
CommonResult<IotDeviceRespDTO> deviceResult = deviceApi.getDevice(getReqDTO);
|
||||
if (!deviceResult.isSuccess() || deviceResult.getData() == null) {
|
||||
log.warn("[authenticateDevice][获取设备信息失败,username: {}]", username);
|
||||
return null;
|
||||
}
|
||||
|
||||
return deviceResult.getData();
|
||||
} catch (Exception e) {
|
||||
log.error("[authenticateDevice][设备认证异常,clientId: {}]", clientId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备上线消息
|
||||
*/
|
||||
private void sendOnlineMessage(IotDeviceRespDTO device) {
|
||||
try {
|
||||
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
|
||||
messageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), upstreamProtocol.getServerId());
|
||||
log.info("[sendOnlineMessage][设备上线,deviceId: {}]", device.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[sendOnlineMessage][发送设备上线消息失败,deviceId: {}]", device.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备离线消息
|
||||
*/
|
||||
private void sendOfflineMessage(IotDeviceRespDTO device) {
|
||||
try {
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
messageService.sendDeviceMessage(offlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), upstreamProtocol.getServerId());
|
||||
log.info("[sendOfflineMessage][设备离线,deviceId: {}]", device.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[sendOfflineMessage][发送设备离线消息失败,deviceId: {}]", device.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 发送响应消息的辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 发送 CONNACK 消息
|
||||
*/
|
||||
private void sendConnAck(ServerWebSocket socket, MqttConnectReturnCode returnCode) {
|
||||
try {
|
||||
// 构建 CONNACK 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
|
||||
MqttConnAckVariableHeader variableHeader = new MqttConnAckVariableHeader(returnCode, false);
|
||||
MqttConnAckMessage connAckMessage = new MqttConnAckMessage(fixedHeader, variableHeader);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, connAckMessage);
|
||||
log.debug("[sendConnAck][发送 CONNACK 消息,returnCode: {}]", returnCode);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendConnAck][发送 CONNACK 消息失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 PUBACK 消息(QoS 1 确认)
|
||||
*/
|
||||
private void sendPubAck(ServerWebSocket socket, int messageId) {
|
||||
try {
|
||||
// 构建 PUBACK 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
|
||||
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
|
||||
MqttMessage pubAckMessage = new MqttMessage(fixedHeader, variableHeader);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, pubAckMessage);
|
||||
log.debug("[sendPubAck][发送 PUBACK 消息,messageId: {}]", messageId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendPubAck][发送 PUBACK 消息失败,messageId: {}]", messageId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 PUBREC 消息(QoS 2 第一步确认)
|
||||
*/
|
||||
private void sendPubRec(ServerWebSocket socket, int messageId) {
|
||||
try {
|
||||
// 构建 PUBREC 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0);
|
||||
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
|
||||
MqttMessage pubRecMessage = new MqttMessage(fixedHeader, variableHeader);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, pubRecMessage);
|
||||
log.debug("[sendPubRec][发送 PUBREC 消息,messageId: {}]", messageId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendPubRec][发送 PUBREC 消息失败,messageId: {}]", messageId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 PUBREL 消息(QoS 2 第二步)
|
||||
*/
|
||||
private void sendPubRel(ServerWebSocket socket, int messageId) {
|
||||
try {
|
||||
// 构建 PUBREL 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.PUBREL, false, MqttQoS.AT_LEAST_ONCE, false, 0);
|
||||
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
|
||||
MqttMessage pubRelMessage = new MqttMessage(fixedHeader, variableHeader);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, pubRelMessage);
|
||||
log.debug("[sendPubRel][发送 PUBREL 消息,messageId: {}]", messageId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendPubRel][发送 PUBREL 消息失败,messageId: {}]", messageId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 PUBCOMP 消息(QoS 2 完成确认)
|
||||
*/
|
||||
private void sendPubComp(ServerWebSocket socket, int messageId) {
|
||||
try {
|
||||
// 构建 PUBCOMP 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0);
|
||||
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
|
||||
MqttMessage pubCompMessage = new MqttMessage(fixedHeader, variableHeader);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, pubCompMessage);
|
||||
log.debug("[sendPubComp][发送 PUBCOMP 消息,messageId: {}]", messageId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendPubComp][发送 PUBCOMP 消息失败,messageId: {}]", messageId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 SUBACK 消息
|
||||
*/
|
||||
private void sendSubAck(ServerWebSocket socket, int messageId, int[] grantedQosList) {
|
||||
try {
|
||||
// 构建 SUBACK 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
|
||||
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
|
||||
MqttSubAckPayload payload = new MqttSubAckPayload(grantedQosList);
|
||||
MqttSubAckMessage subAckMessage = new MqttSubAckMessage(fixedHeader, variableHeader, payload);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, subAckMessage);
|
||||
log.debug("[sendSubAck][发送 SUBACK 消息,messageId: {},主题数量: {}]", messageId, grantedQosList.length);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendSubAck][发送 SUBACK 消息失败,messageId: {}]", messageId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 UNSUBACK 消息
|
||||
*/
|
||||
private void sendUnsubAck(ServerWebSocket socket, int messageId) {
|
||||
try {
|
||||
// 构建 UNSUBACK 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
|
||||
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
|
||||
MqttUnsubAckMessage unsubAckMessage = new MqttUnsubAckMessage(fixedHeader, variableHeader);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, unsubAckMessage);
|
||||
log.debug("[sendUnsubAck][发送 UNSUBACK 消息,messageId: {}]", messageId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendUnsubAck][发送 UNSUBACK 消息失败,messageId: {}]", messageId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 PINGRESP 消息
|
||||
*/
|
||||
private void sendPingResp(ServerWebSocket socket) {
|
||||
try {
|
||||
// 构建 PINGRESP 消息
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(
|
||||
MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0);
|
||||
MqttMessage pingRespMessage = new MqttMessage(fixedHeader);
|
||||
|
||||
// 编码并发送
|
||||
sendMqttMessage(socket, pingRespMessage);
|
||||
log.debug("[sendPingResp][发送 PINGRESP 消息]");
|
||||
} catch (Exception e) {
|
||||
log.error("[sendPingResp][发送 PINGRESP 消息失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 MQTT 消息到 WebSocket
|
||||
*/
|
||||
private void sendMqttMessage(ServerWebSocket socket, MqttMessage mqttMessage) {
|
||||
ByteBuf byteBuf = null;
|
||||
try {
|
||||
// 使用 EmbeddedChannel 编码 MQTT 消息
|
||||
EmbeddedChannel encoderChannel = encoderChannelThreadLocal.get();
|
||||
encoderChannel.writeOutbound(mqttMessage);
|
||||
|
||||
// 读取编码后的 ByteBuf
|
||||
byteBuf = encoderChannel.readOutbound();
|
||||
if (byteBuf != null) {
|
||||
byte[] bytes = new byte[byteBuf.readableBytes()];
|
||||
byteBuf.readBytes(bytes);
|
||||
socket.writeBinaryMessage(Buffer.buffer(bytes));
|
||||
}
|
||||
} finally {
|
||||
if (byteBuf != null) {
|
||||
byteBuf.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -25,8 +24,6 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotTcpConnectionManager connectionManager;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
@@ -36,8 +33,8 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDevic
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 初始化下游处理器
|
||||
this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager);
|
||||
|
||||
this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager);
|
||||
// 注册下游订阅者
|
||||
messageBus.register(this);
|
||||
log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
|
||||
protocol.getServerId(), getTopic());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -50,9 +51,9 @@ public class IotTcpConnectionManager {
|
||||
connectionMap.remove(oldSocket);
|
||||
}
|
||||
|
||||
// 注册新连接
|
||||
connectionMap.put(socket, connectionInfo);
|
||||
deviceSocketMap.put(deviceId, socket);
|
||||
|
||||
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
|
||||
deviceId, socket.remoteAddress(), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
|
||||
}
|
||||
@@ -64,12 +65,12 @@ public class IotTcpConnectionManager {
|
||||
*/
|
||||
public void unregisterConnection(NetSocket socket) {
|
||||
ConnectionInfo connectionInfo = connectionMap.remove(socket);
|
||||
if (connectionInfo != null) {
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
deviceSocketMap.remove(deviceId);
|
||||
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]",
|
||||
deviceId, socket.remoteAddress());
|
||||
if (connectionInfo == null) {
|
||||
return;
|
||||
}
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
deviceSocketMap.remove(deviceId);
|
||||
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +78,7 @@ public class IotTcpConnectionManager {
|
||||
*/
|
||||
public boolean isAuthenticated(NetSocket socket) {
|
||||
ConnectionInfo info = connectionMap.get(socket);
|
||||
return info != null && info.isAuthenticated();
|
||||
return info != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,17 +96,11 @@ public class IotTcpConnectionManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否在线
|
||||
* 根据设备 ID 获取连接信息
|
||||
*/
|
||||
public boolean isDeviceOnline(Long deviceId) {
|
||||
return deviceSocketMap.containsKey(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否离线
|
||||
*/
|
||||
public boolean isDeviceOffline(Long deviceId) {
|
||||
return !isDeviceOnline(deviceId);
|
||||
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
|
||||
NetSocket socket = deviceSocketMap.get(deviceId);
|
||||
return socket != null ? connectionMap.get(socket) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +114,7 @@ public class IotTcpConnectionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
socket.write(io.vertx.core.buffer.Buffer.buffer(data));
|
||||
socket.write(Buffer.buffer(data));
|
||||
log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
@@ -157,11 +152,6 @@ public class IotTcpConnectionManager {
|
||||
* 消息编解码类型(认证后确定)
|
||||
*/
|
||||
private String codecType;
|
||||
// TODO @haohao:有没可能不要 authenticated 字段,通过 deviceId 或者其他的?进一步简化,想的是哈。
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
private boolean authenticated;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* TCP 协议实现包
|
||||
* <p>
|
||||
* 提供基于 Vert.x TCP Server 的 IoT 设备连接和消息处理功能
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
|
||||
@@ -1,9 +1,7 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -19,8 +17,6 @@ public class IotTcpDownstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotTcpConnectionManager connectionManager;
|
||||
|
||||
/**
|
||||
@@ -31,21 +27,16 @@ public class IotTcpDownstreamHandler {
|
||||
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
|
||||
// 1.1 获取设备信息
|
||||
IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());
|
||||
if (deviceInfo == null) {
|
||||
log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
// 1.2 检查设备是否在线
|
||||
if (connectionManager.isDeviceOffline(message.getDeviceId())) {
|
||||
log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
|
||||
// 1. 获取连接信息(包含 codecType)
|
||||
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
|
||||
message.getDeviceId());
|
||||
if (connectionInfo == null) {
|
||||
log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 根据产品 Key 和设备名称编码消息并发送到设备
|
||||
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
// 2. 使用连接时的 codecType 编码消息,并发送到设备
|
||||
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
|
||||
boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
|
||||
if (success) {
|
||||
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
|
||||
|
||||
@@ -10,7 +10,11 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
|
||||
@@ -23,6 +27,8 @@ import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* TCP 上行消息处理器
|
||||
*
|
||||
@@ -74,6 +80,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
|
||||
// 设置消息处理器
|
||||
socket.handler(buffer -> {
|
||||
// TODO @AI:TODO @芋艿:这里应该有拆粘包的问题;
|
||||
try {
|
||||
processMessage(clientId, buffer, socket);
|
||||
} catch (Exception e) {
|
||||
@@ -119,6 +126,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
if (AUTH_METHOD.equals(message.getMethod())) {
|
||||
// 认证请求
|
||||
handleAuthenticationRequest(clientId, message, codecType, socket);
|
||||
} else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
|
||||
// 设备动态注册请求
|
||||
handleRegisterRequest(clientId, message, codecType, socket);
|
||||
} else {
|
||||
// 业务消息
|
||||
handleBusinessRequest(clientId, message, codecType, socket);
|
||||
@@ -162,7 +172,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
}
|
||||
|
||||
// 2.1 解析设备信息
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
|
||||
if (deviceInfo == null) {
|
||||
sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType);
|
||||
return;
|
||||
@@ -189,6 +199,44 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备动态注册请求(一型一密,不需要认证)
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param message 消息信息
|
||||
* @param codecType 消息编解码类型
|
||||
* @param socket 网络连接
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType,
|
||||
NetSocket socket) {
|
||||
try {
|
||||
// 1. 解析注册参数
|
||||
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
|
||||
if (params == null) {
|
||||
log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
|
||||
sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
if (result.isError()) {
|
||||
log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
|
||||
sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 发送成功响应(包含 deviceSecret)
|
||||
sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType);
|
||||
log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]",
|
||||
clientId, params.getDeviceName());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
|
||||
sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务请求
|
||||
*
|
||||
@@ -229,8 +277,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
private String getMessageCodecType(Buffer buffer, NetSocket socket) {
|
||||
// 1. 如果已认证,优先使用缓存的编解码类型
|
||||
IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
|
||||
if (connectionInfo != null && connectionInfo.isAuthenticated() &&
|
||||
StrUtil.isNotBlank(connectionInfo.getCodecType())) {
|
||||
if (connectionInfo != null
|
||||
&& StrUtil.isNotBlank(connectionInfo.getCodecType())) {
|
||||
return connectionInfo.getCodecType();
|
||||
}
|
||||
|
||||
@@ -254,8 +302,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
.setProductKey(device.getProductKey())
|
||||
.setDeviceName(device.getDeviceName())
|
||||
.setClientId(clientId)
|
||||
.setCodecType(codecType)
|
||||
.setAuthenticated(true);
|
||||
.setCodecType(codecType);
|
||||
// 注册连接
|
||||
connectionManager.registerConnection(socket, device.getId(), connectionInfo);
|
||||
}
|
||||
@@ -375,34 +422,87 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 认证参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({"unchecked", "DuplicatedCode"})
|
||||
private IotDeviceAuthReqDTO parseAuthParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof java.util.Map) {
|
||||
java.util.Map<String, Object> paramMap = (java.util.Map<String, Object>) params;
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) params;
|
||||
return new IotDeviceAuthReqDTO()
|
||||
.setClientId(MapUtil.getStr(paramMap, "clientId"))
|
||||
.setUsername(MapUtil.getStr(paramMap, "username"))
|
||||
.setPassword(MapUtil.getStr(paramMap, "password"));
|
||||
}
|
||||
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceAuthReqDTO) {
|
||||
return (IotDeviceAuthReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
String jsonStr = JsonUtils.toJsonString(params);
|
||||
return JsonUtils.parseObject(jsonStr, IotDeviceAuthReqDTO.class);
|
||||
return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析注册参数
|
||||
*
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 注册参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "DuplicatedCode"})
|
||||
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) params;
|
||||
return new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
|
||||
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
|
||||
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
|
||||
}
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceRegisterReqDTO) {
|
||||
return (IotDeviceRegisterReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
String jsonStr = JsonUtils.toJsonString(params);
|
||||
return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册成功响应(包含 deviceSecret)
|
||||
*
|
||||
* @param socket 网络连接
|
||||
* @param requestId 请求 ID
|
||||
* @param registerResp 注册响应
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
private void sendRegisterSuccessResponse(NetSocket socket, String requestId,
|
||||
IotDeviceRegisterRespDTO registerResp, String codecType) {
|
||||
try {
|
||||
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
|
||||
// 2. 发送响应
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
|
||||
socket.write(Buffer.buffer(encodedData));
|
||||
} catch (Exception e) {
|
||||
log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 UDP 下游订阅者:接收下行给设备的消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class IotUdpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotUdpUpstreamProtocol protocol;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotUdpSessionManager sessionManager;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
private IotUdpDownstreamHandler downstreamHandler;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 初始化下游处理器
|
||||
this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol);
|
||||
// 注册下游订阅者
|
||||
messageBus.register(this);
|
||||
log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
|
||||
protocol.getServerId(), getTopic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
try {
|
||||
downstreamHandler.handle(message);
|
||||
} catch (Exception e) {
|
||||
log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.datagram.DatagramSocket;
|
||||
import io.vertx.core.datagram.DatagramSocketOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 网关 UDP 协议:接收设备上行消息
|
||||
* <p>
|
||||
* 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能:
|
||||
* 1. 监听 UDP 端口,接收设备消息
|
||||
* 2. 定期清理不活跃的设备地址映射
|
||||
* 3. 提供 UDP Socket 用于下行消息发送
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotUdpUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.UdpProperties udpProperties;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotUdpSessionManager sessionManager;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
@Getter
|
||||
private DatagramSocket udpSocket;
|
||||
|
||||
/**
|
||||
* 会话清理定时器 ID
|
||||
*/
|
||||
private Long cleanTimerId;
|
||||
|
||||
private IotUdpUpstreamHandler upstreamHandler;
|
||||
|
||||
public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties,
|
||||
IotDeviceService deviceService,
|
||||
IotDeviceMessageService messageService,
|
||||
IotUdpSessionManager sessionManager,
|
||||
Vertx vertx) {
|
||||
this.udpProperties = udpProperties;
|
||||
this.deviceService = deviceService;
|
||||
this.messageService = messageService;
|
||||
this.sessionManager = sessionManager;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort());
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// 1. 初始化上行消息处理器
|
||||
this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager);
|
||||
|
||||
// 2. 创建 UDP Socket 选项
|
||||
DatagramSocketOptions options = new DatagramSocketOptions()
|
||||
.setReceiveBufferSize(udpProperties.getReceiveBufferSize())
|
||||
.setSendBufferSize(udpProperties.getSendBufferSize())
|
||||
.setReuseAddress(true);
|
||||
|
||||
// 3. 创建 UDP Socket
|
||||
udpSocket = vertx.createDatagramSocket(options);
|
||||
|
||||
// 4. 监听端口
|
||||
udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> {
|
||||
if (result.failed()) {
|
||||
log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause());
|
||||
return;
|
||||
}
|
||||
// 设置数据包处理器
|
||||
udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket));
|
||||
log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]",
|
||||
udpProperties.getPort(), udpProperties.getReceiveBufferSize(),
|
||||
udpProperties.getSendBufferSize());
|
||||
|
||||
// 5. 启动会话清理定时器
|
||||
startSessionCleanTimer();
|
||||
});
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
// 1. 取消会话清理定时器
|
||||
if (cleanTimerId != null) {
|
||||
vertx.cancelTimer(cleanTimerId);
|
||||
cleanTimerId = null;
|
||||
log.info("[stop][会话清理定时器已取消]");
|
||||
}
|
||||
|
||||
// 2. 关闭 UDP Socket
|
||||
if (udpSocket != null) {
|
||||
try {
|
||||
udpSocket.close().result();
|
||||
log.info("[stop][IoT 网关 UDP 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 UDP 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动会话清理定时器
|
||||
*/
|
||||
private void startSessionCleanTimer() {
|
||||
cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> {
|
||||
try {
|
||||
// 1. 清理超时的设备地址映射,并获取离线设备列表
|
||||
List<Long> offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs());
|
||||
|
||||
// 2. 为每个离线设备发送离线消息
|
||||
for (Long deviceId : offlineDeviceIds) {
|
||||
sendOfflineMessage(deviceId);
|
||||
}
|
||||
if (CollUtil.isNotEmpty(offlineDeviceIds)) {
|
||||
log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[cleanExpiredMappings][清理超时会话失败]", e);
|
||||
}
|
||||
});
|
||||
log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]",
|
||||
udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备离线消息
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
*/
|
||||
private void sendOfflineMessage(Long deviceId) {
|
||||
try {
|
||||
// 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId);
|
||||
if (device == null) {
|
||||
log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送离线消息
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
messageService.sendDeviceMessage(offlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), serverId);
|
||||
log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]",
|
||||
deviceId, device.getDeviceName());
|
||||
} catch (Exception e) {
|
||||
log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.datagram.DatagramSocket;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* IoT 网关 UDP 会话管理器
|
||||
* <p>
|
||||
* 采用无状态设计,SessionManager 主要用于:
|
||||
* 1. 管理设备地址映射(用于下行消息发送)
|
||||
* 2. 定期清理不活跃的设备地址映射
|
||||
* <p>
|
||||
* 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IotUdpSessionManager {
|
||||
|
||||
/**
|
||||
* 设备 ID -> 会话信息(包含地址和 codecType)
|
||||
*/
|
||||
private final Map<Long, SessionInfo> deviceSessionMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 设备地址 Key -> 最后活跃时间(用于清理)
|
||||
*/
|
||||
private final Map<String, LocalDateTime> lastActiveTimeMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 设备地址 Key -> 设备 ID(反向映射,用于清理时同步)
|
||||
*/
|
||||
private final Map<String, Long> addressDeviceMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 更新设备会话(每次收到上行消息时调用)
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @param address 设备地址
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) {
|
||||
String addressKey = buildAddressKey(address);
|
||||
// 更新设备会话映射
|
||||
deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType));
|
||||
lastActiveTimeMap.put(addressKey, LocalDateTime.now());
|
||||
addressDeviceMap.put(addressKey, deviceId);
|
||||
log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备地址(兼容旧接口,默认不更新 codecType)
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @param address 设备地址
|
||||
*/
|
||||
public void updateDeviceAddress(Long deviceId, InetSocketAddress address) {
|
||||
SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
|
||||
String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null;
|
||||
updateDeviceSession(deviceId, address, codecType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备会话信息
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @return 会话信息
|
||||
*/
|
||||
public SessionInfo getSessionInfo(Long deviceId) {
|
||||
return deviceSessionMap.get(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否在线(即是否有地址映射)
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @return 是否在线
|
||||
*/
|
||||
public boolean isDeviceOnline(Long deviceId) {
|
||||
return deviceSessionMap.containsKey(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否离线
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @return 是否离线
|
||||
*/
|
||||
public boolean isDeviceOffline(Long deviceId) {
|
||||
return !isDeviceOnline(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到设备
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @param data 数据
|
||||
* @param socket UDP Socket
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) {
|
||||
SessionInfo sessionInfo = deviceSessionMap.get(deviceId);
|
||||
if (sessionInfo == null || sessionInfo.getAddress() == null) {
|
||||
log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
InetSocketAddress address = sessionInfo.getAddress();
|
||||
try {
|
||||
socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
|
||||
if (result.succeeded()) {
|
||||
log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
|
||||
deviceId, buildAddressKey(address), data.length);
|
||||
} else {
|
||||
log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
|
||||
deviceId, buildAddressKey(address), result.cause());
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期清理不活跃的设备地址映射
|
||||
*
|
||||
* @param timeoutMs 超时时间(毫秒)
|
||||
* @return 清理的设备 ID 列表(用于发送离线消息)
|
||||
*/
|
||||
public List<Long> cleanExpiredMappings(long timeoutMs) {
|
||||
List<Long> offlineDeviceIds = new ArrayList<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000);
|
||||
Iterator<Map.Entry<String, LocalDateTime>> iterator = lastActiveTimeMap.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
// 未过期,跳过
|
||||
Map.Entry<String, LocalDateTime> entry = iterator.next();
|
||||
if (entry.getValue().isAfter(expireTime)) {
|
||||
continue;
|
||||
}
|
||||
// 过期处理:记录离线设备 ID
|
||||
String addressKey = entry.getKey();
|
||||
Long deviceId = addressDeviceMap.remove(addressKey);
|
||||
if (deviceId == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
SessionInfo sessionInfo = deviceSessionMap.remove(deviceId);
|
||||
if (sessionInfo == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
offlineDeviceIds.add(deviceId);
|
||||
log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]",
|
||||
deviceId, addressKey, entry.getValue());
|
||||
iterator.remove();
|
||||
}
|
||||
return offlineDeviceIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建地址 Key
|
||||
*
|
||||
* @param address 地址
|
||||
* @return 地址 Key
|
||||
*/
|
||||
public String buildAddressKey(InetSocketAddress address) {
|
||||
return address.getHostString() + ":" + address.getPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话信息
|
||||
*/
|
||||
@Data
|
||||
public static class SessionInfo {
|
||||
|
||||
/**
|
||||
* 设备地址
|
||||
*/
|
||||
private InetSocketAddress address;
|
||||
|
||||
/**
|
||||
* 消息编解码类型
|
||||
*/
|
||||
private String codecType;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* UDP 协议实现包
|
||||
* <p>
|
||||
* 提供基于 Vert.x DatagramSocket 的 IoT 设备连接和消息处理功能
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
|
||||
@@ -0,0 +1,70 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.datagram.DatagramSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 UDP 下行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotUdpDownstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotUdpSessionManager sessionManager;
|
||||
|
||||
private final IotUdpUpstreamProtocol protocol;
|
||||
|
||||
public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService,
|
||||
IotUdpSessionManager sessionManager,
|
||||
IotUdpUpstreamProtocol protocol) {
|
||||
this.deviceMessageService = deviceMessageService;
|
||||
this.sessionManager = sessionManager;
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*
|
||||
* @param message 下行消息
|
||||
*/
|
||||
public void handle(IotDeviceMessage message) {
|
||||
try {
|
||||
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
|
||||
// 1. 获取会话信息(包含 codecType)
|
||||
IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId());
|
||||
if (sessionInfo == null) {
|
||||
log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 使用会话中的 codecType 编码消息,并发送到设备
|
||||
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType());
|
||||
DatagramSocket socket = protocol.getUdpSocket();
|
||||
if (socket == null) {
|
||||
log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket);
|
||||
if (success) {
|
||||
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
|
||||
} else {
|
||||
log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.datagram.DatagramPacket;
|
||||
import io.vertx.core.datagram.DatagramSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* UDP 上行消息处理器
|
||||
* <p>
|
||||
* 采用无状态 Token 机制(每次请求携带 token):
|
||||
* 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password
|
||||
* 2. 返回 Token:服务端验证后返回 JWT token
|
||||
* 3. 后续请求:每次请求在 params 中携带 token
|
||||
* 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotUdpUpstreamHandler {
|
||||
|
||||
private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
|
||||
private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
|
||||
|
||||
private static final String AUTH_METHOD = "auth";
|
||||
/**
|
||||
* Token 参数 Key
|
||||
*/
|
||||
private static final String PARAM_KEY_TOKEN = "token";
|
||||
/**
|
||||
* Body 参数 Key(实际请求内容)
|
||||
*/
|
||||
private static final String PARAM_KEY_BODY = "body";
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotUdpSessionManager sessionManager;
|
||||
|
||||
private final IotDeviceTokenService deviceTokenService;
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol,
|
||||
IotDeviceMessageService deviceMessageService,
|
||||
IotDeviceService deviceService,
|
||||
IotUdpSessionManager sessionManager) {
|
||||
this.deviceMessageService = deviceMessageService;
|
||||
this.deviceService = deviceService;
|
||||
this.sessionManager = sessionManager;
|
||||
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.serverId = protocol.getServerId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 UDP 数据包
|
||||
*
|
||||
* @param packet 数据包
|
||||
* @param socket UDP Socket
|
||||
*/
|
||||
public void handle(DatagramPacket packet, DatagramSocket socket) {
|
||||
InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port());
|
||||
Buffer data = packet.data();
|
||||
log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]",
|
||||
sessionManager.buildAddressKey(senderAddress), data.length());
|
||||
try {
|
||||
processMessage(data, senderAddress, socket);
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][处理消息失败,来源: {},错误: {}]",
|
||||
sessionManager.buildAddressKey(senderAddress), e.getMessage(), e);
|
||||
// UDP 无连接,不需要断开连接,只记录错误
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*
|
||||
* @param buffer 消息
|
||||
* @param senderAddress 发送者地址
|
||||
* @param socket UDP Socket
|
||||
*/
|
||||
private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) {
|
||||
// 1. 基础检查
|
||||
if (buffer == null || buffer.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取消息格式类型
|
||||
String codecType = getMessageCodecType(buffer);
|
||||
|
||||
// 3. 解码消息
|
||||
IotDeviceMessage message;
|
||||
try {
|
||||
message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
|
||||
if (message == null) {
|
||||
log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress));
|
||||
sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e);
|
||||
sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 根据消息类型路由处理
|
||||
try {
|
||||
if (AUTH_METHOD.equals(message.getMethod())) {
|
||||
// 认证请求
|
||||
handleAuthenticationRequest(message, codecType, senderAddress, socket);
|
||||
} else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) {
|
||||
// 设备动态注册请求
|
||||
handleRegisterRequest(message, codecType, senderAddress, socket);
|
||||
} else {
|
||||
// 业务消息
|
||||
handleBusinessRequest(message, codecType, senderAddress, socket);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]",
|
||||
sessionManager.buildAddressKey(senderAddress), message.getMethod(), e);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理认证请求
|
||||
*
|
||||
* @param message 消息信息
|
||||
* @param codecType 消息编解码类型
|
||||
* @param senderAddress 发送者地址
|
||||
* @param socket UDP Socket
|
||||
*/
|
||||
private void handleAuthenticationRequest(IotDeviceMessage message, String codecType,
|
||||
InetSocketAddress senderAddress, DatagramSocket socket) {
|
||||
String addressKey = sessionManager.buildAddressKey(senderAddress);
|
||||
try {
|
||||
// 1.1 解析认证参数
|
||||
IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
|
||||
if (authParams == null) {
|
||||
log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType);
|
||||
return;
|
||||
}
|
||||
// 1.2 执行认证
|
||||
if (!validateDeviceAuth(authParams)) {
|
||||
log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]",
|
||||
addressKey, authParams.getUsername());
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 解析设备信息
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
|
||||
if (deviceInfo == null) {
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType);
|
||||
return;
|
||||
}
|
||||
// 2.2 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
if (device == null) {
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3.1 生成 JWT Token(无状态)
|
||||
String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName());
|
||||
|
||||
// 3.2 更新设备会话信息(用于下行消息,保存 codecType)
|
||||
sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType);
|
||||
|
||||
// 3.3 发送上线消息
|
||||
sendOnlineMessage(device);
|
||||
|
||||
// 3.4 发送成功响应(包含 token)
|
||||
sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType);
|
||||
log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]",
|
||||
device.getId(), device.getDeviceName(), addressKey);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备动态注册请求(一型一密,不需要 Token)
|
||||
*
|
||||
* @param message 消息信息
|
||||
* @param codecType 消息编解码类型
|
||||
* @param senderAddress 发送者地址
|
||||
* @param socket UDP Socket
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
private void handleRegisterRequest(IotDeviceMessage message, String codecType,
|
||||
InetSocketAddress senderAddress, DatagramSocket socket) {
|
||||
String addressKey = sessionManager.buildAddressKey(senderAddress);
|
||||
try {
|
||||
// 1. 解析注册参数
|
||||
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
|
||||
if (params == null) {
|
||||
log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
if (result.isError()) {
|
||||
log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg());
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 发送成功响应(包含 deviceSecret)
|
||||
sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType);
|
||||
log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]",
|
||||
params.getDeviceName(), addressKey);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务请求
|
||||
* <p>
|
||||
* 请求参数格式:
|
||||
* - token:JWT 令牌
|
||||
* - body:实际请求内容(可以是 Map、List 或其他类型)
|
||||
*
|
||||
* @param message 消息信息
|
||||
* @param codecType 消息编解码类型
|
||||
* @param senderAddress 发送者地址
|
||||
* @param socket UDP Socket
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleBusinessRequest(IotDeviceMessage message, String codecType,
|
||||
InetSocketAddress senderAddress, DatagramSocket socket) {
|
||||
String addressKey = sessionManager.buildAddressKey(senderAddress);
|
||||
try {
|
||||
// 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]})
|
||||
String token = null;
|
||||
Object body = null;
|
||||
if (message.getParams() instanceof Map) {
|
||||
Map<String, Object> paramsMap = (Map<String, Object>) message.getParams();
|
||||
token = (String) paramsMap.get(PARAM_KEY_TOKEN);
|
||||
body = paramsMap.get(PARAM_KEY_BODY);
|
||||
}
|
||||
if (StrUtil.isBlank(token)) {
|
||||
log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType);
|
||||
return;
|
||||
}
|
||||
// 1.2 验证 token,获取设备信息
|
||||
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
|
||||
if (deviceInfo == null) {
|
||||
log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取设备详细信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
if (device == null) {
|
||||
log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]",
|
||||
addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 更新设备会话信息(保持最新,保存 codecType)
|
||||
sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType);
|
||||
|
||||
// 4. 将 body 设置为实际的 params,发送消息到消息总线
|
||||
message.setParams(body);
|
||||
deviceMessageService.sendDeviceMessage(message, device.getProductKey(),
|
||||
device.getDeviceName(), serverId);
|
||||
log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]",
|
||||
device.getId(), message.getMethod(), addressKey);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e);
|
||||
sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息编解码类型
|
||||
*
|
||||
* @param buffer 消息
|
||||
* @return 消息编解码类型
|
||||
*/
|
||||
private String getMessageCodecType(Buffer buffer) {
|
||||
// 检测消息格式类型
|
||||
return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY
|
||||
: CODEC_TYPE_JSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备上线消息
|
||||
*
|
||||
* @param device 设备信息
|
||||
*/
|
||||
private void sendOnlineMessage(IotDeviceRespDTO device) {
|
||||
try {
|
||||
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
|
||||
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), serverId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证设备认证信息
|
||||
*
|
||||
* @param authParams 认证参数
|
||||
* @return 是否认证成功
|
||||
*/
|
||||
private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) {
|
||||
try {
|
||||
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
|
||||
.setClientId(authParams.getClientId()).setUsername(authParams.getUsername())
|
||||
.setPassword(authParams.getPassword()));
|
||||
result.checkError();
|
||||
return BooleanUtil.isTrue(result.getData());
|
||||
} catch (Exception e) {
|
||||
log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送认证成功响应(包含 token)
|
||||
*
|
||||
* @param socket UDP Socket
|
||||
* @param address 目标地址
|
||||
* @param requestId 请求 ID
|
||||
* @param token JWT Token
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address,
|
||||
String requestId, String token, String codecType) {
|
||||
try {
|
||||
// 构建响应数据
|
||||
Object responseData = MapUtil.builder()
|
||||
.put("success", true)
|
||||
.put("token", token)
|
||||
.put("message", "认证成功")
|
||||
.build();
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功");
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
|
||||
// 发送响应
|
||||
socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
|
||||
if (result.failed()) {
|
||||
log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]",
|
||||
sessionManager.buildAddressKey(address), result.cause());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]",
|
||||
sessionManager.buildAddressKey(address), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册成功响应(包含 deviceSecret)
|
||||
*
|
||||
* @param socket UDP Socket
|
||||
* @param address 目标地址
|
||||
* @param requestId 请求 ID
|
||||
* @param registerResp 注册响应
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address,
|
||||
String requestId, IotDeviceRegisterRespDTO registerResp,
|
||||
String codecType) {
|
||||
try {
|
||||
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
|
||||
// 2. 发送响应
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
|
||||
socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> {
|
||||
if (result.failed()) {
|
||||
log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]",
|
||||
sessionManager.buildAddressKey(address), result.cause());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]",
|
||||
sessionManager.buildAddressKey(address), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误响应
|
||||
*
|
||||
* @param socket UDP Socket
|
||||
* @param address 目标地址
|
||||
* @param requestId 请求 ID
|
||||
* @param errorMessage 错误消息
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address,
|
||||
String requestId, String errorMessage, String codecType) {
|
||||
sendResponse(socket, address, false, errorMessage, requestId, codecType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送响应消息
|
||||
*
|
||||
* @param socket UDP Socket
|
||||
* @param address 目标地址
|
||||
* @param success 是否成功
|
||||
* @param message 消息
|
||||
* @param requestId 请求 ID
|
||||
* @param codecType 消息编解码类型
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success,
|
||||
String message, String requestId, String codecType) {
|
||||
try {
|
||||
// 构建响应数据
|
||||
Object responseData = MapUtil.builder()
|
||||
.put("success", success)
|
||||
.put("message", message)
|
||||
.build();
|
||||
int code = success ? 0 : 401;
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
|
||||
"response", responseData, code, message);
|
||||
|
||||
// 发送响应
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType);
|
||||
socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> {
|
||||
if (ar.failed()) {
|
||||
log.error("[sendResponse][发送响应失败,地址: {}]",
|
||||
sessionManager.buildAddressKey(address), ar.cause());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("[sendResponse][发送响应异常,地址: {}]",
|
||||
sessionManager.buildAddressKey(address), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析认证参数
|
||||
*
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 认证参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private IotDeviceAuthReqDTO parseAuthParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) params;
|
||||
return new IotDeviceAuthReqDTO()
|
||||
.setClientId(MapUtil.getStr(paramMap, "clientId"))
|
||||
.setUsername(MapUtil.getStr(paramMap, "username"))
|
||||
.setPassword(MapUtil.getStr(paramMap, "password"));
|
||||
}
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceAuthReqDTO) {
|
||||
return (IotDeviceAuthReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析注册参数
|
||||
*
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 注册参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "DuplicatedCode"})
|
||||
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) params;
|
||||
return new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
|
||||
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
|
||||
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
|
||||
}
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceRegisterReqDTO) {
|
||||
return (IotDeviceRegisterReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
|
||||
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 WebSocket 下游订阅者:接收下行给设备的消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class IotWebSocketDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
|
||||
|
||||
private final IotWebSocketUpstreamProtocol protocol;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotWebSocketConnectionManager connectionManager;
|
||||
|
||||
private final IotMessageBus messageBus;
|
||||
|
||||
private IotWebSocketDownstreamHandler downstreamHandler;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 初始化下游处理器
|
||||
this.downstreamHandler = new IotWebSocketDownstreamHandler(messageService, connectionManager);
|
||||
// 注册下游订阅者
|
||||
messageBus.register(this);
|
||||
log.info("[init][WebSocket 下游订阅者初始化完成,服务器 ID: {},Topic: {}]",
|
||||
protocol.getServerId(), getTopic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopic() {
|
||||
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
// 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
|
||||
return getTopic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(IotDeviceMessage message) {
|
||||
try {
|
||||
downstreamHandler.handle(message);
|
||||
} catch (Exception e) {
|
||||
log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketUpstreamHandler;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.core.http.HttpServerOptions;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 WebSocket 协议:接收设备上行消息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotWebSocketUpstreamProtocol {
|
||||
|
||||
private final IotGatewayProperties.WebSocketProperties wsProperties;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotDeviceMessageService messageService;
|
||||
|
||||
private final IotWebSocketConnectionManager connectionManager;
|
||||
|
||||
private final Vertx vertx;
|
||||
|
||||
@Getter
|
||||
private final String serverId;
|
||||
|
||||
private HttpServer httpServer;
|
||||
|
||||
public IotWebSocketUpstreamProtocol(IotGatewayProperties.WebSocketProperties wsProperties,
|
||||
IotDeviceService deviceService,
|
||||
IotDeviceMessageService messageService,
|
||||
IotWebSocketConnectionManager connectionManager,
|
||||
Vertx vertx) {
|
||||
this.wsProperties = wsProperties;
|
||||
this.deviceService = deviceService;
|
||||
this.messageService = messageService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.vertx = vertx;
|
||||
this.serverId = IotDeviceMessageUtils.generateServerId(wsProperties.getPort());
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@SuppressWarnings("deprecation")
|
||||
public void start() {
|
||||
// 1.1 创建服务器选项
|
||||
HttpServerOptions options = new HttpServerOptions()
|
||||
.setPort(wsProperties.getPort())
|
||||
.setIdleTimeout(wsProperties.getIdleTimeoutSeconds())
|
||||
.setMaxWebSocketFrameSize(wsProperties.getMaxFrameSize())
|
||||
.setMaxWebSocketMessageSize(wsProperties.getMaxMessageSize());
|
||||
// 1.2 配置 SSL(如果启用)
|
||||
if (Boolean.TRUE.equals(wsProperties.getSslEnabled())) {
|
||||
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
|
||||
.setKeyPath(wsProperties.getSslKeyPath())
|
||||
.setCertPath(wsProperties.getSslCertPath());
|
||||
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
|
||||
}
|
||||
|
||||
// 2. 创建服务器并设置 WebSocket 处理器
|
||||
httpServer = vertx.createHttpServer(options);
|
||||
httpServer.webSocketHandler(socket -> {
|
||||
// 验证路径
|
||||
if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) {
|
||||
log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]",
|
||||
socket.path(), wsProperties.getPath());
|
||||
socket.reject();
|
||||
return;
|
||||
}
|
||||
// 创建上行处理器
|
||||
IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this,
|
||||
messageService, deviceService, connectionManager);
|
||||
handler.handle(socket);
|
||||
});
|
||||
|
||||
// 3. 启动服务器
|
||||
try {
|
||||
httpServer.listen().result();
|
||||
log.info("[start][IoT 网关 WebSocket 协议启动成功,端口:{},路径:{}]", wsProperties.getPort(), wsProperties.getPath());
|
||||
} catch (Exception e) {
|
||||
log.error("[start][IoT 网关 WebSocket 协议启动失败]", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (httpServer != null) {
|
||||
try {
|
||||
httpServer.close().result();
|
||||
log.info("[stop][IoT 网关 WebSocket 协议已停止]");
|
||||
} catch (Exception e) {
|
||||
log.error("[stop][IoT 网关 WebSocket 协议停止失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager;
|
||||
|
||||
import io.vertx.core.http.ServerWebSocket;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* IoT 网关 WebSocket 连接管理器
|
||||
* <p>
|
||||
* 统一管理 WebSocket 连接的认证状态、设备会话和消息发送功能:
|
||||
* 1. 管理 WebSocket 连接的认证状态
|
||||
* 2. 管理设备会话和在线状态
|
||||
* 3. 管理消息发送到设备
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IotWebSocketConnectionManager {
|
||||
|
||||
/**
|
||||
* 连接信息映射:ServerWebSocket -> 连接信息
|
||||
*/
|
||||
private final Map<ServerWebSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 设备 ID -> ServerWebSocket 的映射
|
||||
*/
|
||||
private final Map<Long, ServerWebSocket> deviceSocketMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 注册设备连接(包含认证信息)
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
* @param deviceId 设备 ID
|
||||
* @param connectionInfo 连接信息
|
||||
*/
|
||||
public void registerConnection(ServerWebSocket socket, Long deviceId, ConnectionInfo connectionInfo) {
|
||||
// 如果设备已有其他连接,先清理旧连接
|
||||
ServerWebSocket oldSocket = deviceSocketMap.get(deviceId);
|
||||
if (oldSocket != null && oldSocket != socket) {
|
||||
log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
|
||||
deviceId, oldSocket.remoteAddress());
|
||||
oldSocket.close();
|
||||
// 清理旧连接的映射
|
||||
connectionMap.remove(oldSocket);
|
||||
}
|
||||
|
||||
// 注册新连接
|
||||
connectionMap.put(socket, connectionInfo);
|
||||
deviceSocketMap.put(deviceId, socket);
|
||||
log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
|
||||
deviceId, socket.remoteAddress(), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销设备连接
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
*/
|
||||
public void unregisterConnection(ServerWebSocket socket) {
|
||||
ConnectionInfo connectionInfo = connectionMap.remove(socket);
|
||||
if (connectionInfo == null) {
|
||||
return;
|
||||
}
|
||||
Long deviceId = connectionInfo.getDeviceId();
|
||||
deviceSocketMap.remove(deviceId);
|
||||
log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]",
|
||||
deviceId, socket.remoteAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接信息
|
||||
*/
|
||||
public ConnectionInfo getConnectionInfo(ServerWebSocket socket) {
|
||||
return connectionMap.get(socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备 ID 获取连接信息
|
||||
*/
|
||||
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
|
||||
ServerWebSocket socket = deviceSocketMap.get(deviceId);
|
||||
return socket != null ? connectionMap.get(socket) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到设备(文本消息)
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @param message JSON 消息
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendToDevice(Long deviceId, String message) {
|
||||
ServerWebSocket socket = deviceSocketMap.get(deviceId);
|
||||
if (socket == null) {
|
||||
log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.writeTextMessage(message);
|
||||
log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, message.length());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e);
|
||||
// 发送失败时清理连接
|
||||
unregisterConnection(socket);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接信息(包含认证信息)
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class ConnectionInfo {
|
||||
|
||||
/**
|
||||
* 设备 ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 产品 Key
|
||||
*/
|
||||
private String productKey;
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 客户端 ID
|
||||
*/
|
||||
private String clientId;
|
||||
/**
|
||||
* 消息编解码类型(认证后确定)
|
||||
*/
|
||||
private String codecType;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IoT 网关 WebSocket 下行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class IotWebSocketDownstreamHandler {
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotWebSocketConnectionManager connectionManager;
|
||||
|
||||
/**
|
||||
* 处理下行消息
|
||||
*/
|
||||
public void handle(IotDeviceMessage message) {
|
||||
try {
|
||||
log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
|
||||
// 1. 获取连接信息
|
||||
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
|
||||
message.getDeviceId());
|
||||
if (connectionInfo == null) {
|
||||
log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 编码消息并发送到设备
|
||||
byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType());
|
||||
String jsonMessage = StrUtil.utf8Str(bytes);
|
||||
boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage);
|
||||
if (success) {
|
||||
log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
|
||||
} else {
|
||||
log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message.getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]",
|
||||
message.getDeviceId(), message.getMethod(), message, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.http.ServerWebSocket;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* WebSocket 上行消息处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
|
||||
|
||||
/**
|
||||
* 默认消息编解码类型
|
||||
*/
|
||||
private static final String CODEC_TYPE = IotAlinkDeviceMessageCodec.TYPE;
|
||||
|
||||
private static final String AUTH_METHOD = "auth";
|
||||
|
||||
private final IotDeviceMessageService deviceMessageService;
|
||||
|
||||
private final IotDeviceService deviceService;
|
||||
|
||||
private final IotWebSocketConnectionManager connectionManager;
|
||||
|
||||
private final IotDeviceCommonApi deviceApi;
|
||||
|
||||
private final String serverId;
|
||||
|
||||
public IotWebSocketUpstreamHandler(IotWebSocketUpstreamProtocol protocol,
|
||||
IotDeviceMessageService deviceMessageService,
|
||||
IotDeviceService deviceService,
|
||||
IotWebSocketConnectionManager connectionManager) {
|
||||
this.deviceMessageService = deviceMessageService;
|
||||
this.deviceService = deviceService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
|
||||
this.serverId = protocol.getServerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(ServerWebSocket socket) {
|
||||
String clientId = IdUtil.simpleUUID();
|
||||
log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
|
||||
|
||||
// 1. 设置异常和关闭处理器
|
||||
socket.exceptionHandler(ex -> {
|
||||
log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
|
||||
cleanupConnection(socket);
|
||||
});
|
||||
socket.closeHandler(v -> {
|
||||
log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
|
||||
cleanupConnection(socket);
|
||||
});
|
||||
|
||||
// 2. 设置文本消息处理器
|
||||
socket.textMessageHandler(message -> {
|
||||
try {
|
||||
processMessage(clientId, message, socket);
|
||||
} catch (Exception e) {
|
||||
log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
|
||||
clientId, socket.remoteAddress(), e.getMessage());
|
||||
cleanupConnection(socket);
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param message 消息(JSON 字符串)
|
||||
* @param socket WebSocket 连接
|
||||
* @throws Exception 消息解码失败时抛出异常
|
||||
*/
|
||||
private void processMessage(String clientId, String message, ServerWebSocket socket) throws Exception {
|
||||
// 1.1 基础检查
|
||||
if (StrUtil.isBlank(message)) {
|
||||
return;
|
||||
}
|
||||
// 1.2 解码消息(已认证连接使用其 codecType,未认证连接使用默认 CODEC_TYPE)
|
||||
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
|
||||
String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE;
|
||||
IotDeviceMessage deviceMessage;
|
||||
try {
|
||||
deviceMessage = deviceMessageService.decodeDeviceMessage(
|
||||
StrUtil.utf8Bytes(message), codecType);
|
||||
if (deviceMessage == null) {
|
||||
throw new Exception("解码后消息为空");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new Exception("消息解码失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 2. 根据消息类型路由处理
|
||||
try {
|
||||
if (AUTH_METHOD.equals(deviceMessage.getMethod())) {
|
||||
// 认证请求
|
||||
handleAuthenticationRequest(clientId, deviceMessage, socket);
|
||||
} else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(deviceMessage.getMethod())) {
|
||||
// 设备动态注册请求
|
||||
handleRegisterRequest(clientId, deviceMessage, socket);
|
||||
} else {
|
||||
// 业务消息
|
||||
handleBusinessRequest(clientId, deviceMessage, socket);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]",
|
||||
clientId, deviceMessage.getMethod(), e);
|
||||
// 发送错误响应,避免客户端一直等待
|
||||
try {
|
||||
sendErrorResponse(socket, deviceMessage.getRequestId(), "消息处理失败");
|
||||
} catch (Exception responseEx) {
|
||||
log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理认证请求
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param message 消息信息
|
||||
* @param socket WebSocket 连接
|
||||
*/
|
||||
private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) {
|
||||
try {
|
||||
// 1.1 解析认证参数
|
||||
IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams());
|
||||
if (authParams == null) {
|
||||
log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId);
|
||||
sendErrorResponse(socket, message.getRequestId(), "认证参数不完整");
|
||||
return;
|
||||
}
|
||||
// 1.2 执行认证
|
||||
if (!validateDeviceAuth(authParams)) {
|
||||
log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]",
|
||||
clientId, authParams.getUsername());
|
||||
sendErrorResponse(socket, message.getRequestId(), "认证失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 解析设备信息
|
||||
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
|
||||
if (deviceInfo == null) {
|
||||
sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败");
|
||||
return;
|
||||
}
|
||||
// 2.2 获取设备信息
|
||||
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
|
||||
deviceInfo.getDeviceName());
|
||||
if (device == null) {
|
||||
sendErrorResponse(socket, message.getRequestId(), "设备不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3.1 注册连接
|
||||
registerConnection(socket, device, clientId);
|
||||
// 3.2 发送上线消息
|
||||
sendOnlineMessage(device);
|
||||
// 3.3 发送成功响应
|
||||
sendSuccessResponse(socket, message.getRequestId(), "认证成功");
|
||||
log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]",
|
||||
device.getId(), device.getDeviceName());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e);
|
||||
sendErrorResponse(socket, message.getRequestId(), "认证处理异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备动态注册请求(一型一密,不需要认证)
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param message 消息信息
|
||||
* @param socket WebSocket 连接
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
|
||||
*/
|
||||
private void handleRegisterRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) {
|
||||
try {
|
||||
// 1. 解析注册参数
|
||||
IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
|
||||
if (params == null
|
||||
|| StrUtil.hasEmpty(params.getProductKey(), params.getDeviceName(), params.getProductSecret())) {
|
||||
log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
|
||||
sendErrorResponse(socket, message.getRequestId(), "注册参数不完整");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用动态注册
|
||||
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
|
||||
if (result.isError()) {
|
||||
log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
|
||||
sendErrorResponse(socket, message.getRequestId(), result.getMsg());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 发送成功响应(包含 deviceSecret)
|
||||
sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData());
|
||||
log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]",
|
||||
clientId, params.getDeviceName());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
|
||||
sendErrorResponse(socket, message.getRequestId(), "注册处理异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务请求
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param message 消息信息
|
||||
* @param socket WebSocket 连接
|
||||
*/
|
||||
private void handleBusinessRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) {
|
||||
try {
|
||||
// 1. 获取认证信息并处理业务消息
|
||||
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
|
||||
if (connectionInfo == null) {
|
||||
log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息,客户端 ID: {}]", clientId);
|
||||
sendErrorResponse(socket, message.getRequestId(), "连接未认证");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 发送消息到消息总线
|
||||
deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), serverId);
|
||||
log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}",
|
||||
clientId, message.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册连接信息
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
* @param device 设备
|
||||
* @param clientId 客户端 ID
|
||||
*/
|
||||
private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device, String clientId) {
|
||||
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo()
|
||||
.setDeviceId(device.getId())
|
||||
.setProductKey(device.getProductKey())
|
||||
.setDeviceName(device.getDeviceName())
|
||||
.setClientId(clientId)
|
||||
.setCodecType(CODEC_TYPE);
|
||||
// 注册连接
|
||||
connectionManager.registerConnection(socket, device.getId(), connectionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送设备上线消息
|
||||
*
|
||||
* @param device 设备信息
|
||||
*/
|
||||
private void sendOnlineMessage(IotDeviceRespDTO device) {
|
||||
try {
|
||||
IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
|
||||
deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(),
|
||||
device.getDeviceName(), serverId);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
*/
|
||||
private void cleanupConnection(ServerWebSocket socket) {
|
||||
try {
|
||||
// 1. 发送离线消息(如果已认证)
|
||||
IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket);
|
||||
if (connectionInfo != null) {
|
||||
IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
|
||||
deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
|
||||
connectionInfo.getDeviceName(), serverId);
|
||||
}
|
||||
|
||||
// 2. 注销连接
|
||||
connectionManager.unregisterConnection(socket);
|
||||
} catch (Exception e) {
|
||||
log.error("[cleanupConnection][清理连接失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送响应消息
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
* @param success 是否成功
|
||||
* @param message 消息
|
||||
* @param requestId 请求 ID
|
||||
*/
|
||||
private void sendResponse(ServerWebSocket socket, boolean success, String message, String requestId) {
|
||||
try {
|
||||
Object responseData = MapUtil.builder()
|
||||
.put("success", success)
|
||||
.put("message", message)
|
||||
.build();
|
||||
|
||||
int code = success ? 0 : 401;
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message);
|
||||
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE);
|
||||
socket.writeTextMessage(StrUtil.utf8Str(encodedData));
|
||||
} catch (Exception e) {
|
||||
log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证设备认证信息
|
||||
*
|
||||
* @param authParams 认证参数
|
||||
* @return 是否认证成功
|
||||
*/
|
||||
private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) {
|
||||
try {
|
||||
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
|
||||
.setClientId(authParams.getClientId()).setUsername(authParams.getUsername())
|
||||
.setPassword(authParams.getPassword()));
|
||||
result.checkError();
|
||||
return BooleanUtil.isTrue(result.getData());
|
||||
} catch (Exception e) {
|
||||
log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误响应
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
* @param requestId 请求 ID
|
||||
* @param errorMessage 错误消息
|
||||
*/
|
||||
private void sendErrorResponse(ServerWebSocket socket, String requestId, String errorMessage) {
|
||||
sendResponse(socket, false, errorMessage, requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送成功响应
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
* @param requestId 请求 ID
|
||||
* @param message 消息
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private void sendSuccessResponse(ServerWebSocket socket, String requestId, String message) {
|
||||
sendResponse(socket, true, message, requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析认证参数
|
||||
*
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 认证参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "DuplicatedCode"})
|
||||
private IotDeviceAuthReqDTO parseAuthParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) params;
|
||||
return new IotDeviceAuthReqDTO()
|
||||
.setClientId(MapUtil.getStr(paramMap, "clientId"))
|
||||
.setUsername(MapUtil.getStr(paramMap, "username"))
|
||||
.setPassword(MapUtil.getStr(paramMap, "password"));
|
||||
}
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceAuthReqDTO) {
|
||||
return (IotDeviceAuthReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseAuthParams][解析认证参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析注册参数
|
||||
*
|
||||
* @param params 参数对象(通常为 Map 类型)
|
||||
* @return 注册参数 DTO,解析失败时返回 null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 参数默认为 Map 类型,直接转换
|
||||
if (params instanceof Map) {
|
||||
Map<String, Object> paramMap = (Map<String, Object>) params;
|
||||
return new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(MapUtil.getStr(paramMap, "productKey"))
|
||||
.setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
|
||||
.setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
|
||||
}
|
||||
// 如果已经是目标类型,直接返回
|
||||
if (params instanceof IotDeviceRegisterReqDTO) {
|
||||
return (IotDeviceRegisterReqDTO) params;
|
||||
}
|
||||
|
||||
// 其他情况尝试 JSON 转换
|
||||
return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
|
||||
} catch (Exception e) {
|
||||
log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册成功响应(包含 deviceSecret)
|
||||
*
|
||||
* @param socket WebSocket 连接
|
||||
* @param requestId 请求 ID
|
||||
* @param registerResp 注册响应
|
||||
*/
|
||||
private void sendRegisterSuccessResponse(ServerWebSocket socket, String requestId,
|
||||
IotDeviceRegisterRespDTO registerResp) {
|
||||
try {
|
||||
// 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
|
||||
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
|
||||
// 2. 发送响应
|
||||
byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE);
|
||||
socket.writeTextMessage(StrUtil.utf8Str(encodedData));
|
||||
} catch (Exception e) {
|
||||
log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.service.auth;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
|
||||
/**
|
||||
* IoT 设备 Token Service 接口
|
||||
@@ -24,7 +24,7 @@ public interface IotDeviceTokenService {
|
||||
* @param token 设备 Token
|
||||
* @return 设备信息
|
||||
*/
|
||||
IotDeviceAuthUtils.DeviceInfo verifyToken(String token);
|
||||
IotDeviceIdentity verifyToken(String token);
|
||||
|
||||
/**
|
||||
* 解析用户名
|
||||
@@ -32,6 +32,6 @@ public interface IotDeviceTokenService {
|
||||
* @param username 用户名
|
||||
* @return 设备信息
|
||||
*/
|
||||
IotDeviceAuthUtils.DeviceInfo parseUsername(String username);
|
||||
IotDeviceIdentity parseUsername(String username);
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -48,7 +49,7 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) {
|
||||
public IotDeviceIdentity verifyToken(String token) {
|
||||
Assert.notBlank(token, "token 不能为空");
|
||||
// 校验 JWT Token
|
||||
boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes());
|
||||
@@ -68,11 +69,11 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
|
||||
String deviceName = payload.getStr("deviceName");
|
||||
Assert.notBlank(productKey, "productKey 不能为空");
|
||||
Assert.notBlank(deviceName, "deviceName 不能为空");
|
||||
return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName);
|
||||
return new IotDeviceIdentity(productKey, deviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) {
|
||||
public IotDeviceIdentity parseUsername(String username) {
|
||||
return IotDeviceAuthUtils.parseUsername(username);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
|
||||
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -18,6 +22,8 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||
|
||||
/**
|
||||
@@ -54,6 +60,16 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
|
||||
return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { });
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO) {
|
||||
return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { });
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
|
||||
return doPost("/register-sub", reqDTO, new ParameterizedTypeReference<>() { });
|
||||
}
|
||||
|
||||
private <T, R> CommonResult<R> doPost(String url, T body,
|
||||
ParameterizedTypeReference<CommonResult<R>> responseType) {
|
||||
try {
|
||||
|
||||
@@ -96,6 +96,16 @@ yudao:
|
||||
ssl-cert-path: "classpath:certs/client.jks"
|
||||
ssl-key-path: "classpath:certs/client.jks"
|
||||
# ====================================
|
||||
# 针对引入的 UDP 组件的配置
|
||||
# ====================================
|
||||
udp:
|
||||
enabled: false # 是否启用 UDP
|
||||
port: 8093 # UDP 服务端口
|
||||
receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB)
|
||||
send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB)
|
||||
session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒)
|
||||
session-clean-interval-ms: 30000 # 会话清理间隔(毫秒,默认 30 秒)
|
||||
# ====================================
|
||||
# 针对引入的 MQTT 组件的配置
|
||||
# ====================================
|
||||
mqtt:
|
||||
@@ -105,18 +115,25 @@ yudao:
|
||||
connect-timeout-seconds: 60
|
||||
ssl-enabled: false
|
||||
# ====================================
|
||||
# 针对引入的 MQTT WebSocket 组件的配置
|
||||
# 针对引入的 CoAP 组件的配置
|
||||
# ====================================
|
||||
mqtt-ws:
|
||||
enabled: false # 是否启用 MQTT WebSocket
|
||||
port: 8083 # WebSocket 服务端口
|
||||
path: /mqtt # WebSocket 路径
|
||||
max-message-size: 8192 # 最大消息大小(字节)
|
||||
max-frame-size: 65536 # 最大帧大小(字节)
|
||||
connect-timeout-seconds: 60 # 连接超时时间(秒)
|
||||
keep-alive-timeout-seconds: 300 # 保持连接超时时间(秒)
|
||||
coap:
|
||||
enabled: false # 是否启用 CoAP 协议
|
||||
port: 5683 # CoAP 服务端口(默认 5683)
|
||||
max-message-size: 1024 # 最大消息大小(字节)
|
||||
ack-timeout: 2000 # ACK 超时时间(毫秒)
|
||||
max-retransmit: 4 # 最大重传次数
|
||||
# ====================================
|
||||
# 针对引入的 WebSocket 组件的配置
|
||||
# ====================================
|
||||
websocket:
|
||||
enabled: false # 是否启用 WebSocket 协议
|
||||
port: 8094 # WebSocket 服务端口(默认 8094)
|
||||
path: /ws # WebSocket 路径(默认 /ws)
|
||||
max-message-size: 65536 # 最大消息大小(字节,默认 64KB)
|
||||
max-frame-size: 65536 # 最大帧大小(字节,默认 64KB)
|
||||
idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60)
|
||||
ssl-enabled: false # 是否启用 SSL(wss://)
|
||||
sub-protocol: mqtt # WebSocket 子协议
|
||||
|
||||
--- #################### 日志相关配置 ####################
|
||||
|
||||
@@ -137,6 +154,8 @@ logging:
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
|
||||
cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG
|
||||
# 根日志级别
|
||||
root: INFO
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
import org.eclipse.californium.core.coap.MediaTypeRegistry;
|
||||
import org.eclipse.californium.core.coap.Option;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.config.CoapConfig;
|
||||
import org.eclipse.californium.elements.config.Configuration;
|
||||
import org.eclipse.californium.elements.config.UdpConfig;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* IoT 直连设备 CoAP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 CoAP 协议直接连接平台
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(CoAP 端口 5683)</li>
|
||||
* <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li>
|
||||
* <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
|
||||
* <li>{@link #testEventPost()} - 设备事件上报</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotDirectDeviceCoapProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 5683;
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
|
||||
private static final String DEVICE_NAME = "small";
|
||||
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
|
||||
|
||||
/**
|
||||
* 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
|
||||
*/
|
||||
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk5MjgxOSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.UHLCXsoGNsKbtJcbTV3n1psp03G75hVcVpV4wwd39r4";
|
||||
|
||||
@BeforeAll
|
||||
public static void initCaliforniumConfig() {
|
||||
// 注册 Californium 配置定义
|
||||
CoapConfig.register();
|
||||
UdpConfig.register();
|
||||
// 创建默认配置
|
||||
Configuration.setStandard(Configuration.createStandardWithoutFile());
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 认证测试:获取设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
String payload = JsonUtils.toJsonString(authReqDTO);
|
||||
// 1.2 输出请求
|
||||
log.info("[testAuth][请求 URI: {}]", uri);
|
||||
log.info("[testAuth][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
|
||||
// 2.2 输出结果
|
||||
log.info("[testAuth][响应码: {}]", response.getCode());
|
||||
log.info("[testAuth][响应体: {}]", response.getResponseText());
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 属性上报测试
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testPropertyPost() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("width", 1)
|
||||
.put("height", "2")
|
||||
.build())
|
||||
)
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testPropertyPost][请求 URI: {}]", uri);
|
||||
log.info("[testPropertyPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testPropertyPost][响应码: {}]", response.getCode());
|
||||
log.info("[testPropertyPost][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 事件上报测试
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testEventPost() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDeviceEventPostReqDTO.of(
|
||||
"eat",
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis())
|
||||
)
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testEventPost][请求 URI: {}]", uri);
|
||||
log.info("[testEventPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testEventPost][响应码: {}]", response.getCode());
|
||||
log.info("[testEventPost][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 动态注册测试 =====================
|
||||
|
||||
/**
|
||||
* 直连设备动态注册测试(一型一密)
|
||||
* <p>
|
||||
* 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
|
||||
* <p>
|
||||
* 注意:此接口不需要 Token 认证
|
||||
*/
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
|
||||
// 1.2 构建请求参数
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
|
||||
reqDTO.setProductKey(PRODUCT_KEY);
|
||||
reqDTO.setDeviceName("test-" + System.currentTimeMillis());
|
||||
reqDTO.setProductSecret("test-product-secret");
|
||||
String payload = JsonUtils.toJsonString(reqDTO);
|
||||
// 1.3 输出请求
|
||||
log.info("[testDeviceRegister][请求 URI: {}]", uri);
|
||||
log.info("[testDeviceRegister][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
|
||||
// 2.2 输出结果
|
||||
log.info("[testDeviceRegister][响应码: {}]", response.getCode());
|
||||
log.info("[testDeviceRegister][响应体: {}]", response.getResponseText());
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
import org.eclipse.californium.core.coap.MediaTypeRegistry;
|
||||
import org.eclipse.californium.core.coap.Option;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.config.CoapConfig;
|
||||
import org.eclipse.californium.elements.config.Configuration;
|
||||
import org.eclipse.californium.elements.config.UdpConfig;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 网关设备 CoAP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 CoAP 协议管理子设备拓扑关系
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(CoAP 端口 5683)</li>
|
||||
* <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
|
||||
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
|
||||
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotGatewayDeviceCoapProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 5683;
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
|
||||
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
|
||||
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
|
||||
|
||||
/**
|
||||
* 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
|
||||
*/
|
||||
private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
|
||||
|
||||
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String SUB_DEVICE_NAME = "chazuo-it";
|
||||
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
@BeforeAll
|
||||
public static void initCaliforniumConfig() {
|
||||
// 注册 Californium 配置定义
|
||||
CoapConfig.register();
|
||||
UdpConfig.register();
|
||||
// 创建默认配置
|
||||
Configuration.setStandard(Configuration.createStandardWithoutFile());
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 网关设备认证测试:获取网关设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
String payload = JsonUtils.toJsonString(authReqDTO);
|
||||
// 1.2 输出请求
|
||||
log.info("[testAuth][请求 URI: {}]", uri);
|
||||
log.info("[testAuth][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
|
||||
// 2.2 输出结果
|
||||
log.info("[testAuth][响应码: {}]", response.getCode());
|
||||
log.info("[testAuth][响应体: {}]", response.getResponseText());
|
||||
log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 拓扑管理测试 =====================
|
||||
|
||||
/**
|
||||
* 添加子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要绑定的子设备信息
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testTopoAdd() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/add",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建子设备认证信息
|
||||
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
|
||||
.setClientId(subAuthInfo.getClientId())
|
||||
.setUsername(subAuthInfo.getUsername())
|
||||
.setPassword(subAuthInfo.getPassword());
|
||||
// 1.3 构建请求参数
|
||||
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.4 输出请求
|
||||
log.info("[testTopoAdd][请求 URI: {}]", uri);
|
||||
log.info("[testTopoAdd][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testTopoAdd][响应码: {}]", response.getCode());
|
||||
log.info("[testTopoAdd][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要解绑的子设备信息
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testTopoDelete() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/delete",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建请求参数
|
||||
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(
|
||||
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.3 输出请求
|
||||
log.info("[testTopoDelete][请求 URI: {}]", uri);
|
||||
log.info("[testTopoDelete][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testTopoDelete][响应码: {}]", response.getCode());
|
||||
log.info("[testTopoDelete][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台查询已绑定的子设备列表
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testTopoGet() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/topo/get",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建请求参数(目前为空,预留扩展)
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.3 输出请求
|
||||
log.info("[testTopoGet][请求 URI: {}]", uri);
|
||||
log.info("[testTopoGet][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testTopoGet][响应码: {}]", response.getCode());
|
||||
log.info("[testTopoGet][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备动态注册测试
|
||||
* <p>
|
||||
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
|
||||
* <p>
|
||||
* 注意:此接口需要网关 Token 认证
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testSubDeviceRegister() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/auth/register/sub-device/%s/%s",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建请求参数
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
|
||||
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
|
||||
subDevice.setDeviceName("mougezishebei");
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", Collections.singletonList(subDevice))
|
||||
.build());
|
||||
// 1.3 输出请求
|
||||
log.info("[testSubDeviceRegister][请求 URI: {}]", uri);
|
||||
log.info("[testSubDeviceRegister][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testSubDeviceRegister][响应码: {}]", response.getCode());
|
||||
log.info("[testSubDeviceRegister][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 批量上报测试 =====================
|
||||
|
||||
/**
|
||||
* 批量上报属性测试(网关 + 子设备)
|
||||
* <p>
|
||||
* 网关设备批量上报自身属性、事件,以及子设备的属性、事件
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testPropertyPackPost() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
// 1.3 构建【网关设备】自身事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
|
||||
gatewayEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.build();
|
||||
// 1.4 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
// 1.5 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
|
||||
subDeviceEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("healthCheck", subDeviceEvent)
|
||||
.build();
|
||||
// 1.6 构建子设备数据
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
|
||||
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
|
||||
subDeviceData.setProperties(subDeviceProperties);
|
||||
subDeviceData.setEvents(subDeviceEvents);
|
||||
// 1.7 构建请求参数
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(List.of(subDeviceData));
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.8 输出请求
|
||||
log.info("[testPropertyPackPost][请求 URI: {}]", uri);
|
||||
log.info("[testPropertyPackPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testPropertyPackPost][响应码: {}]", response.getCode());
|
||||
log.info("[testPropertyPackPost][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
import org.eclipse.californium.core.coap.MediaTypeRegistry;
|
||||
import org.eclipse.californium.core.coap.Option;
|
||||
import org.eclipse.californium.core.coap.Request;
|
||||
import org.eclipse.californium.core.config.CoapConfig;
|
||||
import org.eclipse.californium.elements.config.Configuration;
|
||||
import org.eclipse.californium.elements.config.UdpConfig;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* IoT 网关子设备 CoAP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
|
||||
*
|
||||
* <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
|
||||
* <p>网关设备转发子设备请求时,Token 使用子设备自己的信息。
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(CoAP 端口 5683)</li>
|
||||
* <li>确保子设备已通过 {@link IotGatewayDeviceCoapProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
|
||||
* <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
|
||||
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotGatewaySubDeviceCoapProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 5683;
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String DEVICE_NAME = "chazuo-it";
|
||||
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
/**
|
||||
* 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
|
||||
*/
|
||||
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTk1NDY3OSwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.jfbUAoU0xkJl4UvO-NUvcJ6yITPRgUjQ4MKATPuwneg";
|
||||
|
||||
@BeforeAll
|
||||
public static void initCaliforniumConfig() {
|
||||
// 注册 Californium 配置定义
|
||||
CoapConfig.register();
|
||||
UdpConfig.register();
|
||||
// 创建默认配置
|
||||
Configuration.setStandard(Configuration.createStandardWithoutFile());
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备认证测试:获取子设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
String payload = JsonUtils.toJsonString(authReqDTO);
|
||||
// 1.2 输出请求
|
||||
log.info("[testAuth][请求 URI: {}]", uri);
|
||||
log.info("[testAuth][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
CoapResponse response = client.post(payload, MediaTypeRegistry.APPLICATION_JSON);
|
||||
// 2.2 输出结果
|
||||
log.info("[testAuth][响应码: {}]", response.getCode());
|
||||
log.info("[testAuth][响应体: {}]", response.getResponseText());
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备属性上报测试
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testPropertyPost() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.put("status", "online")
|
||||
.put("temperature", 36.5)
|
||||
.build()))
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testPropertyPost][请求 URI: {}]", uri);
|
||||
log.info("[testPropertyPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testPropertyPost][响应码: {}]", response.getCode());
|
||||
log.info("[testPropertyPost][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备事件上报测试
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void testEventPost() throws Exception {
|
||||
// 1.1 构建请求
|
||||
String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDeviceEventPostReqDTO.of(
|
||||
"alarm",
|
||||
MapUtil.<String, Object>builder()
|
||||
.put("level", "warning")
|
||||
.put("message", "temperature too high")
|
||||
.put("threshold", 40)
|
||||
.put("current", 42)
|
||||
.build(),
|
||||
System.currentTimeMillis()))
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testEventPost][请求 URI: {}]", uri);
|
||||
log.info("[testEventPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
CoapClient client = new CoapClient(uri);
|
||||
try {
|
||||
Request request = Request.newPost();
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
log.info("[testEventPost][响应码: {}]", response.getCode());
|
||||
log.info("[testEventPost][响应体: {}]", response.getResponseText());
|
||||
} finally {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
/**
|
||||
* IoT 直连设备 HTTP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 HTTP 协议直接连接平台
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
|
||||
* <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li>
|
||||
* <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
|
||||
* <li>{@link #testEventPost()} - 设备事件上报</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
@SuppressWarnings("HttpUrlsUsage")
|
||||
public class IotDirectDeviceHttpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8092;
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
|
||||
private static final String DEVICE_NAME = "small";
|
||||
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
|
||||
|
||||
/**
|
||||
* 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
|
||||
*/
|
||||
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k";
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 认证测试:获取设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
String payload = JsonUtils.toJsonString(authReqDTO);
|
||||
// 1.2 输出请求
|
||||
log.info("[testAuth][请求 URL: {}]", url);
|
||||
log.info("[testAuth][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
String response = HttpUtil.post(url, payload);
|
||||
// 2.2 输出结果
|
||||
log.info("[testAuth][响应体: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
}
|
||||
|
||||
// ===================== 直连设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 属性上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("width", 1)
|
||||
.put("height", "2")
|
||||
.build())
|
||||
)
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testPropertyPost][请求 URL: {}]", url);
|
||||
log.info("[testPropertyPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testPropertyPost][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 事件上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDeviceEventPostReqDTO.of(
|
||||
"eat",
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis())
|
||||
)
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testEventPost][请求 URL: {}]", url);
|
||||
log.info("[testEventPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testEventPost][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 动态注册测试 =====================
|
||||
|
||||
/**
|
||||
* 直连设备动态注册测试(一型一密)
|
||||
* <p>
|
||||
* 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
|
||||
* <p>
|
||||
* 注意:此接口不需要 Token 认证
|
||||
*/
|
||||
@Test
|
||||
public void testDeviceRegister() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
|
||||
// 1.2 构建请求参数
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
|
||||
reqDTO.setProductKey(PRODUCT_KEY);
|
||||
reqDTO.setDeviceName("test-" + System.currentTimeMillis());
|
||||
reqDTO.setProductSecret("test-product-secret");
|
||||
String payload = JsonUtils.toJsonString(reqDTO);
|
||||
// 1.3 输出请求
|
||||
log.info("[testDeviceRegister][请求 URL: {}]", url);
|
||||
log.info("[testDeviceRegister][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
String response = HttpUtil.post(url, payload);
|
||||
// 2.2 输出结果
|
||||
log.info("[testDeviceRegister][响应体: {}]", response);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* IoT 网关设备 HTTP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 HTTP 协议管理子设备拓扑关系
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
|
||||
* <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
|
||||
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
|
||||
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
@SuppressWarnings("HttpUrlsUsage")
|
||||
public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8092;
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
|
||||
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
|
||||
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
|
||||
|
||||
/**
|
||||
* 网关设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
|
||||
*/
|
||||
private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
|
||||
|
||||
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String SUB_DEVICE_NAME = "chazuo-it";
|
||||
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 网关设备认证测试:获取网关设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
String payload = JsonUtils.toJsonString(authReqDTO);
|
||||
// 1.2 输出请求
|
||||
log.info("[testAuth][请求 URL: {}]", url);
|
||||
log.info("[testAuth][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
String response = HttpUtil.post(url, payload);
|
||||
// 2.2 输出结果
|
||||
log.info("[testAuth][响应体: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
|
||||
}
|
||||
|
||||
// ===================== 拓扑管理测试 =====================
|
||||
|
||||
/**
|
||||
* 添加子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要绑定的子设备信息
|
||||
*/
|
||||
@Test
|
||||
public void testTopoAdd() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/add",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建子设备认证信息
|
||||
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
|
||||
.setClientId(subAuthInfo.getClientId())
|
||||
.setUsername(subAuthInfo.getUsername())
|
||||
.setPassword(subAuthInfo.getPassword());
|
||||
// 1.3 构建请求参数
|
||||
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.4 输出请求
|
||||
log.info("[testTopoAdd][请求 URL: {}]", url);
|
||||
log.info("[testTopoAdd][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", GATEWAY_TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testTopoAdd][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要解绑的子设备信息
|
||||
*/
|
||||
@Test
|
||||
public void testTopoDelete() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/delete",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建请求参数
|
||||
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(
|
||||
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.3 输出请求
|
||||
log.info("[testTopoDelete][请求 URL: {}]", url);
|
||||
log.info("[testTopoDelete][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", GATEWAY_TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testTopoDelete][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台查询已绑定的子设备列表
|
||||
*/
|
||||
@Test
|
||||
public void testTopoGet() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/get",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建请求参数(目前为空,预留扩展)
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.3 输出请求
|
||||
log.info("[testTopoGet][请求 URL: {}]", url);
|
||||
log.info("[testTopoGet][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", GATEWAY_TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testTopoGet][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
|
||||
// TODO @芋艿:待测试
|
||||
|
||||
/**
|
||||
* 子设备动态注册测试
|
||||
* <p>
|
||||
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
|
||||
* <p>
|
||||
* 注意:此接口需要网关 Token 认证
|
||||
*/
|
||||
@Test
|
||||
public void testSubDeviceRegister() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建请求参数
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
|
||||
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
|
||||
subDevice.setDeviceName("mougezishebei");
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", Collections.singletonList(subDevice))
|
||||
.build());
|
||||
// 1.3 输出请求
|
||||
log.info("[testSubDeviceRegister][请求 URL: {}]", url);
|
||||
log.info("[testSubDeviceRegister][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", GATEWAY_TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 批量上报测试 =====================
|
||||
|
||||
/**
|
||||
* 批量上报属性测试(网关 + 子设备)
|
||||
* <p>
|
||||
* 网关设备批量上报自身属性、事件,以及子设备的属性、事件
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPackPost() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/property/pack/post",
|
||||
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
// 1.2 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
// 1.3 构建【网关设备】自身事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
|
||||
gatewayEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.build();
|
||||
// 1.4 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
// 1.5 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
|
||||
subDeviceEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("healthCheck", subDeviceEvent)
|
||||
.build();
|
||||
// 1.6 构建子设备数据
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
|
||||
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
|
||||
subDeviceData.setProperties(subDeviceProperties);
|
||||
subDeviceData.setEvents(subDeviceEvents);
|
||||
// 1.7 构建请求参数
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(List.of(subDeviceData));
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", params)
|
||||
.build());
|
||||
// 1.8 输出请求
|
||||
log.info("[testPropertyPackPost][请求 URL: {}]", url);
|
||||
log.info("[testPropertyPackPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", GATEWAY_TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testPropertyPackPost][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
/**
|
||||
* IoT 网关子设备 HTTP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
|
||||
*
|
||||
* <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
|
||||
* <p>网关设备转发子设备请求时,URL 和 Token 都使用子设备自己的信息。
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
|
||||
* <li>确保子设备已通过 {@link IotGatewayDeviceHttpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
|
||||
* <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
|
||||
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
@SuppressWarnings("HttpUrlsUsage")
|
||||
public class IotGatewaySubDeviceHttpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8092;
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String DEVICE_NAME = "chazuo-it";
|
||||
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
/**
|
||||
* 网关子设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
|
||||
*/
|
||||
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiakF1ZkVNVEYxVzZ3blBobiIsImV4cCI6MTc2OTg3MTI3NCwiZGV2aWNlTmFtZSI6ImNoYXp1by1pdCJ9.99sAlRalzMU3CqRlGStDzCwWSBJq6u3PJw48JQ3NpzQ";
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备认证测试:获取子设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
String payload = JsonUtils.toJsonString(authReqDTO);
|
||||
// 1.2 输出请求
|
||||
log.info("[testAuth][请求 URL: {}]", url);
|
||||
log.info("[testAuth][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
String response = HttpUtil.post(url, payload);
|
||||
// 2.2 输出结果
|
||||
log.info("[testAuth][响应体: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
}
|
||||
|
||||
// ===================== 子设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备属性上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.put("status", "online")
|
||||
.put("temperature", 36.5)
|
||||
.build())
|
||||
)
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testPropertyPost][请求 URL: {}]", url);
|
||||
log.info("[testPropertyPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testPropertyPost][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备事件上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() {
|
||||
// 1.1 构建请求
|
||||
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post",
|
||||
SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
|
||||
String payload = JsonUtils.toJsonString(MapUtil.builder()
|
||||
.put("id", IdUtil.fastSimpleUUID())
|
||||
.put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
|
||||
.put("version", "1.0")
|
||||
.put("params", IotDeviceEventPostReqDTO.of(
|
||||
"alarm",
|
||||
MapUtil.<String, Object>builder()
|
||||
.put("level", "warning")
|
||||
.put("message", "temperature too high")
|
||||
.put("threshold", 40)
|
||||
.put("current", 42)
|
||||
.build(),
|
||||
System.currentTimeMillis())
|
||||
)
|
||||
.build());
|
||||
// 1.2 输出请求
|
||||
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testEventPost][请求 URL: {}]", url);
|
||||
log.info("[testEventPost][请求体: {}]", payload);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (HttpResponse httpResponse = HttpUtil.createPost(url)
|
||||
.header("Authorization", TOKEN)
|
||||
.body(payload)
|
||||
.execute()) {
|
||||
// 2.2 输出结果
|
||||
log.info("[testEventPost][响应体: {}]", httpResponse.body());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.mqtt.MqttClient;
|
||||
import io.vertx.mqtt.MqttClientOptions;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IoT 直连设备 MQTT 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 MQTT 协议直接连接平台
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 设备连接认证</li>
|
||||
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
|
||||
* <li>{@link #testEventPost()} - 设备事件上报</li>
|
||||
* <li>{@link #testSubscribe()} - 订阅下行消息</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
|
||||
* 认证成功后同一连接上的后续请求无需再携带认证信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 1883;
|
||||
private static final int TIMEOUT_SECONDS = 10;
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
|
||||
private static final String DEVICE_NAME = "small";
|
||||
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 连接认证测试 =====================
|
||||
|
||||
/**
|
||||
* 认证测试:获取设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 构建认证信息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
|
||||
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
|
||||
// 2. 创建客户端并连接
|
||||
MqttClient client = connect(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
// 断开连接
|
||||
client.disconnect()
|
||||
.onComplete(disconnectAr -> {
|
||||
if (disconnectAr.succeeded()) {
|
||||
log.info("[testAuth][断开连接成功]");
|
||||
} else {
|
||||
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
} else {
|
||||
log.error("[testAuth][连接失败]", ar.cause());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待测试完成
|
||||
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testAuth][测试超时]");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 属性上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testPropertyPost][连接认证成功]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 3. 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("width", 1)
|
||||
.put("height", "2")
|
||||
.build()),
|
||||
null, null, null);
|
||||
|
||||
// 4. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
|
||||
// 5. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 直连设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 事件上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testEventPost][连接认证成功]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 3. 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
IotDeviceEventPostReqDTO.of(
|
||||
"eat",
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis()),
|
||||
null, null, null);
|
||||
|
||||
// 4. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
|
||||
// 5. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 设备动态注册测试(一型一密) =====================
|
||||
|
||||
/**
|
||||
* 直连设备动态注册测试(一型一密)
|
||||
* <p>
|
||||
* 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
|
||||
* <p>
|
||||
* 注意:此接口不需要认证
|
||||
*/
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1. 连接并认证(使用已有设备连接)
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testDeviceRegister][连接认证成功]");
|
||||
|
||||
// 2.1 构建注册消息
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
|
||||
registerReqDTO.setProductKey(PRODUCT_KEY);
|
||||
registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis());
|
||||
registerReqDTO.setProductSecret("test-product-secret");
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply",
|
||||
registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/auth/register",
|
||||
registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testDeviceRegister][响应消息: {}]", response);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 订阅下行消息测试 =====================
|
||||
|
||||
/**
|
||||
* 订阅下行消息测试:订阅服务端下发的消息
|
||||
*/
|
||||
@Test
|
||||
public void testSubscribe() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testSubscribe][连接认证成功]");
|
||||
|
||||
// 2. 设置消息处理器
|
||||
client.publishHandler(message -> {
|
||||
log.info("[testSubscribe][收到消息: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
});
|
||||
|
||||
// 3. 订阅下行主题
|
||||
String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
|
||||
log.info("[testSubscribe][订阅主题: {}]", topic);
|
||||
|
||||
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.onComplete(subscribeAr -> {
|
||||
if (subscribeAr.succeeded()) {
|
||||
log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
|
||||
// 保持连接 30 秒等待消息
|
||||
vertx.setTimer(30000, id -> {
|
||||
client.disconnect()
|
||||
.onComplete(disconnectAr -> {
|
||||
log.info("[testSubscribe][断开连接]");
|
||||
latch.countDown();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
log.error("[testSubscribe][订阅失败]", subscribeAr.cause());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 等待测试完成
|
||||
boolean completed = latch.await(60, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testSubscribe][测试超时]");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 创建 MQTT 客户端
|
||||
*
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword())
|
||||
.setCleanSession(true)
|
||||
.setKeepAliveInterval(60);
|
||||
return MqttClient.create(vertx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接并认证设备
|
||||
*
|
||||
* @return 已认证的 MQTT 客户端
|
||||
*/
|
||||
private MqttClient connectAndAuth() throws Exception {
|
||||
// 1. 创建客户端并连接
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
MqttClient client = connect(authInfo);
|
||||
|
||||
// 2.1 连接
|
||||
CompletableFuture<MqttClient> future = new CompletableFuture<>();
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
future.complete(client);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2.2 等待连接结果
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
*/
|
||||
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
|
||||
// 1. 订阅响应主题
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
|
||||
future.complete(null);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2. 等待订阅结果
|
||||
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息并等待响应
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 发布主题
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
|
||||
future.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
|
||||
} else {
|
||||
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
*/
|
||||
private void disconnect(MqttClient client) throws Exception {
|
||||
// 1. 断开连接
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
client.disconnect()
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
future.complete(null);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2. 等待断开结果
|
||||
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.mqtt.MqttClient;
|
||||
import io.vertx.mqtt.MqttClientOptions;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IoT 网关设备 MQTT 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 MQTT 协议管理子设备拓扑关系
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 网关设备连接认证</li>
|
||||
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
|
||||
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
|
||||
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
|
||||
* 认证成功后同一连接上的后续请求无需再携带认证信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 1883;
|
||||
private static final int TIMEOUT_SECONDS = 10;
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
|
||||
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
|
||||
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
|
||||
|
||||
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String SUB_DEVICE_NAME = "chazuo-it";
|
||||
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 连接认证测试 =====================
|
||||
|
||||
/**
|
||||
* 网关设备认证测试:获取网关设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 构建认证信息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
|
||||
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
|
||||
// 2. 创建客户端并连接
|
||||
MqttClient client = connect(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
// 断开连接
|
||||
client.disconnect()
|
||||
.onComplete(disconnectAr -> {
|
||||
if (disconnectAr.succeeded()) {
|
||||
log.info("[testAuth][断开连接成功]");
|
||||
} else {
|
||||
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
} else {
|
||||
log.error("[testAuth][连接失败]", ar.cause());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待测试完成
|
||||
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testAuth][测试超时]");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 拓扑管理测试 =====================
|
||||
|
||||
/**
|
||||
* 添加子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要绑定的子设备信息
|
||||
*/
|
||||
@Test
|
||||
public void testTopoAdd() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testTopoAdd][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 2.2 构建子设备认证信息
|
||||
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
|
||||
.setClientId(subAuthInfo.getClientId())
|
||||
.setUsername(subAuthInfo.getUsername())
|
||||
.setPassword(subAuthInfo.getPassword());
|
||||
|
||||
// 2.3 构建请求消息
|
||||
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/add",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoAdd][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要解绑的子设备信息
|
||||
*/
|
||||
@Test
|
||||
public void testTopoDelete() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testTopoDelete][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 2.2 构建请求消息
|
||||
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(
|
||||
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/delete",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoDelete][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台查询已绑定的子设备列表
|
||||
*/
|
||||
@Test
|
||||
public void testTopoGet() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testTopoGet][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 2.2 构建请求消息
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/topo/get",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testTopoGet][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备动态注册测试
|
||||
* <p>
|
||||
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
|
||||
* <p>
|
||||
* 注意:此接口需要网关认证
|
||||
*/
|
||||
@Test
|
||||
public void testSubDeviceRegister() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testSubDeviceRegister][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 2.2 构建请求消息
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
|
||||
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
|
||||
subDevice.setDeviceName("mougezishebei-mqtt");
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
|
||||
Collections.singletonList(subDevice),
|
||||
null, null, null);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 批量上报测试 =====================
|
||||
|
||||
/**
|
||||
* 批量上报属性测试(网关 + 子设备)
|
||||
* <p>
|
||||
* 网关设备批量上报自身属性、事件,以及子设备的属性、事件
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPackPost() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testPropertyPackPost][连接认证成功]");
|
||||
|
||||
// 2.1 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 2.2 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
|
||||
// 2.3 构建【网关设备】自身事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
|
||||
gatewayEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
|
||||
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.build();
|
||||
|
||||
// 2.4 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
|
||||
// 2.5 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
|
||||
subDeviceEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
|
||||
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("healthCheck", subDeviceEvent)
|
||||
.build();
|
||||
|
||||
// 2.6 构建子设备数据
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
|
||||
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
|
||||
subDeviceData.setProperties(subDeviceProperties);
|
||||
subDeviceData.setEvents(subDeviceEvents);
|
||||
|
||||
// 2.7 构建请求消息
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(List.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/property/pack/post",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", response);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 创建 MQTT 客户端
|
||||
*
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword())
|
||||
.setCleanSession(true)
|
||||
.setKeepAliveInterval(60);
|
||||
return MqttClient.create(vertx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接并认证网关设备
|
||||
*
|
||||
* @return 已认证的 MQTT 客户端
|
||||
*/
|
||||
private MqttClient connectAndAuth() throws Exception {
|
||||
// 1. 创建客户端并连接
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
MqttClient client = connect(authInfo);
|
||||
|
||||
// 2.1 连接
|
||||
CompletableFuture<MqttClient> future = new CompletableFuture<>();
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
future.complete(client);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2.2 等待连接结果
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
*/
|
||||
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
|
||||
// 1. 订阅响应主题
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
|
||||
future.complete(null);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2. 等待订阅结果
|
||||
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息并等待响应
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 发布主题
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
|
||||
future.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
|
||||
} else {
|
||||
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
*/
|
||||
private void disconnect(MqttClient client) throws Exception {
|
||||
// 1. 断开连接
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
client.disconnect()
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
future.complete(null);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2. 等待断开结果
|
||||
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.mqtt.MqttClient;
|
||||
import io.vertx.mqtt.MqttClientOptions;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IoT 网关子设备 MQTT 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
|
||||
*
|
||||
* <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
|
||||
* <p>网关设备转发子设备请求时,使用子设备自己的认证信息连接。
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(MQTT 端口 1883)</li>
|
||||
* <li>确保子设备已通过 {@link IotGatewayDeviceMqttProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 子设备连接认证</li>
|
||||
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
|
||||
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注意:MQTT 协议是有状态的长连接,认证在连接时通过 username/password 完成,
|
||||
* 认证成功后同一连接上的后续请求无需再携带认证信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 1883;
|
||||
private static final int TIMEOUT_SECONDS = 10;
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String DEVICE_NAME = "chazuo-it";
|
||||
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 连接认证测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备认证测试:获取子设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 构建认证信息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
log.info("[testAuth][认证信息: clientId={}, username={}, password={}]",
|
||||
authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
|
||||
// 2. 创建客户端并连接
|
||||
MqttClient client = connect(authInfo);
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
// 断开连接
|
||||
client.disconnect()
|
||||
.onComplete(disconnectAr -> {
|
||||
if (disconnectAr.succeeded()) {
|
||||
log.info("[testAuth][断开连接成功]");
|
||||
} else {
|
||||
log.error("[testAuth][断开连接失败]", disconnectAr.cause());
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
} else {
|
||||
log.error("[testAuth][连接失败]", ar.cause());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待测试完成
|
||||
boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testAuth][测试超时]");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备属性上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testPropertyPost][连接认证成功]");
|
||||
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 3. 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.put("status", "online")
|
||||
.put("temperature", 36.5)
|
||||
.build()),
|
||||
null, null, null);
|
||||
|
||||
// 4. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
|
||||
// 5. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 子设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备事件上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
// 1. 连接并认证
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testEventPost][连接认证成功]");
|
||||
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
|
||||
|
||||
// 2. 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribeReply(client, replyTopic);
|
||||
|
||||
// 3. 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
IotDeviceEventPostReqDTO.of(
|
||||
"alarm",
|
||||
MapUtil.<String, Object>builder()
|
||||
.put("level", "warning")
|
||||
.put("message", "temperature too high")
|
||||
.put("threshold", 40)
|
||||
.put("current", 42)
|
||||
.build(),
|
||||
System.currentTimeMillis()),
|
||||
null, null, null);
|
||||
|
||||
// 4. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
|
||||
// 5. 断开连接
|
||||
disconnect(client);
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 创建 MQTT 客户端
|
||||
*
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword())
|
||||
.setCleanSession(true)
|
||||
.setKeepAliveInterval(60);
|
||||
return MqttClient.create(vertx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接并认证子设备
|
||||
*
|
||||
* @return 已认证的 MQTT 客户端
|
||||
*/
|
||||
private MqttClient connectAndAuth() throws Exception {
|
||||
// 1. 创建客户端并连接
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
MqttClient client = connect(authInfo);
|
||||
|
||||
// 2.1 连接
|
||||
CompletableFuture<MqttClient> future = new CompletableFuture<>();
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
future.complete(client);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2.2 等待连接结果
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
*/
|
||||
private void subscribeReply(MqttClient client, String replyTopic) throws Exception {
|
||||
// 1. 订阅响应主题
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic);
|
||||
future.complete(null);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2. 等待订阅结果
|
||||
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息并等待响应
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 发布主题
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
|
||||
future.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false)
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result());
|
||||
} else {
|
||||
log.error("[publishAndWaitReply][消息发布失败]", ar.cause());
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
*/
|
||||
private void disconnect(MqttClient client) throws Exception {
|
||||
// 1. 断开连接
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
client.disconnect()
|
||||
.onComplete(ar -> {
|
||||
if (ar.succeeded()) {
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
future.complete(null);
|
||||
} else {
|
||||
future.completeExceptionally(ar.cause());
|
||||
}
|
||||
});
|
||||
// 2. 等待断开结果
|
||||
future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.HexUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* IoT 直连设备 TCP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台
|
||||
*
|
||||
* <p>支持两种编解码格式:
|
||||
* <ul>
|
||||
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
|
||||
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(TCP 端口 8091)</li>
|
||||
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 设备认证</li>
|
||||
* <li>{@link #testDeviceRegister()} - 设备动态注册(一型一密)</li>
|
||||
* <li>{@link #testPropertyPost()} - 设备属性上报</li>
|
||||
* <li>{@link #testEventPost()} - 设备事件上报</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8091;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
|
||||
private static final String DEVICE_NAME = "small";
|
||||
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 认证测试:获取设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.1 构建认证消息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 动态注册测试 =====================
|
||||
|
||||
/**
|
||||
* 直连设备动态注册测试(一型一密)
|
||||
* <p>
|
||||
* 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
|
||||
* <p>
|
||||
* 注意:此接口不需要认证
|
||||
*/
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1.1 构建注册消息
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
|
||||
registerReqDTO.setProductKey(PRODUCT_KEY);
|
||||
registerReqDTO.setDeviceName("test-tcp-" + System.currentTimeMillis());
|
||||
registerReqDTO.setProductSecret("test-product-secret");
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testDeviceRegister][响应消息: {}]", response);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
} else {
|
||||
log.warn("[testDeviceRegister][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 属性上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testPropertyPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("width", 1)
|
||||
.put("height", "2")
|
||||
.build()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testPropertyPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 事件上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testEventPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
IotDeviceEventPostReqDTO.of(
|
||||
"eat",
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testEventPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 执行设备认证
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
* @return 认证响应消息
|
||||
*/
|
||||
private IotDeviceMessage authenticate(Socket socket) throws Exception {
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
byte[] payload = CODEC.encode(request);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
if (responseBytes != null) {
|
||||
log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]",
|
||||
responseBytes.length,
|
||||
String.format("%02X", responseBytes[0]),
|
||||
HexUtil.encodeHexStr(responseBytes));
|
||||
return CODEC.decode(responseBytes);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 TCP 请求并接收响应
|
||||
*
|
||||
* @param socket TCP Socket
|
||||
* @param payload 请求数据
|
||||
* @return 响应数据
|
||||
*/
|
||||
private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
|
||||
// 1. 发送请求
|
||||
OutputStream out = socket.getOutputStream();
|
||||
InputStream in = socket.getInputStream();
|
||||
out.write(payload);
|
||||
out.flush();
|
||||
|
||||
// 2.1 等待一小段时间让服务器处理
|
||||
Thread.sleep(100);
|
||||
// 2.2 接收响应
|
||||
byte[] buffer = new byte[4096];
|
||||
try {
|
||||
int length = in.read(buffer);
|
||||
if (length > 0) {
|
||||
byte[] response = new byte[length];
|
||||
System.arraycopy(buffer, 0, response, 0, length);
|
||||
return response;
|
||||
}
|
||||
return null;
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 网关设备 TCP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 TCP 协议管理子设备拓扑关系
|
||||
*
|
||||
* <p>支持两种编解码格式:
|
||||
* <ul>
|
||||
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
|
||||
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(TCP 端口 8091)</li>
|
||||
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 网关设备认证</li>
|
||||
* <li>{@link #testTopoAdd()} - 添加子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoDelete()} - 删除子设备拓扑关系</li>
|
||||
* <li>{@link #testTopoGet()} - 获取子设备拓扑关系</li>
|
||||
* <li>{@link #testSubDeviceRegister()} - 子设备动态注册</li>
|
||||
* <li>{@link #testPropertyPackPost()} - 批量上报属性(网关 + 子设备)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8091;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
|
||||
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
|
||||
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
|
||||
|
||||
// ===================== 子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String SUB_DEVICE_NAME = "chazuo-it";
|
||||
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 网关设备认证测试:获取网关设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.1 构建认证消息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 拓扑管理测试 =====================
|
||||
|
||||
/**
|
||||
* 添加子设备拓扑关系测试
|
||||
*/
|
||||
@Test
|
||||
public void testTopoAdd() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testTopoAdd][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建子设备认证信息
|
||||
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
|
||||
.setClientId(subAuthInfo.getClientId())
|
||||
.setUsername(subAuthInfo.getUsername())
|
||||
.setPassword(subAuthInfo.getPassword());
|
||||
// 2.2 构建请求参数
|
||||
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.3 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testTopoAdd][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testTopoAdd][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子设备拓扑关系测试
|
||||
*/
|
||||
@Test
|
||||
public void testTopoDelete() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testTopoDelete][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建请求参数
|
||||
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(
|
||||
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testTopoDelete][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testTopoDelete][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子设备拓扑关系测试
|
||||
*/
|
||||
@Test
|
||||
public void testTopoGet() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testTopoGet][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建请求参数
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testTopoGet][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testTopoGet][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备动态注册测试
|
||||
*/
|
||||
@Test
|
||||
public void testSubDeviceRegister() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testSubDeviceRegister][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建请求参数
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
|
||||
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
|
||||
subDevice.setDeviceName("mougezishebei");
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
|
||||
Collections.singletonList(subDevice),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testSubDeviceRegister][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 批量上报测试 =====================
|
||||
|
||||
/**
|
||||
* 批量上报属性测试(网关 + 子设备)
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPackPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testPropertyPackPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
// 2.2 构建【网关设备】自身事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build());
|
||||
gatewayEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.build();
|
||||
// 2.3 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
// 2.4 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
|
||||
subDeviceEvent.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("healthCheck", subDeviceEvent)
|
||||
.build();
|
||||
// 2.5 构建子设备数据
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData();
|
||||
subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME));
|
||||
subDeviceData.setProperties(subDeviceProperties);
|
||||
subDeviceData.setEvents(subDeviceEvents);
|
||||
// 2.6 构建请求参数
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(List.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.7 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testPropertyPackPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 执行网关设备认证
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
* @return 认证响应消息
|
||||
*/
|
||||
private IotDeviceMessage authenticate(Socket socket) throws Exception {
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
byte[] payload = CODEC.encode(request);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
if (responseBytes != null) {
|
||||
return CODEC.decode(responseBytes);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 TCP 请求并接收响应
|
||||
*
|
||||
* @param socket TCP Socket
|
||||
* @param payload 请求数据
|
||||
* @return 响应数据
|
||||
*/
|
||||
private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
|
||||
// 1. 发送请求
|
||||
OutputStream out = socket.getOutputStream();
|
||||
InputStream in = socket.getInputStream();
|
||||
out.write(payload);
|
||||
out.flush();
|
||||
|
||||
// 2.1 等待一小段时间让服务器处理
|
||||
Thread.sleep(100);
|
||||
// 2.2 接收响应
|
||||
byte[] buffer = new byte[4096];
|
||||
try {
|
||||
int length = in.read(buffer);
|
||||
if (length > 0) {
|
||||
byte[] response = new byte[length];
|
||||
System.arraycopy(buffer, 0, response, 0, length);
|
||||
return response;
|
||||
}
|
||||
return null;
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.tcp;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* IoT 网关子设备 TCP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:子设备(IotProductDeviceTypeEnum 的 SUB 类型)通过网关设备代理上报数据
|
||||
*
|
||||
* <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
|
||||
*
|
||||
* <p>支持两种编解码格式:
|
||||
* <ul>
|
||||
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
|
||||
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(TCP 端口 8091)</li>
|
||||
* <li>确保子设备已通过 {@link IotGatewayDeviceTcpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
|
||||
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 子设备认证</li>
|
||||
* <li>{@link #testPropertyPost()} - 子设备属性上报(由网关代理转发)</li>
|
||||
* <li>{@link #testEventPost()} - 子设备事件上报(由网关代理转发)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注意:TCP 协议是有状态的长连接,认证成功后同一连接上的后续请求无需再携带认证信息
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8091;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "jAufEMTF1W6wnPhn";
|
||||
private static final String DEVICE_NAME = "chazuo-it";
|
||||
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备认证测试:获取子设备 Token
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.1 构建认证消息
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备属性上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备属性上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testPropertyPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.put("status", "online")
|
||||
.put("temperature", 36.5)
|
||||
.build()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testPropertyPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备事件上报测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备事件上报测试
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testEventPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
IotDeviceEventPostReqDTO.of(
|
||||
"alarm",
|
||||
MapUtil.<String, Object>builder()
|
||||
.put("level", "warning")
|
||||
.put("message", "temperature too high")
|
||||
.put("threshold", 40)
|
||||
.put("current", 42)
|
||||
.build(),
|
||||
System.currentTimeMillis()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 3.1 发送请求
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 3.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testEventPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 执行子设备认证
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
* @return 认证响应消息
|
||||
*/
|
||||
private IotDeviceMessage authenticate(Socket socket) throws Exception {
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
byte[] payload = CODEC.encode(request);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
if (responseBytes != null) {
|
||||
return CODEC.decode(responseBytes);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 TCP 请求并接收响应
|
||||
*
|
||||
* @param socket TCP Socket
|
||||
* @param payload 请求数据
|
||||
* @return 响应数据
|
||||
*/
|
||||
private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception {
|
||||
// 1. 发送请求
|
||||
OutputStream out = socket.getOutputStream();
|
||||
InputStream in = socket.getInputStream();
|
||||
out.write(payload);
|
||||
out.flush();
|
||||
|
||||
// 2.1 等待一小段时间让服务器处理
|
||||
Thread.sleep(100);
|
||||
// 2.2 接收响应
|
||||
byte[] buffer = new byte[4096];
|
||||
try {
|
||||
int length = in.read(buffer);
|
||||
if (length > 0) {
|
||||
byte[] response = new byte[length];
|
||||
System.arraycopy(buffer, 0, response, 0, length);
|
||||
return response;
|
||||
}
|
||||
return null;
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
# TCP 二进制协议数据包格式说明
|
||||
|
||||
## 1. 协议概述
|
||||
|
||||
TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。
|
||||
|
||||
### 1.1 协议特点
|
||||
|
||||
- **高效传输**:完全二进制格式,减少数据传输量
|
||||
- **版本控制**:内置协议版本号,支持协议升级
|
||||
- **类型安全**:明确的消息类型标识
|
||||
- **简洁设计**:去除冗余字段,协议更加精简
|
||||
- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容
|
||||
|
||||
## 2. 协议格式
|
||||
|
||||
### 2.1 整体结构
|
||||
|
||||
```
|
||||
+--------+--------+--------+---------------------------+--------+--------+
|
||||
| 魔术字 | 版本号 | 消息类型| 消息长度(4字节) |
|
||||
+--------+--------+--------+---------------------------+--------+--------+
|
||||
| 消息 ID 长度(2字节) | 消息 ID (变长字符串) |
|
||||
+--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
| 方法名长度(2字节) | 方法名(变长字符串) |
|
||||
+--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
| 消息体数据(变长) |
|
||||
+--------+--------+--------+--------+--------+--------+--------+--------+
|
||||
```
|
||||
|
||||
### 2.2 字段详细说明
|
||||
|
||||
| 字段 | 长度 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 |
|
||||
| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 |
|
||||
| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 |
|
||||
| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) |
|
||||
| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 |
|
||||
| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) |
|
||||
| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 |
|
||||
| 方法名 | 变长 | string | 消息方法名(UTF-8编码) |
|
||||
| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 |
|
||||
|
||||
**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置
|
||||
|
||||
### 2.3 协议常量定义
|
||||
|
||||
```java
|
||||
// 协议标识
|
||||
private static final byte MAGIC_NUMBER = (byte) 0x7E;
|
||||
private static final byte PROTOCOL_VERSION = (byte) 0x01;
|
||||
|
||||
// 消息类型
|
||||
private static final byte REQUEST = (byte) 0x01; // 请求消息
|
||||
private static final byte RESPONSE = (byte) 0x02; // 响应消息
|
||||
|
||||
// 协议长度
|
||||
private static final int HEADER_FIXED_LENGTH = 7; // 固定头部长度
|
||||
private static final int MIN_MESSAGE_LENGTH = 11; // 最小消息长度
|
||||
```
|
||||
|
||||
## 3. 消息类型和格式
|
||||
|
||||
### 3.1 请求消息 (REQUEST - 0x01)
|
||||
|
||||
请求消息用于设备向服务器发送数据或请求。
|
||||
|
||||
#### 3.1.1 消息体格式
|
||||
```
|
||||
消息体 = params 数据(JSON格式)
|
||||
```
|
||||
|
||||
#### 3.1.2 示例:设备认证请求
|
||||
|
||||
**消息内容:**
|
||||
- 消息 ID: `auth_1704067200000_123`
|
||||
- 方法名: `auth`
|
||||
- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}`
|
||||
|
||||
**二进制数据包结构:**
|
||||
```
|
||||
7E // 魔术字 (0x7E)
|
||||
01 // 版本号 (0x01)
|
||||
01 // 消息类型 (REQUEST)
|
||||
00 00 00 89 // 消息长度 (137字节)
|
||||
00 19 // 消息 ID 长度 (25字节)
|
||||
61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123"
|
||||
36 37 32 30 30 30 30 30 5F 31
|
||||
32 33
|
||||
00 04 // 方法名长度 (4字节)
|
||||
61 75 74 68 // 方法名: "auth"
|
||||
7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据
|
||||
22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001",
|
||||
30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName",
|
||||
6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"}
|
||||
64 75 63 74 4B 65 79 5F 64 65
|
||||
76 69 63 65 4E 61 6D 65 22 2C
|
||||
22 70 61 73 73 77 6F 72 64 22
|
||||
3A 22 64 65 76 69 63 65 5F 70
|
||||
61 73 73 77 6F 72 64 22 7D
|
||||
```
|
||||
|
||||
#### 3.1.3 示例:属性数据上报
|
||||
|
||||
**消息内容:**
|
||||
- 消息 ID: `property_1704067200000_456`
|
||||
- 方法名: `thing.property.post`
|
||||
- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}`
|
||||
|
||||
### 3.2 响应消息 (RESPONSE - 0x02)
|
||||
|
||||
响应消息用于服务器向设备回复请求结果。
|
||||
|
||||
#### 3.2.1 消息体格式
|
||||
```
|
||||
消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON)
|
||||
```
|
||||
|
||||
#### 3.2.2 字段说明
|
||||
|
||||
| 字段 | 长度 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 |
|
||||
| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 |
|
||||
| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) |
|
||||
| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) |
|
||||
|
||||
#### 3.2.3 示例:认证成功响应
|
||||
|
||||
**消息内容:**
|
||||
- 消息 ID: `auth_response_1704067200000_123`
|
||||
- 方法名: `auth`
|
||||
- 响应码: `0`
|
||||
- 响应消息: `认证成功`
|
||||
- 响应数据: `{"success":true,"message":"认证成功"}`
|
||||
|
||||
**二进制数据包结构:**
|
||||
```
|
||||
7E // 魔术字 (0x7E)
|
||||
01 // 版本号 (0x01)
|
||||
02 // 消息类型 (RESPONSE)
|
||||
00 00 00 A4 // 消息长度 (164字节)
|
||||
00 22 // 消息 ID 长度 (34字节)
|
||||
61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123"
|
||||
6E 73 65 5F 31 37 30 34 30 36
|
||||
37 32 30 30 30 30 30 5F 31 32
|
||||
33
|
||||
00 04 // 方法名长度 (4字节)
|
||||
61 75 74 68 // 方法名: "auth"
|
||||
00 00 00 00 // 响应码 (0 = 成功)
|
||||
00 0C // 响应消息长度 (12字节)
|
||||
E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8)
|
||||
8A 9F
|
||||
7B 22 73 75 63 63 65 73 73 22 // JSON响应数据
|
||||
3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"}
|
||||
73 61 67 65 22 3A 22 E8 AE A4
|
||||
E8 AF 81 E6 88 90 E5 8A 9F 22
|
||||
7D
|
||||
```
|
||||
|
||||
## 4. 编解码器标识
|
||||
|
||||
```java
|
||||
public static final String TYPE = "TCP_BINARY";
|
||||
```
|
||||
|
||||
## 5. 协议优势
|
||||
|
||||
- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量
|
||||
- **解析高效**:直接二进制操作,减少字符串转换开销
|
||||
- **类型安全**:明确的消息类型和字段定义
|
||||
- **设计简洁**:去除冗余字段,协议更加精简高效
|
||||
- **版本控制**:内置版本号支持协议升级
|
||||
|
||||
## 6. 与 JSON 协议对比
|
||||
|
||||
| 特性 | 二进制协议 | JSON协议 |
|
||||
|------|-------------|--------|
|
||||
| 数据大小 | 小(节省30-50%) | 大 |
|
||||
| 解析性能 | 高 | 中等 |
|
||||
| 网络开销 | 低 | 高 |
|
||||
| 可读性 | 差 | 优秀 |
|
||||
| 调试难度 | 高 | 低 |
|
||||
| 扩展性 | 良好 | 优秀 |
|
||||
|
||||
**推荐场景**:
|
||||
- ✅ **高频数据传输**:传感器数据实时上报
|
||||
- ✅ **带宽受限环境**:移动网络、卫星通信
|
||||
- ✅ **性能要求高**:需要低延迟、高吞吐的场景
|
||||
- ✅ **设备资源有限**:嵌入式设备、低功耗设备
|
||||
- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议
|
||||
- ❌ **快速原型开发**:开发效率低
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user