Commit 4e4c776bed8327d3972ab98ea327d7a8f5b4c72c

Authored by YunaiV
1 parent c608b81c

feat(iot):modbus-tcp-slave、modbus-tcp-master 继续优化大量代码,并对接管理后台

Showing 38 changed files with 520 additions and 327 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
1 1 package cn.iocoder.yudao.module.iot.api.device;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
4 6 import cn.iocoder.yudao.framework.common.enums.RpcConstants;
5 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 8 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 9 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
8 10 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
9 11 import cn.iocoder.yudao.module.iot.core.biz.dto.*;
10   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
12   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
13   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
14 12 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
15 13 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
16 14 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
... ... @@ -35,8 +33,6 @@ import java.util.List;
35 33 import java.util.Map;
36 34 import java.util.Set;
37 35  
38   -import java.util.List;
39   -
40 36 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
41 37 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
42 38  
... ... @@ -81,12 +77,13 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
81 77 }
82 78  
83 79 @Override
84   - @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/enabled-configs")
  80 + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/config-list")
85 81 @PermitAll
86 82 @TenantIgnore
87   - public CommonResult<List<IotModbusDeviceConfigRespDTO>> getEnabledModbusDeviceConfigs() {
88   - // 1. 获取所有启用的 Modbus 连接配置
89   - List<IotDeviceModbusConfigDO> configList = modbusConfigService.getEnabledDeviceModbusConfigList();
  83 + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(
  84 + @RequestBody IotModbusDeviceConfigListReqDTO listReqDTO) {
  85 + // 1. 获取 Modbus 连接配置
  86 + List<IotDeviceModbusConfigDO> configList = modbusConfigService.getDeviceModbusConfigList(listReqDTO);
90 87 if (CollUtil.isEmpty(configList)) {
91 88 return success(new ArrayList<>());
92 89 }
... ... @@ -95,6 +92,7 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
95 92 Set<Long> deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId);
96 93 Map<Long, IotDeviceDO> deviceMap = deviceService.getDeviceMap(deviceIds);
97 94 Map<Long, List<IotDeviceModbusPointDO>> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds);
  95 + Map<Long, IotProductDO> productMap = productService.getProductMap(convertSet(deviceMap.values(), IotDeviceDO::getProductId));
98 96 List<IotModbusDeviceConfigRespDTO> result = new ArrayList<>(configList.size());
99 97 for (IotDeviceModbusConfigDO config : configList) {
100 98 // 3.1 获取设备信息
... ... @@ -102,12 +100,20 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
102 100 if (device == null) {
103 101 continue;
104 102 }
105   - // 3.2 获取启用的点位列表
  103 + // 3.2 按 protocolType 筛选(如果非空)
  104 + if (StrUtil.isNotEmpty(listReqDTO.getProtocolType())) {
  105 + IotProductDO product = productMap.get(device.getProductId());
  106 + if (product == null || ObjUtil.notEqual(listReqDTO.getProtocolType(), product.getProtocolType())) {
  107 + continue;
  108 + }
  109 + }
  110 + // 3.3 获取启用的点位列表
106 111 List<IotDeviceModbusPointDO> pointList = pointMap.get(config.getDeviceId());
107 112 if (CollUtil.isEmpty(pointList)) {
108 113 continue;
109 114 }
110   - // 3.3 构建 IotModbusDeviceConfigRespDTO 对象
  115 +
  116 + // 3.4 构建 IotModbusDeviceConfigRespDTO 对象
111 117 IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o ->
112 118 o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())
113 119 .setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)));
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java
... ... @@ -33,13 +33,11 @@ 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")
  36 + @Schema(description = "工作模式", example = "1")
38 37 private Integer mode;
39 38  
40   - // TODO @AI:还是换成 int,然后写注释;不要【:modbus_tcp / modbus_rtu】
41   - @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp")
42   - private String frameFormat;
  39 + @Schema(description = "数据帧格式", example = "1")
  40 + private Integer frameFormat;
43 41  
44 42 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
45 43 private Integer status;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
2 2  
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
3 6 import io.swagger.v3.oas.annotations.media.Schema;
4   -import jakarta.validation.constraints.NotEmpty;
5 7 import jakarta.validation.constraints.NotNull;
6 8 import lombok.Data;
7 9  
... ... @@ -13,12 +15,14 @@ public class IotDeviceModbusConfigSaveReqVO {
13 15 @NotNull(message = "设备编号不能为空")
14 16 private Long deviceId;
15 17  
16   - @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100")
17   - @NotEmpty(message = "Modbus 服务器 IP 地址不能为空")
  18 + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100")
  19 +// @NotEmpty(message = "Modbus 服务器 IP 地址不能为空")
  20 + // TODO @AI:这个字段,要根据情况校验;
18 21 private String ip;
19 22  
20   - @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502")
21   - @NotNull(message = "Modbus 端口不能为空")
  23 + @Schema(description = "Modbus 端口", example = "502")
  24 +// @NotNull(message = "Modbus 端口不能为空")
  25 + // TODO @AI:这个字段,要根据情况校验;
22 26 private Integer port;
23 27  
24 28 @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
... ... @@ -31,13 +35,13 @@ public class IotDeviceModbusConfigSaveReqVO {
31 35 @Schema(description = "重试间隔(毫秒)", example = "1000")
32 36 private Integer retryInterval;
33 37  
34   - // TODO @AI:不要【:1-云端轮询 2-主动上报】
35   - @Schema(description = "模式:1-云端轮询 2-主动上报", example = "1")
  38 + @Schema(description = "工作模式", example = "1")
  39 + @InEnum(IotModbusModeEnum.class)
36 40 private Integer mode;
37 41  
38   - // TODO @AI:不要【:1-云端轮询 2-主动上报】
39   - @Schema(description = "数据帧格式:modbus_tcp / modbus_rtu", example = "modbus_tcp")
40   - private String frameFormat;
  42 + @Schema(description = "数据帧格式", example = "1")
  43 + @InEnum(IotModbusFrameFormatEnum.class)
  44 + private Integer frameFormat;
41 45  
42 46 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
43 47 @NotNull(message = "状态不能为空")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java
... ... @@ -69,7 +69,7 @@ public class IotDeviceModbusConfigDO extends TenantBaseDO {
69 69 *
70 70 * @see cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum
71 71 */
72   - private String frameFormat;
  72 + private Integer frameFormat;
73 73 /**
74 74 * 状态
75 75 *
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java
1 1 package cn.iocoder.yudao.module.iot.dal.mysql.device;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
4 6 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
5 7 import org.apache.ibatis.annotations.Mapper;
6 8  
... ... @@ -18,8 +20,11 @@ public interface IotDeviceModbusConfigMapper extends BaseMapperX&lt;IotDeviceModbus
18 20 return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId);
19 21 }
20 22  
21   - default List<IotDeviceModbusConfigDO> selectListByStatus(Integer status) {
22   - return selectList(IotDeviceModbusConfigDO::getStatus, status);
  23 + default List<IotDeviceModbusConfigDO> selectList(IotModbusDeviceConfigListReqDTO reqDTO) {
  24 + return selectList(new LambdaQueryWrapperX<IotDeviceModbusConfigDO>()
  25 + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqDTO.getStatus())
  26 + .eqIfPresent(IotDeviceModbusConfigDO::getMode, reqDTO.getMode())
  27 + .inIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqDTO.getDeviceIds()));
23 28 }
24 29  
25 30 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java
1 1 package cn.iocoder.yudao.module.iot.service.device;
2 2  
3 3 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
4 5 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
5 6 import jakarta.validation.Valid;
6 7  
... ... @@ -37,10 +38,11 @@ public interface IotDeviceModbusConfigService {
37 38 IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId);
38 39  
39 40 /**
40   - * 获得所有启用的 Modbus 连接配置列表
  41 + * 获得 Modbus 连接配置列表
41 42 *
42   - * @return 启用的 Modbus 连接配置列表
  43 + * @param listReqDTO 查询参数
  44 + * @return Modbus 连接配置列表
43 45 */
44   - List<IotDeviceModbusConfigDO> getEnabledDeviceModbusConfigList();
  46 + List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO);
45 47  
46 48 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java
1 1 package cn.iocoder.yudao.module.iot.service.device;
2 2  
3   -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
7 7 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper;
8 8 import jakarta.annotation.Resource;
... ... @@ -54,8 +54,8 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe
54 54 }
55 55  
56 56 @Override
57   - public List<IotDeviceModbusConfigDO> getEnabledDeviceModbusConfigList() {
58   - return modbusConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  57 + public List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) {
  58 + return modbusConfigMapper.selectList(listReqDTO);
59 59 }
60 60  
61 61 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java
... ... @@ -10,6 +10,9 @@ import javax.annotation.Nullable;
10 10 import java.time.LocalDateTime;
11 11 import java.util.Collection;
12 12 import java.util.List;
  13 +import java.util.Map;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
13 16  
14 17 /**
15 18 * IoT 产品 Service 接口
... ... @@ -122,6 +125,24 @@ public interface IotProductService {
122 125 Long getProductCount(@Nullable LocalDateTime createTime);
123 126  
124 127 /**
  128 + * 批量获得产品列表
  129 + *
  130 + * @param ids 产品编号集合
  131 + * @return 产品列表
  132 + */
  133 + List<IotProductDO> getProductList(Collection<Long> ids);
  134 +
  135 + /**
  136 + * 批量获得产品 Map
  137 + *
  138 + * @param ids 产品编号集合
  139 + * @return 产品 Map(key: 产品编号, value: 产品)
  140 + */
  141 + default Map<Long, IotProductDO> getProductMap(Collection<Long> ids) {
  142 + return convertMap(getProductList(ids), IotProductDO::getId);
  143 + }
  144 +
  145 + /**
125 146 * 批量校验产品存在
126 147 *
127 148 * @param ids 产品编号集合
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
... ... @@ -172,6 +172,11 @@ public class IotProductServiceImpl implements IotProductService {
172 172 }
173 173  
174 174 @Override
  175 + public List<IotProductDO> getProductList(Collection<Long> ids) {
  176 + return productMapper.selectByIds(ids);
  177 + }
  178 +
  179 + @Override
175 180 public void validateProductsExist(Collection<Long> ids) {
176 181 if (CollUtil.isEmpty(ids)) {
177 182 return;
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
1 1 package cn.iocoder.yudao.module.iot.core.biz;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
5   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
6   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
7   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
8   -
9   -import java.util.List;
10   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.*;
11 5 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
12 6 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
13 7 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
... ... @@ -54,10 +48,11 @@ public interface IotDeviceCommonApi {
54 48 CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
55 49  
56 50 /**
57   - * 获取所有启用的 Modbus 设备配置列表
  51 + * 获取 Modbus 设备配置列表
58 52 *
  53 + * @param listReqDTO 查询参数
59 54 * @return Modbus 设备配置列表
60 55 */
61   - CommonResult<List<IotModbusDeviceConfigRespDTO>> getEnabledModbusDeviceConfigs();
  56 + CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO);
