feat(iot):1)重构 modbus tcp 连接的实现为 modbus-tcp-master;2)新增 modbus-tcp-slave【初步实现,代码准备优化】

This commit is contained in:
YunaiV
2026-02-07 23:04:03 +08:00
parent 4319220750
commit a0db86848d
38 changed files with 3612 additions and 71 deletions

View File

@@ -33,6 +33,14 @@ public class IotDeviceModbusConfigRespVO {
@Schema(description = "重试间隔(毫秒)", example = "1000")
private Integer retryInterval;
// TODO @AI不要【1-云端轮询 2-主动上报】
@Schema(description = "模式1-云端轮询 2-主动上报", example = "1")
private Integer mode;
// TODO @AI还是换成 int然后写注释不要【modbus_tcp / modbus_rtu】
@Schema(description = "数据帧格式modbus_tcp / modbus_rtu", example = "modbus_tcp")
private String frameFormat;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;

View File

@@ -31,6 +31,14 @@ public class IotDeviceModbusConfigSaveReqVO {
@Schema(description = "重试间隔(毫秒)", example = "1000")
private Integer retryInterval;
// TODO @AI不要【1-云端轮询 2-主动上报】
@Schema(description = "模式1-云端轮询 2-主动上报", example = "1")
private Integer mode;
// TODO @AI不要【1-云端轮询 2-主动上报】
@Schema(description = "数据帧格式modbus_tcp / modbus_rtu", example = "modbus_tcp")
private String frameFormat;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
private Integer status;

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.device;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -24,7 +25,12 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO {
*/
@TableId
private Long id;
// TODO @AI增加 productId
/**
* 产品编号
*
* 关联 {@link IotProductDO#getId()}
*/
private Long productId;
/**
* 设备编号
*
@@ -52,6 +58,18 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO {
* 重试间隔,单位:毫秒
*/
private Integer retryInterval;
/**
* 模式
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum
*/
private Integer mode;
/**
* 数据帧格式
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum
*/
private String frameFormat;
/**
* 状态
*

View File

@@ -47,8 +47,16 @@ public class IotModbusDeviceConfigRespDTO {
* 重试间隔,单位:毫秒
*/
private Integer retryInterval;
/**
* 模式
*/
private Integer mode;
/**
* 数据帧格式
*/
private String frameFormat;
// ========== 点位配置 ==========
// ========== Modbus 点位配置 ==========
/**
* 点位列表

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.iot.core.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT Modbus 数据帧格式枚举
*
* @author 芋道源码
*/
@Getter
@RequiredArgsConstructor
public enum IotModbusFrameFormatEnum implements ArrayValuable<String> {
MODBUS_TCP("modbus_tcp", "Modbus TCP"),
MODBUS_RTU("modbus_rtu", "Modbus RTU");
public static final String[] ARRAYS = Arrays.stream(values())
.map(IotModbusFrameFormatEnum::getFormat)
.toArray(String[]::new);
/**
* 格式
*/
private final String format;
/**
* 名称
*/
private final String name;
@Override
public String[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.iot.core.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT Modbus 模式枚举
*
* @author 芋道源码
*/
@Getter
@RequiredArgsConstructor
public enum IotModbusModeEnum implements ArrayValuable<Integer> {
POLLING(1, "云端轮询"),
ACTIVE_REPORT(2, "主动上报");
public static final Integer[] ARRAYS = Arrays.stream(values())
.map(IotModbusModeEnum::getMode)
.toArray(Integer[]::new);
/**
* 模式
*/
private final Integer mode;
/**
* 名称
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -25,7 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable<String> {
MQTT("mqtt"),
EMQX("emqx"),
COAP("coap"),
MODBUS_TCP("modbus_tcp");
MODBUS_TCP_MASTER("modbus_tcp_master"),
MODBUS_TCP_SLAVE("modbus_tcp_slave");
public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new);

View File

@@ -4,7 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
@@ -168,10 +169,16 @@ public class IotGatewayProperties {
private IotEmqxConfig emqx;
/**
* Modbus TCP 协议配置
* Modbus TCP Master 协议配置
*/
@Valid
private IotModbusTcpConfig modbusTcp;
private IotModbusTcpMasterConfig modbusTcpMaster;
/**
* Modbus TCP Slave 协议配置
*/
@Valid
private IotModbusTcpSlaveConfig modbusTcpSlave;
}

View File

@@ -7,7 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
@@ -113,8 +114,10 @@ public class IotProtocolManager implements SmartLifecycle {
return createMqttProtocol(config);
case EMQX:
return createEmqxProtocol(config);
case MODBUS_TCP:
return createModbusTcpProtocol(config);
case MODBUS_TCP_MASTER:
return createModbusTcpMasterProtocol(config);
case MODBUS_TCP_SLAVE:
return createModbusTcpSlaveProtocol(config);
default:
throw new IllegalArgumentException(String.format(
"[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
@@ -192,13 +195,23 @@ public class IotProtocolManager implements SmartLifecycle {
}
/**
* 创建 Modbus TCP 协议实例
* 创建 Modbus TCP Master 协议实例
*
* @param config 协议实例配置
* @return Modbus TCP 协议实例
* @return Modbus TCP Master 协议实例
*/
private IotModbusTcpProtocol createModbusTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpProtocol(config);
private IotModbusTcpMasterProtocol createModbusTcpMasterProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpMasterProtocol(config);
}
/**
* 创建 Modbus TCP Slave 协议实例
*
* @param config 协议实例配置
* @return Modbus TCP Slave 协议实例
*/
private IotModbusTcpSlaveProtocol createModbusTcpSlaveProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpSlaveProtocol(config);
}
}

View File

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;

View File

@@ -1,16 +1,16 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT Modbus TCP 协议配置
* IoT Modbus TCP Master 协议配置
*
* @author 芋道源码
*/
@Data
public class IotModbusTcpConfig {
public class IotModbusTcpMasterConfig {
/**
* 配置刷新间隔

View File

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
@@ -9,14 +9,14 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream.IotModbusTcpUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import lombok.Getter;
@@ -27,12 +27,12 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* IoT 网关 Modbus TCP 协议主动轮询 Modbus 从站设备数据
* IoT 网关 Modbus TCP Master 协议主动轮询 Modbus 从站设备数据
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpProtocol implements IotProtocol {
public class IotModbusTcpMasterProtocol implements IotProtocol {
/**
* 协议配置
@@ -71,9 +71,9 @@ public class IotModbusTcpProtocol implements IotProtocol {
private final IotModbusTcpConfigCacheService configCacheService;
private final IotModbusTcpPollScheduler pollScheduler;
public IotModbusTcpProtocol(ProtocolProperties properties) {
IotModbusTcpConfig modbusTcpConfig = properties.getModbusTcp();
Assert.notNull(modbusTcpConfig, "Modbus TCP 协议配置modbusTcp不能为空");
public IotModbusTcpMasterProtocol(ProtocolProperties properties) {
IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster();
Assert.notNull(modbusTcpMasterConfig, "Modbus TCP Master 协议配置modbusTcpMaster)不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
@@ -109,13 +109,13 @@ public class IotModbusTcpProtocol implements IotProtocol {
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MODBUS_TCP;
return IotProtocolTypeEnum.MODBUS_TCP_MASTER;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT Modbus TCP 协议 {} 已经在运行中]", getId());
log.warn("[start][IoT Modbus TCP Master 协议 {} 已经在运行中]", getId());
return;
}
@@ -123,18 +123,18 @@ public class IotModbusTcpProtocol implements IotProtocol {
// 1.1 首次加载配置
refreshConfig();
// 1.2 启动配置刷新定时器
int refreshInterval = properties.getModbusTcp().getConfigRefreshInterval();
int refreshInterval = properties.getModbusTcpMaster().getConfigRefreshInterval();
configRefreshTimerId = vertx.setPeriodic(
TimeUnit.SECONDS.toMillis(refreshInterval),
id -> refreshConfig()
);
running = true;
log.info("[start][IoT Modbus TCP 协议 {} 启动成功serverId={}]", getId(), serverId);
log.info("[start][IoT Modbus TCP Master 协议 {} 启动成功serverId={}]", getId(), serverId);
// 2. 启动下行消息订阅者
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT Modbus TCP 协议 {} 启动失败]", getId(), e);
log.error("[start][IoT Modbus TCP Master 协议 {} 启动失败]", getId(), e);
// 启动失败时关闭资源
if (vertx != null) {
vertx.close();
@@ -151,9 +151,9 @@ public class IotModbusTcpProtocol implements IotProtocol {
// 1. 停止下行消息订阅者
try {
downstreamSubscriber.stop();
log.info("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者已停止]", getId());
log.info("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者停止失败]", getId(), e);
log.error("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
// 2.1 取消配置刷新定时器
@@ -163,22 +163,22 @@ public class IotModbusTcpProtocol implements IotProtocol {
}
// 2.2 停止轮询调度器
pollScheduler.stopAll();
log.info("[stop][IoT Modbus TCP 协议 {} 轮询调度器已停止]", getId());
log.info("[stop][IoT Modbus TCP Master 协议 {} 轮询调度器已停止]", getId());
// 2.3 关闭所有连接
connectionManager.closeAll();
log.info("[stop][IoT Modbus TCP 协议 {} 连接管理器已关闭]", getId());
log.info("[stop][IoT Modbus TCP Master 协议 {} 连接管理器已关闭]", getId());
// 3. 关闭 Vert.x 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT Modbus TCP 协议 {} Vertx 已关闭]", getId());
log.info("[stop][IoT Modbus TCP Master 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP 协议 {} Vertx 关闭失败]", getId(), e);
log.error("[stop][IoT Modbus TCP Master 协议 {} Vertx 关闭失败]", getId(), e);
}
}
running = false;
log.info("[stop][IoT Modbus TCP 协议 {} 已停止]", getId());
log.info("[stop][IoT Modbus TCP Master 协议 {} 已停止]", getId());
}
/**

View File

@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
import com.ghgande.j2mod.modbus.msg.*;
import com.ghgande.j2mod.modbus.procimg.InputRegister;
@@ -12,6 +12,7 @@ import com.ghgande.j2mod.modbus.util.BitVector;
import io.vertx.core.Future;
import lombok.extern.slf4j.Slf4j;
// TODO @AI感觉它更像一个工具类但是名字叫 client 很奇怪
/**
* IoT Modbus TCP 客户端
* <p>

View File

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
@@ -6,10 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -1,13 +1,14 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
// TODO @AI是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java
/**
* IoT Modbus TCP 下行消息订阅器订阅消息总线的下行消息并转发给处理器
*
@@ -17,7 +18,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotModbusTcpProtocol protocol;
private final IotModbusTcpMasterProtocol protocol;
private final IotModbusTcpDownstreamHandler downstreamHandler;
private final IotMessageBus messageBus;
@@ -26,7 +27,7 @@ public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber<Io
*/
public void start() {
messageBus.register(this);
log.info("[start][Modbus TCP 下行消息订阅器已启动, topic={}]", getTopic());
log.info("[start][Modbus TCP Master 下行消息订阅器已启动, topic={}]", getTopic());
}
/**
@@ -34,7 +35,7 @@ public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber<Io
*/
public void stop() {
messageBus.unregister(this);
log.info("[stop][Modbus TCP 下行消息订阅器已停止]");
log.info("[stop][Modbus TCP Master 下行消息订阅器已停止]");
}
@Override

View File

@@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
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.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;

View File

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;

View File

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import com.ghgande.j2mod.modbus.net.TCPMasterConnection;

View File

@@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream.IotModbusTcpUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
import io.vertx.core.Vertx;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentHashMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
// TODO @AI类的命名上要体现上 master其它类似 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster 也要
/**
* IoT Modbus TCP 轮询调度器管理点位的轮询定时器调度读取任务并上报结果
*

View File

@@ -1,9 +1,9 @@
/**
* Modbus TCP 协议实现包
* Modbus TCP Master 协议实现包
* <p>
* 提供基于 j2mod Modbus TCP 主站Master功能支持
* 1. 定时轮询 Modbus 从站设备数据
* 2. 下发属性设置命令到从站设备
* 3. 数据格式转换寄存器值 物模型属性值
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT Modbus TCP Slave 协议配置
*
* @author 芋道源码
*/
@Data
public class IotModbusTcpSlaveConfig {
/**
* 配置刷新间隔(秒)
*/
@NotNull(message = "配置刷新间隔不能为空")
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
private Integer configRefreshInterval = 30;
/**
* 自定义功能码(用于认证等扩展交互)
* Modbus 协议保留 65-72 给用户自定义,默认 65
*/
@NotNull(message = "自定义功能码不能为空")
@Min(value = 65, message = "自定义功能码不能小于 65")
// TODO @AI搞个范围
private Integer customFunctionCode = 65;
/**
* Pending Request 超时时间(毫秒)
*/
@NotNull(message = "请求超时时间不能为空")
private Integer requestTimeout = 5000;
/**
* Pending Request 清理间隔(毫秒)
*/
@NotNull(message = "请求清理间隔不能为空")
private Integer requestCleanupInterval = 10000;
}

View File

@@ -0,0 +1,372 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import io.vertx.core.net.NetServer;
import io.vertx.core.net.NetServerOptions;
import io.vertx.core.net.NetSocket;
import io.vertx.core.parsetools.RecordParser;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
// TODO @AI不用主动上报
/**
* IoT 网关 Modbus TCP Slave 协议
* <p>
* 作为 TCP Server 接收设备主动连接:
* 1. 设备通过自定义功能码FC 65发送认证请求
* 2. 认证成功后,根据设备配置的 mode 决定工作模式:
* - mode=1云端轮询网关主动发送 Modbus 读请求,设备响应
* - mode=2主动上报设备主动上报数据网关透传
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpSlaveProtocol implements IotProtocol {
/**
* 协议配置
*/
private final ProtocolProperties properties;
/**
* 服务器 ID用于消息追踪全局唯一
*/
@Getter
private final String serverId;
/**
* 运行状态
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x 实例
*/
private final Vertx vertx;
/**
* TCP Server
*/
private NetServer netServer;
/**
* 配置刷新定时器 ID
*/
private Long configRefreshTimerId;
/**
* Pending Request 清理定时器 ID
*/
private Long requestCleanupTimerId;
/**
* 未认证连接的帧格式缓存socket → 检测到的帧格式
*/
private final Map<NetSocket, IotModbusFrameFormatEnum> pendingFrameFormats = new ConcurrentHashMap<>();
// ========== 各组件 ==========
private final IotModbusTcpSlaveConfig slaveConfig;
private final IotModbusFrameCodec frameCodec;
private final IotModbusTcpSlaveConnectionManager connectionManager;
private final IotModbusTcpSlaveConfigCacheService configCacheService;
private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
private final IotModbusTcpSlaveUpstreamHandler upstreamHandler;
private final IotModbusTcpSlaveDownstreamHandler downstreamHandler;
private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber;
private final IotModbusTcpSlavePollScheduler pollScheduler;
public IotModbusTcpSlaveProtocol(ProtocolProperties properties) {
this.slaveConfig = properties.getModbusTcpSlave();
Assert.notNull(slaveConfig, "Modbus TCP Slave 协议配置modbusTcpSlave不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化 Vertx
this.vertx = Vertx.vertx();
// 初始化 Manager
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.connectionManager = new IotModbusTcpSlaveConnectionManager();
this.configCacheService = new IotModbusTcpSlaveConfigCacheService(deviceApi);
this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager();
// 初始化帧编解码器
this.frameCodec = new IotModbusFrameCodec(slaveConfig.getCustomFunctionCode());
// 初始化 Handler
IotModbusDataConverter dataConverter = new IotModbusDataConverter();
IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler(
deviceApi, messageService, dataConverter, frameCodec,
connectionManager, configCacheService, pendingRequestManager, serverId);
this.downstreamHandler = new IotModbusTcpSlaveDownstreamHandler(
connectionManager, configCacheService, dataConverter, frameCodec);
// 初始化轮询调度器
this.pollScheduler = new IotModbusTcpSlavePollScheduler(
vertx, connectionManager, frameCodec, pendingRequestManager,
slaveConfig.getRequestTimeout());
// 设置认证成功回调:启动轮询
// TODO @AI感觉直接去调用不用注册回调了更简洁
this.upstreamHandler.setOnAuthSuccess((deviceId, config) -> {
if (config.getMode() != null
&& config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) {
pollScheduler.updatePolling(config);
}
});
// 初始化下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber(
this, downstreamHandler, messageBus);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MODBUS_TCP_SLAVE;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT Modbus TCP Slave 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1.1 首次加载配置
refreshConfig();
// 1.2 启动配置刷新定时器
int refreshInterval = slaveConfig.getConfigRefreshInterval();
configRefreshTimerId = vertx.setPeriodic(
TimeUnit.SECONDS.toMillis(refreshInterval),
id -> refreshConfig());
// 2.1 启动 TCP Server
startTcpServer();
// 2.2 启动 PendingRequest 清理定时器
requestCleanupTimerId = vertx.setPeriodic(
slaveConfig.getRequestCleanupInterval(),
id -> pendingRequestManager.cleanupExpired());
running = true;
log.info("[start][IoT Modbus TCP Slave 协议 {} 启动成功, serverId={}, port={}]",
getId(), serverId, properties.getPort());
// 3. 启动下行消息订阅
downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e);
if (vertx != null) {
vertx.close();
}
// TODO @AI其它相关的 close
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
// 1. 停止下行消息订阅
try {
downstreamSubscriber.stop();
} catch (Exception e) {
log.error("[stop][下行消息订阅器停止失败]", e);
}
// 2.1 取消定时器
if (configRefreshTimerId != null) {
vertx.cancelTimer(configRefreshTimerId);
configRefreshTimerId = null;
}
if (requestCleanupTimerId != null) {
vertx.cancelTimer(requestCleanupTimerId);
requestCleanupTimerId = null;
}
// 2.2 停止轮询
pollScheduler.stopAll();
// 2.3 清理 PendingRequest
pendingRequestManager.clear();
// 2.3 关闭所有连接
connectionManager.closeAll();
// 2.4 关闭 TCP Server
if (netServer != null) {
try {
netServer.close().result();
log.info("[stop][TCP Server 已关闭]");
} catch (Exception e) {
log.error("[stop][TCP Server 关闭失败]", e);
}
}
// 3. 关闭 Vertx
if (vertx != null) {
try {
vertx.close().result();
} catch (Exception e) {
log.error("[stop][Vertx 关闭失败]", e);
}
}
running = false;
log.info("[stop][IoT Modbus TCP Slave 协议 {} 已停止]", getId());
}
/**
* 启动 TCP Server
*/
private void startTcpServer() {
// TODO @AIhost 一定要设置么?
// 1. 创建 TCP Server
NetServerOptions options = new NetServerOptions()
.setPort(properties.getPort())
.setHost("0.0.0.0");
netServer = vertx.createNetServer(options);
// 2. 设置连接处理器
netServer.connectHandler(this::handleConnection);
// TODO @AI是不是 sync 就好,不用 onSuccess/onFailure 了?感觉更简洁。失败,肯定就要抛出异常,结束初始化了!
netServer.listen()
.onSuccess(server -> log.info("[startTcpServer][TCP Server 启动成功, port={}]",
server.actualPort()))
.onFailure(e -> log.error("[startTcpServer][TCP Server 启动失败]", e));
}
/**
* 处理新连接
*/
private void handleConnection(NetSocket socket) {
log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress());
// 1.1 创建带帧格式检测的 RecordParser
// TODO @AI看看怎么从这个类里面拿出去让这个类的职责更单一
RecordParser parser = IotModbusRecordParserFactory.create(
slaveConfig.getCustomFunctionCode(),
// 完整帧回调
// TODO @AI感觉搞个独立的类稍微好点
frameBuffer -> {
byte[] frameBytes = frameBuffer.getBytes();
// 获取该连接的帧格式
ConnectionInfo connInfo = connectionManager.getConnectionInfo(socket);
IotModbusFrameFormatEnum frameFormat = connInfo != null ? connInfo.getFrameFormat() : null;
if (frameFormat == null) {
// 未认证的连接,使用首帧检测到的帧格式
frameFormat = pendingFrameFormats.get(socket);
}
if (frameFormat == null) {
log.warn("[handleConnection][帧格式未检测到, remoteAddress={}]", socket.remoteAddress());
return;
}
// 解码帧
IotModbusFrame frame = frameCodec.decodeResponse(frameBytes, frameFormat);
// 交给 UpstreamHandler 处理
upstreamHandler.handleFrame(socket, frame, frameFormat);
},
// 帧格式检测回调:保存到未认证缓存
detectedFormat -> {
// TODO @AI是不是不用缓存每次都探测因为一般 auth 首包后,基本也没探测的诉求了!
pendingFrameFormats.put(socket, detectedFormat);
// 如果连接已注册(不太可能在检测阶段),也更新
// TODO @AI是否非必须
connectionManager.setFrameFormat(socket, detectedFormat);
log.debug("[handleConnection][帧格式检测: {}, remoteAddress={}]",
detectedFormat, socket.remoteAddress());
}
);
// 1.2 设置数据处理器
socket.handler(parser);
// 2.1 连接关闭处理
socket.closeHandler(v -> {
pendingFrameFormats.remove(socket);
ConnectionInfo info = connectionManager.removeConnection(socket);
// TODO @AIif return 简化下;
if (info != null && info.getDeviceId() != null) {
pollScheduler.stopPolling(info.getDeviceId());
pendingRequestManager.removeDevice(info.getDeviceId());
log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]",
info.getDeviceId(), socket.remoteAddress());
} else {
log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress());
}
});
// 2.2 异常处理
socket.exceptionHandler(e -> {
log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e);
socket.close();
});
}
/**
* 刷新配置
*/
private synchronized void refreshConfig() {
try {
// 1. 从 biz 拉取最新配置
List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
// 2. 更新已连接设备的轮询任务(仅 mode=1
for (IotModbusDeviceConfigRespDTO config : configs) {
try {
if (config.getMode() != null
&& config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) {
// 只有已连接的设备才启动轮询
ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(config.getDeviceId());
if (connInfo != null) {
pollScheduler.updatePolling(config);
}
}
} catch (Exception e) {
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
}
}
// 3. 清理已删除设备的资源
configCacheService.cleanupRemovedDevices(configs, deviceId -> {
pollScheduler.stopPolling(deviceId);
pendingRequestManager.removeDevice(deviceId);
});
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
}
}
}

