Commit b270d82d7529e0a6fd6faa9a82acdb501daefa8f

Authored by YunaiV
1 parent bec8cc6e

feat:【iot】coap 协议接入 100%:,基于 rippling-noodling-wombat.d 规划

yudao-dependencies/pom.xml
@@ -68,6 +68,7 @@ @@ -68,6 +68,7 @@
68 <netty.version>4.2.9.Final</netty.version> 68 <netty.version>4.2.9.Final</netty.version>
69 <mqtt.version>1.2.5</mqtt.version> 69 <mqtt.version>1.2.5</mqtt.version>
70 <vertx.version>4.5.22</vertx.version> 70 <vertx.version>4.5.22</vertx.version>
  71 + <californium.version>3.12.0</californium.version>
71 <!-- 三方云服务相关 --> 72 <!-- 三方云服务相关 -->
72 <awssdk.version>2.40.15</awssdk.version> 73 <awssdk.version>2.40.15</awssdk.version>
73 <justauth.version>1.16.7</justauth.version> 74 <justauth.version>1.16.7</justauth.version>
@@ -653,6 +654,13 @@ @@ -653,6 +654,13 @@
653 <artifactId>org.eclipse.paho.client.mqttv3</artifactId> 654 <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
654 <version>${mqtt.version}</version> 655 <version>${mqtt.version}</version>
655 </dependency> 656 </dependency>
  657 +
  658 + <!-- CoAP - Eclipse Californium -->
  659 + <dependency>
  660 + <groupId>org.eclipse.californium</groupId>
  661 + <artifactId>californium-core</artifactId>
  662 + <version>${californium.version}</version>
  663 + </dependency>
656 </dependencies> 664 </dependencies>
657 </dependencyManagement> 665 </dependencyManagement>
658 666
yudao-module-iot/yudao-module-iot-gateway/pom.xml
@@ -48,6 +48,12 @@ @@ -48,6 +48,12 @@
48 <artifactId>vertx-mqtt</artifactId> 48 <artifactId>vertx-mqtt</artifactId>
49 </dependency> 49 </dependency>
50 50
  51 + <!-- CoAP 相关 - Eclipse Californium -->
  52 + <dependency>
  53 + <groupId>org.eclipse.californium</groupId>
  54 + <artifactId>californium-core</artifactId>
  55 + </dependency>
  56 +
