在工业现场,设备通信系统就像工厂的神经网络,连接着各种传感器、控制器和执行器。当你搭建好这套系统后,最关键的一步就是全面测试,确保每个环节都能正常工作。

就像汽车出厂前要经过严格的路试一样,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);}
}

注意事项:

  1. 写入后添加延迟(2000ms),确保设备有足够时间处理
  2. 通过读取操作验证写入结果,确保写入成功

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通信的核心功能:从基础的线圈和寄存器读写,到复杂的浮点数和字符串处理,为工业设备通信提供了完整的验证方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/95763.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/95763.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/95763.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

少儿编程C++快速教程之——1. 基础语法和输入输出

1. 欢迎来到C编程世界&#xff01; 1.1 什么是编程&#xff1f; 编程就像是给计算机写一份详细的"说明书"&#xff0c;告诉它该做什么、怎么做。C是一种强大的编程语言&#xff0c;可以用来创建游戏、应用程序和各种有趣的软件&#xff01; 1.2 第一个C程序&#xff…

arma::imat22

arma::imat22 是 Armadillo C 线性代数库中定义的一个固定大小的 2x2 有符号整数矩阵类型。它主要用于处理小型、维度在编译时已知的整数矩阵&#xff0c;因其在栈上分配内存&#xff0c;故通常比动态矩阵有更高的效率。 下面是一个汇总了 arma::imat22 主要特性的表格&#xf…

狗都能看懂的HunYuan3D 1.0详解

HunYuan3D 1.0 HunYuan3D 1.0是2024年9月发布的一篇论文。虽然站在现在的时间节点&#xff0c;HunYuan3D系列已经出到2.5了&#xff0c;但是1.0版本的改进思路&#xff0c;和它trick集成的做法&#xff0c;还是很值得学习的。由于文章用到了很多技术&#xff0c;由于篇幅有限&a…

踏脚迈入奇幻乐园

每天早上上班的路上都会经过一个小花园。它被夹在丁字路口的拐角&#xff0c;面积不大&#xff0c;匆匆而过的行人都不会注意到它。但如果顺着几个不起眼的入口走进去&#xff0c;里面却是别有洞天。清早的街道还没有车水马龙的喧哗&#xff0c;花园里静悄悄的。各式各样的花草…

内存越界引发线程函数调用堆栈回溯异常以及INT 3软中断实战分析案例分享

目录 1、问题说明 2、导出dump文件时只是遇到了INT 3软中断&#xff0c;并没有发生异常崩溃 3、函数中发生了栈内存越界&#xff0c;导致线程的栈回溯出异常&#xff0c;堆栈中只显示一行函数调用记录 3.1、处理Json数据时产生了异常 3.2、函数中发生栈内存越界&#xff0…

