Commit a0db86848d112820a80c095725c078693ee13945

Authored by YunaiV
1 parent 43192207

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

Showing 38 changed files with 3612 additions and 71 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java
... ... @@ -33,6 +33,14 @@ public class IotDeviceModbusConfigRespVO {
33 33 @Schema(description = "重试间隔(毫秒)", example = "1000")
34 34 private Integer retryInterval;
35 35  
  36 + // TODO @AI:不要【:1-云端轮询 2-主动上报】
  37 + @Schema(description = "模式:1-云端轮询 2-主动上报", example = "1")
  38 + private Integer mode;
  39 +
  40 + // TODO @AI:还是换成 int,然后写注释;不要【:modbus_tcp / modbus_rtu】
  41 + @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp")
  42 + private String frameFormat;
  43 +
36 44 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
37 45 private Integer status;
38 46  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java
... ... @@ -31,6 +31,14 @@ public class IotDeviceModbusConfigSaveReqVO {
31 31 @Schema(description = "重试间隔(毫秒)", example = "1000")
32 32 private Integer retryInterval;
33 33  
  34 + // TODO @AI:不要【:1-云端轮询 2-主动上报】
  35 + @Schema(description = "模式:1-云端轮询 2-主动上报", example = "1")
  36 + private Integer mode;
  37 +
  38 + // TODO @AI:不要【:1-云端轮询 2-主动上报】
  39 + @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp")
  40 + private String frameFormat;
  41 +
34 42 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
35 43 @NotNull(message = "状态不能为空")
36 44 private Integer status;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java
1 1 package cn.iocoder.yudao.module.iot.dal.dataobject.device;
2 2  
3 3 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
4 5 import com.baomidou.mybatisplus.annotation.KeySequence;
5 6 import com.baomidou.mybatisplus.annotation.TableId;
6 7 import com.baomidou.mybatisplus.annotation.TableName;
... ... @@ -24,7 +25,12 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO {
24 25 */
25 26 @TableId
26 27 private Long id;
27   - // TODO @AI:增加 productId;
  28 + /**
  29 + * 产品编号
  30 + *
  31 + * 关联 {@link IotProductDO#getId()}
  32 + */
  33 + private Long productId;
28 34 /**
29 35 * 设备编号
30 36 *
... ... @@ -53,6 +59,18 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO {
53 59 */
54 60 private Integer retryInterval;
55 61 /**
  62 + * 模式
  63 + *
  64 + * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum
  65 + */
  66 + private Integer mode;
  67 + /**
  68 + * 数据帧格式
  69 + *
  70 + * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum
  71 + */
  72 + private String frameFormat;
  73 + /**
56 74 * 状态
57 75 *
58 76 * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java
... ... @@ -47,8 +47,16 @@ public class IotModbusDeviceConfigRespDTO {
47 47 * 重试间隔,单位:毫秒
48 48 */
49 49 private Integer retryInterval;
  50 + /**
  51 + * 模式
  52 + */
  53 + private Integer mode;
  54 + /**
  55 + * 数据帧格式
  56 + */
  57 + private String frameFormat;
50 58  
51   - // ========== 点位配置 ==========
  59 + // ========== Modbus 点位配置 ==========
52 60  
53 61 /**
54 62 * 点位列表
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  4 +import lombok.Getter;
  5 +import lombok.RequiredArgsConstructor;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * IoT Modbus 数据帧格式枚举
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Getter
  15 +@RequiredArgsConstructor
  16 +public enum IotModbusFrameFormatEnum implements ArrayValuable<String> {
  17 +
  18 + MODBUS_TCP("modbus_tcp", "Modbus TCP"),
  19 + MODBUS_RTU("modbus_rtu", "Modbus RTU");
  20 +
  21 + public static final String[] ARRAYS = Arrays.stream(values())
  22 + .map(IotModbusFrameFormatEnum::getFormat)
  23 + .toArray(String[]::new);
  24 +
  25 + /**
  26 + * 格式
  27 + */
  28 + private final String format;
  29 + /**
  30 + * 名称
  31 + */
  32 + private final String name;
  33 +
  34 + @Override
  35 + public String[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
  39 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusModeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  4 +import lombok.Getter;
  5 +import lombok.RequiredArgsConstructor;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * IoT Modbus 模式枚举
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Getter
  15 +@RequiredArgsConstructor
  16 +public enum IotModbusModeEnum implements ArrayValuable<Integer> {
  17 +
  18 + POLLING(1, "云端轮询"),
  19 + ACTIVE_REPORT(2, "主动上报");
  20 +
  21 + public static final Integer[] ARRAYS = Arrays.stream(values())
  22 + .map(IotModbusModeEnum::getMode)
  23 + .toArray(Integer[]::new);
  24 +
  25 + /**
  26 + * 模式
  27 + */
  28 + private final Integer mode;
  29 + /**
  30 + * 名称
  31 + */
  32 + private final String name;
  33 +
  34 + @Override
  35 + public Integer[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
  39 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java
... ... @@ -25,7 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable&lt;String&gt; {
25 25 MQTT("mqtt"),
26 26 EMQX("emqx"),
27 27 COAP("coap"),
28   - MODBUS_TCP("modbus_tcp");
  28 + MODBUS_TCP_MASTER("modbus_tcp_master"),
  29 + MODBUS_TCP_SLAVE("modbus_tcp_slave");
29 30  
30 31 public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new);
31 32  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
... ... @@ -4,7 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpConfig;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterConfig;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveConfig;
8 9 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
9 10 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
10 11 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
... ... @@ -168,10 +169,16 @@ public class IotGatewayProperties {
168 169 private IotEmqxConfig emqx;
169 170  
170 171 /**
171   - * Modbus TCP 协议配置
  172 + * Modbus TCP Master 协议配置
172 173 */
173 174 @Valid
174   - private IotModbusTcpConfig modbusTcp;
  175 + private IotModbusTcpMasterConfig modbusTcpMaster;
  176 +
  177 + /**
  178 + * Modbus TCP Slave 协议配置
  179 + */
  180 + @Valid
  181 + private IotModbusTcpSlaveConfig modbusTcpSlave;
175 182  
176 183 }
177 184  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
... ... @@ -7,7 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
7 7 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
12 13 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
13 14 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
... ... @@ -113,8 +114,10 @@ public class IotProtocolManager implements SmartLifecycle {
113 114 return createMqttProtocol(config);
114 115 case EMQX:
115 116 return createEmqxProtocol(config);
116   - case MODBUS_TCP:
117   - return createModbusTcpProtocol(config);
  117 + case MODBUS_TCP_MASTER:
  118 + return createModbusTcpMasterProtocol(config);
  119 + case MODBUS_TCP_SLAVE:
  120 + return createModbusTcpSlaveProtocol(config);
118 121 default:
119 122 throw new IllegalArgumentException(String.format(
120 123 "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
... ... @@ -192,13 +195,23 @@ public class IotProtocolManager implements SmartLifecycle {
192 195 }
193 196  
194 197 /**
195   - * 创建 Modbus TCP 协议实例
  198 + * 创建 Modbus TCP Master 协议实例
196 199 *
197 200 * @param config 协议实例配置
198   - * @return Modbus TCP 协议实例
  201 + * @return Modbus TCP Master 协议实例
199 202 */
200   - private IotModbusTcpProtocol createModbusTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
201   - return new IotModbusTcpProtocol(config);
  203 + private IotModbusTcpMasterProtocol createModbusTcpMasterProtocol(IotGatewayProperties.ProtocolProperties config) {
  204 + return new IotModbusTcpMasterProtocol(config);
  205 + }
  206 +
  207 + /**
  208 + * 创建 Modbus TCP Slave 协议实例
  209 + *
  210 + * @param config 协议实例配置
  211 + * @return Modbus TCP Slave 协议实例
  212 + */
  213 + private IotModbusTcpSlaveProtocol createModbusTcpSlaveProtocol(IotGatewayProperties.ProtocolProperties config) {
  214 + return new IotModbusTcpSlaveProtocol(config);
202 215 }
203 216  
204 217 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/codec/IotModbusDataConverter.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common;
2 2  
3 3 import cn.hutool.core.util.ArrayUtil;
4 4 import cn.hutool.core.util.ObjectUtil;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpConfig.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
2 2  
3 3 import jakarta.validation.constraints.Min;
4 4 import jakarta.validation.constraints.NotNull;
5 5 import lombok.Data;
6 6  
7 7 /**
8   - * IoT Modbus TCP 协议配置
  8 + * IoT Modbus TCP Master 协议配置
9 9 *
10 10 * @author 芋道源码
11 11 */
12 12 @Data
13   -public class IotModbusTcpConfig {
  13 +public class IotModbusTcpMasterConfig {
14 14  
15 15 /**
16 16 * 配置刷新间隔(秒)
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpProtocol.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.extra.spring.SpringUtil;
... ... @@ -9,14 +9,14 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
9 9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
10 10 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
11 11 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
13   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamHandler;
15   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream.IotModbusTcpDownstreamSubscriber;
16   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream.IotModbusTcpUpstreamHandler;
17   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService;
18   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
19   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpPollScheduler;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
  19 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpPollScheduler;
20 20 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
21 21 import io.vertx.core.Vertx;
22 22 import lombok.Getter;
... ... @@ -27,12 +27,12 @@ import java.util.List;
27 27 import java.util.concurrent.TimeUnit;
28 28  
29 29 /**
30   - * IoT 网关 Modbus TCP 协议:主动轮询 Modbus 从站设备数据
  30 + * IoT 网关 Modbus TCP Master 协议:主动轮询 Modbus 从站设备数据
31 31 *
32 32 * @author 芋道源码
33 33 */
34 34 @Slf4j
35   -public class IotModbusTcpProtocol implements IotProtocol {
  35 +public class IotModbusTcpMasterProtocol implements IotProtocol {
36 36  
37 37 /**
38 38 * 协议配置
... ... @@ -71,9 +71,9 @@ public class IotModbusTcpProtocol implements IotProtocol {
71 71 private final IotModbusTcpConfigCacheService configCacheService;
72 72 private final IotModbusTcpPollScheduler pollScheduler;
73 73  
74   - public IotModbusTcpProtocol(ProtocolProperties properties) {
75   - IotModbusTcpConfig modbusTcpConfig = properties.getModbusTcp();
76   - Assert.notNull(modbusTcpConfig, "Modbus TCP 协议配置(modbusTcp)不能为空");
  74 + public IotModbusTcpMasterProtocol(ProtocolProperties properties) {
  75 + IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster();
  76 + Assert.notNull(modbusTcpMasterConfig, "Modbus TCP Master 协议配置(modbusTcpMaster)不能为空");
77 77 this.properties = properties;
78 78 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
79 79  
... ... @@ -109,13 +109,13 @@ public class IotModbusTcpProtocol implements IotProtocol {
109 109  
110 110 @Override
111 111 public IotProtocolTypeEnum getType() {
112   - return IotProtocolTypeEnum.MODBUS_TCP;
  112 + return IotProtocolTypeEnum.MODBUS_TCP_MASTER;
113 113 }
114 114  
115 115 @Override
116 116 public void start() {
117 117 if (running) {
118   - log.warn("[start][IoT Modbus TCP 协议 {} 已经在运行中]", getId());
  118 + log.warn("[start][IoT Modbus TCP Master 协议 {} 已经在运行中]", getId());
119 119 return;
120 120 }
121 121  
... ... @@ -123,18 +123,18 @@ public class IotModbusTcpProtocol implements IotProtocol {
123 123 // 1.1 首次加载配置
124 124 refreshConfig();
125 125 // 1.2 启动配置刷新定时器
126   - int refreshInterval = properties.getModbusTcp().getConfigRefreshInterval();
  126 + int refreshInterval = properties.getModbusTcpMaster().getConfigRefreshInterval();
127 127 configRefreshTimerId = vertx.setPeriodic(
128 128 TimeUnit.SECONDS.toMillis(refreshInterval),
129 129 id -> refreshConfig()
130 130 );
131 131 running = true;
132   - log.info("[start][IoT Modbus TCP 协议 {} 启动成功,serverId={}]", getId(), serverId);
  132 + log.info("[start][IoT Modbus TCP Master 协议 {} 启动成功,serverId={}]", getId(), serverId);
133 133  
134 134 // 2. 启动下行消息订阅者
135 135 this.downstreamSubscriber.start();
136 136 } catch (Exception e) {
137   - log.error("[start][IoT Modbus TCP 协议 {} 启动失败]", getId(), e);
  137 + log.error("[start][IoT Modbus TCP Master 协议 {} 启动失败]", getId(), e);
138 138 // 启动失败时关闭资源
139 139 if (vertx != null) {
140 140 vertx.close();
... ... @@ -151,9 +151,9 @@ public class IotModbusTcpProtocol implements IotProtocol {
151 151 // 1. 停止下行消息订阅者
152 152 try {
153 153 downstreamSubscriber.stop();
154   - log.info("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者已停止]", getId());
  154 + log.info("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者已停止]", getId());
155 155 } catch (Exception e) {
156   - log.error("[stop][IoT Modbus TCP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  156 + log.error("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者停止失败]", getId(), e);
157 157 }
158 158  
159 159 // 2.1 取消配置刷新定时器
... ... @@ -163,22 +163,22 @@ public class IotModbusTcpProtocol implements IotProtocol {
163 163 }
164 164 // 2.2 停止轮询调度器
165 165 pollScheduler.stopAll();
166   - log.info("[stop][IoT Modbus TCP 协议 {} 轮询调度器已停止]", getId());
  166 + log.info("[stop][IoT Modbus TCP Master 协议 {} 轮询调度器已停止]", getId());
167 167 // 2.3 关闭所有连接
168 168 connectionManager.closeAll();
169   - log.info("[stop][IoT Modbus TCP 协议 {} 连接管理器已关闭]", getId());
  169 + log.info("[stop][IoT Modbus TCP Master 协议 {} 连接管理器已关闭]", getId());
170 170  
171 171 // 3. 关闭 Vert.x 实例
172 172 if (vertx != null) {
173 173 try {
174 174 vertx.close().result();
175   - log.info("[stop][IoT Modbus TCP 协议 {} Vertx 已关闭]", getId());
  175 + log.info("[stop][IoT Modbus TCP Master 协议 {} Vertx 已关闭]", getId());
176 176 } catch (Exception e) {
177   - log.error("[stop][IoT Modbus TCP 协议 {} Vertx 关闭失败]", getId(), e);
  177 + log.error("[stop][IoT Modbus TCP Master 协议 {} Vertx 关闭失败]", getId(), e);
178 178 }
179 179 }
180 180 running = false;
181   - log.info("[stop][IoT Modbus TCP 协议 {} 已停止]", getId());
  181 + log.info("[stop][IoT Modbus TCP Master 协议 {} 已停止]", getId());
182 182 }
183 183  
184 184 /**
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/client/IotModbusTcpClient.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
4 4 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
6 6 import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
7 7 import com.ghgande.j2mod.modbus.msg.*;
8 8 import com.ghgande.j2mod.modbus.procimg.InputRegister;
... ... @@ -12,6 +12,7 @@ import com.ghgande.j2mod.modbus.util.BitVector;
12 12 import io.vertx.core.Future;
13 13 import lombok.extern.slf4j.Slf4j;
14 14  
  15 +// TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪;
15 16 /**
16 17 * IoT Modbus TCP 客户端
17 18 * <p>
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
... ... @@ -6,10 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 7 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;
8 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
11   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConfigCacheService;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager.IotModbusTcpConnectionManager;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
13 13 import lombok.RequiredArgsConstructor;
14 14 import lombok.extern.slf4j.Slf4j;
15 15  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/downstream/IotModbusTcpDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.downstream;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.IotModbusTcpProtocol;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol;
8 8 import lombok.RequiredArgsConstructor;
9 9 import lombok.extern.slf4j.Slf4j;
10 10  
  11 +// 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
11 12 /**
12 13 * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
13 14 *
... ... @@ -17,7 +18,7 @@ import lombok.extern.slf4j.Slf4j;
17 18 @Slf4j
18 19 public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
19 20  
20   - private final IotModbusTcpProtocol protocol;
  21 + private final IotModbusTcpMasterProtocol protocol;
21 22 private final IotModbusTcpDownstreamHandler downstreamHandler;
22 23 private final IotMessageBus messageBus;
23 24  
... ... @@ -26,7 +27,7 @@ public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber&lt;Io
26 27 */
27 28 public void start() {
28 29 messageBus.register(this);
29   - log.info("[start][Modbus TCP 下行消息订阅器已启动, topic={}]", getTopic());
  30 + log.info("[start][Modbus TCP Master 下行消息订阅器已启动, topic={}]", getTopic());
30 31 }
31 32  
32 33 /**
... ... @@ -34,7 +35,7 @@ public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber&lt;Io
34 35 */
35 36 public void stop() {
36 37 messageBus.unregister(this);
37   - log.info("[stop][Modbus TCP 下行消息订阅器已停止]");
  38 + log.info("[stop][Modbus TCP Master 下行消息订阅器已停止]");
38 39 }
39 40  
40 41 @Override
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/handler/upstream/IotModbusTcpUpstreamHandler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.codec.IotModbusDataConverter;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
9 9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10 10 import lombok.extern.slf4j.Slf4j;
11 11  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConfigCacheService.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpConnectionManager.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
4 4 import com.ghgande.j2mod.modbus.net.TCPMasterConnection;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/manager/IotModbusTcpPollScheduler.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.manager;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.client.IotModbusTcpClient;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp.handler.upstream.IotModbusTcpUpstreamHandler;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
8 8 import io.vertx.core.Vertx;
9 9 import lombok.AllArgsConstructor;
10 10 import lombok.Data;
... ... @@ -16,6 +16,7 @@ import java.util.concurrent.ConcurrentHashMap;
16 16  
17 17 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
18 18  
  19 +// 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 也要!
19 20 /**
20 21 * IoT Modbus TCP 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
21 22 *
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/package-info.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java
1 1 /**
2   - * Modbus TCP 协议实现包
  2 + * Modbus TCP Master 协议实现包
3 3 * <p>
4 4 * 提供基于 j2mod 的 Modbus TCP 主站(Master)功能,支持:
5 5 * 1. 定时轮询 Modbus 从站设备数据
6 6 * 2. 下发属性设置命令到从站设备
7 7 * 3. 数据格式转换(寄存器值 ↔ 物模型属性值)
8 8 */
9   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
10 9 \ No newline at end of file
  10 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
  2 +
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT Modbus TCP Slave 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotModbusTcpSlaveConfig {
  14 +
  15 + /**
  16 + * 配置刷新间隔(秒)
  17 + */
  18 + @NotNull(message = "配置刷新间隔不能为空")
  19 + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
  20 + private Integer configRefreshInterval = 30;
  21 +
  22 + /**
  23 + * 自定义功能码(用于认证等扩展交互)
  24 + * Modbus 协议保留 65-72 给用户自定义,默认 65
  25 + */
  26 + @NotNull(message = "自定义功能码不能为空")
  27 + @Min(value = 65, message = "自定义功能码不能小于 65")
  28 + // TODO @AI:搞个范围;
  29 + private Integer customFunctionCode = 65;
  30 +
  31 + /**
  32 + * Pending Request 超时时间(毫秒)
  33 + */
  34 + @NotNull(message = "请求超时时间不能为空")
  35 + private Integer requestTimeout = 5000;
  36 +
  37 + /**
  38 + * Pending Request 清理间隔(毫秒)
  39 + */
  40 + @NotNull(message = "请求清理间隔不能为空")
  41 + private Integer requestCleanupInterval = 10000;
  42 +
  43 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  8 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
  9 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  10 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  12 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler;
  19 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber;
  20 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler;
  21 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
  22 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
  23 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
  24 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager;
  25 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler;
  26 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  27 +import io.vertx.core.Vertx;
  28 +import io.vertx.core.net.NetServer;
  29 +import io.vertx.core.net.NetServerOptions;
  30 +import io.vertx.core.net.NetSocket;
  31 +import io.vertx.core.parsetools.RecordParser;
  32 +import lombok.Getter;
  33 +import lombok.extern.slf4j.Slf4j;
  34 +
  35 +import java.util.List;
  36 +import java.util.Map;
  37 +import java.util.concurrent.ConcurrentHashMap;
  38 +import java.util.concurrent.TimeUnit;
  39 +
  40 +// TODO @AI:不用主动上报!
  41 +/**
  42 + * IoT 网关 Modbus TCP Slave 协议
  43 + * <p>
  44 + * 作为 TCP Server 接收设备主动连接:
  45 + * 1. 设备通过自定义功能码(FC 65)发送认证请求
  46 + * 2. 认证成功后,根据设备配置的 mode 决定工作模式:
  47 + * - mode=1(云端轮询):网关主动发送 Modbus 读请求,设备响应
  48 + * - mode=2(主动上报):设备主动上报数据,网关透传
  49 + *
  50 + * @author 芋道源码
  51 + */
  52 +@Slf4j
  53 +public class IotModbusTcpSlaveProtocol implements IotProtocol {
  54 +
  55 + /**
  56 + * 协议配置
  57 + */
  58 + private final ProtocolProperties properties;
  59 + /**
  60 + * 服务器 ID(用于消息追踪,全局唯一)
  61 + */
  62 + @Getter
  63 + private final String serverId;
  64 +
  65 + /**
  66 + * 运行状态
  67 + */
  68 + @Getter
  69 + private volatile boolean running = false;
  70 +
  71 + /**
  72 + * Vert.x 实例
  73 + */
  74 + private final Vertx vertx;
  75 + /**
  76 + * TCP Server
  77 + */
  78 + private NetServer netServer;
  79 + /**
  80 + * 配置刷新定时器 ID
  81 + */
  82 + private Long configRefreshTimerId;
  83 + /**
  84 + * Pending Request 清理定时器 ID
  85 + */
  86 + private Long requestCleanupTimerId;
  87 +
  88 + /**
  89 + * 未认证连接的帧格式缓存:socket → 检测到的帧格式
  90 + */
  91 + private final Map<NetSocket, IotModbusFrameFormatEnum> pendingFrameFormats = new ConcurrentHashMap<>();
  92 +
  93 + // ========== 各组件 ==========
  94 +
  95 + private final IotModbusTcpSlaveConfig slaveConfig;
  96 + private final IotModbusFrameCodec frameCodec;
  97 + private final IotModbusTcpSlaveConnectionManager connectionManager;
  98 + private final IotModbusTcpSlaveConfigCacheService configCacheService;
  99 + private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
  100 + private final IotModbusTcpSlaveUpstreamHandler upstreamHandler;
  101 + private final IotModbusTcpSlaveDownstreamHandler downstreamHandler;
  102 + private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber;
  103 + private final IotModbusTcpSlavePollScheduler pollScheduler;
  104 +
  105 + public IotModbusTcpSlaveProtocol(ProtocolProperties properties) {
  106 + this.slaveConfig = properties.getModbusTcpSlave();
  107 + Assert.notNull(slaveConfig, "Modbus TCP Slave 协议配置(modbusTcpSlave)不能为空");
  108 + this.properties = properties;
  109 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  110 +
  111 + // 初始化 Vertx
  112 + this.vertx = Vertx.vertx();
  113 +
  114 + // 初始化 Manager
  115 + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  116 + this.connectionManager = new IotModbusTcpSlaveConnectionManager();
  117 + this.configCacheService = new IotModbusTcpSlaveConfigCacheService(deviceApi);
  118 + this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager();
  119 +
  120 + // 初始化帧编解码器
  121 + this.frameCodec = new IotModbusFrameCodec(slaveConfig.getCustomFunctionCode());
  122 +
  123 + // 初始化 Handler
  124 + IotModbusDataConverter dataConverter = new IotModbusDataConverter();
  125 + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
  126 + this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler(
  127 + deviceApi, messageService, dataConverter, frameCodec,
  128 + connectionManager, configCacheService, pendingRequestManager, serverId);
  129 + this.downstreamHandler = new IotModbusTcpSlaveDownstreamHandler(
  130 + connectionManager, configCacheService, dataConverter, frameCodec);
  131 +
  132 + // 初始化轮询调度器
  133 + this.pollScheduler = new IotModbusTcpSlavePollScheduler(
  134 + vertx, connectionManager, frameCodec, pendingRequestManager,
  135 + slaveConfig.getRequestTimeout());
  136 +
  137 + // 设置认证成功回调:启动轮询
  138 + // TODO @AI:感觉直接去调用,不用注册回调了(更简洁)
  139 + this.upstreamHandler.setOnAuthSuccess((deviceId, config) -> {
  140 + if (config.getMode() != null
  141 + && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) {
  142 + pollScheduler.updatePolling(config);
  143 + }
  144 + });
  145 +
  146 + // 初始化下行消息订阅者
  147 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  148 + this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber(
  149 + this, downstreamHandler, messageBus);
  150 + }
  151 +
  152 + @Override
  153 + public String getId() {
  154 + return properties.getId();
  155 + }
  156 +
  157 + @Override
  158 + public IotProtocolTypeEnum getType() {
  159 + return IotProtocolTypeEnum.MODBUS_TCP_SLAVE;
  160 + }
  161 +
  162 + @Override
  163 + public void start() {
  164 + if (running) {
  165 + log.warn("[start][IoT Modbus TCP Slave 协议 {} 已经在运行中]", getId());
  166 + return;
  167 + }
  168 +
  169 + try {
  170 + // 1.1 首次加载配置
  171 + refreshConfig();
  172 + // 1.2 启动配置刷新定时器
  173 + int refreshInterval = slaveConfig.getConfigRefreshInterval();
  174 + configRefreshTimerId = vertx.setPeriodic(
  175 + TimeUnit.SECONDS.toMillis(refreshInterval),
  176 + id -> refreshConfig());
  177 +
  178 + // 2.1 启动 TCP Server
  179 + startTcpServer();
  180 +
  181 + // 2.2 启动 PendingRequest 清理定时器
  182 + requestCleanupTimerId = vertx.setPeriodic(
  183 + slaveConfig.getRequestCleanupInterval(),
  184 + id -> pendingRequestManager.cleanupExpired());
  185 + running = true;
  186 + log.info("[start][IoT Modbus TCP Slave 协议 {} 启动成功, serverId={}, port={}]",
  187 + getId(), serverId, properties.getPort());
  188 +
  189 + // 3. 启动下行消息订阅
  190 + downstreamSubscriber.start();
  191 + } catch (Exception e) {
  192 + log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e);
  193 + if (vertx != null) {
  194 + vertx.close();
  195 + }
  196 + // TODO @AI:其它相关的 close;
  197 + throw e;
  198 + }
  199 + }
  200 +
  201 + @Override
  202 + public void stop() {
  203 + if (!running) {
  204 + return;
  205 + }
  206 +
  207 + // 1. 停止下行消息订阅
  208 + try {
  209 + downstreamSubscriber.stop();
  210 + } catch (Exception e) {
  211 + log.error("[stop][下行消息订阅器停止失败]", e);
  212 + }
  213 +
  214 + // 2.1 取消定时器
  215 + if (configRefreshTimerId != null) {
  216 + vertx.cancelTimer(configRefreshTimerId);
  217 + configRefreshTimerId = null;
  218 + }
  219 + if (requestCleanupTimerId != null) {
  220 + vertx.cancelTimer(requestCleanupTimerId);
  221 + requestCleanupTimerId = null;
  222 + }
  223 + // 2.2 停止轮询
  224 + pollScheduler.stopAll();
  225 + // 2.3 清理 PendingRequest
  226 + pendingRequestManager.clear();
  227 + // 2.3 关闭所有连接
  228 + connectionManager.closeAll();
  229 + // 2.4 关闭 TCP Server
  230 + if (netServer != null) {
  231 + try {
  232 + netServer.close().result();
  233 + log.info("[stop][TCP Server 已关闭]");
  234 + } catch (Exception e) {
  235 + log.error("[stop][TCP Server 关闭失败]", e);
  236 + }
  237 + }
  238 +
  239 + // 3. 关闭 Vertx
  240 + if (vertx != null) {
  241 + try {
  242 + vertx.close().result();
  243 + } catch (Exception e) {
  244 + log.error("[stop][Vertx 关闭失败]", e);
  245 + }
  246 + }
  247 + running = false;
  248 + log.info("[stop][IoT Modbus TCP Slave 协议 {} 已停止]", getId());
  249 + }
  250 +
  251 + /**
  252 + * 启动 TCP Server
  253 + */
  254 + private void startTcpServer() {
  255 + // TODO @AI:host 一定要设置么?
  256 + // 1. 创建 TCP Server
  257 + NetServerOptions options = new NetServerOptions()
  258 + .setPort(properties.getPort())
  259 + .setHost("0.0.0.0");
  260 + netServer = vertx.createNetServer(options);
  261 +
  262 + // 2. 设置连接处理器
  263 + netServer.connectHandler(this::handleConnection);
  264 + // TODO @AI:是不是 sync 就好,不用 onSuccess/onFailure 了?感觉更简洁。失败,肯定就要抛出异常,结束初始化了!
  265 + netServer.listen()
  266 + .onSuccess(server -> log.info("[startTcpServer][TCP Server 启动成功, port={}]",
  267 + server.actualPort()))
  268 + .onFailure(e -> log.error("[startTcpServer][TCP Server 启动失败]", e));
  269 + }
  270 +
  271 + /**
  272 + * 处理新连接
  273 + */
  274 + private void handleConnection(NetSocket socket) {
  275 + log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress());
  276 +
  277 + // 1.1 创建带帧格式检测的 RecordParser
  278 + // TODO @AI:看看怎么从这个类里面,拿出去;让这个类的职责更单一;
  279 + RecordParser parser = IotModbusRecordParserFactory.create(
  280 + slaveConfig.getCustomFunctionCode(),
  281 + // 完整帧回调
  282 + // TODO @AI:感觉搞个独立的类,稍微好点?!
  283 + frameBuffer -> {
  284 + byte[] frameBytes = frameBuffer.getBytes();
  285 + // 获取该连接的帧格式
  286 + ConnectionInfo connInfo = connectionManager.getConnectionInfo(socket);
  287 + IotModbusFrameFormatEnum frameFormat = connInfo != null ? connInfo.getFrameFormat() : null;
  288 + if (frameFormat == null) {
  289 + // 未认证的连接,使用首帧检测到的帧格式
  290 + frameFormat = pendingFrameFormats.get(socket);
  291 + }
  292 + if (frameFormat == null) {
  293 + log.warn("[handleConnection][帧格式未检测到, remoteAddress={}]", socket.remoteAddress());
  294 + return;
  295 + }
  296 +
  297 + // 解码帧
  298 + IotModbusFrame frame = frameCodec.decodeResponse(frameBytes, frameFormat);
  299 + // 交给 UpstreamHandler 处理
  300 + upstreamHandler.handleFrame(socket, frame, frameFormat);
  301 + },
  302 + // 帧格式检测回调:保存到未认证缓存
  303 + detectedFormat -> {
  304 + // TODO @AI:是不是不用缓存,每次都探测;因为一般 auth 首包后,基本也没探测的诉求了!
  305 + pendingFrameFormats.put(socket, detectedFormat);
  306 + // 如果连接已注册(不太可能在检测阶段),也更新
  307 + // TODO @AI:是否非必须?!
  308 + connectionManager.setFrameFormat(socket, detectedFormat);
  309 + log.debug("[handleConnection][帧格式检测: {}, remoteAddress={}]",
  310 + detectedFormat, socket.remoteAddress());
  311 + }
  312 + );
  313 + // 1.2 设置数据处理器
  314 + socket.handler(parser);
  315 +
  316 + // 2.1 连接关闭处理
  317 + socket.closeHandler(v -> {
  318 + pendingFrameFormats.remove(socket);
  319 + ConnectionInfo info = connectionManager.removeConnection(socket);
  320 + // TODO @AI:if return 简化下;
  321 + if (info != null && info.getDeviceId() != null) {
  322 + pollScheduler.stopPolling(info.getDeviceId());
  323 + pendingRequestManager.removeDevice(info.getDeviceId());
  324 + log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]",
  325 + info.getDeviceId(), socket.remoteAddress());
  326 + } else {
  327 + log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress());
  328 + }
  329 + });
  330 + // 2.2 异常处理
  331 + socket.exceptionHandler(e -> {
  332 + log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e);
  333 + socket.close();
  334 + });
  335 + }
  336 +
  337 + /**
  338 + * 刷新配置
  339 + */
  340 + private synchronized void refreshConfig() {
  341 + try {
  342 + // 1. 从 biz 拉取最新配置
  343 + List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
  344 + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
  345 +
  346 + // 2. 更新已连接设备的轮询任务(仅 mode=1)
  347 + for (IotModbusDeviceConfigRespDTO config : configs) {
  348 + try {
  349 + if (config.getMode() != null
  350 + && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) {
  351 + // 只有已连接的设备才启动轮询
  352 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(config.getDeviceId());
  353 + if (connInfo != null) {
  354 + pollScheduler.updatePolling(config);
  355 + }
  356 + }
  357 + } catch (Exception e) {
  358 + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
  359 + }
  360 + }
  361 +
  362 + // 3. 清理已删除设备的资源
  363 + configCacheService.cleanupRemovedDevices(configs, deviceId -> {
  364 + pollScheduler.stopPolling(deviceId);
  365 + pendingRequestManager.removeDevice(deviceId);
  366 + });
  367 + } catch (Exception e) {
  368 + log.error("[refreshConfig][刷新配置失败]", e);
  369 + }
  370 + }
  371 +
  372 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IoT Modbus 统一帧数据模型(TCP/RTU 公用)
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Data
  12 +@Accessors(chain = true)
  13 +public class IotModbusFrame {
  14 +
  15 + /**
  16 + * 从站地址
  17 + */
  18 + private int slaveId;
  19 + /**
  20 + * 功能码
  21 + */
  22 + private int functionCode;
  23 + /**
  24 + * PDU 数据(不含 slaveId)
  25 + */
  26 + private byte[] pdu;
  27 + /**
  28 + * 事务标识符(TCP 模式特有)
  29 + *
  30 + * // TODO @AI:最好是 @某个类型独有;
  31 + */
  32 + private Integer transactionId;
  33 +
  34 + /**
  35 + * 是否异常响应
  36 + */
  37 + private boolean exception;
  38 + // TODO @AI:是不是要枚举一些异常;另外,是不是覆盖掉 exception;因为只要判断有异常码是不是就可以了;
  39 + /**
  40 + * 异常码(当 exception=true 时有效)
  41 + */
  42 + private Integer exceptionCode;
  43 +
  44 + /**
  45 + * 自定义功能码时的 JSON 字符串
  46 + */
  47 + private String customData;
  48 +
  49 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +
  6 +import java.nio.ByteBuffer;
  7 +import java.nio.ByteOrder;
  8 +import java.nio.charset.StandardCharsets;
  9 +
  10 +/**
  11 + * IoT Modbus 帧编解码器
  12 + * <p>
  13 + * 纯 Modbus 协议编解码,不处理 TCP 粘包(由 RecordParser 处理)。
  14 + * 支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式,以及自定义功能码扩展。
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Slf4j
  19 +public class IotModbusFrameCodec {
  20 +
  21 + private final int customFunctionCode;
  22 +
  23 + public IotModbusFrameCodec(int customFunctionCode) {
  24 + this.customFunctionCode = customFunctionCode;
  25 + }
  26 +
  27 + // ==================== 解码 ====================
  28 +
  29 + /**
  30 + * 解码响应帧(拆包后的完整帧 byte[])
  31 + *
  32 + * @param data 完整帧字节数组
  33 + * @param format 帧格式
  34 + * @return 解码后的 IotModbusFrame
  35 + */
  36 + public IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) {
  37 + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
  38 + return decodeTcpResponse(data);
  39 + } else {
  40 + return decodeRtuResponse(data);
  41 + }
  42 + }
  43 +
  44 + /**
  45 + * 解码 MODBUS_TCP 响应
  46 + * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...]
  47 + */
  48 + private IotModbusFrame decodeTcpResponse(byte[] data) {
  49 + if (data.length < 8) {
  50 + log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length);
  51 + return null;
  52 + }
  53 + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
  54 + int transactionId = buf.getShort() & 0xFFFF;
  55 + buf.getShort(); // protocolId(跳过)// TODO @AI:跳过原因,最好写下;
  56 + buf.getShort(); // length(跳过)// TODO @AI:跳过原因,最好写下;
  57 + int slaveId = buf.get() & 0xFF;
  58 + int functionCode = buf.get() & 0xFF;
  59 + // 提取 PDU 数据(从 functionCode 之后到末尾)
  60 + byte[] pdu = new byte[data.length - 8];
  61 + System.arraycopy(data, 8, pdu, 0, pdu.length);
  62 +
  63 + // 构建 IotModbusFrame
  64 + return buildFrame(slaveId, functionCode, pdu, transactionId);
  65 + }
  66 +
  67 + /**
  68 + * 解码 MODBUS_RTU 响应
  69 + * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)]
  70 + */
  71 + private IotModbusFrame decodeRtuResponse(byte[] data) {
  72 + if (data.length < 4) {
  73 + log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length);
  74 + return null;
  75 + }
  76 + // 校验 CRC
  77 + if (!verifyCrc16(data)) {
  78 + log.warn("[decodeRtuResponse][CRC 校验失败]");
  79 + return null;
  80 + }
  81 + int slaveId = data[0] & 0xFF;
  82 + int functionCode = data[1] & 0xFF;
  83 + // PDU 数据(不含 slaveId、functionCode、CRC)
  84 + byte[] pdu = new byte[data.length - 4];
  85 + System.arraycopy(data, 2, pdu, 0, pdu.length);
  86 +
  87 + // 构建 IotModbusFrame
  88 + return buildFrame(slaveId, functionCode, pdu, null);
  89 + }
  90 +
  91 + /**
  92 + * 构建 IotModbusFrame
  93 + */
  94 + private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) {
  95 + IotModbusFrame frame = new IotModbusFrame()
  96 + .setSlaveId(slaveId)
  97 + .setFunctionCode(functionCode)
  98 + .setPdu(pdu)
  99 + .setTransactionId(transactionId);
  100 +
  101 + // 异常响应
  102 + // TODO @AI:0x80 看看是不是要枚举;
  103 + if ((functionCode & 0x80) != 0) {
  104 + frame.setException(true);
  105 + // TODO @AI:0x7f 看看是不是要枚举;
  106 + frame.setFunctionCode(functionCode & 0x7F);
  107 + if (pdu.length >= 1) {
  108 + frame.setExceptionCode(pdu[0] & 0xFF);
  109 + }
  110 + return frame;
  111 + }
  112 +
  113 + // 自定义功能码
  114 + if (functionCode == customFunctionCode) {
  115 + // data 区格式:[byteCount(1)] [JSON data(N)]
  116 + if (pdu.length >= 1) {
  117 + int byteCount = pdu[0] & 0xFF;
  118 + if (pdu.length >= 1 + byteCount) {
  119 + frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8));
  120 + }
  121 + }
  122 + }
  123 + return frame;
  124 + }
  125 +
  126 + // ==================== 编码 ====================
  127 +
  128 + /**
  129 + * 编码读请求
  130 + *
  131 + * @param slaveId 从站地址
  132 + * @param functionCode 功能码
  133 + * @param startAddress 起始寄存器地址
  134 + * @param quantity 寄存器数量
  135 + * @param format 帧格式
  136 + * @param transactionId 事务 ID(TCP 模式下使用)
  137 + * @return 编码后的字节数组
  138 + */
  139 + public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity,
  140 + IotModbusFrameFormatEnum format, int transactionId) {
  141 + // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)]
  142 + byte[] pdu = new byte[5];
  143 + pdu[0] = (byte) functionCode;
  144 + pdu[1] = (byte) ((startAddress >> 8) & 0xFF);
  145 + pdu[2] = (byte) (startAddress & 0xFF);
  146 + pdu[3] = (byte) ((quantity >> 8) & 0xFF);
  147 + pdu[4] = (byte) (quantity & 0xFF);
  148 + return wrapFrame(slaveId, pdu, format, transactionId);
  149 + }
  150 +
  151 + /**
  152 + * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05)
  153 + *
  154 + * @param slaveId 从站地址
  155 + * @param functionCode 功能码
  156 + * @param address 寄存器地址
  157 + * @param value 值
  158 + * @param format 帧格式
  159 + * @param transactionId 事务 ID
  160 + * @return 编码后的字节数组
  161 + */
  162 + public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value,
  163 + IotModbusFrameFormatEnum format, int transactionId) {
  164 + // PDU: [FC(1)] [Address(2)] [Value(2)]
  165 + byte[] pdu = new byte[5];
  166 + pdu[0] = (byte) functionCode;
  167 + pdu[1] = (byte) ((address >> 8) & 0xFF);
  168 + pdu[2] = (byte) (address & 0xFF);
  169 + pdu[3] = (byte) ((value >> 8) & 0xFF);
  170 + pdu[4] = (byte) (value & 0xFF);
  171 + return wrapFrame(slaveId, pdu, format, transactionId);
  172 + }
  173 +
  174 + /**
  175 + * 编码写多个寄存器请求(FC16)
  176 + *
  177 + * @param slaveId 从站地址
  178 + * @param address 起始地址
  179 + * @param values 值数组
  180 + * @param format 帧格式
  181 + * @param transactionId 事务 ID
  182 + * @return 编码后的字节数组
  183 + */
  184 + public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values,
  185 + IotModbusFrameFormatEnum format, int transactionId) {
  186 + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)]
  187 + int quantity = values.length;
  188 + int byteCount = quantity * 2;
  189 + byte[] pdu = new byte[6 + byteCount];
  190 + pdu[0] = (byte) 16; // FC16
  191 + pdu[1] = (byte) ((address >> 8) & 0xFF);
  192 + pdu[2] = (byte) (address & 0xFF);
  193 + pdu[3] = (byte) ((quantity >> 8) & 0xFF);
  194 + pdu[4] = (byte) (quantity & 0xFF);
  195 + pdu[5] = (byte) byteCount;
  196 + for (int i = 0; i < quantity; i++) {
  197 + pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
  198 + pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF);
  199 + }
  200 + return wrapFrame(slaveId, pdu, format, transactionId);
  201 + }
  202 +
  203 + /**
  204 + * 编码自定义功能码帧(认证响应等)
  205 + *
  206 + * @param slaveId 从站地址
  207 + * @param jsonData JSON 数据
  208 + * @param format 帧格式
  209 + * @param transactionId 事务 ID
  210 + * @return 编码后的字节数组
  211 + */
  212 + public byte[] encodeCustomFrame(int slaveId, String jsonData,
  213 + IotModbusFrameFormatEnum format, int transactionId) {
  214 + byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
  215 + // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)]
  216 + byte[] pdu = new byte[2 + jsonBytes.length];
  217 + pdu[0] = (byte) customFunctionCode;
  218 + pdu[1] = (byte) jsonBytes.length;
  219 + System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length);
  220 + return wrapFrame(slaveId, pdu, format, transactionId);
  221 + }
  222 +
  223 + // ==================== 帧封装 ====================
  224 +
  225 + /**
  226 + * 将 PDU 封装为完整帧
  227 + *
  228 + * @param slaveId 从站地址
  229 + * @param pdu PDU 数据(含 functionCode)
  230 + * @param format 帧格式
  231 + * @param transactionId 事务 ID(TCP 模式下使用)
  232 + * @return 完整帧字节数组
  233 + */
  234 + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) {
  235 + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
  236 + return wrapTcpFrame(slaveId, pdu, transactionId);
  237 + } else {
  238 + return wrapRtuFrame(slaveId, pdu);
  239 + }
  240 + }
  241 +
  242 + /**
  243 + * 封装 MODBUS_TCP 帧
  244 + * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...]
  245 + */
  246 + private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) {
  247 + int length = 1 + pdu.length; // UnitId + PDU
  248 + byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU
  249 + // MBAP Header
  250 + frame[0] = (byte) ((transactionId >> 8) & 0xFF);
  251 + frame[1] = (byte) (transactionId & 0xFF);
  252 + frame[2] = 0; // Protocol ID high
  253 + frame[3] = 0; // Protocol ID low
  254 + frame[4] = (byte) ((length >> 8) & 0xFF);
  255 + frame[5] = (byte) (length & 0xFF);
  256 + // Unit ID
  257 + frame[6] = (byte) slaveId;
  258 + // PDU
  259 + System.arraycopy(pdu, 0, frame, 7, pdu.length);
  260 + return frame;
  261 + }
  262 +
  263 + /**
  264 + * 封装 MODBUS_RTU 帧
  265 + * [SlaveId(1)] [PDU...] [CRC(2)]
  266 + */
  267 + private byte[] wrapRtuFrame(int slaveId, byte[] pdu) {
  268 + byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC
  269 + frame[0] = (byte) slaveId;
  270 + System.arraycopy(pdu, 0, frame, 1, pdu.length);
  271 + // 计算并追加 CRC16
  272 + int crc = calculateCrc16(frame, frame.length - 2);
  273 + frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low
  274 + frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
  275 + return frame;
  276 + }
  277 +
  278 + // ==================== CRC16 工具 ====================
  279 +
  280 + // TODO @AI:hutool 等,有没工具类可以用
  281 + /**
  282 + * 计算 CRC-16/MODBUS
  283 + *
  284 + * @param data 数据
  285 + * @param length 计算长度
  286 + * @return CRC16 值
  287 + */
  288 + public static int calculateCrc16(byte[] data, int length) {
  289 + int crc = 0xFFFF;
  290 + for (int i = 0; i < length; i++) {
  291 + crc ^= (data[i] & 0xFF);
  292 + for (int j = 0; j < 8; j++) {
  293 + if ((crc & 0x0001) != 0) {
  294 + crc >>= 1;
  295 + crc ^= 0xA001;
  296 + } else {
  297 + crc >>= 1;
  298 + }
  299 + }
  300 + }
  301 + return crc;
  302 + }
  303 +
  304 + // TODO @AI:hutool 等,有没工具类可以用
  305 + /**
  306 + * 校验 CRC16
  307 + */
  308 + private boolean verifyCrc16(byte[] data) {
  309 + if (data.length < 3) {
  310 + return false;
  311 + }
  312 + int computed = calculateCrc16(data, data.length - 2);
  313 + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
  314 + return computed == received;
  315 + }
  316 +
  317 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import io.vertx.core.Handler;
  5 +import io.vertx.core.buffer.Buffer;
  6 +import io.vertx.core.parsetools.RecordParser;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +import java.util.function.Consumer;
  10 +
  11 +// TODO @AI:看看是不是不要搞成 factory,而是直接 new;(可以一起讨论下)
  12 +/**
  13 + * IoT Modbus RecordParser 工厂
  14 + * <p>
  15 + * 创建带自动帧格式检测的 RecordParser:
  16 + * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU
  17 + * 2. 检测后自动切换到对应的拆包模式
  18 + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动)
  19 + * - MODBUS_RTU:功能码驱动的状态机
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +@Slf4j
  24 +public class IotModbusRecordParserFactory {
  25 +
  26 + /**
  27 + * 创建带自动帧格式检测的 RecordParser
  28 + *
  29 + * @param customFunctionCode 自定义功能码
  30 + * @param frameHandler 完整帧回调
  31 + * @param onFormatDetected 帧格式检测回调
  32 + * @return RecordParser 实例
  33 + */
  34 + public static RecordParser create(int customFunctionCode,
  35 + Handler<Buffer> frameHandler,
  36 + Consumer<IotModbusFrameFormatEnum> onFormatDetected) {
  37 + // 先创建一个 RecordParser,使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测
  38 + // TODO @AI:最小需要 6 个字节么?有可能更小的情况下,就探测出来?!
  39 + RecordParser parser = RecordParser.newFixed(6);
  40 + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, onFormatDetected));
  41 + return parser;
  42 + }
  43 +
  44 + /**
  45 + * 帧格式检测阶段 Handler
  46 + */
  47 + @SuppressWarnings("ClassCanBeRecord")
  48 + private static class DetectPhaseHandler implements Handler<Buffer> {
  49 +
  50 + private final RecordParser parser;
  51 + private final int customFunctionCode;
  52 + private final Handler<Buffer> frameHandler;
  53 + private final Consumer<IotModbusFrameFormatEnum> onFormatDetected;
  54 +
  55 + // TODO @AI:简化构造方法,使用 lombok;
  56 + DetectPhaseHandler(RecordParser parser, int customFunctionCode,
  57 + Handler<Buffer> frameHandler,
  58 + Consumer<IotModbusFrameFormatEnum> onFormatDetected) {
  59 + this.parser = parser;
  60 + this.customFunctionCode = customFunctionCode;
  61 + this.frameHandler = frameHandler;
  62 + this.onFormatDetected = onFormatDetected;
  63 + }
  64 +
  65 + @Override
  66 + public void handle(Buffer buffer) {
  67 + byte[] header = buffer.getBytes();
  68 + // 检测:byte[2]==0x00 && byte[3]==0x00 && 1<=length<=253
  69 + int protocolId = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF);
  70 + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF);
  71 +
  72 + if (protocolId == 0x0000 && length >= 1 && length <= 253) {
  73 + // MODBUS_TCP
  74 + log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]");
  75 + onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_TCP);
  76 + // 切换到 TCP 拆包模式,处理当前首帧
  77 + TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler);
  78 + parser.handler(tcpHandler);
  79 + // 当前 header 是 MBAP 的前 6 字节,需要继续读 length 字节
  80 + tcpHandler.handleMbapHeader(header, length);
  81 + } else {
  82 + // MODBUS_RTU
  83 + log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]");
  84 + onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_RTU);
  85 + // 切换到 RTU 拆包模式,处理当前首帧
  86 + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, customFunctionCode, frameHandler);
  87 + parser.handler(rtuHandler);
  88 + // 当前 header 包含前 6 字节(slaveId + FC + 部分数据),需要拼接处理
  89 + rtuHandler.handleInitialBytes(header);
  90 + }
  91 + }
  92 + }
  93 +
  94 + /**
  95 + * MODBUS_TCP 拆包 Handler(两阶段 RecordParser)
  96 + * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length
  97 + * Phase 2: fixedSizeMode(length) → 读 unitId + PDU
  98 + */
  99 + private static class TcpFrameHandler implements Handler<Buffer> {
  100 +
  101 + private final RecordParser parser;
  102 + private final Handler<Buffer> frameHandler;
  103 + private byte[] mbapHeader;
  104 + private boolean waitingForBody = false;
  105 +
  106 + // TODO @AI:lombok
  107 + TcpFrameHandler(RecordParser parser, Handler<Buffer> frameHandler) {
  108 + this.parser = parser;
  109 + this.frameHandler = frameHandler;
  110 + }
  111 +
  112 + /**
  113 + * 处理首帧的 MBAP 头
  114 + */
  115 + void handleMbapHeader(byte[] header, int length) {
  116 + this.mbapHeader = header;
  117 + this.waitingForBody = true;
  118 + parser.fixedSizeMode(length);
  119 + }
  120 +
  121 + @Override
  122 + public void handle(Buffer buffer) {
  123 + if (waitingForBody) {
  124 + // Phase 2: 收到 body(unitId + PDU)
  125 + byte[] body = buffer.getBytes();
  126 + // 拼接完整帧:MBAP(6) + body
  127 + Buffer frame = Buffer.buffer(mbapHeader.length + body.length);
  128 + frame.appendBytes(mbapHeader);
  129 + frame.appendBytes(body);
  130 + frameHandler.handle(frame);
  131 + // 切回 Phase 1
  132 + waitingForBody = false;
  133 + mbapHeader = null;
  134 + parser.fixedSizeMode(6);
  135 + } else {
  136 + // Phase 1: 收到 MBAP 头 6 字节
  137 + byte[] header = buffer.getBytes();
  138 + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF);
  139 + if (length < 1 || length > 253) {
  140 + log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length);
  141 + parser.fixedSizeMode(6);
  142 + return;
  143 + }
  144 + this.mbapHeader = header;
  145 + this.waitingForBody = true;
  146 + parser.fixedSizeMode(length);
  147 + }
  148 + }
  149 + }
  150 +
  151 + /**
  152 + * MODBUS_RTU 拆包 Handler(功能码驱动的状态机)
  153 + * <p>
  154 + * 状态机流程:
  155 + * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode
  156 + * Phase 2: 根据 functionCode 确定剩余长度:
  157 + * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2)
  158 + * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
  159 + * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
  160 + * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
  161 + */
  162 + private static class RtuFrameHandler implements Handler<Buffer> {
  163 +
  164 + private static final int STATE_HEADER = 0;
  165 + private static final int STATE_EXCEPTION_BODY = 1;
  166 + private static final int STATE_READ_BYTE_COUNT = 2;
  167 + private static final int STATE_READ_DATA = 3;
  168 + private static final int STATE_WRITE_BODY = 4;
  169 +
  170 + private final RecordParser parser;
  171 + private final int customFunctionCode;
  172 + private final Handler<Buffer> frameHandler;
  173 +
  174 + private int state = STATE_HEADER;
  175 + private byte slaveId;
  176 + private byte functionCode;
  177 + private byte byteCount;
  178 +
  179 + // TODO @AI:lombok
  180 + RtuFrameHandler(RecordParser parser, int customFunctionCode, Handler<Buffer> frameHandler) {
  181 + this.parser = parser;
  182 + this.customFunctionCode = customFunctionCode;
  183 + this.frameHandler = frameHandler;
  184 + }
  185 +
  186 + /**
  187 + * 处理首帧检测阶段传来的初始 6 字节
  188 + * 由于 RTU 首帧跳过了格式检测,我们需要拼接处理
  189 + */
  190 + void handleInitialBytes(byte[] initialBytes) {
  191 + // initialBytes 包含 6 字节:[slaveId][FC][...4 bytes...]
  192 + this.slaveId = initialBytes[0];
  193 + this.functionCode = initialBytes[1];
  194 + int fc = functionCode & 0xFF;
  195 +
  196 + // 根据功能码,确定还需要多少字节
  197 + if ((fc & 0x80) != 0) {
  198 + // 异常响应:还需要 exceptionCode(1) + CRC(2) = 3 字节
  199 + // 我们已经有 4 字节剩余(initialBytes[2..5]),足够
  200 + // 拼接完整帧并交付
  201 + // 完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5
  202 + Buffer frame = Buffer.buffer(5);
  203 + frame.appendByte(slaveId);
  204 + frame.appendByte(functionCode);
  205 + frame.appendBytes(initialBytes, 2, 3); // exceptionCode + CRC
  206 + frameHandler.handle(frame);
  207 + // 剩余 1 字节需要留给下一帧,但 RecordParser 不支持回推
  208 + // 简化处理:重置状态,开始读下一帧
  209 + resetToHeader();
  210 + } else if (isReadResponse(fc) || fc == customFunctionCode) {
  211 + // 读响应或自定义 FC:initialBytes[2] = byteCount
  212 + this.byteCount = initialBytes[2];
  213 + int bc = byteCount & 0xFF;
  214 + // 已有数据:initialBytes[3..5] = 3 字节
  215 + // 还需:byteCount + CRC(2) - 3 字节已有
  216 + int remaining = bc + 2 - 3;
  217 + if (remaining <= 0) {
  218 + // 数据已足够,组装完整帧
  219 + int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC
  220 + Buffer frame = Buffer.buffer(totalLen);
  221 + frame.appendByte(slaveId);
  222 + frame.appendByte(functionCode);
  223 + frame.appendByte(byteCount);
  224 + frame.appendBytes(initialBytes, 3, bc + 2); // data + CRC
  225 + frameHandler.handle(frame);
  226 + resetToHeader();
  227 + } else {
  228 + // 需要继续读
  229 + state = STATE_READ_DATA;
  230 + // 保存已有数据片段
  231 + parser.fixedSizeMode(remaining);
  232 + // 在 handle() 中需要拼接 initialBytes[3..5] + 新读取的数据
  233 + // 为了简化,我们用一个 Buffer 暂存
  234 + this.pendingData = Buffer.buffer();
  235 + this.pendingData.appendBytes(initialBytes, 3, 3);
  236 + this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC
  237 + }
  238 + } else if (isWriteResponse(fc)) {
  239 + // 写响应:FC05/06/15/16,总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8
  240 + // 已有 6 字节,还需 2 字节
  241 + state = STATE_WRITE_BODY;
  242 + this.pendingData = Buffer.buffer();
  243 + this.pendingData.appendBytes(initialBytes, 2, 4); // 4 bytes already
  244 + parser.fixedSizeMode(2); // need 2 more bytes (CRC)
  245 + } else {
  246 + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
  247 + resetToHeader();
  248 + }
  249 + }
  250 +
  251 + private Buffer pendingData;
  252 + private int expectedDataLen;
  253 +
  254 + @Override
  255 + public void handle(Buffer buffer) {
  256 + switch (state) {
  257 + case STATE_HEADER:
  258 + handleHeader(buffer);
  259 + break;
  260 + case STATE_EXCEPTION_BODY:
  261 + handleExceptionBody(buffer);
  262 + break;
  263 + case STATE_READ_BYTE_COUNT:
  264 + handleReadByteCount(buffer);
  265 + break;
  266 + case STATE_READ_DATA:
  267 + handleReadData(buffer);
  268 + break;
  269 + case STATE_WRITE_BODY:
  270 + handleWriteBody(buffer);
  271 + break;
  272 + default:
  273 + resetToHeader();
  274 + }
  275 + }
  276 +
  277 + private void handleHeader(Buffer buffer) {
  278 + byte[] header = buffer.getBytes();
  279 + this.slaveId = header[0];
  280 + this.functionCode = header[1];
  281 + int fc = functionCode & 0xFF;
  282 +
  283 + if ((fc & 0x80) != 0) {
  284 + // 异常响应
  285 + state = STATE_EXCEPTION_BODY;
  286 + parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
  287 + } else if (isReadResponse(fc) || fc == customFunctionCode) {
  288 + // 读响应或自定义 FC
  289 + state = STATE_READ_BYTE_COUNT;
  290 + parser.fixedSizeMode(1); // byteCount
  291 + } else if (isWriteResponse(fc)) {
  292 + // 写响应
  293 + state = STATE_WRITE_BODY;
  294 + pendingData = Buffer.buffer();
  295 + parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2)
  296 + } else {
  297 + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
  298 + resetToHeader();
  299 + }
  300 + }
  301 +
  302 + private void handleExceptionBody(Buffer buffer) {
  303 + // buffer = exceptionCode(1) + CRC(2)
  304 + Buffer frame = Buffer.buffer();
  305 + frame.appendByte(slaveId);
  306 + frame.appendByte(functionCode);
  307 + frame.appendBuffer(buffer);
  308 + frameHandler.handle(frame);
  309 + resetToHeader();
  310 + }
  311 +
  312 + private void handleReadByteCount(Buffer buffer) {
  313 + this.byteCount = buffer.getByte(0);
  314 + int bc = byteCount & 0xFF;
  315 + state = STATE_READ_DATA;
  316 + pendingData = Buffer.buffer();
  317 + expectedDataLen = bc + 2; // data(bc) + CRC(2)
  318 + parser.fixedSizeMode(expectedDataLen);
  319 + }
  320 +
  321 + private void handleReadData(Buffer buffer) {
  322 + pendingData.appendBuffer(buffer);
  323 + if (pendingData.length() >= expectedDataLen) {
  324 + // 组装完整帧
  325 + Buffer frame = Buffer.buffer();
  326 + frame.appendByte(slaveId);
  327 + frame.appendByte(functionCode);
  328 + frame.appendByte(byteCount);
  329 + frame.appendBuffer(pendingData);
  330 + frameHandler.handle(frame);
  331 + resetToHeader();
  332 + }
  333 + // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode)
  334 + }
  335 +
  336 + private void handleWriteBody(Buffer buffer) {
  337 + pendingData.appendBuffer(buffer);
  338 + // 完整帧
  339 + Buffer frame = Buffer.buffer();
  340 + frame.appendByte(slaveId);
  341 + frame.appendByte(functionCode);
  342 + frame.appendBuffer(pendingData);
  343 + frameHandler.handle(frame);
  344 + resetToHeader();
  345 + }
  346 +
  347 + private void resetToHeader() {
  348 + state = STATE_HEADER;
  349 + pendingData = null;
  350 + parser.fixedSizeMode(2); // slaveId + FC
  351 + }
  352 +
  353 + private boolean isReadResponse(int fc) {
  354 + return fc >= 1 && fc <= 4;
  355 + }
  356 +
  357 + private boolean isWriteResponse(int fc) {
  358 + return fc == 5 || fc == 6 || fc == 15 || fc == 16;
  359 + }
  360 + }
  361 +
  362 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +
  5 +/**
  6 + * IoT Modbus 响应值提取器
  7 + * <p>
  8 + * 从解码后的 IotModbusFrame 中提取寄存器值,用于后续的点位翻译。
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Slf4j
  13 +public class IotModbusResponseParser {
  14 +
  15 + /**
  16 + * 从帧中提取寄存器值(FC01-04 读响应)
  17 + *
  18 + * @param frame 解码后的 Modbus 帧
  19 + * @return 寄存器值数组(int[]),失败返回 null
  20 + */
  21 + @SuppressWarnings("EnhancedSwitchMigration")
  22 + public static int[] extractValues(IotModbusFrame frame) {
  23 + if (frame == null || frame.isException()) {
  24 + return null;
  25 + }
  26 + byte[] pdu = frame.getPdu();
  27 + if (pdu == null || pdu.length < 1) {
  28 + return null;
  29 + }
  30 +
  31 + // TODO @AI:jmodbus 看看有没可以复用的枚举类
  32 + int functionCode = frame.getFunctionCode();
  33 + switch (functionCode) {
  34 + case 1: // Read Coils
  35 + case 2: // Read Discrete Inputs
  36 + return extractCoilValues(pdu);
  37 + case 3: // Read Holding Registers
  38 + case 4: // Read Input Registers
  39 + return extractRegisterValues(pdu);
  40 + default:
  41 + log.warn("[extractValues][不支持的功能码: {}]", functionCode);
  42 + return null;
  43 + }
  44 + }
  45 +
  46 + /**
  47 + * 提取线圈/离散输入值
  48 + * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)]
  49 + */
  50 + private static int[] extractCoilValues(byte[] pdu) {
  51 + if (pdu.length < 2) {
  52 + return null;
  53 + }
  54 + int byteCount = pdu[0] & 0xFF;
  55 + int bitCount = byteCount * 8;
  56 + int[] values = new int[bitCount];
  57 + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
  58 + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
  59 + }
  60 + return values;
  61 + }
  62 +
  63 + /**
  64 + * 提取寄存器值
  65 + * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)]
  66 + */
  67 + private static int[] extractRegisterValues(byte[] pdu) {
  68 + if (pdu.length < 2) {
  69 + return null;
  70 + }
  71 + int byteCount = pdu[0] & 0xFF;
  72 + int registerCount = byteCount / 2;
  73 + int[] values = new int[registerCount];
  74 + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
  75 + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
  76 + }
  77 + return values;
  78 + }
  79 +
  80 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  8 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
  15 +import lombok.RequiredArgsConstructor;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +
  18 +import java.util.Map;
  19 +import java.util.concurrent.atomic.AtomicInteger;
  20 +
  21 +// 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 有一些复用逻辑;
  22 +/**
  23 + * IoT Modbus TCP Slave 下行消息处理器
  24 + * <p>
  25 + * 负责:
  26 + * 1. 处理下行消息(如属性设置 thing.service.property.set)
  27 + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@RequiredArgsConstructor
  32 +@Slf4j
  33 +public class IotModbusTcpSlaveDownstreamHandler {
  34 +
  35 + private final IotModbusTcpSlaveConnectionManager connectionManager;
  36 + private final IotModbusTcpSlaveConfigCacheService configCacheService;
  37 + private final IotModbusDataConverter dataConverter;
  38 + private final IotModbusFrameCodec frameCodec;
  39 +
  40 + /**
  41 + * TCP 事务 ID 自增器
  42 + */
  43 + private final AtomicInteger transactionIdCounter = new AtomicInteger(0);
  44 +
  45 + /**
  46 + * 处理下行消息
  47 + */
  48 + @SuppressWarnings("unchecked")
  49 + public void handle(IotDeviceMessage message) {
  50 + // 1.1 检查是否是属性设置消息
  51 + if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) {
  52 + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod());
  53 + return;
  54 + }
  55 + // 1.2 获取设备配置
  56 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
  57 + if (config == null) {
  58 + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
  59 + return;
  60 + }
  61 + // 1.3 获取连接信息
  62 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
  63 + if (connInfo == null) {
  64 + log.warn("[handle][设备 {} 没有连接]", message.getDeviceId());
  65 + return;
  66 + }
  67 +
  68 + // 2. 解析属性值并写入
  69 + Object params = message.getParams();
  70 + if (!(params instanceof Map)) {
  71 + log.warn("[handle][params 不是 Map 类型: {}]", params);
  72 + return;
  73 + }
  74 + Map<String, Object> propertyMap = (Map<String, Object>) params;
  75 + for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
  76 + String identifier = entry.getKey();
  77 + Object value = entry.getValue();
  78 + // 2.1 查找对应的点位配置
  79 + IotModbusPointRespDTO point = findPoint(config, identifier);
  80 + if (point == null) {
  81 + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
  82 + continue;
  83 + }
  84 + // 2.2 检查是否支持写操作
  85 + if (!isWritable(point.getFunctionCode())) {
  86 + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
  87 + continue;
  88 + }
  89 +
  90 + // 2.3 执行写入
  91 + writeProperty(config.getDeviceId(), connInfo, point, value);
  92 + }
  93 + }
  94 +
  95 + /**
  96 + * 写入属性值
  97 + */
  98 + private void writeProperty(Long deviceId, ConnectionInfo connInfo,
  99 + IotModbusPointRespDTO point, Object value) {
  100 + // 1. 转换属性值为原始值
  101 + int[] rawValues = dataConverter.convertToRawValues(value, point);
  102 +
  103 + // 2. 确定帧格式和事务 ID
  104 + IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
  105 + if (frameFormat == null) {
  106 + frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP;
  107 + }
  108 + int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF;
  109 + int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
  110 +
  111 + // 3. 编码写请求
  112 + byte[] data;
  113 + IotModbusFunctionCodeEnum fcEnum = IotModbusFunctionCodeEnum.valueOf(point.getFunctionCode());
  114 + if (fcEnum == null) {
  115 + log.warn("[writeProperty][未知功能码: {}]", point.getFunctionCode());
  116 + return;
  117 + }
  118 + if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) {
  119 + // 单个值:使用单写功能码(FC05/FC06)
  120 + data = frameCodec.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(),
  121 + point.getRegisterAddress(), rawValues[0], frameFormat, transactionId);
  122 + } else if (fcEnum.getWriteMultipleCode() != null) {
  123 + // 多个值:使用多写功能码(FC15/FC16)
  124 + data = frameCodec.encodeWriteMultipleRegistersRequest(slaveId,
  125 + point.getRegisterAddress(), rawValues, frameFormat, transactionId);
  126 + } else {
  127 + log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier());
  128 + return;
  129 + }
  130 +
  131 + // 4. 发送
  132 + connectionManager.sendToDevice(deviceId, data);
  133 + log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
  134 + deviceId, point.getIdentifier(), value);
  135 + }
  136 +
  137 + /**
  138 + * 查找点位配置
  139 + */
  140 + private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
  141 + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
  142 + }
  143 +
  144 + /**
  145 + * 检查功能码是否支持写操作
  146 + */
  147 + private boolean isWritable(Integer functionCode) {
  148 + IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode);
  149 + return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable());
  150 + }
  151 +
  152 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
  5 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  6 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol;
  8 +import lombok.RequiredArgsConstructor;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +// 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
  12 +/**
  13 + * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@RequiredArgsConstructor
  18 +@Slf4j
  19 +public class IotModbusTcpSlaveDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
  20 +
  21 + private final IotModbusTcpSlaveProtocol protocol;
  22 + private final IotModbusTcpSlaveDownstreamHandler downstreamHandler;
  23 + private final IotMessageBus messageBus;
  24 +
  25 + /**
  26 + * 启动订阅
  27 + */
  28 + public void start() {
  29 + messageBus.register(this);
  30 + log.info("[start][Modbus TCP Slave 下行消息订阅器已启动, topic={}]", getTopic());
  31 + }
  32 +
  33 + /**
  34 + * 停止订阅
  35 + */
  36 + public void stop() {
  37 + messageBus.unregister(this);
  38 + log.info("[stop][Modbus TCP Slave 下行消息订阅器已停止]");
  39 + }
  40 +
  41 + @Override
  42 + public String getTopic() {
  43 + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
  44 + }
  45 +
  46 + @Override
  47 + public String getGroup() {
  48 + return getTopic(); // 点对点消费
  49 + }
  50 +
  51 + @Override
  52 + public void onMessage(IotDeviceMessage message) {
  53 + log.debug("[onMessage][收到下行消息: {}]", message);
  54 + try {
  55 + downstreamHandler.handle(message);
  56 + } catch (Exception e) {
  57 + log.error("[onMessage][处理下行消息失败]", e);
  58 + }
  59 + }
  60 +
  61 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.extra.spring.SpringUtil;
  7 +import cn.hutool.json.JSONObject;
  8 +import cn.hutool.json.JSONUtil;
  9 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  10 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  11 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  13 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  14 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  15 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  16 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
  17 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  18 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  19 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  20 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  21 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
  22 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
  23 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusResponseParser;
  24 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
  25 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
  26 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
  27 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager;
  28 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest;
  29 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  30 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  31 +import io.vertx.core.net.NetSocket;
  32 +import lombok.Setter;
  33 +import lombok.extern.slf4j.Slf4j;
  34 +
  35 +import java.util.Map;
  36 +import java.util.function.BiConsumer;
  37 +
  38 +// TODO @AI:逻辑有点多,看看是不是分区域!
  39 +/**
  40 + * IoT Modbus TCP Slave 上行数据处理器
  41 + * <p>
  42 + * 处理:
  43 + * 1. 自定义 FC 认证
  44 + * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post
  45 + * 3. 主动上报(mode=2)→ 透传 property.report TODO @AI:这种模式,应该不用支持;因为主动上报,都走标准的 tcp 即可;
  46 + *
  47 + * @author 芋道源码
  48 + */
  49 +@Slf4j
  50 +public class IotModbusTcpSlaveUpstreamHandler {
  51 +
  52 + private final IotDeviceCommonApi deviceApi;
  53 + private final IotDeviceMessageService messageService;
  54 + private final IotModbusDataConverter dataConverter;
  55 + private final IotModbusFrameCodec frameCodec;
  56 + private final IotModbusTcpSlaveConnectionManager connectionManager;
  57 + private final IotModbusTcpSlaveConfigCacheService configCacheService;
  58 + private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
  59 + private final String serverId;
  60 +
  61 + /**
  62 + * 认证成功回调:(deviceId, config) → 启动轮询等
  63 + */
  64 + @Setter
  65 + private BiConsumer<Long, IotModbusDeviceConfigRespDTO> onAuthSuccess;
  66 +
  67 + public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi,
  68 + IotDeviceMessageService messageService,
  69 + IotModbusDataConverter dataConverter,
  70 + IotModbusFrameCodec frameCodec,
  71 + IotModbusTcpSlaveConnectionManager connectionManager,
  72 + IotModbusTcpSlaveConfigCacheService configCacheService,
  73 + IotModbusTcpSlavePendingRequestManager pendingRequestManager,
  74 + String serverId) {
  75 + this.deviceApi = deviceApi;
  76 + this.messageService = messageService;
  77 + this.dataConverter = dataConverter;
  78 + this.frameCodec = frameCodec;
  79 + this.connectionManager = connectionManager;
  80 + this.configCacheService = configCacheService;
  81 + this.pendingRequestManager = pendingRequestManager;
  82 + this.serverId = serverId;
  83 + }
  84 +
  85 + /**
  86 + * 处理帧
  87 + */
  88 + public void handleFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
  89 + if (frame == null) {
  90 + return;
  91 + }
  92 + // 1.1 自定义功能码(认证等扩展)
  93 + if (StrUtil.isNotEmpty(frame.getCustomData())) {
  94 + handleCustomFrame(socket, frame, frameFormat);
  95 + return;
  96 + }
  97 + // 1.2 异常响应
  98 + if (frame.isException()) {
  99 + // TODO @AI:这种需要返回一个结果给 modbus client?
  100 + log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]",
  101 + frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode());
  102 + return;
  103 + }
  104 + // 1.3 未认证连接,丢弃
  105 + if (!connectionManager.isAuthenticated(socket)) {
  106 + // TODO @AI:这种需要返回一个结果给 modbus client?
  107 + log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress());
  108 + return;
  109 + }
  110 +
  111 + // TODO @AI:获取不到,看看要不要也打个告警;然后
  112 + // 2. 标准 Modbus 响应
  113 + ConnectionInfo info = connectionManager.getConnectionInfo(socket);
  114 + if (info == null) {
  115 + return;
  116 + }
  117 + // TODO @AI:可以断言下,必须是云端轮询;
  118 + if (info.getMode() != null && info.getMode().equals(IotModbusModeEnum.ACTIVE_REPORT.getMode())) {
  119 + // mode=2:主动上报,透传
  120 + handleActiveReport(info, frame);
  121 + } else {
  122 + // mode=1:云端轮询,匹配 PendingRequest
  123 + handlePollingResponse(info, frame, frameFormat);
  124 + }
  125 + }
  126 +
  127 + /**
  128 + * 处理自定义功能码帧
  129 + */
  130 + private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
  131 + try {
  132 + // TODO @AI:直接使用 JsonUtils 去解析出 IotDeviceMessage
  133 + JSONObject json = JSONUtil.parseObj(frame.getCustomData());
  134 + String method = json.getStr("method");
  135 + // TODO @AI: method 枚举下;
  136 + if ("auth".equals(method)) {
  137 + handleAuth(socket, frame, json, frameFormat);
  138 + return;
  139 + }
  140 + // TODO @AI:把 frame 都打印下;
  141 + log.warn("[handleCustomFrame][未知 method: {}]", method);
  142 + } catch (Exception e) {
  143 + // TODO @AI:各种情况的翻译;看看怎么弄比较合适;是不是要用 fc 自定义的 callback 下?
  144 + log.error("[handleCustomFrame][解析自定义 FC 数据失败]", e);
  145 + }
  146 + }
  147 +
  148 + /**
  149 + * 处理认证请求
  150 + */
  151 + private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json,
  152 + IotModbusFrameFormatEnum frameFormat) {
  153 + // TODO @AI:参数为空的校验;
  154 + JSONObject params = json.getJSONObject("params");
  155 + if (params == null) {
  156 + sendAuthResponse(socket, frame, frameFormat, 1, "params 为空");
  157 + return;
  158 + }
  159 + // TODO @AI:参数判空;
  160 + String clientId = params.getStr("clientId");
  161 + String username = params.getStr("username");
  162 + String password = params.getStr("password");
  163 +
  164 + try {
  165 + // 1. 调用认证 API
  166 + IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO()
  167 + .setClientId(clientId).setUsername(username).setPassword(password);
  168 + CommonResult<Boolean> authResult = deviceApi.authDevice(authReq);
  169 + // TODO @AI:应该不用 close 吧?!
  170 + // TODO @AI:BooleanUtils.isFalse
  171 + if (authResult == null || !authResult.isSuccess() || !Boolean.TRUE.equals(authResult.getData())) {
  172 + log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username);
  173 + sendAuthResponse(socket, frame, frameFormat, 1, "认证失败");
  174 + socket.close();
  175 + return;
  176 + }
  177 +
  178 + // 2. 认证成功,查找设备配置(通过 username 作为 deviceName 查找)
  179 + // TODO 根据实际的认证模型优化查找逻辑
  180 + // TODO @AI:通过 device
  181 + IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password);
  182 + if (config == null) {
  183 + // 退而求其次,遍历缓存查找
  184 + log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username);
  185 + }
  186 + // 2.2 解析设备信息
  187 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  188 + Assert.notNull(deviceInfo, "解析设备信息失败");
  189 + // 2.3 获取设备信息
  190 + // TODO @AI:这里要优化下,不要通过 spring 这样注入;
  191 + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
  192 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  193 + Assert.notNull(device, "设备不存在");
  194 + // TODO @AI:校验 frameFormat 是否一致;不一致,连接也失败;
  195 +
  196 + // 3. 注册连接
  197 + ConnectionInfo connectionInfo = new ConnectionInfo()
  198 + .setDeviceId(device.getId())
  199 + .setSlaveId(frame.getSlaveId())
  200 + .setFrameFormat(frameFormat)
  201 + .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode());
  202 +
  203 + if (config != null) {
  204 + connectionInfo.setDeviceId(config.getDeviceId())
  205 + .setProductKey(config.getProductKey())
  206 + .setDeviceName(config.getDeviceName());
  207 + }
  208 + connectionManager.registerConnection(socket, connectionInfo);
  209 +
  210 + // 4. 发送认证成功响应
  211 + sendAuthResponse(socket, frame, frameFormat, 0, "success");
  212 + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId,
  213 + config != null ? config.getDeviceId() : "unknown");
  214 +
  215 + // 5. 回调:启动轮询等
  216 + // TODO @AI:是不是不要 callback,而是主动调用!
  217 + if (onAuthSuccess != null && config != null) {
  218 + onAuthSuccess.accept(config.getDeviceId(), config);
  219 + }
  220 + } catch (Exception e) {
  221 + log.error("[handleAuth][认证异常]", e);
  222 + sendAuthResponse(socket, frame, frameFormat, 1, "认证异常");
  223 + socket.close();
  224 + }
  225 + }
  226 +
  227 + /**
  228 + * 发送认证响应
  229 + */
  230 + private void sendAuthResponse(NetSocket socket, IotModbusFrame frame,
  231 + IotModbusFrameFormatEnum frameFormat,
  232 + int code, String message) {
  233 + // TODO @AI:不一定用 auth response;而是 custom?
  234 + JSONObject resp = new JSONObject();
  235 + resp.set("method", "auth");
  236 + resp.set("code", code);
  237 + resp.set("message", message);
  238 + byte[] data = frameCodec.encodeCustomFrame(frame.getSlaveId(), resp.toString(),
  239 + frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0);
  240 + connectionManager.sendToSocket(socket, data);
  241 + }
  242 +
  243 + /**
  244 + * 处理轮询响应(mode=1)
  245 + */
  246 + private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame,
  247 + IotModbusFrameFormatEnum frameFormat) {
  248 + // 1.1 匹配 PendingRequest
  249 + PendingRequest request = pendingRequestManager.matchResponse(
  250 + info.getDeviceId(), frame, frameFormat);
  251 + if (request == null) {
  252 + log.debug("[handlePollingResponse][未匹配到 PendingRequest, deviceId={}, FC={}]",
  253 + info.getDeviceId(), frame.getFunctionCode());
  254 + return;
  255 + }
  256 + // 1.2 提取寄存器值
  257 + int[] rawValues = IotModbusResponseParser.extractValues(frame);
  258 + if (rawValues == null) {
  259 + log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]",
  260 + info.getDeviceId(), request.getIdentifier());
  261 + return;
  262 + }
  263 + // 1.3 查找点位配置
  264 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId());
  265 + if (config == null || config.getPoints() == null) {
  266 + return;
  267 + }
  268 + // TODO @AI:findone arrayUtil;
  269 + var point = config.getPoints().stream()
  270 + .filter(p -> p.getId().equals(request.getPointId()))
  271 + .findFirst().orElse(null);
  272 + if (point == null) {
  273 + return;
  274 + }
  275 +
  276 + // TODO @AI:拆成 2.1、2.2
  277 + // 4. 点位翻译 → 上报
  278 + Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point);
  279 + Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
  280 + IotDeviceMessage message = IotDeviceMessage.requestOf(
  281 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
  282 + messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId);
  283 + log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]",
  284 + info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue);
  285 + }
  286 +
  287 + // TODO @AI:不需要这个逻辑;
  288 + /**
  289 + * 处理主动上报(mode=2)
  290 + * 设备直接上报 property.report 格式:{propertyId: value},不做点位翻译
  291 + */
  292 + @SuppressWarnings("unchecked")
  293 + private void handleActiveReport(ConnectionInfo info, IotModbusFrame frame) {
  294 + // mode=2 下设备上报标准 Modbus 帧,但由于没有点位翻译,
  295 + // 这里暂时将原始寄存器值以 FC+地址 为 key 上报
  296 + int[] rawValues = IotModbusResponseParser.extractValues(frame);
  297 + if (rawValues == null) {
  298 + return;
  299 + }
  300 +
  301 + // 简单上报:以 "register_FC{fc}" 作为属性名
  302 + String propertyKey = "register_FC" + frame.getFunctionCode();
  303 + Map<String, Object> params = MapUtil.of(propertyKey, rawValues);
  304 + IotDeviceMessage message = IotDeviceMessage.requestOf(
  305 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
  306 + messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId);
  307 +
  308 + log.debug("[handleActiveReport][设备={}, FC={}, 原始值={}]",
  309 + info.getDeviceId(), frame.getFunctionCode(), rawValues);
  310 + }
  311 +
  312 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  7 +import lombok.RequiredArgsConstructor;
  8 +import lombok.extern.slf4j.Slf4j;
  9 +
  10 +import java.math.BigDecimal;
  11 +import java.util.*;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +import java.util.function.Consumer;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  16 +
  17 +// TODO @AI:和 IotModbusTcpConfigCacheService 基本一致?!
  18 +/**
  19 + * IoT Modbus TCP Slave 配置缓存服务
  20 + * <p>
  21 + * 负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@RequiredArgsConstructor
  26 +@Slf4j
  27 +public class IotModbusTcpSlaveConfigCacheService {
  28 +
  29 + private final IotDeviceCommonApi deviceApi;
  30 +
  31 + /**
  32 + * 配置缓存:deviceId -> 配置
  33 + */
  34 + private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
  35 +
  36 + // TODO @AI:它的 diff 算法,是不是不用和 IotModbusTcpConfigCacheService 完全一致;更多是1)首次连接时,查找;2)断开连接,移除;3)定时轮询更新;
  37 + /**
  38 + * 已知的设备 ID 集合
  39 + */
  40 + private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
  41 +
  42 + /**
  43 + * 刷新配置
  44 + *
  45 + * @return 最新的配置列表
  46 + */
  47 + public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
  48 + try {
  49 + // 1. 从远程获取配置
  50 + // TODO @AI:需要过滤下,只查找连接的设备列表;并且只有主动轮询的,才会处理;方法名,应该是 List 结尾;
  51 + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs();
  52 + if (result == null || !result.isSuccess() || result.getData() == null) {
  53 + log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result);
  54 + return new ArrayList<>(configCache.values());
  55 + }
  56 + List<IotModbusDeviceConfigRespDTO> configs = new ArrayList<>(result.getData());
  57 +
  58 + // 2. 追加 Mock 测试数据(一次性测试用途)
  59 + // TODO @芋艿:测试完成后移除
  60 + configs.addAll(buildMockConfigs());
  61 +
  62 + // 3. 更新缓存
  63 + for (IotModbusDeviceConfigRespDTO config : configs) {
  64 + configCache.put(config.getDeviceId(), config);
  65 + }
  66 + return configs;
  67 + } catch (Exception e) {
  68 + log.error("[refreshConfig][刷新配置失败]", e);
  69 + return new ArrayList<>(configCache.values());
  70 + }
  71 + }
  72 +
  73 + /**
  74 + * 构建 Mock 测试配置数据(一次性测试用途)
  75 + *
  76 + * 设备:PRODUCT_KEY=4aymZgOTOOCrDKRT, DEVICE_NAME=small
  77 + * 点位:temperature(FC03, 地址 0)、humidity(FC03, 地址 1)
  78 + *
  79 + * TODO @芋艿:测试完成后移除
  80 + */
  81 + private List<IotModbusDeviceConfigRespDTO> buildMockConfigs() {
  82 + IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO();
  83 + config.setDeviceId(1L);
  84 + config.setProductKey("4aymZgOTOOCrDKRT");
  85 + config.setDeviceName("small");
  86 + config.setSlaveId(1);
  87 + config.setMode(1); // 云端轮询
  88 + config.setFrameFormat("modbus_tcp");
  89 +
  90 + // 点位列表
  91 + List<IotModbusPointRespDTO> points = new ArrayList<>();
  92 +
  93 + // 点位 1:温度 - 保持寄存器 FC03, 地址 0, 1 个寄存器, INT16
  94 + IotModbusPointRespDTO point1 = new IotModbusPointRespDTO();
  95 + point1.setId(1L);
  96 + point1.setIdentifier("temperature");
  97 + point1.setName("温度");
  98 + point1.setFunctionCode(3); // FC03 读保持寄存器
  99 + point1.setRegisterAddress(0);
  100 + point1.setRegisterCount(1);
  101 + point1.setRawDataType("INT16");
  102 + point1.setByteOrder("BIG_ENDIAN");
  103 + point1.setScale(new BigDecimal("0.1"));
  104 + point1.setPollInterval(5000); // 5 秒轮询一次
  105 + points.add(point1);
  106 +
  107 + // 点位 2:湿度 - 保持寄存器 FC03, 地址 1, 1 个寄存器, UINT16
  108 + IotModbusPointRespDTO point2 = new IotModbusPointRespDTO();
  109 + point2.setId(2L);
  110 + point2.setIdentifier("humidity");
  111 + point2.setName("湿度");
  112 + point2.setFunctionCode(3); // FC03 读保持寄存器
  113 + point2.setRegisterAddress(1);
  114 + point2.setRegisterCount(1);
  115 + point2.setRawDataType("UINT16");
  116 + point2.setByteOrder("BIG_ENDIAN");
  117 + point2.setScale(new BigDecimal("0.1"));
  118 + point2.setPollInterval(5000); // 5 秒轮询一次
  119 + points.add(point2);
  120 +
  121 + config.setPoints(points);
  122 + log.info("[buildMockConfigs][已加载 Mock 配置, deviceId={}, points={}]", config.getDeviceId(), points.size());
  123 + return Collections.singletonList(config);
  124 + }
  125 +
  126 + /**
  127 + * 获取设备配置
  128 + */
  129 + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
  130 + return configCache.get(deviceId);
  131 + }
  132 +
  133 + // TODO @AI:这个逻辑,是不是非必须?
  134 + /**
  135 + * 通过 clientId + username + password 查找设备配置(认证用)
  136 + * 暂通过遍历缓存实现,后续可优化为索引
  137 + */
  138 + public IotModbusDeviceConfigRespDTO findConfigByAuth(String clientId, String username, String password) {
  139 + // TODO @芋艿:测试完成后移除 mock 逻辑,改为正式查找
  140 + // Mock:通过 clientId(格式 productKey.deviceName)匹配缓存中的设备
  141 + if (clientId != null && clientId.contains(".")) {
  142 + String[] parts = clientId.split("\\.", 2);
  143 + String productKey = parts[0];
  144 + String deviceName = parts[1];
  145 + for (IotModbusDeviceConfigRespDTO config : configCache.values()) {
  146 + if (productKey.equals(config.getProductKey()) && deviceName.equals(config.getDeviceName())) {
  147 + return config;
  148 + }
  149 + }
  150 + }
  151 + return null;
  152 + }
  153 +
  154 + /**
  155 + * 清理已删除设备的资源
  156 + */
  157 + public void cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs, Consumer<Long> cleanupAction) {
  158 + Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
  159 + Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
  160 + removedDeviceIds.removeAll(currentDeviceIds);
  161 +
  162 + for (Long deviceId : removedDeviceIds) {
  163 + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
  164 + configCache.remove(deviceId);
  165 + cleanupAction.accept(deviceId);
  166 + }
  167 +
  168 + knownDeviceIds.clear();
  169 + knownDeviceIds.addAll(currentDeviceIds);
  170 + }
  171 +
  172 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import io.vertx.core.buffer.Buffer;
  5 +import io.vertx.core.net.NetSocket;
  6 +import lombok.Data;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +import java.util.Map;
  10 +import java.util.concurrent.ConcurrentHashMap;
  11 +
  12 +/**
  13 + * IoT Modbus TCP Slave 连接管理器
  14 + * <p>
  15 + * 管理设备 TCP 连接:socket ↔ 设备双向映射
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Slf4j
  20 +public class IotModbusTcpSlaveConnectionManager {
  21 +
  22 + /**
  23 + * socket → 连接信息
  24 + */
  25 + private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
  26 +
  27 + /**
  28 + * deviceId → socket
  29 + */
  30 + private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>();
  31 +
  32 + /**
  33 + * 连接信息
  34 + */
  35 + @Data
  36 + public static class ConnectionInfo {
  37 +
  38 + /**
  39 + * 设备编号
  40 + */
  41 + private Long deviceId;
  42 + /**
  43 + * 产品标识
  44 + */
  45 + private String productKey;
  46 + /**
  47 + * 设备名称
  48 + */
  49 + private String deviceName;
  50 + /**
  51 + * 从站地址
  52 + */
  53 + private Integer slaveId;
  54 +
  55 + /**
  56 + * 帧格式(首帧自动检测得到)
  57 + */
  58 + private IotModbusFrameFormatEnum frameFormat;
  59 +
  60 + // TODO @AI:mode 是否非必须?!
  61 + /**
  62 + * 模式:1-云端轮询 2-主动上报
  63 + */
  64 + private Integer mode;
  65 +
  66 + }
  67 +
  68 + /**
  69 + * 注册已认证的连接
  70 + */
  71 + public void registerConnection(NetSocket socket, ConnectionInfo info) {
  72 + connectionMap.put(socket, info);
  73 + deviceSocketMap.put(info.getDeviceId(), socket);
  74 + log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]",
  75 + info.getDeviceId(), socket.remoteAddress());
  76 + }
  77 +
  78 + // TODO @芋艿:待定是不是要保留?!
  79 + /**
  80 + * 设置连接的帧格式(首帧检测后调用)
  81 + */
  82 + public void setFrameFormat(NetSocket socket, IotModbusFrameFormatEnum frameFormat) {
  83 + ConnectionInfo info = connectionMap.get(socket);
  84 + if (info != null) {
  85 + info.setFrameFormat(frameFormat);
  86 + }
  87 + }
  88 +
  89 + /**
  90 + * 获取连接信息
  91 + */
  92 + public ConnectionInfo getConnectionInfo(NetSocket socket) {
  93 + return connectionMap.get(socket);
  94 + }
  95 +
  96 + /**
  97 + * 根据设备 ID 获取连接信息
  98 + */
  99 + public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
  100 + NetSocket socket = deviceSocketMap.get(deviceId);
  101 + return socket != null ? connectionMap.get(socket) : null;
  102 + }
  103 +
  104 + // TODO @AI:不用判断连接是否认证;
  105 + /**
  106 + * 判断连接是否已认证
  107 + */
  108 + public boolean isAuthenticated(NetSocket socket) {
  109 + return connectionMap.containsKey(socket);
  110 + }
  111 +
  112 + /**
  113 + * 移除连接
  114 + */
  115 + public ConnectionInfo removeConnection(NetSocket socket) {
  116 + ConnectionInfo info = connectionMap.remove(socket);
  117 + if (info != null && info.getDeviceId() != null) {
  118 + deviceSocketMap.remove(info.getDeviceId());
  119 + log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId());
  120 + }
  121 + return info;
  122 + }
  123 +
  124 + /**
  125 + * 发送数据到设备
  126 + */
  127 + public void sendToDevice(Long deviceId, byte[] data) {
  128 + NetSocket socket = deviceSocketMap.get(deviceId);
  129 + if (socket == null) {
  130 + log.warn("[sendToDevice][设备 {} 没有连接]", deviceId);
  131 + return;
  132 + }
  133 + // TODO @AI:直接复用 sendToSocket 方法?!
  134 + socket.write(Buffer.buffer(data));
  135 + }
  136 +
  137 + /**
  138 + * 发送数据到指定 socket
  139 + */
  140 + public void sendToSocket(NetSocket socket, byte[] data) {
  141 + socket.write(Buffer.buffer(data));
  142 + }
  143 +
  144 + // TODO @AI:貌似别的都没这个,是不是可以去掉哈?!
  145 + /**
  146 + * 关闭所有连接
  147 + */
  148 + public void closeAll() {
  149 + for (NetSocket socket : connectionMap.keySet()) {
  150 + try {
  151 + socket.close();
  152 + } catch (Exception e) {
  153 + log.error("[closeAll][关闭连接失败]", e);
  154 + }
  155 + }
  156 + connectionMap.clear();
  157 + deviceSocketMap.clear();
  158 + }
  159 +
  160 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Data;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +import java.util.Deque;
  10 +import java.util.Map;
  11 +import java.util.concurrent.ConcurrentHashMap;
  12 +import java.util.concurrent.ConcurrentLinkedDeque;
  13 +
  14 +/**
  15 + * IoT Modbus TCP Slave 待响应请求管理器
  16 + * <p>
  17 + * 管理轮询下发的请求,用于匹配设备响应:
  18 + * - TCP 模式:按 transactionId 精确匹配
  19 + * - RTU 模式:按 slaveId + functionCode FIFO 匹配
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +@Slf4j
  24 +public class IotModbusTcpSlavePendingRequestManager {
  25 +
  26 + /**
  27 + * deviceId → 有序队列
  28 + */
  29 + private final Map<Long, Deque<PendingRequest>> pendingRequests = new ConcurrentHashMap<>();
  30 +
  31 + /**
  32 + * 待响应请求信息
  33 + */
  34 + @Data
  35 + @AllArgsConstructor
  36 + public static class PendingRequest {
  37 +
  38 + private Long deviceId;
  39 + private Long pointId;
  40 + private String identifier;
  41 + private int slaveId;
  42 + private int functionCode;
  43 + private int registerAddress;
  44 + private int registerCount;
  45 + private Integer transactionId;
  46 + private long expireAt;
  47 +
  48 + }
  49 +
  50 + /**
  51 + * 添加待响应请求
  52 + */
  53 + public void addRequest(PendingRequest request) {
  54 + pendingRequests.computeIfAbsent(request.getDeviceId(), k -> new ConcurrentLinkedDeque<>())
  55 + .addLast(request);
  56 + }
  57 +
  58 + /**
  59 + * 匹配响应(TCP 模式按 transactionId,RTU 模式按 FIFO)
  60 + *
  61 + * @param deviceId 设备 ID
  62 + * @param frame 收到的响应帧
  63 + * @param frameFormat 帧格式
  64 + * @return 匹配到的 PendingRequest,没有匹配返回 null
  65 + */
  66 + public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame,
  67 + IotModbusFrameFormatEnum frameFormat) {
  68 + Deque<PendingRequest> queue = pendingRequests.get(deviceId);
  69 + // TODO @AI:CollUtil.isEmpty(queue)
  70 + if (queue == null || queue.isEmpty()) {
  71 + return null;
  72 + }
  73 +
  74 + if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) {
  75 + // TCP 模式:按 transactionId 精确匹配
  76 + return matchByTransactionId(queue, frame.getTransactionId());
  77 + } else {
  78 + // RTU 模式:FIFO,匹配 slaveId + functionCode
  79 + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode());
  80 + }
  81 + }
  82 +
  83 + /**
  84 + * 按 transactionId 匹配
  85 + */
  86 + private PendingRequest matchByTransactionId(Deque<PendingRequest> queue, int transactionId) {
  87 + // TODO @AI:需要兼容 jdk8;
  88 + for (var it = queue.iterator(); it.hasNext(); ) {
  89 + PendingRequest req = it.next();
  90 + if (req.getTransactionId() != null && req.getTransactionId() == transactionId) {
  91 + it.remove();
  92 + return req;
  93 + }
  94 + }
  95 + return null;
  96 + }
  97 +
  98 + /**
  99 + * 按 FIFO 匹配
  100 + */
  101 + private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode) {
  102 + // TODO @AI:需要兼容 jdk8;
  103 + for (var it = queue.iterator(); it.hasNext(); ) {
  104 + PendingRequest req = it.next();
  105 + if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) {
  106 + it.remove();
  107 + return req;
  108 + }
  109 + }
  110 + return null;
  111 + }
  112 +
  113 + /**
  114 + * 清理过期请求
  115 + */
  116 + public void cleanupExpired() {
  117 + long now = System.currentTimeMillis();
  118 + for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) {
  119 + Deque<PendingRequest> queue = entry.getValue();
  120 + int removed = 0;
  121 + while (!queue.isEmpty()) {
  122 + PendingRequest req = queue.peekFirst();
  123 + // TODO @AI:if return 减少括号层级;
  124 + if (req != null && req.getExpireAt() < now) {
  125 + queue.pollFirst();
  126 + removed++;
  127 + } else {
  128 + break; // 队列有序,后面的没过期
  129 + }
  130 + }
  131 + if (removed > 0) {
  132 + log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed);
  133 + }
  134 + }
  135 + }
  136 +
  137 + /**
  138 + * 清理指定设备的所有待响应请求
  139 + */
  140 + public void removeDevice(Long deviceId) {
  141 + pendingRequests.remove(deviceId);
  142 + }
  143 +
  144 + /**
  145 + * 清理所有待响应请求
  146 + */
  147 + public void clear() {
  148 + pendingRequests.clear();
  149 + }
  150 +
  151 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest;
  10 +import io.vertx.core.Vertx;
  11 +import lombok.AllArgsConstructor;
  12 +import lombok.Data;
  13 +import lombok.RequiredArgsConstructor;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +
  16 +import java.util.*;
  17 +import java.util.concurrent.ConcurrentHashMap;
  18 +import java.util.concurrent.atomic.AtomicInteger;
  19 +
  20 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  21 +
  22 +// TODO @AI:和 IotModbusTcpPollScheduler 很像,是不是可以做一些复用?
  23 +/**
  24 + * IoT Modbus TCP Slave 轮询调度器
  25 + * <p>
  26 + * 管理点位的轮询定时器,为 mode=1(云端轮询)的设备调度读取任务。
  27 + * 与 tcpmaster 不同,这里不直接通过 j2mod 读取,而是:
  28 + * 1. 编码 Modbus 读请求帧
  29 + * 2. 通过 ConnectionManager 发送到设备的 TCP 连接
  30 + * 3. 将请求注册到 PendingRequestManager,等待设备响应
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +@RequiredArgsConstructor
  35 +@Slf4j
  36 +public class IotModbusTcpSlavePollScheduler {
  37 +
  38 + private final Vertx vertx;
  39 + private final IotModbusTcpSlaveConnectionManager connectionManager;
  40 + private final IotModbusFrameCodec frameCodec;
  41 + private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
  42 + private final int requestTimeout;
  43 +
  44 + /**
  45 + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
  46 + */
  47 + private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
  48 +
  49 + /**
  50 + * TCP 事务 ID 自增器
  51 + */
  52 + private final AtomicInteger transactionIdCounter = new AtomicInteger(0);
  53 +
  54 + /**
  55 + * 点位定时器信息
  56 + */
  57 + @Data
  58 + @AllArgsConstructor
  59 + private static class PointTimerInfo {
  60 +
  61 + /**
  62 + * Vert.x 定时器 ID
  63 + */
  64 + private Long timerId;
  65 + /**
  66 + * 轮询间隔(用于判断是否需要更新定时器)
  67 + */
  68 + private Integer pollInterval;
  69 +
  70 + }
  71 +
  72 + /**
  73 + * 更新轮询任务(增量更新)
  74 + */
  75 + public void updatePolling(IotModbusDeviceConfigRespDTO config) {
  76 + Long deviceId = config.getDeviceId();
  77 + List<IotModbusPointRespDTO> newPoints = config.getPoints();
  78 + Map<Long, PointTimerInfo> currentTimers = devicePointTimers
  79 + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
  80 + // 1.1 计算新配置中的点位 ID 集合
  81 + Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
  82 + // 1.2 计算删除的点位 ID 集合
  83 + Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
  84 + removedPointIds.removeAll(newPointIds);
  85 +
  86 + // 2. 处理删除的点位:停止不再存在的定时器
  87 + for (Long pointId : removedPointIds) {
  88 + PointTimerInfo timerInfo = currentTimers.remove(pointId);
  89 + if (timerInfo != null) {
  90 + vertx.cancelTimer(timerInfo.getTimerId());
  91 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
  92 + }
  93 + }
  94 +
  95 + // 3. 处理新增和修改的点位
  96 + if (CollUtil.isEmpty(newPoints)) {
  97 + return;
  98 + }
  99 + for (IotModbusPointRespDTO point : newPoints) {
  100 + Long pointId = point.getId();
  101 + Integer newPollInterval = point.getPollInterval();
  102 + PointTimerInfo existingTimer = currentTimers.get(pointId);
  103 + // 3.1 新增点位:创建定时器
  104 + if (existingTimer == null) {
  105 + Long timerId = createPollTimer(config, point);
  106 + if (timerId != null) {
  107 + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
  108 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
  109 + deviceId, pointId, newPollInterval);
  110 + }
  111 + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
  112 + // 3.2 pollInterval 变化:重建定时器
  113 + vertx.cancelTimer(existingTimer.getTimerId());
  114 + Long timerId = createPollTimer(config, point);
  115 + if (timerId != null) {
  116 + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
  117 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
  118 + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
  119 + } else {
  120 + currentTimers.remove(pointId);
  121 + }
  122 + }
  123 + // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置)
  124 + }
  125 + }
  126 +
  127 + /**
  128 + * 创建轮询定时器
  129 + */
  130 + private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) {
  131 + if (point.getPollInterval() == null || point.getPollInterval() <= 0) {
  132 + return null;
  133 + }
  134 + return vertx.setPeriodic(point.getPollInterval(), timerId -> {
  135 + try {
  136 + pollPoint(config, point);
  137 + } catch (Exception e) {
  138 + log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]",
  139 + config.getDeviceId(), point.getIdentifier(), e);
  140 + }
  141 + });
  142 + }
  143 +
  144 + /**
  145 + * 轮询单个点位:编码读请求帧 → 发送 → 注册 PendingRequest
  146 + */
  147 + private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) {
  148 + Long deviceId = config.getDeviceId();
  149 + // 1. 获取连接信息
  150 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId);
  151 + if (connInfo == null) {
  152 + log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId);
  153 + return;
  154 + }
  155 +
  156 + // 2.1 确定帧格式和事务 ID
  157 + // TODO @AI:不允许为空!
  158 + IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
  159 + if (frameFormat == null) {
  160 + frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP;
  161 + }
  162 + // TODO @AI:transactionId 需要根据设备来么?然后递增也根据 IotModbusFrameFormatEnum.MODBUS_TCP 提前判断;
  163 + int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF;
  164 + int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
  165 + // 2.2 编码读请求
  166 + byte[] data = frameCodec.encodeReadRequest(slaveId, point.getFunctionCode(),
  167 + point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId);
  168 + // 2.3 注册 PendingRequest
  169 + PendingRequest pendingRequest = new PendingRequest(
  170 + deviceId, point.getId(), point.getIdentifier(),
  171 + slaveId, point.getFunctionCode(),
  172 + point.getRegisterAddress(), point.getRegisterCount(),
  173 + frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? transactionId : null,
  174 + System.currentTimeMillis() + requestTimeout);
  175 + pendingRequestManager.addRequest(pendingRequest);
  176 +
  177 + // 3. 发送读请求
  178 + connectionManager.sendToDevice(deviceId, data);
  179 + log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
  180 + deviceId, point.getIdentifier(), point.getFunctionCode(),
  181 + point.getRegisterAddress(), point.getRegisterCount());
  182 + }
  183 +
  184 + /**
  185 + * 停止设备的轮询
  186 + */
  187 + public void stopPolling(Long deviceId) {
  188 + Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
  189 + if (CollUtil.isEmpty(timers)) {
  190 + return;
  191 + }
  192 + for (PointTimerInfo timerInfo : timers.values()) {
  193 + vertx.cancelTimer(timerInfo.getTimerId());
  194 + }
  195 + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
  196 + }
  197 +
  198 + /**
  199 + * 停止所有轮询
  200 + */
  201 + public void stopAll() {
  202 + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
  203 + stopPolling(deviceId);
  204 + }
  205 + }
  206 +
  207 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -164,14 +164,26 @@ yudao:
