1. 程式人生 > 實用技巧 >基於RXTXcomm的串列埠通訊及異常修復

基於RXTXcomm的串列埠通訊及異常修復

串列埠通訊同步顯示及異常修復

依賴第三方jar包:RXTXcomm.jar (下載見文末連結)

一、程式碼分析:

step_1: 獲取埠

/**
 * 檢測並獲取當前裝置所有的可用埠(此處可包括USB埠和藍芽埠)
 * @return 返回包含所有可用埠的名稱的列表(如COM4、COM6等)
 * 可將返回的列表依次輸出以檢視
 * 當然也可以通過‘裝置管理器-埠’來檢視可用埠
 */
public ArrayList<String> findPorts() {
    // 呼叫jar包內的getPortIdentifiers函式,獲得當前所有可用埠的列舉
    Enumeration<CommPortIdentifier> portList = CommPortIdentifier.getPortIdentifiers();
    ArrayList<String> portNameList = new ArrayList<String>();
    // 將可用埠名新增到List並返回該List
    while (portList.hasMoreElements()) {
        String portName = portList.nextElement().getName();
        portNameList.add(portName);
    }
    return portNameList;
}

step_2: 開啟串列埠

/**
 * 通過上一步獲取的埠名來開啟串列埠並設定串列埠引數
 * @param portName 埠名
 * @param baudrate 波特率(需與電子秤的波特率一致,一般為9600,建議作為final巨集觀常量放在程式開頭)
 * @return 返回開啟的串列埠,若非串列埠則返回null
 * @throws PortInUseException 當埠已被佔用時丟擲異常
 */
public SerialPort openPort(String portName, int baudrate) throws PortInUseException {
    try {
        // 通過埠名識別埠
        CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
        // 開啟埠,並給埠名字和一個timeout(開啟操作的超時時間)
        CommPort commPort = portIdentifier.open(portName, 2000);
        // 判斷埠是不是串列埠
        if (commPort instanceof SerialPort) {
            SerialPort serialPort = (SerialPort) commPort;
            try {
                // 設定一下串列埠的波特率等引數
                // 資料位:8
                // 停止位:1
                // 校驗位:None
                serialPort.setSerialPortParams(baudrate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
            } catch (UnsupportedCommOperationException e) {
                e.printStackTrace();
            }
            return serialPort;
        }
    } catch (NoSuchPortException e1) {
        e1.printStackTrace();
    }
    return null;
}

step_3: 新增串列埠事件監聽

/**
 * 為開啟的串列埠新增資料到達事件監聽、通訊中斷監聽
 * @param serialPort 已開啟的串列埠
 * @param listener 監聽器
 */
public void addListener(SerialPort serialPort, DataAvailableListener listener) {
    try {
        /**
         * 給串列埠新增監聽器
         * 函式addEventListener為jar包自帶函式
         * 函式addEventListener的引數listener必須為SerialPortEventListener型別
         * 所以DataAvailableListener必須實現SerialPortEventListener介面
         */
        serialPort.addEventListener(listener);
        // 設定當有資料到達時喚醒監聽接收執行緒
        serialPort.notifyOnDataAvailable(true);
        // 設定當通訊中斷時喚醒中斷執行緒
        serialPort.notifyOnBreakInterrupt(true);
    } catch (TooManyListenersException e) {
        e.printStackTrace();
    } catch (NullPointerException e) {
        e.printStackTrace();
    }
}

/**
 * 自定義監聽器,實現jar包中定義的SerialPortEventListener介面,並覆寫serialEvent方法
 */
public class DataAvailableListener implements SerialPortEventListener {
    @Override
    public void serialEvent(SerialPortEvent serialPortEvent) {
        /**
         * 總共有10類事件可以監聽
         * 此處只對兩類事件進行了反應和處理
         */
        switch (serialPortEvent.getEventType()) {
            case SerialPortEvent.DATA_AVAILABLE: //接收到資料事件
                byte[] data;
                try {
                    if (mSerialport == null) {
                        System.out.println("串列埠物件為空,監聽失敗!");
                    } else {
                        // 讀取串列埠資料
                        data = readFromPort(mSerialport);

                        // 將ASCII碼陣列轉化為對應的字串
                        String text = new String(data);

                        // 去除不必要的字元
                        text = text.replaceAll(" ", "");
                        text = text.replaceAll("\r", "");
                        text = text.replaceAll("\n", "");
                        text = text.replaceAll("\t", "");
                        if (text.length() > 0) {
                            //將處理後的重量資訊列印輸出
                            System.out.println(text);
                        }
                    }
                } catch (Exception e) {
                    System.out.println(e.toString());
                    // 發生讀取錯誤時顯示錯誤資訊後退出系統
                    System.exit(0);
                } break;

            case SerialPortEvent.OUTPUT_BUFFER_EMPTY: // 2.輸出緩衝區已清空
                break;

            case SerialPortEvent.CTS: // 3.清除待發送資料
                break;

            case SerialPortEvent.DSR: // 4.待發送資料準備好了
                break;

            case SerialPortEvent.RI: // 5.振鈴指示
                break;

            case SerialPortEvent.CD: // 6.載波檢測
                break;

            case SerialPortEvent.OE: // 7.溢位(溢位)錯誤
                break;

            case SerialPortEvent.PE: // 8.奇偶校驗錯誤
                break;

            case SerialPortEvent.FE: // 9.幀錯誤
                break;

            case SerialPortEvent.BI: // 10.通訊中斷
                System.out.println("與串列埠裝置通訊中斷");
                System.exit(0);
                break;

            default:
                break;
        }
    }
}

step_4: 從串列埠讀取收到的資料

/**
 * 從串列埠中按位元組讀取收到的資料
 * @param serialPort 開啟後有資料傳達的串列埠
 * @return 以位元組陣列的形式返回收到的資料資訊
 */
public byte[] readFromPort(SerialPort serialPort) {
    InputStream in = null;
    byte[] bytes = {};// 採用位元組陣列儲存傳來的ASCII碼值,方便之後轉化為字串
    try {
        in = serialPort.getInputStream();//得到串列埠輸入流
        // 緩衝區大小為一個位元組
        byte[] readBuffer = new byte[1];
        int bytesNum = in.read(readBuffer);
        while (bytesNum > 0) {
            bytes = concat(bytes, readBuffer);
            bytesNum = in.read(readBuffer);//將讀取到的二進位制資料存於readBuffer並返回讀取到的位元組數
        }//按照位元組將資料加入到位元組陣列中
    } catch (IOException e) {
        restart(); //捕獲異常並讀取
    } finally {
        try {
            if (in != null) {
                in.close();
                in = null;
            }
        } catch (IOException e) {
            restart(); //捕獲異常並讀取
        }
    }
    return bytes;
}

/**
 * 將兩位元組數組合併為同一個
 * @param firstArray
 * @param secondArray
 * @return 返回合併後的位元組陣列
 */
public byte[] concat(byte[] firstArray, byte[] secondArray) {
    if (firstArray == null || secondArray == null) {
        return null;
    }
    byte[] bytes = new byte[firstArray.length + secondArray.length];
    System.arraycopy(firstArray, 0, bytes, 0, firstArray.length);
    System.arraycopy(secondArray, 0, bytes, firstArray.length, secondArray.length);
    return bytes;
}

主函式:

private SerialPort mSerialport = null;
private final int BAUDRATE = 9600;// 波特率,預設為9600

public static void main(String[] args) {
    String commName = null;
    if (findPorts().size() > 0) {
        // 獲取埠名稱,預設取第一個埠
        commName = findPorts().get(0); // step_1
    }
    if (commName == null) {// 說明不存在可用埠
        System.out.println("沒有搜尋到有效埠!");
    } else {
        try {
            mSerialport = openPort(commName, BAUDRATE); // step_2
            if (mSerialport != null) {
                System.out.println("串列埠已開啟");
            }
        } catch (PortInUseException e) {
            System.out.println("串列埠已被佔用!");
        }
        
        // 新增串列埠監聽
    	addListener(mSerialport, new DataAvailableListener()); // step_3、step_4
    }
}

二、問題與解決

1、問題描述

在程式執行約2~3分鐘後會按照一定週期出現如下異常,並中斷執行

2、原因分析

主要是兩種錯誤:

  • 第一個是 IOException 異常,是在呼叫 readFromPort 函式從串列埠讀取資料的過程中,從更底層被丟擲後在 readFromPort 函式中被捕獲的。

  • 第二個 Error 也是從底層的.c檔案中出的錯,右側的亂碼 "�ܾ����ʡ�" 翻譯成 GBK 編碼後是 "拒絕訪問" 。

  • 可見這些錯誤來自於jar包的底層程式碼,於是有兩種解決思路:

      1. 除錯修改jar包的內部程式碼
      1. 考慮用於串列埠通訊的其他java解決方案,不用RXTX
      1. 採用一些上層操作掩蓋底層報錯

3、解決辦法

​ 因為無意間發現當出現以上報錯使執行中斷時,如果能關閉串列埠然後再次開啟串列埠,此時又能成功接收到資料並顯示,雖然之後還會繼續出現報錯,但是每次報錯都能通過對串列埠的重啟來解決,同時考慮到報錯具有一定的週期性,因此考慮新建一個執行緒來週期性地對埠進行重啟,具體程式碼如下:

private Thread restartThread = new Thread(new RestartThread());

public void restart() { //在 step_4 的 readFromPort 函式中捕獲 IOException 後執行
    if (!restartThread.isAlive() || restartThread.isInterrupted()) {
        restartThread.start();
    }
}

class RestartThread implements Runnable {
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            closeSerialPort();
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            openSerialPort();
        }
    }
}