62 57  
63 58 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.biz.dto;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +import java.util.Set;
  7 +
  8 +/**
  9 + * IoT Modbus 设备配置列表查询 Request DTO
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class IotModbusDeviceConfigListReqDTO {
  16 +
  17 + /**
  18 + * 状态
  19 + */
  20 + private Integer status;
  21 +
  22 + /**
  23 + * 模式
  24 + */
  25 + private Integer mode;
  26 +
  27 + /**
  28 + * 协议类型
  29 + */
  30 + private String protocolType;
  31 +
  32 + /**
  33 + * 设备 ID 集合
  34 + */
  35 + private Set<Long> deviceIds;
  36 +
  37 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java
... ... @@ -54,7 +54,7 @@ public class IotModbusDeviceConfigRespDTO {
54 54 /**
55 55 * 数据帧格式
56 56 */
57   - private String frameFormat;
  57 + private Integer frameFormat;
58 58  
59 59 // ========== Modbus 点位配置 ==========
60 60  
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFrameFormatEnum.java
... ... @@ -13,26 +13,22 @@ import java.util.Arrays;
13 13 */
14 14 @Getter
15 15 @RequiredArgsConstructor
16   -public enum IotModbusFrameFormatEnum implements ArrayValuable<String> {
  16 +public enum IotModbusFrameFormatEnum implements ArrayValuable<Integer> {
17 17  
18   - MODBUS_TCP("modbus_tcp", "Modbus TCP"),
19   - MODBUS_RTU("modbus_rtu", "Modbus RTU");
  18 + MODBUS_TCP(1),
  19 + MODBUS_RTU(2);
20 20  
21   - public static final String[] ARRAYS = Arrays.stream(values())
  21 + public static final Integer[] ARRAYS = Arrays.stream(values())
22 22 .map(IotModbusFrameFormatEnum::getFormat)
23   - .toArray(String[]::new);
  23 + .toArray(Integer[]::new);
24 24  
25 25 /**
26 26 * 格式
27 27 */
28   - private final String format;
29   - /**
30   - * 名称
31   - */
32   - private final String name;
  28 + private final Integer format;
33 29  
34 30 @Override
35   - public String[] array() {
  31 + public Integer[] array() {
36 32 return ARRAYS;
37 33 }
38 34  
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
... ... @@ -25,6 +25,14 @@ public class IotDeviceAuthUtils {
25 25 return String.format("%s.%s", productKey, deviceName);
26 26 }
27 27  
  28 + public static String buildClientIdFromUsername(String username) {
  29 + IotDeviceIdentity identity = parseUsername(username);
  30 + if (identity == null) {
  31 + return null;
  32 + }
  33 + return buildClientId(identity.getProductKey(), identity.getDeviceName());
  34 + }
  35 +
28 36 public static String buildUsername(String productKey, String deviceName) {
29 37 return String.format("%s&%s", deviceName, productKey);
30 38 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java
... ... @@ -32,6 +32,10 @@ public abstract class AbstractIotModbusPollScheduler {
32 32 * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
33 33 */
34 34 private static final long MIN_REQUEST_INTERVAL = 1000;
  35 + /**
  36 + * 每个设备请求队列的最大长度,超出时丢弃最旧请求
  37 + */
  38 + private static final int MAX_QUEUE_SIZE = 1000;
35 39  
36 40 /**
37 41 * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
... ... @@ -159,6 +163,11 @@ public abstract class AbstractIotModbusPollScheduler {
159 163 private void submitPollRequest(Long deviceId, Long pointId) {
160 164 // 1. 【重要】将请求添加到设备的请求队列
161 165 Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
  166 + while (queue.size() >= MAX_QUEUE_SIZE) {
  167 + // 超出上限时,丢弃最旧的请求
  168 + queue.poll();
  169 + log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE);
  170 + }
162 171 queue.offer(() -> pollPoint(deviceId, pointId));
163 172  
164 173 // 2. 处理设备请求队列(如果没有延迟 timer 在等待)
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ArrayUtil;
5 5 import cn.hutool.core.util.ObjectUtil;
  6 +import cn.hutool.core.util.StrUtil;
6 7 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
7 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
8 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
... ... @@ -331,10 +332,10 @@ public class IotModbusCommonUtils {
331 332 // 其他字节序调整
332 333 byte[] result = new byte[bytes.length];
333 334 switch (byteOrderEnum) {
334   - case BA: // 小端序(16 位)
335   - if (bytes.length >= 2) {
336   - result[0] = bytes[1];
337   - result[1] = bytes[0];
  335 + case BA: // 小端序:按每 2 字节一组交换(16 位场景 [1,0],32 位场景 [1,0,3,2])
  336 + for (int i = 0; i + 1 < bytes.length; i += 2) {
  337 + result[i] = bytes[i + 1];
  338 + result[i + 1] = bytes[i];
338 339 }
339 340 break;
340 341 case CDAB: // 大端字交换(32 位)
... ... @@ -509,6 +510,9 @@ public class IotModbusCommonUtils {
509 510 * @return 匹配的点位配置,未找到返回 null
510 511 */
511 512 public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
  513 + if (config == null || StrUtil.isBlank(identifier)) {
  514 + return null;
  515 + }
512 516 return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
513 517 }
514 518  
... ... @@ -520,6 +524,9 @@ public class IotModbusCommonUtils {
520 524 * @return 匹配的点位配置,未找到返回 null
521 525 */
522 526 public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) {
  527 + if (config == null || pointId == null) {
  528 + return null;
  529 + }
523 530 return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
524 531 }
525 532  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
4   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager;
5 5 import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
6 6 import com.ghgande.j2mod.modbus.msg.*;
7 7 import com.ghgande.j2mod.modbus.procimg.InputRegister;
... ... @@ -19,7 +19,7 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.I
19 19 * <p>
20 20 * 封装基于 j2mod 的 Modbus TCP 读写操作:
21 21 * 1. 根据功能码创建对应的 Modbus 读/写请求
22   - * 2. 通过 {@link IotModbusTcpConnectionManager.ModbusConnection} 执行事务
  22 + * 2. 通过 {@link IotModbusTcpMasterConnectionManager.ModbusConnection} 执行事务
23 23 * 3. 从响应中提取原始值
24 24 *
25 25 * @author 芋道源码
... ... @@ -36,9 +36,9 @@ public class IotModbusTcpMasterUtils {
36 36 * @param point 点位配置
37 37 * @return 原始值(int 数组)
38 38 */
39   - public static Future<int[]> read(IotModbusTcpConnectionManager.ModbusConnection connection,
40   - Integer slaveId,
41   - IotModbusPointRespDTO point) {
  39 + public static Future<int[]> read(IotModbusTcpMasterConnectionManager.ModbusConnection connection,
  40 + Integer slaveId,
  41 + IotModbusPointRespDTO point) {
42 42 return connection.executeBlocking(tcpConnection -> {
43 43 try {
44 44 // 1. 创建请求
... ... @@ -70,10 +70,10 @@ public class IotModbusTcpMasterUtils {
70 70 * @param values 要写入的值
71 71 * @return 是否成功
72 72 */
73   - public static Future<Boolean> write(IotModbusTcpConnectionManager.ModbusConnection connection,
74   - Integer slaveId,
75   - IotModbusPointRespDTO point,
76   - int[] values) {
  73 + public static Future<Boolean> write(IotModbusTcpMasterConnectionManager.ModbusConnection connection,
  74 + Integer slaveId,
  75 + IotModbusPointRespDTO point,
  76 + int[] values) {
77 77 return connection.executeBlocking(tcpConnection -> {
78 78 try {
79 79 // 1. 创建请求
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java
... ... @@ -6,16 +6,15 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
6 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
7 7 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
8 8 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
9   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
11 10 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
12 11 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
13   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler;
14   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber;
15   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
16   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
17   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
18   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpPollScheduler;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamHandler;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamSubscriber;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterPollScheduler;
19 18 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
20 19 import io.vertx.core.Vertx;
21 20 import lombok.Getter;
... ... @@ -23,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
23 22 import org.redisson.api.RedissonClient;
24 23  
25 24 import java.util.List;
  25 +import java.util.Set;
26 26 import java.util.concurrent.TimeUnit;
27 27  
28 28 /**
... ... @@ -61,15 +61,14 @@ public class IotModbusTcpMasterProtocol implements IotProtocol {
61 61 /**
62 62 * 连接管理器
63 63 */
64   - private final IotModbusTcpConnectionManager connectionManager;
  64 + private final IotModbusTcpMasterConnectionManager connectionManager;
65 65 /**
66 66 * 下行消息订阅者
67 67 */
68   - private final IotModbusTcpDownstreamSubscriber downstreamSubscriber;
  68 + private final IotModbusTcpMasterDownstreamSubscriber downstreamSubscriber;
69 69  
70   - private final IotModbusTcpConfigCacheService configCacheService;
71   - private final IotModbusTcpPollScheduler pollScheduler;
72   - private final IotDeviceMessageService messageService;
  70 + private final IotModbusTcpMasterConfigCacheService configCacheService;
  71 + private final IotModbusTcpMasterPollScheduler pollScheduler;
73 72  
74 73 public IotModbusTcpMasterProtocol(ProtocolProperties properties) {
75 74 IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster();
... ... @@ -83,22 +82,23 @@ public class IotModbusTcpMasterProtocol implements IotProtocol {
83 82 // 初始化 Manager
84 83 RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
85 84 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
86   - this.connectionManager = new IotModbusTcpConnectionManager(redissonClient, vertx);
87   - this.configCacheService = new IotModbusTcpConfigCacheService(deviceApi);
  85 + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
  86 + this.configCacheService = new IotModbusTcpMasterConfigCacheService(deviceApi);
  87 + // DONE @AI:上线/下线消息已移到 ConnectionManager 内部处理,不再走回调
  88 + this.connectionManager = new IotModbusTcpMasterConnectionManager(redissonClient, vertx,
  89 + messageService, configCacheService, serverId);
88 90  
89 91 // 初始化 Handler
90   - this.messageService = SpringUtil.getBean(IotDeviceMessageService.class);
91   - IotDeviceMessageService messageService = this.messageService;
92   - IotModbusTcpUpstreamHandler upstreamHandler = new IotModbusTcpUpstreamHandler(messageService, serverId);
93   - IotModbusTcpDownstreamHandler downstreamHandler = new IotModbusTcpDownstreamHandler(connectionManager,
  92 + IotModbusTcpMasterUpstreamHandler upstreamHandler = new IotModbusTcpMasterUpstreamHandler(messageService, serverId);
  93 + IotModbusTcpMasterDownstreamHandler downstreamHandler = new IotModbusTcpMasterDownstreamHandler(connectionManager,
94 94 configCacheService);
95 95  
96 96 // 初始化轮询调度器
97   - this.pollScheduler = new IotModbusTcpPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService);
  97 + this.pollScheduler = new IotModbusTcpMasterPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService);
98 98  
99 99 // 初始化下行消息订阅者
100 100 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
101   - this.downstreamSubscriber = new IotModbusTcpDownstreamSubscriber(this, downstreamHandler, messageBus);
  101 + this.downstreamSubscriber = new IotModbusTcpMasterDownstreamSubscriber(this, downstreamHandler, messageBus);
102 102 }
103 103  
104 104 @Override
... ... @@ -196,46 +196,21 @@ public class IotModbusTcpMasterProtocol implements IotProtocol {
196 196 // 2. 更新连接和轮询任务
197 197 for (IotModbusDeviceConfigRespDTO config : configs) {
198 198 try {
199   - // 2.1 检测是否为首次连接
200   - boolean isNewConnection = connectionManager.getConnection(config.getDeviceId()) == null;
201   - // 2.2 确保连接存在
  199 + // 2.1 确保连接存在
202 200 connectionManager.ensureConnection(config);
203   - // 2.3 首次建连成功后发送上线消息
204   - // TODO @AI:在这里判断上线 ,会不会有点奇怪???
205   - if (isNewConnection && connectionManager.getConnection(config.getDeviceId()) != null) {
206   - try {
207   - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
208   - messageService.sendDeviceMessage(onlineMessage,
209   - config.getProductKey(), config.getDeviceName(), serverId);
210   - } catch (Exception ex) {
211   - log.error("[refreshConfig][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex);
212   - }
213   - }
214   - // 2.4 更新轮询任务
  201 + // 2.2 更新轮询任务
215 202 pollScheduler.updatePolling(config);
216 203 } catch (Exception e) {
217 204 log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
218 205 }
219 206 }
220 207  
221   - // 3. 清理已删除设备的资源(仅 API 成功时才执行)
222   - configCacheService.cleanupRemovedDevices(configs, deviceId -> {
223   - // 3.1 发送设备下线消息
224   - // TODO @AI:在这里判断上线 ,会不会有点奇怪???
225   - IotModbusDeviceConfigRespDTO removedConfig = configCacheService.getConfig(deviceId);
226   - if (removedConfig != null) {
227   - try {
228   - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
229   - messageService.sendDeviceMessage(offlineMessage,
230   - removedConfig.getProductKey(), removedConfig.getDeviceName(), serverId);
231   - } catch (Exception ex) {
232   - log.error("[refreshConfig][发送设备下线消息失败, deviceId={}]", deviceId, ex);
233   - }
234   - }
235   - // 3.2 停止轮询和移除连接
  208 + // 3. 清理已删除设备的资源
  209 + Set<Long> removedDeviceIds = configCacheService.cleanupRemovedDevices(configs);
  210 + for (Long deviceId : removedDeviceIds) {
236 211 pollScheduler.stopPolling(deviceId);
237 212 connectionManager.removeDevice(deviceId);
238   - });
  213 + }
239 214 } catch (Exception e) {
240 215 log.error("[refreshConfig][刷新配置失败]", e);
241 216 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/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/IotModbusTcpMasterDownstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream;
2 2  
  3 +import cn.hutool.core.util.ObjUtil;
3 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
4 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
5 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 8 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
8 9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils;
9   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager;
11 12 import lombok.RequiredArgsConstructor;
12 13 import lombok.extern.slf4j.Slf4j;
13 14  
14 15 import java.util.Map;
15 16  
16 17 /**
17   - * IoT Modbus TCP 下行消息处理器
  18 + * IoT Modbus TCP Master 下行消息处理器
18 19 * <p>
19 20 * 负责:
20 21 * 1. 处理下行消息(如属性设置 thing.service.property.set)
21   - * 2. 执行 Modbus 写入操作
  22 + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
22 23 *
23 24 * @author 芋道源码
24 25 */
25 26 @RequiredArgsConstructor
26 27 @Slf4j
27   -public class IotModbusTcpDownstreamHandler {
  28 +public class IotModbusTcpMasterDownstreamHandler {
28 29  
29   - private final IotModbusTcpConnectionManager connectionManager;
30   - private final IotModbusTcpConfigCacheService configCacheService;
  30 + private final IotModbusTcpMasterConnectionManager connectionManager;
  31 + private final IotModbusTcpMasterConfigCacheService configCacheService;
31 32  
32 33 /**
33 34 * 处理下行消息
34 35 */
35   - @SuppressWarnings("unchecked")
  36 + @SuppressWarnings({"unchecked", "DuplicatedCode"})
36 37 public void handle(IotDeviceMessage message) {
37 38 // 1.1 检查是否是属性设置消息
38   - if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) {
  39 + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
39 40 log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod());
40 41 return;
41 42 }
... ... @@ -78,7 +79,7 @@ public class IotModbusTcpDownstreamHandler {
78 79 */
79 80 private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) {
80 81 // 1.1 获取连接
81   - IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId());
  82 + IotModbusTcpMasterConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId());
82 83 if (connection == null) {
83 84 log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId());
84 85 return;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/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/IotModbusTcpMasterDownstreamSubscriber.java
... ... @@ -12,13 +12,13 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotModbusTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
  15 +public class IotModbusTcpMasterDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17   - private final IotModbusTcpDownstreamHandler downstreamHandler;
  17 + private final IotModbusTcpMasterDownstreamHandler downstreamHandler;
18 18  
19   - public IotModbusTcpDownstreamSubscriber(IotModbusTcpMasterProtocol protocol,
20   - IotModbusTcpDownstreamHandler downstreamHandler,
21   - IotMessageBus messageBus) {
  19 + public IotModbusTcpMasterDownstreamSubscriber(IotModbusTcpMasterProtocol protocol,
  20 + IotModbusTcpMasterDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
22 22 super(protocol, messageBus);
23 23 this.downstreamHandler = downstreamHandler;
24 24 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/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/IotModbusTcpMasterUpstreamHandler.java
... ... @@ -17,13 +17,14 @@ import java.util.Map;
17 17 * @author 芋道源码
18 18 */
19 19 @Slf4j
20   -public class IotModbusTcpUpstreamHandler {
  20 +public class IotModbusTcpMasterUpstreamHandler {
21 21  
22 22 private final IotDeviceMessageService messageService;
  23 +
23 24 private final String serverId;
24 25  
25   - public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService,
26   - String serverId) {
  26 + public IotModbusTcpMasterUpstreamHandler(IotDeviceMessageService messageService,
  27 + String serverId) {
27 28 this.messageService = messageService;
28 29 this.serverId = serverId;
29 30 }
... ... @@ -39,7 +40,7 @@ public class IotModbusTcpUpstreamHandler {
39 40 IotModbusPointRespDTO point,
40 41 int[] rawValue) {
41 42 try {
42   - // 1.1 转换原始值为物模型属性值
  43 + // 1.1 转换原始值为物模型属性值(点位翻译)
43 44 Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point);
44 45 log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
45 46 config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/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/IotModbusTcpMasterConfigCacheService.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 5 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
5 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  8 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
  9 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 10 import lombok.RequiredArgsConstructor;
7 11 import lombok.extern.slf4j.Slf4j;
8 12  
9   -import java.util.*;
  13 +import java.util.HashSet;
  14 +import java.util.List;
  15 +import java.util.Map;
  16 +import java.util.Set;
10 17 import java.util.concurrent.ConcurrentHashMap;
11   -import java.util.function.Consumer;
12 18  
13 19 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
14 20  
15 21 /**
16   - * IoT Modbus TCP 配置缓存服务,负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更
  22 + * IoT Modbus TCP Master 配置缓存服务
17 23 *
18 24 * @author 芋道源码
19 25 */
20 26 @RequiredArgsConstructor
21 27 @Slf4j
22   -public class IotModbusTcpConfigCacheService {
  28 +public class IotModbusTcpMasterConfigCacheService {
23 29  
24 30 private final IotDeviceCommonApi deviceApi;
25 31  
... ... @@ -31,7 +37,7 @@ public class IotModbusTcpConfigCacheService {
31 37 /**
32 38 * 已知的设备 ID 集合(作用:用于检测已删除的设备)
33 39 *
34   - * @see #cleanupRemovedDevices(List, Consumer)
  40 + * @see #cleanupRemovedDevices(List)
35 41 */
36 42 private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
37 43  
... ... @@ -43,7 +49,9 @@ public class IotModbusTcpConfigCacheService {
43 49 public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
44 50 try {
45 51 // 1. 从远程获取配置
46   - CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs();
  52 + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
  53 + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
  54 + .setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_MASTER.getType()));
47 55 result.checkError();
48 56 List<IotModbusDeviceConfigRespDTO> configs = result.getData();
49 57  
... ... @@ -69,28 +77,30 @@ public class IotModbusTcpConfigCacheService {
69 77 }
70 78  
71 79 /**
72   - * 清理已删除设备的资源,并更新已知设备 ID 集合
  80 + * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合
  81 + *
  82 + * DONE @AI:不再使用 callback 模式,返回已删除的设备 ID 集合,由调用方直接清理
73 83 *
74 84 * @param currentConfigs 当前有效的配置列表
75   - * @param cleanupAction 清理动作
  85 + * @return 已删除的设备 ID 集合
76 86 */
77   - public void cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs, Consumer<Long> cleanupAction) {
  87 + public Set<Long> cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs) {
78 88 // 1.1 获取当前有效的设备 ID
79 89 Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
80 90 // 1.2 找出已删除的设备(基于旧的 knownDeviceIds)
81 91 Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
82 92 removedDeviceIds.removeAll(currentDeviceIds);
83 93  
84   - // 2. 清理已删除设备(先执行 cleanupAction,再从缓存移除,保证 action 中仍可获取 config)
  94 + // 2. 清理已删除设备的缓存
85 95 for (Long deviceId : removedDeviceIds) {
86 96 log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
87   - cleanupAction.accept(deviceId);
88 97 configCache.remove(deviceId);
89 98 }
90 99  
91 100 // 3. 更新已知设备 ID 集合为当前有效的设备 ID
92 101 knownDeviceIds.clear();
93 102 knownDeviceIds.addAll(currentDeviceIds);
  103 + return removedDeviceIds;
94 104 }
95 105  
96 106 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/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/IotModbusTcpMasterConnectionManager.java
... ... @@ -2,12 +2,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
2 2  
3 3 import cn.hutool.core.util.ObjUtil;
4 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  5 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  6 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
5 7 import com.ghgande.j2mod.modbus.net.TCPMasterConnection;
6 8 import io.vertx.core.Context;
7 9 import io.vertx.core.Future;
8 10 import io.vertx.core.Vertx;
9 11 import lombok.Data;
10   -import lombok.RequiredArgsConstructor;
11 12 import lombok.extern.slf4j.Slf4j;
12 13 import org.redisson.api.RLock;
13 14 import org.redisson.api.RedissonClient;
... ... @@ -26,14 +27,16 @@ import java.util.concurrent.ConcurrentHashMap;
26 27 *
27 28 * @author 芋道源码
28 29 */
29   -@RequiredArgsConstructor
30 30 @Slf4j
31   -public class IotModbusTcpConnectionManager {
  31 +public class IotModbusTcpMasterConnectionManager {
32 32  
33 33 private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:";
34 34  
35 35 private final RedissonClient redissonClient;
36 36 private final Vertx vertx;
  37 + private final IotDeviceMessageService messageService;
  38 + private final IotModbusTcpMasterConfigCacheService configCacheService;
  39 + private final String serverId;
37 40  
38 41 /**
39 42 * 连接池:key = ip:port
... ... @@ -45,8 +48,21 @@ public class IotModbusTcpConnectionManager {
45 48 */
46 49 private final Map<Long, String> deviceConnectionMap = new ConcurrentHashMap<>();
47 50  
  51 + public IotModbusTcpMasterConnectionManager(RedissonClient redissonClient, Vertx vertx,
  52 + IotDeviceMessageService messageService,
  53 + IotModbusTcpMasterConfigCacheService configCacheService,
  54 + String serverId) {
  55 + this.redissonClient = redissonClient;
  56 + this.vertx = vertx;
  57 + this.messageService = messageService;
  58 + this.configCacheService = configCacheService;
  59 + this.serverId = serverId;
  60 + }
  61 +
48 62 /**
49 63 * 确保连接存在
  64 + * <p>
  65 + * 首次建连成功时,直接发送设备上线消息
50 66 *
51 67 * @param config 设备配置
52 68 */
... ... @@ -59,37 +75,39 @@ public class IotModbusTcpConnectionManager {
59 75 config.getDeviceId(), oldConnectionKey, connectionKey);
60 76 removeDevice(config.getDeviceId());
61 77 }
62   - // 1.2 记录设备与连接的关系
  78 + // 1.2 记录设备与连接的映射
63 79 deviceConnectionMap.put(config.getDeviceId(), connectionKey);
64 80  
65   - // 2. 情况一:连接已存在,添加设备引用
  81 + // 2. 情况一:连接已存在,注册设备并发送上线消息
66 82 ModbusConnection connection = connectionPool.get(connectionKey);
67 83 if (connection != null) {
68   - connection.addDevice(config.getDeviceId(), config.getSlaveId());
  84 + addDeviceAndOnline(connection, config);
69 85 return;
70 86 }
71 87  
72   - // 3. 情况二:连接不存在,创建新连接
73   - // 3.1 尝试获取分布式锁
  88 + // 3. 情况二:连接不存在,加分布式锁创建新连接
74 89 RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey);
75 90 if (!lock.tryLock()) {
76 91 log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey);
77 92 return;
78 93 }
79   - // 3.2 double-check:拿到锁后再次检查,避免并发创建重复连接
80 94 try {
  95 + // 3.1 double-check:拿到锁后再次检查,避免并发创建重复连接
81 96 connection = connectionPool.get(connectionKey);
82 97 if (connection != null) {
83   - connection.addDevice(config.getDeviceId(), config.getSlaveId());
84   - lock.unlock();
  98 + addDeviceAndOnline(connection, config);
85 99 return;
86 100 }
87   - // 3.3 创建新连接
88   - connection = createConnection(config, lock);
  101 + // 3.2 创建新连接
  102 + connection = createConnection(config);
89 103 connectionPool.put(connectionKey, connection);
90 104 log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey);
  105 + // 3.3 注册设备并发送上线消息
  106 + addDeviceAndOnline(connection, config);
91 107 } catch (Exception e) {
92 108 log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e);
  109 + } finally {
  110 + // TODO @AI:如果这里释放,会不会出现,集群模式下,多个节点同时创建连接的情况?需要验证一下 Redisson 的分布式锁特性?!
93 111 lock.unlock();
94 112 }
95 113 }
... ... @@ -97,7 +115,7 @@ public class IotModbusTcpConnectionManager {
97 115 /**
98 116 * 创建 Modbus TCP 连接
99 117 */
100   - private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config, RLock lock) throws Exception {
  118 + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception {
101 119 // 1. 创建 TCP 连接
102 120 TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp()));
103 121 tcpConnection.setPort(config.getPort());
... ... @@ -105,12 +123,10 @@ public class IotModbusTcpConnectionManager {
105 123 tcpConnection.connect();
106 124  
107 125 // 2. 创建 Modbus 连接对象
108   - ModbusConnection connection = new ModbusConnection()
  126 + return new ModbusConnection()
109 127 .setConnectionKey(buildConnectionKey(config.getIp(), config.getPort()))
110   - .setTcpConnection(tcpConnection).setLock(lock).setContext(vertx.getOrCreateContext())
  128 + .setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext())
111 129 .setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval());
112   - connection.addDevice(config.getDeviceId(), config.getSlaveId());
113   - return connection;
114 130 }
115 131  
116 132 /**
... ... @@ -137,25 +153,71 @@ public class IotModbusTcpConnectionManager {
137 153  
138 154 /**
139 155 * 移除设备
  156 + * <p>
  157 + * 移除时直接发送设备下线消息
140 158 */
141 159 public void removeDevice(Long deviceId) {
142   - // 1. 移除设备引用
  160 + // 1.1 移除设备时,发送下线消息
  161 + sendOfflineMessage(deviceId);
  162 + // 1.2 移除设备引用
143 163 String connectionKey = deviceConnectionMap.remove(deviceId);
144 164 if (connectionKey == null) {
145 165 return;
146 166 }
  167 +
  168 + // 2.1 移除连接中的设备引用
147 169 ModbusConnection connection = connectionPool.get(connectionKey);
148 170 if (connection == null) {
149 171 return;
150 172 }
151 173 connection.removeDevice(deviceId);
152   -
153   - // 2. 如果没有设备引用了,关闭连接
  174 + // 2.2 如果没有设备引用了,关闭连接
154 175 if (connection.getDeviceCount() == 0) {
155 176 closeConnection(connectionKey);
156 177 }
157 178 }
158 179  
  180 + // ==================== 设备连接 & 上下线消息 ====================
  181 +
  182 + /**
  183 + * 注册设备到连接,并发送上线消息
  184 + */
  185 + private void addDeviceAndOnline(ModbusConnection connection,
  186 + IotModbusDeviceConfigRespDTO config) {
  187 + connection.addDevice(config.getDeviceId(), config.getSlaveId());
  188 + sendOnlineMessage(config);
  189 + }
  190 +
  191 + /**
  192 + * 发送设备上线消息
  193 + */
  194 + private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) {
  195 + try {
  196 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  197 + messageService.sendDeviceMessage(onlineMessage,
  198 + config.getProductKey(), config.getDeviceName(), serverId);
  199 + } catch (Exception ex) {
  200 + log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex);
  201 + }
  202 + }
  203 +
  204 + /**
  205 + * 发送设备下线消息
  206 + */
  207 + private void sendOfflineMessage(Long deviceId) {
  208 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
  209 + if (config == null) {
  210 + return;
  211 + }
  212 + try {
  213 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  214 + messageService.sendDeviceMessage(offlineMessage,
  215 + config.getProductKey(), config.getDeviceName(), serverId);
  216 + } catch (Exception ex) {
  217 + log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex);
  218 + }
  219 + }
  220 +
159 221 /**
160 222 * 关闭指定连接
161 223 */
... ... @@ -170,10 +232,10 @@ public class IotModbusTcpConnectionManager {
170 232 connection.getTcpConnection().close();
171 233 }
172 234 // 强制解锁,避免死锁(正常情况下应该不会发生锁未释放的情况)
173   - RLock lock = connection.getLock();
174   - if (lock != null && lock.isLocked()) {
175   - lock.forceUnlock();
176   - }
  235 +// RLock lock = connection.getLock();
  236 +// if (lock != null && lock.isLocked()) {
  237 +// lock.forceUnlock();
  238 +// }
177 239 log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey);
178 240 } catch (Exception e) {
179 241 log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e);
... ... @@ -202,11 +264,14 @@ public class IotModbusTcpConnectionManager {
202 264  
203 265 private String connectionKey;
204 266 private TCPMasterConnection tcpConnection;
205   - private RLock lock;
206 267 private Integer timeout;
207 268 private Integer retryInterval;
  269 +
208 270 private Context context;
209 271  
  272 + // TODO @AI:是不是需要 lock?!避免集群模式下的竞争(肯定不能让别的节点连接上)!!!【另外,RLock 在节点(持有所锁的节点) cransh 的时候,会自动释放】
  273 +// private RLock lock;
  274 +
210 275 /**
211 276 * 设备 ID 到 slave ID 的映射
212 277 */
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/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/IotModbusTcpMasterPollScheduler.java
... ... @@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
7 7 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler;
11 11 import io.vertx.core.Vertx;
12 12 import lombok.extern.slf4j.Slf4j;
13 13  
... ... @@ -17,16 +17,16 @@ import lombok.extern.slf4j.Slf4j;
17 17 * @author 芋道源码
18 18 */
19 19 @Slf4j
20   -public class IotModbusTcpPollScheduler extends AbstractIotModbusPollScheduler {
  20 +public class IotModbusTcpMasterPollScheduler extends AbstractIotModbusPollScheduler {
21 21  
22   - private final IotModbusTcpConnectionManager connectionManager;
23   - private final IotModbusTcpUpstreamHandler upstreamHandler;
24   - private final IotModbusTcpConfigCacheService configCacheService;
  22 + private final IotModbusTcpMasterConnectionManager connectionManager;
  23 + private final IotModbusTcpMasterUpstreamHandler upstreamHandler;
  24 + private final IotModbusTcpMasterConfigCacheService configCacheService;
25 25  
26   - public IotModbusTcpPollScheduler(Vertx vertx,
27   - IotModbusTcpConnectionManager connectionManager,
28   - IotModbusTcpUpstreamHandler upstreamHandler,
29   - IotModbusTcpConfigCacheService configCacheService) {
  26 + public IotModbusTcpMasterPollScheduler(Vertx vertx,
  27 + IotModbusTcpMasterConnectionManager connectionManager,
  28 + IotModbusTcpMasterUpstreamHandler upstreamHandler,
  29 + IotModbusTcpMasterConfigCacheService configCacheService) {
30 30 super(vertx);
31 31 this.connectionManager = connectionManager;
32 32 this.upstreamHandler = upstreamHandler;
... ... @@ -54,7 +54,7 @@ public class IotModbusTcpPollScheduler extends AbstractIotModbusPollScheduler {
54 54 }
55 55  
56 56 // 2.1 获取连接
57   - IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
  57 + IotModbusTcpMasterConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
58 58 if (connection == null) {
59 59 log.warn("[pollPoint][设备 {} 没有连接]", deviceId);
60 60 return;
... ...
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 Master 协议实现包
  2 + * Modbus TCP Master(主站)协议:网关主动连接并轮询 Modbus 从站设备
3 3 * <p>
4   - * 提供基于 j2mod 的 Modbus TCP 主站(Master)功能,支持:
5   - * 1. 定时轮询 Modbus 从站设备数据
6   - * 2. 下发属性设置命令到从站设备
7   - * 3. 数据格式转换(寄存器值 ↔ 物模型属性值)
  4 + * 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置
8 5 */
9 6 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/IotModbusTcpSlaveProtocol.java
... ... @@ -81,22 +81,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
81 81 */
82 82 private Long requestCleanupTimerId;
83 83  
84   - // ========== 各组件 ==========
85   - // TODO @芋艿:稍后排序下,有点小乱;
  84 + /**
  85 + * 连接管理器
  86 + */
  87 + private final IotModbusTcpSlaveConnectionManager connectionManager;
  88 + /**
  89 + * 下行消息订阅者
  90 + */
  91 + private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber;
86 92  
87   - private final IotModbusTcpSlaveConfig slaveConfig;
88 93 private final IotModbusFrameDecoder frameDecoder;
  94 + @SuppressWarnings("FieldCanBeLocal")
89 95 private final IotModbusFrameEncoder frameEncoder;
90   - private final IotModbusTcpSlaveConnectionManager connectionManager;
  96 +
91 97 private final IotModbusTcpSlaveConfigCacheService configCacheService;
92 98 private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
93 99 private final IotModbusTcpSlaveUpstreamHandler upstreamHandler;
94   - private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber;
95 100 private final IotModbusTcpSlavePollScheduler pollScheduler;
96 101 private final IotDeviceMessageService messageService;
97 102  
98 103 public IotModbusTcpSlaveProtocol(ProtocolProperties properties) {
99   - this.slaveConfig = properties.getModbusTcpSlave();
  104 + IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave();
100 105 Assert.notNull(slaveConfig, "Modbus TCP Slave 协议配置(modbusTcpSlave)不能为空");
101 106 this.properties = properties;
102 107 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
... ... @@ -124,10 +129,9 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
124 129  
125 130 // 初始化 Handler
126 131 this.messageService = SpringUtil.getBean(IotDeviceMessageService.class);
127   - IotDeviceMessageService messageService = this.messageService;
128 132 IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
129 133 this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler(
130   - deviceApi, messageService, frameEncoder,
  134 + deviceApi, this.messageService, frameEncoder,
131 135 connectionManager, configCacheService, pendingRequestManager,
132 136 pollScheduler, deviceService, serverId);
133 137  
... ... @@ -158,9 +162,9 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
158 162  
159 163 try {
160 164 // 1. 启动配置刷新定时器
161   - int refreshInterval = slaveConfig.getConfigRefreshInterval();
  165 + IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave();
162 166 configRefreshTimerId = vertx.setPeriodic(
163   - TimeUnit.SECONDS.toMillis(refreshInterval),
  167 + TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()),
164 168 id -> refreshConfig());
165 169  
166 170 // 2.1 启动 TCP Server
... ... @@ -178,6 +182,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
178 182 downstreamSubscriber.start();
179 183 } catch (Exception e) {
180 184 log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e);
  185 + // TODO @芋艿:后续统一优化 stop 逻辑;
181 186 if (configRefreshTimerId != null) {
182 187 vertx.cancelTimer(configRefreshTimerId);
183 188 configRefreshTimerId = null;
... ... @@ -223,9 +228,9 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
223 228 pollScheduler.stopAll();
224 229 // 2.3 清理 PendingRequest
225 230 pendingRequestManager.clear();
226   - // 2.3 关闭所有连接
  231 + // 2.4 关闭所有连接
227 232 connectionManager.closeAll();
228   - // 2.4 关闭 TCP Server
  233 + // 2.5 关闭 TCP Server
229 234 if (netServer != null) {
230 235 try {
231 236 netServer.close().result();
... ... @@ -308,9 +313,6 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
308 313  
309 314 /**
310 315 * 刷新已连接设备的配置(定时调用)
311   - * <p>
312   - * 与 tcpmaster 不同,slave 只刷新已连接设备的配置,不做全量 diff。
313   - * 设备的新增(认证时)和删除(断连时)分别在 {@link #handleConnection} 中处理。
314 316 */
315 317 private synchronized void refreshConfig() {
316 318 try {
... ... @@ -321,6 +323,10 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
321 323 }
322 324 List<IotModbusDeviceConfigRespDTO> configs =
323 325 configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds);
  326 + if (configs == null) {
  327 + log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]");
  328 + return;
  329 + }
324 330 log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size());
325 331  
326 332 // 2. 更新已连接设备的轮询任务
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java
... ... @@ -29,7 +29,7 @@ public class IotModbusFrame {
29 29 /**
30 30 * 事务标识符
31 31 * <p>
32   - * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值,
  32 + * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值
33 33 */
34 34 private Integer transactionId;
35 35  
... ... @@ -37,14 +37,13 @@ public class IotModbusFrame {
37 37 * 异常码
38 38 * <p>
39 39 * 当功能码最高位为 1 时(异常响应),此字段存储异常码。
40   - * 为 null 表示非异常响应。
41 40 *
42 41 * @see IotModbusCommonUtils#FC_EXCEPTION_MASK
43 42 */
44 43 private Integer exceptionCode;
45 44  
46 45 /**
47   - * 自定义功能码时的 JSON 字符串
  46 + * 自定义功能码时的 JSON 字符串(用于 auth 认证等等)
48 47 */
49 48 private String customData;
50 49  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
... ... @@ -20,20 +20,20 @@ import java.util.function.BiConsumer;
20 20 * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU
21 21 * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理
22 22 * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回
23   - * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动)
24   - * - MODBUS_RTU:功能码驱动的状态机
  23 + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动)
  24 + * - MODBUS_RTU:功能码驱动的状态机
25 25 *
26 26 * @author 芋道源码
27 27 */
  28 +@RequiredArgsConstructor
28 29 @Slf4j
29 30 public class IotModbusFrameDecoder {
30 31  
  32 + /**
  33 + * 自定义功能码
  34 + */
31 35 private final int customFunctionCode;
32 36  
33   - public IotModbusFrameDecoder(int customFunctionCode) {
34   - this.customFunctionCode = customFunctionCode;
35   - }
36   -
37 37 /**
38 38 * 创建带自动帧格式检测的 RecordParser
39 39 *
... ... @@ -82,7 +82,7 @@ public class IotModbusFrameDecoder {
82 82 // 提取 PDU 数据(从 functionCode 之后到末尾)
83 83 byte[] pdu = new byte[data.length - 8];
84 84 System.arraycopy(data, 8, pdu, 0, pdu.length);
85   -
  85 + // 构建 IotModbusFrame
86 86 return buildFrame(slaveId, functionCode, pdu, transactionId);
87 87 }
88 88  
... ... @@ -105,7 +105,7 @@ public class IotModbusFrameDecoder {
105 105 // PDU 数据(不含 slaveId、functionCode、CRC)
106 106 byte[] pdu = new byte[data.length - 4];
107 107 System.arraycopy(data, 2, pdu, 0, pdu.length);
108   -
  108 + // 构建 IotModbusFrame
109 109 return buildFrame(slaveId, functionCode, pdu, null);
110 110 }
111 111  
... ... @@ -144,7 +144,6 @@ public class IotModbusFrameDecoder {
144 144 /**
145 145 * 帧格式检测阶段 Handler(仅处理首包,探测后切换到对应的拆包 Handler)
146 146 */
147   - @SuppressWarnings("ClassCanBeRecord")
148 147 @RequiredArgsConstructor
149 148 private class DetectPhaseHandler implements Handler<Buffer> {
150 149  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
... ... @@ -51,7 +51,7 @@ public class IotModbusTcpSlaveDownstreamHandler {
51 51 /**
52 52 * 处理下行消息
53 53 */
54   - @SuppressWarnings("unchecked")
  54 + @SuppressWarnings({"unchecked", "DuplicatedCode"})
55 55 public void handle(IotDeviceMessage message) {
56 56 // 1.1 检查是否是属性设置消息
57 57 if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
... ... @@ -125,17 +125,26 @@ public class IotModbusTcpSlaveDownstreamHandler {
125 125 point.getRegisterAddress(), rawValues[0], frameFormat, transactionId);
126 126 } else if (writeMultipleCode != null) {
127 127 // 多个值:使用多写功能码(FC15/FC16)
128   - data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId,
129   - point.getRegisterAddress(), rawValues, frameFormat, transactionId);
  128 + if (writeMultipleCode == IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS) {
  129 + data = frameEncoder.encodeWriteMultipleCoilsRequest(slaveId,
  130 + point.getRegisterAddress(), rawValues, frameFormat, transactionId);
  131 + } else {
  132 + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId,
  133 + point.getRegisterAddress(), rawValues, frameFormat, transactionId);
  134 + }
130 135 } else {
131 136 log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode);
132 137 return;
133 138 }
134 139  
135   - // 2. 发送
136   - connectionManager.sendToDevice(deviceId, data);
137   - log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
138   - deviceId, point.getIdentifier(), value);
  140 + // 2. 发送消息
  141 + connectionManager.sendToDevice(deviceId, data).onSuccess(v ->
  142 + log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
  143 + deviceId, point.getIdentifier(), value)
  144 + ).onFailure(e ->
  145 + log.error("[writeProperty][写入失败, deviceId={}, identifier={}, value={}]",
  146 + deviceId, point.getIdentifier(), value, e)
  147 + );
139 148 }
140 149  
141 150 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4 3 import cn.hutool.core.lang.Assert;
5 4 import cn.hutool.core.map.MapUtil;
6 5 import cn.hutool.core.util.BooleanUtil;
  6 +import cn.hutool.core.util.ObjUtil;
7 7 import cn.hutool.core.util.StrUtil;
8 8 import cn.iocoder.yudao.framework.common.exception.ServiceException;
9 9 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
... ... @@ -61,6 +61,7 @@ public class IotModbusTcpSlaveUpstreamHandler {
61 61 private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
62 62 private final IotModbusTcpSlavePollScheduler pollScheduler;
63 63 private final IotDeviceService deviceService;
  64 +
64 65 private final String serverId;
65 66  
66 67 public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi,
... ... @@ -153,16 +154,20 @@ public class IotModbusTcpSlaveUpstreamHandler {
153 154 // 1. 解析认证参数
154 155 IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
155 156 Assert.notNull(request, "认证参数不能为空");
156   - Assert.notBlank(request.getClientId(), "clientId 不能为空");
157 157 Assert.notBlank(request.getUsername(), "username 不能为空");
158 158 Assert.notBlank(request.getPassword(), "password 不能为空");
  159 + // 特殊:考虑到 modbus 消息体积较小,默认 clientId 传递空串
  160 + if (StrUtil.isBlank(request.getClientId())) {
  161 + request.setClientId(IotDeviceAuthUtils.buildClientIdFromUsername(request.getUsername()));
  162 + }
  163 + Assert.notBlank(request.getClientId(), "clientId 不能为空");
159 164  
160 165 // 2.1 调用认证 API
161 166 CommonResult<Boolean> result = deviceApi.authDevice(request);
162 167 result.checkError();
163 168 if (BooleanUtil.isFalse(result.getData())) {
164 169 log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername());
165   - sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, 1, "认证失败");
  170 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "认证失败");
166 171 return;
167 172 }
168 173 // 2.2 解析设备信息
... ... @@ -171,7 +176,21 @@ public class IotModbusTcpSlaveUpstreamHandler {
171 176 // 2.3 获取设备信息
172 177 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
173 178 Assert.notNull(device, "设备不存在");
174   - // TODO @AI:2.4 必须找到连接配置;
  179 + // 2.4 加载设备 Modbus 配置,无配置则阻断认证
  180 + IotModbusDeviceConfigRespDTO modbusConfig = configCacheService.loadDeviceConfig(device.getId());
  181 + if (modbusConfig == null) {
  182 + log.warn("[handleAuth][设备 {} 没有 Modbus 点位配置, 拒绝认证]", device.getId());
  183 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "设备无 Modbus 配置");
  184 + return;
  185 + }
  186 + // 2.5 协议不一致,阻断认证
  187 + if (ObjUtil.notEqual(frameFormat.getFormat(), modbusConfig.getFrameFormat())) {
  188 + log.warn("[handleAuth][设备 {} frameFormat 不一致, 连接协议={}, 设备配置={},拒绝认证]",
  189 + device.getId(), frameFormat.getFormat(), modbusConfig.getFrameFormat());
  190 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(),
  191 + "frameFormat 协议不一致");
  192 + return;
  193 + }
175 194  
176 195 // 3.1 注册连接
177 196 ConnectionInfo connectionInfo = new ConnectionInfo()
... ... @@ -189,13 +208,8 @@ public class IotModbusTcpSlaveUpstreamHandler {
189 208 GlobalErrorCodeConstants.SUCCESS.getCode(), "success");
190 209 log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId());
191 210  
192   - // 4. 加载设备配置并启动轮询
193   - IotModbusDeviceConfigRespDTO config = configCacheService.loadDeviceConfig(device.getId());
194   - if (config != null) {
195   - pollScheduler.updatePolling(config);
196   - } else {
197   - log.warn("[handleAuth][认证成功但未找到设备配置, deviceId={}]", device.getId());
198   - }
  211 + // 4. 启动轮询
  212 + pollScheduler.updatePolling(modbusConfig);
199 213 }
200 214  
201 215 /**
... ... @@ -245,20 +259,19 @@ public class IotModbusTcpSlaveUpstreamHandler {
245 259 }
246 260 // 2.3 查找点位配置
247 261 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId());
248   - if (config == null || CollUtil.isEmpty(config.getPoints())) {
249   - return;
250   - }
251 262 IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId());
252 263 if (point == null) {
253 264 return;
254 265 }
255 266  
256   - // 3.1 点位翻译
  267 + // 3.1 转换原始值为物模型属性值(点位翻译)
257 268 Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point);
258   - // 3.2 上报属性
  269 + // 3.2 构造属性上报消息
259 270 Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
260 271 IotDeviceMessage message = IotDeviceMessage.requestOf(
261 272 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
  273 +
  274 + // 4. 发送到消息总线
262 275 messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId);
263 276 log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]",
264 277 info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 6 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
6 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
7 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  10 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  11 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
  12 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
8 13 import lombok.RequiredArgsConstructor;
9 14 import lombok.extern.slf4j.Slf4j;
10 15  
... ... @@ -13,16 +18,7 @@ import java.util.*;
13 18 import java.util.concurrent.ConcurrentHashMap;
14 19  
15 20 /**
16   - * IoT Modbus TCP Slave 配置缓存服务
17   - * <p>
18   - * 与 tcpmaster 的 {@code IotModbusTcpConfigCacheService} 不同:
19   - * - tcpmaster 启动时拉全量配置 → 主动建连,需要全量 diff 检测新增/删除设备
20   - * - tcpslave 设备主动连接 → 认证时按需加载配置,断连时清理,定时刷新已连接设备的配置
21   - * <p>
22   - * 配置生命周期:
23   - * 1. 认证时:{@link #loadDeviceConfig(Long)} 按 deviceId 从 API 加载配置到缓存
24   - * 2. 断连时:{@link #removeConfig(Long)} 从缓存中移除
25   - * 3. 定时刷新:{@link #refreshConnectedDeviceConfigList(Set)} 批量刷新已连接设备的配置
  21 + * IoT Modbus TCP Slave 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备
26 22 *
27 23 * @author 芋道源码
28 24 */
... ... @@ -41,44 +37,33 @@ public class IotModbusTcpSlaveConfigCacheService {
41 37  
42 38 /**
43 39 * 加载单个设备的配置(认证成功后调用)
44   - * <p>
45   - * 从远程 API 获取全量配置,然后按 deviceId 匹配。
46   - * 如果远程获取失败,尝试从 Mock 数据中匹配。
47 40 *
48 41 * @param deviceId 设备 ID
49   - * @return 设备配置,未找到返回 null
  42 + * @return 设备配置
50 43 */
51 44 public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) {
52 45 try {
53   - // 1. 从远程 API 获取全量配置
54   - // TODO @AI:等待修复,不着急;
55   - CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs();
56   - if (result != null && result.isSuccess() && result.getData() != null) {
57   - for (IotModbusDeviceConfigRespDTO config : result.getData()) {
58   - // 顺便更新缓存(其他已连接设备也受益)
59   - configCache.put(config.getDeviceId(), config);
60   - if (config.getDeviceId().equals(deviceId)) {
61   - return config;
62   - }
63   - }
  46 + // 1. 从远程 API 获取配置
  47 + IotModbusDeviceConfigListReqDTO reqDTO = new IotModbusDeviceConfigListReqDTO()
  48 + .setStatus(CommonStatusEnum.ENABLE.getStatus())
  49 + .setMode(IotModbusModeEnum.POLLING.getMode())
  50 + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SLAVE.getType())
  51 + .setDeviceIds(Collections.singleton(deviceId));
  52 + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(reqDTO);
  53 + result.checkError();
  54 + IotModbusDeviceConfigRespDTO modbusConfig = CollUtil.getFirst(result.getData());
  55 + if (modbusConfig == null) {
  56 + log.warn("[loadDeviceConfig][远程获取配置失败,未找到数据, deviceId={}]", deviceId);
  57 + return null;
64 58 }
  59 +
  60 + // 2. 更新缓存并返回
  61 + configCache.put(modbusConfig.getDeviceId(), modbusConfig);
  62 + return modbusConfig;
65 63 } catch (Exception e) {
66 64 log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e);
  65 + return null;
67 66 }
68   -
69   - // 2. 远程未找到,尝试 Mock 数据(仅 mockEnabled=true 时)
70   - // DONE @AI:【from codex】【中】Mock 数据已通过 mockEnabled 配置开关控制,线上环境不会污染真实配置。
71   - // TODO @芋艿:测试完成后移除
72   - if (true) {
73   - for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) {
74   - configCache.put(mockConfig.getDeviceId(), mockConfig);
75   - if (mockConfig.getDeviceId().equals(deviceId)) {
76   - return mockConfig;
77   - }
78   - }
79   - }
80   -
81   - return configCache.get(deviceId);
82 67 }
83 68  
84 69 // ==================== 定时刷新(已连接设备) ====================
... ... @@ -96,26 +81,25 @@ public class IotModbusTcpSlaveConfigCacheService {
96 81 return Collections.emptyList();
97 82 }
98 83 try {
99   - // 1. 从远程获取全量配置
100   - // TODO @AI:传递 ids 批量查询;需要分批啦;
101   - CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs();
102   - List<IotModbusDeviceConfigRespDTO> allConfigs;
103   - if (result != null && result.isSuccess() && result.getData() != null) {
104   - allConfigs = new ArrayList<>(result.getData());
105   - } else {
106   - log.warn("[refreshConnectedDeviceConfigList][获取 Modbus 配置失败: {}]", result);
107   - allConfigs = new ArrayList<>();
108   - }
  84 + // 1. 从远程获取已连接设备的配置
  85 + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
  86 + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
  87 + .setMode(IotModbusModeEnum.POLLING.getMode())
  88 + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SLAVE.getType())
  89 + .setDeviceIds(connectedDeviceIds));
  90 + List<IotModbusDeviceConfigRespDTO> modbusConfigs = result.getCheckedData();
109 91  
110 92 // 2. 追加 Mock 测试数据(仅 mockEnabled=true 时)
111 93 // TODO @芋艿:测试完成后移除
  94 + // TODO @claude-code:【严重】同上,if(true) 导致 mockEnabled 开关失效,Mock 数据永远加载
112 95 if (true) {
113   - allConfigs.addAll(buildMockConfigs());
  96 + modbusConfigs.addAll(buildMockConfigs());
114 97 }
115 98  
116   - // 3. 只保留已连接设备的配置,更新缓存
  99 + // 2. 只保留已连接设备的配置,更新缓存
  100 + // TODO @AI:是不是直接添加到 configCache 缓存(或者覆盖),然后返回 modbusConfigs 就 ok 了?!
117 101 List<IotModbusDeviceConfigRespDTO> connectedConfigs = new ArrayList<>();
118   - for (IotModbusDeviceConfigRespDTO config : allConfigs) {
  102 + for (IotModbusDeviceConfigRespDTO config : modbusConfigs) {
119 103 if (connectedDeviceIds.contains(config.getDeviceId())) {
120 104 configCache.put(config.getDeviceId(), config);
121 105 connectedConfigs.add(config);
... ... @@ -124,15 +108,7 @@ public class IotModbusTcpSlaveConfigCacheService {
124 108 return connectedConfigs;
125 109 } catch (Exception e) {
126 110 log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e);
127   - // 降级:返回缓存中已连接设备的配置
128   - List<IotModbusDeviceConfigRespDTO> fallback = new ArrayList<>();
129   - for (Long deviceId : connectedDeviceIds) {
130   - IotModbusDeviceConfigRespDTO config = configCache.get(deviceId);
131   - if (config != null) {
132   - fallback.add(config);
133   - }
134   - }
135   - return fallback;
  111 + return null;
136 112 }
137 113 }
138 114  
... ... @@ -142,7 +118,12 @@ public class IotModbusTcpSlaveConfigCacheService {
142 118 * 获取设备配置
143 119 */
144 120 public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
145   - return configCache.get(deviceId);
  121 + IotModbusDeviceConfigRespDTO config = configCache.get(deviceId);
  122 + if (config != null) {
  123 + return config;
  124 + }
  125 + // 缓存未命中,从远程 API 获取
  126 + return loadDeviceConfig(deviceId);
146 127 }
147 128  
148 129 /**
... ... @@ -169,7 +150,7 @@ public class IotModbusTcpSlaveConfigCacheService {
169 150 config.setDeviceName("small");
170 151 config.setSlaveId(1);
171 152 config.setMode(1); // 云端轮询
172   - config.setFrameFormat("modbus_tcp");
  153 + config.setFrameFormat(IotModbusFrameFormatEnum.MODBUS_TCP.getFormat());
173 154  
174 155 // 点位列表
175 156 List<IotModbusPointRespDTO> points = new ArrayList<>();
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import io.vertx.core.Future;
4 5 import io.vertx.core.buffer.Buffer;
5 6 import io.vertx.core.net.NetSocket;
6 7 import lombok.Data;
... ... @@ -129,21 +130,25 @@ public class IotModbusTcpSlaveConnectionManager {
129 130  
130 131 /**
131 132 * 发送数据到设备
  133 + *
  134 + * @return 发送结果 Future
132 135 */
133   - public void sendToDevice(Long deviceId, byte[] data) {
  136 + public Future<Void> sendToDevice(Long deviceId, byte[] data) {
134 137 NetSocket socket = deviceSocketMap.get(deviceId);
135 138 if (socket == null) {
136 139 log.warn("[sendToDevice][设备 {} 没有连接]", deviceId);
137   - return;
  140 + return Future.failedFuture("设备 " + deviceId + " 没有连接");
138 141 }
139   - sendToSocket(socket, data);
  142 + return sendToSocket(socket, data);
140 143 }
141 144  
142 145 /**
143 146 * 发送数据到指定 socket
  147 + *
  148 + * @return 发送结果 Future
144 149 */
145   - public void sendToSocket(NetSocket socket, byte[] data) {
146   - socket.write(Buffer.buffer(data));
  150 + public Future<Void> sendToSocket(NetSocket socket, byte[] data) {
  151 + return socket.write(Buffer.buffer(data));
147 152 }
148 153  
149 154 /**
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
5 6 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
6 7 import lombok.AllArgsConstructor;
7 8 import lombok.Data;
... ... @@ -76,8 +77,9 @@ public class IotModbusTcpSlavePendingRequestManager {
76 77 if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) {
77 78 return matchByTransactionId(queue, frame.getTransactionId());
78 79 }
79   - // RTU 模式:FIFO,匹配 slaveId + functionCode
80   - return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode());
  80 + // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount
  81 + int responseRegisterCount = extractRegisterCountFromResponse(frame);
  82 + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount);
81 83 }
82 84  
83 85 /**
... ... @@ -96,13 +98,16 @@ public class IotModbusTcpSlavePendingRequestManager {
96 98 }
97 99  
98 100 /**
99   - * 按 FIFO 匹配
  101 + * 按 FIFO 匹配(slaveId + functionCode + registerCount)
100 102 */
101   - private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode) {
  103 + private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode,
  104 + int responseRegisterCount) {
102 105 Iterator<PendingRequest> it = queue.iterator();
103 106 while (it.hasNext()) {
104 107 PendingRequest req = it.next();
105   - if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) {
  108 + if (req.getSlaveId() == slaveId
  109 + && req.getFunctionCode() == functionCode
  110 + && (responseRegisterCount <= 0 || req.getRegisterCount() == responseRegisterCount)) {
106 111 it.remove();
107 112 return req;
108 113 }
... ... @@ -110,6 +115,29 @@ public class IotModbusTcpSlavePendingRequestManager {
110 115 return null;
111 116 }
112 117  
  118 + // TODO @AI:是不是放到 modbus 工具类里,更合适?
  119 + /**
  120 + * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断)
  121 + *
  122 + * @return registerCount,无法提取时返回 -1(匹配时跳过校验)
  123 + */
  124 + private int extractRegisterCountFromResponse(IotModbusFrame frame) {
  125 + byte[] pdu = frame.getPdu();
  126 + if (pdu == null || pdu.length < 1) {
  127 + return -1;
  128 + }
  129 + int byteCount = pdu[0] & 0xFF;
  130 + int fc = frame.getFunctionCode();
  131 + // FC03/04 寄存器读响应:registerCount = byteCount / 2
  132 + if (fc == IotModbusCommonUtils.FC_READ_HOLDING_REGISTERS
  133 + || fc == IotModbusCommonUtils.FC_READ_INPUT_REGISTERS) {
  134 + return byteCount / 2;
  135 + }
  136 + // FC01/02 线圈/离散输入读响应:registerCount = byteCount * 8(线圈数量)
  137 + // 但因为按 bit 打包有余位,无法精确反推,返回 -1 跳过校验
  138 + return -1;
  139 + }
  140 +
113 141 /**
114 142 * 清理过期请求
115 143 */
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java
... ... @@ -55,6 +55,7 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul
55 55 * 轮询单个点位
56 56 */
57 57 @Override
  58 + @SuppressWarnings("DuplicatedCode")
58 59 protected void pollPoint(Long deviceId, Long pointId) {
59 60 // 1.1 从 configCache 获取最新配置
60 61 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
... ... @@ -78,7 +79,7 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul
78 79 // 2.2 获取 slave ID
79 80 IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat();
80 81 Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId);
81   - int slaveId = connection.getSlaveId();
  82 + Integer slaveId = connection.getSlaveId();
82 83 Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId);
83 84  
84 85 // 3.1 编码读请求
... ... @@ -96,10 +97,13 @@ public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollSchedul
96 97 System.currentTimeMillis() + requestTimeout);
97 98 pendingRequestManager.addRequest(pendingRequest);
98 99 // 3.3 发送读请求
99   - connectionManager.sendToDevice(deviceId, data);
100   - log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
101   - deviceId, point.getIdentifier(), point.getFunctionCode(),
102   - point.getRegisterAddress(), point.getRegisterCount());
  100 + connectionManager.sendToDevice(deviceId, data).onSuccess(v ->
  101 + log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
  102 + deviceId, point.getIdentifier(), point.getFunctionCode(),
  103 + point.getRegisterAddress(), point.getRegisterCount())
  104 + ).onFailure(e ->
  105 + log.warn("[pollPoint][发送失败, 设备={}, 点位={}]", deviceId, point.getIdentifier(), e)
  106 + );
103 107 }
104 108  
105 109 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java 0 → 100644
  1 +/**
  2 + * Modbus TCP Slave(从站)协议:设备主动连接网关,自定义 FC65 认证后由网关云端轮询
  3 + * <p>
  4 + * TCP Server 模式,支持 MODBUS_TCP / MODBUS_RTU 帧格式自动检测
  5 + */
  6 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
... ... @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
6 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
9 10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
10 11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
11 12 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
... ... @@ -64,8 +65,8 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
64 65 }
65 66  
66 67 @Override
67   - public CommonResult<List<IotModbusDeviceConfigRespDTO>> getEnabledModbusDeviceConfigs() {
68   - return doPost("/rpc-api/iot/modbus/enabled-configs", null, new ParameterizedTypeReference<>() { });
  68 + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) {
  69 + return doPost("/rpc-api/iot/modbus/config-list", listReqDTO, new ParameterizedTypeReference<>() { });
69 70 }
70 71  
71 72 @Override
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -167,7 +167,7 @@ yudao:
167 167 # 针对引入的 Modbus TCP Master 组件的配置
168 168 # ====================================
169 169 - id: modbus-tcp-master-1
170   - enabled: false
  170 + enabled: true
171 171 protocol: modbus_tcp_master
172 172 port: 502
173 173 modbus-tcp-master:
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java
... ... @@ -67,9 +67,9 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
67 67  
68 68 // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
69 69  
70   - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
71   - private static final String DEVICE_NAME = "small";
72   - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
  70 + private static final String PRODUCT_KEY = "modbus_tcp_slave_product_demo";
  71 + private static final String DEVICE_NAME = "modbus_tcp_slave_device_demo_tcp";
  72 + private static final String DEVICE_SECRET = "8e4adeb3d25342ab88643421d3fba3f6";
73 73  
74 74 @BeforeAll
75 75 static void setUp() {
... ... @@ -128,7 +128,6 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
128 128  
129 129 // 2. 设置持续监听:每收到一个读请求,自动回复
130 130 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
131   - CompletableFuture<Void> done = new CompletableFuture<>();
132 131 RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> {
133 132 log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]",
134 133 frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId());
... ... @@ -201,6 +200,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
201 200 */
202 201 private IotModbusFrame authenticate(NetSocket socket) throws Exception {
203 202 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  203 + authInfo.setClientId(""); // 特殊:考虑到 modbus 消息长度限制,默认 clientId 不发送
204 204 byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
205 205 return sendAndReceive(socket, authFrame);
206 206 }
... ...