2026-06-30 24681c81c09022f584a57006f2534b5f74723414
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
import io.vertx.core.Vertx;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
 
import java.util.*;
 
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 
/**
 * IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
 *
 * @author 芋道源码
 */
@Slf4j
public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler {
 
    private final IotModbusTcpClientConnectionManager connectionManager;
    private final IotModbusTcpClientUpstreamHandler upstreamHandler;
    private final IotModbusTcpClientConfigCacheService configCacheService;
 
    public IotModbusTcpClientPollScheduler(Vertx vertx,
                                           IotModbusTcpClientConnectionManager connectionManager,
                                           IotModbusTcpClientUpstreamHandler upstreamHandler,
                                           IotModbusTcpClientConfigCacheService configCacheService) {
        super(vertx);
        this.connectionManager = connectionManager;
        this.upstreamHandler = upstreamHandler;
        this.configCacheService = configCacheService;
    }
 
    // ========== 轮询执行 ==========
 
    @Override
    protected List<PollTask> buildPollTasks(IotModbusDeviceConfigRespDTO config) {
        return convertList(buildReadSegments(config), segment -> new PollTask(segment.getKey(), segment.getPollInterval()));
    }
 
    /**
     * 轮询读取段
     */
    @Override
    protected void pollTask(Long deviceId, String taskKey) {
        // 1.1 从 configCache 获取最新配置
        IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
        if (config == null || CollUtil.isEmpty(config.getPoints())) {
            log.warn("[pollTask][设备 {} 没有配置]", deviceId);
            return;
        }
        // 1.2 查找读取段。配置变化后,如果当前 taskKey 已不存在,直接跳过等待下一轮 updatePolling 清理 timer
        ReadSegment segment = findReadSegment(config, taskKey);
        if (segment == null) {
            log.debug("[pollTask][设备 {} 读取段 {} 未找到,跳过陈旧轮询任务]", deviceId, taskKey);
            return;
        }
 
        // 2.1 获取连接
        IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
        if (connection == null) {
            log.warn("[pollTask][设备 {} 没有连接]", deviceId);
            return;
        }
        // 2.2 获取 slave ID
        Integer slaveId = connectionManager.getSlaveId(deviceId);
        Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId);
 
        // 3. 执行 Modbus 批量读取
        IotModbusTcpClientUtils.read(connection, slaveId, segment.getFunctionCode(),
                        segment.getStartAddress(), segment.getRegisterCount())
                .onSuccess(rawValues -> handleSegmentReadResult(config, segment, rawValues))
                .onFailure(e -> log.error("[pollTask][读取点位段失败, deviceId={}, segment={}]",
                        deviceId, segment.getKey(), e));
    }
 
    /**
     * 轮询单个点位
     */
    @Override
    protected void pollPoint(Long deviceId, Long pointId) {
        // 1.1 从 configCache 获取最新配置
        IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
        if (config == null || CollUtil.isEmpty(config.getPoints())) {
            log.warn("[pollPoint][设备 {} 没有配置]", deviceId);
            return;
        }
        // 1.2 查找点位
        IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
        if (point == null) {
            log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
            return;
        }
 
        // 2.1 获取连接
        IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
        if (connection == null) {
            log.warn("[pollPoint][设备 {} 没有连接]", deviceId);
            return;
        }
        // 2.2 获取 slave ID
        Integer slaveId = connectionManager.getSlaveId(deviceId);
        Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId);
 
        // 3. 执行 Modbus 读取
        IotModbusTcpClientUtils.read(connection, slaveId, point)
                .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue))
                .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]",
                        deviceId, point.getIdentifier(), e));
    }
 
    private void handleSegmentReadResult(IotModbusDeviceConfigRespDTO config,
                                         ReadSegment segment,
                                         int[] rawValues) {
        for (IotModbusPointRespDTO point : segment.getPoints()) {
            // 批量读取返回的是整个连续地址段,需要按点位地址切片后再复用单点上报逻辑
            int[] pointRawValues = extractPointRawValues(rawValues, segment, point);
            if (pointRawValues == null) {
                log.warn("[handleSegmentReadResult][读取段结果长度不足, deviceId={}, segment={}, identifier={}]",
                        config.getDeviceId(), segment.getKey(), point.getIdentifier());
                continue;
            }
            upstreamHandler.handleReadResult(config, point, pointRawValues);
        }
    }
 
    private ReadSegment findReadSegment(IotModbusDeviceConfigRespDTO config, String taskKey) {
        return CollUtil.findOne(buildReadSegments(config), segment -> segment.getKey().equals(taskKey));
    }
 
    /**
     * 构建批量读取段
     *
     * <p>只合并功能码、轮询间隔相同,且地址连续或重叠的点位;不跨功能码、不跨轮询间隔,避免改变原有轮询语义。
     * 同时按 Modbus 协议限制控制单次读取长度,超过限制时拆成多个读取段。
     */
    static List<ReadSegment> buildReadSegments(IotModbusDeviceConfigRespDTO config) {
        if (config == null) {
            return Collections.emptyList();
        }
 
        // 1. 按功能码和轮询间隔分组:两者任一不同,都不能共用同一次 Modbus 读请求
        List<IotModbusPointRespDTO> validPoints = filterList(config.getPoints(), IotModbusTcpClientPollScheduler::isValidReadPoint);
        if (CollUtil.isEmpty(validPoints)) {
            return Collections.emptyList();
        }
        Map<SegmentGroupKey, List<IotModbusPointRespDTO>> pointsByGroup = convertMultiMap(validPoints,
                point -> new SegmentGroupKey(point.getFunctionCode(), point.getPollInterval()));
 
        // 2. 组内按地址排序后,合并连续或重叠区间,生成实际轮询的读取段
        List<ReadSegment> segments = new ArrayList<>();
        for (Map.Entry<SegmentGroupKey, List<IotModbusPointRespDTO>> entry : pointsByGroup.entrySet()) {
            List<IotModbusPointRespDTO> points = entry.getValue();
            points.sort(Comparator.comparing(IotModbusPointRespDTO::getRegisterAddress)
                    .thenComparing(IotModbusPointRespDTO::getRegisterCount)
                    .thenComparing(IotModbusPointRespDTO::getId));
            buildReadSegments(entry.getKey(), points, segments);
        }
        // 3. 固定排序,保证生成的 taskKey 稳定,便于 updatePolling 做增量更新
        segments.sort(Comparator.comparing(ReadSegment::getFunctionCode)
                .thenComparing(ReadSegment::getPollInterval)
                .thenComparing(ReadSegment::getStartAddress));
        return segments;
    }
 
    private static void buildReadSegments(SegmentGroupKey groupKey,
                                          List<IotModbusPointRespDTO> points,
                                          List<ReadSegment> segments) {
        ReadSegment current = null;
        int maxRegisterCount = getMaxRegisterCount(groupKey.getFunctionCode());
        // points 已按 registerAddress 排序,因此可以线性合并连续/重叠地址段
        for (IotModbusPointRespDTO point : points) {
            int pointStartAddress = point.getRegisterAddress();
            int pointEndAddress = pointStartAddress + point.getRegisterCount();
            // 1. 当前点位无法合并时,新建一个读取段
            if (current == null || !canMerge(current, pointStartAddress, pointEndAddress, maxRegisterCount)) {
                current = new ReadSegment(groupKey.getFunctionCode(), groupKey.getPollInterval(),
                        pointStartAddress, point.getRegisterCount(), new ArrayList<>());
                segments.add(current);
            } else {
                // 2. 当前点位可合并时,扩展读取段覆盖范围
                current.setRegisterCount(Math.max(current.getEndAddress(), pointEndAddress) - current.getStartAddress());
            }
            // 3. 记录读取段包含的点位,读取成功后按点位逐个切片上报
            current.getPoints().add(point);
        }
    }
 
    /**
     * 判断点位是否可以合并到当前读取段
     *
     * <p>仅合并连续或重叠区间,不合并存在地址空洞的区间,避免额外读取无关寄存器。
     */
    private static boolean canMerge(ReadSegment segment, int pointStartAddress, int pointEndAddress, int maxRegisterCount) {
        if (pointStartAddress > segment.getEndAddress()) {
            return false;
        }
        int mergedRegisterCount = Math.max(segment.getEndAddress(), pointEndAddress) - segment.getStartAddress();
        return mergedRegisterCount <= maxRegisterCount;
    }
 
    /**
     * 从批量读取结果中提取单个点位的原始值
     *
     * <p>例如读取段从地址 10 开始,点位地址为 12、数量为 2,则取 rawValues[2..4)。
     */
    static int[] extractPointRawValues(int[] rawValues, ReadSegment segment, IotModbusPointRespDTO point) {
        if (rawValues == null) {
            return null;
        }
        // 1. 计算点位在批量读取结果中的相对偏移
        int offset = point.getRegisterAddress() - segment.getStartAddress();
        int end = offset + point.getRegisterCount();
        // 2. 防御异常响应长度,避免越界影响同一读取段内其它点位
        if (offset < 0 || end > rawValues.length) {
            return null;
        }
        // 3. 返回单个点位需要的原始寄存器值
        return Arrays.copyOfRange(rawValues, offset, end);
    }
 
    private static boolean isValidReadPoint(IotModbusPointRespDTO point) {
        return point != null
                && point.getId() != null
                && point.getFunctionCode() != null
                && point.getRegisterAddress() != null
                && point.getRegisterCount() != null
                && point.getRegisterCount() > 0
                && point.getPollInterval() != null
                && point.getPollInterval() > 0;
    }
 
    @SuppressWarnings("EnhancedSwitchMigration")
    private static int getMaxRegisterCount(Integer functionCode) {
        switch (functionCode) {
            case IotModbusCommonUtils.FC_READ_COILS:
            case IotModbusCommonUtils.FC_READ_DISCRETE_INPUTS:
                return 2000;
            case IotModbusCommonUtils.FC_READ_HOLDING_REGISTERS:
            case IotModbusCommonUtils.FC_READ_INPUT_REGISTERS:
                return 125;
            default:
                return 0;
        }
    }
 
    /**
     * 读取段分组 Key
     */
    @Data
    @AllArgsConstructor
    static class SegmentGroupKey {
 
        private Integer functionCode;
        private Integer pollInterval;
 
    }
 
    /**
     * 一次 Modbus 批量读请求对应的连续地址段
     */
    @Data
    @AllArgsConstructor
    static class ReadSegment {
 
        private Integer functionCode;
        private Integer pollInterval;
        private Integer startAddress;
        private Integer registerCount;
        private List<IotModbusPointRespDTO> points;
 
        String getKey() {
            return functionCode + ":" + pollInterval + ":" + startAddress + ":" + registerCount;
        }
 
        int getEndAddress() {
            return startAddress + registerCount;
        }
 
    }
 
}