View File

@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* IoT Modbus 统一帧数据模型TCP/RTU 公用)
*
* @author 芋道源码
*/
@Data
@Accessors(chain = true)
public class IotModbusFrame {
/**
* 从站地址
*/
private int slaveId;
/**
* 功能码
*/
private int functionCode;
/**
* PDU 数据(不含 slaveId
*/
private byte[] pdu;
/**
* 事务标识符TCP 模式特有)
*
* // TODO @AI最好是 @某个类型独有;
*/
private Integer transactionId;
/**
* 是否异常响应
*/
private boolean exception;
// TODO @AI是不是要枚举一些异常另外是不是覆盖掉 exception因为只要判断有异常码是不是就可以了
/**
* 异常码(当 exception=true 时有效)
*/
private Integer exceptionCode;
/**
* 自定义功能码时的 JSON 字符串
*/
private String customData;
}

View File

@@ -0,0 +1,317 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
/**
* IoT Modbus 帧编解码器
* <p>
* 纯 Modbus 协议编解码,不处理 TCP 粘包(由 RecordParser 处理)。
* 支持 MODBUS_TCPMBAP和 MODBUS_RTUCRC16两种帧格式以及自定义功能码扩展。
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusFrameCodec {
private final int customFunctionCode;
public IotModbusFrameCodec(int customFunctionCode) {
this.customFunctionCode = customFunctionCode;
}
// ==================== 解码 ====================
/**
* 解码响应帧(拆包后的完整帧 byte[]
*
* @param data 完整帧字节数组
* @param format 帧格式
* @return 解码后的 IotModbusFrame
*/
public IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) {
if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
return decodeTcpResponse(data);
} else {
return decodeRtuResponse(data);
}
}
/**
* 解码 MODBUS_TCP 响应
* 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...]
*/
private IotModbusFrame decodeTcpResponse(byte[] data) {
if (data.length < 8) {
log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length);
return null;
}
ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
int transactionId = buf.getShort() & 0xFFFF;
buf.getShort(); // protocolId跳过// TODO @AI跳过原因最好写下
buf.getShort(); // length跳过// TODO @AI跳过原因最好写下
int slaveId = buf.get() & 0xFF;
int functionCode = buf.get() & 0xFF;
// 提取 PDU 数据(从 functionCode 之后到末尾)
byte[] pdu = new byte[data.length - 8];
System.arraycopy(data, 8, pdu, 0, pdu.length);
// 构建 IotModbusFrame
return buildFrame(slaveId, functionCode, pdu, transactionId);
}
/**
* 解码 MODBUS_RTU 响应
* 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)]
*/
private IotModbusFrame decodeRtuResponse(byte[] data) {
if (data.length < 4) {
log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length);
return null;
}
// 校验 CRC
if (!verifyCrc16(data)) {
log.warn("[decodeRtuResponse][CRC 校验失败]");
return null;
}
int slaveId = data[0] & 0xFF;
int functionCode = data[1] & 0xFF;
// PDU 数据(不含 slaveId、functionCode、CRC
byte[] pdu = new byte[data.length - 4];
System.arraycopy(data, 2, pdu, 0, pdu.length);
// 构建 IotModbusFrame
return buildFrame(slaveId, functionCode, pdu, null);
}
/**
* 构建 IotModbusFrame
*/
private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) {
IotModbusFrame frame = new IotModbusFrame()
.setSlaveId(slaveId)
.setFunctionCode(functionCode)
.setPdu(pdu)
.setTransactionId(transactionId);
// 异常响应
// TODO @AI0x80 看看是不是要枚举;
if ((functionCode & 0x80) != 0) {
frame.setException(true);
// TODO @AI0x7f 看看是不是要枚举;
frame.setFunctionCode(functionCode & 0x7F);
if (pdu.length >= 1) {
frame.setExceptionCode(pdu[0] & 0xFF);
}
return frame;
}
// 自定义功能码
if (functionCode == customFunctionCode) {
// data 区格式:[byteCount(1)] [JSON data(N)]
if (pdu.length >= 1) {
int byteCount = pdu[0] & 0xFF;
if (pdu.length >= 1 + byteCount) {
frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8));
}
}
}
return frame;
}
// ==================== 编码 ====================
/**
* 编码读请求
*
* @param slaveId 从站地址
* @param functionCode 功能码
* @param startAddress 起始寄存器地址
* @param quantity 寄存器数量
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用)
* @return 编码后的字节数组
*/
public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity,
IotModbusFrameFormatEnum format, int transactionId) {
// PDU: [FC(1)] [StartAddress(2)] [Quantity(2)]
byte[] pdu = new byte[5];
pdu[0] = (byte) functionCode;
pdu[1] = (byte) ((startAddress >> 8) & 0xFF);
pdu[2] = (byte) (startAddress & 0xFF);
pdu[3] = (byte) ((quantity >> 8) & 0xFF);
pdu[4] = (byte) (quantity & 0xFF);
return wrapFrame(slaveId, pdu, format, transactionId);
}
/**
* 编码写请求(单个寄存器 FC06 / 单个线圈 FC05
*
* @param slaveId 从站地址
* @param functionCode 功能码
* @param address 寄存器地址
* @param value 值
* @param format 帧格式
* @param transactionId 事务 ID
* @return 编码后的字节数组
*/
public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value,
IotModbusFrameFormatEnum format, int transactionId) {
// PDU: [FC(1)] [Address(2)] [Value(2)]
byte[] pdu = new byte[5];
pdu[0] = (byte) functionCode;
pdu[1] = (byte) ((address >> 8) & 0xFF);
pdu[2] = (byte) (address & 0xFF);
pdu[3] = (byte) ((value >> 8) & 0xFF);
pdu[4] = (byte) (value & 0xFF);
return wrapFrame(slaveId, pdu, format, transactionId);
}
/**
* 编码写多个寄存器请求FC16
*
* @param slaveId 从站地址
* @param address 起始地址
* @param values 值数组
* @param format 帧格式
* @param transactionId 事务 ID
* @return 编码后的字节数组
*/
public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values,
IotModbusFrameFormatEnum format, int transactionId) {
// PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)]
int quantity = values.length;
int byteCount = quantity * 2;
byte[] pdu = new byte[6 + byteCount];
pdu[0] = (byte) 16; // FC16
pdu[1] = (byte) ((address >> 8) & 0xFF);
pdu[2] = (byte) (address & 0xFF);
pdu[3] = (byte) ((quantity >> 8) & 0xFF);
pdu[4] = (byte) (quantity & 0xFF);
pdu[5] = (byte) byteCount;
for (int i = 0; i < quantity; i++) {
pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF);
}
return wrapFrame(slaveId, pdu, format, transactionId);
}
/**
* 编码自定义功能码帧(认证响应等)
*
* @param slaveId 从站地址
* @param jsonData JSON 数据
* @param format 帧格式
* @param transactionId 事务 ID
* @return 编码后的字节数组
*/
public byte[] encodeCustomFrame(int slaveId, String jsonData,
IotModbusFrameFormatEnum format, int transactionId) {
byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
// PDU: [FC(1)] [ByteCount(1)] [JSON data(N)]
byte[] pdu = new byte[2 + jsonBytes.length];
pdu[0] = (byte) customFunctionCode;
pdu[1] = (byte) jsonBytes.length;
System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length);
return wrapFrame(slaveId, pdu, format, transactionId);
}
// ==================== 帧封装 ====================
/**
* 将 PDU 封装为完整帧
*
* @param slaveId 从站地址
* @param pdu PDU 数据(含 functionCode
* @param format 帧格式
* @param transactionId 事务 IDTCP 模式下使用)
* @return 完整帧字节数组
*/
private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) {
if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
return wrapTcpFrame(slaveId, pdu, transactionId);
} else {
return wrapRtuFrame(slaveId, pdu);
}
}
/**
* 封装 MODBUS_TCP 帧
* [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...]
*/
private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) {
int length = 1 + pdu.length; // UnitId + PDU
byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU
// MBAP Header
frame[0] = (byte) ((transactionId >> 8) & 0xFF);
frame[1] = (byte) (transactionId & 0xFF);
frame[2] = 0; // Protocol ID high
frame[3] = 0; // Protocol ID low
frame[4] = (byte) ((length >> 8) & 0xFF);
frame[5] = (byte) (length & 0xFF);
// Unit ID
frame[6] = (byte) slaveId;
// PDU
System.arraycopy(pdu, 0, frame, 7, pdu.length);
return frame;
}
/**
* 封装 MODBUS_RTU 帧
* [SlaveId(1)] [PDU...] [CRC(2)]
*/
private byte[] wrapRtuFrame(int slaveId, byte[] pdu) {
byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC
frame[0] = (byte) slaveId;
System.arraycopy(pdu, 0, frame, 1, pdu.length);
// 计算并追加 CRC16
int crc = calculateCrc16(frame, frame.length - 2);
frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low
frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
return frame;
}
// ==================== CRC16 工具 ====================
// TODO @AIhutool 等,有没工具类可以用
/**
* 计算 CRC-16/MODBUS
*
* @param data 数据
* @param length 计算长度
* @return CRC16 值
*/
public static int calculateCrc16(byte[] data, int length) {
int crc = 0xFFFF;
for (int i = 0; i < length; i++) {
crc ^= (data[i] & 0xFF);
for (int j = 0; j < 8; j++) {
if ((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
// TODO @AIhutool 等,有没工具类可以用
/**
* 校验 CRC16
*/
private boolean verifyCrc16(byte[] data) {
if (data.length < 3) {
return false;
}
int computed = calculateCrc16(data, data.length - 2);
int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
return computed == received;
}
}

View File

@@ -0,0 +1,362 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Consumer;
// TODO @AI看看是不是不要搞成 factory而是直接 new可以一起讨论下
/**
* IoT Modbus RecordParser 工厂
* <p>
* 创建带自动帧格式检测的 RecordParser
* 1. 首帧检测:读前 6 字节,判断 MODBUS_TCPProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU
* 2. 检测后自动切换到对应的拆包模式
* - MODBUS_TCP两阶段 RecordParserMBAP length 字段驱动)
* - MODBUS_RTU功能码驱动的状态机
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusRecordParserFactory {
/**
* 创建带自动帧格式检测的 RecordParser
*
* @param customFunctionCode 自定义功能码
* @param frameHandler 完整帧回调
* @param onFormatDetected 帧格式检测回调
* @return RecordParser 实例
*/
public static RecordParser create(int customFunctionCode,
Handler<Buffer> frameHandler,
Consumer<IotModbusFrameFormatEnum> onFormatDetected) {
// 先创建一个 RecordParser使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测
// TODO @AI最小需要 6 个字节么?有可能更小的情况下,就探测出来?!
RecordParser parser = RecordParser.newFixed(6);
parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, onFormatDetected));
return parser;
}
/**
* 帧格式检测阶段 Handler
*/
@SuppressWarnings("ClassCanBeRecord")
private static class DetectPhaseHandler implements Handler<Buffer> {
private final RecordParser parser;
private final int customFunctionCode;
private final Handler<Buffer> frameHandler;
private final Consumer<IotModbusFrameFormatEnum> onFormatDetected;
// TODO @AI简化构造方法使用 lombok
DetectPhaseHandler(RecordParser parser, int customFunctionCode,
Handler<Buffer> frameHandler,
Consumer<IotModbusFrameFormatEnum> onFormatDetected) {
this.parser = parser;
this.customFunctionCode = customFunctionCode;
this.frameHandler = frameHandler;
this.onFormatDetected = onFormatDetected;
}
@Override
public void handle(Buffer buffer) {
byte[] header = buffer.getBytes();
// 检测byte[2]==0x00 && byte[3]==0x00 && 1<=length<=253
int protocolId = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF);
int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF);
if (protocolId == 0x0000 && length >= 1 && length <= 253) {
// MODBUS_TCP
log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]");
onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_TCP);
// 切换到 TCP 拆包模式,处理当前首帧
TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler);
parser.handler(tcpHandler);
// 当前 header 是 MBAP 的前 6 字节,需要继续读 length 字节
tcpHandler.handleMbapHeader(header, length);
} else {
// MODBUS_RTU
log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]");
onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_RTU);
// 切换到 RTU 拆包模式,处理当前首帧
RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, customFunctionCode, frameHandler);
parser.handler(rtuHandler);
// 当前 header 包含前 6 字节slaveId + FC + 部分数据),需要拼接处理
rtuHandler.handleInitialBytes(header);
}
}
}
/**
* MODBUS_TCP 拆包 Handler两阶段 RecordParser
* Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length
* Phase 2: fixedSizeMode(length) → 读 unitId + PDU
*/
private static class TcpFrameHandler implements Handler<Buffer> {
private final RecordParser parser;
private final Handler<Buffer> frameHandler;
private byte[] mbapHeader;
private boolean waitingForBody = false;
// TODO @AIlombok
TcpFrameHandler(RecordParser parser, Handler<Buffer> frameHandler) {
this.parser = parser;
this.frameHandler = frameHandler;
}
/**
* 处理首帧的 MBAP 头
*/
void handleMbapHeader(byte[] header, int length) {
this.mbapHeader = header;
this.waitingForBody = true;
parser.fixedSizeMode(length);
}
@Override
public void handle(Buffer buffer) {
if (waitingForBody) {
// Phase 2: 收到 bodyunitId + PDU
byte[] body = buffer.getBytes();
// 拼接完整帧MBAP(6) + body
Buffer frame = Buffer.buffer(mbapHeader.length + body.length);
frame.appendBytes(mbapHeader);
frame.appendBytes(body);
frameHandler.handle(frame);
// 切回 Phase 1
waitingForBody = false;
mbapHeader = null;
parser.fixedSizeMode(6);
} else {
// Phase 1: 收到 MBAP 头 6 字节
byte[] header = buffer.getBytes();
int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF);
if (length < 1 || length > 253) {
log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length);
parser.fixedSizeMode(6);
return;
}
this.mbapHeader = header;
this.waitingForBody = true;
parser.fixedSizeMode(length);
}
}
}
/**
* MODBUS_RTU 拆包 Handler功能码驱动的状态机
* <p>
* 状态机流程:
* Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode
* Phase 2: 根据 functionCode 确定剩余长度:
* - 异常响应 (FC & 0x80)fixedSizeMode(3) → exceptionCode(1) + CRC(2)
* - 自定义 FC / FC01-04 响应fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
* - FC05/06 响应fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
* - FC15/16 响应fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
*/
private static class RtuFrameHandler implements Handler<Buffer> {
private static final int STATE_HEADER = 0;
private static final int STATE_EXCEPTION_BODY = 1;
private static final int STATE_READ_BYTE_COUNT = 2;
private static final int STATE_READ_DATA = 3;
private static final int STATE_WRITE_BODY = 4;
private final RecordParser parser;
private final int customFunctionCode;
private final Handler<Buffer> frameHandler;
private int state = STATE_HEADER;
private byte slaveId;
private byte functionCode;
private byte byteCount;
// TODO @AIlombok
RtuFrameHandler(RecordParser parser, int customFunctionCode, Handler<Buffer> frameHandler) {
this.parser = parser;
this.customFunctionCode = customFunctionCode;
this.frameHandler = frameHandler;
}
/**
* 处理首帧检测阶段传来的初始 6 字节
* 由于 RTU 首帧跳过了格式检测,我们需要拼接处理
*/
void handleInitialBytes(byte[] initialBytes) {
// initialBytes 包含 6 字节:[slaveId][FC][...4 bytes...]
this.slaveId = initialBytes[0];
this.functionCode = initialBytes[1];
int fc = functionCode & 0xFF;
// 根据功能码,确定还需要多少字节
if ((fc & 0x80) != 0) {
// 异常响应:还需要 exceptionCode(1) + CRC(2) = 3 字节
// 我们已经有 4 字节剩余initialBytes[2..5]),足够
// 拼接完整帧并交付
// 完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5
Buffer frame = Buffer.buffer(5);
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendBytes(initialBytes, 2, 3); // exceptionCode + CRC
frameHandler.handle(frame);
// 剩余 1 字节需要留给下一帧,但 RecordParser 不支持回推
// 简化处理:重置状态,开始读下一帧
resetToHeader();
} else if (isReadResponse(fc) || fc == customFunctionCode) {
// 读响应或自定义 FCinitialBytes[2] = byteCount
this.byteCount = initialBytes[2];
int bc = byteCount & 0xFF;
// 已有数据initialBytes[3..5] = 3 字节
// 还需byteCount + CRC(2) - 3 字节已有
int remaining = bc + 2 - 3;
if (remaining <= 0) {
// 数据已足够,组装完整帧
int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC
Buffer frame = Buffer.buffer(totalLen);
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendByte(byteCount);
frame.appendBytes(initialBytes, 3, bc + 2); // data + CRC
frameHandler.handle(frame);
resetToHeader();
} else {
// 需要继续读
state = STATE_READ_DATA;
// 保存已有数据片段
parser.fixedSizeMode(remaining);
// 在 handle() 中需要拼接 initialBytes[3..5] + 新读取的数据
// 为了简化,我们用一个 Buffer 暂存
this.pendingData = Buffer.buffer();
this.pendingData.appendBytes(initialBytes, 3, 3);
this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC
}
} else if (isWriteResponse(fc)) {
// 写响应FC05/06/15/16总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8
// 已有 6 字节,还需 2 字节
state = STATE_WRITE_BODY;
this.pendingData = Buffer.buffer();
this.pendingData.appendBytes(initialBytes, 2, 4); // 4 bytes already
parser.fixedSizeMode(2); // need 2 more bytes (CRC)
} else {
log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
resetToHeader();
}
}
private Buffer pendingData;
private int expectedDataLen;
@Override
public void handle(Buffer buffer) {
switch (state) {
case STATE_HEADER:
handleHeader(buffer);
break;
case STATE_EXCEPTION_BODY:
handleExceptionBody(buffer);
break;
case STATE_READ_BYTE_COUNT:
handleReadByteCount(buffer);
break;
case STATE_READ_DATA:
handleReadData(buffer);
break;
case STATE_WRITE_BODY:
handleWriteBody(buffer);
break;
default:
resetToHeader();
}
}
private void handleHeader(Buffer buffer) {
byte[] header = buffer.getBytes();
this.slaveId = header[0];
this.functionCode = header[1];
int fc = functionCode & 0xFF;
if ((fc & 0x80) != 0) {
// 异常响应
state = STATE_EXCEPTION_BODY;
parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
} else if (isReadResponse(fc) || fc == customFunctionCode) {
// 读响应或自定义 FC
state = STATE_READ_BYTE_COUNT;
parser.fixedSizeMode(1); // byteCount
} else if (isWriteResponse(fc)) {
// 写响应
state = STATE_WRITE_BODY;
pendingData = Buffer.buffer();
parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2)
} else {
log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
resetToHeader();
}
}
private void handleExceptionBody(Buffer buffer) {
// buffer = exceptionCode(1) + CRC(2)
Buffer frame = Buffer.buffer();
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendBuffer(buffer);
frameHandler.handle(frame);
resetToHeader();
}
private void handleReadByteCount(Buffer buffer) {
this.byteCount = buffer.getByte(0);
int bc = byteCount & 0xFF;
state = STATE_READ_DATA;
pendingData = Buffer.buffer();
expectedDataLen = bc + 2; // data(bc) + CRC(2)
parser.fixedSizeMode(expectedDataLen);
}
private void handleReadData(Buffer buffer) {
pendingData.appendBuffer(buffer);
if (pendingData.length() >= expectedDataLen) {
// 组装完整帧
Buffer frame = Buffer.buffer();
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendByte(byteCount);
frame.appendBuffer(pendingData);
frameHandler.handle(frame);
resetToHeader();
}
// 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode
}
private void handleWriteBody(Buffer buffer) {
pendingData.appendBuffer(buffer);
// 完整帧
Buffer frame = Buffer.buffer();
frame.appendByte(slaveId);
frame.appendByte(functionCode);
frame.appendBuffer(pendingData);
frameHandler.handle(frame);
resetToHeader();
}
private void resetToHeader() {
state = STATE_HEADER;
pendingData = null;
parser.fixedSizeMode(2); // slaveId + FC
}
private boolean isReadResponse(int fc) {
return fc >= 1 && fc <= 4;
}
private boolean isWriteResponse(int fc) {
return fc == 5 || fc == 6 || fc == 15 || fc == 16;
}
}
}

