修正 modbus 超时问题
This commit is contained in:
@ -1,66 +1,62 @@
|
|||||||
package com.xzzn.common.core.modbus;
|
package com.xzzn.common.core.modbus;
|
||||||
|
|
||||||
|
import com.serotonin.modbus4j.ModbusFactory;
|
||||||
import com.serotonin.modbus4j.ModbusMaster;
|
import com.serotonin.modbus4j.ModbusMaster;
|
||||||
|
import com.serotonin.modbus4j.ip.IpParameters;
|
||||||
import com.xzzn.common.core.modbus.domain.DeviceConfig;
|
import com.xzzn.common.core.modbus.domain.DeviceConfig;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
import javax.annotation.PreDestroy;
|
|
||||||
|
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modbus连接管理器
|
||||||
|
* 不使用连接池,每次请求创建新连接,使用完立即销毁
|
||||||
|
* 避免复用已被服务端断开的连接导致Connection reset
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class Modbus4jConnectionManager {
|
public class Modbus4jConnectionManager {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(Modbus4jConnectionManager.class);
|
private static final Logger logger = LoggerFactory.getLogger(Modbus4jConnectionManager.class);
|
||||||
|
|
||||||
private final Map<String, GenericObjectPool<ModbusMaster>> connectionPools = new ConcurrentHashMap<>();
|
private final ModbusFactory modbusFactory = new ModbusFactory();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新连接
|
||||||
|
*/
|
||||||
public ModbusMaster borrowMaster(DeviceConfig config) throws Exception {
|
public ModbusMaster borrowMaster(DeviceConfig config) throws Exception {
|
||||||
String poolKey = getPoolKey(config);
|
IpParameters params = new IpParameters();
|
||||||
GenericObjectPool<ModbusMaster> pool = connectionPools.computeIfAbsent(poolKey, key -> {
|
params.setHost(config.getHost());
|
||||||
PooledModbusMasterFactory factory = new PooledModbusMasterFactory(config.getHost(), config.getPort());
|
params.setPort(config.getPort());
|
||||||
GenericObjectPoolConfig<ModbusMaster> poolConfig = new GenericObjectPoolConfig<>();
|
params.setEncapsulated(false);
|
||||||
poolConfig.setMaxTotal(20); // 池中最大连接数
|
|
||||||
poolConfig.setMinIdle(4); // 最小空闲连接数
|
ModbusMaster master = modbusFactory.createTcpMaster(params, true);
|
||||||
poolConfig.setTestOnBorrow(true); // 借用时测试连接有效性
|
master.init();
|
||||||
poolConfig.setTestOnReturn(true); // 归还时测试连接有效性
|
logger.debug("创建新Modbus连接: {}:{}", config.getHost(), config.getPort());
|
||||||
poolConfig.setTestWhileIdle(true); // 空闲时测试连接有效性
|
return master;
|
||||||
poolConfig.setMaxWaitMillis(3000); // 获取连接的最大等待时间3秒
|
|
||||||
poolConfig.setTimeBetweenEvictionRunsMillis(30000); // 30秒检查一次空闲连接
|
|
||||||
poolConfig.setMinEvictableIdleTimeMillis(60000); // 空闲超过60秒的连接可以被驱逐
|
|
||||||
|
|
||||||
return new GenericObjectPool<>(factory, poolConfig);
|
|
||||||
});
|
|
||||||
return pool.borrowObject();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭连接
|
||||||
|
*/
|
||||||
public void returnMaster(DeviceConfig config, ModbusMaster master) {
|
public void returnMaster(DeviceConfig config, ModbusMaster master) {
|
||||||
if (master == null) {
|
destroyMaster(master, config);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
String poolKey = getPoolKey(config);
|
/**
|
||||||
GenericObjectPool<ModbusMaster> pool = connectionPools.get(poolKey);
|
* 废弃连接(与returnMaster相同)
|
||||||
if (pool != null) {
|
*/
|
||||||
pool.returnObject(master);
|
public void invalidateMaster(DeviceConfig config, ModbusMaster master) {
|
||||||
|
destroyMaster(master, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void destroyMaster(ModbusMaster master, DeviceConfig config) {
|
||||||
|
if (master != null) {
|
||||||
|
try {
|
||||||
|
master.destroy();
|
||||||
|
logger.debug("已关闭Modbus连接: {}:{}", config.getHost(), config.getPort());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("关闭Modbus连接异常: {}:{}", config.getHost(), config.getPort(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPoolKey(DeviceConfig config) {
|
|
||||||
return config.getHost() + ":" + config.getPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreDestroy
|
|
||||||
public void shutdown() {
|
|
||||||
connectionPools.values().forEach(GenericObjectPool::close);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import static com.xzzn.common.enums.RegisterType.COIL;
|
import static com.xzzn.common.enums.RegisterType.COIL;
|
||||||
@ -43,8 +44,12 @@ import static com.xzzn.common.enums.RegisterType.DISCRETE_INPUT;
|
|||||||
public class ModbusProcessor {
|
public class ModbusProcessor {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ModbusProcessor.class);
|
private static final Logger logger = LoggerFactory.getLogger(ModbusProcessor.class);
|
||||||
private int readTimeout = 5000;
|
@Value("${modbus.read-timeout:8000}")
|
||||||
private int writeTimeout = 3000;
|
private int readTimeout;
|
||||||
|
@Value("${modbus.write-timeout:5000}")
|
||||||
|
private int writeTimeout;
|
||||||
|
@Value("${modbus.read-retries:1}")
|
||||||
|
private int readRetries;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisCache redisCache;
|
private RedisCache redisCache;
|
||||||
@ -54,20 +59,25 @@ public class ModbusProcessor {
|
|||||||
public boolean writeDataToDevice(DeviceConfig config) {
|
public boolean writeDataToDevice(DeviceConfig config) {
|
||||||
logger.info("writeDataToDevice: {}", JSON.toJSONString(config));
|
logger.info("writeDataToDevice: {}", JSON.toJSONString(config));
|
||||||
ModbusMaster master = null;
|
ModbusMaster master = null;
|
||||||
boolean result;
|
boolean result = false;
|
||||||
|
boolean hasError = false;
|
||||||
try {
|
try {
|
||||||
master = connectionManager.borrowMaster(config);
|
master = connectionManager.borrowMaster(config);
|
||||||
// 设置了Modbus通信的超时时间为3000毫秒(3秒)。当主设备与从设备通信时,若在3秒内未收到响应,则认为通信超时并抛出异常。这有助于避免长时间等待无响应的设备。
|
|
||||||
master.setTimeout(writeTimeout);
|
master.setTimeout(writeTimeout);
|
||||||
result = writeTagValue(master, config, config.getWriteTags());
|
result = writeTagValue(master, config, config.getWriteTags());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to borrow connection or write to devices '{}'", config.getDeviceName(), e);
|
logger.error("Failed to borrow connection or write to devices '{}'", config.getDeviceName(), e);
|
||||||
result = false;
|
hasError = true;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
// 关键:无论成功与否,都必须将连接归还到池中
|
|
||||||
if (master != null) {
|
if (master != null) {
|
||||||
connectionManager.returnMaster(config, master);
|
if (hasError) {
|
||||||
|
// 发生异常时废弃连接,下次重新创建
|
||||||
|
connectionManager.invalidateMaster(config, master);
|
||||||
|
} else {
|
||||||
|
// 正常时归还连接
|
||||||
|
connectionManager.returnMaster(config, master);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -224,16 +234,14 @@ public class ModbusProcessor {
|
|||||||
ModbusMaster master = connectionManager.borrowMaster(config);
|
ModbusMaster master = connectionManager.borrowMaster(config);
|
||||||
// 设置了Modbus通信的超时时间为5000毫秒(5秒)。当主设备与从设备通信时,若在5秒内未收到响应,则认为通信超时并抛出异常。这有助于避免长时间等待无响应的设备。
|
// 设置了Modbus通信的超时时间为5000毫秒(5秒)。当主设备与从设备通信时,若在5秒内未收到响应,则认为通信超时并抛出异常。这有助于避免长时间等待无响应的设备。
|
||||||
master.setTimeout(readTimeout);
|
master.setTimeout(readTimeout);
|
||||||
|
master.setRetries(readRetries);
|
||||||
return master;
|
return master;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> readDataFromDevice(DeviceConfig config, ModbusMaster master) {
|
public Map<String, Object> readDataFromDevice(DeviceConfig config, ModbusMaster master) {
|
||||||
Map<String, Object> deviceData = new HashMap<>();
|
Map<String, Object> deviceData = new HashMap<>();
|
||||||
// ModbusMaster master = null; // 将master的声明提前
|
boolean hasError = false;
|
||||||
try {
|
try {
|
||||||
// master = connectionManager.borrowMaster(config);
|
|
||||||
// 设置了Modbus通信的超时时间为3000毫秒(3秒)。当主设备与从设备通信时,若在3秒内未收到响应,则认为通信超时并抛出异常。这有助于避免长时间等待无响应的设备。
|
|
||||||
// master.setTimeout(5000);
|
|
||||||
BatchResults<String> results = readTagValues(master, config.getSlaveId(), config.getTags());
|
BatchResults<String> results = readTagValues(master, config.getSlaveId(), config.getTags());
|
||||||
for (TagConfig tag : config.getTags()) {
|
for (TagConfig tag : config.getTags()) {
|
||||||
if (Objects.equals(tag.getDataType(), "FOUR_BYTE_FLOAT_DBCA")){
|
if (Objects.equals(tag.getDataType(), "FOUR_BYTE_FLOAT_DBCA")){
|
||||||
@ -243,28 +251,22 @@ public class ModbusProcessor {
|
|||||||
}else {
|
}else {
|
||||||
deviceData.put(tag.getKey(), results.getValue(tag.getKey()));
|
deviceData.put(tag.getKey(), results.getValue(tag.getKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// Object value = readTagValue(master, config.getSlaveId(), tag);
|
|
||||||
// if (value != null) {
|
|
||||||
// deviceData.put(tag.getKey(), value);
|
|
||||||
// }
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// logger.error("Failed to read tag '{}' from devices '{}'", tag.getKey(), config.getDeviceName(), e);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed read from devices '{}'", config.getDeviceName(), e);
|
logger.error("Failed read from devices '{}'", config.getDeviceName(), e);
|
||||||
|
hasError = true;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
// 关键:无论成功与否,都必须将连接归还到池中
|
|
||||||
if (master != null) {
|
if (master != null) {
|
||||||
connectionManager.returnMaster(config, master);
|
if (hasError) {
|
||||||
|
// 发生异常时废弃连接,下次重新创建
|
||||||
|
connectionManager.invalidateMaster(config, master);
|
||||||
|
} else {
|
||||||
|
// 正常时归还连接
|
||||||
|
connectionManager.returnMaster(config, master);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// String deviceNumber = config.getDeviceNumber();
|
|
||||||
// redisCache.setCacheObject(deviceNumber, deviceData);
|
|
||||||
return deviceData;
|
return deviceData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,77 +342,101 @@ public class ModbusProcessor {
|
|||||||
|
|
||||||
private BatchResults<String> readTagValues(ModbusMaster master, int slaveId, List<TagConfig> tags) throws Exception {
|
private BatchResults<String> readTagValues(ModbusMaster master, int slaveId, List<TagConfig> tags) throws Exception {
|
||||||
try {
|
try {
|
||||||
BatchRead<String> batch = new BatchRead<>();
|
BatchResults<String> results = sendBatchRead(master, slaveId, tags);
|
||||||
tags.forEach(tag -> {
|
logBatchResults(tags, results);
|
||||||
Map<Integer, RegisterType> type = ModBusType.REGISTER_TYPE;
|
|
||||||
Map<String, Integer> DATA_LENGTH = ModBusType.LENGTH;
|
|
||||||
int firstDigit = Integer.parseInt(tag.getAddress().substring(0, 1));
|
|
||||||
int address = 0;
|
|
||||||
int addressLength = tag.getAddress().length();
|
|
||||||
int exp = (int) Math.pow(10, addressLength-1);
|
|
||||||
if (firstDigit != 0){
|
|
||||||
int digit = Integer.parseInt(tag.getAddress());
|
|
||||||
address = digit % (exp);
|
|
||||||
}else {
|
|
||||||
address = Integer.parseInt(tag.getAddress());
|
|
||||||
}
|
|
||||||
RegisterType registerType = type.get(firstDigit);
|
|
||||||
int dataLength = DATA_LENGTH.get(tag.getDataType());
|
|
||||||
switch (registerType) {
|
|
||||||
case COIL: {
|
|
||||||
BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, address);
|
|
||||||
batch.addLocator(tag.getKey(), loc);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case DISCRETE_INPUT: {
|
|
||||||
BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, address);
|
|
||||||
batch.addLocator(tag.getKey(), loc);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HOLDING_REGISTER: {
|
|
||||||
// logger.info("HOLDING_REGISTER: {}",tag.getAddress());
|
|
||||||
if (dataLength == 28){
|
|
||||||
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, address, 4);
|
|
||||||
batch.addLocator(tag.getKey(), locator);
|
|
||||||
}else {
|
|
||||||
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, address, dataLength);
|
|
||||||
batch.addLocator(tag.getKey(), loc);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case INPUT_REGISTER: {
|
|
||||||
// logger.info("INPUT_REGISTER: {}",tag.getAddress());
|
|
||||||
BaseLocator<Number> loc = BaseLocator.inputRegister(slaveId, address, dataLength);
|
|
||||||
batch.addLocator(tag.getKey(), loc);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BatchResults<String> results = master.send(batch);
|
|
||||||
|
|
||||||
List<String> logInfoList = new ArrayList<>();
|
|
||||||
for (TagConfig tag : tags){
|
|
||||||
StringBuilder logInfo = new StringBuilder();
|
|
||||||
logInfo.append(tag.getAddress());
|
|
||||||
if (tag.getBit()!=null){
|
|
||||||
// logger.info("批处理读取寄存器成功: {}",tag.getAddress() +"(" + tag.getBit() + "):" + results.getValue(tag.getKey()));
|
|
||||||
logInfo.append("(" + tag.getBit() + "):");
|
|
||||||
}else {
|
|
||||||
// logger.info("批处理读取寄存器成功: {}",tag.getAddress() + ":" + results.getValue(tag.getKey()));
|
|
||||||
logInfo.append(":");
|
|
||||||
}
|
|
||||||
logInfo.append(results.getValue(tag.getKey()));
|
|
||||||
logInfoList.add(logInfo.toString());
|
|
||||||
}
|
|
||||||
logger.info("批处理读取寄存器成功: {}", JSON.toJSONString(logInfoList));
|
|
||||||
return results;
|
return results;
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
|
if (isTimeoutException(e)) {
|
||||||
|
logger.warn("批量读取超时,尝试降级为单点读取,slaveId: {}", slaveId);
|
||||||
|
BatchResults<String> fallback = new BatchResults<>();
|
||||||
|
for (TagConfig tag : tags) {
|
||||||
|
Object value = readTagValue(master, slaveId, tag);
|
||||||
|
fallback.addResult(tag.getKey(), value);
|
||||||
|
}
|
||||||
|
logBatchResults(tags, fallback);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
logger.error("Failed to read master '{}'", slaveId, e);
|
logger.error("Failed to read master '{}'", slaveId, e);
|
||||||
throw new Exception(e);
|
throw new Exception(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BatchResults<String> sendBatchRead(ModbusMaster master, int slaveId, List<TagConfig> tags) throws Exception {
|
||||||
|
BatchRead<String> batch = new BatchRead<>();
|
||||||
|
tags.forEach(tag -> {
|
||||||
|
Map<Integer, RegisterType> type = ModBusType.REGISTER_TYPE;
|
||||||
|
Map<String, Integer> DATA_LENGTH = ModBusType.LENGTH;
|
||||||
|
int firstDigit = Integer.parseInt(tag.getAddress().substring(0, 1));
|
||||||
|
int address = 0;
|
||||||
|
int addressLength = tag.getAddress().length();
|
||||||
|
int exp = (int) Math.pow(10, addressLength - 1);
|
||||||
|
if (firstDigit != 0) {
|
||||||
|
int digit = Integer.parseInt(tag.getAddress());
|
||||||
|
address = digit % (exp);
|
||||||
|
} else {
|
||||||
|
address = Integer.parseInt(tag.getAddress());
|
||||||
|
}
|
||||||
|
RegisterType registerType = type.get(firstDigit);
|
||||||
|
int dataLength = DATA_LENGTH.get(tag.getDataType());
|
||||||
|
switch (registerType) {
|
||||||
|
case COIL: {
|
||||||
|
BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, address);
|
||||||
|
batch.addLocator(tag.getKey(), loc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DISCRETE_INPUT: {
|
||||||
|
BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, address);
|
||||||
|
batch.addLocator(tag.getKey(), loc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case HOLDING_REGISTER: {
|
||||||
|
if (dataLength == 28) {
|
||||||
|
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, address, 4);
|
||||||
|
batch.addLocator(tag.getKey(), locator);
|
||||||
|
} else {
|
||||||
|
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, address, dataLength);
|
||||||
|
batch.addLocator(tag.getKey(), loc);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case INPUT_REGISTER: {
|
||||||
|
BaseLocator<Number> loc = BaseLocator.inputRegister(slaveId, address, dataLength);
|
||||||
|
batch.addLocator(tag.getKey(), loc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return master.send(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logBatchResults(List<TagConfig> tags, BatchResults<String> results) {
|
||||||
|
List<String> logInfoList = new ArrayList<>();
|
||||||
|
for (TagConfig tag : tags) {
|
||||||
|
StringBuilder logInfo = new StringBuilder();
|
||||||
|
logInfo.append(tag.getAddress());
|
||||||
|
if (tag.getBit() != null) {
|
||||||
|
logInfo.append("(").append(tag.getBit()).append("):");
|
||||||
|
} else {
|
||||||
|
logInfo.append(":");
|
||||||
|
}
|
||||||
|
logInfo.append(results.getValue(tag.getKey()));
|
||||||
|
logInfoList.add(logInfo.toString());
|
||||||
|
}
|
||||||
|
logger.info("批处理读取寄存器成功: {}", JSON.toJSONString(logInfoList));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTimeoutException(Throwable e) {
|
||||||
|
Throwable current = e;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof com.serotonin.modbus4j.sero.messaging.TimeoutException) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = current.getCause();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static float convertValueToFloat(Object value) {
|
public static float convertValueToFloat(Object value) {
|
||||||
if (!(value instanceof Number)) {
|
if (!(value instanceof Number)) {
|
||||||
throw new IllegalArgumentException("Input must be a Number");
|
throw new IllegalArgumentException("Input must be a Number");
|
||||||
|
|||||||
@ -31,9 +31,11 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||||
@ -56,10 +58,14 @@ public class ModbusPoller {
|
|||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private final Map<String, Integer> deviceFailureCounts = new ConcurrentHashMap<>();
|
private final Map<String, Integer> deviceFailureCounts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Resource(name = "modbusExecutor")
|
||||||
|
private ExecutorService modbusExecutor;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ModbusProcessor modbusProcessor;
|
private ModbusProcessor modbusProcessor;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IEmsAlarmRecordsService iEmsAlarmRecordsService;
|
private IEmsAlarmRecordsService iEmsAlarmRecordsService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISysJobService iSysJobService;
|
private ISysJobService iSysJobService;
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -99,8 +105,8 @@ public class ModbusPoller {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 host:port 分组
|
// 按主机IP分组(同一网关的不同端口也归为一组,避免并发访问导致Connection Reset)
|
||||||
Map<String, List<DeviceConfig>> groupedConfigs = new HashMap<>();
|
Map<String, List<DeviceConfig>> groupedByHost = new HashMap<>();
|
||||||
for (Path filePath : jsonFiles) {
|
for (Path filePath : jsonFiles) {
|
||||||
DeviceConfig config = null;
|
DeviceConfig config = null;
|
||||||
try {
|
try {
|
||||||
@ -110,51 +116,44 @@ public class ModbusPoller {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (config.isEnabled()) {
|
if (config.isEnabled()) {
|
||||||
String key = config.getHost() + ":" + config.getPort();
|
// 只按主机IP分组,确保同一网关的所有端口串行访问
|
||||||
groupedConfigs.computeIfAbsent(key, k -> new ArrayList<>()).add(config);
|
String hostKey = config.getHost();
|
||||||
|
groupedByHost.computeIfAbsent(hostKey, k -> new ArrayList<>()).add(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int interval = getScheduledTaskInterval();
|
// 使用单线程 executor 串行执行所有主机的 Modbus 操作
|
||||||
// 为每个 host:port 启动一个任务
|
// 将所有主机的设备按顺序串行处理,避免任何并发访问
|
||||||
for (Map.Entry<String, List<DeviceConfig>> entry : groupedConfigs.entrySet()) {
|
modbusExecutor.submit(() -> {
|
||||||
String groupKey = entry.getKey();
|
for (Map.Entry<String, List<DeviceConfig>> entry : groupedByHost.entrySet()) {
|
||||||
List<DeviceConfig> configs = entry.getValue();
|
String hostKey = entry.getKey();
|
||||||
try {
|
List<DeviceConfig> configs = entry.getValue();
|
||||||
CompletableFuture.runAsync(() -> {
|
for (DeviceConfig config : configs) {
|
||||||
for (DeviceConfig config : configs) {
|
try {
|
||||||
try {
|
scheduledStart(config);
|
||||||
scheduledStart(config);
|
// 每次读取后等待200ms,给Modbus网关足够的处理时间
|
||||||
} catch (Exception e) {
|
Thread.sleep(200);
|
||||||
log.error("采集设备数据异常: {}", config.getDeviceName(), e);
|
} catch (InterruptedException ie) {
|
||||||
}
|
Thread.currentThread().interrupt();
|
||||||
}
|
log.warn("Modbus轮询被中断");
|
||||||
})
|
return;
|
||||||
.exceptionally(e -> {
|
} catch (Exception e) {
|
||||||
log.error("采集设备数据{}轮询异常", groupKey, e);
|
log.error("采集设备数据异常: {}", config.getDeviceName(), e);
|
||||||
return null;
|
}
|
||||||
})
|
}
|
||||||
.thenRun(() -> log.info("采集设备数据{}轮询任务执行完成", groupKey));
|
log.info("采集设备数据{}轮询任务执行完成", hostKey);
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("采集设备数据{}任务失败", groupKey, e);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scheduledStart(DeviceConfig config) {
|
public void scheduledStart(DeviceConfig config) {
|
||||||
if (config.isEnabled()) {
|
if (config.isEnabled()) {
|
||||||
log.info("Reading data from devices: {}", config.getDeviceName());
|
log.info("Reading data from devices: {}", config.getDeviceName());
|
||||||
ModbusMaster master = null;
|
|
||||||
try {
|
// 带重试的读取,最多重试2次
|
||||||
master = modbusProcessor.borrowMaster(config);
|
Map<String, Object> data = readWithRetry(config, 2);
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to borrow connection '{}'", config.getDeviceName(), e);
|
|
||||||
// 处理设备连接失败的情况,更新设备状态为离线,添加报警记录
|
|
||||||
addDeviceOfflineRecord(siteId, config.getDeviceNumber());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<String> rawValuEmptyList = new ArrayList<>();
|
List<String> rawValuEmptyList = new ArrayList<>();
|
||||||
Map<String, Object> data = modbusProcessor.readDataFromDevice(config, master);
|
|
||||||
// 在这里处理采集到的数据空
|
// 在这里处理采集到的数据空
|
||||||
config.getTags().forEach(tag -> {
|
config.getTags().forEach(tag -> {
|
||||||
Object rawValue = data.get(tag.getKey());
|
Object rawValue = data.get(tag.getKey());
|
||||||
@ -191,6 +190,51 @@ public class ModbusPoller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带重试的读取方法
|
||||||
|
*/
|
||||||
|
private Map<String, Object> readWithRetry(DeviceConfig config, int maxRetries) {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
ModbusMaster master = modbusProcessor.borrowMaster(config);
|
||||||
|
data = modbusProcessor.readDataFromDevice(config, master);
|
||||||
|
|
||||||
|
// 如果读取成功(有数据),直接返回
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
log.info("设备 {} 第 {} 次重试成功", config.getDeviceName(), attempt);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取返回空数据,等待后重试
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
log.warn("设备 {} 读取返回空数据,等待1秒后重试 ({}/{})",
|
||||||
|
config.getDeviceName(), attempt + 1, maxRetries);
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设备 {} 读取异常 ({}/{}): {}",
|
||||||
|
config.getDeviceName(), attempt + 1, maxRetries, e.getMessage());
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有重试都失败
|
||||||
|
log.error("设备 {} 读取失败,已重试 {} 次", config.getDeviceName(), maxRetries);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
private void processingData(Map<String, Object> data, String deviceNumber) {
|
private void processingData(Map<String, Object> data, String deviceNumber) {
|
||||||
if (CollectionUtils.isEmpty(data)) {
|
if (CollectionUtils.isEmpty(data)) {
|
||||||
// 增加失败计数
|
// 增加失败计数
|
||||||
@ -270,4 +314,4 @@ public class ModbusPoller {
|
|||||||
return Math.toIntExact(CronUtils.getNextExecutionIntervalMillis(sysJobs.get(0).getCronExpression()));
|
return Math.toIntExact(CronUtils.getNextExecutionIntervalMillis(sysJobs.get(0).getCronExpression()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,8 +39,10 @@ import java.util.Date;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -85,28 +87,26 @@ public class StrategyPoller {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ModbusProcessor modbusProcessor;
|
private ModbusProcessor modbusProcessor;
|
||||||
|
|
||||||
|
@Resource(name = "modbusExecutor")
|
||||||
|
private ExecutorService modbusExecutor;
|
||||||
|
|
||||||
public void pollAllDevices() {
|
public void pollAllDevices() {
|
||||||
logger.info("开始执行运行策略数据轮询...");
|
logger.info("开始执行运行策略数据轮询...");
|
||||||
List<StrategyRunningVo> strategyRunningVoList = emsStrategyRunningMapper.getPendingPollerStrategy(null);
|
List<StrategyRunningVo> strategyRunningVoList = emsStrategyRunningMapper.getPendingPollerStrategy(null);
|
||||||
strategyRunningVoList.forEach(strategyVo -> {
|
strategyRunningVoList.forEach(strategyVo -> {
|
||||||
Long strategyId = strategyVo.getId();
|
Long strategyId = strategyVo.getId();
|
||||||
if (strategyLocks.putIfAbsent(strategyId, true) == null) {
|
if (strategyLocks.putIfAbsent(strategyId, true) == null) {
|
||||||
try {
|
// 使用共享的modbusExecutor串行执行,避免与ModbusPoller并发访问导致通讯故障
|
||||||
CompletableFuture.runAsync(() -> {
|
modbusExecutor.submit(() -> {
|
||||||
processData(strategyVo);
|
try {
|
||||||
})
|
processData(strategyVo);
|
||||||
.exceptionally(e -> {
|
} catch (Exception e) {
|
||||||
logger.error("运行策略{}轮询异常", strategyVo.getId(), e);
|
logger.error("运行策略{}轮询异常", strategyVo.getId(), e);
|
||||||
return null;
|
} finally {
|
||||||
})
|
logger.info("运行策略{}轮询任务执行完成,释放锁", strategyVo.getId());
|
||||||
.thenRun(() -> {
|
strategyLocks.remove(strategyId);
|
||||||
logger.info("运行策略{}轮询任务执行完成,释放锁", strategyVo.getId());
|
}
|
||||||
strategyLocks.remove(strategyId);
|
});
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("运行策略{}任务失败", strategyVo.getId(), e);
|
|
||||||
strategyLocks.remove(strategyId);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.info("策略{}已在处理中,跳过重复执行", strategyId);
|
logger.info("策略{}已在处理中,跳过重复执行", strategyId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user