串口通信同步顯示及異常修復
依賴第三方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