View File

@@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
import lombok.extern.slf4j.Slf4j;
/**
* IoT Modbus 响应值提取器
* <p>
* 从解码后的 IotModbusFrame 中提取寄存器值,用于后续的点位翻译。
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusResponseParser {
/**
* 从帧中提取寄存器值FC01-04 读响应)
*
* @param frame 解码后的 Modbus 帧
* @return 寄存器值数组int[]),失败返回 null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static int[] extractValues(IotModbusFrame frame) {
if (frame == null || frame.isException()) {
return null;
}
byte[] pdu = frame.getPdu();
if (pdu == null || pdu.length < 1) {
return null;
}
// TODO @AIjmodbus 看看有没可以复用的枚举类
int functionCode = frame.getFunctionCode();
switch (functionCode) {
case 1: // Read Coils
case 2: // Read Discrete Inputs
return extractCoilValues(pdu);
case 3: // Read Holding Registers
case 4: // Read Input Registers
return extractRegisterValues(pdu);
default:
log.warn("[extractValues][不支持的功能码: {}]", functionCode);
return null;
}
}
/**
* 提取线圈/离散输入值
* PDU 格式FC01/02 响应):[ByteCount(1)] [CoilStatus(N)]
*/
private static int[] extractCoilValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int bitCount = byteCount * 8;
int[] values = new int[bitCount];
for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
}
return values;
}
/**
* 提取寄存器值
* PDU 格式FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)]
*/
private static int[] extractRegisterValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int registerCount = byteCount / 2;
int[] values = new int[registerCount];
for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
}
return values;
}
}

