Java串口通信 RXTX 解决过程


背景介绍:

  由于第一次用Java与硬件通信,网上查了许多资料,在这进行整理,便于以后学习。本人串口测试是USB串口设备连接电脑,在设备管理器中找到端口名称(也可以通过一些虚拟串口工具模拟)。

下面主要简述获取串口消息返回值的一些问题,在最下面将会附上完整代码。

准备工作:

    RXTX包:mfz-rxtx-2.2-20081207-win-x64.zip,解压,RXTXcomm.jar加入项目依赖库里,rxtxParallel.dll和rxtxSerial.dll放入jdk的bin目录下(我使用的jdk1.8)

RXTX工具类编写:

  编写基础方法:获取可用端口名,开启端口,发送命令,接受命令,关闭端口

import gnu.io.*;

import javax.sound.midi.SoundbankResource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.TooManyListenersException;

import static com.why.rxtx.utill.HexadecimalUtil.get16NumAdd0;
import static com.why.rxtx.utill.HexadecimalUtil.hexStringToByteArray;
import static com.why.rxtx.utill.OrderUtil.retuenLogOrder;
import static com.why.rxtx.utill.OrderUtil.send;

/**
 * 使用rxtx连接串口工具类
 */
public class RXTXUtil {
   
    private static final String DEMONAME = "串口测试";

    /**
     * 检测系统中可用的端口
     */
    private CommPortIdentifier portId;
    /**
     * 获得系统可用的端口名称列表
     */
    private static Enumeration portList;
    /**
     * 输入流
     */
    private static InputStream inputStream;
    /**
     * RS-232的串行口
     */
    private static SerialPort serialPort;
    /**
     * 返回结果
     */
    private static String res=null;
    

    

    /**
     * 获得系统可用的端口名称列表
     * @return 可用端口名称列表
     */
    @SuppressWarnings("unchecked")
    public static void getSystemPort(){
        List systemPorts = new ArrayList<>();
        //获得系统可用的端口
        portList = CommPortIdentifier.getPortIdentifiers();
        while(portList.hasMoreElements()) {
            String portName = portList.nextElement().getName();//获得端口的名字
            systemPorts.add(portName);
        }

    }

    /**
     * 开启串口
     * @param serialPortName 串口名称
     * @param baudRate 波特率
     * @return 串口对象
     */
    public static void openSerialPort(String serialPortName,int baudRate) {
        try {
            //通过端口名称得到端口
            CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(serialPortName);
            //打开端口,(自定义名字,打开超时时间)
            CommPort commPort = portIdentifier.open(serialPortName, 5000);
            //判断是不是串口
            if (commPort instanceof SerialPort) {
                serialPort = (SerialPort) commPort;
                //设置串口参数(波特率,数据位8,停止位1,校验位无)
                serialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
                //    System.out.println("开启串口成功,串口名称:"+serialPortName);
            }else {
                //是其他类型的端口
                throw new NoSuchPortException();
            }
        } catch (NoSuchPortException e) {
            e.printStackTrace();
        } catch (PortInUseException e) {
            e.printStackTrace();
        } catch (UnsupportedCommOperationException e) {
            e.printStackTrace();
        }

    }

