在工业现场,设备通信系统就像工厂的神经网络,连接着各种传感器、控制器和执行器。当你搭建好这套系统后,最关键的一步就是全面测试,确保每个环节都能正常工作。
就像汽车出厂前要经过严格的路试一样,Modbus RTU通信系统也需要经过全方位的测试验证。我们要检查能否正确读取温度传感器的数据、控制电机的启停、处理网络异常等各种情况。
本文基于实际工业项目的测试经验,详细介绍Modbus RTU通信的完整测试方案,帮你构建稳定可靠的工业通信系统。
1. 测试环境搭建
1.1 测试类基础结构
在工厂的质检车间,每台设备都要经过标准化的检测流程。我们的测试框架也是如此,需要建立一套标准的测试环境:
@Slf4j
@Disabled("需要实际设备连接才能运行")
@SpringBootTest
public class ModbusSerialTest {@Autowiredprivate ModbusSerialService modbusSerialService;@Autowiredprivate ModbusSerialConfig config;private int slaveId = 1; // 默认从站地址@BeforeEachpublic void setup() {// 初始化测试环境}@AfterEachpublic 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 {@Autowiredprivate 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命令或PowerShellif (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的寄存器,值为100boolean 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的线圈,值为trueboolean 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通信的核心功能:从基础的线圈和寄存器读写,到复杂的浮点数和字符串处理,为工业设备通信提供了完整的验证方案。