View File

@@ -0,0 +1,152 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
// TODO @AI看看能不能和 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java 有一些复用逻辑;
/**
* IoT Modbus TCP Slave 下行消息处理器
* <p>
* 负责:
* 1. 处理下行消息(如属性设置 thing.service.property.set
* 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpSlaveDownstreamHandler {
private final IotModbusTcpSlaveConnectionManager connectionManager;
private final IotModbusTcpSlaveConfigCacheService configCacheService;
private final IotModbusDataConverter dataConverter;
private final IotModbusFrameCodec frameCodec;
/**
* TCP 事务 ID 自增器
*/
private final AtomicInteger transactionIdCounter = new AtomicInteger(0);
/**
* 处理下行消息
*/
@SuppressWarnings("unchecked")
public void handle(IotDeviceMessage message) {
// 1.1 检查是否是属性设置消息
if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) {
log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod());
return;
}
// 1.2 获取设备配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
if (config == null) {
log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
return;
}
// 1.3 获取连接信息
ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
if (connInfo == null) {
log.warn("[handle][设备 {} 没有连接]", message.getDeviceId());
return;
}
// 2. 解析属性值并写入
Object params = message.getParams();
if (!(params instanceof Map)) {
log.warn("[handle][params 不是 Map 类型: {}]", params);
return;
}
Map<String, Object> propertyMap = (Map<String, Object>) params;
for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
String identifier = entry.getKey();
Object value = entry.getValue();
// 2.1 查找对应的点位配置
IotModbusPointRespDTO point = findPoint(config, identifier);
if (point == null) {
log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
continue;
}
// 2.2 检查是否支持写操作
if (!isWritable(point.getFunctionCode())) {
log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
continue;
}
// 2.3 执行写入
writeProperty(config.getDeviceId(), connInfo, point, value);
}
}
/**
* 写入属性值
*/
private void writeProperty(Long deviceId, ConnectionInfo connInfo,
IotModbusPointRespDTO point, Object value) {
// 1. 转换属性值为原始值
int[] rawValues = dataConverter.convertToRawValues(value, point);
// 2. 确定帧格式和事务 ID
IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
if (frameFormat == null) {
frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP;
}
int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF;
int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
// 3. 编码写请求
byte[] data;
IotModbusFunctionCodeEnum fcEnum = IotModbusFunctionCodeEnum.valueOf(point.getFunctionCode());
if (fcEnum == null) {
log.warn("[writeProperty][未知功能码: {}]", point.getFunctionCode());
return;
}
if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) {
// 单个值使用单写功能码FC05/FC06
data = frameCodec.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(),
point.getRegisterAddress(), rawValues[0], frameFormat, transactionId);
} else if (fcEnum.getWriteMultipleCode() != null) {
// 多个值使用多写功能码FC15/FC16
data = frameCodec.encodeWriteMultipleRegistersRequest(slaveId,
point.getRegisterAddress(), rawValues, frameFormat, transactionId);
} else {
log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier());
return;
}
// 4. 发送
connectionManager.sendToDevice(deviceId, data);
log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
deviceId, point.getIdentifier(), value);
}
/**
* 查找点位配置
*/
private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
}
/**
* 检查功能码是否支持写操作
*/
private boolean isWritable(Integer functionCode) {
IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode);
return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable());
}
}