51 <!-- 测试相关 --> 57 <!-- 测试相关 -->
52 <dependency> 58 <dependency>
53 <groupId>cn.iocoder.boot</groupId> 59 <groupId>cn.iocoder.boot</groupId>
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
@@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.iot.gateway.config; @@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.iot.gateway.config;
3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; 4 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; 5 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler;  
7 -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler;  
8 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
@@ -207,11 +205,8 @@ public class IotGatewayConfiguration { @@ -207,11 +205,8 @@ public class IotGatewayConfiguration {
207 public static class CoapProtocolConfiguration { 205 public static class CoapProtocolConfiguration {
208 206
209 @Bean 207 @Bean
210 - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties,  
211 - IotCoapAuthHandler authHandler,  
212 - IotCoapUpstreamHandler upstreamHandler) {  
213 - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap(),  
214 - authHandler, upstreamHandler); 208 + public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
  209 + return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
215 } 210 }
216 211
217 @Bean 212 @Bean
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java
@@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit; @@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit;
22 * 基于 Eclipse Californium 实现,支持: 22 * 基于 Eclipse Californium 实现,支持:
23 * 1. 认证:POST /auth 23 * 1. 认证:POST /auth
24 * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post 24 * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
25 - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/{eventId}/post 25 + * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
26 * 26 *
27 * @author 芋道源码 27 * @author 芋道源码
28 */ 28 */
@@ -31,20 +31,13 @@ public class IotCoapUpstreamProtocol { @@ -31,20 +31,13 @@ public class IotCoapUpstreamProtocol {
31 31
32 private final IotGatewayProperties.CoapProperties coapProperties; 32 private final IotGatewayProperties.CoapProperties coapProperties;
33 33
34 - private final IotCoapAuthHandler authHandler;  
35 - private final IotCoapUpstreamHandler upstreamHandler;  
36 -  
37 private CoapServer coapServer; 34 private CoapServer coapServer;
38 35
39 @Getter 36 @Getter
40 private final String serverId; 37 private final String serverId;
41 38
42 - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties,  
43 - IotCoapAuthHandler authHandler,  
44 - IotCoapUpstreamHandler upstreamHandler) { 39 + public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
45 this.coapProperties = coapProperties; 40 this.coapProperties = coapProperties;
46 - this.authHandler = authHandler;  
47 - this.upstreamHandler = upstreamHandler;  
48 this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); 41 this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
49 } 42 }
50 43
@@ -61,9 +54,11 @@ public class IotCoapUpstreamProtocol { @@ -61,9 +54,11 @@ public class IotCoapUpstreamProtocol {
61 coapServer = new CoapServer(config); 54 coapServer = new CoapServer(config);
62 55
63 // 2.1 添加 /auth 认证资源 56 // 2.1 添加 /auth 认证资源
  57 + IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
64 IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler); 58 IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
65 coapServer.add(authResource); 59 coapServer.add(authResource);
66 // 2.2 添加 /topic 根资源(用于上行消息) 60 // 2.2 添加 /topic 根资源(用于上行消息)
  61 + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
67 IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler); 62 IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
68 coapServer.add(topicResource); 63 coapServer.add(topicResource);
69 64
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java
1 /** 1 /**
2 - * IoT 网关 CoAP 协议  
3 - *  
4 - * 基于 Eclipse Californium 实现,支持设备通过 CoAP 协议进行:  
5 - * 1. 属性上报:POST /sys/{productKey}/{deviceName}/thing/property/post  
6 - * 2. 事件上报:POST /sys/{productKey}/{deviceName}/thing/event/{eventId}/post  
7 - *  
8 - * 认证方式:通过 URI Query 参数 token 进行认证  
9 - * 示例:coap://server:5683/sys/pk/dn/thing/property/post?token=xxx  
10 - *  
11 - * @author 芋道源码 2 + * CoAP 协议实现包
  3 + * <p>
  4 + * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能
  5 + * <p>
  6 + * URI 路径:
  7 + * - 认证:POST /auth
  8 + * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  9 + * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  10 + * <p>
  11 + * Token 通过 CoAP Option 2088 携带
12 */ 12 */
13 -// TODO @AI:参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java (现在注释应该有点不太对)  
14 package cn.iocoder.yudao.module.iot.gateway.protocol.coap; 13 package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java
@@ -4,6 +4,7 @@ import cn.hutool.core.lang.Assert; @@ -4,6 +4,7 @@ import cn.hutool.core.lang.Assert;
4 import cn.hutool.core.map.MapUtil; 4 import cn.hutool.core.map.MapUtil;
5 import cn.hutool.core.util.BooleanUtil; 5 import cn.hutool.core.util.BooleanUtil;
6 import cn.hutool.core.util.StrUtil; 6 import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.extra.spring.SpringUtil;
7 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 10 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
@@ -11,19 +12,15 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; @@ -11,19 +12,15 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 12 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 13 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
14 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; 16 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
15 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 17 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
16 -import lombok.RequiredArgsConstructor;  
17 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
18 import org.eclipse.californium.core.coap.CoAP; 19 import org.eclipse.californium.core.coap.CoAP;
19 -import org.eclipse.californium.core.coap.MediaTypeRegistry;  
20 import org.eclipse.californium.core.server.resources.CoapExchange; 20 import org.eclipse.californium.core.server.resources.CoapExchange;
21 -import org.springframework.stereotype.Component;  
22 21
23 import java.util.Map; 22 import java.util.Map;
24 23
25 -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;  
26 -  
27 /** 24 /**
28 * IoT 网关 CoAP 协议的【认证】处理器 25 * IoT 网关 CoAP 协议的【认证】处理器
29 * 26 *
@@ -31,53 +28,55 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -31,53 +28,55 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
31 * 28 *
32 * @author 芋道源码 29 * @author 芋道源码
33 */ 30 */
34 -@Component  
35 -@RequiredArgsConstructor  
36 @Slf4j 31 @Slf4j
37 public class IotCoapAuthHandler { 32 public class IotCoapAuthHandler {
38 33
39 private final IotDeviceTokenService deviceTokenService; 34 private final IotDeviceTokenService deviceTokenService;
40 -  
41 private final IotDeviceCommonApi deviceApi; 35 private final IotDeviceCommonApi deviceApi;
42 -  
43 private final IotDeviceMessageService deviceMessageService; 36 private final IotDeviceMessageService deviceMessageService;
44 37
  38 + public IotCoapAuthHandler() {
  39 + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  40 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  41 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  42 + }
  43 +
45 /** 44 /**
46 * 处理认证请求 45 * 处理认证请求
47 * 46 *
48 * @param exchange CoAP 交换对象 47 * @param exchange CoAP 交换对象
49 * @param protocol 协议对象 48 * @param protocol 协议对象
50 */ 49 */
  50 + @SuppressWarnings("unchecked")
51 public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { 51 public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
52 try { 52 try {
53 // 1.1 解析请求体 53 // 1.1 解析请求体
54 byte[] payload = exchange.getRequestPayload(); 54 byte[] payload = exchange.getRequestPayload();
55 if (payload == null || payload.length == 0) { 55 if (payload == null || payload.length == 0) {
56 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); 56 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
57 return; 57 return;
58 } 58 }
59 Map<String, Object> body; 59 Map<String, Object> body;
60 try { 60 try {
61 body = JsonUtils.parseObject(new String(payload), Map.class); 61 body = JsonUtils.parseObject(new String(payload), Map.class);
62 } catch (Exception e) { 62 } catch (Exception e) {
63 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); 63 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
64 return; 64 return;
65 } 65 }
66 - // TODO @AI:通过 hutool maputil 去获取,简化下;  
67 // 1.2 解析参数 66 // 1.2 解析参数
68 - String clientId = (String) body.get("clientId"); 67 + String clientId = MapUtil.getStr(body, "clientId");
69 if (StrUtil.isEmpty(clientId)) { 68 if (StrUtil.isEmpty(clientId)) {
70 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空"); 69 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空");
71 return; 70 return;
72 } 71 }
73 - String username = (String) body.get("username"); 72 + String username = MapUtil.getStr(body, "username");
74 if (StrUtil.isEmpty(username)) { 73 if (StrUtil.isEmpty(username)) {
75 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空"); 74 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空");
76 return; 75 return;
77 } 76 }
78 - String password = (String) body.get("password"); 77 + String password = MapUtil.getStr(body, "password");
79 if (StrUtil.isEmpty(password)) { 78 if (StrUtil.isEmpty(password)) {
80 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空"); 79 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空");
81 return; 80 return;
82 } 81 }
83 82
@@ -86,12 +85,12 @@ public class IotCoapAuthHandler { @@ -86,12 +85,12 @@ public class IotCoapAuthHandler {
86 .setClientId(clientId).setUsername(username).setPassword(password)); 85 .setClientId(clientId).setUsername(username).setPassword(password));
87 if (result.isError()) { 86 if (result.isError()) {
88 log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg()); 87 log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg());
89 - respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg()); 88 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg());
90 return; 89 return;
91 } 90 }
92 if (!BooleanUtil.isTrue(result.getData())) { 91 if (!BooleanUtil.isTrue(result.getData())) {
93 log.warn("[handle][认证失败,clientId: {}]", clientId); 92 log.warn("[handle][认证失败,clientId: {}]", clientId);
94 - respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败"); 93 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败");
95 return; 94 return;
96 } 95 }
97 // 2.2 生成 Token 96 // 2.2 生成 Token
@@ -108,31 +107,11 @@ public class IotCoapAuthHandler { @@ -108,31 +107,11 @@ public class IotCoapAuthHandler {
108 // 4. 返回成功响应 107 // 4. 返回成功响应
109 log.info("[handle][认证成功,productKey: {}, deviceName: {}]", 108 log.info("[handle][认证成功,productKey: {}, deviceName: {}]",
110 deviceInfo.getProductKey(), deviceInfo.getDeviceName()); 109 deviceInfo.getProductKey(), deviceInfo.getDeviceName());
111 - respondSuccess(exchange, MapUtil.of("token", token)); 110 + IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token));
112 } catch (Exception e) { 111 } catch (Exception e) {
113 log.error("[handle][认证处理异常]", e); 112 log.error("[handle][认证处理异常]", e);
114 - respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); 113 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
115 } 114 }
116 } 115 }
117 116
118 - // TODO @AI:抽到 coap 的 util 里;  
119 - /**  
120 - * 返回成功响应  
121 - */  
122 - private void respondSuccess(CoapExchange exchange, Object data) {  
123 - CommonResult<Object> result = success(data);  
124 - String json = JsonUtils.toJsonString(result);  
125 - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);  
126 - }  
127 -  
128 - // TODO @AI:抽到 coap 的 util 里;  
129 - /**  
130 - * 返回错误响应  
131 - */  
132 - private void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {  
133 - CommonResult<Object> result = CommonResult.error(code.value, message);  
134 - String json = JsonUtils.toJsonString(result);  
135 - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);  
136 - }  
137 -  
138 } 117 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
3 import cn.hutool.core.map.MapUtil; 5 import cn.hutool.core.map.MapUtil;
4 import cn.hutool.core.text.StrPool; 6 import cn.hutool.core.text.StrPool;
  7 +import cn.hutool.core.util.ArrayUtil;
