Commit 88f090b66f2c9cc2cb151dbfe9f3275b237df85c

Authored by YunaiV
1 parent 4e4c776b

feat(iot):modbus-tcp-slave、modbus-tcp-master 接着大量优化,并且修复 modbus rtu 编解码的问题

Showing 27 changed files with 224 additions and 213 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java
@@ -16,13 +16,9 @@ public class IotDeviceModbusConfigSaveReqVO { @@ -16,13 +16,9 @@ public class IotDeviceModbusConfigSaveReqVO {
16 private Long deviceId; 16 private Long deviceId;
17 17
18 @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") 18 @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100")
19 -// @NotEmpty(message = "Modbus 服务器 IP 地址不能为空")  
20 - // TODO @AI:这个字段,要根据情况校验;  
21 private String ip; 19 private String ip;
22 20
23 @Schema(description = "Modbus 端口", example = "502") 21 @Schema(description = "Modbus 端口", example = "502")
24 -// @NotNull(message = "Modbus 端口不能为空")  
25 - // TODO @AI:这个字段,要根据情况校验;  
26 private Integer port; 22 private Integer port;
27 23
28 @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 24 @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java
@@ -29,7 +29,12 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { @@ -29,7 +29,12 @@ public class IotDeviceModbusPointDO extends TenantBaseDO {
29 */ 29 */
30 @TableId 30 @TableId
31 private Long id; 31 private Long id;
32 - // TODO @AI:增加 productId; 32 + /**
  33 + * 产品编号
  34 + *
  35 + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()}
  36 + */
  37 + private Long productId;
33 /** 38 /**
34 * 设备编号 39 * 设备编号
35 * 40 *
@@ -42,7 +47,6 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { @@ -42,7 +47,6 @@ public class IotDeviceModbusPointDO extends TenantBaseDO {
42 * 关联 {@link IotThingModelDO#getId()} 47 * 关联 {@link IotThingModelDO#getId()}
43 */ 48 */
44 private Long thingModelId; 49 private Long thingModelId;
45 - // TODO @AI:每次物模型的变更时,需要按需刷下 identifier、name 配置;  
46 /** 50 /**
47 * 属性标识符 51 * 属性标识符
48 * 52 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java
@@ -39,4 +39,9 @@ public interface IotDeviceModbusPointMapper extends BaseMapperX<IotDeviceModbusP @@ -39,4 +39,9 @@ public interface IotDeviceModbusPointMapper extends BaseMapperX<IotDeviceModbusP
39 IotDeviceModbusPointDO::getIdentifier, identifier); 39 IotDeviceModbusPointDO::getIdentifier, identifier);
40 } 40 }
41 41
  42 + default void updateByThingModelId(Long thingModelId, IotDeviceModbusPointDO updateObj) {
  43 + update(updateObj, new LambdaQueryWrapperX<IotDeviceModbusPointDO>()
  44 + .eq(IotDeviceModbusPointDO::getThingModelId, thingModelId));
  45 + }
  46 +
42 } 47 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java
@@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable&lt;Integer&gt; {
19 TCP(2, "TCP"), 19 TCP(2, "TCP"),
20 WEBSOCKET(3, "WebSocket"), 20 WEBSOCKET(3, "WebSocket"),
21 21
22 - MQTT(10, "MQTT"), // TODO 待实现; 22 + MQTT(10, "MQTT"), // TODO @puhui999:待实现;
23 23
24 - DATABASE(20, "Database"), // TODO @puhui999:待实现;可以简单点,对应的表名是什么,字段先固定了。 24 + DATABASE(20, "Database"), // TODO @puhui999:待实现;
25 REDIS(21, "Redis"), 25 REDIS(21, "Redis"),
26 26
27 ROCKETMQ(30, "RocketMQ"), 27 ROCKETMQ(30, "RocketMQ"),
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java
@@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber&lt;IotDevic @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber&lt;IotDevic
67 IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId()); 67 IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId());
68 devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now()); 68 devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now());
69 // 1.2 更新设备的连接 server 69 // 1.2 更新设备的连接 server
70 - // TODO 芋艿:HTTP 网关的上行消息,不应该更新 serverId,会覆盖掉 MQTT 等长连接的 serverId,导致下行消息无法发送。  
71 devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId()); 70 devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId());
72 71
73 // 2. 未上线的设备,强制上线 72 // 2. 未上线的设备,强制上线
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java
@@ -9,7 +9,6 @@ import jakarta.annotation.Resource; @@ -9,7 +9,6 @@ import jakarta.annotation.Resource;
9 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
10 import org.springframework.stereotype.Component; 10 import org.springframework.stereotype.Component;
11 11
12 -// TODO @puhui999:后面重构哈  
13 /** 12 /**
14 * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 13 * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景
15 * 14 *
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.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.StrUtil;
3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; 6 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
  10 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
7 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; 12 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper;
  13 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
8 import jakarta.annotation.Resource; 14 import jakarta.annotation.Resource;
9 import org.springframework.stereotype.Service; 15 import org.springframework.stereotype.Service;
10 import org.springframework.validation.annotation.Validated; 16 import org.springframework.validation.annotation.Validated;
@@ -25,11 +31,17 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe @@ -25,11 +31,17 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe
25 31
26 @Resource 32 @Resource
27 private IotDeviceService deviceService; 33 private IotDeviceService deviceService;
  34 + @Resource
  35 + private IotProductService productService;
28 36
29 @Override 37 @Override
30 public void saveDeviceModbusConfig(IotDeviceModbusConfigSaveReqVO saveReqVO) { 38 public void saveDeviceModbusConfig(IotDeviceModbusConfigSaveReqVO saveReqVO) {
31 - // 1. 校验设备存在  
32 - deviceService.validateDeviceExists(saveReqVO.getDeviceId()); 39 + // 1.1 校验设备存在
  40 + IotDeviceDO device = deviceService.validateDeviceExists(saveReqVO.getDeviceId());
  41 + // 1.2 根据产品 protocolType 条件校验
  42 + IotProductDO product = productService.getProduct(device.getProductId());
  43 + Assert.notNull(product, "产品不存在");
  44 + validateModbusConfigByProtocolType(saveReqVO, product.getProtocolType());
33 45
34 // 2. 根据数据库中是否已有配置,决定是新增还是更新 46 // 2. 根据数据库中是否已有配置,决定是新增还是更新
35 IotDeviceModbusConfigDO existConfig = modbusConfigMapper.selectByDeviceId(saveReqVO.getDeviceId()); 47 IotDeviceModbusConfigDO existConfig = modbusConfigMapper.selectByDeviceId(saveReqVO.getDeviceId());
@@ -58,4 +70,21 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe @@ -58,4 +70,21 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe
58 return modbusConfigMapper.selectList(listReqDTO); 70 return modbusConfigMapper.selectList(listReqDTO);
59 } 71 }
60 72
  73 + // TODO @AI:还是希望在 IotDeviceModbusConfigSaveReqVO 里,通过 validator 校验?!或者通过 group 来处理?
  74 + private void validateModbusConfigByProtocolType(IotDeviceModbusConfigSaveReqVO saveReqVO, String protocolType) {
  75 + IotProtocolTypeEnum protocolTypeEnum = IotProtocolTypeEnum.of(protocolType);
  76 + if (protocolTypeEnum == null) {
  77 + return;
  78 + }
  79 + if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_MASTER) {
  80 + Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Master 模式下,IP 地址不能为空");
  81 + Assert.notNull(saveReqVO.getPort(), "Master 模式下,端口不能为空");
  82 + Assert.notNull(saveReqVO.getTimeout(), "Master 模式下,连接超时时间不能为空");
  83 + Assert.notNull(saveReqVO.getRetryInterval(), "Master 模式下,重试间隔不能为空");
  84 + } else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SLAVE) {
  85 + Assert.notNull(saveReqVO.getMode(), "Slave 模式下,工作模式不能为空");
  86 + Assert.notNull(saveReqVO.getFrameFormat(), "Slave 模式下,数据帧格式不能为空");
  87 + }
  88 + }
  89 +
61 } 90 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java
@@ -56,6 +56,15 @@ public interface IotDeviceModbusPointService { @@ -56,6 +56,15 @@ public interface IotDeviceModbusPointService {
56 PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); 56 PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO);
57 57
58 /** 58 /**
  59 + * 物模型变更时,更新关联点位的冗余字段(identifier、name)
  60 + *
  61 + * @param thingModelId 物模型编号
  62 + * @param identifier 物模型标识符
  63 + * @param name 物模型名称
  64 + */
  65 + void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name);
  66 +
  67 + /**
59 * 根据设备编号批量获得启用的点位配置 Map 68 * 根据设备编号批量获得启用的点位配置 Map
60 * 69 *
61 * @param deviceIds 设备编号集合 70 * @param deviceIds 设备编号集合
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java
@@ -75,7 +75,12 @@ public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointServ @@ -75,7 +75,12 @@ public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointServ
75 modbusPointMapper.updateById(updateObj); 75 modbusPointMapper.updateById(updateObj);
76 } 76 }
77 77
78 - // TODO @AI:物模型更新的时候,更新下 identifier、name 信息;例如说 updateDeviceModbusPoint(thingModelId, identifier、name) 方法; 78 + @Override
  79 + public void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name) {
  80 + IotDeviceModbusPointDO updateObj = new IotDeviceModbusPointDO()
  81 + .setIdentifier(identifier).setName(name);
  82 + modbusPointMapper.updateByThingModelId(thingModelId, updateObj);
  83 + }
79 84
80 private IotThingModelDO validateThingModelExists(Long id) { 85 private IotThingModelDO validateThingModelExists(Long id) {
81 IotThingModelDO thingModel = thingModelService.getThingModel(id); 86 IotThingModelDO thingModel = thingModelService.getThingModel(id);
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java
@@ -14,8 +14,6 @@ import java.time.Duration; @@ -14,8 +14,6 @@ import java.time.Duration;
14 14
15 // TODO @芋艿:数据库 15 // TODO @芋艿:数据库
16 // TODO @芋艿:mqtt 16 // TODO @芋艿:mqtt
17 -// TODO @芋艿:tcp  
18 -// TODO @芋艿:websocket  
19 17
20 /** 18 /**
21 * 可缓存的 {@link IotDataRuleAction} 抽象实现 19 * 可缓存的 {@link IotDataRuleAction} 抽象实现
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java
@@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
15 import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; 15 import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper;
16 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; 16 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
17 import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; 17 import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
  18 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
18 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 19 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
19 import jakarta.annotation.Resource; 20 import jakarta.annotation.Resource;
20 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
@@ -51,6 +52,9 @@ public class IotThingModelServiceImpl implements IotThingModelService { @@ -51,6 +52,9 @@ public class IotThingModelServiceImpl implements IotThingModelService {
51 @Lazy // 延迟加载,解决循环依赖 52 @Lazy // 延迟加载,解决循环依赖
52 private IotProductService productService; 53 private IotProductService productService;
53 54
  55 + @Resource
  56 + private IotDeviceModbusPointService deviceModbusPointService;
  57 +
54 @Override 58 @Override
55 @Transactional(rollbackFor = Exception.class) 59 @Transactional(rollbackFor = Exception.class)
56 public Long createThingModel(IotThingModelSaveReqVO createReqVO) { 60 public Long createThingModel(IotThingModelSaveReqVO createReqVO) {
@@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService { @@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService {
84 IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); 88 IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO);
85 thingModelMapper.updateById(thingModel); 89 thingModelMapper.updateById(thingModel);
86 90
87 - // 3. 删除缓存 91 + // 3. 同步更新 Modbus 点位的冗余字段(identifier、name)
  92 + deviceModbusPointService.updateDeviceModbusPointByThingModel(
  93 + updateReqVO.getId(), updateReqVO.getIdentifier(), updateReqVO.getName());
  94 +
  95 + // 4. 删除缓存
88 deleteThingModelListCache(updateReqVO.getProductId()); 96 deleteThingModelListCache(updateReqVO.getProductId());
89 } 97 }
90 98
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java
@@ -500,6 +500,27 @@ public class IotModbusCommonUtils { @@ -500,6 +500,27 @@ public class IotModbusCommonUtils {
500 return values; 500 return values;
501 } 501 }
502 502
  503 + /**
  504 + * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断)
  505 + *
  506 + * @param frame 解码后的 Modbus 响应帧
  507 + * @return registerCount,无法提取时返回 -1(匹配时跳过校验)
  508 + */
  509 + public static int extractRegisterCountFromResponse(IotModbusFrame frame) {
  510 + byte[] pdu = frame.getPdu();
  511 + if (pdu == null || pdu.length < 1) {
  512 + return -1;
  513 + }
  514 + int byteCount = pdu[0] & 0xFF;
  515 + int fc = frame.getFunctionCode();
  516 + // FC03/04 寄存器读响应:registerCount = byteCount / 2
  517 + if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) {
  518 + return byteCount / 2;
  519 + }
  520 + // FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验
  521 + return -1;
  522 + }
  523 +