164 164 trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
165 165 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码
166 166 # ====================================
167   - # 针对引入的 Modbus TCP 组件的配置
  167 + # 针对引入的 Modbus TCP Master 组件的配置
168 168 # ====================================
169   - - id: modbus-tcp-1
170   - enabled: true
171   - protocol: modbus_tcp
  169 + - id: modbus-tcp-master-1
  170 + enabled: false
  171 + protocol: modbus_tcp_master
172 172 port: 502
173   - modbus-tcp:
  173 + modbus-tcp-master:
174 174 config-refresh-interval: 30 # 配置刷新间隔(秒)
  175 + # ====================================
  176 + # 针对引入的 Modbus TCP Slave 组件的配置
  177 + # ====================================
  178 + - id: modbus-tcp-slave-1
  179 + enabled: true
  180 + protocol: modbus_tcp_slave
  181 + port: 503
  182 + modbus-tcp-slave:
  183 + config-refresh-interval: 30 # 配置刷新间隔(秒)
  184 + custom-function-code: 65 # 自定义功能码(用于认证等扩展交互)
  185 + request-timeout: 5000 # Pending Request 超时时间(毫秒)
  186 + request-cleanup-interval: 10000 # Pending Request 清理间隔(毫秒)
175 187  
176 188 --- #################### 日志相关配置 ####################
177 189  
... ... @@ -193,7 +205,7 @@ logging:
193 205 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
194 206 cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
195 207 cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG
196   - cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp: DEBUG
  208 + cn.iocoder.yudao.module.iot.gateway.protocol.modbus: DEBUG