5 import cn.hutool.core.util.ObjUtil; 8 import cn.hutool.core.util.ObjUtil;
6 import cn.hutool.core.util.StrUtil; 9 import cn.hutool.core.util.StrUtil;
7 -import cn.iocoder.yudao.framework.common.pojo.CommonResult;  
8 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 10 +import cn.hutool.extra.spring.SpringUtil;
9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
12 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; 15 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
13 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 16 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
14 -import lombok.RequiredArgsConstructor;  
15 import lombok.extern.slf4j.Slf4j; 17 import lombok.extern.slf4j.Slf4j;
16 import org.eclipse.californium.core.coap.CoAP; 18 import org.eclipse.californium.core.coap.CoAP;
17 -import org.eclipse.californium.core.coap.MediaTypeRegistry;  
18 import org.eclipse.californium.core.server.resources.CoapExchange; 19 import org.eclipse.californium.core.server.resources.CoapExchange;
19 -import org.springframework.stereotype.Component;  
20 20
21 import java.util.List; 21 import java.util.List;
22 22
23 -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;  
24 -  
25 /** 23 /**
26 * IoT 网关 CoAP 协议的【上行】处理器 24 * IoT 网关 CoAP 协议的【上行】处理器
27 * 25 *
28 * 处理设备通过 CoAP 协议发送的上行消息,包括: 26 * 处理设备通过 CoAP 协议发送的上行消息,包括:
29 * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post 27 * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
30 - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/{eventId}/post 28 + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
31 * 29 *
32 * Token 通过自定义 CoAP Option 2088 携带 30 * Token 通过自定义 CoAP Option 2088 携带
33 * 31 *
34 * @author 芋道源码 32 * @author 芋道源码
35 */ 33 */
36 -@Component  
37 -@RequiredArgsConstructor  
38 @Slf4j 34 @Slf4j
39 public class IotCoapUpstreamHandler { 35 public class IotCoapUpstreamHandler {
40 36
41 - /**  
42 - * 自定义 CoAP Option 编号,用于携带 Token  
43 - * CoAP Option 范围 2048-65535 属于实验/自定义范围  
44 - */  
45 - public static final int OPTION_TOKEN = 2088;  
46 -  
47 private final IotDeviceTokenService deviceTokenService; 37 private final IotDeviceTokenService deviceTokenService;
48 private final IotDeviceMessageService deviceMessageService; 38 private final IotDeviceMessageService deviceMessageService;
49 39
  40 + public IotCoapUpstreamHandler() {
  41 + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  42 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  43 + }
  44 +
50 /** 45 /**
51 * 处理 CoAP 请求 46 * 处理 CoAP 请求
52 * 47 *
53 * @param exchange CoAP 交换对象 48 * @param exchange CoAP 交换对象
54 - * @param httpMethod HTTP 方法  
55 * @param protocol 协议对象 49 * @param protocol 协议对象
56 */ 50 */
57 - public void handle(CoapExchange exchange, String httpMethod, IotCoapUpstreamProtocol protocol) { 51 + public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
58 try { 52 try {
59 - // TODO @AI:这种路径的解析,不用了,简化下,类似 IotHttpUpstreamHandler 这种就很简洁;  
60 - // 1. 解析 URI 路径:/topic/sys/{productKey}/{deviceName}/thing/...  
61 - // 完整路径是 [topic, sys, productKey, deviceName, thing, ...] 53 + // 1. 解析通用参数
62 List<String> uriPath = exchange.getRequestOptions().getUriPath(); 54 List<String> uriPath = exchange.getRequestOptions().getUriPath();
63 - if (uriPath.size() < 6) {  
64 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,  
65 - "URI 路径格式错误,期望:/topic/sys/{productKey}/{deviceName}/..."); 55 + String productKey = CollUtil.get(uriPath, 2);
  56 + String deviceName = CollUtil.get(uriPath, 3);
  57 + byte[] payload = exchange.getRequestPayload();
  58 + if (StrUtil.isEmpty(productKey)) {
  59 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
66 return; 60 return;
67 } 61 }
68 -  
69 - // 验证路径格式:第一个应该是 "topic",第二个应该是 "sys"  
70 - if (!"topic".equals(uriPath.get(0)) || !"sys".equals(uriPath.get(1))) {  
71 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "URI 路径格式错误,期望以 /topic/sys 开头"); 62 + if (StrUtil.isEmpty(deviceName)) {
  63 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
72 return; 64 return;
73 } 65 }
74 -  
75 - // 解析 productKey 和 deviceName(索引 2 和 3)  
76 - String productKey = uriPath.get(2);  
77 - String deviceName = uriPath.get(3);  
78 -  
79 - // 2. 认证:优先从自定义 Option 获取 token,兼容 Query 参数  
80 - String token = getTokenFromOption(exchange);  
81 - if (StrUtil.isEmpty(token)) {  
82 - // 兼容 Query 参数方式  
83 - // TODO @AI:不用兼容 query,简化下;  
84 - token = getQueryParam(exchange, "token"); 66 + if (ArrayUtil.isEmpty(payload)) {
  67 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
  68 + return;
85 } 69 }
  70 +
  71 + // 2. 认证:从自定义 Option 获取 token
  72 + String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
86 if (StrUtil.isEmpty(token)) { 73 if (StrUtil.isEmpty(token)) {
87 - respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "缺少 token(请使用 Option " + OPTION_TOKEN + " 或 Query 参数携带)"); 74 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
88 return; 75 return;
89 } 76 }
90 -  
91 // 验证 token 77 // 验证 token
92 - // TODO @AI:这里参考 IotHttpAbstractHandler 简化点校验;  
93 IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token); 78 IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token);
94 if (deviceInfo == null) { 79 if (deviceInfo == null) {
95 - respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期"); 80 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
96 return; 81 return;
97 } 82 }
98 // 验证设备信息匹配 83 // 验证设备信息匹配
99 if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) 84 if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
100 || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { 85 || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
101 - respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配"); 86 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
102 return; 87 return;
103 } 88 }
104 89
105 - // 3. 解析 method:将 URI 路径转换为 method 格式  
106 - // /topic/sys/pk/dn/thing/property/post -> thing.property.post  
107 - // 路径是 [sys, pk, dn, thing, property, post],从索引 3 开始  
108 - String method = buildMethod(uriPath); 90 + // 2.1 解析 method:deviceName 后面的路径,用 . 拼接
  91 + // 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
  92 + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