LeetCode 240: 搜索二维矩阵 II - 算法详解(秒懂系列

文章目录LeetCode 240: 搜索二维矩阵 II - 算法详解题目描述Java解决方案算法思路核心理念为什么选择右上角&#xff1f;可视化演示过程示例1&#xff1a;查找 target 5示例2&#xff1a;查找 target 20 (不存在)算法分析时间复杂度空间复杂度算法优势关键要点扩展思考LeetCo…

洛谷 B4071 [GESP202412 五级] 武器强化

思考难度低&#xff0c;但是代码难度相对较高的题&#xff0c;故做个记录。首先&#xff0c;题目说了要花费最少的钱&#xff0c;所以我们每次拿最便宜的材料给武器1思想&#xff1a;每次都拿最便宜的材料然后考虑一下这个思想是否正确&#xff0c;找一下反例&#xff0c;每次拿…

SQL工具30年演进史:从Oracle到Navicat、DBeaver,再到Web原生SQLynx

目录 一、1990s&#xff1a;厂商自带的数据库工具时代 二、2000s&#xff1a;Navicat等商业数据库管理工具崛起 三、2010s&#xff1a;DBeaver等开源SQL工具兴起 四、2020s&#xff1a;SQLynx&#xff0c;Web原生数据库管理工具 五、SQL工具30年时间线对比 六、总结&…

C语言制作扫雷游戏(拓展版赋源码)

目录 引言&#xff1a; 三个新功能实现 1.可以选择难度或自定义 实现难点解析 代码实现&#xff08;附源码&#xff09; 扫雷.c game.h game.c 2.对选择位置进行标记或取消标记 一.框架 我们先理一下思路 如何构造框架 二.取消标记函数 三.标记函数 四.加入清屏&#xff0c;进…

Python快速入门专业版(十):字符串特殊操作:去除空格、判断类型与编码转换

目录引1.去除空格&#xff1a;清理字符串的实用技巧1.1 三类去空格方法&#xff1a;strip()、lstrip()、rstrip()1.2 实战案例&#xff1a;处理用户输入的空格问题2.判断类型&#xff1a;验证字符串内容的特性2.1 常用类型判断方法2.2 实战案例&#xff1a;验证用户输入的合法性…

Gamma AI:AI演示文稿制作工具,高效解决PPT框架搭建难与排版耗时问题

你做 PPT 的时候是不是也常陷入 “两难”&#xff1f;要么对着空白幻灯片发呆&#xff0c;不知道怎么搭框架 —— 比如要做 “产品季度迭代复盘”&#xff0c;既想放数据又想讲问题&#xff0c;结果页面堆得像乱炖&#xff1b;要么好不容易凑完内容&#xff0c;又花两小时调排版…

【应用案例】AI 给医用过滤器 “找茬”:3 大难点 + 全流程解决方案

【应用案例】AI 给医用过滤器 “找茬”&#xff1a;3 大难点 全流程解决方案&#x1f3af;医用过滤器进行医疗AI检测&#x1f3af;先看痛点&#xff1a;医用过滤器检测难在哪&#xff1f;&#x1f3af;AI检测方案&#xff1a;3步实现“零漏检”1. 硬件定制&#xff1a;让缺陷“…

【数据库相关】TxSQL新增数据库节点步骤

TxSQL新增数据库节点步骤准备工作与注意事项具体操作步骤第 1 步&#xff1a;在主库上创建复制专用账号第 2 步&#xff1a;对主库进行锁表并获取二进制日志坐标第 3 步&#xff1a;备份主库数据并传输到新从库第 4 步&#xff1a;主库解锁第 5 步&#xff1a;在新从库服务器上…

Jmeter快速安装配置全指南

1、JDK安装(Java Development Kit) 1.1.JDK下载 JDK下载址&#xff1a; Java Downloads | Oracle &#xff08;jdk-8u211-windows-x64.exe&#xff09; Android 基于 Java 语言开发&#xff0c;所以必须安装Java环境&#xff0c;Java 环境分JDK 和JRE &#xff0c;JDK提…

设计模式最佳实践 - 模板模式 + 责任链模式

废话不多说&#xff0c;直接切入正题&#xff0c;本篇要讲的是 模板模式 责任链模式 实践。该最佳实践本身就是一种对 责任链模式的增强&#xff0c;模板模式通过 父类 强耦合&#xff0c;预定义好 责任链 next 方法 的前后一些切面行为&#xff0c;优雅简洁。先上示例&#x…

Python快速入门专业版(十一):布尔值与None:Python中的“真假”与“空值”(附逻辑判断案例)

目录引言&#xff1a;为什么“真假”与“空值”是编程的核心逻辑1.布尔值&#xff08;bool&#xff09;&#xff1a;Python中的“真”与“假”1.1 布尔值的基础特性1.2 布尔运算&#xff1a;and、or、not的逻辑规则代码示例&#xff1a;基础布尔运算进阶特性&#xff1a;短路求…

C++学习知识小结

1. 什么是类&#xff1f;什么是对象&#xff1f;两者之间什么关系&#xff1f; 类是一类事物的共同特征的抽象描述&#xff0c;它定义这类所有的属性和方法 可以理解为模版类本身不占用空间&#xff0c;它只是一种定义&#xff0c;描述了对象一个是什么样子、能做什么 对象是根…

9. Mono项目与Unity的关系

1.Mono项目简介 2.Mono项目与Unity是如何结合的 3.从Mono到IL2CPP演变过程1.Mono项目简介 1).定义Mono是一个自由、开源的项目, 由Xamarin现属于微软主导开发; 它的目标是创建一个一套兼容于微软.NET Framework 的跨平台工具2).核心功能a.C#编译器能将你写的C#代码编译成IL(中间…

谷歌Genie 3:让你的照片变成可以玩的游戏世界

你是否曾凝视着一张完美的旅行照片&#xff0c;想象着如果能走进那个画面&#xff0c;自由探索会是怎样一种体验&#xff1f;或者&#xff0c;你是否曾被一幅画的奇幻氛围所吸引&#xff0c;渴望能在那片色彩斑斓的世界里奔跑跳跃&#xff1f;过去&#xff0c;这只是白日梦。而…

Cursor 提示词探索——如何打造真正懂自己的Agent

最近看到鱼皮的Cursor提示词分享&#xff08;微信公众平台)&#xff0c;刚好之前也在做Agent开发&#xff0c;跟提示词打交道的多&#xff0c;也经常发现 ai 蠢蠢的&#xff0c;一点不会根据提示词设计的来&#xff0c;按鱼皮的分享研究了一下&#xff0c;写了这篇博客。 Curs…