    /**
     * 向串口发送数据
     * @param order 发送的命令
     */
    public static void sendData( String order) {
        //16进制表示的字符串转换为字节数组
        byte[] data =hexStringToByteArray(order);
        OutputStream os = null;
        try {
            os = serialPort.getOutputStream();//获得串口的输出流
            os.write(data);
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭流操作
            try {
                if (os != null) {
                    os.close();
                    os = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 从串口读取数据
     * @return 读取的数据
     */
    public static String  readData() {
        //保存串口返回信息
        StringBuffer res=new StringBuffer(40);
        InputStream is = null;
        byte[] bytes = null;
        try {
            is = serialPort.getInputStream();//获得串口的输入流
            int bufflenth = is.available();//获得数据长度
            while (bufflenth != 0) {
                bytes = new byte[bufflenth];//初始化byte数组
                is.read(bytes);
                bufflenth = is.available();
            }
            if(bytes!=null) {
                for (int i = 0; i < bytes.length; i++) {
                    //转换成16进制数(FF)
                    res.append(get16NumAdd0((bytes[i]&0xff)+"",2));
                }
            }
            System.out.println("res: "+res.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                    is = null;
                }
            } catch(IOException e) {
                e.printStackTrace();
            }
        }

        return res.toString();
    }

    /**
     * 关闭串口
     *
     */
    public static void closeSerialPort() {
        if(serialPort != null) {
            serialPort.close();
            //System.out.println("关闭了串口:"+serialPort.getName());
            serialPort = null;
        }
    }

    
}

  在一些基本方法写完后,便得开始测试串口通信了

  于是就开始编写测试代码了

/**
     * 串口命令执行
     * @param order 命令
     * @param portName 端口名
     * @param baudRate 波特率
     * @return
     * @throws UnsupportedEncodingException
     */
    public synchronized  static String executeOrder(String order,String portName,int baudRate)  {
        String str="";
        if (serialPort==null) {
            openSerialPort(portName, baudRate);
        }
        //发送消息
        sendData(order);
               //接收消息
        String str=readData();
        
        return res;
    }
                

很遗憾上面代码输入命令,端口号等等后,返回结果一直是null, 突然间发现和以前写读写io流还是有一定区别的。原因在于,io流是有文件在那可以随时读写,而串口需要等待返回信息。

然后便开始改写代码。通过上网查询,发现有一个端口监听器,于是有了下面代码

/**
     * 串口命令执行
     * @param order 命令
     * @param portName 端口名
     * @param baudRate 波特率
     * @return
     * @throws UnsupportedEncodingException
     */
    public synchronized  static String executeOrder(String order,String portName,int baudRate)  {
        String str="";
        if (serialPort==null) {
            openSerialPort(portName, baudRate);
        }
        //发送消息
        sendData(order);
        
        //设置监听
        setListenerToSerialPort( new SerialPortEventListener(){
            @Override
            public void serialEvent(SerialPortEvent serialPortEvent) {
                if(serialPortEvent.getEventType() == SerialPortEvent.DATA_AVAILABLE) {//数据通知
                    String str=readData();
                    res=returnCheck(order,str);
                }
            }
        });

        
        return res;
    }

/**
     * 给串口设置监听
     * @param listener
     */
    public static void setListenerToSerialPort( SerialPortEventListener listener) {
        try {
            //给串口添加事件监听
            serialPort.addEventListener(listener);
        } catch (TooManyListenersException e) {
            e.printStackTrace();
        }
        serialPort.notifyOnDataAvailable(true);//串口有数据监听
        serialPort.notifyOnBreakInterrupt(true);//中断事件监听

    }

经过再次执行,发现返回的依旧是null,于是在监听器里打印串口消息输出,发现是先返回 null,然后再打印串口消息。于是得出结论监听器是异步的,但是我们需要返回值去判断是否执行成功,

显然这不符合要求。于是再做修改,需要把这异步返回值接收到。于是有了下面两种方式:

第一种:放弃监听器,使用循环去读取串口返回信息,通过循环次数设置最大请求时间

/**
     * 串口命令执行
     * @param order 命令
     * @param portName 端口名
     * @param baudRate 波特率
     * @return
     * @throws UnsupportedEncodingException
     */
    public synchronized  static String executeOrder(String order,String portName,int baudRate)  {
        String str="";
        if (serialPort==null) {
            openSerialPort(portName, baudRate);
        }
        //发送消息
        sendData(order);
        //代替监听
        for (int i = 0; i <1000; i++) {
            str=readData();
            str=returnCheck(order,str);
            if (str!=null){
                return str;
            }else {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
    }

第二种:使用监听,通过循环判断是否有串口消息返回,与第一种本质还是相同的

/**
     * 串口命令执行
     * @param order 命令
     * @param portName 端口名
     * @param baudRate 波特率
     * @return
     * @throws UnsupportedEncodingException
     */
    public synchronized  static String executeOrder(String order,String portName,int baudRate)  {
        String str="";
        if (serialPort==null) {
            openSerialPort(portName, baudRate);
        }
        //发送消息
        sendData(order);
        
        //设置监听
        setListenerToSerialPort( new SerialPortEventListener(){
            @Override
            public void serialEvent(SerialPortEvent serialPortEvent) {
                if(serialPortEvent.getEventType() == SerialPortEvent.DATA_AVAILABLE) {//数据通知
                    String str=readData();
                    res=returnCheck(order,str);
                }
            }
        });

        //监听是异步请求,如果要获取监听里的内容做返回值,
        // 可以通过循环去延迟来获取返回值,
        // 设置最大延迟防止死循环
        long startTime = System.currentTimeMillis();
        while (res==null){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
            if ((endTime-startTime)/1000.0>20){//最长20s返回
                return res;
            }
        }
        return res;
    }

到此串口消息通信就已解决了,下面附上完整代码,包括一些使用到的进制转换方法。

package com.along.outboundmanage.utill;

import gnu.io.*;

import javax.sound.midi.SoundbankResource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.TooManyListenersException;

import static com.why.rxtx.utill.HexadecimalUtil.get16NumAdd0;
import static com.why.rxtx.utill.HexadecimalUtil.hexStringToByteArray;



/**
 * 使用rxtx连接串口工具类
 */
public class RXTXUtil {
    
    private static final String DEMONAME = "串口测试";

    /**
     * 检测系统中可用的端口
     */
    private CommPortIdentifier portId;
    /**
     * 获得系统可用的端口名称列表
     */
    private static Enumeration portList;
    /**
     * 输入流
     */
    private static InputStream inputStream;
    /**
     * RS-232的串行口
     */
    private static SerialPort serialPort;
    /**
     * 返回结果
     */
    private static String res=null;
    /**
     * 串口命令执行
     * @param order 命令
     * @param portName 端口名
     * @param baudRate 波特率
     * @return
     * @throws UnsupportedEncodingException
     */
    public synchronized  static String executeOrder(String order,String portName,int baudRate)  {
        String str="";
        if (serialPort==null) {
            openSerialPort(portName, baudRate);
        }
        //发送消息
        sendData(order);
        //代替监听
        /*for (int i = 0; i <1000; i++) {
            str=readData();
            
            if (str!=null){
                return str;
            }else {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }*/
        //设置监听
        setListenerToSerialPort( new SerialPortEventListener(){
            @Override
            public void serialEvent(SerialPortEvent serialPortEvent) {
                if(serialPortEvent.getEventType() == SerialPortEvent.DATA_AVAILABLE) {//数据通知
                    String str=readData();
                    res=returnCheck(order,str);
                }
            }
        });

        //监听是异步请求,如果要获取监听里的内容做返回值,
        // 可以通过循环去延迟来获取返回值,
        // 设置最大延迟防止死循环
        long startTime = System.currentTimeMillis();
        while (res==null){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
            if ((endTime-startTime)/1000.0>20){//最长20s返回
                return res;
            }
        }
        return res;
    }

    


    /**
     * 获得系统可用的端口名称列表
     * @return 可用端口名称列表
     */
    @SuppressWarnings("unchecked")
    public static void getSystemPort(){
        List systemPorts = new ArrayList<>();
        //获得系统可用的端口
        portList = CommPortIdentifier.getPortIdentifiers();
        while(portList.hasMoreElements()) {
            String portName = portList.nextElement().getName();//获得端口的名字
            systemPorts.add(portName);
        }

    }

    /**
     * 开启串口
     * @param serialPortName 串口名称
     * @param baudRate 波特率
     * @return 串口对象
     */
    public static void openSerialPort(String serialPortName,int baudRate) {
        try {
            //通过端口名称得到端口
            CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(serialPortName);
            //打开端口,(自定义名字,打开超时时间)
            CommPort commPort = portIdentifier.open(serialPortName, 5000);
            //判断是不是串口
            if (commPort instanceof SerialPort) {
                serialPort = (SerialPort) commPort;
                //设置串口参数(波特率,数据位8,停止位1,校验位无)
                serialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
                //    System.out.println("开启串口成功,串口名称:"+serialPortName);
            }else {
                //是其他类型的端口
                throw new NoSuchPortException();
            }
        } catch (NoSuchPortException e) {
            e.printStackTrace();
        } catch (PortInUseException e) {
            e.printStackTrace();
        } catch (UnsupportedCommOperationException e) {
            e.printStackTrace();
        }

    }

    /**
     * 向串口发送数据
     * @param order 发送的命令
     */
    public static void sendData( String order) {
        //16进制表示的字符串转换为字节数组
        byte[] data =hexStringToByteArray(order);
        OutputStream os = null;
        try {
            os = serialPort.getOutputStream();//获得串口的输出流
            os.write(data);
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭流操作
            try {
                if (os != null) {
                    os.close();
                    os = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 从串口读取数据
     * @return 读取的数据
     */
    public static String  readData() {
        //保存串口返回信息
        StringBuffer res=new StringBuffer(40);
        InputStream is = null;
        byte[] bytes = null;
        try {
            is = serialPort.getInputStream();//获得串口的输入流
            int bufflenth = is.available();//获得数据长度
            while (bufflenth != 0) {
                bytes = new byte[bufflenth];//初始化byte数组
                is.read(bytes);
                bufflenth = is.available();
            }
            if(bytes!=null) {
                for (int i = 0; i < bytes.length; i++) {
                    //转换成16进制数(FF)
                    res.append(get16NumAdd0((bytes[i]&0xff)+"",2));
                }
            }
            System.out.println("res: "+res.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                    is = null;
                }
            } catch(IOException e) {
                e.printStackTrace();
            }
        }

        return res.toString();
    }

    /**
     * 关闭串口
     *
     */
    public static void closeSerialPort() {
        if(serialPort != null) {
            serialPort.close();
            //System.out.println("关闭了串口:"+serialPort.getName());
            serialPort = null;
        }
    }

    /**
     * 给串口设置监听
     * @param listener
     */
    public static void setListenerToSerialPort( SerialPortEventListener listener) {
        try {
            //给串口添加事件监听
            serialPort.addEventListener(listener);
        } catch (TooManyListenersException e) {
            e.printStackTrace();
        }
        serialPort.notifyOnDataAvailable(true);//串口有数据监听
        serialPort.notifyOnBreakInterrupt(true);//中断事件监听

    }
}
import org.thymeleaf.util.StringUtils;


/**
 * 进制换
 * */
public class HexadecimalUtil {
    

    /**
     * 十六进制转十进制
     *
     * @param num
     * @return
     */
    public static Integer get10HexNum(String num) {
        if (num.contains("0X")) {
            num = num.replace("0X", "");
        }
        return Integer.parseInt(num.substring(0), 16);
    }

    /**
     * 十进制转十六进制
     *
     * @param num
     * @return
     */
    public static String get16Num(Object num) {

        return Integer.toHexString(Integer.parseInt(num + ""));
    }

    /**
     * 十进制转十六进制,设置长度,不足补0
     *
     * @param num
     * @return
     */
    public static String get16NumAdd0(String num, int len) {
        String str = Integer.toHexString(Integer.parseInt(num)).toUpperCase();
        String res = "";
        if (len >= str.length()) {
            res = StringUtils.repeat("0", (len - str.length())) + str;
        } else {
            return str;
        }
        return res;
    }



    //num & 0xff
    public static int low8(Object num) {
        return Integer.parseInt(num + "") & 0xff;
    }

    //获取高四位
    public static int getHeight4(byte data) {
        int height;
        height = ((data & 0xf0) >> 4);
        return height;
    }

    /**
     * 16进制表示的字符串转换为字节数组
     *
     * @param hexString 16进制表示的字符串
     * @return byte[] 字节数组
     */
    public static byte[] hexStringToByteArray(String hexString) {
        hexString = hexString.replaceAll(" ", "");
        int len = hexString.length();
        byte[] bytes = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            // 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节
            bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
                    .digit(hexString.charAt(i + 1), 16));
        }
        return bytes;
    }
}