109 93
110 - // 4. 解析并处理消息体  
111 - byte[] payload = exchange.getRequestPayload();  
112 - if (payload == null || payload.length == 0) {  
113 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");  
114 - return;  
115 - }  
116 -  
117 - // 5. 解码消息  
118 - IotDeviceMessage message;  
119 - try {  
120 - message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);  
121 - } catch (Exception e) {  
122 - log.error("[handle][消息解码失败]", e);  
123 - respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "消息解码失败:" + e.getMessage());  
124 - return;  
125 - }  
126 -  
127 - // 校验 method  
128 - // TODO @AI:不用校验 method;以 message 解析出来的为主;  
129 - if (!method.equals(message.getMethod())) {  
130 - log.warn("[handle][method 不匹配,URI: {}, 消息: {}]", method, message.getMethod());  
131 - }  
132 -  
133 - // 6. 发送消息到消息总线 94 + // 2.2 解码消息
  95 + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
  96 + Assert.equals(method, message.getMethod(), "method 不匹配");
  97 + // 2.3 发送消息到消息总线
134 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId()); 98 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
135 99
136 - // 7. 返回成功响应  
137 - respondSuccess(exchange, message.getId()); 100 + // 3. 返回成功响应
  101 + IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
138 } catch (Exception e) { 102 } catch (Exception e) {
139 log.error("[handle][CoAP 请求处理异常]", e); 103 log.error("[handle][CoAP 请求处理异常]", e);
140 - respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");  
141 - }  
142 - }  
143 -  
144 - /**  
145 - * 构建 method 字符串  
146 - *  
147 - * 将 URI 路径转换为 method 格式,例如:  
148 - * [sys, pk, dn, thing, property, post] -> thing.property.post  
149 - *  
150 - * @param uriPath URI 路径列表  
151 - * @return method 字符串  
152 - */  
153 - private String buildMethod(List<String> uriPath) {  
154 - // 跳过 sys, productKey, deviceName,从第4个元素开始  
155 - if (uriPath.size() > 3) {  
156 - return String.join(StrPool.DOT, uriPath.subList(3, uriPath.size()));  
157 - }  
158 - return "";  
159 - }  
160 -  
161 - // TODO @AI:抽到 coap 的 util 里;  
162 - /**  
163 - * 从自定义 CoAP Option 中获取 Token  
164 - *  
165 - * @param exchange CoAP 交换对象  
166 - * @return Token 值,如果不存在则返回 null  
167 - */  
168 - private String getTokenFromOption(CoapExchange exchange) {  
169 - // 尝试从自定义 Option 2088 获取 Token  
170 - byte[] tokenBytes = exchange.getRequestOptions().getOthers().stream()  
171 - .filter(option -> option.getNumber() == OPTION_TOKEN)  
172 - .findFirst()  
173 - .map(option -> option.getValue())  
174 - .orElse(null);  
175 - if (tokenBytes != null) {  
176 - return new String(tokenBytes);  
177 - }  
178 - return null;  
179 - }  
180 -  
181 - // TODO @AI:抽到 coap 的 util 里;  
182 - /**  
183 - * 从 URI Query 参数中获取指定 key 的值  
184 - *  
185 - * @param exchange CoAP 交换对象  
186 - * @param key 参数名  
187 - * @return 参数值,如果不存在则返回 null  
188 - */  
189 - private String getQueryParam(CoapExchange exchange, String key) {  
190 - for (String query : exchange.getRequestOptions().getUriQuery()) {  
191 - if (query.startsWith(key + "=")) {  
192 - return query.substring((key + "=").length());  
193 - } 104 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
194 } 105 }
195 - return null;  
196 - }  
197 -  
198 - // TODO @AI:抽到 coap 的 util 里;  
199 - /**  
200 - * 返回成功响应  
201 - *  
202 - * @param exchange CoAP 交换对象  
203 - * @param messageId 消息 ID  
204 - */  
205 - private void respondSuccess(CoapExchange exchange, String messageId) {  
206 - CommonResult<Object> result = CommonResult.success(MapUtil.of("messageId", messageId));  
207 - String json = JsonUtils.toJsonString(result);  
208 - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);  
209 - }  
210 -  
211 - // TODO @AI:抽到 coap 的 util 里;  
212 - /**  
213 - * 返回错误响应  
214 - *  
215 - * @param exchange CoAP 交换对象  
216 - * @param code CoAP 响应码  
217 - * @param message 错误消息  
218 - */  
219 - private void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {  
220 - // 将 CoAP 响应码映射到业务错误码  
221 - int errorCode = mapCoapCodeToErrorCode(code);  
222 - CommonResult<Object> result = CommonResult.error(errorCode, message);  
223 - String json = JsonUtils.toJsonString(result);  
224 - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);  
225 - }  
226 -  
227 - // TODO @AI:兼容 jdk8 的写法;  
228 - /**  
229 - * 将 CoAP 响应码映射到业务错误码  
230 - *  
231 - * @param code CoAP 响应码  
232 - * @return 业务错误码  
233 - */  
234 - private int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {  
235 - return switch (code) {  
236 - case BAD_REQUEST -> BAD_REQUEST.getCode();  
237 - case UNAUTHORIZED -> UNAUTHORIZED.getCode();  
238 - case FORBIDDEN -> FORBIDDEN.getCode();  
239 - default -> INTERNAL_SERVER_ERROR.getCode();  
240 - };  
241 } 106 }
242 107
243 } 108 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java
@@ -51,17 +51,17 @@ public class IotCoapUpstreamTopicResource extends CoapResource { @@ -51,17 +51,17 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
51 51
52 @Override 52 @Override
53 public void handleGET(CoapExchange exchange) { 53 public void handleGET(CoapExchange exchange) {
54 - upstreamHandler.handle(exchange, "GET", protocol); 54 + upstreamHandler.handle(exchange, protocol);
55 } 55 }
56 56
57 @Override 57 @Override
58 public void handlePOST(CoapExchange exchange) { 58 public void handlePOST(CoapExchange exchange) {
59 - upstreamHandler.handle(exchange, "POST", protocol); 59 + upstreamHandler.handle(exchange, protocol);
60 } 60 }
61 61
62 @Override 62 @Override
63 public void handlePUT(CoapExchange exchange) { 63 public void handlePUT(CoapExchange exchange) {
64 - upstreamHandler.handle(exchange, "PUT", protocol); 64 + upstreamHandler.handle(exchange, protocol);
65 } 65 }
66 66
67 } 67 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import org.eclipse.californium.core.coap.CoAP;
  7 +import org.eclipse.californium.core.coap.MediaTypeRegistry;
  8 +import org.eclipse.californium.core.coap.Option;
  9 +import org.eclipse.californium.core.server.resources.CoapExchange;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  12 +
  13 +/**
  14 + * IoT CoAP 协议工具类
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +public class IotCoapUtils {
  19 +
  20 + /**
  21 + * 自定义 CoAP Option 编号,用于携带 Token
  22 + * <p>
  23 + * CoAP Option 范围 2048-65535 属于实验/自定义范围
  24 + */
  25 + public static final int OPTION_TOKEN = 2088;
  26 +
  27 + /**
  28 + * 返回成功响应
  29 + *
  30 + * @param exchange CoAP 交换对象
  31 + * @param data 响应数据
  32 + */
  33 + public static void respondSuccess(CoapExchange exchange, Object data) {
  34 + CommonResult<Object> result = CommonResult.success(data);
  35 + String json = JsonUtils.toJsonString(result);
  36 + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
  37 + }
  38 +
  39 + /**
  40 + * 返回错误响应
  41 + *
  42 + * @param exchange CoAP 交换对象
  43 + * @param code CoAP 响应码
  44 + * @param message 错误消息
  45 + */
  46 + public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {
  47 + int errorCode = mapCoapCodeToErrorCode(code);
  48 + CommonResult<Object> result = CommonResult.error(errorCode, message);
  49 + String json = JsonUtils.toJsonString(result);
  50 + exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);
  51 + }
  52 +
  53 + /**
  54 + * 从自定义 CoAP Option 中获取 Token
  55 + *
  56 + * @param exchange CoAP 交换对象
  57 + * @param optionNumber Option 编号
  58 + * @return Token 值,如果不存在则返回 null
  59 + */
  60 + public static String getTokenFromOption(CoapExchange exchange, int optionNumber) {
  61 + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
  62 + o -> o.getNumber() == optionNumber);
  63 + return option != null ? new String(option.getValue()) : null;
  64 + }
  65 +
  66 + /**
  67 + * 将 CoAP 响应码映射到业务错误码
  68 + *
  69 + * @param code CoAP 响应码
  70 + * @return 业务错误码
  71 + */
  72 + public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {
  73 + if (code == CoAP.ResponseCode.BAD_REQUEST) {
  74 + return BAD_REQUEST.getCode();
  75 + } else if (code == CoAP.ResponseCode.UNAUTHORIZED) {
  76 + return UNAUTHORIZED.getCode();
  77 + } else if (code == CoAP.ResponseCode.FORBIDDEN) {
  78 + return FORBIDDEN.getCode();
  79 + } else {
  80 + return INTERNAL_SERVER_ERROR.getCode();
  81 + }
  82 + }
  83 +
  84 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -108,7 +108,7 @@ yudao: @@ -108,7 +108,7 @@ yudao:
108 # 针对引入的 MQTT WebSocket 组件的配置 108 # 针对引入的 MQTT WebSocket 组件的配置
109 # ==================================== 109 # ====================================
110 mqtt-ws: 110 mqtt-ws:
111 - enabled: true # 是否启用 MQTT WebSocket 111 + enabled: false # 是否启用 MQTT WebSocket
112 port: 8083 # WebSocket 服务端口 112 port: 8083 # WebSocket 服务端口
113 path: /mqtt # WebSocket 路径 113 path: /mqtt # WebSocket 路径
114 max-message-size: 8192 # 最大消息大小(字节) 114 max-message-size: 8192 # 最大消息大小(字节)
@@ -121,7 +121,7 @@ yudao: @@ -121,7 +121,7 @@ yudao:
121 # 针对引入的 CoAP 组件的配置 121 # 针对引入的 CoAP 组件的配置
122 # ==================================== 122 # ====================================
123 coap: 123 coap:
124 - enabled: false # 是否启用 CoAP 协议 124 + enabled: true # 是否启用 CoAP 协议
125 port: 5683 # CoAP 服务端口(默认 5683) 125 port: 5683 # CoAP 服务端口(默认 5683)
126 max-message-size: 1024 # 最大消息大小(字节) 126 max-message-size: 1024 # 最大消息大小(字节)
127 ack-timeout: 2000 # ACK 超时时间(毫秒) 127 ack-timeout: 2000 # ACK 超时时间(毫秒)
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocolIntegrationTest.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.coap; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
2 2
3 -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler; 3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
4 import lombok.extern.slf4j.Slf4j; 8 import lombok.extern.slf4j.Slf4j;
5 import org.eclipse.californium.core.CoapClient; 9 import org.eclipse.californium.core.CoapClient;
6 import org.eclipse.californium.core.CoapResponse; 10 import org.eclipse.californium.core.CoapResponse;
7 import org.eclipse.californium.core.coap.MediaTypeRegistry; 11 import org.eclipse.californium.core.coap.MediaTypeRegistry;
8 import org.eclipse.californium.core.coap.Option; 12 import org.eclipse.californium.core.coap.Option;
9 import org.eclipse.californium.core.coap.Request; 13 import org.eclipse.californium.core.coap.Request;
10 -import org.junit.jupiter.api.*; 14 +import org.eclipse.californium.core.config.CoapConfig;
  15 +import org.eclipse.californium.elements.config.Configuration;
  16 +import org.eclipse.californium.elements.config.UdpConfig;
  17 +import org.junit.jupiter.api.BeforeAll;
  18 +import org.junit.jupiter.api.Test;