View File

@@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
// TODO @AI是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java
/**
* IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpSlaveDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotModbusTcpSlaveProtocol protocol;
private final IotModbusTcpSlaveDownstreamHandler downstreamHandler;
private final IotMessageBus messageBus;
/**
* 启动订阅
*/
public void start() {
messageBus.register(this);
log.info("[start][Modbus TCP Slave 下行消息订阅器已启动, topic={}]", getTopic());
}
/**
* 停止订阅
*/
public void stop() {
messageBus.unregister(this);
log.info("[stop][Modbus TCP Slave 下行消息订阅器已停止]");
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
return getTopic(); // 点对点消费
}
@Override
public void onMessage(IotDeviceMessage message) {
log.debug("[onMessage][收到下行消息: {}]", message);
try {
downstreamHandler.handle(message);
} catch (Exception e) {
log.error("[onMessage][处理下行消息失败]", e);
}
}
}

View File

@@ -0,0 +1,312 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusResponseParser;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.net.NetSocket;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.function.BiConsumer;
// TODO @AI逻辑有点多看看是不是分区域
/**
* IoT Modbus TCP Slave 上行数据处理器
* <p>
* 处理:
* 1. 自定义 FC 认证
* 2. 轮询响应mode=1→ 点位翻译 → thing.property.post
* 3. 主动上报mode=2→ 透传 property.report TODO @AI这种模式应该不用支持因为主动上报都走标准的 tcp 即可;
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpSlaveUpstreamHandler {
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService messageService;
private final IotModbusDataConverter dataConverter;
private final IotModbusFrameCodec frameCodec;
private final IotModbusTcpSlaveConnectionManager connectionManager;
private final IotModbusTcpSlaveConfigCacheService configCacheService;
private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
private final String serverId;
/**
* 认证成功回调:(deviceId, config) → 启动轮询等
*/
@Setter
private BiConsumer<Long, IotModbusDeviceConfigRespDTO> onAuthSuccess;
public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi,
IotDeviceMessageService messageService,
IotModbusDataConverter dataConverter,
IotModbusFrameCodec frameCodec,
IotModbusTcpSlaveConnectionManager connectionManager,
IotModbusTcpSlaveConfigCacheService configCacheService,
IotModbusTcpSlavePendingRequestManager pendingRequestManager,
String serverId) {
this.deviceApi = deviceApi;
this.messageService = messageService;
this.dataConverter = dataConverter;
this.frameCodec = frameCodec;
this.connectionManager = connectionManager;
this.configCacheService = configCacheService;
this.pendingRequestManager = pendingRequestManager;
this.serverId = serverId;
}
/**
* 处理帧
*/
public void handleFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
if (frame == null) {
return;
}
// 1.1 自定义功能码(认证等扩展)
if (StrUtil.isNotEmpty(frame.getCustomData())) {
handleCustomFrame(socket, frame, frameFormat);
return;
}
// 1.2 异常响应
if (frame.isException()) {
// TODO @AI这种需要返回一个结果给 modbus client
log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]",
frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode());
return;
}
// 1.3 未认证连接,丢弃
if (!connectionManager.isAuthenticated(socket)) {
// TODO @AI这种需要返回一个结果给 modbus client
log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress());
return;
}
// TODO @AI获取不到看看要不要也打个告警然后
// 2. 标准 Modbus 响应
ConnectionInfo info = connectionManager.getConnectionInfo(socket);
if (info == null) {
return;
}
// TODO @AI可以断言下必须是云端轮询
if (info.getMode() != null && info.getMode().equals(IotModbusModeEnum.ACTIVE_REPORT.getMode())) {
// mode=2主动上报透传
handleActiveReport(info, frame);
} else {
// mode=1云端轮询匹配 PendingRequest
handlePollingResponse(info, frame, frameFormat);
}
}
/**
* 处理自定义功能码帧
*/
private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
try {
// TODO @AI直接使用 JsonUtils 去解析出 IotDeviceMessage
JSONObject json = JSONUtil.parseObj(frame.getCustomData());
String method = json.getStr("method");
// TODO @AI method 枚举下;
if ("auth".equals(method)) {
handleAuth(socket, frame, json, frameFormat);
return;
}
// TODO @AI把 frame 都打印下;
log.warn("[handleCustomFrame][未知 method: {}]", method);
} catch (Exception e) {
// TODO @AI各种情况的翻译看看怎么弄比较合适是不是要用 fc 自定义的 callback 下?
log.error("[handleCustomFrame][解析自定义 FC 数据失败]", e);
}
}
/**
* 处理认证请求
*/
private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json,
IotModbusFrameFormatEnum frameFormat) {
// TODO @AI参数为空的校验
JSONObject params = json.getJSONObject("params");
if (params == null) {
sendAuthResponse(socket, frame, frameFormat, 1, "params 为空");
return;
}
// TODO @AI参数判空
String clientId = params.getStr("clientId");
String username = params.getStr("username");
String password = params.getStr("password");
try {
// 1. 调用认证 API
IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password);
CommonResult<Boolean> authResult = deviceApi.authDevice(authReq);
// TODO @AI应该不用 close 吧?!
// TODO @AIBooleanUtils.isFalse
if (authResult == null || !authResult.isSuccess() || !Boolean.TRUE.equals(authResult.getData())) {
log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username);
sendAuthResponse(socket, frame, frameFormat, 1, "认证失败");
socket.close();
return;
}
// 2. 认证成功,查找设备配置(通过 username 作为 deviceName 查找)
// TODO 根据实际的认证模型优化查找逻辑
// TODO @AI通过 device
IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password);
if (config == null) {
// 退而求其次,遍历缓存查找
log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username);
}
// 2.2 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
Assert.notNull(deviceInfo, "解析设备信息失败");
// 2.3 获取设备信息
// TODO @AI这里要优化下不要通过 spring 这样注入;
IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notNull(device, "设备不存在");
// TODO @AI校验 frameFormat 是否一致;不一致,连接也失败;
// 3. 注册连接
ConnectionInfo connectionInfo = new ConnectionInfo()
.setDeviceId(device.getId())
.setSlaveId(frame.getSlaveId())
.setFrameFormat(frameFormat)
.setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode());
if (config != null) {
connectionInfo.setDeviceId(config.getDeviceId())
.setProductKey(config.getProductKey())
.setDeviceName(config.getDeviceName());
}
connectionManager.registerConnection(socket, connectionInfo);
// 4. 发送认证成功响应
sendAuthResponse(socket, frame, frameFormat, 0, "success");
log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId,
config != null ? config.getDeviceId() : "unknown");
// 5. 回调:启动轮询等
// TODO @AI是不是不要 callback而是主动调用
if (onAuthSuccess != null && config != null) {
onAuthSuccess.accept(config.getDeviceId(), config);
}
} catch (Exception e) {
log.error("[handleAuth][认证异常]", e);
sendAuthResponse(socket, frame, frameFormat, 1, "认证异常");
socket.close();
}
}
/**
* 发送认证响应
*/
private void sendAuthResponse(NetSocket socket, IotModbusFrame frame,
IotModbusFrameFormatEnum frameFormat,
int code, String message) {
// TODO @AI不一定用 auth response而是 custom
JSONObject resp = new JSONObject();
resp.set("method", "auth");
resp.set("code", code);
resp.set("message", message);
byte[] data = frameCodec.encodeCustomFrame(frame.getSlaveId(), resp.toString(),
frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0);
connectionManager.sendToSocket(socket, data);
}
/**
* 处理轮询响应mode=1
*/
private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame,
IotModbusFrameFormatEnum frameFormat) {
// 1.1 匹配 PendingRequest
PendingRequest request = pendingRequestManager.matchResponse(
info.getDeviceId(), frame, frameFormat);
if (request == null) {
log.debug("[handlePollingResponse][未匹配到 PendingRequest, deviceId={}, FC={}]",
info.getDeviceId(), frame.getFunctionCode());
return;
}
// 1.2 提取寄存器值
int[] rawValues = IotModbusResponseParser.extractValues(frame);
if (rawValues == null) {
log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]",
info.getDeviceId(), request.getIdentifier());
return;
}
// 1.3 查找点位配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId());
if (config == null || config.getPoints() == null) {
return;
}
// TODO @AIfindone arrayUtil
var point = config.getPoints().stream()
.filter(p -> p.getId().equals(request.getPointId()))
.findFirst().orElse(null);
if (point == null) {
return;
}
// TODO @AI拆成 2.1、2.2
// 4. 点位翻译 → 上报
Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point);
Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
IotDeviceMessage message = IotDeviceMessage.requestOf(
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId);
log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]",
info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue);
}
// TODO @AI不需要这个逻辑
/**
* 处理主动上报mode=2
* 设备直接上报 property.report 格式:{propertyId: value},不做点位翻译
*/
@SuppressWarnings("unchecked")
private void handleActiveReport(ConnectionInfo info, IotModbusFrame frame) {
// mode=2 下设备上报标准 Modbus 帧,但由于没有点位翻译,
// 这里暂时将原始寄存器值以 FC+地址 为 key 上报
int[] rawValues = IotModbusResponseParser.extractValues(frame);
if (rawValues == null) {
return;
}
// 简单上报:以 "register_FC{fc}" 作为属性名
String propertyKey = "register_FC" + frame.getFunctionCode();
Map<String, Object> params = MapUtil.of(propertyKey, rawValues);
IotDeviceMessage message = IotDeviceMessage.requestOf(
IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId);
log.debug("[handleActiveReport][设备={}, FC={}, 原始值={}]",
info.getDeviceId(), frame.getFunctionCode(), rawValues);
}
}

