【同步】BOOT 和 CLOUD 的功能(IoT)
This commit is contained in:
@@ -9,7 +9,7 @@ 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 cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.californium.core.CoapClient;
|
||||
import org.eclipse.californium.core.CoapResponse;
|
||||
@@ -23,6 +23,8 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN;
|
||||
|
||||
/**
|
||||
* IoT 直连设备 CoAP 协议集成测试(手动测试)
|
||||
*
|
||||
@@ -134,7 +136,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
@@ -177,7 +179,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
@@ -202,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
|
||||
// 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 deviceName = "test-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
String payload = JsonUtils.toJsonString(reqDTO);
|
||||
// 1.3 输出请求
|
||||
log.info("[testDeviceRegister][请求 URI: {}]", uri);
|
||||
|
||||
@@ -13,7 +13,6 @@ 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;
|
||||
@@ -30,6 +29,8 @@ import org.junit.jupiter.api.Test;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN;
|
||||
|
||||
/**
|
||||
* IoT 网关设备 CoAP 协议集成测试(手动测试)
|
||||
*
|
||||
@@ -158,7 +159,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
@@ -201,7 +202,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
@@ -242,7 +243,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
@@ -289,7 +290,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
@@ -362,7 +363,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
|
||||
@@ -8,7 +8,6 @@ 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;
|
||||
@@ -22,6 +21,8 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN;
|
||||
|
||||
/**
|
||||
* IoT 网关子设备 CoAP 协议集成测试(手动测试)
|
||||
*
|
||||
@@ -137,7 +138,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
@@ -185,7 +186,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest {
|
||||
request.setURI(uri);
|
||||
request.setPayload(payload);
|
||||
request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON);
|
||||
request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN));
|
||||
request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN));
|
||||
|
||||
CoapResponse response = client.advanced(request);
|
||||
// 2.2 输出结果
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* IoT 网关 EMQX 协议集成测试包
|
||||
*
|
||||
* <p>
|
||||
* 测试类直接使用 mqtt 包下的单测即可,因为设备都是通过 MQTT 协议连接 EMQX Broker。
|
||||
*
|
||||
* @see cn.iocoder.yudao.module.iot.gateway.protocol.mqtt
|
||||
*
|
||||
* <h2>架构</h2>
|
||||
* <pre>
|
||||
* +--------+ MQTT +-------------+ HTTP Hook +---------+
|
||||
* | 设备 | --------------> | EMQX Broker | ----------------> | 网关 |
|
||||
* +--------+ +-------------+ +---------+
|
||||
* </pre>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
|
||||
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -11,6 +10,7 @@ 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.core.util.IotProductAuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -92,9 +92,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
|
||||
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")
|
||||
@@ -126,9 +124,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
|
||||
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(),
|
||||
@@ -163,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
|
||||
// 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 deviceName = "test-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
String payload = JsonUtils.toJsonString(reqDTO);
|
||||
// 1.3 输出请求
|
||||
log.info("[testDeviceRegister][请求 URL: {}]", url);
|
||||
|
||||
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
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;
|
||||
@@ -20,7 +19,6 @@ import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -121,9 +119,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
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 输出请求
|
||||
@@ -155,9 +151,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
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 输出请求
|
||||
@@ -187,9 +181,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
// 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 输出请求
|
||||
@@ -208,8 +200,6 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
|
||||
// TODO @芋艿:待测试
|
||||
|
||||
/**
|
||||
* 子设备动态注册测试
|
||||
* <p>
|
||||
@@ -227,9 +217,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
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 输出请求
|
||||
@@ -263,9 +251,9 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
.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());
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("message", "gateway started").build())
|
||||
.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.build();
|
||||
@@ -274,26 +262,24 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
|
||||
.put("power", 100)
|
||||
.build();
|
||||
// 1.5 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
|
||||
subDeviceEvent.setTime(System.currentTimeMillis());
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("errorCode", 0).build())
|
||||
.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);
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData()
|
||||
.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))
|
||||
.setProperties(subDeviceProperties)
|
||||
.setEvents(subDeviceEvents);
|
||||
// 1.7 构建请求参数
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(ListUtil.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 输出请求
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -94,9 +93,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest {
|
||||
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")
|
||||
@@ -130,9 +127,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest {
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus;
|
||||
|
||||
import com.ghgande.j2mod.modbus.io.ModbusRTUTCPTransport;
|
||||
import com.ghgande.j2mod.modbus.msg.*;
|
||||
import com.ghgande.j2mod.modbus.procimg.*;
|
||||
import com.ghgande.j2mod.modbus.slave.ModbusSlave;
|
||||
import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;
|
||||
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* Modbus RTU over TCP 完整 Demo
|
||||
*
|
||||
* 架构:Master(主站)启动 TCP Server 监听 → Slave(从站)主动 TCP 连接上来
|
||||
* 通信协议:RTU 帧格式(带 CRC)通过 TCP 传输,而非标准 MBAP 头
|
||||
*
|
||||
* 流程:
|
||||
* 1. Master 启动 TCP ServerSocket 监听端口
|
||||
* 2. Slave(从站模拟器)作为 TCP Client 连接到 Master
|
||||
* 3. Master 通过 accept 得到的 Socket,使用 {@link ModbusRTUTCPTransport} 发送读写请求
|
||||
*
|
||||
* 实现说明:
|
||||
* 因为 j2mod 的 ModbusSlave 只能以 TCP Server 模式运行(监听端口等待 Master 连接),
|
||||
* 不支持"Slave 作为 TCP Client 主动连接 Master"的模式。
|
||||
* 所以这里用一个 TCP 桥接(bridge)来模拟:
|
||||
* - Slave 在本地内部端口启动(RTU over TCP 模式)
|
||||
* - 一个桥接线程同时连接 Master Server 和 Slave 内部端口,做双向数据转发
|
||||
* - Master 视角:看到的是 Slave 主动连上来
|
||||
*
|
||||
* 依赖:j2mod 3.2.1(pom.xml 中已声明)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Deprecated // 仅技术演示,非是必须的
|
||||
public class ModbusRtuOverTcpDemo {
|
||||
|
||||
/**
|
||||
* Master(主站)TCP Server 监听端口
|
||||
*/
|
||||
private static final int PORT = 5021;
|
||||
/**
|
||||
* Slave 内部端口(仅本地中转用,不对外暴露)
|
||||
*/
|
||||
private static final int SLAVE_INTERNAL_PORT = PORT + 100;
|
||||
/**
|
||||
* Modbus 从站地址
|
||||
*/
|
||||
private static final int SLAVE_ID = 1;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// ===================== 第一步:Master 启动 TCP Server 监听 =====================
|
||||
ServerSocket serverSocket = new ServerSocket(PORT);
|
||||
System.out.println("===================================================");
|
||||
System.out.println("[Master] TCP Server 已启动,监听端口: " + PORT);
|
||||
System.out.println("[Master] 等待 Slave 连接...");
|
||||
System.out.println("===================================================");
|
||||
|
||||
// ===================== 第二步:后台启动 Slave,它会主动连接 Master =====================
|
||||
ModbusSlave slave = startSlaveInBackground();
|
||||
|
||||
// Master accept Slave 的连接
|
||||
Socket slaveSocket = serverSocket.accept();
|
||||
System.out.println("[Master] Slave 已连接: " + slaveSocket.getRemoteSocketAddress());
|
||||
|
||||
// ===================== 第三步:Master 通过 RTU over TCP 发送读写请求 =====================
|
||||
// 使用 ModbusRTUTCPTransport 包装 Socket(RTU 帧 = SlaveID + 功能码 + 数据 + CRC,无 MBAP 头)
|
||||
ModbusRTUTCPTransport transport = new ModbusRTUTCPTransport(slaveSocket);
|
||||
|
||||
try {
|
||||
System.out.println("[Master] RTU over TCP 通道已建立\n");
|
||||
|
||||
// 1. 读操作演示:4 种功能码
|
||||
demoReadCoils(transport); // 功能码 01:读线圈
|
||||
demoReadDiscreteInputs(transport); // 功能码 02:读离散输入
|
||||
demoReadHoldingRegisters(transport); // 功能码 03:读保持寄存器
|
||||
demoReadInputRegisters(transport); // 功能码 04:读输入寄存器
|
||||
|
||||
// 2. 写操作演示 + 读回验证
|
||||
demoWriteCoil(transport); // 功能码 05:写单个线圈
|
||||
demoWriteRegister(transport); // 功能码 06:写单个保持寄存器
|
||||
|
||||
System.out.println("\n===================================================");
|
||||
System.out.println("所有 RTU over TCP 读写操作执行成功!");
|
||||
System.out.println("===================================================");
|
||||
} finally {
|
||||
// 清理资源
|
||||
transport.close();
|
||||
slaveSocket.close();
|
||||
serverSocket.close();
|
||||
slave.close();
|
||||
System.out.println("[Master] 资源已关闭");
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Slave 设备模拟(作为 TCP Client 连接 Master) =====================
|
||||
|
||||
/**
|
||||
* 在后台启动从站模拟器,并通过 TCP 桥接连到 Master Server
|
||||
*
|
||||
* @return ModbusSlave 实例(用于最后关闭资源)
|
||||
*/
|
||||
private static ModbusSlave startSlaveInBackground() throws Exception {
|
||||
// 1. 创建进程映像,初始化寄存器数据
|
||||
SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID);
|
||||
// 1.1 线圈(Coil,功能码 01/05)- 可读写,地址 0~9
|
||||
for (int i = 0; i < 10; i++) {
|
||||
spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0));
|
||||
}
|
||||
// 1.2 离散输入(Discrete Input,功能码 02)- 只读,地址 0~9
|
||||
for (int i = 0; i < 10; i++) {
|
||||
spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0));
|
||||
}
|
||||
// 1.3 保持寄存器(Holding Register,功能码 03/06/16)- 可读写,地址 0~19
|
||||
for (int i = 0; i < 20; i++) {
|
||||
spi.addRegister(new SimpleRegister(i * 100));
|
||||
}
|
||||
// 1.4 输入寄存器(Input Register,功能码 04)- 只读,地址 0~19
|
||||
for (int i = 0; i < 20; i++) {
|
||||
spi.addInputRegister(new SimpleInputRegister(i * 10 + 1));
|
||||
}
|
||||
|
||||
// 2. 启动 Slave(RTU over TCP 模式,在本地内部端口监听)
|
||||
ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(SLAVE_INTERNAL_PORT, 5, true);
|
||||
slave.addProcessImage(SLAVE_ID, spi);
|
||||
slave.open();
|
||||
System.out.println("[Slave] 从站模拟器已启动(内部端口: " + SLAVE_INTERNAL_PORT + ")");
|
||||
|
||||
// 3. 启动桥接线程:TCP Client 连接 Master Server,同时连接 Slave 内部端口,双向转发
|
||||
Thread bridgeThread = new Thread(() -> {
|
||||
try {
|
||||
Socket toMaster = new Socket("127.0.0.1", PORT);
|
||||
Socket toSlave = new Socket("127.0.0.1", SLAVE_INTERNAL_PORT);
|
||||
System.out.println("[Bridge] 已建立桥接: Master(" + PORT + ") <-> Slave(" + SLAVE_INTERNAL_PORT + ")");
|
||||
|
||||
// 双向桥接:Master ↔ Bridge ↔ Slave
|
||||
Thread forward = new Thread(() -> bridge(toMaster, toSlave), "bridge-master→slave");
|
||||
Thread backward = new Thread(() -> bridge(toSlave, toMaster), "bridge-slave→master");
|
||||
forward.setDaemon(true);
|
||||
backward.setDaemon(true);
|
||||
forward.start();
|
||||
backward.start();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, "bridge-setup");
|
||||
bridgeThread.setDaemon(true);
|
||||
bridgeThread.start();
|
||||
|
||||
return slave;
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP 双向桥接:从 src 读取数据,写入 dst
|
||||
*/
|
||||
private static void bridge(Socket src, Socket dst) {
|
||||
try {
|
||||
byte[] buf = new byte[1024];
|
||||
var in = src.getInputStream();
|
||||
var out = dst.getOutputStream();
|
||||
int len;
|
||||
while ((len = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, len);
|
||||
out.flush();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 连接关闭时正常退出
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Master 读写操作 =====================
|
||||
|
||||
/**
|
||||
* 发送请求并接收响应(通用方法)
|
||||
*/
|
||||
private static ModbusResponse sendRequest(ModbusRTUTCPTransport transport, ModbusRequest request) throws Exception {
|
||||
request.setUnitID(SLAVE_ID);
|
||||
transport.writeRequest(request);
|
||||
return transport.readResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能码 01:读线圈(Read Coils)
|
||||
*/
|
||||
private static void demoReadCoils(ModbusRTUTCPTransport transport) throws Exception {
|
||||
ReadCoilsRequest request = new ReadCoilsRequest(0, 5);
|
||||
ReadCoilsResponse response = (ReadCoilsResponse) sendRequest(transport, request);
|
||||
|
||||
StringBuilder sb = new StringBuilder("[功能码 01] 读线圈(0~4): ");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
sb.append(response.getCoilStatus(i) ? "ON" : "OFF");
|
||||
if (i < 4) {
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
System.out.println(sb);
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能码 02:读离散输入(Read Discrete Inputs)
|
||||
*/
|
||||
private static void demoReadDiscreteInputs(ModbusRTUTCPTransport transport) throws Exception {
|
||||
ReadInputDiscretesRequest request = new ReadInputDiscretesRequest(0, 5);
|
||||
ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) sendRequest(transport, request);
|
||||
|
||||
StringBuilder sb = new StringBuilder("[功能码 02] 读离散输入(0~4): ");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
sb.append(response.getDiscreteStatus(i) ? "ON" : "OFF");
|
||||
if (i < 4) {
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
System.out.println(sb);
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能码 03:读保持寄存器(Read Holding Registers)
|
||||
*/
|
||||
private static void demoReadHoldingRegisters(ModbusRTUTCPTransport transport) throws Exception {
|
||||
ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(0, 5);
|
||||
ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) sendRequest(transport, request);
|
||||
|
||||
StringBuilder sb = new StringBuilder("[功能码 03] 读保持寄存器(0~4): ");
|
||||
for (int i = 0; i < response.getWordCount(); i++) {
|
||||
sb.append(response.getRegisterValue(i));
|
||||
if (i < response.getWordCount() - 1) {
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
System.out.println(sb);
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能码 04:读输入寄存器(Read Input Registers)
|
||||
*/
|
||||
private static void demoReadInputRegisters(ModbusRTUTCPTransport transport) throws Exception {
|
||||
ReadInputRegistersRequest request = new ReadInputRegistersRequest(0, 5);
|
||||
ReadInputRegistersResponse response = (ReadInputRegistersResponse) sendRequest(transport, request);
|
||||
|
||||
StringBuilder sb = new StringBuilder("[功能码 04] 读输入寄存器(0~4): ");
|
||||
for (int i = 0; i < response.getWordCount(); i++) {
|
||||
sb.append(response.getRegisterValue(i));
|
||||
if (i < response.getWordCount() - 1) {
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
System.out.println(sb);
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能码 05:写单个线圈(Write Single Coil)+ 读回验证
|
||||
*/
|
||||
private static void demoWriteCoil(ModbusRTUTCPTransport transport) throws Exception {
|
||||
int address = 0;
|
||||
|
||||
// 1. 先读取当前值
|
||||
ReadCoilsRequest readReq = new ReadCoilsRequest(address, 1);
|
||||
ReadCoilsResponse readResp = (ReadCoilsResponse) sendRequest(transport, readReq);
|
||||
boolean beforeValue = readResp.getCoilStatus(0);
|
||||
|
||||
// 2. 写入相反的值
|
||||
boolean writeValue = !beforeValue;
|
||||
WriteCoilRequest writeReq = new WriteCoilRequest(address, writeValue);
|
||||
sendRequest(transport, writeReq);
|
||||
|
||||
// 3. 读回验证
|
||||
ReadCoilsResponse verifyResp = (ReadCoilsResponse) sendRequest(transport, readReq);
|
||||
boolean afterValue = verifyResp.getCoilStatus(0);
|
||||
|
||||
System.out.println("[功能码 05] 写线圈: 地址=" + address
|
||||
+ ", 写入前=" + (beforeValue ? "ON" : "OFF")
|
||||
+ ", 写入值=" + (writeValue ? "ON" : "OFF")
|
||||
+ ", 读回值=" + (afterValue ? "ON" : "OFF")
|
||||
+ (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能码 06:写单个保持寄存器(Write Single Register)+ 读回验证
|
||||
*/
|
||||
private static void demoWriteRegister(ModbusRTUTCPTransport transport) throws Exception {
|
||||
int address = 0;
|
||||
int writeValue = 12345;
|
||||
|
||||
// 1. 先读取当前值
|
||||
ReadMultipleRegistersRequest readReq = new ReadMultipleRegistersRequest(address, 1);
|
||||
ReadMultipleRegistersResponse readResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
|
||||
int beforeValue = readResp.getRegisterValue(0);
|
||||
|
||||
// 2. 写入新值
|
||||
WriteSingleRegisterRequest writeReq = new WriteSingleRegisterRequest(address, new SimpleRegister(writeValue));
|
||||
sendRequest(transport, writeReq);
|
||||
|
||||
// 3. 读回验证
|
||||
ReadMultipleRegistersResponse verifyResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
|
||||
int afterValue = verifyResp.getRegisterValue(0);
|
||||
|
||||
System.out.println("[功能码 06] 写保持寄存器: 地址=" + address
|
||||
+ ", 写入前=" + beforeValue
|
||||
+ ", 写入值=" + writeValue
|
||||
+ ", 读回值=" + afterValue
|
||||
+ (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
|
||||
|
||||
import com.ghgande.j2mod.modbus.procimg.*;
|
||||
import com.ghgande.j2mod.modbus.slave.ModbusSlave;
|
||||
import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Modbus TCP 从站模拟器(手动测试)
|
||||
*
|
||||
* <p>测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Client 网关连接和读写数据
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>运行 {@link #testStartSlaveSimulator()} 启动模拟从站(默认端口 5020,从站地址 1)</li>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-client 协议)</li>
|
||||
* <li>确保数据库有对应的 Modbus Client 设备配置(ip=127.0.0.1, port=5020, slaveId=1)</li>
|
||||
* <li>网关会自动连接模拟从站并开始轮询读取寄存器数据</li>
|
||||
* <li>模拟器每 5 秒自动更新输入寄存器和保持寄存器的值,模拟传感器数据变化</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>可用寄存器:
|
||||
* <ul>
|
||||
* <li>线圈 (Coil, 功能码 01/05): 地址 0-9,交替 true/false</li>
|
||||
* <li>离散输入 (Discrete Input, 功能码 02): 地址 0-9,每 3 个一个 true</li>
|
||||
* <li>保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19,初始值 0,100,200,...</li>
|
||||
* <li>输入寄存器 (Input Register, 功能码 04): 地址 0-19,初始值 1,11,21,...</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IoTModbusTcpClientIntegrationTest {
|
||||
|
||||
private static final int PORT = 5020;
|
||||
private static final int SLAVE_ID = 1;
|
||||
|
||||
/**
|
||||
* 启动 Modbus TCP 从站模拟器
|
||||
*
|
||||
* <p>模拟器会持续运行,每 5 秒更新一次寄存器数据,直到手动停止
|
||||
*/
|
||||
@SuppressWarnings({"InfiniteLoopStatement", "BusyWait"})
|
||||
@Test
|
||||
public void testStartSlaveSimulator() throws Exception {
|
||||
// 1. 创建进程映像(Process Image),存储寄存器数据
|
||||
SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID);
|
||||
|
||||
// 2.1 初始化线圈(Coil,功能码 01/05)- 离散输出,可读写
|
||||
// 地址 0-9,共 10 个线圈
|
||||
for (int i = 0; i < 10; i++) {
|
||||
spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); // 交替 true/false
|
||||
}
|
||||
|
||||
// 2.2 初始化离散输入(Discrete Input,功能码 02)- 只读
|
||||
// 地址 0-9,共 10 个离散输入
|
||||
for (int i = 0; i < 10; i++) {
|
||||
spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); // 每 3 个一个 true
|
||||
}
|
||||
|
||||
// 2.3 初始化保持寄存器(Holding Register,功能码 03/06/16)- 可读写
|
||||
// 地址 0-19,共 20 个寄存器
|
||||
for (int i = 0; i < 20; i++) {
|
||||
spi.addRegister(new SimpleRegister(i * 100)); // 值为 0, 100, 200, ...
|
||||
}
|
||||
|
||||
// 2.4 初始化输入寄存器(Input Register,功能码 04)- 只读
|
||||
// 地址 0-19,共 20 个寄存器
|
||||
SimpleInputRegister[] inputRegisters = new SimpleInputRegister[20];
|
||||
for (int i = 0; i < 20; i++) {
|
||||
inputRegisters[i] = new SimpleInputRegister(i * 10 + 1); // 值为 1, 11, 21, ...
|
||||
spi.addInputRegister(inputRegisters[i]);
|
||||
}
|
||||
|
||||
// 3.1 创建 Modbus TCP 从站
|
||||
ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(PORT, 5);
|
||||
slave.addProcessImage(SLAVE_ID, spi);
|
||||
// 3.2 启动从站
|
||||
slave.open();
|
||||
|
||||
log.info("[testStartSlaveSimulator][Modbus TCP 从站模拟器已启动, 端口: {}, 从站地址: {}]", PORT, SLAVE_ID);
|
||||
log.info("[testStartSlaveSimulator][可用寄存器: 线圈(01/05) 0-9, 离散输入(02) 0-9, " +
|
||||
"保持寄存器(03/06/16) 0-19, 输入寄存器(04) 0-19]");
|
||||
|
||||
// 4. 添加关闭钩子
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
log.info("[testStartSlaveSimulator][正在关闭模拟器...]");
|
||||
slave.close();
|
||||
log.info("[testStartSlaveSimulator][模拟器已关闭]");
|
||||
}));
|
||||
|
||||
// 5. 保持运行,定时更新输入寄存器模拟数据变化
|
||||
int counter = 0;
|
||||
while (true) {
|
||||
Thread.sleep(5000); // 每 5 秒更新一次
|
||||
counter++;
|
||||
|
||||
// 更新输入寄存器的值,模拟传感器数据变化
|
||||
for (int i = 0; i < 20; i++) {
|
||||
int newValue = (i * 10 + 1) + counter;
|
||||
inputRegisters[i].setValue(newValue);
|
||||
}
|
||||
|
||||
// 更新保持寄存器的第一个值
|
||||
spi.getRegister(0).setValue(counter * 100);
|
||||
log.info("[testStartSlaveSimulator][数据已更新, counter={}, 保持寄存器[0]={}, 输入寄存器[0]={}]",
|
||||
counter, counter * 100, 1 + counter);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
|
||||
|
||||
import cn.hutool.core.util.HexUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetClient;
|
||||
import io.vertx.core.net.NetClientOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
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.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP Server 协议集成测试 — MODBUS_RTU 帧格式(手动测试)
|
||||
*
|
||||
* <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)</li>
|
||||
* <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 自定义功能码认证</li>
|
||||
* <li>{@link #testPollingResponse()} - 轮询响应</li>
|
||||
* <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotModbusTcpServerRtuIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 503;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
private static final int CUSTOM_FC = 65;
|
||||
private static final int SLAVE_ID = 1;
|
||||
|
||||
private static Vertx vertx;
|
||||
private static NetClient netClient;
|
||||
|
||||
// ===================== 编解码器 =====================
|
||||
|
||||
private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC);
|
||||
private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC);
|
||||
|
||||
// ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo";
|
||||
private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_rtu";
|
||||
private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e";
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
NetClientOptions options = new NetClientOptions()
|
||||
.setConnectTimeout(TIMEOUT_MS)
|
||||
.setIdleTimeout(TIMEOUT_MS);
|
||||
netClient = vertx.createNetClient(options);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
if (netClient != null) {
|
||||
netClient.close();
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 认证测试:发送自定义功能码 FC65 认证帧(RTU 格式),验证认证成功响应
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 构造并发送认证帧
|
||||
IotModbusFrame response = authenticate(socket);
|
||||
|
||||
// 2. 验证响应
|
||||
log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
|
||||
response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
|
||||
JSONObject json = JSONUtil.parseObj(response.getCustomData());
|
||||
assertEquals(0, json.getInt("code"));
|
||||
log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 轮询响应测试 =====================
|
||||
|
||||
/**
|
||||
* 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回
|
||||
*/
|
||||
@Test
|
||||
public void testPollingResponse() throws Exception {
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先认证
|
||||
IotModbusFrame authResponse = authenticate(socket);
|
||||
log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
|
||||
JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData());
|
||||
assertEquals(0, authJson.getInt("code"));
|
||||
|
||||
// 2. 设置持续监听:每收到一个读请求,自动回复
|
||||
log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
|
||||
CompletableFuture<Void> done = new CompletableFuture<>();
|
||||
// 注意:使用 requestMode=true,因为设备端收到的是网关下发的读请求(非响应)
|
||||
RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> {
|
||||
log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]",
|
||||
frame.getSlaveId(), frame.getFunctionCode());
|
||||
// 解析读请求中的起始地址和数量
|
||||
byte[] pdu = frame.getPdu();
|
||||
int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF);
|
||||
int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF);
|
||||
log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity);
|
||||
|
||||
// 构造读响应帧(模拟寄存器数据,RTU 格式)
|
||||
int[] registerValues = new int[quantity];
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
|
||||
}
|
||||
byte[] responseData = buildReadResponse(frame.getSlaveId(),
|
||||
frame.getFunctionCode(), registerValues);
|
||||
socket.write(Buffer.buffer(responseData));
|
||||
log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
|
||||
}, true);
|
||||
socket.handler(parser);
|
||||
|
||||
// 3. 持续等待(200 秒),期间会自动回复所有收到的读请求
|
||||
Thread.sleep(200000);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 属性设置测试 =====================
|
||||
|
||||
/**
|
||||
* 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求(RTU 格式)
|
||||
* <p>
|
||||
* 注意:需手动在平台触发 property.set
|
||||
*/
|
||||
@Test
|
||||
public void testPropertySetWrite() throws Exception {
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先认证
|
||||
IotModbusFrame authResponse = authenticate(socket);
|
||||
log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData());
|
||||
|
||||
// 2. 等待网关下发写请求(需手动在平台触发 property.set)
|
||||
log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]");
|
||||
IotModbusFrame writeRequest = waitForRequest(socket);
|
||||
log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]",
|
||||
writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
|
||||
HexUtil.encodeHexStr(writeRequest.getPdu()));
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 建立 TCP 连接
|
||||
*/
|
||||
private CompletableFuture<NetSocket> connect() {
|
||||
CompletableFuture<NetSocket> future = new CompletableFuture<>();
|
||||
netClient.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onSuccess(future::complete)
|
||||
.onFailure(future::completeExceptionally);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行认证并返回响应帧
|
||||
*/
|
||||
private IotModbusFrame authenticate(NetSocket socket) throws Exception {
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
authInfo.setClientId("");
|
||||
byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
return sendAndReceive(socket, authFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码)
|
||||
*/
|
||||
private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
|
||||
CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
|
||||
// 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame)
|
||||
RecordParser parser = FRAME_DECODER.createRecordParser(
|
||||
(frame, frameFormat) -> {
|
||||
try {
|
||||
log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat);
|
||||
responseFuture.complete(frame);
|
||||
} catch (Exception e) {
|
||||
responseFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 发送请求
|
||||
log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length);
|
||||
socket.write(Buffer.buffer(frameData));
|
||||
|
||||
// 等待响应
|
||||
return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待接收网关下发的请求帧(不发送,只等待接收)
|
||||
*/
|
||||
private IotModbusFrame waitForRequest(NetSocket socket) throws Exception {
|
||||
CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>();
|
||||
// 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame)
|
||||
RecordParser parser = FRAME_DECODER.createRecordParser(
|
||||
(frame, frameFormat) -> {
|
||||
try {
|
||||
log.info("[waitForRequest][检测到帧格式: {}]", frameFormat);
|
||||
requestFuture.complete(frame);
|
||||
} catch (Exception e) {
|
||||
requestFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 等待(超时 30 秒,因为轮询间隔可能比较长)
|
||||
return requestFuture.get(30000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造认证帧(MODBUS_RTU 格式)
|
||||
* <p>
|
||||
* JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
|
||||
* <p>
|
||||
* RTU 帧格式:[SlaveId(1)] [FC=0x41(1)] [ByteCount(1)] [JSON(N)] [CRC16(2)]
|
||||
*/
|
||||
private byte[] buildAuthFrame(String clientId, String username, String password) {
|
||||
JSONObject params = new JSONObject();
|
||||
params.set("clientId", clientId);
|
||||
params.set("username", username);
|
||||
params.set("password", password);
|
||||
JSONObject json = new JSONObject();
|
||||
json.set("method", "auth");
|
||||
json.set("params", params);
|
||||
return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(),
|
||||
IotModbusFrameFormatEnum.MODBUS_RTU, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 FC03/FC01-04 读响应帧(MODBUS_RTU 格式)
|
||||
* <p>
|
||||
* RTU 帧格式:[SlaveId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] [CRC16(2)]
|
||||
*/
|
||||
private byte[] buildReadResponse(int slaveId, int functionCode, int[] registerValues) {
|
||||
int byteCount = registerValues.length * 2;
|
||||
// 帧长度:SlaveId(1) + FC(1) + ByteCount(1) + Data(N*2) + CRC(2)
|
||||
int totalLength = 1 + 1 + 1 + byteCount + 2;
|
||||
byte[] frame = new byte[totalLength];
|
||||
frame[0] = (byte) slaveId;
|
||||
frame[1] = (byte) functionCode;
|
||||
frame[2] = (byte) byteCount;
|
||||
for (int i = 0; i < registerValues.length; i++) {
|
||||
frame[3 + i * 2] = (byte) ((registerValues[i] >> 8) & 0xFF);
|
||||
frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF);
|
||||
}
|
||||
// 计算 CRC16
|
||||
int crc = IotModbusCommonUtils.calculateCrc16(frame, totalLength - 2);
|
||||
frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low
|
||||
frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
|
||||
return frame;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
|
||||
|
||||
import cn.hutool.core.util.HexUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
|
||||
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetClient;
|
||||
import io.vertx.core.net.NetClientOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
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.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* IoT Modbus TCP Server 协议集成测试 — MODBUS_TCP 帧格式(手动测试)
|
||||
*
|
||||
* <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_TCP(MBAP 头)帧格式通信
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)</li>
|
||||
* <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
* <li>{@link #testAuth()} - 自定义功能码认证</li>
|
||||
* <li>{@link #testPollingResponse()} - 轮询响应</li>
|
||||
* <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Disabled
|
||||
public class IotModbusTcpServerTcpIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 503;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
private static final int CUSTOM_FC = 65;
|
||||
private static final int SLAVE_ID = 1;
|
||||
|
||||
private static Vertx vertx;
|
||||
private static NetClient netClient;
|
||||
|
||||
// ===================== 编解码器 =====================
|
||||
|
||||
private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC);
|
||||
private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC);
|
||||
|
||||
// ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo";
|
||||
private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_tcp";
|
||||
private static final String DEVICE_SECRET = "8e4adeb3d25342ab88643421d3fba3f6";
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
NetClientOptions options = new NetClientOptions()
|
||||
.setConnectTimeout(TIMEOUT_MS)
|
||||
.setIdleTimeout(TIMEOUT_MS);
|
||||
netClient = vertx.createNetClient(options);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
if (netClient != null) {
|
||||
netClient.close();
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
* 认证测试:发送自定义功能码 FC65 认证帧,验证认证成功响应
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 构造并发送认证帧
|
||||
IotModbusFrame response = authenticate(socket);
|
||||
|
||||
// 2. 验证响应
|
||||
log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
|
||||
response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
|
||||
JSONObject json = JSONUtil.parseObj(response.getCustomData());
|
||||
assertEquals(0, json.getInt("code"));
|
||||
log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 轮询响应测试 =====================
|
||||
|
||||
/**
|
||||
* 轮询响应测试:认证后持续监听网关下发的读请求,每次收到都自动构造读响应帧发回
|
||||
*/
|
||||
@Test
|
||||
public void testPollingResponse() throws Exception {
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先认证
|
||||
IotModbusFrame authResponse = authenticate(socket);
|
||||
log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
|
||||
JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData());
|
||||
assertEquals(0, authJson.getInt("code"));
|
||||
|
||||
// 2. 设置持续监听:每收到一个读请求,自动回复
|
||||
log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
|
||||
RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> {
|
||||
log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]",
|
||||
frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId());
|
||||
// 解析读请求中的起始地址和数量
|
||||
byte[] pdu = frame.getPdu();
|
||||
int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF);
|
||||
int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF);
|
||||
log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity);
|
||||
|
||||
// 构造读响应帧(模拟寄存器数据)
|
||||
int[] registerValues = new int[quantity];
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
|
||||
}
|
||||
byte[] responseData = buildReadResponse(frame.getTransactionId(),
|
||||
frame.getSlaveId(), frame.getFunctionCode(), registerValues);
|
||||
socket.write(Buffer.buffer(responseData));
|
||||
log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 3. 持续等待(200 秒),期间会自动回复所有收到的读请求
|
||||
Thread.sleep(200000);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 属性设置测试 =====================
|
||||
|
||||
/**
|
||||
* 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求
|
||||
* <p>
|
||||
* 注意:需手动在平台触发 property.set
|
||||
*/
|
||||
@Test
|
||||
public void testPropertySetWrite() throws Exception {
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先认证
|
||||
IotModbusFrame authResponse = authenticate(socket);
|
||||
log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData());
|
||||
|
||||
// 2. 等待网关下发写请求(需手动在平台触发 property.set)
|
||||
log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]");
|
||||
IotModbusFrame writeRequest = waitForRequest(socket);
|
||||
log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]",
|
||||
writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
|
||||
writeRequest.getTransactionId(), HexUtil.encodeHexStr(writeRequest.getPdu()));
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 建立 TCP 连接
|
||||
*/
|
||||
private CompletableFuture<NetSocket> connect() {
|
||||
CompletableFuture<NetSocket> future = new CompletableFuture<>();
|
||||
netClient.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onSuccess(future::complete)
|
||||
.onFailure(future::completeExceptionally);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行认证并返回响应帧
|
||||
*/
|
||||
private IotModbusFrame authenticate(NetSocket socket) throws Exception {
|
||||
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
|
||||
authInfo.setClientId(""); // 特殊:考虑到 modbus 消息长度限制,默认 clientId 不发送
|
||||
byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
|
||||
return sendAndReceive(socket, authFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码)
|
||||
*/
|
||||
private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
|
||||
CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
|
||||
// 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame)
|
||||
RecordParser parser = FRAME_DECODER.createRecordParser(
|
||||
(frame, frameFormat) -> {
|
||||
try {
|
||||
log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat);
|
||||
responseFuture.complete(frame);
|
||||
} catch (Exception e) {
|
||||
responseFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 发送请求
|
||||
log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length);
|
||||
socket.write(Buffer.buffer(frameData));
|
||||
|
||||
// 等待响应
|
||||
return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待接收网关下发的请求帧(不发送,只等待接收)
|
||||
*/
|
||||
private IotModbusFrame waitForRequest(NetSocket socket) throws Exception {
|
||||
CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>();
|
||||
// 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame)
|
||||
RecordParser parser = FRAME_DECODER.createRecordParser(
|
||||
(frame, frameFormat) -> {
|
||||
try {
|
||||
log.info("[waitForRequest][检测到帧格式: {}]", frameFormat);
|
||||
requestFuture.complete(frame);
|
||||
} catch (Exception e) {
|
||||
requestFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 等待(超时 30 秒,因为轮询间隔可能比较长)
|
||||
return requestFuture.get(30000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造认证帧(MODBUS_TCP 格式)
|
||||
* <p>
|
||||
* JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
|
||||
*/
|
||||
private byte[] buildAuthFrame(String clientId, String username, String password) {
|
||||
JSONObject params = new JSONObject();
|
||||
params.set("clientId", clientId);
|
||||
params.set("username", username);
|
||||
params.set("password", password);
|
||||
JSONObject json = new JSONObject();
|
||||
json.set("method", "auth");
|
||||
json.set("params", params);
|
||||
return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(),
|
||||
IotModbusFrameFormatEnum.MODBUS_TCP, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 FC03/FC01-04 读响应帧(MODBUS_TCP 格式)
|
||||
* <p>
|
||||
* 格式:[MBAP(6)] [UnitId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)]
|
||||
*/
|
||||
private byte[] buildReadResponse(int transactionId, int slaveId, int functionCode, int[] registerValues) {
|
||||
int byteCount = registerValues.length * 2;
|
||||
// PDU: FC(1) + ByteCount(1) + Data(N*2)
|
||||
int pduLength = 1 + 1 + byteCount;
|
||||
// 完整帧:MBAP(6) + UnitId(1) + PDU
|
||||
int totalLength = 6 + 1 + pduLength;
|
||||
ByteBuffer buf = ByteBuffer.allocate(totalLength).order(ByteOrder.BIG_ENDIAN);
|
||||
// MBAP Header
|
||||
buf.putShort((short) transactionId); // Transaction ID
|
||||
buf.putShort((short) 0); // Protocol ID
|
||||
buf.putShort((short) (1 + pduLength)); // Length (UnitId + PDU)
|
||||
// UnitId
|
||||
buf.put((byte) slaveId);
|
||||
// PDU
|
||||
buf.put((byte) functionCode);
|
||||
buf.put((byte) byteCount);
|
||||
for (int value : registerValues) {
|
||||
buf.putShort((short) value);
|
||||
}
|
||||
return buf.array();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
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 cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
@@ -23,7 +22,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
@@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@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][测试超时]");
|
||||
MqttClient client = createClient(authInfo);
|
||||
try {
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("width", 1)
|
||||
.put("height", "2")
|
||||
.build()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 2.2 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 直连设备事件上报测试 =====================
|
||||
@@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
IotDeviceEventPostReqDTO.of(
|
||||
"eat",
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 设备动态注册测试(一型一密) =====================
|
||||
@@ -197,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
/**
|
||||
* 直连设备动态注册测试(一型一密)
|
||||
* <p>
|
||||
* 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
|
||||
* 认证方式:
|
||||
* - clientId: 任意值 + "|authType=register|" 后缀
|
||||
* - username: {deviceName}&{productKey}(与普通认证相同)
|
||||
* - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256)
|
||||
* <p>
|
||||
* 注意:此接口不需要认证
|
||||
* 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证
|
||||
*/
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1. 连接并认证(使用已有设备连接)
|
||||
MqttClient client = connectAndAuth();
|
||||
log.info("[testDeviceRegister][连接认证成功]");
|
||||
// 1.1 构建注册参数
|
||||
String deviceName = "test-mqtt-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
// 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀)
|
||||
String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|";
|
||||
String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName);
|
||||
log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]",
|
||||
clientId, username, sign);
|
||||
// 1.3 创建客户端并连接(连接时服务端自动处理注册)
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(clientId)
|
||||
.setUsername(username)
|
||||
.setPassword(sign)
|
||||
.setCleanSession(true)
|
||||
.setKeepAliveInterval(60);
|
||||
MqttClient client = MqttClient.create(vertx, options);
|
||||
|
||||
// 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);
|
||||
try {
|
||||
// 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应)
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testDeviceRegister][连接成功,等待注册响应...]");
|
||||
|
||||
// 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 进行一机一密认证]");
|
||||
// 3.1 设置消息处理器,接收注册响应
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
client.publishHandler(message -> {
|
||||
log.info("[testDeviceRegister][收到响应: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
// 3.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 4. 断开连接
|
||||
disconnect(client);
|
||||
// 4. 等待注册响应
|
||||
IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testDeviceRegister][注册响应: {}]", response);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 订阅下行消息测试 =====================
|
||||
@@ -237,44 +234,41 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@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();
|
||||
try {
|
||||
// 2. 设置消息处理器:收到属性设置时,回复 _reply 消息
|
||||
client.publishHandler(message -> {
|
||||
log.info("[testSubscribe][收到消息: topic={}, payload={}]",
|
||||
message.topicName(), message.payload().toString());
|
||||
// 收到属性设置消息时,回复 _reply
|
||||
if (message.topicName().endsWith("/thing/property/set")) {
|
||||
try {
|
||||
IotDeviceMessage received = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
IotDeviceMessage reply = IotDeviceMessage.replyOf(
|
||||
received.getRequestId(), "thing.property.set_reply", null, 0, null);
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/set_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
byte[] replyPayload = SERIALIZER.serialize(reply);
|
||||
client.publish(replyTopic, Buffer.buffer(replyPayload), MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
log.info("[testSubscribe][已回复属性设置: topic={}]", replyTopic);
|
||||
} catch (Exception e) {
|
||||
log.error("[testSubscribe][回复属性设置异常]", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 等待测试完成
|
||||
boolean completed = latch.await(60, TimeUnit.SECONDS);
|
||||
if (!completed) {
|
||||
log.warn("[testSubscribe][测试超时]");
|
||||
// 3. 订阅下行主题(属性设置 + 服务调用)
|
||||
String topic = String.format("/sys/%s/%s/#", PRODUCT_KEY, DEVICE_NAME);
|
||||
log.info("[testSubscribe][订阅主题: {}]", topic);
|
||||
subscribe(client, topic);
|
||||
log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
|
||||
|
||||
// 4. 保持连接 30 秒等待消息
|
||||
Thread.sleep(30000);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +280,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
@@ -302,44 +296,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @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<>();
|
||||
MqttClient client = createClient(authInfo);
|
||||
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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
* 订阅主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 主题
|
||||
*/
|
||||
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);
|
||||
private void subscribe(MqttClient client, String topic) throws Exception {
|
||||
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[subscribe][订阅主题成功: {}]", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,34 +323,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
|
||||
throws Exception {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = 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);
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
// 2. 序列化并发布消息
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
|
||||
SERIALIZER.getType(), 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());
|
||||
}
|
||||
});
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[publishAndWaitReply][消息发布成功]");
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
// 3. 等待响应
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
@@ -390,19 +357,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
|
||||
* @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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
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;
|
||||
@@ -13,8 +12,8 @@ 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 cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
@@ -27,10 +26,8 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
@@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@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);
|
||||
@@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
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][测试超时]");
|
||||
MqttClient client = createClient(authInfo);
|
||||
try {
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 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 构建子设备认证信息
|
||||
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()
|
||||
.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
|
||||
params);
|
||||
|
||||
// 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);
|
||||
// 2.3 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建请求消息
|
||||
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO()
|
||||
.setSubDevices(Collections.singletonList(
|
||||
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
|
||||
params);
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建请求消息
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params);
|
||||
|
||||
// 2.2 构建请求消息
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
@@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建请求消息
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO()
|
||||
.setProductKey(SUB_DEVICE_PRODUCT_KEY)
|
||||
.setDeviceName("mougezishebei-mqtt");
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
|
||||
Collections.singletonList(subDevice));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 批量上报测试 =====================
|
||||
@@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
|
||||
// 2.2 构建【网关设备】自身属性
|
||||
Map<String, Object> gatewayProperties = MapUtil.<String, Object>builder()
|
||||
.put("temperature", 25.5)
|
||||
.build();
|
||||
// 2.2 构建【网关设备】自身事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("message", "gateway started").build())
|
||||
.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil
|
||||
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.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.3 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
|
||||
// 2.4 构建【网关子设备】属性
|
||||
Map<String, Object> subDeviceProperties = MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.build();
|
||||
// 2.4 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("errorCode", 0).build())
|
||||
.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> subDeviceEvents = MapUtil
|
||||
.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("healthCheck", subDeviceEvent)
|
||||
.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.5 构建子设备数据
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData()
|
||||
.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))
|
||||
.setProperties(subDeviceProperties)
|
||||
.setEvents(subDeviceEvents);
|
||||
|
||||
// 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.6 构建请求消息
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO()
|
||||
.setProperties(gatewayProperties)
|
||||
.setEvents(gatewayEvents)
|
||||
.setSubDevices(ListUtil.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params);
|
||||
|
||||
// 2.7 构建请求消息
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(ListUtil.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.7 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply",
|
||||
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
@@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
@@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @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<>();
|
||||
MqttClient client = createClient(authInfo);
|
||||
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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
* 订阅主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 主题
|
||||
*/
|
||||
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);
|
||||
private void subscribe(MqttClient client, String topic) throws Exception {
|
||||
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[subscribe][订阅主题成功: {}]", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
|
||||
throws Exception {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = 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);
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
// 2. 序列化并发布消息
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
|
||||
SERIALIZER.getType(), 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());
|
||||
}
|
||||
});
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[publishAndWaitReply][消息发布成功]");
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
// 3. 等待响应
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
@@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest {
|
||||
* @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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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 cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
@@ -22,7 +21,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器(MQTT 使用 Alink 协议) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
@@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
*/
|
||||
@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][测试超时]");
|
||||
MqttClient client = createClient(authInfo);
|
||||
try {
|
||||
client.connect(SERVER_PORT, SERVER_HOST)
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId());
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.put("status", "online")
|
||||
.put("temperature", 36.5)
|
||||
.build()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 子设备事件上报测试 =====================
|
||||
@@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
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);
|
||||
try {
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
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()));
|
||||
|
||||
// 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);
|
||||
// 2.2 订阅 _reply 主题
|
||||
String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME);
|
||||
subscribe(client, replyTopic);
|
||||
|
||||
// 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);
|
||||
// 3. 发布消息并等待响应
|
||||
String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME);
|
||||
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
disconnect(client);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
@@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @param authInfo 认证信息
|
||||
* @return MQTT 客户端
|
||||
*/
|
||||
private MqttClient connect(IotDeviceAuthReqDTO authInfo) {
|
||||
private MqttClient createClient(IotDeviceAuthReqDTO authInfo) {
|
||||
MqttClientOptions options = new MqttClientOptions()
|
||||
.setClientId(authInfo.getClientId())
|
||||
.setUsername(authInfo.getUsername())
|
||||
@@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @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<>();
|
||||
MqttClient client = createClient(authInfo);
|
||||
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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅响应主题
|
||||
* 订阅主题
|
||||
*
|
||||
* @param client MQTT 客户端
|
||||
* @param replyTopic 响应主题
|
||||
* @param client MQTT 客户端
|
||||
* @param topic 主题
|
||||
*/
|
||||
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);
|
||||
private void subscribe(MqttClient client, String topic) throws Exception {
|
||||
client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value())
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[subscribe][订阅主题成功: {}]", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) {
|
||||
private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request)
|
||||
throws Exception {
|
||||
// 1. 设置消息处理器,接收响应
|
||||
CompletableFuture<IotDeviceMessage> future = new CompletableFuture<>();
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = 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);
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes());
|
||||
responseFuture.complete(response);
|
||||
});
|
||||
|
||||
// 2. 编码并发布消息
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]",
|
||||
CODEC.type(), topic, new String(payload));
|
||||
|
||||
// 2. 序列化并发布消息
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]",
|
||||
SERIALIZER.getType(), 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());
|
||||
}
|
||||
});
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[publishAndWaitReply][消息发布成功]");
|
||||
|
||||
// 3. 等待响应(超时返回 null)
|
||||
// 3. 等待响应
|
||||
try {
|
||||
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("[publishAndWaitReply][等待响应超时或失败]");
|
||||
return null;
|
||||
@@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest {
|
||||
* @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);
|
||||
.toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
log.info("[disconnect][断开连接成功]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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;
|
||||
@@ -10,32 +8,35 @@ 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 cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetClient;
|
||||
import io.vertx.core.net.NetClientOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
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.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
@@ -58,10 +59,31 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
private static final int SERVER_PORT = 8091;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
private static Vertx vertx;
|
||||
private static NetClient netClient;
|
||||
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
// ===================== 编解码器 =====================
|
||||
|
||||
/**
|
||||
* 消息序列化器
|
||||
*/
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
/**
|
||||
* TCP 帧编解码器
|
||||
*/
|
||||
private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create(
|
||||
new IotTcpConfig.CodecConfig()
|
||||
.setType(IotTcpCodecTypeEnum.DELIMITER.getType())
|
||||
.setDelimiter("\\n")
|
||||
// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType())
|
||||
// .setLengthFieldOffset(0)
|
||||
// .setLengthFieldLength(4)
|
||||
// .setLengthAdjustment(0)
|
||||
// .setInitialBytesToStrip(4)
|
||||
// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType())
|
||||
// .setFixedLength(256)
|
||||
);
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
@@ -69,6 +91,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
private static final String DEVICE_NAME = "small";
|
||||
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
NetClientOptions options = new NetClientOptions()
|
||||
.setConnectTimeout(TIMEOUT_MS)
|
||||
.setIdleTimeout(TIMEOUT_MS);
|
||||
netClient = vertx.createNetClient(options);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
if (netClient != null) {
|
||||
netClient.close();
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
@@ -76,28 +117,21 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.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);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,29 +146,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@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);
|
||||
// 1. 构建注册消息
|
||||
String deviceName = "test-tcp-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testDeviceRegister][响应消息: {}]", response);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,35 +175,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testPropertyPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 2. 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
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);
|
||||
.build()));
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,95 +204,87 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testEventPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 2. 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
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);
|
||||
System.currentTimeMillis()));
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 建立 TCP 连接
|
||||
*
|
||||
* @return 连接 Future
|
||||
*/
|
||||
private CompletableFuture<NetSocket> connect() {
|
||||
CompletableFuture<NetSocket> future = new CompletableFuture<>();
|
||||
netClient.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onSuccess(future::complete)
|
||||
.onFailure(future::completeExceptionally);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行设备认证
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
* @return 认证响应消息
|
||||
*/
|
||||
private IotDeviceMessage authenticate(Socket socket) throws Exception {
|
||||
private IotDeviceMessage authenticate(NetSocket 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;
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo);
|
||||
return sendAndReceive(socket, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 TCP 请求并接收响应
|
||||
* 发送消息并接收响应
|
||||
*
|
||||
* @param socket TCP Socket
|
||||
* @param payload 请求数据
|
||||
* @return 响应数据
|
||||
* @param socket TCP 连接
|
||||
* @param request 请求消息
|
||||
* @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;
|
||||
private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception {
|
||||
// 1. 使用 FRAME_CODEC 创建解码器
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> {
|
||||
try {
|
||||
// 反序列化响应
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes());
|
||||
responseFuture.complete(response);
|
||||
} catch (Exception e) {
|
||||
responseFuture.completeExceptionally(e);
|
||||
}
|
||||
return null;
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 2.1 序列化 + 帧编码
|
||||
byte[] serializedData = SERIALIZER.serialize(request);
|
||||
Buffer frameData = FRAME_CODEC.encode(serializedData);
|
||||
log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length());
|
||||
// 2.2 发送请求
|
||||
socket.write(frameData);
|
||||
|
||||
// 3. 等待响应
|
||||
IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", SERIALIZER.serialize(response).length);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,35 +13,36 @@ 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 cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetClient;
|
||||
import io.vertx.core.net.NetClientOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
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.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
@@ -66,10 +67,31 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
private static final int SERVER_PORT = 8091;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
private static Vertx vertx;
|
||||
private static NetClient netClient;
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
// ===================== 编解码器 =====================
|
||||
|
||||
/**
|
||||
* 消息序列化器
|
||||
*/
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
/**
|
||||
* TCP 帧编解码器
|
||||
*/
|
||||
private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create(
|
||||
new IotTcpConfig.CodecConfig()
|
||||
.setType(IotTcpCodecTypeEnum.DELIMITER.getType())
|
||||
.setDelimiter("\\n")
|
||||
// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType())
|
||||
// .setLengthFieldOffset(0)
|
||||
// .setLengthFieldLength(4)
|
||||
// .setLengthAdjustment(0)
|
||||
// .setInitialBytesToStrip(4)
|
||||
// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType())
|
||||
// .setFixedLength(256)
|
||||
);
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
@@ -83,6 +105,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
private static final String SUB_DEVICE_NAME = "chazuo-it";
|
||||
private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
NetClientOptions options = new NetClientOptions()
|
||||
.setConnectTimeout(TIMEOUT_MS)
|
||||
.setIdleTimeout(TIMEOUT_MS);
|
||||
netClient = vertx.createNetClient(options);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
if (netClient != null) {
|
||||
netClient.close();
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
@@ -90,29 +131,22 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.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);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +157,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testTopoAdd() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testTopoAdd][认证响应: {}]", authResponse);
|
||||
@@ -140,24 +173,16 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
// 2.2 构建请求参数
|
||||
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.3 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
params);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testTopoAdd][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,35 +191,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testTopoDelete() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testTopoDelete][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建请求参数
|
||||
// 2. 构建请求参数
|
||||
IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(
|
||||
new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
params);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testTopoDelete][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,33 +218,23 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testTopoGet() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testTopoGet][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建请求参数
|
||||
// 2. 构建请求参数
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
params);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testTopoGet][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,35 +245,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testSubDeviceRegister() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 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(),
|
||||
// 2. 构建请求参数
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO()
|
||||
.setProductKey(SUB_DEVICE_PRODUCT_KEY)
|
||||
.setDeviceName("mougezishebei");
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
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);
|
||||
Collections.singletonList(subDevice));
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,9 +274,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPackPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testPropertyPackPost][认证响应: {}]", authResponse);
|
||||
@@ -291,9 +285,9 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
.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());
|
||||
IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("message", "gateway started").build())
|
||||
.setTime(System.currentTimeMillis());
|
||||
Map<String, IotDevicePropertyPackPostReqDTO.EventValue> gatewayEvents = MapUtil.<String, IotDevicePropertyPackPostReqDTO.EventValue>builder()
|
||||
.put("statusReport", gatewayEvent)
|
||||
.build();
|
||||
@@ -302,97 +296,95 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest {
|
||||
.put("power", 100)
|
||||
.build();
|
||||
// 2.4 构建【网关子设备】事件
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue();
|
||||
subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build());
|
||||
subDeviceEvent.setTime(System.currentTimeMillis());
|
||||
IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue()
|
||||
.setValue(MapUtil.builder().put("errorCode", 0).build())
|
||||
.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);
|
||||
IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData()
|
||||
.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))
|
||||
.setProperties(subDeviceProperties)
|
||||
.setEvents(subDeviceEvents);
|
||||
// 2.6 构建请求参数
|
||||
IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO();
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(ListUtil.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.7 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
params);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 建立 TCP 连接
|
||||
*
|
||||
* @return 连接 Future
|
||||
*/
|
||||
private CompletableFuture<NetSocket> connect() {
|
||||
CompletableFuture<NetSocket> future = new CompletableFuture<>();
|
||||
netClient.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onSuccess(future::complete)
|
||||
.onFailure(future::completeExceptionally);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行网关设备认证
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
* @return 认证响应消息
|
||||
*/
|
||||
private IotDeviceMessage authenticate(Socket socket) throws Exception {
|
||||
private IotDeviceMessage authenticate(NetSocket 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;
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo);
|
||||
return sendAndReceive(socket, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 TCP 请求并接收响应
|
||||
* 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑)
|
||||
*
|
||||
* @param socket TCP Socket
|
||||
* @param payload 请求数据
|
||||
* @return 响应数据
|
||||
* @param socket TCP 连接
|
||||
* @param request 请求消息
|
||||
* @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;
|
||||
private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception {
|
||||
// 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑)
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> {
|
||||
try {
|
||||
// 反序列化响应
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes());
|
||||
responseFuture.complete(response);
|
||||
} catch (Exception e) {
|
||||
responseFuture.completeExceptionally(e);
|
||||
}
|
||||
return null;
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 2.1 序列化 + 帧编码
|
||||
byte[] serializedData = SERIALIZER.serialize(request);
|
||||
Buffer frameData = FRAME_CODEC.encode(serializedData);
|
||||
log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length());
|
||||
// 2.2 发送请求
|
||||
socket.write(frameData);
|
||||
|
||||
// 3. 等待响应
|
||||
IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
log.info("[sendAndReceive][收到响应,数据长度: {} 字节]",
|
||||
SERIALIZER.serialize(response).length);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
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 cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
|
||||
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetClient;
|
||||
import io.vertx.core.net.NetClientOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.parsetools.RecordParser;
|
||||
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.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IoT 网关子设备 TCP 协议集成测试(手动测试)
|
||||
@@ -26,17 +34,10 @@ import java.net.Socket;
|
||||
*
|
||||
* <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>
|
||||
@@ -58,10 +59,31 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
|
||||
private static final int SERVER_PORT = 8091;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
private static Vertx vertx;
|
||||
private static NetClient netClient;
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
// ===================== 编解码器 =====================
|
||||
|
||||
/**
|
||||
* 消息序列化器
|
||||
*/
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
/**
|
||||
* TCP 帧编解码器
|
||||
*/
|
||||
private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create(
|
||||
new IotTcpConfig.CodecConfig()
|
||||
.setType(IotTcpCodecTypeEnum.DELIMITER.getType())
|
||||
.setDelimiter("\\n")
|
||||
// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType())
|
||||
// .setLengthFieldOffset(0)
|
||||
// .setLengthFieldLength(4)
|
||||
// .setLengthAdjustment(0)
|
||||
// .setInitialBytesToStrip(4)
|
||||
// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType())
|
||||
// .setFixedLength(256)
|
||||
);
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
@@ -69,6 +91,25 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
|
||||
private static final String DEVICE_NAME = "chazuo-it";
|
||||
private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af";
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
NetClientOptions options = new NetClientOptions()
|
||||
.setConnectTimeout(TIMEOUT_MS)
|
||||
.setIdleTimeout(TIMEOUT_MS);
|
||||
netClient = vertx.createNetClient(options);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
if (netClient != null) {
|
||||
netClient.close();
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
/**
|
||||
@@ -76,28 +117,21 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.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);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO);
|
||||
|
||||
// 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][未收到响应]");
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,37 +142,27 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testPropertyPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 2. 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
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);
|
||||
.build()));
|
||||
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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,16 +173,14 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
|
||||
NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
// 1. 先进行认证
|
||||
IotDeviceMessage authResponse = authenticate(socket);
|
||||
log.info("[testEventPost][认证响应: {}]", authResponse);
|
||||
|
||||
// 2.1 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 2. 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
IotDeviceEventPostReqDTO.of(
|
||||
"alarm",
|
||||
@@ -168,78 +190,77 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest {
|
||||
.put("threshold", 40)
|
||||
.put("current", 42)
|
||||
.build(),
|
||||
System.currentTimeMillis()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
System.currentTimeMillis()));
|
||||
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][未收到响应]");
|
||||
}
|
||||
// 3. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(socket, request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 建立 TCP 连接
|
||||
*
|
||||
* @return 连接 Future
|
||||
*/
|
||||
private CompletableFuture<NetSocket> connect() {
|
||||
CompletableFuture<NetSocket> future = new CompletableFuture<>();
|
||||
netClient.connect(SERVER_PORT, SERVER_HOST)
|
||||
.onSuccess(future::complete)
|
||||
.onFailure(future::completeExceptionally);
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行子设备认证
|
||||
*
|
||||
* @param socket TCP 连接
|
||||
* @return 认证响应消息
|
||||
*/
|
||||
private IotDeviceMessage authenticate(Socket socket) throws Exception {
|
||||
private IotDeviceMessage authenticate(NetSocket 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;
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo);
|
||||
return sendAndReceive(socket, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 TCP 请求并接收响应
|
||||
* 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑)
|
||||
*
|
||||
* @param socket TCP Socket
|
||||
* @param payload 请求数据
|
||||
* @return 响应数据
|
||||
* @param socket TCP 连接
|
||||
* @param request 请求消息
|
||||
* @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;
|
||||
private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception {
|
||||
// 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑)
|
||||
CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
|
||||
RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> {
|
||||
try {
|
||||
// 反序列化响应
|
||||
IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes());
|
||||
responseFuture.complete(response);
|
||||
} catch (Exception e) {
|
||||
responseFuture.completeExceptionally(e);
|
||||
}
|
||||
return null;
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
socket.handler(parser);
|
||||
|
||||
// 2.1 序列化 + 帧编码
|
||||
byte[] serializedData = SERIALIZER.serialize(request);
|
||||
Buffer frameData = FRAME_CODEC.encode(serializedData);
|
||||
log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length());
|
||||
// 2.2 发送请求
|
||||
socket.write(frameData);
|
||||
|
||||
// 3. 等待响应
|
||||
IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
log.info("[sendAndReceive][收到响应,数据长度: {} 字节]",
|
||||
SERIALIZER.serialize(response).length);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
|
||||
|
||||
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;
|
||||
@@ -9,9 +8,9 @@ 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 cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -27,16 +26,9 @@ import java.util.Map;
|
||||
*
|
||||
* <p>测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台
|
||||
*
|
||||
* <p>支持两种编解码格式:
|
||||
* <ul>
|
||||
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
|
||||
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
|
||||
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
|
||||
* <li>运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
@@ -58,10 +50,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
private static final int SERVER_PORT = 8093;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
/**
|
||||
* 消息序列化器
|
||||
*/
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
@@ -72,7 +66,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
/**
|
||||
* 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里
|
||||
*/
|
||||
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk0ODYzOCwiZGV2aWNlTmFtZSI6InNtYWxsIn0.TrOJisXhloZ3quLBOAIyowmpq6Syp9PHiEpfj-nQ9xo";
|
||||
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc3MDUyNTA0MywiZGV2aWNlTmFtZSI6InNtYWxsIn0.W9Mo-Oe1ZNLDkINndKieUeW1XhDzhVp0W0zTAwO6hJM";
|
||||
|
||||
// ===================== 认证测试 =====================
|
||||
|
||||
@@ -81,30 +75,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.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);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
}
|
||||
|
||||
// ===================== 动态注册测试 =====================
|
||||
@@ -118,30 +100,21 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1.1 构建注册消息
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
|
||||
registerReqDTO.setProductKey(PRODUCT_KEY);
|
||||
registerReqDTO.setDeviceName("test-udp-" + 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);
|
||||
// 1. 构建注册消息
|
||||
String deviceName = "test-udp-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
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][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testDeviceRegister][响应消息: {}]", response);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
}
|
||||
|
||||
// ===================== 直连设备属性上报测试 =====================
|
||||
@@ -151,31 +124,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
// 1.1 构建属性上报消息(UDP 协议:token 放在 params 中)
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 1. 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("width", 1)
|
||||
.put("height", "2")
|
||||
.build())),
|
||||
null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
.build())));
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testPropertyPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
// ===================== 直连设备事件上报测试 =====================
|
||||
@@ -185,31 +144,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
// 1.1 构建事件上报消息(UDP 协议:token 放在 params 中)
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 1. 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
withToken(IotDeviceEventPostReqDTO.of(
|
||||
"eat",
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis())),
|
||||
null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
System.currentTimeMillis())));
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testEventPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
@@ -232,30 +177,36 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 UDP 请求并接收响应
|
||||
* 发送 UDP 消息并接收响应
|
||||
*
|
||||
* @param socket UDP Socket
|
||||
* @param payload 请求数据
|
||||
* @return 响应数据
|
||||
* @param request 请求消息
|
||||
* @return 响应消息
|
||||
*/
|
||||
public static byte[] sendAndReceive(DatagramSocket socket, byte[] payload) throws Exception {
|
||||
InetAddress address = InetAddress.getByName(SERVER_HOST);
|
||||
private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception {
|
||||
// 1. 序列化请求
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length);
|
||||
|
||||
// 发送请求
|
||||
DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT);
|
||||
socket.send(sendPacket);
|
||||
// 2. 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
InetAddress address = InetAddress.getByName(SERVER_HOST);
|
||||
DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT);
|
||||
socket.send(sendPacket);
|
||||
|
||||
// 接收响应
|
||||
byte[] receiveData = new byte[4096];
|
||||
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
|
||||
try {
|
||||
socket.receive(receivePacket);
|
||||
byte[] response = new byte[receivePacket.getLength()];
|
||||
System.arraycopy(receivePacket.getData(), 0, response, 0, receivePacket.getLength());
|
||||
return response;
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
// 3. 接收响应
|
||||
byte[] receiveData = new byte[4096];
|
||||
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
|
||||
try {
|
||||
socket.receive(receivePacket);
|
||||
byte[] responseBytes = new byte[receivePacket.getLength()];
|
||||
System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength());
|
||||
log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", responseBytes.length);
|
||||
return SERIALIZER.deserialize(responseBytes);
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
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;
|
||||
@@ -13,36 +12,27 @@ 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 cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive;
|
||||
|
||||
/**
|
||||
* IoT 网关设备 UDP 协议集成测试(手动测试)
|
||||
*
|
||||
* <p>测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系
|
||||
*
|
||||
* <p>支持两种编解码格式:
|
||||
* <ul>
|
||||
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
|
||||
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
|
||||
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
|
||||
* <li>运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
@@ -63,12 +53,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd
|
||||
@Disabled
|
||||
public class IotGatewayDeviceUdpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8093;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
/**
|
||||
* 消息序列化器
|
||||
*/
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
@@ -93,185 +87,101 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.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);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
|
||||
}
|
||||
|
||||
// ===================== 拓扑管理测试 =====================
|
||||
|
||||
/**
|
||||
* 添加子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要绑定的子设备信息
|
||||
*/
|
||||
@Test
|
||||
public void testTopoAdd() throws Exception {
|
||||
// 1.1 构建子设备认证信息
|
||||
// 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());
|
||||
// 1.2 构建请求参数
|
||||
IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO();
|
||||
params.setSubDevices(Collections.singletonList(subDeviceAuth));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
|
||||
withToken(params),
|
||||
null, null, null);
|
||||
// 1.3 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), withToken(params));
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testTopoAdd][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testTopoAdd][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testTopoAdd][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台上报需要解绑的子设备信息
|
||||
*/
|
||||
@Test
|
||||
public void testTopoDelete() throws Exception {
|
||||
// 1.1 构建请求参数
|
||||
// 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(),
|
||||
withToken(params),
|
||||
null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), withToken(params));
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testTopoDelete][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testTopoDelete][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testTopoDelete][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子设备拓扑关系测试
|
||||
* <p>
|
||||
* 网关设备向平台查询已绑定的子设备列表
|
||||
*/
|
||||
@Test
|
||||
public void testTopoGet() throws Exception {
|
||||
// 1.1 构建请求参数(目前为空,预留扩展)
|
||||
// 1. 构建请求参数
|
||||
IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO();
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
withToken(params),
|
||||
null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), withToken(params));
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testTopoGet][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testTopoGet][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testTopoGet][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
// ===================== 子设备注册测试 =====================
|
||||
|
||||
/**
|
||||
* 子设备动态注册测试
|
||||
* <p>
|
||||
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
|
||||
* <p>
|
||||
* 注意:此接口需要网关 Token 认证
|
||||
*/
|
||||
@Test
|
||||
public void testSubDeviceRegister() throws Exception {
|
||||
// 1.1 构建请求参数
|
||||
// 1. 构建请求参数
|
||||
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
|
||||
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
|
||||
subDevice.setDeviceName("mougezishebei");
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
|
||||
withToken(Collections.singletonList(subDevice)),
|
||||
null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
withToken(Collections.singletonList(subDevice)));
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testSubDeviceRegister][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
// ===================== 批量上报测试 =====================
|
||||
|
||||
/**
|
||||
* 批量上报属性测试(网关 + 子设备)
|
||||
* <p>
|
||||
* 网关设备批量上报自身属性、事件,以及子设备的属性、事件
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPackPost() throws Exception {
|
||||
@@ -307,40 +217,18 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest {
|
||||
params.setProperties(gatewayProperties);
|
||||
params.setEvents(gatewayEvents);
|
||||
params.setSubDevices(ListUtil.of(subDeviceData));
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
withToken(params),
|
||||
null, null, null);
|
||||
// 1.7 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), withToken(params));
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testPropertyPackPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 构建带 token 的 params
|
||||
* <p>
|
||||
* 返回格式:{token: "xxx", body: params}
|
||||
* - token:JWT 令牌
|
||||
* - body:实际请求内容(可以是 Map、List 或其他类型)
|
||||
*
|
||||
* @param params 原始参数(Map、List 或对象)
|
||||
* @return 包含 token 和 body 的 Map
|
||||
*/
|
||||
private Map<String, Object> withToken(Object params) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@@ -349,4 +237,31 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 UDP 消息并接收响应
|
||||
*/
|
||||
private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception {
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length);
|
||||
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
InetAddress address = InetAddress.getByName(SERVER_HOST);
|
||||
DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT);
|
||||
socket.send(sendPacket);
|
||||
|
||||
byte[] receiveData = new byte[4096];
|
||||
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
|
||||
try {
|
||||
socket.receive(receivePacket);
|
||||
byte[] responseBytes = new byte[receivePacket.getLength()];
|
||||
System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength());
|
||||
return SERIALIZER.deserialize(responseBytes);
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
package cn.iocoder.yudao.module.iot.gateway.protocol.udp;
|
||||
|
||||
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 cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive;
|
||||
|
||||
/**
|
||||
* IoT 网关子设备 UDP 协议集成测试(手动测试)
|
||||
*
|
||||
@@ -29,17 +27,10 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd
|
||||
* <p><b>重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。</b>
|
||||
* <p>网关设备转发子设备请求时,Token 使用子设备自己的信息。
|
||||
*
|
||||
* <p>支持两种编解码格式:
|
||||
* <ul>
|
||||
* <li>{@link IotTcpJsonDeviceMessageCodec} - JSON 格式</li>
|
||||
* <li>{@link IotTcpBinaryDeviceMessageCodec} - 二进制格式</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用步骤:
|
||||
* <ol>
|
||||
* <li>启动 yudao-module-iot-gateway 服务(UDP 端口 8093)</li>
|
||||
* <li>确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关</li>
|
||||
* <li>修改 {@link #CODEC} 选择测试的编解码格式</li>
|
||||
* <li>运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
|
||||
* <li>运行以下测试方法:
|
||||
* <ul>
|
||||
@@ -57,12 +48,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd
|
||||
@Disabled
|
||||
public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
|
||||
|
||||
private static final String SERVER_HOST = "127.0.0.1";
|
||||
private static final int SERVER_PORT = 8093;
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
|
||||
// ===================== 编解码器选择(修改此处切换 JSON / Binary) =====================
|
||||
// ===================== 序列化器 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec();
|
||||
// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec();
|
||||
/**
|
||||
* 消息序列化器
|
||||
*/
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
@@ -82,30 +77,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testAuth() throws Exception {
|
||||
// 1.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);
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testAuth][响应消息: {}]", response);
|
||||
log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]");
|
||||
}
|
||||
|
||||
// ===================== 子设备属性上报测试 =====================
|
||||
@@ -115,33 +98,19 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testPropertyPost() throws Exception {
|
||||
// 1.1 构建属性上报消息(UDP 协议:token 放在 params 中)
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 1. 构建属性上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
|
||||
withToken(IotDevicePropertyPostReqDTO.of(MapUtil.<String, Object>builder()
|
||||
.put("power", 100)
|
||||
.put("status", "online")
|
||||
.put("temperature", 36.5)
|
||||
.build())),
|
||||
null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
.build())));
|
||||
log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testPropertyPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testPropertyPost][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
// ===================== 子设备事件上报测试 =====================
|
||||
@@ -151,9 +120,8 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
public void testEventPost() throws Exception {
|
||||
// 1.1 构建事件上报消息(UDP 协议:token 放在 params 中)
|
||||
IotDeviceMessage request = IotDeviceMessage.of(
|
||||
IdUtil.fastSimpleUUID(),
|
||||
// 1. 构建事件上报消息
|
||||
IotDeviceMessage request = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
|
||||
withToken(IotDeviceEventPostReqDTO.of(
|
||||
"alarm",
|
||||
@@ -163,38 +131,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
|
||||
.put("threshold", 40)
|
||||
.put("current", 42)
|
||||
.build(),
|
||||
System.currentTimeMillis())),
|
||||
null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
System.currentTimeMillis())));
|
||||
log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]");
|
||||
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
|
||||
// 2.1 发送请求
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
byte[] responseBytes = sendAndReceive(socket, payload);
|
||||
// 2.2 解码响应
|
||||
if (responseBytes != null) {
|
||||
IotDeviceMessage response = CODEC.decode(responseBytes);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
} else {
|
||||
log.warn("[testEventPost][未收到响应]");
|
||||
}
|
||||
}
|
||||
// 2. 发送并接收响应
|
||||
IotDeviceMessage response = sendAndReceive(request);
|
||||
log.info("[testEventPost][响应消息: {}]", response);
|
||||
}
|
||||
|
||||
// ===================== 辅助方法 =====================
|
||||
|
||||
/**
|
||||
* 构建带 token 的 params
|
||||
* <p>
|
||||
* 返回格式:{token: "xxx", body: params}
|
||||
* - token:JWT 令牌
|
||||
* - body:实际请求内容(可以是 Map、List 或其他类型)
|
||||
*
|
||||
* @param params 原始参数(Map、List 或对象)
|
||||
* @return 包含 token 和 body 的 Map
|
||||
*/
|
||||
private Map<String, Object> withToken(Object params) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@@ -203,4 +151,31 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 UDP 消息并接收响应
|
||||
*/
|
||||
private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception {
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length);
|
||||
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setSoTimeout(TIMEOUT_MS);
|
||||
InetAddress address = InetAddress.getByName(SERVER_HOST);
|
||||
DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT);
|
||||
socket.send(sendPacket);
|
||||
|
||||
byte[] receiveData = new byte[4096];
|
||||
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
|
||||
try {
|
||||
socket.receive(receivePacket);
|
||||
byte[] responseBytes = new byte[receivePacket.getLength()];
|
||||
System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength());
|
||||
return SERIALIZER.deserialize(responseBytes);
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn("[sendAndReceive][接收响应超时]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ 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 cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.WebSocket;
|
||||
import io.vertx.core.http.WebSocketClient;
|
||||
@@ -61,7 +62,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
|
||||
// ===================== 编解码器选择 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) =====================
|
||||
|
||||
@@ -95,10 +96,10 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 1.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 2.1 创建 WebSocket 连接(同步)
|
||||
WebSocket ws = createWebSocketConnection();
|
||||
@@ -109,7 +110,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
|
||||
// 3. 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testAuth][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
@@ -131,16 +132,19 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
@Test
|
||||
public void testDeviceRegister() throws Exception {
|
||||
// 1.1 构建注册消息
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
|
||||
registerReqDTO.setProductKey(PRODUCT_KEY);
|
||||
registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis());
|
||||
registerReqDTO.setProductSecret("test-product-secret");
|
||||
String deviceName = "test-ws-" + System.currentTimeMillis();
|
||||
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
|
||||
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
|
||||
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
|
||||
.setProductKey(PRODUCT_KEY)
|
||||
.setDeviceName(deviceName)
|
||||
.setSign(sign);
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
|
||||
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 1.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 2.1 创建 WebSocket 连接(同步)
|
||||
WebSocket ws = createWebSocketConnection();
|
||||
@@ -151,7 +155,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
|
||||
// 3. 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testDeviceRegister][响应消息: {}]", responseMessage);
|
||||
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
|
||||
} else {
|
||||
@@ -186,16 +190,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
.put("height", "2")
|
||||
.build()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testPropertyPost][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testPropertyPost][未收到响应]");
|
||||
@@ -229,16 +233,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
MapUtil.<String, Object>builder().put("rice", 3).build(),
|
||||
System.currentTimeMillis()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testEventPost][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testEventPost][未收到响应]");
|
||||
@@ -308,13 +312,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
|
||||
byte[] payload = CODEC.encode(request);
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[authenticate][发送认证请求: {}]", jsonMessage);
|
||||
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
if (response != null) {
|
||||
return CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
return SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ 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 cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.WebSocket;
|
||||
import io.vertx.core.http.WebSocketClient;
|
||||
@@ -67,9 +67,9 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器选择 =====================
|
||||
// ===================== 序列化器选择 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) =====================
|
||||
|
||||
@@ -110,10 +110,10 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 1.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 2.1 创建 WebSocket 连接(同步)
|
||||
WebSocket ws = createWebSocketConnection();
|
||||
@@ -124,7 +124,7 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
|
||||
// 3. 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testAuth][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
@@ -164,16 +164,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.3 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.3 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testTopoAdd][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testTopoAdd][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testTopoAdd][未收到响应]");
|
||||
@@ -205,16 +205,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testTopoDelete][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testTopoDelete][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testTopoDelete][未收到响应]");
|
||||
@@ -244,16 +244,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
IotDeviceMessageMethodEnum.TOPO_GET.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testTopoGet][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testTopoGet][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testTopoGet][未收到响应]");
|
||||
@@ -287,16 +287,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(),
|
||||
Collections.singletonList(subDevice),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testSubDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testSubDeviceRegister][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testSubDeviceRegister][未收到响应]");
|
||||
@@ -358,16 +358,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(),
|
||||
params,
|
||||
null, null, null);
|
||||
// 2.7 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.7 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testPropertyPackPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testPropertyPackPost][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testPropertyPackPost][未收到响应]");
|
||||
@@ -438,13 +438,13 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest {
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
|
||||
byte[] payload = CODEC.encode(request);
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[authenticate][发送认证请求: {}]", jsonMessage);
|
||||
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
if (response != null) {
|
||||
return CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
return SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ 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 cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
|
||||
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.WebSocket;
|
||||
import io.vertx.core.http.WebSocketClient;
|
||||
@@ -60,9 +60,9 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
|
||||
|
||||
private static Vertx vertx;
|
||||
|
||||
// ===================== 编解码器选择 =====================
|
||||
// ===================== 序列化器选择 =====================
|
||||
|
||||
private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec();
|
||||
private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer();
|
||||
|
||||
// ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) =====================
|
||||
|
||||
@@ -96,10 +96,10 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
|
||||
.setUsername(authInfo.getUsername())
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
// 1.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 1.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 2.1 创建 WebSocket 连接(同步)
|
||||
WebSocket ws = createWebSocketConnection();
|
||||
@@ -110,7 +110,7 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
|
||||
|
||||
// 3. 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testAuth][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testAuth][未收到响应]");
|
||||
@@ -146,16 +146,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
|
||||
.put("temperature", 36.5)
|
||||
.build()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testPropertyPost][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testPropertyPost][未收到响应]");
|
||||
@@ -195,16 +195,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
|
||||
.build(),
|
||||
System.currentTimeMillis()),
|
||||
null, null, null);
|
||||
// 2.2 编码
|
||||
byte[] payload = CODEC.encode(request);
|
||||
// 2.2 序列化
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request);
|
||||
log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request);
|
||||
|
||||
// 3.1 发送并等待响应
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
// 3.2 解码响应
|
||||
if (response != null) {
|
||||
IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
log.info("[testEventPost][响应消息: {}]", responseMessage);
|
||||
} else {
|
||||
log.warn("[testEventPost][未收到响应]");
|
||||
@@ -274,13 +274,13 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest {
|
||||
.setPassword(authInfo.getPassword());
|
||||
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null);
|
||||
|
||||
byte[] payload = CODEC.encode(request);
|
||||
byte[] payload = SERIALIZER.serialize(request);
|
||||
String jsonMessage = StrUtil.utf8Str(payload);
|
||||
log.info("[authenticate][发送认证请求: {}]", jsonMessage);
|
||||
|
||||
String response = sendAndReceive(ws, jsonMessage);
|
||||
if (response != null) {
|
||||
return CODEC.decode(StrUtil.utf8Bytes(response));
|
||||
return SERIALIZER.deserialize(StrUtil.utf8Bytes(response));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user