Commit 4d578b239c8a1a7f3f2cc7c16f094787bac4169d
1 parent
3ab33527
feat(iot):modbus-tcp-slave 整体代码进一步优化
Showing
6 changed files
with
152 additions
and
145 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.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.collection.CollUtil; | ||
| 3 | import cn.hutool.core.lang.Assert; | 4 | import cn.hutool.core.lang.Assert; |
| 4 | import cn.hutool.extra.spring.SpringUtil; | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; | ||
| 8 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 9 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 9 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| @@ -32,9 +32,9 @@ import lombok.Getter; | @@ -32,9 +32,9 @@ import lombok.Getter; | ||
| 32 | import lombok.extern.slf4j.Slf4j; | 32 | import lombok.extern.slf4j.Slf4j; |
| 33 | 33 | ||
| 34 | import java.util.List; | 34 | import java.util.List; |
| 35 | +import java.util.Set; | ||
| 35 | import java.util.concurrent.TimeUnit; | 36 | import java.util.concurrent.TimeUnit; |
| 36 | 37 | ||
| 37 | -// DONE @AI:不用主动上报! | ||
| 38 | /** | 38 | /** |
| 39 | * IoT 网关 Modbus TCP Slave 协议 | 39 | * IoT 网关 Modbus TCP Slave 协议 |
| 40 | * <p> | 40 | * <p> |
| @@ -152,10 +152,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -152,10 +152,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 152 | } | 152 | } |
| 153 | 153 | ||
| 154 | try { | 154 | try { |
| 155 | - // 1.1 首次加载配置 | ||
| 156 | - // TODO @AI:可能首次不用加载;你在想想; | ||
| 157 | - refreshConfig(); | ||
| 158 | - // 1.2 启动配置刷新定时器 | 155 | + // 1. 启动配置刷新定时器 |
| 159 | int refreshInterval = slaveConfig.getConfigRefreshInterval(); | 156 | int refreshInterval = slaveConfig.getConfigRefreshInterval(); |
| 160 | configRefreshTimerId = vertx.setPeriodic( | 157 | configRefreshTimerId = vertx.setPeriodic( |
| 161 | TimeUnit.SECONDS.toMillis(refreshInterval), | 158 | TimeUnit.SECONDS.toMillis(refreshInterval), |
| @@ -286,6 +283,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -286,6 +283,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 286 | } | 283 | } |
| 287 | pollScheduler.stopPolling(info.getDeviceId()); | 284 | pollScheduler.stopPolling(info.getDeviceId()); |
| 288 | pendingRequestManager.removeDevice(info.getDeviceId()); | 285 | pendingRequestManager.removeDevice(info.getDeviceId()); |
| 286 | + configCacheService.removeConfig(info.getDeviceId()); | ||
| 289 | log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", | 287 | log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", |
| 290 | info.getDeviceId(), socket.remoteAddress()); | 288 | info.getDeviceId(), socket.remoteAddress()); |
| 291 | }); | 289 | }); |
| @@ -297,35 +295,30 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -297,35 +295,30 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 297 | } | 295 | } |
| 298 | 296 | ||
| 299 | /** | 297 | /** |
| 300 | - * 刷新配置 | 298 | + * 刷新已连接设备的配置(定时调用) |
| 299 | + * <p> | ||
| 300 | + * 与 tcpmaster 不同,slave 只刷新已连接设备的配置,不做全量 diff。 | ||
| 301 | + * 设备的新增(认证时)和删除(断连时)分别在 {@link #handleConnection} 中处理。 | ||
| 301 | */ | 302 | */ |
| 302 | private synchronized void refreshConfig() { | 303 | private synchronized void refreshConfig() { |
| 303 | try { | 304 | try { |
| 304 | - // 1. 从 biz 拉取最新配置 | ||
| 305 | - List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig(); | ||
| 306 | - log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); | 305 | + // 1. 只刷新已连接设备的配置 |
| 306 | + Set<Long> connectedDeviceIds = connectionManager.getConnectedDeviceIds(); | ||
| 307 | + if (CollUtil.isEmpty(connectedDeviceIds)) { | ||
| 308 | + return; | ||
| 309 | + } | ||
| 310 | + List<IotModbusDeviceConfigRespDTO> configs = | ||
| 311 | + configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds); | ||
| 312 | + log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size()); | ||
| 307 | 313 | ||
| 308 | - // 2. 更新已连接设备的轮询任务(仅 mode=1) | 314 | + // 2. 更新已连接设备的轮询任务 |
| 309 | for (IotModbusDeviceConfigRespDTO config : configs) { | 315 | for (IotModbusDeviceConfigRespDTO config : configs) { |
| 310 | try { | 316 | try { |
| 311 | - if (config.getMode() != null | ||
| 312 | - && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) { | ||
| 313 | - // 只有已连接的设备才启动轮询 | ||
| 314 | - ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(config.getDeviceId()); | ||
| 315 | - if (connInfo != null) { | ||
| 316 | - pollScheduler.updatePolling(config); | ||
| 317 | - } | ||
| 318 | - } | 317 | + pollScheduler.updatePolling(config); |
| 319 | } catch (Exception e) { | 318 | } catch (Exception e) { |
| 320 | log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); | 319 | log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); |
| 321 | } | 320 | } |
| 322 | } | 321 | } |
| 323 | - | ||
| 324 | - // 3. 清理已删除设备的资源 | ||
| 325 | - configCacheService.cleanupRemovedDevices(configs, deviceId -> { | ||
| 326 | - pollScheduler.stopPolling(deviceId); | ||
| 327 | - pendingRequestManager.removeDevice(deviceId); | ||
| 328 | - }); | ||
| 329 | } catch (Exception e) { | 322 | } catch (Exception e) { |
| 330 | log.error("[refreshConfig][刷新配置失败]", e); | 323 | log.error("[refreshConfig][刷新配置失败]", e); |
| 331 | } | 324 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
| @@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | @@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 16 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | 16 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 17 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 17 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 18 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 18 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 19 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; | ||
| 20 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 19 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 21 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 20 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 22 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 21 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 4 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 5 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| @@ -10,15 +11,18 @@ import lombok.extern.slf4j.Slf4j; | @@ -10,15 +11,18 @@ import lombok.extern.slf4j.Slf4j; | ||
| 10 | import java.math.BigDecimal; | 11 | import java.math.BigDecimal; |
| 11 | import java.util.*; | 12 | import java.util.*; |
| 12 | import java.util.concurrent.ConcurrentHashMap; | 13 | import java.util.concurrent.ConcurrentHashMap; |
| 13 | -import java.util.function.Consumer; | ||
| 14 | 14 | ||
| 15 | -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | ||
| 16 | - | ||
| 17 | -// TODO @AI:和 IotModbusTcpConfigCacheService 基本一致?! | ||
| 18 | /** | 15 | /** |
| 19 | * IoT Modbus TCP Slave 配置缓存服务 | 16 | * IoT Modbus TCP Slave 配置缓存服务 |
| 20 | * <p> | 17 | * <p> |
| 21 | - * 负责:从 biz 拉取 Modbus 设备配置,缓存配置数据,并检测配置变更 | 18 | + * 与 tcpmaster 的 {@code IotModbusTcpConfigCacheService} 不同: |
| 19 | + * - tcpmaster 启动时拉全量配置 → 主动建连,需要全量 diff 检测新增/删除设备 | ||
| 20 | + * - tcpslave 设备主动连接 → 认证时按需加载配置,断连时清理,定时刷新已连接设备的配置 | ||
| 21 | + * <p> | ||
| 22 | + * 配置生命周期: | ||
| 23 | + * 1. 认证时:{@link #loadDeviceConfig(Long)} 按 deviceId 从 API 加载配置到缓存 | ||
| 24 | + * 2. 断连时:{@link #removeConfig(Long)} 从缓存中移除 | ||
| 25 | + * 3. 定时刷新:{@link #refreshConnectedDeviceConfigList(Set)} 批量刷新已连接设备的配置 | ||
| 22 | * | 26 | * |
| 23 | * @author 芋道源码 | 27 | * @author 芋道源码 |
| 24 | */ | 28 | */ |
| @@ -33,43 +37,118 @@ public class IotModbusTcpSlaveConfigCacheService { | @@ -33,43 +37,118 @@ public class IotModbusTcpSlaveConfigCacheService { | ||
| 33 | */ | 37 | */ |
| 34 | private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>(); | 38 | private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>(); |
| 35 | 39 | ||
| 36 | - // TODO @AI:它的 diff 算法,是不是不用和 IotModbusTcpConfigCacheService 完全一致;更多是1)首次连接时,查找;2)断开连接,移除;3)定时轮询更新; | 40 | + // ==================== 按需加载(认证时) ==================== |
| 41 | + | ||
| 37 | /** | 42 | /** |
| 38 | - * 已知的设备 ID 集合 | 43 | + * 加载单个设备的配置(认证成功后调用) |
| 44 | + * <p> | ||
| 45 | + * 从远程 API 获取全量配置,然后按 deviceId 匹配。 | ||
| 46 | + * 如果远程获取失败,尝试从 Mock 数据中匹配。 | ||
| 47 | + * | ||
| 48 | + * @param deviceId 设备 ID | ||
| 49 | + * @return 设备配置,未找到返回 null | ||
| 39 | */ | 50 | */ |
| 40 | - private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet(); | 51 | + public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) { |
| 52 | + try { | ||
| 53 | + // 1. 从远程 API 获取全量配置 | ||
| 54 | + // TODO @AI:等待修复,不着急; | ||
| 55 | + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs(); | ||
| 56 | + if (result != null && result.isSuccess() && result.getData() != null) { | ||
| 57 | + for (IotModbusDeviceConfigRespDTO config : result.getData()) { | ||
| 58 | + // 顺便更新缓存(其他已连接设备也受益) | ||
| 59 | + configCache.put(config.getDeviceId(), config); | ||
| 60 | + if (config.getDeviceId().equals(deviceId)) { | ||
| 61 | + return config; | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + } catch (Exception e) { | ||
| 66 | + log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + // 2. 远程未找到,尝试 Mock 数据 | ||
| 70 | + // TODO @芋艿:测试完成后移除 | ||
| 71 | + for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) { | ||
| 72 | + configCache.put(mockConfig.getDeviceId(), mockConfig); | ||
| 73 | + if (mockConfig.getDeviceId().equals(deviceId)) { | ||
| 74 | + return mockConfig; | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + return configCache.get(deviceId); | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + // ==================== 定时刷新(已连接设备) ==================== | ||
| 41 | 82 | ||
| 42 | /** | 83 | /** |
| 43 | - * 刷新配置 | 84 | + * 刷新已连接设备的配置缓存 |
| 85 | + * <p> | ||
| 86 | + * 定时调用,从远程 API 拉取最新配置,只更新已连接设备的缓存。 | ||
| 44 | * | 87 | * |
| 45 | - * @return 最新的配置列表 | 88 | + * @param connectedDeviceIds 当前已连接的设备 ID 集合 |
| 89 | + * @return 已连接设备的最新配置列表 | ||
| 46 | */ | 90 | */ |
| 47 | - public List<IotModbusDeviceConfigRespDTO> refreshConfig() { | 91 | + public List<IotModbusDeviceConfigRespDTO> refreshConnectedDeviceConfigList(Set<Long> connectedDeviceIds) { |
| 92 | + if (CollUtil.isEmpty(connectedDeviceIds)) { | ||
| 93 | + return Collections.emptyList(); | ||
| 94 | + } | ||
| 48 | try { | 95 | try { |
| 49 | - // 1. 从远程获取配置 | ||
| 50 | - // TODO @AI:需要过滤下,只查找连接的设备列表;并且只有主动轮询的,才会处理;方法名,应该是 List 结尾; | 96 | + // 1. 从远程获取全量配置 |
| 97 | + // TODO @AI:传递 ids 批量查询;需要分批啦; | ||
| 51 | CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs(); | 98 | CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs(); |
| 52 | - if (result == null || !result.isSuccess() || result.getData() == null) { | ||
| 53 | - log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); | ||
| 54 | - return new ArrayList<>(configCache.values()); | 99 | + List<IotModbusDeviceConfigRespDTO> allConfigs; |
| 100 | + if (result != null && result.isSuccess() && result.getData() != null) { | ||
| 101 | + allConfigs = new ArrayList<>(result.getData()); | ||
| 102 | + } else { | ||
| 103 | + log.warn("[refreshConnectedDeviceConfigList][获取 Modbus 配置失败: {}]", result); | ||
| 104 | + allConfigs = new ArrayList<>(); | ||
| 55 | } | 105 | } |
| 56 | - List<IotModbusDeviceConfigRespDTO> configs = new ArrayList<>(result.getData()); | ||
| 57 | 106 | ||
| 58 | - // 2. 追加 Mock 测试数据(一次性测试用途) | 107 | + // 2. 追加 Mock 测试数据 |
| 59 | // TODO @芋艿:测试完成后移除 | 108 | // TODO @芋艿:测试完成后移除 |
| 60 | - configs.addAll(buildMockConfigs()); | ||
| 61 | - | ||
| 62 | - // 3. 更新缓存 | ||
| 63 | - for (IotModbusDeviceConfigRespDTO config : configs) { | ||
| 64 | - configCache.put(config.getDeviceId(), config); | 109 | + allConfigs.addAll(buildMockConfigs()); |
| 110 | + | ||
| 111 | + // 3. 只保留已连接设备的配置,更新缓存 | ||
| 112 | + List<IotModbusDeviceConfigRespDTO> connectedConfigs = new ArrayList<>(); | ||
| 113 | + for (IotModbusDeviceConfigRespDTO config : allConfigs) { | ||
| 114 | + if (connectedDeviceIds.contains(config.getDeviceId())) { | ||
| 115 | + configCache.put(config.getDeviceId(), config); | ||
| 116 | + connectedConfigs.add(config); | ||
| 117 | + } | ||
| 65 | } | 118 | } |
| 66 | - return configs; | 119 | + return connectedConfigs; |
| 67 | } catch (Exception e) { | 120 | } catch (Exception e) { |
| 68 | - log.error("[refreshConfig][刷新配置失败]", e); | ||
| 69 | - return new ArrayList<>(configCache.values()); | 121 | + log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e); |
| 122 | + // 降级:返回缓存中已连接设备的配置 | ||
| 123 | + List<IotModbusDeviceConfigRespDTO> fallback = new ArrayList<>(); | ||
| 124 | + for (Long deviceId : connectedDeviceIds) { | ||
| 125 | + IotModbusDeviceConfigRespDTO config = configCache.get(deviceId); | ||
| 126 | + if (config != null) { | ||
| 127 | + fallback.add(config); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + return fallback; | ||
| 70 | } | 131 | } |
| 71 | } | 132 | } |
| 72 | 133 | ||
| 134 | + // ==================== 缓存操作 ==================== | ||
| 135 | + | ||
| 136 | + /** | ||
| 137 | + * 获取设备配置 | ||
| 138 | + */ | ||
| 139 | + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { | ||
| 140 | + return configCache.get(deviceId); | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + /** | ||
| 144 | + * 移除设备配置缓存(设备断连时调用) | ||
| 145 | + */ | ||
| 146 | + public void removeConfig(Long deviceId) { | ||
| 147 | + configCache.remove(deviceId); | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + // ==================== Mock 数据 ==================== | ||
| 151 | + | ||
| 73 | /** | 152 | /** |
| 74 | * 构建 Mock 测试配置数据(一次性测试用途) | 153 | * 构建 Mock 测试配置数据(一次性测试用途) |
| 75 | * | 154 | * |
| @@ -123,50 +202,4 @@ public class IotModbusTcpSlaveConfigCacheService { | @@ -123,50 +202,4 @@ public class IotModbusTcpSlaveConfigCacheService { | ||
| 123 | return Collections.singletonList(config); | 202 | return Collections.singletonList(config); |
| 124 | } | 203 | } |
| 125 | 204 | ||
| 126 | - /** | ||
| 127 | - * 获取设备配置 | ||
| 128 | - */ | ||
| 129 | - public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { | ||
| 130 | - return configCache.get(deviceId); | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - // TODO @AI:这个逻辑,是不是非必须? | ||
| 134 | - /** | ||
| 135 | - * 通过 clientId + username + password 查找设备配置(认证用) | ||
| 136 | - * 暂通过遍历缓存实现,后续可优化为索引 | ||
| 137 | - */ | ||
| 138 | - public IotModbusDeviceConfigRespDTO findConfigByAuth(String clientId, String username, String password) { | ||
| 139 | - // TODO @芋艿:测试完成后移除 mock 逻辑,改为正式查找 | ||
| 140 | - // Mock:通过 clientId(格式 productKey.deviceName)匹配缓存中的设备 | ||
| 141 | - if (clientId != null && clientId.contains(".")) { | ||
| 142 | - String[] parts = clientId.split("\\.", 2); | ||
| 143 | - String productKey = parts[0]; | ||
| 144 | - String deviceName = parts[1]; | ||
| 145 | - for (IotModbusDeviceConfigRespDTO config : configCache.values()) { | ||
| 146 | - if (productKey.equals(config.getProductKey()) && deviceName.equals(config.getDeviceName())) { | ||
| 147 | - return config; | ||
| 148 | - } | ||
| 149 | - } | ||
| 150 | - } | ||
| 151 | - return null; | ||
| 152 | - } | ||
| 153 | - | ||
| 154 | - /** | ||
| 155 | - * 清理已删除设备的资源 | ||
| 156 | - */ | ||
| 157 | - public void cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs, Consumer<Long> cleanupAction) { | ||
| 158 | - Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); | ||
| 159 | - Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds); | ||
| 160 | - removedDeviceIds.removeAll(currentDeviceIds); | ||
| 161 | - | ||
| 162 | - for (Long deviceId : removedDeviceIds) { | ||
| 163 | - log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); | ||
| 164 | - configCache.remove(deviceId); | ||
| 165 | - cleanupAction.accept(deviceId); | ||
| 166 | - } | ||
| 167 | - | ||
| 168 | - knownDeviceIds.clear(); | ||
| 169 | - knownDeviceIds.addAll(currentDeviceIds); | ||
| 170 | - } | ||
| 171 | - | ||
| 172 | } | 205 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java
| @@ -4,9 +4,11 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | @@ -4,9 +4,11 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 4 | import io.vertx.core.buffer.Buffer; | 4 | import io.vertx.core.buffer.Buffer; |
| 5 | import io.vertx.core.net.NetSocket; | 5 | import io.vertx.core.net.NetSocket; |
| 6 | import lombok.Data; | 6 | import lombok.Data; |
| 7 | +import lombok.experimental.Accessors; | ||
| 7 | import lombok.extern.slf4j.Slf4j; | 8 | import lombok.extern.slf4j.Slf4j; |
| 8 | 9 | ||
| 9 | import java.util.Map; | 10 | import java.util.Map; |
| 11 | +import java.util.Set; | ||
| 10 | import java.util.concurrent.ConcurrentHashMap; | 12 | import java.util.concurrent.ConcurrentHashMap; |
| 11 | 13 | ||
| 12 | /** | 14 | /** |
| @@ -33,6 +35,7 @@ public class IotModbusTcpSlaveConnectionManager { | @@ -33,6 +35,7 @@ public class IotModbusTcpSlaveConnectionManager { | ||
| 33 | * 连接信息 | 35 | * 连接信息 |
| 34 | */ | 36 | */ |
| 35 | @Data | 37 | @Data |
| 38 | + @Accessors(chain = true) | ||
| 36 | public static class ConnectionInfo { | 39 | public static class ConnectionInfo { |
| 37 | 40 | ||
| 38 | /** | 41 | /** |
| @@ -57,12 +60,6 @@ public class IotModbusTcpSlaveConnectionManager { | @@ -57,12 +60,6 @@ public class IotModbusTcpSlaveConnectionManager { | ||
| 57 | */ | 60 | */ |
| 58 | private IotModbusFrameFormatEnum frameFormat; | 61 | private IotModbusFrameFormatEnum frameFormat; |
| 59 | 62 | ||
| 60 | - // TODO @AI:mode 是否非必须?! | ||
| 61 | - /** | ||
| 62 | - * 模式:1-云端轮询 2-主动上报 | ||
| 63 | - */ | ||
| 64 | - private Integer mode; | ||
| 65 | - | ||
| 66 | } | 63 | } |
| 67 | 64 | ||
| 68 | /** | 65 | /** |
| @@ -75,17 +72,6 @@ public class IotModbusTcpSlaveConnectionManager { | @@ -75,17 +72,6 @@ public class IotModbusTcpSlaveConnectionManager { | ||
| 75 | info.getDeviceId(), socket.remoteAddress()); | 72 | info.getDeviceId(), socket.remoteAddress()); |
| 76 | } | 73 | } |
| 77 | 74 | ||
| 78 | - // TODO @芋艿:待定是不是要保留?! | ||
| 79 | - /** | ||
| 80 | - * 设置连接的帧格式(首帧检测后调用) | ||
| 81 | - */ | ||
| 82 | - public void setFrameFormat(NetSocket socket, IotModbusFrameFormatEnum frameFormat) { | ||
| 83 | - ConnectionInfo info = connectionMap.get(socket); | ||
| 84 | - if (info != null) { | ||
| 85 | - info.setFrameFormat(frameFormat); | ||
| 86 | - } | ||
| 87 | - } | ||
| 88 | - | ||
| 89 | /** | 75 | /** |
| 90 | * 获取连接信息 | 76 | * 获取连接信息 |
| 91 | */ | 77 | */ |
| @@ -101,12 +87,11 @@ public class IotModbusTcpSlaveConnectionManager { | @@ -101,12 +87,11 @@ public class IotModbusTcpSlaveConnectionManager { | ||
| 101 | return socket != null ? connectionMap.get(socket) : null; | 87 | return socket != null ? connectionMap.get(socket) : null; |
| 102 | } | 88 | } |
| 103 | 89 | ||
| 104 | - // TODO @AI:不用判断连接是否认证; | ||
| 105 | /** | 90 | /** |
| 106 | - * 判断连接是否已认证 | 91 | + * 获取所有已连接设备的 ID 集合 |
| 107 | */ | 92 | */ |
| 108 | - public boolean isAuthenticated(NetSocket socket) { | ||
| 109 | - return connectionMap.containsKey(socket); | 93 | + public Set<Long> getConnectedDeviceIds() { |
| 94 | + return deviceSocketMap.keySet(); | ||
| 110 | } | 95 | } |
| 111 | 96 | ||
| 112 | /** | 97 | /** |
| @@ -130,8 +115,7 @@ public class IotModbusTcpSlaveConnectionManager { | @@ -130,8 +115,7 @@ public class IotModbusTcpSlaveConnectionManager { | ||
| 130 | log.warn("[sendToDevice][设备 {} 没有连接]", deviceId); | 115 | log.warn("[sendToDevice][设备 {} 没有连接]", deviceId); |
| 131 | return; | 116 | return; |
| 132 | } | 117 | } |
| 133 | - // TODO @AI:直接复用 sendToSocket 方法?! | ||
| 134 | - socket.write(Buffer.buffer(data)); | 118 | + sendToSocket(socket, data); |
| 135 | } | 119 | } |
| 136 | 120 | ||
| 137 | /** | 121 | /** |
| @@ -141,7 +125,6 @@ public class IotModbusTcpSlaveConnectionManager { | @@ -141,7 +125,6 @@ public class IotModbusTcpSlaveConnectionManager { | ||
| 141 | socket.write(Buffer.buffer(data)); | 125 | socket.write(Buffer.buffer(data)); |
| 142 | } | 126 | } |
| 143 | 127 | ||
| 144 | - // TODO @AI:貌似别的都没这个,是不是可以去掉哈?! | ||
| 145 | /** | 128 | /** |
| 146 | * 关闭所有连接 | 129 | * 关闭所有连接 |
| 147 | */ | 130 | */ |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 5 | import lombok.AllArgsConstructor; | 6 | import lombok.AllArgsConstructor; |
| @@ -7,6 +8,7 @@ import lombok.Data; | @@ -7,6 +8,7 @@ import lombok.Data; | ||
| 7 | import lombok.extern.slf4j.Slf4j; | 8 | import lombok.extern.slf4j.Slf4j; |
| 8 | 9 | ||
| 9 | import java.util.Deque; | 10 | import java.util.Deque; |
| 11 | +import java.util.Iterator; | ||
| 10 | import java.util.Map; | 12 | import java.util.Map; |
| 11 | import java.util.concurrent.ConcurrentHashMap; | 13 | import java.util.concurrent.ConcurrentHashMap; |
| 12 | import java.util.concurrent.ConcurrentLinkedDeque; | 14 | import java.util.concurrent.ConcurrentLinkedDeque; |
| @@ -66,26 +68,24 @@ public class IotModbusTcpSlavePendingRequestManager { | @@ -66,26 +68,24 @@ public class IotModbusTcpSlavePendingRequestManager { | ||
| 66 | public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame, | 68 | public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame, |
| 67 | IotModbusFrameFormatEnum frameFormat) { | 69 | IotModbusFrameFormatEnum frameFormat) { |
| 68 | Deque<PendingRequest> queue = pendingRequests.get(deviceId); | 70 | Deque<PendingRequest> queue = pendingRequests.get(deviceId); |
| 69 | - // TODO @AI:CollUtil.isEmpty(queue) | ||
| 70 | - if (queue == null || queue.isEmpty()) { | 71 | + if (CollUtil.isEmpty(queue)) { |
| 71 | return null; | 72 | return null; |
| 72 | } | 73 | } |
| 73 | 74 | ||
| 75 | + // TCP 模式:按 transactionId 精确匹配 | ||
| 74 | if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) { | 76 | if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) { |
| 75 | - // TCP 模式:按 transactionId 精确匹配 | ||
| 76 | return matchByTransactionId(queue, frame.getTransactionId()); | 77 | return matchByTransactionId(queue, frame.getTransactionId()); |
| 77 | - } else { | ||
| 78 | - // RTU 模式:FIFO,匹配 slaveId + functionCode | ||
| 79 | - return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode()); | ||
| 80 | } | 78 | } |
| 79 | + // RTU 模式:FIFO,匹配 slaveId + functionCode | ||
| 80 | + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode()); | ||
| 81 | } | 81 | } |
| 82 | 82 | ||
| 83 | /** | 83 | /** |
| 84 | * 按 transactionId 匹配 | 84 | * 按 transactionId 匹配 |
| 85 | */ | 85 | */ |
| 86 | private PendingRequest matchByTransactionId(Deque<PendingRequest> queue, int transactionId) { | 86 | private PendingRequest matchByTransactionId(Deque<PendingRequest> queue, int transactionId) { |
| 87 | - // TODO @AI:需要兼容 jdk8; | ||
| 88 | - for (var it = queue.iterator(); it.hasNext(); ) { | 87 | + Iterator<PendingRequest> it = queue.iterator(); |
| 88 | + while (it.hasNext()) { | ||
| 89 | PendingRequest req = it.next(); | 89 | PendingRequest req = it.next(); |
| 90 | if (req.getTransactionId() != null && req.getTransactionId() == transactionId) { | 90 | if (req.getTransactionId() != null && req.getTransactionId() == transactionId) { |
| 91 | it.remove(); | 91 | it.remove(); |
| @@ -99,8 +99,8 @@ public class IotModbusTcpSlavePendingRequestManager { | @@ -99,8 +99,8 @@ public class IotModbusTcpSlavePendingRequestManager { | ||
| 99 | * 按 FIFO 匹配 | 99 | * 按 FIFO 匹配 |
| 100 | */ | 100 | */ |
| 101 | private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode) { | 101 | private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode) { |
| 102 | - // TODO @AI:需要兼容 jdk8; | ||
| 103 | - for (var it = queue.iterator(); it.hasNext(); ) { | 102 | + Iterator<PendingRequest> it = queue.iterator(); |
| 103 | + while (it.hasNext()) { | ||
| 104 | PendingRequest req = it.next(); | 104 | PendingRequest req = it.next(); |
| 105 | if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) { | 105 | if (req.getSlaveId() == slaveId && req.getFunctionCode() == functionCode) { |
| 106 | it.remove(); | 106 | it.remove(); |
| @@ -120,13 +120,11 @@ public class IotModbusTcpSlavePendingRequestManager { | @@ -120,13 +120,11 @@ public class IotModbusTcpSlavePendingRequestManager { | ||
| 120 | int removed = 0; | 120 | int removed = 0; |
| 121 | while (!queue.isEmpty()) { | 121 | while (!queue.isEmpty()) { |
| 122 | PendingRequest req = queue.peekFirst(); | 122 | PendingRequest req = queue.peekFirst(); |
| 123 | - // TODO @AI:if return 减少括号层级; | ||
| 124 | - if (req != null && req.getExpireAt() < now) { | ||
| 125 | - queue.pollFirst(); | ||
| 126 | - removed++; | ||
| 127 | - } else { | 123 | + if (req == null || req.getExpireAt() >= now) { |
| 128 | break; // 队列有序,后面的没过期 | 124 | break; // 队列有序,后面的没过期 |
| 129 | } | 125 | } |
| 126 | + queue.pollFirst(); | ||
| 127 | + removed++; | ||
| 130 | } | 128 | } |
| 131 | if (removed > 0) { | 129 | if (removed > 0) { |
| 132 | log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); | 130 | log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java
| @@ -19,12 +19,11 @@ import java.util.concurrent.atomic.AtomicInteger; | @@ -19,12 +19,11 @@ import java.util.concurrent.atomic.AtomicInteger; | ||
| 19 | 19 | ||
| 20 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | 20 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| 21 | 21 | ||
| 22 | -// TODO @AI:和 IotModbusTcpPollScheduler 很像,是不是可以做一些复用? | ||
| 23 | /** | 22 | /** |
| 24 | * IoT Modbus TCP Slave 轮询调度器 | 23 | * IoT Modbus TCP Slave 轮询调度器 |
| 25 | * <p> | 24 | * <p> |
| 26 | - * 管理点位的轮询定时器,为 mode=1(云端轮询)的设备调度读取任务。 | ||
| 27 | - * 与 tcpmaster 不同,这里不直接通过 j2mod 读取,而是: | 25 | + * 管理点位的轮询定时器,为云端轮询模式的设备调度读取任务。 |
| 26 | + * 与 tcpmaster 的 {@code IotModbusTcpPollScheduler} 不同,这里不通过 j2mod 直接读取,而是: | ||
| 28 | * 1. 编码 Modbus 读请求帧 | 27 | * 1. 编码 Modbus 读请求帧 |
| 29 | * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 | 28 | * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 |
| 30 | * 3. 将请求注册到 PendingRequestManager,等待设备响应 | 29 | * 3. 将请求注册到 PendingRequestManager,等待设备响应 |
| @@ -154,13 +153,15 @@ public class IotModbusTcpSlavePollScheduler { | @@ -154,13 +153,15 @@ public class IotModbusTcpSlavePollScheduler { | ||
| 154 | } | 153 | } |
| 155 | 154 | ||
| 156 | // 2.1 确定帧格式和事务 ID | 155 | // 2.1 确定帧格式和事务 ID |
| 157 | - // TODO @AI:不允许为空! | ||
| 158 | IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); | 156 | IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); |
| 159 | if (frameFormat == null) { | 157 | if (frameFormat == null) { |
| 160 | - frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP; | 158 | + log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId); |
| 159 | + return; | ||
| 161 | } | 160 | } |
| 162 | - // TODO @AI:transactionId 需要根据设备来么?然后递增也根据 IotModbusFrameFormatEnum.MODBUS_TCP 提前判断; | ||
| 163 | - int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; | 161 | + // TODO @AI:是不是得按照设备递增? |
| 162 | + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP | ||
| 163 | + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) | ||
| 164 | + : null; | ||
| 164 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; | 165 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; |
| 165 | // 2.2 编码读请求 | 166 | // 2.2 编码读请求 |
| 166 | byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), | 167 | byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), |
| @@ -170,7 +171,7 @@ public class IotModbusTcpSlavePollScheduler { | @@ -170,7 +171,7 @@ public class IotModbusTcpSlavePollScheduler { | ||
| 170 | deviceId, point.getId(), point.getIdentifier(), | 171 | deviceId, point.getId(), point.getIdentifier(), |
| 171 | slaveId, point.getFunctionCode(), | 172 | slaveId, point.getFunctionCode(), |
| 172 | point.getRegisterAddress(), point.getRegisterCount(), | 173 | point.getRegisterAddress(), point.getRegisterCount(), |
| 173 | - frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP ? transactionId : null, | 174 | + transactionId, |
| 174 | System.currentTimeMillis() + requestTimeout); | 175 | System.currentTimeMillis() + requestTimeout); |
| 175 | pendingRequestManager.addRequest(pendingRequest); | 176 | pendingRequestManager.addRequest(pendingRequest); |
| 176 | 177 |