View File

@@ -0,0 +1,172 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
// TODO @AI和 IotModbusTcpConfigCacheService 基本一致?!
/**
* IoT Modbus TCP Slave 配置缓存服务
* <p>
* 负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpSlaveConfigCacheService {
private final IotDeviceCommonApi deviceApi;
/**
* 配置缓存deviceId -> 配置
*/
private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
// TODO @AI它的 diff 算法,是不是不用和 IotModbusTcpConfigCacheService 完全一致更多是1首次连接时查找2断开连接移除3定时轮询更新
/**
* 已知的设备 ID 集合
*/
private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
/**
* 刷新配置
*
* @return 最新的配置列表
*/
public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
try {
// 1. 从远程获取配置
// TODO @AI需要过滤下只查找连接的设备列表并且只有主动轮询的才会处理方法名应该是 List 结尾;
CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs();
if (result == null || !result.isSuccess() || result.getData() == null) {
log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result);
return new ArrayList<>(configCache.values());
}
List<IotModbusDeviceConfigRespDTO> configs = new ArrayList<>(result.getData());
// 2. 追加 Mock 测试数据(一次性测试用途)
// TODO @芋艿:测试完成后移除
configs.addAll(buildMockConfigs());
// 3. 更新缓存
for (IotModbusDeviceConfigRespDTO config : configs) {
configCache.put(config.getDeviceId(), config);
}
return configs;
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
return new ArrayList<>(configCache.values());
}
}
/**
* 构建 Mock 测试配置数据(一次性测试用途)
*
* 设备PRODUCT_KEY=4aymZgOTOOCrDKRT, DEVICE_NAME=small
* 点位temperatureFC03, 地址 0、humidityFC03, 地址 1
*
* TODO @芋艿:测试完成后移除
*/
private List<IotModbusDeviceConfigRespDTO> buildMockConfigs() {
IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO();
config.setDeviceId(1L);
config.setProductKey("4aymZgOTOOCrDKRT");
config.setDeviceName("small");
config.setSlaveId(1);
config.setMode(1); // 云端轮询
config.setFrameFormat("modbus_tcp");
// 点位列表
List<IotModbusPointRespDTO> points = new ArrayList<>();
// 点位 1温度 - 保持寄存器 FC03, 地址 0, 1 个寄存器, INT16
IotModbusPointRespDTO point1 = new IotModbusPointRespDTO();
point1.setId(1L);
point1.setIdentifier("temperature");
point1.setName("温度");
point1.setFunctionCode(3); // FC03 读保持寄存器
point1.setRegisterAddress(0);
point1.setRegisterCount(1);
point1.setRawDataType("INT16");
point1.setByteOrder("BIG_ENDIAN");
point1.setScale(new BigDecimal("0.1"));
point1.setPollInterval(5000); // 5 秒轮询一次
points.add(point1);
// 点位 2湿度 - 保持寄存器 FC03, 地址 1, 1 个寄存器, UINT16
IotModbusPointRespDTO point2 = new IotModbusPointRespDTO();
point2.setId(2L);
point2.setIdentifier("humidity");
point2.setName("湿度");
point2.setFunctionCode(3); // FC03 读保持寄存器
point2.setRegisterAddress(1);
point2.setRegisterCount(1);
point2.setRawDataType("UINT16");
point2.setByteOrder("BIG_ENDIAN");
point2.setScale(new BigDecimal("0.1"));
point2.setPollInterval(5000); // 5 秒轮询一次
points.add(point2);
config.setPoints(points);
log.info("[buildMockConfigs][已加载 Mock 配置, deviceId={}, points={}]", config.getDeviceId(), points.size());
return Collections.singletonList(config);
}
/**
* 获取设备配置
*/
public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
return configCache.get(deviceId);
}
// TODO @AI这个逻辑是不是非必须
/**
* 通过 clientId + username + password 查找设备配置(认证用)
* 暂通过遍历缓存实现,后续可优化为索引
*/
public IotModbusDeviceConfigRespDTO findConfigByAuth(String clientId, String username, String password) {
// TODO @芋艿:测试完成后移除 mock 逻辑,改为正式查找
// Mock通过 clientId格式 productKey.deviceName匹配缓存中的设备
if (clientId != null && clientId.contains(".")) {
String[] parts = clientId.split("\\.", 2);
String productKey = parts[0];
String deviceName = parts[1];
for (IotModbusDeviceConfigRespDTO config : configCache.values()) {
if (productKey.equals(config.getProductKey()) && deviceName.equals(config.getDeviceName())) {
return config;
}
}
}
return null;
}
/**
* 清理已删除设备的资源
*/
public void cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs, Consumer<Long> cleanupAction) {
Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
removedDeviceIds.removeAll(currentDeviceIds);
for (Long deviceId : removedDeviceIds) {
log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
configCache.remove(deviceId);
cleanupAction.accept(deviceId);
}
knownDeviceIds.clear();
knownDeviceIds.addAll(currentDeviceIds);
}
}

