Commit 593455a0851b5fc2d5bce1cc88041bca125ed84c
1 parent
ed78834e
feat:【iot】modbus-tcp 协议接入:35% 初始化:基于 keen-painting-lark.md 规划(修复 point 更新处理不正确的问题)
Showing
2 changed files
with
79 additions
and
29 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpPollScheduler.java
| ... | ... | @@ -4,14 +4,16 @@ import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 6 | 6 | import io.vertx.core.Vertx; |
| 7 | +import lombok.AllArgsConstructor; | |
| 8 | +import lombok.Data; | |
| 7 | 9 | import lombok.RequiredArgsConstructor; |
| 8 | 10 | import lombok.extern.slf4j.Slf4j; |
| 9 | 11 | |
| 10 | -import java.util.ArrayList; | |
| 11 | -import java.util.List; | |
| 12 | -import java.util.Map; | |
| 12 | +import java.util.*; | |
| 13 | 13 | import java.util.concurrent.ConcurrentHashMap; |
| 14 | 14 | |
| 15 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | |
| 16 | + | |
| 15 | 17 | /** |
| 16 | 18 | * IoT Modbus TCP 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 |
| 17 | 19 | * |
| ... | ... | @@ -27,36 +29,86 @@ public class IotModbusTcpPollScheduler { |
| 27 | 29 | private final IotModbusTcpUpstreamHandler upstreamHandler; |
| 28 | 30 | |
| 29 | 31 | /** |
| 30 | - * 设备的定时器列表:deviceId -> timerId 列表 | |
| 32 | + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) | |
| 31 | 33 | */ |
| 32 | - private final Map<Long, List<Long>> deviceTimers = new ConcurrentHashMap<>(); | |
| 34 | + private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>(); | |
| 33 | 35 | |
| 34 | 36 | /** |
| 35 | - * 更新轮询任务 | |
| 37 | + * 点位定时器信息 | |
| 38 | + */ | |
| 39 | + @Data | |
| 40 | + @AllArgsConstructor | |
| 41 | + private static class PointTimerInfo { | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * Vert.x 定时器 ID | |
| 45 | + */ | |
| 46 | + private Long timerId; | |
| 47 | + /** | |
| 48 | + * 轮询间隔(用于判断是否需要更新定时器) | |
| 49 | + */ | |
| 50 | + private Integer pollInterval; | |
| 51 | + | |
| 52 | + } | |
| 53 | + | |
| 54 | + /** | |
| 55 | + * 更新轮询任务(增量更新) | |
| 56 | + * | |
| 57 | + * 1. 【删除】点位:停止对应的轮询定时器 | |
| 58 | + * 2. 【新增】点位:创建对应的轮询定时器 | |
| 59 | + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 | |
| 60 | + * 4. 其他属性变化(包括未变化的):不处理(下次轮询时自动使用新配置) | |
| 36 | 61 | */ |
| 37 | 62 | public void updatePolling(IotModbusDeviceConfigRespDTO config) { |
| 38 | 63 | Long deviceId = config.getDeviceId(); |
| 64 | + List<IotModbusPointRespDTO> newPoints = config.getPoints(); | |
| 65 | + Map<Long, PointTimerInfo> currentTimers = devicePointTimers | |
| 66 | + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); | |
| 67 | + // 1.1 计算新配置(包括新增和修改的点位)中的点位 ID 集合 | |
| 68 | + Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); | |
| 69 | + // 1.2 计算删除的点位 ID 集合 | |
| 70 | + Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet()); | |
| 71 | + removedPointIds.removeAll(newPointIds); | |
| 39 | 72 | |
| 40 | - // 1. 停止旧的轮询任务 | |
| 41 | - stopPolling(deviceId); | |
| 73 | + // 2. 处理删除的点位:停止不再存在的定时器 | |
| 74 | + for (Long pointId : removedPointIds) { | |
| 75 | + PointTimerInfo timerInfo = currentTimers.remove(pointId); | |
| 76 | + if (timerInfo != null) { | |
| 77 | + vertx.cancelTimer(timerInfo.getTimerId()); | |
| 78 | + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); | |
| 79 | + } | |
| 80 | + } | |
| 42 | 81 | |
| 43 | - // 2.1 为每个点位创建新的轮询任务 | |
| 44 | - if (CollUtil.isEmpty(config.getPoints())) { | |
| 82 | + // 3. 处理新增和修改的点位 | |
| 83 | + if (CollUtil.isEmpty(newPoints)) { | |
| 45 | 84 | return; |
| 46 | 85 | } |
| 47 | - List<Long> timerIds = new ArrayList<>(config.getPoints().size()); | |
| 48 | - for (IotModbusPointRespDTO point : config.getPoints()) { | |
| 49 | - Long timerId = createPollTimer(config, point); | |
| 50 | - if (timerId != null) { | |
| 51 | - timerIds.add(timerId); | |
| 86 | + for (IotModbusPointRespDTO point : newPoints) { | |
| 87 | + Long pointId = point.getId(); | |
| 88 | + Integer newPollInterval = point.getPollInterval(); | |
| 89 | + PointTimerInfo existingTimer = currentTimers.get(pointId); | |
| 90 | + // 3.1 新增点位:创建定时器 | |
| 91 | + if (existingTimer == null) { | |
| 92 | + Long timerId = createPollTimer(config, point); | |
| 93 | + if (timerId != null) { | |
| 94 | + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); | |
| 95 | + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", | |
| 96 | + deviceId, pointId, newPollInterval); | |
| 97 | + } | |
| 98 | + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { | |
| 99 | + // 3.2 pollInterval 变化:重建定时器 | |
| 100 | + vertx.cancelTimer(existingTimer.getTimerId()); | |
| 101 | + Long timerId = createPollTimer(config, point); | |
| 102 | + if (timerId != null) { | |
| 103 | + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); | |
| 104 | + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", | |
| 105 | + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); | |
| 106 | + } else { | |
| 107 | + currentTimers.remove(pointId); | |
| 108 | + } | |
| 52 | 109 | } |
| 110 | + // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置) | |
| 53 | 111 | } |
| 54 | - // 2.2 记录定时器 | |
| 55 | - if (CollUtil.isEmpty(timerIds)) { | |
| 56 | - return; | |
| 57 | - } | |
| 58 | - deviceTimers.put(deviceId, timerIds); | |
| 59 | - log.debug("[updatePolling][设备 {} 创建了 {} 个轮询定时器]", deviceId, timerIds.size()); | |
| 60 | 112 | } |
| 61 | 113 | |
| 62 | 114 | /** |
| ... | ... | @@ -104,21 +156,21 @@ public class IotModbusTcpPollScheduler { |
| 104 | 156 | * 停止设备的轮询 |
| 105 | 157 | */ |
| 106 | 158 | public void stopPolling(Long deviceId) { |
| 107 | - List<Long> timerIds = deviceTimers.remove(deviceId); | |
| 108 | - if (CollUtil.isEmpty(timerIds)) { | |
| 159 | + Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId); | |
| 160 | + if (CollUtil.isEmpty(timers)) { | |
| 109 | 161 | return; |
| 110 | 162 | } |
| 111 | - for (Long timerId : timerIds) { | |
| 112 | - vertx.cancelTimer(timerId); | |
| 163 | + for (PointTimerInfo timerInfo : timers.values()) { | |
| 164 | + vertx.cancelTimer(timerInfo.getTimerId()); | |
| 113 | 165 | } |
| 114 | - log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timerIds.size()); | |
| 166 | + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); | |
| 115 | 167 | } |
| 116 | 168 | |
| 117 | 169 | /** |
| 118 | 170 | * 停止所有轮询 |
| 119 | 171 | */ |
| 120 | 172 | public void stopAll() { |
| 121 | - for (Long deviceId : deviceTimers.keySet()) { | |
| 173 | + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { | |
| 122 | 174 | stopPolling(deviceId); |
| 123 | 175 | } |
| 124 | 176 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbustcp/IotModbusTcpUpstreamProtocol.java
| ... | ... | @@ -92,8 +92,6 @@ public class IotModbusTcpUpstreamProtocol { |
| 92 | 92 | // 2.1 确保连接存在 |
| 93 | 93 | connectionManager.ensureConnection(config); |
| 94 | 94 | // 2.2 更新轮询任务 |
| 95 | - // DONE @AI:【重要】当前实现是全量更新轮询任务,未来可优化为增量更新(只更新变化的点位) | |
| 96 | - // TODO @AI:【超级重要,这次必须优化】需要对比 point 的更新:1)如果 points 删除了,需要停止对应的轮询定时器;2)如果 points 新增了,需要新增对应的轮询定时器;3)如果 points 只修改了 pollInterval,需要更新对应的轮询定时器;4)如果 points 其他属性修改了,不需要处理轮询定时器 | |
| 97 | 95 | pollScheduler.updatePolling(config); |
| 98 | 96 | } catch (Exception e) { |
| 99 | 97 | log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); | ... | ... |