503 // ==================== 点位查找 ==================== 524 // ==================== 点位查找 ====================
504 525
505 /** 526 /**
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java
@@ -84,7 +84,6 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { @@ -84,7 +84,6 @@ public class IotModbusTcpMasterProtocol implements IotProtocol {
84 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); 84 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
85 IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); 85 IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
86 this.configCacheService = new IotModbusTcpMasterConfigCacheService(deviceApi); 86 this.configCacheService = new IotModbusTcpMasterConfigCacheService(deviceApi);
87 - // DONE @AI:上线/下线消息已移到 ConnectionManager 内部处理,不再走回调  
88 this.connectionManager = new IotModbusTcpMasterConnectionManager(redissonClient, vertx, 87 this.connectionManager = new IotModbusTcpMasterConnectionManager(redissonClient, vertx,
89 messageService, configCacheService, serverId); 88 messageService, configCacheService, serverId);
90 89
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java
@@ -79,8 +79,6 @@ public class IotModbusTcpMasterConfigCacheService { @@ -79,8 +79,6 @@ public class IotModbusTcpMasterConfigCacheService {
79 /** 79 /**
80 * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合 80 * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合
81 * 81 *
82 - * DONE @AI:不再使用 callback 模式,返回已删除的设备 ID 集合,由调用方直接清理  
83 - *  
84 * @param currentConfigs 当前有效的配置列表 82 * @param currentConfigs 当前有效的配置列表
85 * @return 已删除的设备 ID 集合 83 * @return 已删除的设备 ID 集合
86 */ 84 */
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java
@@ -96,18 +96,19 @@ public class IotModbusTcpMasterConnectionManager { @@ -96,18 +96,19 @@ public class IotModbusTcpMasterConnectionManager {
96 connection = connectionPool.get(connectionKey); 96 connection = connectionPool.get(connectionKey);
97 if (connection != null) { 97 if (connection != null) {
98 addDeviceAndOnline(connection, config); 98 addDeviceAndOnline(connection, config);
  99 + lock.unlock();
99 return; 100 return;
100 } 101 }
101 // 3.2 创建新连接 102 // 3.2 创建新连接
102 connection = createConnection(config); 103 connection = createConnection(config);
  104 + connection.setLock(lock);
103 connectionPool.put(connectionKey, connection); 105 connectionPool.put(connectionKey, connection);
104 log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); 106 log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey);
105 // 3.3 注册设备并发送上线消息 107 // 3.3 注册设备并发送上线消息
106 addDeviceAndOnline(connection, config); 108 addDeviceAndOnline(connection, config);
107 } catch (Exception e) { 109 } catch (Exception e) {
108 log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); 110 log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e);
109 - } finally {  
110 - // TODO @AI:如果这里释放,会不会出现,集群模式下,多个节点同时创建连接的情况?需要验证一下 Redisson 的分布式锁特性?! 111 + // 建连失败,释放锁让其他节点可重试
111 lock.unlock(); 112 lock.unlock();
112 } 113 }
113 } 114 }
@@ -231,11 +232,11 @@ public class IotModbusTcpMasterConnectionManager { @@ -231,11 +232,11 @@ public class IotModbusTcpMasterConnectionManager {
231 if (connection.getTcpConnection() != null) { 232 if (connection.getTcpConnection() != null) {
232 connection.getTcpConnection().close(); 233 connection.getTcpConnection().close();
233 } 234 }
234 - // 强制解锁,避免死锁(正常情况下应该不会发生锁未释放的情况)  
235 -// RLock lock = connection.getLock();  
236 -// if (lock != null && lock.isLocked()) {  
237 -// lock.forceUnlock();  
238 -// } 235 + // 释放分布式锁,让其他节点可接管
  236 + RLock lock = connection.getLock();
  237 + if (lock != null && lock.isHeldByCurrentThread()) {
  238 + lock.unlock();
  239 + }
239 log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); 240 log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey);
240 } catch (Exception e) { 241 } catch (Exception e) {
241 log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); 242 log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e);
@@ -266,17 +267,21 @@ public class IotModbusTcpMasterConnectionManager { @@ -266,17 +267,21 @@ public class IotModbusTcpMasterConnectionManager {
266 private TCPMasterConnection tcpConnection; 267 private TCPMasterConnection tcpConnection;
267 private Integer timeout; 268 private Integer timeout;
268 private Integer retryInterval; 269 private Integer retryInterval;
269 -  
270 - private Context context;  
271 -  
272 - // TODO @AI:是不是需要 lock?!避免集群模式下的竞争(肯定不能让别的节点连接上)!!!【另外,RLock 在节点(持有所锁的节点) cransh 的时候,会自动释放】  
273 -// private RLock lock;  
274 -  
275 /** 270 /**
276 * 设备 ID 到 slave ID 的映射 271 * 设备 ID 到 slave ID 的映射
277 */ 272 */
278 private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>(); 273 private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>();
279 274
  275 + /**
  276 + * 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站
  277 + */
  278 + private RLock lock;
  279 +
  280 + /**
  281 + * Vert.x Context,用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行
  282 + */
  283 + private Context context;
  284 +
280 public void addDevice(Long deviceId, Integer slaveId) { 285 public void addDevice(Long deviceId, Integer slaveId) {
281 deviceSlaveMap.put(deviceId, slaveId); 286 deviceSlaveMap.put(deviceId, slaveId);
282 } 287 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java
@@ -41,10 +41,4 @@ public class IotModbusTcpSlaveConfig { @@ -41,10 +41,4 @@ public class IotModbusTcpSlaveConfig {
41 @NotNull(message = "请求清理间隔不能为空") 41 @NotNull(message = "请求清理间隔不能为空")
42 private Integer requestCleanupInterval = 10000; 42 private Integer requestCleanupInterval = 10000;
43 43
44 - // TODO @AI:可以去掉这个开关,因为本身就是模拟的,稍后我自己也会手动或者让你去掉(听我指令!)  
45 - /**  
46 - * 是否启用 Mock 测试数据(仅开发/测试环境使用,线上务必关闭)  
47 - */  
48 - private Boolean mockEnabled = false;  
49 -  
50 } 44 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
@@ -29,21 +29,36 @@ import java.util.function.BiConsumer; @@ -29,21 +29,36 @@ import java.util.function.BiConsumer;
29 @Slf4j 29 @Slf4j
30 public class IotModbusFrameDecoder { 30 public class IotModbusFrameDecoder {
31 31
  32 + private static final Boolean REQUEST_MODE_DEFAULT = false;
  33 +
32 /** 34 /**
33 * 自定义功能码 35 * 自定义功能码
34 */ 36 */
35 private final int customFunctionCode; 37 private final int customFunctionCode;
36 38
37 /** 39 /**
38 - * 创建带自动帧格式检测的 RecordParser 40 + * 创建带自动帧格式检测的 RecordParser(默认响应模式)
39 * 41 *
40 * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) 42 * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式)
41 * @return RecordParser 实例 43 * @return RecordParser 实例
42 */ 44 */
43 public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler) { 45 public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler) {
  46 + return createRecordParser(frameHandler, REQUEST_MODE_DEFAULT);
  47 + }
  48 +
  49 + /**
  50 + * 创建带自动帧格式检测的 RecordParser
  51 + *
  52 + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式)
  53 + * @param requestMode 是否为请求模式(true:接收方收到的是 Modbus 请求帧,FC01-04 按固定 8 字节解析;
  54 + * false:接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析)
  55 + * @return RecordParser 实例
  56 + */
  57 + public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler,
  58 + boolean requestMode) {
44 // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 59 // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测
45 RecordParser parser = RecordParser.newFixed(6); 60 RecordParser parser = RecordParser.newFixed(6);
46 - parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler)); 61 + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, requestMode));
47 return parser; 62 return parser;
48 } 63 }
49 64
@@ -150,6 +165,7 @@ public class IotModbusFrameDecoder { @@ -150,6 +165,7 @@ public class IotModbusFrameDecoder {
150 private final RecordParser parser; 165 private final RecordParser parser;
151 private final int customFunctionCode; 166 private final int customFunctionCode;
152 private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; 167 private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
  168 + private final boolean requestMode;
153 169
154 @Override 170 @Override
155 public void handle(Buffer buffer) { 171 public void handle(Buffer buffer) {
@@ -169,7 +185,7 @@ public class IotModbusFrameDecoder { @@ -169,7 +185,7 @@ public class IotModbusFrameDecoder {
169 } else { 185 } else {
170 // MODBUS_RTU:切换到 RTU 拆包 Handler 186 // MODBUS_RTU:切换到 RTU 拆包 Handler
171 log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); 187 log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]");
172 - RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode); 188 + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode, requestMode);
173 parser.handler(rtuHandler); 189 parser.handler(rtuHandler);
174 // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理 190 // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理
175 rtuHandler.handleFirstBytes(bytes); 191 rtuHandler.handleFirstBytes(bytes);
@@ -248,6 +264,9 @@ public class IotModbusFrameDecoder { @@ -248,6 +264,9 @@ public class IotModbusFrameDecoder {
248 * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) 264 * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
249 * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) 265 * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
250 * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) 266 * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
  267 + * <p>
  268 + * 请求模式(requestMode=true)时,FC01-04 按固定 8 字节解析(与写响应相同路径),
  269 + * 因为读请求格式为 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)]