View File

@@ -0,0 +1,160 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* IoT Modbus TCP Slave 连接管理器
* <p>
* 管理设备 TCP 连接socket ↔ 设备双向映射
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpSlaveConnectionManager {
/**
* socket → 连接信息
*/
private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
/**
* deviceId → socket
*/
private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>();
/**
* 连接信息
*/
@Data
public static class ConnectionInfo {
/**
* 设备编号
*/
private Long deviceId;
/**
* 产品标识
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 从站地址
*/
private Integer slaveId;
/**
* 帧格式(首帧自动检测得到)
*/
private IotModbusFrameFormatEnum frameFormat;
// TODO @AImode 是否非必须?!
/**
* 模式1-云端轮询 2-主动上报
*/
private Integer mode;
}
/**
* 注册已认证的连接
*/
public void registerConnection(NetSocket socket, ConnectionInfo info) {
connectionMap.put(socket, info);
deviceSocketMap.put(info.getDeviceId(), socket);
log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]",
info.getDeviceId(), socket.remoteAddress());
}
// TODO @芋艿:待定是不是要保留?!
/**
* 设置连接的帧格式(首帧检测后调用)
*/
public void setFrameFormat(NetSocket socket, IotModbusFrameFormatEnum frameFormat) {
ConnectionInfo info = connectionMap.get(socket);
if (info != null) {
info.setFrameFormat(frameFormat);
}
}
/**
* 获取连接信息
*/
public ConnectionInfo getConnectionInfo(NetSocket socket) {
return connectionMap.get(socket);
}
/**
* 根据设备 ID 获取连接信息
*/
public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
NetSocket socket = deviceSocketMap.get(deviceId);
return socket != null ? connectionMap.get(socket) : null;
}
// TODO @AI不用判断连接是否认证
/**
* 判断连接是否已认证
*/
public boolean isAuthenticated(NetSocket socket) {
return connectionMap.containsKey(socket);
}
/**
* 移除连接
*/
public ConnectionInfo removeConnection(NetSocket socket) {
ConnectionInfo info = connectionMap.remove(socket);
if (info != null && info.getDeviceId() != null) {
deviceSocketMap.remove(info.getDeviceId());
log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId());
}
return info;
}
/**
* 发送数据到设备
*/
public void sendToDevice(Long deviceId, byte[] data) {
NetSocket socket = deviceSocketMap.get(deviceId);
if (socket == null) {
log.warn("[sendToDevice][设备 {} 没有连接]", deviceId);
return;
}
// TODO @AI直接复用 sendToSocket 方法?!
socket.write(Buffer.buffer(data));
}
/**
* 发送数据到指定 socket
*/
public void sendToSocket(NetSocket socket, byte[] data) {
socket.write(Buffer.buffer(data));
}
// TODO @AI貌似别的都没这个是不是可以去掉哈
/**
* 关闭所有连接
*/
public void closeAll() {
for (NetSocket socket : connectionMap.keySet()) {
try {
socket.close();
} catch (Exception e) {
log.error("[closeAll][关闭连接失败]", e);
}
}
connectionMap.clear();
deviceSocketMap.clear();
}
}

View File

@@ -0,0 +1,151 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
/**
* IoT Modbus TCP Slave 待响应请求管理器
* <p>
* 管理轮询下发的请求,用于匹配设备响应:
* - TCP 模式:按 transactionId 精确匹配
* - RTU 模式:按 slaveId + functionCode FIFO 匹配
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpSlavePendingRequestManager {
/**
* deviceId → 有序队列
*/
private final Map<Long, Deque<PendingRequest>> pendingRequests = new ConcurrentHashMap<>();
/**
* 待响应请求信息
*/
@Data
@AllArgsConstructor
public static class PendingRequest {
private Long deviceId;
private Long pointId;
private String identifier;
private int slaveId;
private int functionCode;
private int registerAddress;
private int registerCount;
private Integer transactionId;
private long expireAt;
}
/**
* 添加待响应请求
*/
public void addRequest(PendingRequest request) {
pendingRequests.computeIfAbsent(request.getDeviceId(), k -> new ConcurrentLinkedDeque<>())
.addLast(request);
}
/**
* 匹配响应TCP 模式按 transactionIdRTU 模式按 FIFO
*
* @param deviceId 设备 ID
* @param frame 收到的响应帧
* @param frameFormat 帧格式
* @return 匹配到的 PendingRequest没有匹配返回 null
*/
public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame,
IotModbusFrameFormatEnum frameFormat) {
Deque<PendingRequest> queue = pendingRequests.get(deviceId);
// TODO @AICollUtil.isEmpty(queue)
if (queue == null || queue.isEmpty()) {
return null;
}
if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) {
// TCP 模式:按 transactionId 精确匹配
return matchByTransactionId(queue, frame.getTransactionId());
} else {
// RTU 模式FIFO匹配 slaveId + functionCode
return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode());
}
}
/**
* 按 transactionId 匹配
*/
private PendingRequest matchByTransactionId(Deque<PendingRequest> queue, int transactionId) {
// TODO @AI需要兼容 jdk8
for (var it = queue.iterator(); it.hasNext(); ) {
PendingRequest req = it.next();
if (req.getTransactionId() != null && req.getTransactionId() == transactionId) {
it.remove();
return req;
}
}
return null;
}
/**
* 按 FIFO 匹配
*/
private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode) {
// TODO @AI需要兼容 jdk8
for (var it = queue.iterator(); it.hasNext(); ) {
PendingRequest req = it.next();
if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) {
it.remove();
return req;
}
}
return null;
}
/**
* 清理过期请求
*/
public void cleanupExpired() {
long now = System.currentTimeMillis();
for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) {
Deque<PendingRequest> queue = entry.getValue();
int removed = 0;
while (!queue.isEmpty()) {
PendingRequest req = queue.peekFirst();
// TODO @AIif return 减少括号层级;
if (req != null && req.getExpireAt() < now) {
queue.pollFirst();
removed++;
} else {
break; // 队列有序,后面的没过期
}
}
if (removed > 0) {
log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed);
}
}
}
/**
* 清理指定设备的所有待响应请求
*/
public void removeDevice(Long deviceId) {
pendingRequests.remove(deviceId);
}
/**
* 清理所有待响应请求
*/
public void clear() {
pendingRequests.clear();
}
}

View File

