在工業現場,設備通信系統就像工廠的神經網絡,連接著各種傳感器、控制器和執行器。當你搭建好這套系統后,最關鍵的一步就是全面測試,確保每個環節都能正常工作。
就像汽車出廠前要經過嚴格的路試一樣,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通信的核心功能:從基礎的線圈和寄存器讀寫,到復雜的浮點數和字符串處理,為工業設備通信提供了完整的驗證方案。