串口通信同步顯示及異常修復
依賴第三方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包的底層代碼,於是有兩種解決思路:
-  
            
- 調試修改jar包的內部代碼
 
 -  
            
- 考慮用於串口通信的其他java解決方案,不用RXTX
 
 -  
            
- 采用一些上層操作掩蓋底層報錯
 
 
 -  
            
 
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,但並不會影響數據的顯示,串口仍然會正常的接受並將數據顯示在控制台,因此,在我們的實際應用中,我們不需要關注控制台的輸出,只需要將重量的數據傳達給我們所需要的顯示的前端,這樣一來,前端仍然能正常顯示數據,后端控制台的報錯異常就這樣被掩蓋了。
三、拓展
 通過 netty機制 + websocket協議 將數據顯示到web前端,源碼見文末鏈接中extra文件夾
 運行方式:新建project引入RXTXcomm.jar包,運行MainClass,之后打開index.html即可顯示
源碼及jar包鏈接:
https://gitee.com/LarryHawkingYoung/RXTXcomm_SerialPort_BugResolved.git
