diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java
index e62f85f..a41634b 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java
@@ -12,6 +12,7 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
@@ -19,6 +20,8 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
*
* 封装通用的定时器管理、per-device 请求队列限速逻辑。
* 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。
+ * 如需将多个点位合并为一个轮询任务,可覆盖 {@link #buildPollTasks(IotModbusDeviceConfigRespDTO)}
+ * 和 {@link #pollTask(Long, String)}。
*
*
* @author 芋道源码
@@ -38,9 +41,9 @@ public abstract class AbstractIotModbusPollScheduler {
private static final int MAX_QUEUE_SIZE = 1000;
/**
- * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
+ * 设备轮询任务的定时器映射:deviceId -> (taskKey -> PollTimerInfo)
*/
- private final Map> devicePointTimers = new ConcurrentHashMap<>();
+ private final Map> devicePollTimers = new ConcurrentHashMap<>();
/**
* per-device 请求队列:deviceId -> 待执行请求队列
@@ -60,11 +63,29 @@ public abstract class AbstractIotModbusPollScheduler {
}
/**
- * 点位定时器信息
+ * 轮询任务信息
*/
@Data
@AllArgsConstructor
- private static class PointTimerInfo {
+ protected static class PollTask {
+
+ /**
+ * 任务标识
+ */
+ private String key;
+ /**
+ * 轮询间隔(用于判断是否需要更新定时器)
+ */
+ private Integer pollInterval;
+
+ }
+
+ /**
+ * 轮询定时器信息
+ */
+ @Data
+ @AllArgsConstructor
+ private static class PollTimerInfo {
/**
* Vert.x 定时器 ID
@@ -82,75 +103,84 @@ public abstract class AbstractIotModbusPollScheduler {
/**
* 更新轮询任务(增量更新)
*
- * 1. 【删除】点位:停止对应的轮询定时器
- * 2. 【新增】点位:创建对应的轮询定时器
- * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器
- * 【修改】其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point)
+ * 1. 【删除】任务:停止对应的轮询定时器
+ * 2. 【新增】任务:创建对应的轮询定时器
+ * 3. 【修改】任务:pollInterval 变化,重建对应的轮询定时器
+ * 【修改】其他属性变化:不需要重建定时器(pollTask 运行时从 configCache 取最新配置)
*/
public void updatePolling(IotModbusDeviceConfigRespDTO config) {
Long deviceId = config.getDeviceId();
- List newPoints = config.getPoints();
- Map currentTimers = devicePointTimers
+ List newTasks = buildPollTasks(config);
+ Map currentTimers = devicePollTimers
.computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
- // 1.1 计算新配置中的点位 ID 集合
- Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
- // 1.2 计算删除的点位 ID 集合
- Set removedPointIds = new HashSet<>(currentTimers.keySet());
- removedPointIds.removeAll(newPointIds);
-
- // 2. 处理删除的点位:停止不再存在的定时器
- for (Long pointId : removedPointIds) {
- PointTimerInfo timerInfo = currentTimers.remove(pointId);
+ // 1.1 计算新配置中的任务 Key 集合
+ Set newTaskKeys = convertSet(newTasks, PollTask::getKey);
+ // 1.2 计算删除的任务 Key 集合
+ Set removedTaskKeys = new HashSet<>(currentTimers.keySet());
+ removedTaskKeys.removeAll(newTaskKeys);
+
+ // 2. 处理删除的任务:停止不再存在的定时器
+ for (String taskKey : removedTaskKeys) {
+ PollTimerInfo timerInfo = currentTimers.remove(taskKey);
if (timerInfo != null) {
vertx.cancelTimer(timerInfo.getTimerId());
- log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
+ log.debug("[updatePolling][设备 {} 轮询任务 {} 定时器已删除]", deviceId, taskKey);
}
}
- // 3. 处理新增和修改的点位
- if (CollUtil.isEmpty(newPoints)) {
+ // 3. 处理新增和修改的任务
+ if (CollUtil.isEmpty(newTasks)) {
return;
}
- for (IotModbusPointRespDTO point : newPoints) {
- Long pointId = point.getId();
- Integer newPollInterval = point.getPollInterval();
- PointTimerInfo existingTimer = currentTimers.get(pointId);
- // 3.1 新增点位:创建定时器
+ for (PollTask task : newTasks) {
+ String taskKey = task.getKey();
+ Integer newPollInterval = task.getPollInterval();
+ PollTimerInfo existingTimer = currentTimers.get(taskKey);
+ // 3.1 新增任务:创建定时器
if (existingTimer == null) {
- Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
+ Long timerId = createPollTimer(deviceId, taskKey, newPollInterval);
if (timerId != null) {
- currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
- log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
- deviceId, pointId, newPollInterval);
+ currentTimers.put(taskKey, new PollTimerInfo(timerId, newPollInterval));
+ log.debug("[updatePolling][设备 {} 轮询任务 {} 定时器已创建, interval={}ms]",
+ deviceId, taskKey, newPollInterval);
}
} else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
// 3.2 pollInterval 变化:重建定时器
vertx.cancelTimer(existingTimer.getTimerId());
- Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
+ Long timerId = createPollTimer(deviceId, taskKey, newPollInterval);
if (timerId != null) {
- currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
- log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
- deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
+ currentTimers.put(taskKey, new PollTimerInfo(timerId, newPollInterval));
+ log.debug("[updatePolling][设备 {} 轮询任务 {} 定时器已更新, interval={}ms -> {}ms]",
+ deviceId, taskKey, existingTimer.getPollInterval(), newPollInterval);
} else {
- currentTimers.remove(pointId);
+ currentTimers.remove(taskKey);
}
}
- // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置
+ // 3.3 其他属性变化:无需重建定时器,因为 pollTask() 运行时从 configCache 获取最新配置
}
}
/**
+ * 构建轮询任务列表
+ *
+ * 默认每个点位一个轮询任务。TCP Client 等协议可覆盖该方法,将多个点位合并为一个批量读取任务。
+ */
+ protected List buildPollTasks(IotModbusDeviceConfigRespDTO config) {
+ return convertList(config.getPoints(), point -> new PollTask(String.valueOf(point.getId()), point.getPollInterval()));
+ }
+
+ /**
* 创建轮询定时器
*/
- private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
+ private Long createPollTimer(Long deviceId, String taskKey, Integer pollInterval) {
if (pollInterval == null || pollInterval <= 0) {
return null;
}
return vertx.setPeriodic(pollInterval, timerId -> {
try {
- submitPollRequest(deviceId, pointId);
+ submitPollRequest(deviceId, taskKey);
} catch (Exception e) {
- log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
+ log.error("[createPollTimer][轮询任务失败, deviceId={}, taskKey={}]", deviceId, taskKey, e);
}
});
}
@@ -160,7 +190,7 @@ public abstract class AbstractIotModbusPollScheduler {
/**
* 提交轮询请求到设备请求队列(保证同设备请求间隔)
*/
- private void submitPollRequest(Long deviceId, Long pointId) {
+ private void submitPollRequest(Long deviceId, String taskKey) {
// 1. 【重要】将请求添加到设备的请求队列
Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
while (queue.size() >= MAX_QUEUE_SIZE) {
@@ -168,7 +198,7 @@ public abstract class AbstractIotModbusPollScheduler {
queue.poll();
log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE);
}
- queue.offer(() -> pollPoint(deviceId, pointId));
+ queue.offer(() -> pollTask(deviceId, taskKey));
// 2. 处理设备请求队列(如果没有延迟 timer 在等待)
processDeviceQueue(deviceId);
@@ -239,6 +269,15 @@ public abstract class AbstractIotModbusPollScheduler {
// ========== 轮询执行 ==========
/**
+ * 轮询任务
+ *
+ * 默认将任务标识作为点位 ID,执行单点轮询。
+ */
+ protected void pollTask(Long deviceId, String taskKey) {
+ pollPoint(deviceId, Long.valueOf(taskKey));
+ }
+
+ /**
* 轮询单个点位(子类实现具体的读取逻辑)
*
* @param deviceId 设备 ID
@@ -252,25 +291,25 @@ public abstract class AbstractIotModbusPollScheduler {
* 停止设备的轮询
*/
public void stopPolling(Long deviceId) {
- Map timers = devicePointTimers.remove(deviceId);
- if (CollUtil.isEmpty(timers)) {
- return;
- }
- for (PointTimerInfo timerInfo : timers.values()) {
- vertx.cancelTimer(timerInfo.getTimerId());
+ Map timers = devicePollTimers.remove(deviceId);
+ if (CollUtil.isNotEmpty(timers)) {
+ for (PollTimerInfo timerInfo : timers.values()) {
+ vertx.cancelTimer(timerInfo.getTimerId());
+ }
}
// 清理请求队列
deviceRequestQueues.remove(deviceId);
deviceLastRequestTime.remove(deviceId);
deviceDelayTimerActive.remove(deviceId);
- log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
+ log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId,
+ CollUtil.isEmpty(timers) ? 0 : timers.size());
}
/**
* 停止所有轮询
*/
public void stopAll() {
- for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
+ for (Long deviceId : new ArrayList<>(devicePollTimers.keySet())) {
stopPolling(deviceId);
}
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java
index 1324f3a..1d52f8d 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java
@@ -39,11 +39,38 @@ public class IotModbusTcpClientUtils {
public static Future read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
Integer slaveId,
IotModbusPointRespDTO point) {
+ return read(connection, slaveId, point.getFunctionCode(),
+ point.getRegisterAddress(), point.getRegisterCount(), point.getIdentifier());
+ }
+
+ /**
+ * 读取 Modbus 数据
+ *
+ * @param connection Modbus 连接
+ * @param slaveId 从站地址
+ * @param functionCode 功能码
+ * @param registerAddress 寄存器起始地址
+ * @param registerCount 寄存器数量
+ * @return 原始值(int 数组)
+ */
+ public static Future read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
+ Integer slaveId,
+ Integer functionCode,
+ Integer registerAddress,
+ Integer registerCount) {
+ return read(connection, slaveId, functionCode, registerAddress, registerCount, null);
+ }
+
+ private static Future read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
+ Integer slaveId,
+ Integer functionCode,
+ Integer registerAddress,
+ Integer registerCount,
+ String identifier) {
return connection.executeBlocking(tcpConnection -> {
try {
// 1. 创建请求
- ModbusRequest request = createReadRequest(point.getFunctionCode(),
- point.getRegisterAddress(), point.getRegisterCount());
+ ModbusRequest request = createReadRequest(functionCode, registerAddress, registerCount);
request.setUnitID(slaveId);
// 2. 执行事务(请求)
@@ -53,10 +80,10 @@ public class IotModbusTcpClientUtils {
// 3. 解析响应
ModbusResponse response = transaction.getResponse();
- return extractValues(response, point.getFunctionCode());
+ return extractValues(response, functionCode);
} catch (Exception e) {
- throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]",
- slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
+ throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, functionCode=%d, address=%d, count=%d]",
+ slaveId, identifier, functionCode, registerAddress, registerCount), e);
}
});
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java
index 946937d..fba2f75 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java
@@ -9,8 +9,16 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbu
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 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
*
@@ -35,6 +43,47 @@ public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollSchedu
// ========== 轮询执行 ==========
+ @Override
+ protected List 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));
+ }
+
/**
* 轮询单个点位
*/
@@ -70,4 +119,174 @@ public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollSchedu
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));
+ }
+
+ /**
+ * 构建批量读取段
+ *
+ * 只合并功能码、轮询间隔相同,且地址连续或重叠的点位;不跨功能码、不跨轮询间隔,避免改变原有轮询语义。
+ * 同时按 Modbus 协议限制控制单次读取长度,超过限制时拆成多个读取段。
+ */
+ static List buildReadSegments(IotModbusDeviceConfigRespDTO config) {
+ if (config == null) {
+ return Collections.emptyList();
+ }
+
+ // 1. 按功能码和轮询间隔分组:两者任一不同,都不能共用同一次 Modbus 读请求
+ List validPoints = filterList(config.getPoints(), IotModbusTcpClientPollScheduler::isValidReadPoint);
+ if (CollUtil.isEmpty(validPoints)) {
+ return Collections.emptyList();
+ }
+ Map> pointsByGroup = convertMultiMap(validPoints,
+ point -> new SegmentGroupKey(point.getFunctionCode(), point.getPollInterval()));
+
+ // 2. 组内按地址排序后,合并连续或重叠区间,生成实际轮询的读取段
+ List segments = new ArrayList<>();
+ for (Map.Entry> entry : pointsByGroup.entrySet()) {
+ List 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 points,
+ List 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);
+ }
+ }
+
+ /**
+ * 判断点位是否可以合并到当前读取段
+ *
+ * 仅合并连续或重叠区间,不合并存在地址空洞的区间,避免额外读取无关寄存器。
+ */
+ 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;
+ }
+
+ /**
+ * 从批量读取结果中提取单个点位的原始值
+ *
+ *
例如读取段从地址 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 points;
+
+ String getKey() {
+ return functionCode + ":" + pollInterval + ":" + startAddress + ":" + registerCount;
+ }
+
+ int getEndAddress() {
+ return startAddress + registerCount;
+ }
+
+ }
+
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollSchedulerTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollSchedulerTest.java
new file mode 100644
index 0000000..214db67
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollSchedulerTest.java
@@ -0,0 +1,147 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
+
+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.tcpclient.manager.IotModbusTcpClientPollScheduler.ReadSegment;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.FC_READ_COILS;
+import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.FC_READ_HOLDING_REGISTERS;
+import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.FC_READ_INPUT_REGISTERS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link IotModbusTcpClientPollScheduler} 的单元测试
+ *
+ * @author 芋道源码
+ */
+public class IotModbusTcpClientPollSchedulerTest {
+
+ @Test
+ public void testBuildReadSegments_mergeContinuousPoints() {
+ // 准备参数
+ IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 1, 1000);
+ IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_HOLDING_REGISTERS, 1, 2, 1000);
+ IotModbusPointRespDTO point03 = randomPoint(3L, FC_READ_HOLDING_REGISTERS, 4, 1, 1000);
+ IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02, point03);
+
+ // 调用
+ List segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
+
+ // 断言
+ assertEquals(2, segments.size());
+ assertEquals(0, segments.get(0).getStartAddress());
+ assertEquals(3, segments.get(0).getRegisterCount());
+ assertIterableEquals(Arrays.asList(point01, point02), segments.get(0).getPoints());
+ assertEquals(4, segments.get(1).getStartAddress());
+ assertEquals(1, segments.get(1).getRegisterCount());
+ assertIterableEquals(Collections.singletonList(point03), segments.get(1).getPoints());
+ }
+
+ @Test
+ public void testBuildReadSegments_mergeOverlappingPoints() {
+ // 准备参数
+ IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 2, 1000);
+ IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_HOLDING_REGISTERS, 1, 1, 1000);
+ IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02);
+
+ // 调用
+ List segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
+
+ // 断言
+ assertEquals(1, segments.size());
+ assertEquals(0, segments.get(0).getStartAddress());
+ assertEquals(2, segments.get(0).getRegisterCount());
+ assertIterableEquals(Arrays.asList(point01, point02), segments.get(0).getPoints());
+ }
+
+ @Test
+ public void testBuildReadSegments_notMergeDifferentFunctionCodeOrPollInterval() {
+ // 准备参数
+ IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 1, 1000);
+ IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_INPUT_REGISTERS, 1, 1, 1000);
+ IotModbusPointRespDTO point03 = randomPoint(3L, FC_READ_HOLDING_REGISTERS, 1, 1, 2000);
+ IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02, point03);
+
+ // 调用
+ List segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
+
+ // 断言
+ assertEquals(3, segments.size());
+ assertEquals(FC_READ_HOLDING_REGISTERS, segments.get(0).getFunctionCode());
+ assertEquals(1000, segments.get(0).getPollInterval());
+ assertEquals(FC_READ_HOLDING_REGISTERS, segments.get(1).getFunctionCode());
+ assertEquals(2000, segments.get(1).getPollInterval());
+ assertEquals(FC_READ_INPUT_REGISTERS, segments.get(2).getFunctionCode());
+ assertEquals(1000, segments.get(2).getPollInterval());
+ }
+
+ @Test
+ public void testBuildReadSegments_splitWhenExceedsMaxRegisterCount() {
+ // 准备参数
+ IotModbusPointRespDTO point01 = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 0, 100, 1000);
+ IotModbusPointRespDTO point02 = randomPoint(2L, FC_READ_HOLDING_REGISTERS, 100, 30, 1000);
+ IotModbusDeviceConfigRespDTO config = randomConfig(point01, point02);
+
+ // 调用
+ List segments = IotModbusTcpClientPollScheduler.buildReadSegments(config);
+
+ // 断言
+ assertEquals(2, segments.size());
+ assertEquals(0, segments.get(0).getStartAddress());
+ assertEquals(100, segments.get(0).getRegisterCount());
+ assertEquals(100, segments.get(1).getStartAddress());
+ assertEquals(30, segments.get(1).getRegisterCount());
+ }
+
+ @Test
+ public void testExtractPointRawValues() {
+ // 准备参数
+ IotModbusPointRespDTO point = randomPoint(1L, FC_READ_HOLDING_REGISTERS, 12, 2, 1000);
+ ReadSegment segment = new ReadSegment(FC_READ_HOLDING_REGISTERS, 1000, 10, 4, Collections.singletonList(point));
+ int[] rawValues = new int[]{100, 200, 300, 400};
+
+ // 调用
+ int[] pointRawValues = IotModbusTcpClientPollScheduler.extractPointRawValues(rawValues, segment, point);
+
+ // 断言
+ assertArrayEquals(new int[]{300, 400}, pointRawValues);
+ }
+
+ @Test
+ public void testExtractPointRawValuesForCoils() {
+ // 准备参数
+ IotModbusPointRespDTO point = randomPoint(1L, FC_READ_COILS, 3, 2, 1000);
+ ReadSegment segment = new ReadSegment(FC_READ_COILS, 1000, 0, 5, Collections.singletonList(point));
+ int[] rawValues = new int[]{1, 0, 1, 1, 0, 0, 0, 0}; // 线圈响应可能按字节补齐
+
+ // 调用
+ int[] pointRawValues = IotModbusTcpClientPollScheduler.extractPointRawValues(rawValues, segment, point);
+
+ // 断言
+ assertArrayEquals(new int[]{1, 0}, pointRawValues);
+ }
+
+ private static IotModbusDeviceConfigRespDTO randomConfig(IotModbusPointRespDTO... points) {
+ IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO();
+ config.setDeviceId(1L);
+ config.setPoints(Arrays.asList(points));
+ return config;
+ }
+
+ private static IotModbusPointRespDTO randomPoint(Long id, Integer functionCode, Integer registerAddress,
+ Integer registerCount, Integer pollInterval) {
+ IotModbusPointRespDTO point = new IotModbusPointRespDTO();
+ point.setId(id);
+ point.setFunctionCode(functionCode);
+ point.setRegisterAddress(registerAddress);
+ point.setRegisterCount(registerCount);
+ point.setPollInterval(pollInterval);
+ return point;
+ }
+
+}