Java Modbus通信实战(四):Modbus通信测试与故障排查
在工业现场,设备通信系统就像工厂的神经网络,连接着各种传感器、控制器和执行器。当你搭建好这套系统后,最关键的一步就是全面测试,确保每个环节都能正常工作。
就像汽车出厂前要经过严格的路试一样,Modbus RTU通信系统也需要经过全方位的测试验证。我们要检查能否正确读取温度传感器的数据、控制电机的启停、处理网络异常等各种情况。
本文基于实际工业项目的测试经验,详细介绍Modbus RTU通信的完整测试方案,帮你构建稳定可靠的工业通信系统。
1. 测试环境搭建
1.1 测试类基础结构
在工厂的质检车间,每台设备都要经过标准化的检测流程。我们的测试框架也是如此,需要建立一套标准的测试环境:
@Slf4j
@Disabled("需要实际设备连接才能运行")
@SpringBootTest
public class ModbusSerialTest {
@Autowired
private ModbusSerialService modbusSerialService;
@Autowired
private ModbusSerialConfig config;
private int slaveId = 1; // 默认从站地址
@BeforeEach
public void setup() {
// 初始化测试环境
}
@AfterEach
public void cleanup() {
// 清理资源
}
// 各种测试方法
}
关键配置说明:
- @Disabled注解:相当于设备检测的"安全锁",防止在没有实际设备连接时误触发测试
- @BeforeEach:就像工人上班前的设备检查,确保测试环境准备就绪
- @AfterEach:如同下班后的设备关机程序,及时释放系统资源
1.2 测试初始化和清理
在钢铁厂,每次开炉前都要检查设备状态,生产结束后要安全关闭。我们的测试流程也遵循同样的原则:
@BeforeEach
public void setup() {
log.info("开始Modbus串口测试");
// 获取配置文件中的设备地址
slaveId = config.getDeviceAddress();
// 输出可用串口列表
String[] portNames = modbusSerialService.getAvailablePortNames().toArray(new String[0]);
log.info("可用串口列表: {}", Arrays.toString(portNames));
// 可选:测试串口连接
// boolean connected = modbusSerialService.testConnection(config.getPortName());
}
@AfterEach
public void cleanup() {
// 关闭连接,释放资源
modbusSerialService.closeConnection();
log.info("Modbus串口测试结束,连接已关闭");
}
1.3 服务类实现
这个服务类就像工厂的"中央控制室",集中管理所有设备的通信操作:
/**
* Modbus串口通信服务
*
* @author XYIoT
*/
@Slf4j
@Service
public class ModbusSerialService {
@Autowired
private ModbusSerialConfig config;
/**
* 获取可用串口列表
*
* @return 串口名称列表
*/
public List<String> getAvailablePortNames() {
return ModbusSerialUtil.getPortNames();
}
/**
* 测试串口连接
*
* @param portName 串口名称
* @return 连接结果
*/
public boolean testConnection(String portName) {
return ModbusSerialUtil.testConnection(portName);
}
/**
* 读取保持寄存器并解析
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 数量
* @return 解析后的数据
*/
public Map<String, Object> readHoldingRegisters(int slaveId, int offset, int quantity) {
int[] registers = ModbusSerialUtil.readHoldingRegisters(slaveId, offset, quantity);
Map<String, Object> result = new HashMap<>();
result.put("slaveId", slaveId);
result.put("startAddress", offset);
result.put("registers", registers);
return result;
}
/**
* 读取输入寄存器并解析
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 数量
* @return 解析后的数据
*/
public Map<String, Object> readInputRegisters(int slaveId, int offset, int quantity) {
int[] registers = ModbusSerialUtil.readInputRegisters(slaveId, offset, quantity);
Map<String, Object> result = new HashMap<>();
result.put("slaveId", slaveId);
result.put("startAddress", offset);
result.put("registers", registers);
return result;
}
/**
* 读取线圈状态并解析
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 数量
* @return 解析后的数据
*/
public Map<String, Object> readCoils(int slaveId, int offset, int quantity) {
boolean[] coils = ModbusSerialUtil.readCoils(slaveId, offset, quantity);
Map<String, Object> result = new HashMap<>();
result.put("slaveId", slaveId);
result.put("startAddress", offset);
result.put("coils", coils);
return result;
}
/**
* 读取离散输入状态并解析
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 数量
* @return 解析后的数据
*/
public Map<String, Object> readDiscreteInputs(int slaveId, int offset, int quantity) {
boolean[] inputs = ModbusSerialUtil.readDiscreteInputs(slaveId, offset, quantity);
Map<String, Object> result = new HashMap<>();
result.put("slaveId", slaveId);
result.put("startAddress", offset);
result.put("inputs", inputs);
return result;
}
/**
* 写入单个保持寄存器
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param value 写入值
* @return 操作结果
*/
public boolean writeSingleRegister(int slaveId, int offset, int value) {
try {
ModbusSerialUtil.writeSingleRegister(slaveId, offset, value);
return true;
} catch (Exception e) {
log.error("单个保持寄存器写入操作失败", e);
return false;
}
}
/**
* 写入多个保持寄存器
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param values 写入值数组
* @return 操作结果
*/
public boolean writeMultipleRegisters(int slaveId, int offset, int[] values) {
try {
ModbusSerialUtil.writeMultipleRegisters(slaveId, offset, values);
return true;
} catch (Exception e) {
log.error("多个保持寄存器写入操作失败", e);
return false;
}
}
/**
* 写入单个线圈
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param value 写入值
* @return 操作结果
*/
public boolean writeSingleCoil(int slaveId, int offset, boolean value) {
try {
ModbusSerialUtil.writeSingleCoil(slaveId, offset, value);
return true;
} catch (Exception e) {
log.error("单个线圈写入操作失败", e);
return false;
}
}
/**
* 写入多个线圈
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param values 写入值数组
* @return 操作结果
*/
public boolean writeMultipleCoils(int slaveId, int offset, boolean[] values) {
try {
ModbusSerialUtil.writeMultipleCoils(slaveId, offset, values);
return true;
} catch (Exception e) {
log.error("多个线圈写入操作失败", e);
return false;
}
}
/**
* 关闭连接
*/
public void closeConnection() {
ModbusSerialUtil.close(null);
}
/**
* 读取模拟量数据
*
* @param slaveId 从站地址
* @param offset 起始寄存器
* @param quantity 数量
* @param dataType 数据类型:1-无符号16位整数,2-有符号16位整数,3-无符号32位整数,4-有符号32位整数,5-浮点数
* @return 解析后的数据
*/
public double[] readAnalogValue(int slaveId, int offset, int quantity, int dataType) {
int[] registers = ModbusSerialUtil.readHoldingRegisters(slaveId, offset, quantity * (dataType >= 3 ? 2 : 1));
double[] values = new double[quantity];
for (int i = 0; i < quantity; i++) {
switch (dataType) {
case 1: // 无符号16位整数
values[i] = registers[i] & 0xFFFF;
break;
case 2: // 有符号16位整数
values[i] = (short) registers[i];
break;
case 3: // 无符号32位整数
values[i] = ((long) (registers[i * 2] & 0xFFFF) << 16) | (registers[i * 2 + 1] & 0xFFFF);
break;
case 4: // 有符号32位整数
values[i] = ((long) registers[i * 2] << 16) | (registers[i * 2 + 1] & 0xFFFF);
break;
case 5: // 浮点数
int highWord = registers[i * 2];
int lowWord = registers[i * 2 + 1];
int intValue = (highWord << 16) | (lowWord & 0xFFFF);
values[i] = Float.intBitsToFloat(intValue);
break;
default:
values[i] = registers[i];
}
}
return values;
}
}
1.4 配置类
配置类就像设备的"技术档案",详细记录了通信的各项参数:
/**
* Modbus串口通信配置类
*
* @author XYIoT
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "modbus.serial")
public class ModbusSerialConfig {
/**
* 串口名称
*/
private String portName = "COM3";
/**
* 波特率
*/
private int baudRate = 9600;
/**
* 数据位
*/
private int dataBits = 8;
/**
* 停止位
*/
private int stopBits = 1;
/**
* 校验位 (0-NONE, 1-ODD, 2-EVEN)
*/
private int parity = 0;
/**
* 超时时间(毫秒)
*/
private int timeout = 1000;
/**
* 设备地址
*/
private int deviceAddress = 1;
}
1.5 工具类
工具类是系统的"技术核心",负责执行具体的设备通信任务:
/**
* Modbus串口通信工具类
*
* @author XYIoT
*/
@Slf4j
@Component
public class ModbusSerialUtil {
/**
* 连接缓存,根据串口名称缓存连接实例
*/
private static final Map<String, ModbusMaster> CONNECTION_CACHE = new HashMap<>();
/**
* 获取串口配置
*/
private static ModbusSerialConfig getConfig() {
return SpringUtils.getBean(ModbusSerialConfig.class);
}
/**
* 获取ModbusMaster实例
*
* @return ModbusMaster对象
*/
public static ModbusMaster getMaster() {
return getMaster(null);
}
/**
* 根据串口名称获取ModbusMaster实例
*
* @param portName 串口名称,为null则使用配置文件中的默认值
* @return ModbusMaster对象
*/
public static ModbusMaster getMaster(String portName) {
ModbusSerialConfig config = getConfig();
String port = StringUtils.isEmpty(portName) ? config.getPortName() : portName;
log.info("正在连接Modbus串口: {}", port);
// 先从缓存获取
if (CONNECTION_CACHE.containsKey(port) && CONNECTION_CACHE.get(port) != null) {
ModbusMaster cachedMaster = CONNECTION_CACHE.get(port);
try {
if (!cachedMaster.isConnected()) {
log.info("缓存连接未连接,尝试重新连接");
cachedMaster.connect();
}
return cachedMaster;
} catch (Exception e) {
log.warn("缓存连接失效: {},正在创建新连接", e.getMessage());
// 如果缓存连接有问题,继续创建新连接
}
}
// 创建新的连接
try {
// 初始化配置
Modbus.setLogLevel(Modbus.LogLevel.LEVEL_DEBUG);
SerialParameters serialParameters = new SerialParameters();
serialParameters.setDevice(port);
// 设置波特率
try {
serialParameters.setBaudRate(BaudRate.getBaudRate(config.getBaudRate()));
} catch (Exception e) {
log.warn("波特率设置失败: {},采用默认值9600", e.getMessage());
serialParameters.setBaudRate(BaudRate.BAUD_RATE_9600);
}
serialParameters.setDataBits(config.getDataBits());
serialParameters.setStopBits(config.getStopBits());
// 设置校验位
switch (config.getParity()) {
case 1:
serialParameters.setParity(Parity.ODD);
break;
case 2:
serialParameters.setParity(Parity.EVEN);
break;
default:
serialParameters.setParity(Parity.NONE);
break;
}
log.info("通信参数配置: 波特率={}, 数据位={}, 停止位={}, 校验位={}",
config.getBaudRate(), config.getDataBits(),
config.getStopBits(), config.getParity());
SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
// 创建ModbusMaster实例
ModbusMaster master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
master.setResponseTimeout(config.getTimeout());
try {
// 尝试连接串口
log.info("正在建立串口连接...");
SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
master.connect();
log.info("串口连接建立成功");
// 连接成功,放入缓存
CONNECTION_CACHE.put(port, master);
return master;
} catch (ModbusIOException e) {
log.error("串口连接建立失败: {}", e.getMessage());
throw new Exception("串口连接建立失败: " + e.getMessage(), e);
}
} catch (Exception e) {
log.error("Modbus串口连接创建失败", e);
throw new ServiceException("Modbus串口连接创建失败: " + (e.getMessage() != null ? e.getMessage() : "未知错误,请检查串口配置和设备连接"));
}
}
/**
* 关闭连接
*
* @param portName 串口名称,为null则关闭所有连接
*/
public static void close(String portName) {
if (StringUtils.isEmpty(portName)) {
// 关闭所有连接
for (Map.Entry<String, ModbusMaster> entry : CONNECTION_CACHE.entrySet()) {
try {
if (entry.getValue() != null) {
entry.getValue().disconnect();
}
} catch (ModbusIOException e) {
log.error("Modbus串口[{}]连接关闭失败: {}", entry.getKey(), e.getMessage());
}
}
CONNECTION_CACHE.clear();
} else {
// 关闭指定连接
ModbusMaster master = CONNECTION_CACHE.get(portName);
if (master != null) {
try {
master.disconnect();
CONNECTION_CACHE.remove(portName);
} catch (ModbusIOException e) {
log.error("Modbus串口[{}]连接关闭失败: {}", portName, e.getMessage());
}
}
}
}
/**
* 读取保持寄存器
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 读取数量
* @return 寄存器值数组
*/
public static int[] readHoldingRegisters(int slaveId, int offset, int quantity) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
log.info("检测到连接断开,正在重新连接...");
master.connect();
// 连接建立后稍作等待,确保设备通信就绪
Thread.sleep(500);
}
// 添加重试逻辑
int maxRetries = 3;
ModbusIOException lastIoException = null;
ModbusProtocolException lastProtocolException = null;
for (int retry = 0; retry < maxRetries; retry++) {
try {
log.info("正在读取保持寄存器 (第{}/{}次): 从站地址={}, 起始地址={}, 寄存器数量={}",
retry + 1, maxRetries, slaveId, offset, quantity);
int[] result = master.readHoldingRegisters(slaveId, offset, quantity);
log.info("保持寄存器读取成功,数据: {}", Arrays.toString(result));
return result;
} catch (ModbusIOException e) {
lastIoException = e;
log.warn("保持寄存器读取IO异常 (第{}/{}次): {}", retry + 1, maxRetries, e.getMessage());
// 重试前延迟一段时间
Thread.sleep(1000);
} catch (ModbusProtocolException e) {
lastProtocolException = e;
log.warn("保持寄存器读取协议异常 (第{}/{}次): {}", retry + 1, maxRetries, e.getMessage());
Thread.sleep(1000);
}
}
// 重试失败后抛出最后捕获的异常
if (lastIoException != null) {
throw lastIoException;
}
if (lastProtocolException != null) {
throw lastProtocolException;
}
// 如果没有捕获到异常但仍然失败,抛出通用异常
throw new ModbusIOException("保持寄存器读取失败,多次重试后仍未成功");
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("保持寄存器读取操作失败: {}", e.getMessage());
throw new ServiceException("保持寄存器读取操作失败: " + e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("保持寄存器读取操作被中断: {}", e.getMessage());
throw new ServiceException("保持寄存器读取操作被中断: " + e.getMessage());
}
}
/**
* 读取输入寄存器
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 读取数量
* @return 寄存器值数组
*/
public static int[] readInputRegisters(int slaveId, int offset, int quantity) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
return master.readInputRegisters(slaveId, offset, quantity);
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("输入寄存器读取操作失败: {}", e.getMessage());
throw new ServiceException("输入寄存器读取操作失败: " + e.getMessage());
}
}
/**
* 读取线圈状态
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 读取数量
* @return 线圈状态数组
*/
public static boolean[] readCoils(int slaveId, int offset, int quantity) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
return master.readCoils(slaveId, offset, quantity);
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("线圈状态读取操作失败: {}", e.getMessage());
throw new ServiceException("线圈状态读取操作失败: " + e.getMessage());
}
}
/**
* 读取离散输入状态
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param quantity 读取数量
* @return 离散输入状态数组
*/
public static boolean[] readDiscreteInputs(int slaveId, int offset, int quantity) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
return master.readDiscreteInputs(slaveId, offset, quantity);
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("离散输入状态读取操作失败: {}", e.getMessage());
throw new ServiceException("离散输入状态读取操作失败: " + e.getMessage());
}
}
/**
* 写入单个保持寄存器
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param value 写入值
*/
public static void writeSingleRegister(int slaveId, int offset, int value) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
master.writeSingleRegister(slaveId, offset, value);
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("写入单个保持寄存器失败: {}", e.getMessage());
throw new ServiceException("写入单个保持寄存器失败: " + e.getMessage());
}
}
/**
* 写入多个保持寄存器
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param values 写入值数组
*/
public static void writeMultipleRegisters(int slaveId, int offset, int[] values) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
master.writeMultipleRegisters(slaveId, offset, values);
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("写入多个保持寄存器失败: {}", e.getMessage());
throw new ServiceException("写入多个保持寄存器失败: " + e.getMessage());
}
}
/**
* 写入单个线圈
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param value 写入值
*/
public static void writeSingleCoil(int slaveId, int offset, boolean value) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
master.writeSingleCoil(slaveId, offset, value);
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("写入单个线圈失败: {}", e.getMessage());
throw new ServiceException("写入单个线圈失败: " + e.getMessage());
}
}
/**
* 写入多个线圈
*
* @param slaveId 从站地址
* @param offset 偏移量
* @param values 写入值数组
*/
public static void writeMultipleCoils(int slaveId, int offset, boolean[] values) {
ModbusMaster master = getMaster();
try {
if (!master.isConnected()) {
master.connect();
}
master.writeMultipleCoils(slaveId, offset, values);
} catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
log.error("写入多个线圈失败: {}", e.getMessage());
throw new ServiceException("写入多个线圈失败: " + e.getMessage());
}
}
/**
* 获取可用串口列表
*
* @return 串口名称数组
*/
public static List<String> getPortNames() {
List<String> portList = new ArrayList<>();
// 方法1: 通过系统命令检测
try {
List<String> systemPorts = getSystemPortNames();
if (!systemPorts.isEmpty()) {
log.info("通过系统命令获取到串口: {}", systemPorts);
portList.addAll(systemPorts);
}
} catch (Exception e) {
log.warn("通过系统命令获取串口列表失败: {}", e.getMessage());
}
// 方法2: 使用jlibmodbus的SerialUtils获取
if (portList.isEmpty()) {
try {
String[] ports = SerialUtils.getPortIdentifiers().toArray(new String[0]);
if (ports != null && ports.length > 0) {
log.info("通过SerialUtils.getPortIdentifiers()获取到串口: {}", Arrays.toString(ports));
portList.addAll(Arrays.asList(ports));
} else {
log.warn("SerialUtils.getPortIdentifiers()返回空列表");
}
} catch (Exception e) {
log.warn("通过SerialUtils获取串口列表失败: {}", e.getMessage());
}
}
// 方法3: 使用javax.comm或gnu.io的方式获取
if (portList.isEmpty()) {
try {
// 通过反射调用RXTX库的方法
Class<?> commPortIdentifierClass = Class.forName("gnu.io.CommPortIdentifier");
Method getPortIdentifiersMethod = commPortIdentifierClass.getMethod("getPortIdentifiers");
Enumeration<?> portEnum = (Enumeration<?>) getPortIdentifiersMethod.invoke(null);
Method getNameMethod = commPortIdentifierClass.getMethod("getName");
Method getPortTypeMethod = commPortIdentifierClass.getMethod("getPortType");
while (portEnum.hasMoreElements()) {
Object portId = portEnum.nextElement();
// 只添加串行端口类型,通常判断portType == 1 (表示串行端口)
int portType = (Integer) getPortTypeMethod.invoke(portId);
if (portType == 1) {
String portName = (String) getNameMethod.invoke(portId);
if (!portList.contains(portName)) {
portList.add(portName);
}
}
}
log.info("通过RXTX库获取到串口: {}", portList);
} catch (Exception e) {
log.warn("通过RXTX库获取串口列表失败: {}", e.getMessage());
}
}
// 方法4: 直接尝试常见COM口名称
if (portList.isEmpty()) {
log.info("尝试添加常见COM口");
// Windows系统常见的串口命名
for (int i = 1; i <= 10; i++) {
String comPort = "COM" + i;
if (!portList.contains(comPort)) {
portList.add(comPort);
}
}
// Linux/Unix系统常见的串口命名
String[] unixDevs = {
"/dev/ttyS0", "/dev/ttyS1", "/dev/ttyS2", "/dev/ttyS3",
"/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2", "/dev/ttyUSB3",
"/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyACM2", "/dev/ttyACM3"
};
for (String dev : unixDevs) {
if (!portList.contains(dev)) {
portList.add(dev);
}
}
}
log.info("最终获取到的串口列表: {}", portList);
return portList;
}
/**
* 检测串口连接状态
*
* @param portName 串口名称
* @return 是否连接成功
*/
public static boolean testConnection(String portName) {
ModbusMaster master = null;
try {
log.info("开始测试串口连接: {}", portName);
// 创建一个新的连接实例进行测试,而不是使用缓存
ModbusSerialConfig config = getConfig();
// 初始化配置
SerialParameters serialParameters = new SerialParameters();
serialParameters.setDevice(portName);
serialParameters.setBaudRate(BaudRate.getBaudRate(config.getBaudRate()));
serialParameters.setDataBits(config.getDataBits());
serialParameters.setStopBits(config.getStopBits());
// 设置校验位
switch (config.getParity()) {
case 1:
serialParameters.setParity(Parity.ODD);
break;
case 2:
serialParameters.setParity(Parity.EVEN);
break;
default:
serialParameters.setParity(Parity.NONE);
break;
}
log.info("测试参数: 波特率={}, 数据位={}, 停止位={}, 校验位={}",
config.getBaudRate(), config.getDataBits(),
config.getStopBits(), config.getParity());
SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
// 创建ModbusMaster实例用于测试
log.info("serialParameters: {}", serialParameters);
master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
master.setResponseTimeout(config.getTimeout());
// 尝试连接
log.info("开始连接测试...");
try {
master.connect();
} catch (Exception e) {
log.error("连接串口失败,详细错误:", e);
// 输出更多调试信息
}
boolean connected = master.isConnected();
log.info("连接测试结果: {}", connected ? "成功" : "失败");
return connected;
} catch (Exception e) {
log.error("Modbus串口连接测试失败: {}", e.getMessage(), e);
return false;
} finally {
if (master != null) {
try {
master.disconnect();
log.info("测试连接已断开");
} catch (Exception e) {
log.error("关闭Modbus测试连接失败: {}", e.getMessage());
}
}
}
}
/**
* 通过系统命令检查COM端口
*
* @return 系统COM端口列表
*/
public static List<String> getSystemPortNames() {
List<String> portList = new ArrayList<>();
String osName = System.getProperty("os.name").toLowerCase();
Process process = null;
try {
// Windows系统使用mode命令或PowerShell
if (osName.contains("win")) {
log.info("检测Windows系统COM端口");
// 尝试使用PowerShell命令
try {
process = Runtime.getRuntime().exec(new String[] {
"powershell.exe", "-Command",
"[System.IO.Ports.SerialPort]::getportnames()"
});
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !portList.contains(line)) {
portList.add(line);
}
}
}
log.info("PowerShell检测到的COM端口: {}", portList);
} catch (Exception e) {
log.warn("PowerShell检测COM端口失败: {}", e.getMessage());
}
// 如果PowerShell失败,尝试使用mode命令
if (portList.isEmpty()) {
try {
process = Runtime.getRuntime().exec("mode");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.startsWith("COM")) {
String portName = line.split("\\s+")[0].trim();
if (!portList.contains(portName)) {
portList.add(portName);
}
}
}
}
log.info("mode命令检测到的COM端口: {}", portList);
} catch (Exception e) {
log.warn("mode命令检测COM端口失败: {}", e.getMessage());
}
}
}
// Linux/Unix系统
else if (osName.contains("nix") || osName.contains("nux") || osName.contains("mac")) {
log.info("检测Unix/Linux系统串口");
process = Runtime.getRuntime().exec("ls -la /dev/tty*");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("ttyS") || line.contains("ttyUSB") ||
line.contains("ttyACM") || line.contains("cu.")) {
String[] parts = line.split("\\s+");
String portName = "/dev/" + parts[parts.length - 1];
if (!portList.contains(portName)) {
portList.add(portName);
}
}
}
}
log.info("ls命令检测到的串口: {}", portList);
}
return portList;
} catch (Exception e) {
log.warn("通过系统命令检测COM端口失败: {}", e.getMessage());
return portList;
} finally {
if (process != null) {
process.destroy();
}
}
}
}
2. 读取操作测试
在工业现场,读取操作就像工程师查看仪表盘,需要从不同类型的设备获取各种数据。温度传感器提供温度值,压力表显示压力数据,开关状态指示设备运行情况。Modbus支持多种读取操作,我们需要用不同的功能码读取设备的各种数据:
2.1 读取保持寄存器(功能码03)
保持寄存器相当于设备的"参数设置面板",存储着各种可调节的参数。就像变频器的频率设定、温控器的目标温度、流量计的量程设置等,这些参数既可以读取也可以修改:
@Test
public void testReadHoldingRegisters() {
try {
// 读取地址为0的10个保持寄存器
Map<String, Object> result = modbusSerialService.readHoldingRegisters(slaveId, 0, 10);
log.info("读取保持寄存器结果: {}", result);
// 输出每个寄存器的值
int[] registers = (int[]) result.get("registers");
for (int i = 0; i < registers.length; i++) {
log.info("寄存器[{}] = {}", i, registers[i]);
}
} catch (Exception e) {
log.error("读取保持寄存器测试失败", e);
}
}
保持寄存器应用场景:
- 设备配置参数存储
- PLC控制参数
- 工作状态设置值
2.2 读取输入寄存器(功能码04)
输入寄存器就像工厂里的"数据显示屏",专门用来显示各种测量数据。比如锅炉的当前温度、水泵的实际流量、电机的运行电流等,这些数据只能读取,无法通过通信修改:
@Test
public void testReadInputRegisters() {
try {
// 读取地址为0的10个输入寄存器
Map<String, Object> result = modbusSerialService.readInputRegisters(slaveId, 0, 10);
log.info("读取输入寄存器结果: {}", result);
// 输出每个寄存器的值
int[] registers = (int[]) result.get("registers");
for (int i = 0; i < registers.length; i++) {
log.info("输入寄存器[{}] = {}", i, registers[i]);
}
} catch (Exception e) {
log.error("读取输入寄存器测试失败", e);
}
}
输入寄存器应用场景:
- 传感器测量值(温度、湿度、压力等)
- ADC转换结果
- 设备状态信息
2.3 读取线圈状态(功能码01)
线圈状态就像控制柜里的"指示灯",显示各种设备的开关状态。比如电机是否运行、阀门是否打开、报警器是否激活等:
@Test
public void testReadCoils() {
try {
// 读取地址为0的10个线圈
Map<String, Object> result = modbusSerialService.readCoils(slaveId, 0, 10);
log.info("读取线圈状态结果: {}", result);
// 输出每个线圈的状态
boolean[] coils = (boolean[]) result.get("coils");
for (int i = 0; i < coils.length; i++) {
log.info("线圈[{}] = {}", i, coils[i]);
}
} catch (Exception e) {
log.error("读取线圈状态测试失败", e);
}
}
线圈应用场景:
- 控制继电器、电磁阀等执行器
- 设备开关控制
- 控制指示灯
2.4 读取离散输入状态(功能码02)
离散输入就像工厂里的"状态检测器",用来监测各种开关量信号。比如安全门是否关闭、限位开关是否触发、故障指示是否出现等,这些信号只能检测,无法控制:
@Test
public void testReadDiscreteInputs() {
try {
// 读取地址为0的10个离散输入
Map<String, Object> result = modbusSerialService.readDiscreteInputs(slaveId, 0, 10);
log.info("读取离散输入状态结果: {}", result);
// 输出每个离散输入的状态
boolean[] inputs = (boolean[]) result.get("inputs");
for (int i = 0; i < inputs.length; i++) {
log.info("离散输入[{}] = {}", i, inputs[i]);
}
} catch (Exception e) {
log.error("读取离散输入状态测试失败", e);
}
}
离散输入应用场景:
- 开关量输入(按钮、开关、限位开关等)
- 数字传感器状态
- 故障指示信号
3. 写入操作测试
写入操作是工业控制的核心功能,就像操作员在控制室调节各种设备参数。比如调节反应釜的温度、控制输送带的速度、开关冷却水阀门等,每个操作都直接影响生产工艺和产品质量。
Modbus支持多种写入操作,就像遥控器控制电视一样,我们可以向设备发送各种控制指令:
3.1 写入单个保持寄存器(功能码06)
单个寄存器写入就像精确调节一个参数,比如设定变频器的运行频率、调节温控器的目标温度等。就像调节空调温度一样,有时我们只需要修改一个参数:
@Test
public void testWriteSingleRegister() {
try {
// 写入地址为0的寄存器,值为100
boolean result = modbusSerialService.writeSingleRegister(slaveId, 0, 100);
log.info("写入单个保持寄存器结果: {}", result ? "成功" : "失败");
// 增加延迟,给设备足够处理时间
log.info("等待设备处理写入操作...");
Thread.sleep(2000);
// 读取写入后的值进行验证
if (result) {
Map<String, Object> readResult = modbusSerialService.readHoldingRegisters(slaveId, 0, 1);
int[] values = (int[]) readResult.get("registers");
log.info("写入后读取的值: {}", values[0]);
}
} catch (Exception e) {
log.error("写入单个保持寄存器测试失败", e);
}
}
注意事项:
- 写入后添加延迟(2000ms),确保设备有足够时间处理
- 通过读取操作验证写入结果,确保写入成功
3.2 写入多个保持寄存器(功能码16)
批量寄存器写入适合同时设置多个相关参数,比如配置PID控制器的比例、积分、微分参数,或者设置多段温度曲线。就像一次性设置空调的温度、风速、模式一样,批量操作更高效:
@Test
public void testWriteMultipleRegisters() {
try {
// 写入地址为0开始的3个寄存器
int[] values = {100, 200, 300};
boolean result = modbusSerialService.writeMultipleRegisters(slaveId, 0, values);
log.info("写入多个保持寄存器结果: {}", result ? "成功" : "失败");
// 读取写入后的值进行验证
if (result) {
Map<String, Object> readResult = modbusSerialService.readHoldingRegisters(slaveId, 0, 3);
int[] readValues = (int[]) readResult.get("registers");
log.info("写入后读取的值: {}", Arrays.toString(readValues));
}
} catch (Exception e) {
log.error("写入多个保持寄存器测试失败", e);
}
}
应用场景:
- 批量更新配置参数
- 设置多通道值
- 写入复杂数据结构(浮点数、32位整数等)
3.3 写入单个线圈(功能码05)
单个线圈控制就像操作控制柜上的一个按钮,比如启动一台电机、打开一个阀门、激活一个报警器。就像按下电灯开关,控制单个设备的开关:
@Test
public void testWriteSingleCoil() {
try {
// 写入地址为0的线圈,值为true
boolean result = modbusSerialService.writeSingleCoil(slaveId, 0, true);
log.info("写入单个线圈结果: {}", result ? "成功" : "失败");
// 读取写入后的值进行验证
if (result) {
Map<String, Object> readResult = modbusSerialService.readCoils(slaveId, 0, 1);
boolean[] values = (boolean[]) readResult.get("coils");
log.info("写入后读取的值: {}", values[0]);
}
} catch (Exception e) {
log.error("写入单个线圈测试失败", e);
}
}
3.4 写入多个线圈(功能码15)
批量线圈控制适合同时操作多个相关设备,比如启动一条生产线上的所有电机、关闭一个区域的所有阀门。就像总控制台,一次性控制多个设备的开关:
@Test
public void testWriteMultipleCoils() {
try {
// 写入地址为0开始的3个线圈
boolean[] values = {true, false, true};
boolean result = modbusSerialService.writeMultipleCoils(slaveId, 0, values);
log.info("写入多个线圈结果: {}", result ? "成功" : "失败");
// 读取写入后的值进行验证
if (result) {
Map<String, Object> readResult = modbusSerialService.readCoils(slaveId, 0, 3);
boolean[] readValues = (boolean[]) readResult.get("coils");
log.info("写入后读取的值: {}", Arrays.toString(readValues));
}
} catch (Exception e) {
log.error("写入多个线圈测试失败", e);
}
}
应用场景:
- 批量控制多个设备状态
- LED状态设置
- 多点输出控制
4. 高级数据类型测试
工业现场的数据类型多种多样,就像不同的仪表有不同的测量范围和精度。温度可能是小数,计数器是整数,状态是布尔值。我们需要确保系统能正确处理各种数据格式。
Modbus就像只会说简单词汇的外国人,只懂布尔值和16位整数。但我们可以把简单词汇组合成复杂句子,实现更丰富的数据类型:
@Test
public void testReadAnalogValue() {
try {
// 读取浮点数(32位,占用2个寄存器)
double[] floatValues = modbusSerialService.readAnalogValue(slaveId, 0, 2, 5);
log.info("读取浮点数: {}", Arrays.toString(floatValues));
// 读取16位整数
double[] int16Values = modbusSerialService.readAnalogValue(slaveId, 0, 4, 2);
log.info("读取16位整数: {}", Arrays.toString(int16Values));
// 读取32位整数(占用2个寄存器)
double[] int32Values = modbusSerialService.readAnalogValue(slaveId, 0, 2, 4);
log.info("读取32位整数: {}", Arrays.toString(int32Values));
} catch (Exception e) {
log.error("读取模拟量测试失败", e);
}
}
这个测试方法展示了如何读取不同类型的模拟量:
参数说明:
slaveId:从站地址- 第二个参数:起始地址
- 第三个参数:数据类型(2表示32位浮点数,4表示16位整数,5表示32位整数)
- 第四个参数:要读取的点数
5. 测试技巧与最佳实践
就像医生体检有标准流程一样,Modbus测试也有一套最佳实践:
5.1 异常处理
就像开车系安全带一样,异常处理是测试的"安全带",确保单个测试失败不会影响其他测试:
try {
// 测试代码
} catch (Exception e) {
log.error("测试失败", e);
}
5.2 数据验证
就像寄信后查看是否送达一样,写操作后要读取验证,确保数据正确写入:
// 写入操作
boolean result = modbusSerialService.writeSingleRegister(slaveId, 0, 100);
// 读取验证
Map<String, Object> readResult = modbusSerialService.readHoldingRegisters(slaveId, 0, 1);
int[] values = (int[]) readResult.get("registers");
assert values[0] == 100;
5.3 时序控制
Modbus设备就像老式电脑,需要时间"思考",特别是写操作后要给它缓冲时间:
// 写入操作
boolean result = modbusSerialService.writeSingleRegister(slaveId, 0, 100);
// 等待设备处理
Thread.sleep(2000);
// 读取验证
5.4 资源释放
就像用完水龙头要关闭一样,测试完成后要释放串口资源,避免资源泄露:
@AfterEach
public void cleanup() {
modbusSerialService.closeConnection();
}
6. 常见问题与解决方案
在工业现场,Modbus通信问题就像设备故障一样常见。以下是几种典型问题的诊断和处理方法:
6.1 通信超时
现象:变频器控制指令发送后无响应,测试抛出超时异常
处理方法:
- 检查RS485线缆连接是否牢固
- 调整超时参数(通常设置为2-5秒)
- 确认波特率设置与设备一致(常用9600或19200)
6.2 校验和错误
现象:温度传感器数据读取时出现CRC校验失败
处理方法:
- 在操作间增加50-100ms延迟
- 检查通信参数配置(数据位、停止位、校验位)
- 更换质量更好的屏蔽双绞线
6.3 设备无响应
现象:PLC模块完全不回应任何Modbus指令
处理方法:
- 确认设备从站地址配置正确(通常为1-247)
- 验证设备是否支持所使用的功能码
- 检查设备电源和运行状态指示灯
7. 扩展应用
这套测试框架在实际工程项目中有广泛的应用价值:
生产线自动化测试
在汽车制造生产线上,每台新安装的焊接机器人都需要通过Modbus通信测试,确保能正确接收工艺参数和反馈状态信息。
设备调试与维护
当钢铁厂的轧机出现通信故障时,维护工程师可以使用这套测试代码快速定位问题,验证PLC与上位机之间的数据交换是否正常。
系统集成验证
在水处理厂的SCADA系统集成项目中,需要验证不同厂商的流量计、压力变送器等设备是否都能正确响应Modbus指令。
性能基准测试
对于大型化工装置的DCS系统,需要测试在高负载情况下Modbus通信的响应时间和稳定性,确保满足实时控制要求。
8. 总结
本文构建的测试框架涵盖了Modbus RTU通信的核心功能:从基础的线圈和寄存器读写,到复杂的浮点数和字符串处理,为工业设备通信提供了完整的验证方案。
- 点赞
- 收藏
- 关注作者
评论(0)