Commit a0db86848d112820a80c095725c078693ee13945
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<String> { |
| 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
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<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<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
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 | +} | ... | ... |