public void closeSerialPort() {
    if (mSerialport != null) {
        mSerialport.close();
    }
    mSerialport = null;
}

public void openSerialPort() {
    String commName = null;
    if (findPorts().size() > 0) {
        // 獲取埠名稱,預設取第一個埠
        commName = findPorts().get(0); // step_1
    }
    if (commName == null) {// 說明不存在可用埠
        System.out.println("沒有搜尋到有效埠!");
    } else {
        try {
            mSerialport = openPort(commName, BAUDRATE); // step_2
            if (mSerialport != null) {
                System.out.println("串列埠已開啟");
            }
        } catch (PortInUseException e) {
            System.out.println("串列埠已被佔用!");
        }
        
        // 新增串列埠監聽
    	addListener(mSerialport, new DataAvailableListener()); // step_3、step_4
    }
}

並將主函式修改為:

private SerialPort mSerialport = null;
private final int BAUDRATE = 9600;// 波特率,預設為9600

public static void main(String[] args) {
    openSerialPort();
}

4、效果分析

​ 採用這種方式,當遇到第一個IOException時,就會按照一定的週期重啟重新整理視窗,可以看到控制檯在不斷的重新整理,雖然時常會出現Error,但並不會影響資料的顯示,串列埠仍然會正常的接受並將資料顯示在控制檯,因此,在我們的實際應用中,我們不需要關注控制檯的輸出,只需要將重量的資料傳達給我們所需要的顯示的前端,這樣一來,前端仍然能正常顯示資料,後端控制檯的報錯異常就這樣被掩蓋了。

原始碼及jar包連結:

https://gitee.com/LarryHawkingYoung/RXTXcomm_SerialPort_BugResolved.git