【同步】BOOT 和 CLOUD 的功能(IoT)

This commit is contained in:
YunaiV
2026-02-14 16:35:48 +08:00
parent 2d4251eda7
commit 92eda45afd
245 changed files with 14927 additions and 7689 deletions

View File

@@ -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);

View File

@@ -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 输出结果

View File

@@ -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 输出结果

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 输出请求

View File

@@ -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()

View File

@@ -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.1pom.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 包装 SocketRTU 帧 = 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. 启动 SlaveRTU 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 ? " ✓ 验证通过" : " ✗ 验证失败"));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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_RTUCRC16帧格式通信
*
* <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;
}
}

View File

@@ -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_TCPMBAP 头)帧格式通信
*
* <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();
}
}

View File

@@ -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][断开连接成功]");
}
}

View File

@@ -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][断开连接成功]");
}
}

View File

@@ -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][断开连接成功]");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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}
* - tokenJWT 令牌
* - 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;
}
}
}
}

View File

@@ -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}
* - tokenJWT 令牌
* - 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;
}
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}