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