11 19
12 /** 20 /**
13 * IoT 网关 CoAP 协议集成测试(手动测试) 21 * IoT 网关 CoAP 协议集成测试(手动测试)
14 * 22 *
15 - * 使用步骤:  
16 - * 1. 启动 CoAP 网关服务(端口 5683)  
17 - * 2. 运行 testAuth() 获取 token  
18 - * 3. 将 token 粘贴到 TOKEN 常量  
19 - * 4. 运行 testPropertyPost() 或 testEventPost() 23 + * <p>使用步骤:
  24 + * <ol>
  25 + * <li>启动 yudao-module-iot-gateway 服务(CoAP 端口 5683)</li>
  26 + * <li>运行 {@link #testAuth()} 获取 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
  27 + * <li>运行 {@link #testPropertyPost()} 测试属性上报,或运行 {@link #testEventPost()} 测试事件上报</li>
  28 + * </ol>
20 * 29 *
21 * @author 芋道源码 30 * @author 芋道源码
22 */ 31 */
23 @Slf4j 32 @Slf4j
24 -@TestMethodOrder(MethodOrderer.OrderAnnotation.class)  
25 -class IotCoapProtocolIntegrationTest { 33 +public class IotCoapProtocolIntegrationTest {
26 34
27 private static final String SERVER_HOST = "127.0.0.1"; 35 private static final String SERVER_HOST = "127.0.0.1";
28 private static final int SERVER_PORT = 5683; 36 private static final int SERVER_PORT = 5683;
29 37
30 - // 设备信息(根据实际情况修改)  
31 - private static final String PRODUCT_KEY = "testProductKey";  
32 - private static final String DEVICE_NAME = "testDeviceName"; 38 + // 设备信息(根据实际情况修改 PRODUCT_KEY、DEVICE_NAME、PASSWORD)
  39 + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
  40 + private static final String DEVICE_NAME = "small";
  41 + private static final String PASSWORD = "509e2b08f7598eb139d276388c600435913ba4c94cd0d50aebc5c0d1855bcb75";
  42 +
33 private static final String CLIENT_ID = PRODUCT_KEY + "." + DEVICE_NAME; 43 private static final String CLIENT_ID = PRODUCT_KEY + "." + DEVICE_NAME;
34 private static final String USERNAME = DEVICE_NAME + "&" + PRODUCT_KEY; 44 private static final String USERNAME = DEVICE_NAME + "&" + PRODUCT_KEY;
35 - private static final String PASSWORD = "testPassword123";  
36 -  
37 - // TODO: 运行 testAuth() 后,将返回的 token 粘贴到这里  
38 - private static final String TOKEN = "粘贴你的token到这里";  
39 45
40 - // ========== 1. 认证测试 ========== 46 + /**
  47 + * 设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
  48 + */
  49 + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k";
  50 +
  51 + @BeforeAll
  52 + public static void initCaliforniumConfig() {
  53 + // 注册 Californium 配置定义
  54 + CoapConfig.register();
  55 + UdpConfig.register();
  56 + // 创建默认配置
  57 + Configuration.setStandard(Configuration.createStandardWithoutFile());
  58 + }
41 59
  60 + /**
  61 + * 认证测试:获取设备 Token
  62 + */
42 @Test 63 @Test
43 - @Order(1)  
44 - @DisplayName("1. 认证 - 获取 Token")  
45 - void testAuth() throws Exception { 64 + @SuppressWarnings("deprecation")
  65 + public void testAuth() throws Exception {
46 String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT); 66 String uri = String.format("coap://%s:%d/auth", SERVER_HOST, SERVER_PORT);
47 -  
48 - String payload = String.format("""  
49 - {  
50 - "clientId": "%s",  
51 - "username": "%s",  
52 - "password": "%s"  
53 - }  
54 - """, CLIENT_ID, USERNAME, PASSWORD); 67 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  68 + .put("clientId", CLIENT_ID)
  69 + .put("username", USERNAME)
  70 + .put("password", PASSWORD)
  71 + .build());
55 72
56 CoapClient client = new CoapClient(uri); 73 CoapClient client = new CoapClient(uri);
57 try { 74 try {
@@ -68,38 +85,33 @@ class IotCoapProtocolIntegrationTest { @@ -68,38 +85,33 @@ class IotCoapProtocolIntegrationTest {
68 } 85 }
69 } 86 }
70 87
71 - // ========== 2. 属性上报测试 ==========  
72 - 88 + /**
  89 + * 属性上报测试
  90 + */
73 @Test 91 @Test
74 - @Order(2)  
75 - @DisplayName("2. 属性上报")  
76 - void testPropertyPost() throws Exception { 92 + @SuppressWarnings("deprecation")
  93 + public void testPropertyPost() throws Exception {
77 String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post", 94 String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/property/post",
78 SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); 95 SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
79 -  
80 - String payload = """  
81 - {  
82 - "id": "123",  
83 - "method": "thing.property.post",  
84 - "params": {  
85 - "temperature": 25.5,  
86 - "humidity": 60  
87 - }  
88 - }  
89 - """; 96 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  97 + .put("id", IdUtil.fastSimpleUUID())
  98 + .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
  99 + .put("version", "1.0")
  100 + .put("params", MapUtil.builder()
  101 + .put("width", 1)
  102 + .put("height", "2")
  103 + .build())
  104 + .build());
90 105
91 CoapClient client = new CoapClient(uri); 106 CoapClient client = new CoapClient(uri);
92 try { 107 try {
93 - // 构造带自定义 Option 的请求  
94 Request request = Request.newPost(); 108 Request request = Request.newPost();
95 request.setURI(uri); 109 request.setURI(uri);
96 request.setPayload(payload); 110 request.setPayload(payload);
97 request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); 111 request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
98 - // 添加自定义 Token Option (2088)  
99 - request.getOptions().addOption(new Option(IotCoapUpstreamHandler.OPTION_TOKEN, TOKEN)); 112 + request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
100 113
101 log.info("[testPropertyPost][请求 URI: {}]", uri); 114 log.info("[testPropertyPost][请求 URI: {}]", uri);
102 - log.info("[testPropertyPost][Token: {}]", TOKEN);  
103 log.info("[testPropertyPost][请求体: {}]", payload); 115 log.info("[testPropertyPost][请求体: {}]", payload);
104 116
105 CoapResponse response = client.advanced(request); 117 CoapResponse response = client.advanced(request);
@@ -111,39 +123,35 @@ class IotCoapProtocolIntegrationTest { @@ -111,39 +123,35 @@ class IotCoapProtocolIntegrationTest {
111 } 123 }
112 } 124 }
113 125
114 - // ========== 3. 事件上报测试 ==========  
115 - 126 + /**
  127 + * 事件上报测试
  128 + */
116 @Test 129 @Test
117 - @Order(3)  
118 - @DisplayName("3. 事件上报")  
119 - void testEventPost() throws Exception {  
120 - String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/alarm/post", 130 + @SuppressWarnings("deprecation")
  131 + public void testEventPost() throws Exception {
  132 + String uri = String.format("coap://%s:%d/topic/sys/%s/%s/thing/event/post",
121 SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); 133 SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME);
122 -  
123 - String payload = """  
124 - {  
125 - "id": "456",  
126 - "method": "thing.event.alarm.post",  
127 - "params": {  
128 - "alarmType": "temperature_high",  
129 - "level": "warning",  
130 - "value": 85.2  
131 - }  
132 - }  
133 - """; 134 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  135 + .put("id", IdUtil.fastSimpleUUID())
  136 + .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod())
  137 + .put("version", "1.0")
  138 + .put("identifier", "eat")
  139 + .put("params", MapUtil.builder()
  140 + .put("width", 1)
  141 + .put("height", "2")
  142 + .put("oneThree", "3")
  143 + .build())
  144 + .build());
134 145
135 CoapClient client = new CoapClient(uri); 146 CoapClient client = new CoapClient(uri);
136 try { 147 try {
137 - // 构造带自定义 Option 的请求  
138 Request request = Request.newPost(); 148 Request request = Request.newPost();
139 request.setURI(uri); 149 request.setURI(uri);
140 request.setPayload(payload); 150 request.setPayload(payload);
141 request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); 151 request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
142 - // 添加自定义 Token Option (2088)  
143 - request.getOptions().addOption(new Option(IotCoapUpstreamHandler.OPTION_TOKEN, TOKEN)); 152 + request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
144 153
145 log.info("[testEventPost][请求 URI: {}]", uri); 154 log.info("[testEventPost][请求 URI: {}]", uri);
146 - log.info("[testEventPost][Token: {}]", TOKEN);  
147 log.info("[testEventPost][请求体: {}]", payload); 155 log.info("[testEventPost][请求体: {}]", payload);
148 156
149 CoapResponse response = client.advanced(request); 157 CoapResponse response = client.advanced(request);