串口通信同步显示及异常修复
依赖第三方jar包:RXTXcomm.jar (下载见文末链接)
一、代码分析:
step_1: 获取端口
/**
* 检测并获取当前设备所有的可用端口(此处可包括USB端口和蓝牙端口)
* @return 返回包含所有可用端口的名称的列表(如COM4、COM6等)
* 可将返回的列表依次输出以查看
* 当然也可以通过‘设备管理器-端口’来查看可用端口
*/
public ArrayList findPorts() {
// 调用jar包内的getPortIdentifiers函数,获得当前所有可用端口的枚举
Enumeration portList = CommPortIdentifier.getPortIdentifiers();
ArrayList portNameList = new ArrayList();
// 将可用端口名添加到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