Commit 88f090b66f2c9cc2cb151dbfe9f3275b237df85c
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<Integer> { | @@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable<Integer> { | ||
| 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<IotDevic | @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber<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 | } |