Commit 593455a0851b5fc2d5bce1cc88041bca125ed84c

Authored by YunaiV
1 parent ed78834e

feat:【iot】modbus-tcp 协议接入:35% 初始化:基于 keen-painting-lark.md 规划(修复 point 更新处理不正确的问题)

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);
... ...