251 */ 270 */
252 @RequiredArgsConstructor 271 @RequiredArgsConstructor
253 private class RtuFrameHandler implements Handler<Buffer> { 272 private class RtuFrameHandler implements Handler<Buffer> {
@@ -261,6 +280,12 @@ public class IotModbusFrameDecoder { @@ -261,6 +280,12 @@ public class IotModbusFrameDecoder {
261 private final RecordParser parser; 280 private final RecordParser parser;
262 private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; 281 private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
263 private final int customFunctionCode; 282 private final int customFunctionCode;
  283 + /**
  284 + * 请求模式:
  285 + * - true 表示接收方收到的是 Modbus 请求帧(如设备端收到网关下发的读请求),FC01-04 按固定 8 字节帧解析
  286 + * - false 表示接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析
  287 + */
  288 + private final boolean requestMode;
264 289
265 private int state = STATE_HEADER; 290 private int state = STATE_HEADER;
266 private byte slaveId; 291 private byte slaveId;
@@ -289,6 +314,13 @@ public class IotModbusFrameDecoder { @@ -289,6 +314,13 @@ public class IotModbusFrameDecoder {
289 frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC 314 frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC
290 emitFrame(frame); 315 emitFrame(frame);
291 resetToHeader(); 316 resetToHeader();
  317 + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) {
  318 + // 请求模式下的读请求:固定 8 字节 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)]
  319 + // 已有 6 字节,还需 2 字节(CRC)
  320 + state = STATE_WRITE_BODY;
  321 + this.pendingData = Buffer.buffer();
  322 + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节(StartAddr + Quantity)
  323 + parser.fixedSizeMode(2); // 还需 2 字节(CRC)
292 } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { 324 } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
293 // 读响应或自定义 FC:bytes[2] = byteCount 325 // 读响应或自定义 FC:bytes[2] = byteCount
294 this.byteCount = bytes[2]; 326 this.byteCount = bytes[2];
@@ -359,6 +391,11 @@ public class IotModbusFrameDecoder { @@ -359,6 +391,11 @@ public class IotModbusFrameDecoder {
359 // 异常响应 391 // 异常响应
360 state = STATE_EXCEPTION_BODY; 392 state = STATE_EXCEPTION_BODY;
361 parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) 393 parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
  394 + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) {
  395 + // 请求模式下的读请求:固定 8 字节,已读 2 字节(slaveId + FC),还需 6 字节
  396 + state = STATE_WRITE_BODY;
  397 + pendingData = Buffer.buffer();
  398 + parser.fixedSizeMode(6); // StartAddr(2) + Quantity(2) + CRC(2)
362 } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { 399 } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
363 // 读响应或自定义 FC 400 // 读响应或自定义 FC
364 state = STATE_READ_BYTE_COUNT; 401 state = STATE_READ_BYTE_COUNT;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
@@ -46,9 +46,6 @@ public class IotModbusFrameEncoder { @@ -46,9 +46,6 @@ public class IotModbusFrameEncoder {
46 /** 46 /**
47 * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) 47 * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05)
48 * 48 *
49 - * DONE @AI:【from codex】【高】FC05 写线圈时,value 已转换为 Modbus 标准值(非0 → 0xFF00,0 → 0x0000);  
50 - * 新增 encodeWriteMultipleCoilsRequest 方法用于 FC15 编码(按 bit 打包)。  
51 - *  
52 * @param slaveId 从站地址 49 * @param slaveId 从站地址
53 * @param functionCode 功能码 50 * @param functionCode 功能码
54 * @param address 寄存器地址 51 * @param address 寄存器地址
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
@@ -109,7 +109,6 @@ public class IotModbusTcpSlaveDownstreamHandler { @@ -109,7 +109,6 @@ public class IotModbusTcpSlaveDownstreamHandler {
109 // 1.2 确定帧格式和事务 ID 109 // 1.2 确定帧格式和事务 ID
110 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); 110 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
111 Assert.notNull(frameFormat, "连接帧格式不能为空"); 111 Assert.notNull(frameFormat, "连接帧格式不能为空");
112 - // TODO @AI:【from 芋艿】需要和按照 deviceId 进行自增么???  
113 Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP 112 Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP
114 ? (transactionIdCounter.incrementAndGet() & 0xFFFF) 113 ? (transactionIdCounter.incrementAndGet() & 0xFFFF)
115 : null; 114 : null;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java
@@ -6,15 +6,15 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -6,15 +6,15 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 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; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
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; 9 import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
12 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 10 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
13 import lombok.RequiredArgsConstructor; 11 import lombok.RequiredArgsConstructor;
14 import lombok.extern.slf4j.Slf4j; 12 import lombok.extern.slf4j.Slf4j;
15 13
16 -import java.math.BigDecimal;  
17 -import java.util.*; 14 +import java.util.Collections;
  15 +import java.util.List;
  16 +import java.util.Map;
  17 +import java.util.Set;
18 import java.util.concurrent.ConcurrentHashMap; 18 import java.util.concurrent.ConcurrentHashMap;
19 19
20 /** 20 /**
@@ -33,8 +33,6 @@ public class IotModbusTcpSlaveConfigCacheService { @@ -33,8 +33,6 @@ public class IotModbusTcpSlaveConfigCacheService {
33 */ 33 */
34 private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>(); 34 private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
35 35
36 - // ==================== 按需加载(认证时) ====================  
37 -  
38 /** 36 /**
39 * 加载单个设备的配置(认证成功后调用) 37 * 加载单个设备的配置(认证成功后调用)
40 * 38 *
@@ -66,8 +64,6 @@ public class IotModbusTcpSlaveConfigCacheService { @@ -66,8 +64,6 @@ public class IotModbusTcpSlaveConfigCacheService {
66 } 64 }
67 } 65 }
68 66
69 - // ==================== 定时刷新(已连接设备) ====================  
70 -  
71 /** 67 /**
72 * 刷新已连接设备的配置缓存 68 * 刷新已连接设备的配置缓存
73 * <p> 69 * <p>
@@ -89,31 +85,17 @@ public class IotModbusTcpSlaveConfigCacheService { @@ -89,31 +85,17 @@ public class IotModbusTcpSlaveConfigCacheService {
89 .setDeviceIds(connectedDeviceIds)); 85 .setDeviceIds(connectedDeviceIds));
90 List<IotModbusDeviceConfigRespDTO> modbusConfigs = result.getCheckedData(); 86 List<IotModbusDeviceConfigRespDTO> modbusConfigs = result.getCheckedData();
91 87
92 - // 2. 追加 Mock 测试数据(仅 mockEnabled=true 时)  
93 - // TODO @芋艿:测试完成后移除  
94 - // TODO @claude-code:【严重】同上,if(true) 导致 mockEnabled 开关失效,Mock 数据永远加载  
95 - if (true) {  
96 - modbusConfigs.addAll(buildMockConfigs());  
97 - }  
98 -  
99 - // 2. 只保留已连接设备的配置,更新缓存  
100 - // TODO @AI:是不是直接添加到 configCache 缓存(或者覆盖),然后返回 modbusConfigs 就 ok 了?!  
101 - List<IotModbusDeviceConfigRespDTO> connectedConfigs = new ArrayList<>(); 88 + // 2. 更新缓存并返回
102 for (IotModbusDeviceConfigRespDTO config : modbusConfigs) { 89 for (IotModbusDeviceConfigRespDTO config : modbusConfigs) {
103 - if (connectedDeviceIds.contains(config.getDeviceId())) {  
104 - configCache.put(config.getDeviceId(), config);  
105 - connectedConfigs.add(config);  
106 - } 90 + configCache.put(config.getDeviceId(), config);
107 } 91 }
108 - return connectedConfigs; 92 + return modbusConfigs;
109 } catch (Exception e) { 93 } catch (Exception e) {
110 log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e); 94 log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e);
111 return null; 95 return null;
112 } 96 }
113 } 97 }
114 98
115 - // ==================== 缓存操作 ====================  
116 -  
117 /** 99 /**
118 * 获取设备配置 100 * 获取设备配置
119 */ 101 */
@@ -133,59 +115,4 @@ public class IotModbusTcpSlaveConfigCacheService { @@ -133,59 +115,4 @@ public class IotModbusTcpSlaveConfigCacheService {
133 configCache.remove(deviceId); 115 configCache.remove(deviceId);
134 } 116 }
135 117
136 - // ==================== Mock 数据 ====================  
137 -  
138 - /**  
139 - * 构建 Mock 测试配置数据(一次性测试用途)  
140 - *  
141 - * 设备:PRODUCT_KEY=4aymZgOTOOCrDKRT, DEVICE_NAME=small  
142 - * 点位:temperature(FC03, 地址 0)、humidity(FC03, 地址 1)  
143 - *  
144 - * TODO @芋艿:测试完成后移除  
145 - */  
146 - private List<IotModbusDeviceConfigRespDTO> buildMockConfigs() {  
147 - IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO();  
148 - config.setDeviceId(25L);  
149 - config.setProductKey("4aymZgOTOOCrDKRT");  
150 - config.setDeviceName("small");  
151 - config.setSlaveId(1);  
152 - config.setMode(1); // 云端轮询  
153 - config.setFrameFormat(IotModbusFrameFormatEnum.MODBUS_TCP.getFormat());  
154 -  
155 - // 点位列表  
156 - List<IotModbusPointRespDTO> points = new ArrayList<>();  
157 -  
158 - // 点位 1:温度 - 保持寄存器 FC03, 地址 0, 1 个寄存器, INT16  
159 - IotModbusPointRespDTO point1 = new IotModbusPointRespDTO();  
160 - point1.setId(1L);  
161 - point1.setIdentifier("temperature");  
162 - point1.setName("温度");  
163 - point1.setFunctionCode(3); // FC03 读保持寄存器  
164 - point1.setRegisterAddress(0);  
165 - point1.setRegisterCount(1);  
166 - point1.setRawDataType("INT16");  
167 - point1.setByteOrder("BIG_ENDIAN");  
168 - point1.setScale(new BigDecimal("0.1"));  
169 - point1.setPollInterval(5000); // 5 秒轮询一次  
170 - points.add(point1);  
171 -  
172 - // 点位 2:湿度 - 保持寄存器 FC03, 地址 1, 1 个寄存器, UINT16  
173 - IotModbusPointRespDTO point2 = new IotModbusPointRespDTO();  
174 - point2.setId(2L);  
175 - point2.setIdentifier("humidity");  
176 - point2.setName("湿度");  
177 - point2.setFunctionCode(3); // FC03 读保持寄存器  
178 - point2.setRegisterAddress(1);  
179 - point2.setRegisterCount(1);  
180 - point2.setRawDataType("UINT16");  
181 - point2.setByteOrder("BIG_ENDIAN");  
182 - point2.setScale(new BigDecimal("0.1"));  
183 - point2.setPollInterval(5000); // 5 秒轮询一次  
184 - points.add(point2);  
185 -  
186 - config.setPoints(points);  
187 - log.info("[buildMockConfigs][已加载 Mock 配置, deviceId={}, points={}]", config.getDeviceId(), points.size());  
188 - return Collections.singletonList(config);  
189 - }  
190 -  
191 } 118 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java
@@ -78,7 +78,7 @@ public class IotModbusTcpSlavePendingRequestManager { @@ -78,7 +78,7 @@ public class IotModbusTcpSlavePendingRequestManager {
78 return matchByTransactionId(queue, frame.getTransactionId()); 78 return matchByTransactionId(queue, frame.getTransactionId());
79 } 79 }
80 // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount 80 // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount
81 - int responseRegisterCount = extractRegisterCountFromResponse(frame); 81 + int responseRegisterCount = IotModbusCommonUtils.extractRegisterCountFromResponse(frame);
82 return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount); 82 return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount);
83 } 83 }
84 84
@@ -115,29 +115,6 @@ public class IotModbusTcpSlavePendingRequestManager { @@ -115,29 +115,6 @@ public class IotModbusTcpSlavePendingRequestManager {
115 return null; 115 return null;
116 } 116 }
117 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 -  
141 /** 118 /**
142 * 清理过期请求 119 * 清理过期请求
143 */ 120 */
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException; @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  9 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
9 import io.vertx.mqtt.MqttEndpoint; 10 import io.vertx.mqtt.MqttEndpoint;
10 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
11 12
@@ -58,6 +59,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { @@ -58,6 +59,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
58 Assert.notNull(connectionInfo, "无法获取连接信息"); 59 Assert.notNull(connectionInfo, "无法获取连接信息");
59 Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); 60 Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配");
60 Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); 61 Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配");
  62 + // 1.4 校验 topic 是否允许发布
  63 + if (!IotMqttTopicUtils.isTopicPublishAllowed(topic, productKey, deviceName)) {
  64 + log.warn("[handleBusinessRequest][topic 不允许发布,客户端 ID: {},主题: {}]", clientId, topic);
  65 + return;
  66 + }
61 67
62 // 2. 反序列化消息 68 // 2. 反序列化消息
63 message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); 69 message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
@@ -91,7 +91,6 @@ public class IotUdpUpstreamHandler { @@ -91,7 +91,6 @@ public class IotUdpUpstreamHandler {
91 this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); 91 this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
92 } 92 }
93 93
94 - // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现  
95 /** 94 /**
96 * 处理 UDP 数据包 95 * 处理 UDP 数据包
97 * 96 *
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
@@ -101,8 +101,6 @@ public final class IotMqttTopicUtils { @@ -101,8 +101,6 @@ public final class IotMqttTopicUtils {
101 * @param deviceName 设备名称 101 * @param deviceName 设备名称
102 * @return 是否允许发布 102 * @return 是否允许发布
103 */ 103 */
104 - // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用;  
105 - // TODO @AI:那你改下 mqtt,也调用!!!  
106 public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) { 104 public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) {
107 if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { 105 if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) {
108 return false; 106 return false;
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/ModbusTcpSlaveSimulatorTest.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java
@@ -3,21 +3,49 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; @@ -3,21 +3,49 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster;
3 import com.ghgande.j2mod.modbus.procimg.*; 3 import com.ghgande.j2mod.modbus.procimg.*;
4 import com.ghgande.j2mod.modbus.slave.ModbusSlave; 4 import com.ghgande.j2mod.modbus.slave.ModbusSlave;
5 import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; 5 import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.junit.jupiter.api.Disabled;
  8 +import org.junit.jupiter.api.Test;
6 9
7 /** 10 /**
8 - * Modbus TCP 从站模拟器 11 + * Modbus TCP 从站模拟器(手动测试)
9 * 12 *
10 - * 用于测试 Modbus TCP 网关的连接和数据读写功能 13 + * <p>测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Master 网关连接和读写数据
  14 + *
  15 + * <p>使用步骤:
  16 + * <ol>
  17 + * <li>运行 {@link #testStartSlaveSimulator()} 启动模拟从站(默认端口 5020,从站地址 1)</li>
  18 + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-master 协议)</li>
  19 + * <li>确保数据库有对应的 Modbus Master 设备配置(ip=127.0.0.1, port=5020, slaveId=1)</li>
  20 + * <li>网关会自动连接模拟从站并开始轮询读取寄存器数据</li>
  21 + * <li>模拟器每 5 秒自动更新输入寄存器和保持寄存器的值,模拟传感器数据变化</li>
  22 + * </ol>
  23 + *
  24 + * <p>可用寄存器:
  25 + * <ul>
  26 + * <li>线圈 (Coil, 功能码 01/05): 地址 0-9,交替 true/false</li>
  27 + * <li>离散输入 (Discrete Input, 功能码 02): 地址 0-9,每 3 个一个 true</li>
  28 + * <li>保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19,初始值 0,100,200,...</li>
  29 + * <li>输入寄存器 (Input Register, 功能码 04): 地址 0-19,初始值 1,11,21,...</li>
  30 + * </ul>
11 * 31 *
12 * @author 芋道源码 32 * @author 芋道源码
13 */ 33 */
14 -public class ModbusTcpSlaveSimulatorTest { 34 +@Slf4j
  35 +@Disabled
  36 +public class IoTModbusTcpMasterIntegrationTest {
15 37
16 private static final int PORT = 5020; 38 private static final int PORT = 5020;
17 private static final int SLAVE_ID = 1; 39 private static final int SLAVE_ID = 1;
18 40
19 - @SuppressWarnings({"InfiniteLoopStatement", "BusyWait", "CommentedOutCode"})  
20 - public static void main(String[] args) throws Exception { 41 + /**
  42 + * 启动 Modbus TCP 从站模拟器
  43 + *
  44 + * <p>模拟器会持续运行,每 5 秒更新一次寄存器数据,直到手动停止
  45 + */
  46 + @SuppressWarnings({"InfiniteLoopStatement", "BusyWait"})
  47 + @Test
  48 + public void testStartSlaveSimulator() throws Exception {
21 // 1. 创建进程映像(Process Image),存储寄存器数据 49 // 1. 创建进程映像(Process Image),存储寄存器数据
22 SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); 50 SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID);
23 51
@@ -53,24 +81,15 @@ public class ModbusTcpSlaveSimulatorTest { @@ -53,24 +81,15 @@ public class ModbusTcpSlaveSimulatorTest {
53 // 3.2 启动从站 81 // 3.2 启动从站
54 slave.open(); 82 slave.open();
55 83
56 - System.out.println("===================================================");  
57 - System.out.println("Modbus TCP 从站模拟器已启动");  
58 - System.out.println("端口: " + PORT);  
59 - System.out.println("从站地址 (Slave ID): " + SLAVE_ID);  
60 - System.out.println("===================================================");  
61 - System.out.println("可用寄存器:");  
62 - System.out.println(" - 线圈 (Coil, 功能码 01/05): 地址 0-9");  
63 - System.out.println(" - 离散输入 (Discrete Input, 功能码 02): 地址 0-9");  
64 - System.out.println(" - 保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19");  
65 - System.out.println(" - 输入寄存器 (Input Register, 功能码 04): 地址 0-19");  
66 - System.out.println("===================================================");  
67 - System.out.println("按 Ctrl+C 停止模拟器"); 84 + log.info("[testStartSlaveSimulator][Modbus TCP 从站模拟器已启动, 端口: {}, 从站地址: {}]", PORT, SLAVE_ID);
  85 + log.info("[testStartSlaveSimulator][可用寄存器: 线圈(01/05) 0-9, 离散输入(02) 0-9, " +
  86 + "保持寄存器(03/06/16) 0-19, 输入寄存器(04) 0-19]");
68 87
69 // 4. 添加关闭钩子 88 // 4. 添加关闭钩子
70 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 89 Runtime.getRuntime().addShutdownHook(new Thread(() -> {
71 - System.out.println("\n正在关闭模拟器..."); 90 + log.info("[testStartSlaveSimulator][正在关闭模拟器...]");
72 slave.close(); 91 slave.close();
73 - System.out.println("模拟器已关闭"); 92 + log.info("[testStartSlaveSimulator][模拟器已关闭]");
74 })); 93 }));
75 94
76 // 5. 保持运行,定时更新输入寄存器模拟数据变化 95 // 5. 保持运行,定时更新输入寄存器模拟数据变化
@@ -87,9 +106,8 @@ public class ModbusTcpSlaveSimulatorTest { @@ -87,9 +106,8 @@ public class ModbusTcpSlaveSimulatorTest {
87 106
88 // 更新保持寄存器的第一个值 107 // 更新保持寄存器的第一个值
89 spi.getRegister(0).setValue(counter * 100); 108 spi.getRegister(0).setValue(counter * 100);
90 -// System.out.println("[" + java.time.LocalTime.now() + "] 数据已更新, counter=" + counter  
91 -// + ", 保持寄存器[0]=" + (counter * 100)  
92 -// + ", 输入寄存器[0]=" + (1 + counter)); 109 + log.info("[testStartSlaveSimulator][数据已更新, counter={}, 保持寄存器[0]={}, 输入寄存器[0]={}]",
  110 + counter, counter * 100, 1 + counter);
93 } 111 }
94 } 112 }
95 113
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
2 2
  3 +import cn.hutool.core.util.HexUtil;
