本文主要介绍了Java串口通信技术探究,重点分析了RXTX库单例测试以及串口工具的使用。通过实例演示了如何使用SerialPortTool类进行串口操作,包括打开串口、关闭串口、发送数据和接收数据等基本功能。同时,对在运行过程中可能出现的错误进行了分析,并提供了一些解决办法。
一、创建串口工具类
在开始之前,我们需要创建一个简单的Java项目来测试RXTX库。
使用Java IDE(例如:Eclipse、IntelliJ IDEA)创建一个新的Java项目。
在项目中添加RXTX库的Maven依赖。在pom.xml文件中添加以下代码:
<dependency> <groupId>com.ruoyi</groupId> <artifactId>rxtxcomm</artifactId> <version>2.2</version> <scope>system</scope> <systemPath>D:/Software/Java/jre1.8.0_231/lib/ext/RXTXcomm.jar</systemPath> </dependency>
import gnu.io.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * Created by Yeats * 串口工具类 */ public class SerialPortTool { /** * slf4j 日志记录器 */ private static final Logger logger = LoggerFactory.getLogger(SerialPortTool.class); /** * 查找电脑上所有可用 com 端口 * * @return 可用端口名称列表,没有时 列表为空 */ public static final ArrayList<String> findSystemAllComPort() { /** * getPortIdentifiers:获得电脑主板当前所有可用串口 */ Enumeration<CommPortIdentifier> portList = CommPortIdentifier.getPortIdentifiers(); ArrayList<String> portNameList = new ArrayList<>(); /** * 将可用串口名添加到 List 列表 */ while (portList.hasMoreElements()) { String portName = portList.nextElement().getName();//名称如 COM1、COM2.... portNameList.add(portName); } return portNameList; } /** * 打开电脑上指定的串口 * * @param portName 端口名称,如 COM1,为 null 时,默认使用电脑中能用的端口中的第一个 * @param b 波特率(baudrate),如 9600 * @param d 数据位(datebits),如 SerialPort.DATABITS_8 = 8 * @param s 停止位(stopbits),如 SerialPort.STOPBITS_1 = 1 * @param p 校验位 (parity),如 SerialPort.PARITY_NONE = 0 * @return 打开的串口对象,打开失败时,返回 null */ public static final SerialPort openComPort(String portName, int b, int d, int s, int p) { CommPort commPort = null; try { //当没有传入可用的 com 口时,默认使用电脑中可用的 com 口中的第一个 if (portName == null || "".equals(portName)) { List<String> comPortList = findSystemAllComPort(); if (comPortList != null && comPortList.size() > 0) { portName = comPortList.get(0); } } logger.info("开始打开串口:portName=" + portName + ",baudrate=" + b + ",datebits=" + d + ",stopbits=" + s + ",parity=" + p); //通过端口名称识别指定 COM 端口 CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName); /** * open(String TheOwner, int i):打开端口 * TheOwner 自定义一个端口名称,随便自定义即可 * i:打开的端口的超时时间,单位毫秒,超时则抛出异常:PortInUseException if in use. * 如果此时串口已经被占用,则抛出异常:gnu.io.PortInUseException: Unknown Application */ commPort = portIdentifier.open(portName, 5000); /** * 判断端口是不是串口 * public abstract class SerialPort extends CommPort */ if (commPort instanceof SerialPort) { SerialPort serialPort = (SerialPort) commPort; /** * 设置串口参数:setSerialPortParams( int b, int d, int s, int p ) * b:波特率(baudrate) * d:数据位(datebits),SerialPort 支持 5,6,7,8 * s:停止位(stopbits),SerialPort 支持 1,2,3 * p:校验位 (parity),SerialPort 支持 0,1,2,3,4 * 如果参数设置错误,则抛出异常:gnu.io.UnsupportedCommOperationException: Invalid Parameter * 此时必须关闭串口,否则下次 portIdentifier.open 时会打不开串口,因为已经被占用 */ serialPort.setSerialPortParams(b, d, s, p); logger.info("打开串口 " + portName + " 成功..."); return serialPort; } else { logger.error("当前端口 " + commPort.getName() + " 不是串口..."); } } catch (NoSuchPortException e) { e.printStackTrace(); } catch (PortInUseException e) { logger.warn("串口 " + portName + " 已经被占用,请先解除占用..."); e.printStackTrace(); } catch (UnsupportedCommOperationException e) { logger.warn("串口参数设置错误,关闭串口,数据位[5-8]、停止位[1-3]、验证位[0-4]..."); e.printStackTrace(); if (commPort != null) {//此时必须关闭串口,否则下次 portIdentifier.open 时会打不开串口,因为已经被占用 commPort.close(); } } logger.error("打开串口 " + portName + " 失败..."); return null; } /** * 往串口发送数据 * * @param serialPort 串口对象 * @param orders 待发送数据 */ public static void sendDataToComPort(SerialPort serialPort, byte[] orders) { OutputStream outputStream = null; try { if (serialPort != null) { outputStream = serialPort.getOutputStream(); outputStream.write(orders); outputStream.flush(); logger.info("往串口 " + serialPort.getName() + " 发送数据:" + Arrays.toString(orders) + " 完成..."); } else { logger.error("gnu.io.SerialPort 为null,取消数据发送..."); } } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 从串口读取数据 * * @param serialPort 要读取的串口 * @return 读取的数据 */ public static byte[] getDataFromComPort(SerialPort serialPort) { InputStream inputStream = null; byte[] data = null; try { if (serialPort != null) { inputStream = serialPort.getInputStream(); // 等待数据接收完成 Thread.sleep(500); // 获取可读取的字节数 int availableBytes = inputStream.available(); if (availableBytes > 0) { data = new byte[availableBytes]; int readBytes = inputStream.read(data); logger.info("从串口 " + serialPort.getName() + " 接收到数据:" + Arrays.toString(data) + " 完成..."); } else { logger.warn("从串口 " + serialPort.getName() + " 接收到空数据..."); } } else { logger.error("gnu.io.SerialPort 为null,取消数据接收..."); } } catch (IOException | InterruptedException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return data; } /** * 关闭串口 * * @param serialPort 待关闭的串口对象 */ public static void closeComPort(SerialPort serialPort) { if (serialPort != null) { serialPort.close(); logger.info("关闭串口 " + serialPort.getName()); } } /** * 16进制字符串转十进制字节数组 * 这是常用的方法,如某些硬件的通信指令就是提供的16进制字符串,发送时需要转为字节数组再进行发送 * * @param strSource 16进制字符串,如 "455A432F5600",每两位对应字节数组中的一个10进制元素 * 默认会去除参数字符串中的空格,所以参数 "45 5A 43 2F 56 00" 也是可以的 * @return 十进制字节数组, 如 [69, 90, 67, 47, 86, 0] */ public static byte[] hexString2Bytes(String strSource) { if (strSource == null || "".equals(strSource.trim())) { System.out.println("hexString2Bytes 参数为空,放弃转换."); return null; } strSource = strSource.replace(" ", ""); int l = strSource.length() / 2; byte[] ret = new byte[l]; for (int i = 0; i < l; i++) { ret[i] = Integer.valueOf(strSource.substring(i * 2, i * 2 + 2), 16).byteValue(); } return ret; } /** * 给串口设置监听 * * @param serialPort serialPort 要读取的串口 * @param listener SerialPortEventListener监听对象 * @throws TooManyListenersException 监听对象太多 */ public static void setListenerToSerialPort(SerialPort serialPort, SerialPortEventListener listener) throws TooManyListenersException { //给串口添加事件监听 serialPort.addEventListener(listener); //串口有数据监听 serialPort.notifyOnDataAvailable(true); //中断事件监听 serialPort.notifyOnBreakInterrupt(true); } }
二、串口工具测试
如果电脑有串口,可以直接使用串口线连接硬件使用,如果没有串口,可以使用虚拟串口工具。
Virtual Serial Port Driver是一款虚拟串口工具,简称为VSPD,VSPD官方安装指南:https://www.virtual-serial-port.org/user-guides/standard/installation.html
public static void main(String[] args) throws TooManyListenersException, NoSuchPortException, PortInUseException, UnsupportedCommOperationException { // 获得系统端口列表 SerialPortTool.findSystemAllComPort(); // 开启端口COM5,波特率9600,数据位8,停止位1,校验位0 SerialPort serialPort = SerialPortTool.openComPort("COM5", 9600, 8, 1, 0); // 定时发送数据 ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { //发送数据 SerialPortTool.sendDataToComPort(serialPort, "A1".getBytes()); } }, 0, 5, TimeUnit.SECONDS); // 设置监听 SerialPortTool.setListenerToSerialPort(serialPort, new SerialPortEventListener() { // 串口事件监听 @Override public void serialEvent(SerialPortEvent arg0) { // 数据接收事件 if (arg0.getEventType() == SerialPortEvent.DATA_AVAILABLE) { // 获取接收到的数据 byte[] bytes = SerialPortTool.getDataFromComPort(serialPort); System.out.println("收到的数据长度:" + bytes.length); System.out.println("收到的数据:" + new String(bytes)); } } }); }
这里输入A1时,实际上是将字符’A’和字符’1’转换成了ASCII码,分别为65和49,因此发送的数据是[65, 49]。
接收到的数据是从串口接收到的字节数据,接收到的数据是[65, 49],然后转换成字符A1。
三、运行时会遇到的错误
如果在运行时遇到以下错误
JVM崩溃
如果用高版本的JDK使用在使用RXTX接收串口消息时会出现的错误
# # A fatal error has been detected by the Java Runtime Environment: # # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000180004465, pid=6856, tid=0x00000000000061dc # # JRE version: Java(TM) SE Runtime Environment (8.0_361) (build 1.8.0_361-b09) # Java VM: Java HotSpot(TM) 64-Bit Server VM (25.361-b09 mixed mode windows-amd64 compressed oops) # Problematic frame: # C [rxtxSerial.dll+0x4465] # # Failed to write core dump. Minidumps are not enabled by default on client versions of Windows # # An error report file with more information is saved as: # D:\Project\hs_err_pid6856.log # # If you would like to submit a bug report, please visit: # http://bugreport.java.com/bugreport/crash.jsp # The crash happened outside the Java Virtual Machine in native code. # See problematic frame for where to report the bug. #
从错误消息中,我们可以看到错误类型为EXCEPTION_ACCESS_VIOLATION (0xc0000005),表示Java虚拟机试图访问受限制的内存区域。这是Java程序崩溃的一种常见原因,通常是由于内存泄漏、缓冲区溢出或其他与内存管理相关的错误引起的。
要解决这个问题,我们需要先找出崩溃的原因。从错误消息中可以看出,崩溃发生在rxtxSerial.dll文件的第0x4465行,代码存在一个缓冲区溢出漏洞。当程序执行到这一行代码时,它会尝试写入更多的数据到缓冲区,但缓冲区已经满了。这会导致程序崩溃,并显示上述错误消息。
在控制台中输入java -version
即可查看Java版本号
这里建议使用低版本的JDK8是jdk-8u231
,下载地址:https://www.oracle.com/cn/java/technologies/javase/javase8u211-later-archive-downloads.html
无法找到指定的类
Exception in thread "main" java.lang.NoClassDefFoundError: gnu/io/SerialPortEventListener
java.lang.NoClassDefFoundError 是一个运行时异常,表示程序在运行时无法找到指定的类。程序无法找到 gnu/io/SerialPortEventListener 这个类,需要确保您的项目中包含了这个类。
或者
java.lang.UnsatisfiedLinkError no rxtxSerial in java.library.path
java.lang.UnsatisfiedLinkError 是一个运行时异常,表示程序在运行时无法找到指定的类或动态链接库(DLL)。在这个例子中,程序无法找到 no rxtxSerial 这个类。
在IDEA的Project Structure中,确保你的正确安装了我推荐的低版本的JDK
并且在JDK中Classpath加入了RXTXcomm.jar包,
同时为了确保RXTX中的DLL(动态链接库)文件能使用,不仅放在jre/bin里边,还需放在C:\Windows\System32中