基于RXTXcomm的串口通信及异常修复

串口通信同步显示及异常修复

依赖第三方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分钟后会按照一定周期出现如下异常,并中断运行

基于RXTXcomm的串口通信及异常修复_第1张图片

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,但并不会影响数据的显示,串口仍然会正常的接受并将数据显示在控制台,因此,在我们的实际应用中,我们不需要关注控制台的输出,只需要将重量的数据传达给我们所需要的显示的前端,这样一来,前端仍然能正常显示数据,后端控制台的报错异常就这样被掩盖了。

三、拓展

​ 通过 netty机制 + websocket协议 将数据显示到web前端,源码见文末链接中extra文件夹
运行方式:新建project引入RXTXcomm.jar包,运行MainClass,之后打开index.html即可显示

源码及jar包链接:

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

你可能感兴趣的:(基于RXTXcomm的串口通信及异常修复)