3 import cn.hutool.json.JSONObject; 4 import cn.hutool.json.JSONObject;
4 import cn.hutool.json.JSONUtil; 5 import cn.hutool.json.JSONUtil;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 8 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
11 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;  
12 import io.vertx.core.Vertx; 13 import io.vertx.core.Vertx;
13 import io.vertx.core.buffer.Buffer; 14 import io.vertx.core.buffer.Buffer;
14 import io.vertx.core.net.NetClient; 15 import io.vertx.core.net.NetClient;
@@ -24,6 +25,8 @@ import org.junit.jupiter.api.Test; @@ -24,6 +25,8 @@ import org.junit.jupiter.api.Test;
24 import java.util.concurrent.CompletableFuture; 25 import java.util.concurrent.CompletableFuture;
25 import java.util.concurrent.TimeUnit; 26 import java.util.concurrent.TimeUnit;
26 27
  28 +import static org.junit.jupiter.api.Assertions.assertEquals;
  29 +
27 /** 30 /**
28 * IoT Modbus TCP Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试) 31 * IoT Modbus TCP Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试)
29 * 32 *
@@ -46,7 +49,7 @@ import java.util.concurrent.TimeUnit; @@ -46,7 +49,7 @@ import java.util.concurrent.TimeUnit;
46 */ 49 */
47 @Slf4j 50 @Slf4j
48 @Disabled 51 @Disabled
49 -public class IotModbusTcpSlaveModbusRtuIntegrationTest { 52 +public class IotModbusTcpSlaveRtuIntegrationTest {
50 53
51 private static final String SERVER_HOST = "127.0.0.1"; 54 private static final String SERVER_HOST = "127.0.0.1";
52 private static final int SERVER_PORT = 503; 55 private static final int SERVER_PORT = 503;
@@ -65,9 +68,9 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { @@ -65,9 +68,9 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
65 68
66 // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== 69 // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
67 70
68 - private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";  
69 - private static final String DEVICE_NAME = "small";  
70 - private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; 71 + private static final String PRODUCT_KEY = "modbus_tcp_slave_product_demo";
  72 + private static final String DEVICE_NAME = "modbus_tcp_slave_device_demo_rtu";
  73 + private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e";
71 74
72 @BeforeAll 75 @BeforeAll
73 static void setUp() { 76 static void setUp() {
@@ -104,6 +107,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { @@ -104,6 +107,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
104 log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", 107 log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
105 response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); 108 response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
106 JSONObject json = JSONUtil.parseObj(response.getCustomData()); 109 JSONObject json = JSONUtil.parseObj(response.getCustomData());
  110 + assertEquals(0, json.getInt("code"));
107 log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); 111 log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
108 } finally { 112 } finally {
109 socket.close(); 113 socket.close();
@@ -122,10 +126,13 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { @@ -122,10 +126,13 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
122 // 1. 先认证 126 // 1. 先认证
123 IotModbusFrame authResponse = authenticate(socket); 127 IotModbusFrame authResponse = authenticate(socket);
124 log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); 128 log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
  129 + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData());
  130 + assertEquals(0, authJson.getInt("code"));
125 131
126 // 2. 设置持续监听:每收到一个读请求,自动回复 132 // 2. 设置持续监听:每收到一个读请求,自动回复
127 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); 133 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
128 CompletableFuture<Void> done = new CompletableFuture<>(); 134 CompletableFuture<Void> done = new CompletableFuture<>();
  135 + // 注意:使用 requestMode=true,因为设备端收到的是网关下发的读请求(非响应)
129 RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { 136 RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> {
130 log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]", 137 log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]",
131 frame.getSlaveId(), frame.getFunctionCode()); 138 frame.getSlaveId(), frame.getFunctionCode());
@@ -144,7 +151,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { @@ -144,7 +151,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
144 frame.getFunctionCode(), registerValues); 151 frame.getFunctionCode(), registerValues);
145 socket.write(Buffer.buffer(responseData)); 152 socket.write(Buffer.buffer(responseData));
146 log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); 153 log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
147 - }); 154 + }, true);
148 socket.handler(parser); 155 socket.handler(parser);
149 156
150 // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 157 // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求
@@ -174,7 +181,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { @@ -174,7 +181,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
174 IotModbusFrame writeRequest = waitForRequest(socket); 181 IotModbusFrame writeRequest = waitForRequest(socket);
175 log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]", 182 log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]",
176 writeRequest.getSlaveId(), writeRequest.getFunctionCode(), 183 writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
177 - bytesToHex(writeRequest.getPdu())); 184 + HexUtil.encodeHexStr(writeRequest.getPdu()));
178 } finally { 185 } finally {
179 socket.close(); 186 socket.close();
180 } 187 }
@@ -198,6 +205,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { @@ -198,6 +205,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
198 */ 205 */
199 private IotModbusFrame authenticate(NetSocket socket) throws Exception { 206 private IotModbusFrame authenticate(NetSocket socket) throws Exception {
200 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); 207 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  208 + authInfo.setClientId("");
201 byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); 209 byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
202 return sendAndReceive(socket, authFrame); 210 return sendAndReceive(socket, authFrame);
203 } 211 }
@@ -291,18 +299,4 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { @@ -291,18 +299,4 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
291 return frame; 299 return frame;
292 } 300 }
293 301
294 - /**  
295 - * 字节数组转十六进制字符串  
296 - */  
297 - private static String bytesToHex(byte[] bytes) {  
298 - if (bytes == null) {  
299 - return "null";  
300 - }  
301 - StringBuilder sb = new StringBuilder();  
302 - for (byte b : bytes) {  
303 - sb.append(String.format("%02X ", b));  
304 - }  
305 - return sb.toString().trim();  
306 - }  
307 -  
308 } 302 }
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
2 2
  3 +import cn.hutool.core.util.HexUtil;