@@ -0,0 +1,207 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest;
import io.vertx.core.Vertx;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
// TODO @AI和 IotModbusTcpPollScheduler 很像,是不是可以做一些复用?
/**
* IoT Modbus TCP Slave 轮询调度器
* <p>
* 管理点位的轮询定时器,为 mode=1云端轮询的设备调度读取任务。
* 与 tcpmaster 不同,这里不直接通过 j2mod 读取,而是:
* 1. 编码 Modbus 读请求帧
* 2. 通过 ConnectionManager 发送到设备的 TCP 连接
* 3. 将请求注册到 PendingRequestManager等待设备响应
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpSlavePollScheduler {
private final Vertx vertx;
private final IotModbusTcpSlaveConnectionManager connectionManager;
private final IotModbusFrameCodec frameCodec;
private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
private final int requestTimeout;
/**
* 设备点位的定时器映射deviceId -> (pointId -> PointTimerInfo)
*/
private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
/**
* TCP 事务 ID 自增器
*/
private final AtomicInteger transactionIdCounter = new AtomicInteger(0);
/**
* 点位定时器信息
*/
@Data
@AllArgsConstructor
private static class PointTimerInfo {
/**
* Vert.x 定时器 ID
*/
private Long timerId;
/**
* 轮询间隔(用于判断是否需要更新定时器)
*/
private Integer pollInterval;
}
/**
* 更新轮询任务(增量更新)
*/
public void updatePolling(IotModbusDeviceConfigRespDTO config) {
Long deviceId = config.getDeviceId();
List<IotModbusPointRespDTO> newPoints = config.getPoints();
Map<Long, PointTimerInfo> currentTimers = devicePointTimers
.computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
// 1.1 计算新配置中的点位 ID 集合
Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
// 1.2 计算删除的点位 ID 集合
Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
removedPointIds.removeAll(newPointIds);
// 2. 处理删除的点位:停止不再存在的定时器
for (Long pointId : removedPointIds) {
PointTimerInfo timerInfo = currentTimers.remove(pointId);
if (timerInfo != null) {
vertx.cancelTimer(timerInfo.getTimerId());
log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
}
}
// 3. 处理新增和修改的点位
if (CollUtil.isEmpty(newPoints)) {
return;
}
for (IotModbusPointRespDTO point : newPoints) {
Long pointId = point.getId();
Integer newPollInterval = point.getPollInterval();
PointTimerInfo existingTimer = currentTimers.get(pointId);
// 3.1 新增点位:创建定时器
if (existingTimer == null) {
Long timerId = createPollTimer(config, point);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
deviceId, pointId, newPollInterval);
}
} else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
// 3.2 pollInterval 变化:重建定时器
vertx.cancelTimer(existingTimer.getTimerId());
Long timerId = createPollTimer(config, point);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
} else {
currentTimers.remove(pointId);
}
}
// 3.3 其他属性变化:不处理(下次轮询时自动使用新配置)
}
}
/**
* 创建轮询定时器
*/
private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) {
if (point.getPollInterval() == null || point.getPollInterval() <= 0) {
return null;
}
return vertx.setPeriodic(point.getPollInterval(), timerId -> {
try {
pollPoint(config, point);
} catch (Exception e) {
log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]",
config.getDeviceId(), point.getIdentifier(), e);
}
});
}
/**
* 轮询单个点位:编码读请求帧 → 发送 → 注册 PendingRequest
*/
private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) {
Long deviceId = config.getDeviceId();
// 1. 获取连接信息
ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId);
if (connInfo == null) {
log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId);
return;
}
// 2.1 确定帧格式和事务 ID
// TODO @AI不允许为空
IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
if (frameFormat == null) {
frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP;
}
// TODO @AItransactionId 需要根据设备来么?然后递增也根据 IotModbusFrameFormatEnum.MODBUS_TCP 提前判断;
int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF;
int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
// 2.2 编码读请求
byte[] data = frameCodec.encodeReadRequest(slaveId, point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId);
// 2.3 注册 PendingRequest
PendingRequest pendingRequest = new PendingRequest(
deviceId, point.getId(), point.getIdentifier(),
slaveId, point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount(),
frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? transactionId : null,
System.currentTimeMillis() + requestTimeout);
pendingRequestManager.addRequest(pendingRequest);
// 3. 发送读请求
connectionManager.sendToDevice(deviceId, data);
log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
deviceId, point.getIdentifier(), point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount());
}
/**
* 停止设备的轮询
*/
public void stopPolling(Long deviceId) {
Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
if (CollUtil.isEmpty(timers)) {
return;
}
for (PointTimerInfo timerInfo : timers.values()) {
vertx.cancelTimer(timerInfo.getTimerId());
}
log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
}
/**
* 停止所有轮询
*/
public void stopAll() {
for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
stopPolling(deviceId);
}
}
}

View File

@@ -164,14 +164,26 @@ yudao:
trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码
# ====================================
# 针对引入的 Modbus TCP 组件的配置
# 针对引入的 Modbus TCP Master 组件的配置
# ====================================
- id: modbus-tcp-1
enabled: true
protocol: modbus_tcp
- id: modbus-tcp-master-1
enabled: false
protocol: modbus_tcp_master
port: 502
modbus-tcp:
modbus-tcp-master:
config-refresh-interval: 30 # 配置刷新间隔(秒)
# ====================================
# 针对引入的 Modbus TCP Slave 组件的配置
# ====================================
- id: modbus-tcp-slave-1
enabled: true
protocol: modbus_tcp_slave
port: 503
modbus-tcp-slave:
config-refresh-interval: 30 # 配置刷新间隔(秒)
custom-function-code: 65 # 自定义功能码(用于认证等扩展交互)
request-timeout: 5000 # Pending Request 超时时间(毫秒)
request-cleanup-interval: 10000 # Pending Request 清理间隔(毫秒)
--- #################### 日志相关配置 ####################
@@ -193,7 +205,7 @@ logging:
cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp: DEBUG
cn.iocoder.yudao.module.iot.gateway.protocol.modbus: DEBUG
# 根日志级别
root: INFO

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

@@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
import com.ghgande.j2mod.modbus.procimg.*;
import com.ghgande.j2mod.modbus.slave.ModbusSlave;

View File

@@ -0,0 +1,309 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
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.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory;
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;
/**
* IoT Modbus TCP Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试)
*
* <p>测试场景设备TCP Client连接到网关TCP Server使用 MODBUS_RTUCRC16帧格式通信
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 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 IotModbusTcpSlaveModbusRtuIntegrationTest {
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 IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC);
// ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
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();
}
}
// ===================== 认证测试 =====================
/**
* 认证测试:发送自定义功能码 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());
log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
} finally {
socket.close();
}
}
// ===================== 轮询响应测试 =====================
/**
* 轮询响应测试:认证后等待网关下发 FC03 读请求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());
// 2. 等待网关下发读请求
log.info("[testPollingResponse][等待网关下发读请求...]");
IotModbusFrame readRequest = waitForRequest(socket);
log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}]",
readRequest.getSlaveId(), readRequest.getFunctionCode());
// 3. 解析读请求中的起始地址和数量
byte[] pdu = readRequest.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);
// 4. 构造读响应帧模拟寄存器数据RTU 格式)
int[] registerValues = new int[quantity];
for (int i = 0; i < quantity; i++) {
registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
}
byte[] responseData = buildReadResponse(readRequest.getSlaveId(),
readRequest.getFunctionCode(), registerValues);
socket.write(Buffer.buffer(responseData));
log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
// 5. 等待一段时间让网关处理
Thread.sleep(20000);
} 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(),
bytesToHex(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);
byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
return sendAndReceive(socket, authFrame);
}
/**
* 发送帧并等待响应(使用 IotModbusRecordParserFactory 自动检测帧格式)
*/
private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
// 使用 RecordParserFactory 创建拆包器(自动检测帧格式)
RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC,
buffer -> {
try {
// 检测到的帧格式应该是 RTU使用 RTU 格式解码
IotModbusFrame frame = FRAME_CODEC.decodeResponse(
buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU);
responseFuture.complete(frame);
} catch (Exception e) {
responseFuture.completeExceptionally(e);
}
},
format -> log.info("[sendAndReceive][检测到帧格式: {}]", format));
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<>();
// 使用 RecordParserFactory 创建拆包器
RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC,
buffer -> {
try {
IotModbusFrame frame = FRAME_CODEC.decodeResponse(
buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU);
requestFuture.complete(frame);
} catch (Exception e) {
requestFuture.completeExceptionally(e);
}
},
format -> log.info("[waitForRequest][检测到帧格式: {}]", format));
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_CODEC.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 = IotModbusFrameCodec.calculateCrc16(frame, totalLength - 2);
frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low
frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
return frame;
}
/**
* 字节数组转十六进制字符串
*/
private static String bytesToHex(byte[] bytes) {
if (bytes == null) {
return "null";
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
}

View File

@@ -0,0 +1,334 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
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.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
import io.vertx.core.Handler;
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;
/**
* IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试)
*
* <p>测试场景设备TCP Client连接到网关TCP Server使用 MODBUS_TCPMBAP 头)帧格式通信
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 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 IotModbusTcpSlaveModbusTcpIntegrationTest {
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 IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC);
// ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
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();
}
}
// ===================== 认证测试 =====================
/**
* 认证测试:发送自定义功能码 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());
log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
} finally {
socket.close();
}
}
// ===================== 轮询响应测试 =====================
/**
* 轮询响应测试:认证后等待网关下发 FC03 读请求,构造读响应帧发回
*/
@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());
// 2. 等待网关下发读请求
log.info("[testPollingResponse][等待网关下发读请求...]");
IotModbusFrame readRequest = waitForRequest(socket);
log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}, transactionId={}]",
readRequest.getSlaveId(), readRequest.getFunctionCode(), readRequest.getTransactionId());
// 3. 解析读请求中的起始地址和数量
byte[] pdu = readRequest.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);
// 4. 构造读响应帧(模拟寄存器数据)
int[] registerValues = new int[quantity];
for (int i = 0; i < quantity; i++) {
registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
}
byte[] responseData = buildReadResponse(readRequest.getTransactionId(),
readRequest.getSlaveId(), readRequest.getFunctionCode(), registerValues);
socket.write(Buffer.buffer(responseData));
log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
// 5. 等待一段时间让网关处理
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(), bytesToHex(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);
byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
return sendAndReceive(socket, authFrame);
}
/**
* 发送帧并等待响应MODBUS_TCP 格式)
* <p>
* 使用两阶段 RecordParser 拆包fixedSizeMode(6) 读 MBAP 头 → fixedSizeMode(length) 读 body
*/
private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
// 创建 TCP 两阶段拆包 RecordParser
RecordParser parser = RecordParser.newFixed(6);
parser.handler(new TcpRecordParserHandler(parser, responseFuture));
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<>();
RecordParser parser = RecordParser.newFixed(6);
parser.handler(new TcpRecordParserHandler(parser, requestFuture));
socket.handler(parser);
// 等待(超时 30 秒,因为轮询间隔可能比较长)
return requestFuture.get(30000, TimeUnit.MILLISECONDS);
}
/**
* MODBUS_TCP 两阶段拆包 Handler
*/
private class TcpRecordParserHandler implements Handler<Buffer> {
private final RecordParser parser;
private final CompletableFuture<IotModbusFrame> future;
private byte[] mbapHeader;
private boolean waitingForBody = false;
TcpRecordParserHandler(RecordParser parser, CompletableFuture<IotModbusFrame> future) {
this.parser = parser;
this.future = future;
}
@Override
public void handle(Buffer buffer) {
try {
if (waitingForBody) {
// Phase 2: 收到 bodyunitId + PDU
byte[] body = buffer.getBytes();
byte[] fullFrame = new byte[mbapHeader.length + body.length];
System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length);
System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length);
IotModbusFrame frame = FRAME_CODEC.decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP);
future.complete(frame);
} else {
// Phase 1: 收到 MBAP 头 6 字节
this.mbapHeader = buffer.getBytes();
int length = ((mbapHeader[4] & 0xFF) << 8) | (mbapHeader[5] & 0xFF);
this.waitingForBody = true;
parser.fixedSizeMode(length);
}
} catch (Exception e) {
future.completeExceptionally(e);
}
}
}
/**
* 构造认证帧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_CODEC.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();
}
/**
* 字节数组转十六进制字符串
*/
private static String bytesToHex(byte[] bytes) {
if (bytes == null) {
return "null";
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
}