197 209 # 根日志级别
198 210 root: INFO
199 211  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus;
  2 +
  3 +import com.ghgande.j2mod.modbus.io.ModbusRTUTCPTransport;
  4 +import com.ghgande.j2mod.modbus.msg.*;
  5 +import com.ghgande.j2mod.modbus.procimg.*;
  6 +import com.ghgande.j2mod.modbus.slave.ModbusSlave;
  7 +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;
  8 +
  9 +import java.net.ServerSocket;
  10 +import java.net.Socket;
  11 +
  12 +/**
  13 + * Modbus RTU over TCP 完整 Demo
  14 + *
  15 + * 架构:Master(主站)启动 TCP Server 监听 → Slave(从站)主动 TCP 连接上来
  16 + * 通信协议:RTU 帧格式(带 CRC)通过 TCP 传输,而非标准 MBAP 头
  17 + *
  18 + * 流程:
  19 + * 1. Master 启动 TCP ServerSocket 监听端口
  20 + * 2. Slave(从站模拟器)作为 TCP Client 连接到 Master
  21 + * 3. Master 通过 accept 得到的 Socket,使用 {@link ModbusRTUTCPTransport} 发送读写请求
  22 + *
  23 + * 实现说明:
  24 + * 因为 j2mod 的 ModbusSlave 只能以 TCP Server 模式运行(监听端口等待 Master 连接),
  25 + * 不支持"Slave 作为 TCP Client 主动连接 Master"的模式。
  26 + * 所以这里用一个 TCP 桥接(bridge)来模拟:
  27 + * - Slave 在本地内部端口启动(RTU over TCP 模式)
  28 + * - 一个桥接线程同时连接 Master Server 和 Slave 内部端口,做双向数据转发
  29 + * - Master 视角:看到的是 Slave 主动连上来
  30 + *
  31 + * 依赖:j2mod 3.2.1(pom.xml 中已声明)
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Deprecated // 仅技术演示,非是必须的
  36 +public class ModbusRtuOverTcpDemo {
  37 +
  38 + /**
  39 + * Master(主站)TCP Server 监听端口
  40 + */
  41 + private static final int PORT = 5021;
  42 + /**
  43 + * Slave 内部端口(仅本地中转用,不对外暴露)
  44 + */
  45 + private static final int SLAVE_INTERNAL_PORT = PORT + 100;
  46 + /**
  47 + * Modbus 从站地址
  48 + */
  49 + private static final int SLAVE_ID = 1;
  50 +
  51 + public static void main(String[] args) throws Exception {
  52 + // ===================== 第一步:Master 启动 TCP Server 监听 =====================
  53 + ServerSocket serverSocket = new ServerSocket(PORT);
  54 + System.out.println("===================================================");
  55 + System.out.println("[Master] TCP Server 已启动,监听端口: " + PORT);
  56 + System.out.println("[Master] 等待 Slave 连接...");
  57 + System.out.println("===================================================");
  58 +
  59 + // ===================== 第二步:后台启动 Slave,它会主动连接 Master =====================
  60 + ModbusSlave slave = startSlaveInBackground();
  61 +
  62 + // Master accept Slave 的连接
  63 + Socket slaveSocket = serverSocket.accept();
  64 + System.out.println("[Master] Slave 已连接: " + slaveSocket.getRemoteSocketAddress());
  65 +
  66 + // ===================== 第三步:Master 通过 RTU over TCP 发送读写请求 =====================
  67 + // 使用 ModbusRTUTCPTransport 包装 Socket(RTU 帧 = SlaveID + 功能码 + 数据 + CRC,无 MBAP 头)
  68 + ModbusRTUTCPTransport transport = new ModbusRTUTCPTransport(slaveSocket);
  69 +
  70 + try {
  71 + System.out.println("[Master] RTU over TCP 通道已建立\n");
  72 +
  73 + // 1. 读操作演示:4 种功能码
  74 + demoReadCoils(transport); // 功能码 01:读线圈
  75 + demoReadDiscreteInputs(transport); // 功能码 02:读离散输入
  76 + demoReadHoldingRegisters(transport); // 功能码 03:读保持寄存器
  77 + demoReadInputRegisters(transport); // 功能码 04:读输入寄存器
  78 +
  79 + // 2. 写操作演示 + 读回验证
  80 + demoWriteCoil(transport); // 功能码 05:写单个线圈
  81 + demoWriteRegister(transport); // 功能码 06:写单个保持寄存器
  82 +
  83 + System.out.println("\n===================================================");
  84 + System.out.println("所有 RTU over TCP 读写操作执行成功!");
  85 + System.out.println("===================================================");
  86 + } finally {
  87 + // 清理资源
  88 + transport.close();
  89 + slaveSocket.close();
  90 + serverSocket.close();
  91 + slave.close();
  92 + System.out.println("[Master] 资源已关闭");
  93 + }
  94 + }
  95 +
  96 + // ===================== Slave 设备模拟(作为 TCP Client 连接 Master) =====================
  97 +
  98 + /**
  99 + * 在后台启动从站模拟器,并通过 TCP 桥接连到 Master Server
  100 + *
  101 + * @return ModbusSlave 实例(用于最后关闭资源)
  102 + */
  103 + private static ModbusSlave startSlaveInBackground() throws Exception {
  104 + // 1. 创建进程映像,初始化寄存器数据
  105 + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID);
  106 + // 1.1 线圈(Coil,功能码 01/05)- 可读写,地址 0~9
  107 + for (int i = 0; i < 10; i++) {
  108 + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0));
  109 + }
  110 + // 1.2 离散输入(Discrete Input,功能码 02)- 只读,地址 0~9
  111 + for (int i = 0; i < 10; i++) {
  112 + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0));
  113 + }
  114 + // 1.3 保持寄存器(Holding Register,功能码 03/06/16)- 可读写,地址 0~19
  115 + for (int i = 0; i < 20; i++) {
  116 + spi.addRegister(new SimpleRegister(i * 100));
  117 + }
  118 + // 1.4 输入寄存器(Input Register,功能码 04)- 只读,地址 0~19
  119 + for (int i = 0; i < 20; i++) {
  120 + spi.addInputRegister(new SimpleInputRegister(i * 10 + 1));
  121 + }
  122 +
  123 + // 2. 启动 Slave(RTU over TCP 模式,在本地内部端口监听)
  124 + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(SLAVE_INTERNAL_PORT, 5, true);
  125 + slave.addProcessImage(SLAVE_ID, spi);
  126 + slave.open();
  127 + System.out.println("[Slave] 从站模拟器已启动(内部端口: " + SLAVE_INTERNAL_PORT + ")");
  128 +
  129 + // 3. 启动桥接线程:TCP Client 连接 Master Server,同时连接 Slave 内部端口,双向转发
  130 + Thread bridgeThread = new Thread(() -> {
  131 + try {
  132 + Socket toMaster = new Socket("127.0.0.1", PORT);
  133 + Socket toSlave = new Socket("127.0.0.1", SLAVE_INTERNAL_PORT);
  134 + System.out.println("[Bridge] 已建立桥接: Master(" + PORT + ") <-> Slave(" + SLAVE_INTERNAL_PORT + ")");
  135 +
  136 + // 双向桥接:Master ↔ Bridge ↔ Slave
  137 + Thread forward = new Thread(() -> bridge(toMaster, toSlave), "bridge-master→slave");
  138 + Thread backward = new Thread(() -> bridge(toSlave, toMaster), "bridge-slave→master");
  139 + forward.setDaemon(true);
  140 + backward.setDaemon(true);
  141 + forward.start();
  142 + backward.start();
  143 + } catch (Exception e) {
  144 + e.printStackTrace();
  145 + }
  146 + }, "bridge-setup");
  147 + bridgeThread.setDaemon(true);
  148 + bridgeThread.start();
  149 +
  150 + return slave;
  151 + }
  152 +
  153 + /**
  154 + * TCP 双向桥接:从 src 读取数据,写入 dst
  155 + */
  156 + private static void bridge(Socket src, Socket dst) {
  157 + try {
  158 + byte[] buf = new byte[1024];
  159 + var in = src.getInputStream();
  160 + var out = dst.getOutputStream();
  161 + int len;
  162 + while ((len = in.read(buf)) != -1) {
  163 + out.write(buf, 0, len);
  164 + out.flush();
  165 + }
  166 + } catch (Exception ignored) {
  167 + // 连接关闭时正常退出
  168 + }
  169 + }
  170 +
  171 + // ===================== Master 读写操作 =====================
  172 +
  173 + /**
  174 + * 发送请求并接收响应(通用方法)
  175 + */
  176 + private static ModbusResponse sendRequest(ModbusRTUTCPTransport transport, ModbusRequest request) throws Exception {
  177 + request.setUnitID(SLAVE_ID);
  178 + transport.writeRequest(request);
  179 + return transport.readResponse();
  180 + }
  181 +
  182 + /**
  183 + * 功能码 01:读线圈(Read Coils)
  184 + */
  185 + private static void demoReadCoils(ModbusRTUTCPTransport transport) throws Exception {
  186 + ReadCoilsRequest request = new ReadCoilsRequest(0, 5);
  187 + ReadCoilsResponse response = (ReadCoilsResponse) sendRequest(transport, request);
  188 +
  189 + StringBuilder sb = new StringBuilder("[功能码 01] 读线圈(0~4): ");
  190 + for (int i = 0; i < 5; i++) {
  191 + sb.append(response.getCoilStatus(i) ? "ON" : "OFF");
  192 + if (i < 4) {
  193 + sb.append(", ");
  194 + }
  195 + }
  196 + System.out.println(sb);
  197 + }
  198 +
  199 + /**
  200 + * 功能码 02:读离散输入(Read Discrete Inputs)
  201 + */
  202 + private static void demoReadDiscreteInputs(ModbusRTUTCPTransport transport) throws Exception {
  203 + ReadInputDiscretesRequest request = new ReadInputDiscretesRequest(0, 5);
  204 + ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) sendRequest(transport, request);
  205 +
  206 + StringBuilder sb = new StringBuilder("[功能码 02] 读离散输入(0~4): ");
  207 + for (int i = 0; i < 5; i++) {
  208 + sb.append(response.getDiscreteStatus(i) ? "ON" : "OFF");
  209 + if (i < 4) {
  210 + sb.append(", ");
  211 + }
  212 + }
  213 + System.out.println(sb);
  214 + }
  215 +
  216 + /**
  217 + * 功能码 03:读保持寄存器(Read Holding Registers)
  218 + */
  219 + private static void demoReadHoldingRegisters(ModbusRTUTCPTransport transport) throws Exception {
  220 + ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(0, 5);
  221 + ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) sendRequest(transport, request);
  222 +
  223 + StringBuilder sb = new StringBuilder("[功能码 03] 读保持寄存器(0~4): ");
  224 + for (int i = 0; i < response.getWordCount(); i++) {
  225 + sb.append(response.getRegisterValue(i));
  226 + if (i < response.getWordCount() - 1) {
  227 + sb.append(", ");
  228 + }
  229 + }
  230 + System.out.println(sb);
  231 + }
  232 +
  233 + /**
  234 + * 功能码 04:读输入寄存器(Read Input Registers)
  235 + */
  236 + private static void demoReadInputRegisters(ModbusRTUTCPTransport transport) throws Exception {
  237 + ReadInputRegistersRequest request = new ReadInputRegistersRequest(0, 5);
  238 + ReadInputRegistersResponse response = (ReadInputRegistersResponse) sendRequest(transport, request);
  239 +
  240 + StringBuilder sb = new StringBuilder("[功能码 04] 读输入寄存器(0~4): ");
  241 + for (int i = 0; i < response.getWordCount(); i++) {
  242 + sb.append(response.getRegisterValue(i));
  243 + if (i < response.getWordCount() - 1) {
  244 + sb.append(", ");
  245 + }
  246 + }
  247 + System.out.println(sb);
  248 + }
  249 +
  250 + /**
  251 + * 功能码 05:写单个线圈(Write Single Coil)+ 读回验证
  252 + */
  253 + private static void demoWriteCoil(ModbusRTUTCPTransport transport) throws Exception {
  254 + int address = 0;
  255 +
  256 + // 1. 先读取当前值
  257 + ReadCoilsRequest readReq = new ReadCoilsRequest(address, 1);
  258 + ReadCoilsResponse readResp = (ReadCoilsResponse) sendRequest(transport, readReq);
  259 + boolean beforeValue = readResp.getCoilStatus(0);
  260 +
  261 + // 2. 写入相反的值
  262 + boolean writeValue = !beforeValue;
  263 + WriteCoilRequest writeReq = new WriteCoilRequest(address, writeValue);
  264 + sendRequest(transport, writeReq);
  265 +
  266 + // 3. 读回验证
  267 + ReadCoilsResponse verifyResp = (ReadCoilsResponse) sendRequest(transport, readReq);
  268 + boolean afterValue = verifyResp.getCoilStatus(0);
  269 +
  270 + System.out.println("[功能码 05] 写线圈: 地址=" + address
  271 + + ", 写入前=" + (beforeValue ? "ON" : "OFF")
  272 + + ", 写入值=" + (writeValue ? "ON" : "OFF")
  273 + + ", 读回值=" + (afterValue ? "ON" : "OFF")
  274 + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
  275 + }
  276 +
  277 + /**
  278 + * 功能码 06:写单个保持寄存器(Write Single Register)+ 读回验证
  279 + */
  280 + private static void demoWriteRegister(ModbusRTUTCPTransport transport) throws Exception {
  281 + int address = 0;
  282 + int writeValue = 12345;
  283 +
  284 + // 1. 先读取当前值
  285 + ReadMultipleRegistersRequest readReq = new ReadMultipleRegistersRequest(address, 1);
  286 + ReadMultipleRegistersResponse readResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
  287 + int beforeValue = readResp.getRegisterValue(0);
  288 +
  289 + // 2. 写入新值
  290 + WriteSingleRegisterRequest writeReq = new WriteSingleRegisterRequest(address, new SimpleRegister(writeValue));
  291 + sendRequest(transport, writeReq);
  292 +
  293 + // 3. 读回验证
  294 + ReadMultipleRegistersResponse verifyResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
  295 + int afterValue = verifyResp.getRegisterValue(0);
  296 +
  297 + System.out.println("[功能码 06] 写保持寄存器: 地址=" + address
  298 + + ", 写入前=" + beforeValue
  299 + + ", 写入值=" + writeValue
  300 + + ", 读回值=" + afterValue
  301 + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
  302 + }
  303 +
  304 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/ModbusTcpSlaveSimulatorTest.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbustcp;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
2 2  
3 3 import com.ghgande.j2mod.modbus.procimg.*;
4 4 import com.ghgande.j2mod.modbus.slave.ModbusSlave;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
  2 +
  3 +import cn.hutool.json.JSONObject;
  4 +import cn.hutool.json.JSONUtil;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  7 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory;
  11 +import io.vertx.core.Vertx;
  12 +import io.vertx.core.buffer.Buffer;
  13 +import io.vertx.core.net.NetClient;
  14 +import io.vertx.core.net.NetClientOptions;
  15 +import io.vertx.core.net.NetSocket;
  16 +import io.vertx.core.parsetools.RecordParser;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.junit.jupiter.api.AfterAll;
  19 +import org.junit.jupiter.api.BeforeAll;
  20 +import org.junit.jupiter.api.Disabled;
  21 +import org.junit.jupiter.api.Test;
  22 +
  23 +import java.util.concurrent.CompletableFuture;
  24 +import java.util.concurrent.TimeUnit;
  25 +
  26 +/**
  27 + * IoT Modbus TCP Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试)
  28 + *
  29 + * <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信
  30 + *
  31 + * <p>使用步骤:
  32 + * <ol>
  33 + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)</li>
  34 + * <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)</li>
  35 + * <li>运行以下测试方法:
  36 + * <ul>
  37 + * <li>{@link #testAuth()} - 自定义功能码认证</li>
  38 + * <li>{@link #testPollingResponse()} - 轮询响应</li>
  39 + * <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li>
  40 + * </ul>
  41 + * </li>
  42 + * </ol>
  43 + *
  44 + * @author 芋道源码
  45 + */
  46 +@Slf4j
  47 +@Disabled
  48 +public class IotModbusTcpSlaveModbusRtuIntegrationTest {
  49 +
  50 + private static final String SERVER_HOST = "127.0.0.1";
  51 + private static final int SERVER_PORT = 503;
  52 + private static final int TIMEOUT_MS = 5000;
  53 +
  54 + private static final int CUSTOM_FC = 65;
  55 + private static final int SLAVE_ID = 1;
  56 +
  57 + private static Vertx vertx;
  58 + private static NetClient netClient;
  59 +
  60 + // ===================== 编解码器 =====================
  61 +
  62 + private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC);
  63 +
  64 + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
  65 +
  66 + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
  67 + private static final String DEVICE_NAME = "small";
  68 + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
  69 +
  70 + @BeforeAll
  71 + static void setUp() {
  72 + vertx = Vertx.vertx();
  73 + NetClientOptions options = new NetClientOptions()
  74 + .setConnectTimeout(TIMEOUT_MS)
  75 + .setIdleTimeout(TIMEOUT_MS);
  76 + netClient = vertx.createNetClient(options);
  77 + }
  78 +
  79 + @AfterAll
  80 + static void tearDown() {
  81 + if (netClient != null) {
  82 + netClient.close();
  83 + }
  84 + if (vertx != null) {
  85 + vertx.close();
  86 + }
  87 + }
  88 +
  89 + // ===================== 认证测试 =====================
  90 +
  91 + /**
  92 + * 认证测试:发送自定义功能码 FC65 认证帧(RTU 格式),验证认证成功响应
  93 + */
  94 + @Test
  95 + public void testAuth() throws Exception {
  96 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  97 + try {
  98 + // 1. 构造并发送认证帧
  99 + IotModbusFrame response = authenticate(socket);
  100 +
  101 + // 2. 验证响应
  102 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
  103 + response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
  104 + JSONObject json = JSONUtil.parseObj(response.getCustomData());
  105 + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
  106 + } finally {
  107 + socket.close();
  108 + }
  109 + }
  110 +
  111 + // ===================== 轮询响应测试 =====================
  112 +
  113 + /**
  114 + * 轮询响应测试:认证后等待网关下发 FC03 读请求(RTU 格式),构造读响应帧发回
  115 + */
  116 + @Test
  117 + public void testPollingResponse() throws Exception {
  118 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  119 + try {
  120 + // 1. 先认证
  121 + IotModbusFrame authResponse = authenticate(socket);
  122 + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
  123 +
  124 + // 2. 等待网关下发读请求
  125 + log.info("[testPollingResponse][等待网关下发读请求...]");
  126 + IotModbusFrame readRequest = waitForRequest(socket);
  127 + log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}]",
  128 + readRequest.getSlaveId(), readRequest.getFunctionCode());
  129 +
  130 + // 3. 解析读请求中的起始地址和数量
  131 + byte[] pdu = readRequest.getPdu();
  132 + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF);
  133 + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF);
  134 + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity);
  135 +
  136 + // 4. 构造读响应帧(模拟寄存器数据,RTU 格式)
  137 + int[] registerValues = new int[quantity];
  138 + for (int i = 0; i < quantity; i++) {
  139 + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
  140 + }
  141 + byte[] responseData = buildReadResponse(readRequest.getSlaveId(),
  142 + readRequest.getFunctionCode(), registerValues);
  143 + socket.write(Buffer.buffer(responseData));
  144 + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
  145 +
  146 + // 5. 等待一段时间让网关处理
  147 + Thread.sleep(20000);
  148 + } finally {
  149 + socket.close();
  150 + }
  151 + }
  152 +
  153 + // ===================== 属性设置测试 =====================
  154 +
  155 + /**
  156 + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求(RTU 格式)
  157 + * <p>
  158 + * 注意:需手动在平台触发 property.set
  159 + */
  160 + @Test
  161 + public void testPropertySetWrite() throws Exception {
  162 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  163 + try {
  164 + // 1. 先认证
  165 + IotModbusFrame authResponse = authenticate(socket);
  166 + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData());
  167 +
  168 + // 2. 等待网关下发写请求(需手动在平台触发 property.set)
  169 + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]");
  170 + IotModbusFrame writeRequest = waitForRequest(socket);
  171 + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]",
  172 + writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
  173 + bytesToHex(writeRequest.getPdu()));
  174 + } finally {
  175 + socket.close();
  176 + }
  177 + }
  178 +
  179 + // ===================== 辅助方法 =====================
  180 +
  181 + /**
  182 + * 建立 TCP 连接
  183 + */
  184 + private CompletableFuture<NetSocket> connect() {
  185 + CompletableFuture<NetSocket> future = new CompletableFuture<>();
  186 + netClient.connect(SERVER_PORT, SERVER_HOST)
  187 + .onSuccess(future::complete)
  188 + .onFailure(future::completeExceptionally);
  189 + return future;
  190 + }
  191 +
  192 + /**
  193 + * 执行认证并返回响应帧
  194 + */
  195 + private IotModbusFrame authenticate(NetSocket socket) throws Exception {
  196 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  197 + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
  198 + return sendAndReceive(socket, authFrame);
  199 + }
  200 +
  201 + /**
  202 + * 发送帧并等待响应(使用 IotModbusRecordParserFactory 自动检测帧格式)
  203 + */
  204 + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
  205 + CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
  206 + // 使用 RecordParserFactory 创建拆包器(自动检测帧格式)
  207 + RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC,
  208 + buffer -> {
  209 + try {
  210 + // 检测到的帧格式应该是 RTU,使用 RTU 格式解码
  211 + IotModbusFrame frame = FRAME_CODEC.decodeResponse(
  212 + buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU);
  213 + responseFuture.complete(frame);
  214 + } catch (Exception e) {
  215 + responseFuture.completeExceptionally(e);
  216 + }
  217 + },
  218 + format -> log.info("[sendAndReceive][检测到帧格式: {}]", format));
  219 + socket.handler(parser);
  220 +
  221 + // 发送请求
  222 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length);
  223 + socket.write(Buffer.buffer(frameData));
  224 +
  225 + // 等待响应
  226 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  227 + }
  228 +
  229 + /**
  230 + * 等待接收网关下发的请求帧(不发送,只等待接收)
  231 + */
  232 + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception {
  233 + CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>();
  234 + // 使用 RecordParserFactory 创建拆包器
  235 + RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC,
  236 + buffer -> {
  237 + try {
  238 + IotModbusFrame frame = FRAME_CODEC.decodeResponse(
  239 + buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU);
  240 + requestFuture.complete(frame);
  241 + } catch (Exception e) {
  242 + requestFuture.completeExceptionally(e);
  243 + }
  244 + },
  245 + format -> log.info("[waitForRequest][检测到帧格式: {}]", format));
  246 + socket.handler(parser);
  247 +
  248 + // 等待(超时 30 秒,因为轮询间隔可能比较长)
  249 + return requestFuture.get(30000, TimeUnit.MILLISECONDS);
  250 + }
  251 +
  252 + /**
  253 + * 构造认证帧(MODBUS_RTU 格式)
  254 + * <p>
  255 + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
  256 + * <p>
  257 + * RTU 帧格式:[SlaveId(1)] [FC=0x41(1)] [ByteCount(1)] [JSON(N)] [CRC16(2)]
  258 + */
  259 + private byte[] buildAuthFrame(String clientId, String username, String password) {
  260 + JSONObject params = new JSONObject();
  261 + params.set("clientId", clientId);
  262 + params.set("username", username);
  263 + params.set("password", password);
  264 + JSONObject json = new JSONObject();
  265 + json.set("method", "auth");
  266 + json.set("params", params);
  267 + return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(),
  268 + IotModbusFrameFormatEnum.MODBUS_RTU, 0);
  269 + }
  270 +
  271 + /**
  272 + * 构造 FC03/FC01-04 读响应帧(MODBUS_RTU 格式)
  273 + * <p>
  274 + * RTU 帧格式:[SlaveId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] [CRC16(2)]
  275 + */
  276 + private byte[] buildReadResponse(int slaveId, int functionCode, int[] registerValues) {
  277 + int byteCount = registerValues.length * 2;
  278 + // 帧长度:SlaveId(1) + FC(1) + ByteCount(1) + Data(N*2) + CRC(2)
  279 + int totalLength = 1 + 1 + 1 + byteCount + 2;
  280 + byte[] frame = new byte[totalLength];
  281 + frame[0] = (byte) slaveId;
  282 + frame[1] = (byte) functionCode;
  283 + frame[2] = (byte) byteCount;
  284 + for (int i = 0; i < registerValues.length; i++) {
  285 + frame[3 + i * 2] = (byte) ((registerValues[i] >> 8) & 0xFF);
  286 + frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF);
  287 + }
  288 + // 计算 CRC16
  289 + int crc = IotModbusFrameCodec.calculateCrc16(frame, totalLength - 2);
  290 + frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low
  291 + frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
  292 + return frame;
  293 + }
  294 +
  295 + /**
  296 + * 字节数组转十六进制字符串
  297 + */
  298 + private static String bytesToHex(byte[] bytes) {
  299 + if (bytes == null) {
  300 + return "null";
  301 + }
  302 + StringBuilder sb = new StringBuilder();
  303 + for (byte b : bytes) {
  304 + sb.append(String.format("%02X ", b));
  305 + }
  306 + return sb.toString().trim();
  307 + }
  308 +
  309 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
  2 +
  3 +import cn.hutool.json.JSONObject;
  4 +import cn.hutool.json.JSONUtil;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  7 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
  10 +import io.vertx.core.Handler;
  11 +import io.vertx.core.Vertx;
  12 +import io.vertx.core.buffer.Buffer;
  13 +import io.vertx.core.net.NetClient;
  14 +import io.vertx.core.net.NetClientOptions;
  15 +import io.vertx.core.net.NetSocket;
  16 +import io.vertx.core.parsetools.RecordParser;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.junit.jupiter.api.AfterAll;
  19 +import org.junit.jupiter.api.BeforeAll;
  20 +import org.junit.jupiter.api.Disabled;
  21 +import org.junit.jupiter.api.Test;
  22 +
  23 +import java.nio.ByteBuffer;
  24 +import java.nio.ByteOrder;
  25 +import java.util.concurrent.CompletableFuture;
  26 +import java.util.concurrent.TimeUnit;
  27 +
  28 +/**
  29 + * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试)
  30 + *
  31 + * <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_TCP(MBAP 头)帧格式通信
  32 + *
  33 + * <p>使用步骤:
  34 + * <ol>
  35 + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)</li>
  36 + * <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)</li>
  37 + * <li>运行以下测试方法:
  38 + * <ul>
  39 + * <li>{@link #testAuth()} - 自定义功能码认证</li>
  40 + * <li>{@link #testPollingResponse()} - 轮询响应</li>
  41 + * <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li>
  42 + * </ul>
  43 + * </li>
  44 + * </ol>
  45 + *
  46 + * @author 芋道源码
  47 + */
  48 +@Slf4j
  49 +@Disabled
  50 +public class IotModbusTcpSlaveModbusTcpIntegrationTest {
  51 +
  52 + private static final String SERVER_HOST = "127.0.0.1";
  53 + private static final int SERVER_PORT = 503;
  54 + private static final int TIMEOUT_MS = 5000;
  55 +
  56 + private static final int CUSTOM_FC = 65;
  57 + private static final int SLAVE_ID = 1;
  58 +
  59 + private static Vertx vertx;
  60 + private static NetClient netClient;
  61 +
  62 + // ===================== 编解码器 =====================
  63 +
  64 + private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC);
  65 +
  66 + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
  67 +
  68 + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
  69 + private static final String DEVICE_NAME = "small";
  70 + private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
  71 +
  72 + @BeforeAll
  73 + static void setUp() {
  74 + vertx = Vertx.vertx();
  75 + NetClientOptions options = new NetClientOptions()
  76 + .setConnectTimeout(TIMEOUT_MS)
  77 + .setIdleTimeout(TIMEOUT_MS);
  78 + netClient = vertx.createNetClient(options);
  79 + }
  80 +
  81 + @AfterAll
  82 + static void tearDown() {
  83 + if (netClient != null) {
  84 + netClient.close();
  85 + }
  86 + if (vertx != null) {
  87 + vertx.close();
  88 + }
  89 + }
  90 +
  91 + // ===================== 认证测试 =====================
  92 +
  93 + /**
  94 + * 认证测试:发送自定义功能码 FC65 认证帧,验证认证成功响应
  95 + */
  96 + @Test
  97 + public void testAuth() throws Exception {
  98 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  99 + try {
  100 + // 1. 构造并发送认证帧
  101 + IotModbusFrame response = authenticate(socket);
  102 +
  103 + // 2. 验证响应
  104 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
  105 + response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
  106 + JSONObject json = JSONUtil.parseObj(response.getCustomData());
  107 + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
  108 + } finally {
  109 + socket.close();
  110 + }
  111 + }
  112 +
  113 + // ===================== 轮询响应测试 =====================
  114 +
  115 + /**
  116 + * 轮询响应测试:认证后等待网关下发 FC03 读请求,构造读响应帧发回
  117 + */
  118 + @Test
  119 + public void testPollingResponse() throws Exception {
  120 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  121 + try {
  122 + // 1. 先认证
  123 + IotModbusFrame authResponse = authenticate(socket);
  124 + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
  125 +
  126 + // 2. 等待网关下发读请求
  127 + log.info("[testPollingResponse][等待网关下发读请求...]");
  128 + IotModbusFrame readRequest = waitForRequest(socket);
  129 + log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}, transactionId={}]",
  130 + readRequest.getSlaveId(), readRequest.getFunctionCode(), readRequest.getTransactionId());
  131 +
  132 + // 3. 解析读请求中的起始地址和数量
  133 + byte[] pdu = readRequest.getPdu();
  134 + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF);
  135 + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF);
  136 + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity);
  137 +
  138 + // 4. 构造读响应帧(模拟寄存器数据)
  139 + int[] registerValues = new int[quantity];
  140 + for (int i = 0; i < quantity; i++) {
  141 + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
  142 + }
  143 + byte[] responseData = buildReadResponse(readRequest.getTransactionId(),
  144 + readRequest.getSlaveId(), readRequest.getFunctionCode(), registerValues);
  145 + socket.write(Buffer.buffer(responseData));
  146 + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
  147 +
  148 + // 5. 等待一段时间让网关处理
  149 + Thread.sleep(200000);
  150 + } finally {
  151 + socket.close();
  152 + }
  153 + }
  154 +
  155 + // ===================== 属性设置测试 =====================
  156 +
  157 + /**
  158 + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求
  159 + * <p>
  160 + * 注意:需手动在平台触发 property.set
  161 + */
  162 + @Test
  163 + public void testPropertySetWrite() throws Exception {
  164 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  165 + try {
  166 + // 1. 先认证
  167 + IotModbusFrame authResponse = authenticate(socket);
  168 + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData());
  169 +
  170 + // 2. 等待网关下发写请求(需手动在平台触发 property.set)
  171 + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]");
  172 + IotModbusFrame writeRequest = waitForRequest(socket);
  173 + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]",
  174 + writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
  175 + writeRequest.getTransactionId(), bytesToHex(writeRequest.getPdu()));
  176 + } finally {
  177 + socket.close();
  178 + }
  179 + }
  180 +
  181 + // ===================== 辅助方法 =====================
  182 +
  183 + /**
  184 + * 建立 TCP 连接
  185 + */
  186 + private CompletableFuture<NetSocket> connect() {
  187 + CompletableFuture<NetSocket> future = new CompletableFuture<>();
  188 + netClient.connect(SERVER_PORT, SERVER_HOST)
  189 + .onSuccess(future::complete)
  190 + .onFailure(future::completeExceptionally);
  191 + return future;
  192 + }
  193 +
  194 + /**
  195 + * 执行认证并返回响应帧
  196 + */
  197 + private IotModbusFrame authenticate(NetSocket socket) throws Exception {
  198 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  199 + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
  200 + return sendAndReceive(socket, authFrame);
  201 + }
  202 +
  203 + /**
  204 + * 发送帧并等待响应(MODBUS_TCP 格式)
  205 + * <p>
  206 + * 使用两阶段 RecordParser 拆包:fixedSizeMode(6) 读 MBAP 头 → fixedSizeMode(length) 读 body
  207 + */
  208 + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
  209 + CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
  210 + // 创建 TCP 两阶段拆包 RecordParser
  211 + RecordParser parser = RecordParser.newFixed(6);
  212 + parser.handler(new TcpRecordParserHandler(parser, responseFuture));
  213 + socket.handler(parser);
  214 +
  215 + // 发送请求
  216 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length);
  217 + socket.write(Buffer.buffer(frameData));
  218 +
  219 + // 等待响应
  220 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  221 + }
  222 +
  223 + /**
  224 + * 等待接收网关下发的请求帧(不发送,只等待接收)
  225 + */
  226 + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception {
  227 + CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>();
  228 + RecordParser parser = RecordParser.newFixed(6);
  229 + parser.handler(new TcpRecordParserHandler(parser, requestFuture));
  230 + socket.handler(parser);
  231 +
  232 + // 等待(超时 30 秒,因为轮询间隔可能比较长)
  233 + return requestFuture.get(30000, TimeUnit.MILLISECONDS);
  234 + }
  235 +
  236 + /**
  237 + * MODBUS_TCP 两阶段拆包 Handler
  238 + */
  239 + private class TcpRecordParserHandler implements Handler<Buffer> {
  240 +
  241 + private final RecordParser parser;
  242 + private final CompletableFuture<IotModbusFrame> future;
  243 + private byte[] mbapHeader;
  244 + private boolean waitingForBody = false;
  245 +
  246 + TcpRecordParserHandler(RecordParser parser, CompletableFuture<IotModbusFrame> future) {
  247 + this.parser = parser;
  248 + this.future = future;
  249 + }
  250 +
  251 + @Override
  252 + public void handle(Buffer buffer) {
  253 + try {
  254 + if (waitingForBody) {
  255 + // Phase 2: 收到 body(unitId + PDU)
  256 + byte[] body = buffer.getBytes();
  257 + byte[] fullFrame = new byte[mbapHeader.length + body.length];
  258 + System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length);
  259 + System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length);
  260 +
  261 + IotModbusFrame frame = FRAME_CODEC.decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP);
  262 + future.complete(frame);
  263 + } else {
  264 + // Phase 1: 收到 MBAP 头 6 字节
  265 + this.mbapHeader = buffer.getBytes();
  266 + int length = ((mbapHeader[4] & 0xFF) << 8) | (mbapHeader[5] & 0xFF);
  267 + this.waitingForBody = true;
  268 + parser.fixedSizeMode(length);
  269 + }
  270 + } catch (Exception e) {
  271 + future.completeExceptionally(e);
  272 + }
  273 + }
  274 + }
  275 +
  276 + /**
  277 + * 构造认证帧(MODBUS_TCP 格式)
  278 + * <p>
  279 + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
  280 + */
  281 + private byte[] buildAuthFrame(String clientId, String username, String password) {
  282 + JSONObject params = new JSONObject();
  283 + params.set("clientId", clientId);
  284 + params.set("username", username);
  285 + params.set("password", password);
  286 + JSONObject json = new JSONObject();
  287 + json.set("method", "auth");
  288 + json.set("params", params);
  289 + return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(),
  290 + IotModbusFrameFormatEnum.MODBUS_TCP, 1);
  291 + }
  292 +
  293 + /**
  294 + * 构造 FC03/FC01-04 读响应帧(MODBUS_TCP 格式)
  295 + * <p>
  296 + * 格式:[MBAP(6)] [UnitId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)]
  297 + */
  298 + private byte[] buildReadResponse(int transactionId, int slaveId, int functionCode, int[] registerValues) {
  299 + int byteCount = registerValues.length * 2;
  300 + // PDU: FC(1) + ByteCount(1) + Data(N*2)
  301 + int pduLength = 1 + 1 + byteCount;
  302 + // 完整帧:MBAP(6) + UnitId(1) + PDU
  303 + int totalLength = 6 + 1 + pduLength;
  304 + ByteBuffer buf = ByteBuffer.allocate(totalLength).order(ByteOrder.BIG_ENDIAN);
  305 + // MBAP Header
  306 + buf.putShort((short) transactionId); // Transaction ID
  307 + buf.putShort((short) 0); // Protocol ID
  308 + buf.putShort((short) (1 + pduLength)); // Length (UnitId + PDU)
  309 + // UnitId
  310 + buf.put((byte) slaveId);
  311 + // PDU
  312 + buf.put((byte) functionCode);
  313 + buf.put((byte) byteCount);
  314 + for (int value : registerValues) {
  315 + buf.putShort((short) value);
  316 + }
  317 + return buf.array();
  318 + }
  319 +
  320 + /**
  321 + * 字节数组转十六进制字符串
  322 + */
  323 + private static String bytesToHex(byte[] bytes) {
  324 + if (bytes == null) {
  325 + return "null";
  326 + }
  327 + StringBuilder sb = new StringBuilder();
  328 + for (byte b : bytes) {
  329 + sb.append(String.format("%02X ", b));
  330 + }
  331 + return sb.toString().trim();
  332 + }
  333 +
  334 +}
... ...