3 import cn.hutool.json.JSONObject; 4 import cn.hutool.json.JSONObject;
4 import cn.hutool.json.JSONUtil; 5 import cn.hutool.json.JSONUtil;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
@@ -25,7 +26,8 @@ import java.nio.ByteOrder; @@ -25,7 +26,8 @@ import java.nio.ByteOrder;
25 import java.util.concurrent.CompletableFuture; 26 import java.util.concurrent.CompletableFuture;
26 import java.util.concurrent.TimeUnit; 27 import java.util.concurrent.TimeUnit;
27 28
28 -// TODO @芋艿:【晚点改】单测需要简化! 29 +import static org.junit.jupiter.api.Assertions.assertEquals;
  30 +
29 /** 31 /**
30 * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) 32 * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试)
31 * 33 *
@@ -48,7 +50,7 @@ import java.util.concurrent.TimeUnit; @@ -48,7 +50,7 @@ import java.util.concurrent.TimeUnit;
48 */ 50 */
49 @Slf4j 51 @Slf4j
50 @Disabled 52 @Disabled
51 -public class IotModbusTcpSlaveModbusTcpIntegrationTest { 53 +public class IotModbusTcpSlaveTcpIntegrationTest {
52 54
53 private static final String SERVER_HOST = "127.0.0.1"; 55 private static final String SERVER_HOST = "127.0.0.1";
54 private static final int SERVER_PORT = 503; 56 private static final int SERVER_PORT = 503;
@@ -106,6 +108,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { @@ -106,6 +108,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
106 log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", 108 log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
107 response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); 109 response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
108 JSONObject json = JSONUtil.parseObj(response.getCustomData()); 110 JSONObject json = JSONUtil.parseObj(response.getCustomData());
  111 + assertEquals(0, json.getInt("code"));
109 log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); 112 log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
110 } finally { 113 } finally {
111 socket.close(); 114 socket.close();
@@ -124,7 +127,8 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { @@ -124,7 +127,8 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
124 // 1. 先认证 127 // 1. 先认证
125 IotModbusFrame authResponse = authenticate(socket); 128 IotModbusFrame authResponse = authenticate(socket);
126 log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); 129 log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
127 - // TODO @AI:这里断言下,认证必须成功! 130 + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData());
  131 + assertEquals(0, authJson.getInt("code"));
128 132
129 // 2. 设置持续监听:每收到一个读请求,自动回复 133 // 2. 设置持续监听:每收到一个读请求,自动回复
130 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); 134 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
@@ -176,7 +180,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { @@ -176,7 +180,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
176 IotModbusFrame writeRequest = waitForRequest(socket); 180 IotModbusFrame writeRequest = waitForRequest(socket);
177 log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]", 181 log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]",
178 writeRequest.getSlaveId(), writeRequest.getFunctionCode(), 182 writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
179 - writeRequest.getTransactionId(), bytesToHex(writeRequest.getPdu())); 183 + writeRequest.getTransactionId(), HexUtil.encodeHexStr(writeRequest.getPdu()));
180 } finally { 184 } finally {
181 socket.close(); 185 socket.close();
182 } 186 }
@@ -295,18 +299,4 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { @@ -295,18 +299,4 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
295 return buf.array(); 299 return buf.array();
296 } 300 }
297 301
298 - /**  
299 - * 字节数组转十六进制字符串  
300 - */  
301 - private static String bytesToHex(byte[] bytes) {  
302 - if (bytes == null) {  
303 - return "null";  
304 - }  
305 - StringBuilder sb = new StringBuilder();  
306 - for (byte b : bytes) {  
307 - sb.append(String.format("%02X ", b));  
308 - }  
309 - return sb.toString().trim();  
310 - }  
311 -  
312 } 302 }