Java串口通信技术探究2:RXTX库单例测试及应用

news2024/9/25 19:25:30

目录

  • 一、创建串口工具类
  • 二、串口工具测试
  • 三、运行时会遇到的错误
    • JVM崩溃
    • 无法找到指定的类

本文主要介绍了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中

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1438892.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL篇----第十一篇

系列文章目录 文章目录 系列文章目录前言一、BLOB 和 TEXT 有什么区别?二、MySQL_fetch_array 和 MySQL_fetch_object 的区别是什么?三、MyISAM 表格将在哪里存储,并且还提供其存储格式?四、MySQL 如何优化 DISTINCT?五、如何显示前 50 行?前言 前些天发现了一个巨牛的人…

ProtonMail邮箱怎么样?国内有什么替代品?

ProtonMail作为业界知名的加密邮箱提供者&#xff0c;其安全性、隐私保护等特性让不少追求私密通信的用户趋之若鹜。然而对于国内用户而言&#xff0c;ProtonMail可能并非最佳选择&#xff0c;受限于许多因素&#xff0c;从语言支持到服务器位置再到可访问性&#xff0c;都可能…

parse库,一个优雅的python库

前言 在Python中&#xff0c;format方法和f-strings是两种常用的字符串插值方法。 name "Haige" age "18" print(f"{name} is {age} years old.")# Haige is 18 years old.而如果是要从字符串中提取期望的值呢&#xff1f;相信很多人的第一或…

tkinter绘制组件(41)——菜单按钮

tkinter绘制组件&#xff08;41&#xff09;——菜单按钮 引言布局函数结构按钮部分菜单显示完整代码函数 效果测试代码最终效果 github项目pip下载结语 引言 TinUI5的新控件&#xff0c;菜单按钮&#xff0c;menubutton。 这是一个与TinUI菜单&#xff08;menubar&#xff0…

【C++】基础知识讲解(引用、内联、auto,基于范围for循环)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;http://t.csdnimg.cn/eCa5z 目录 引用 概念 特性 使用场景 作参数 作返回值 传值、传引用效率比较 引用和指针的区别 内联函数 概念…

excel 导出 The maximum length of cell contents (text) is 32767 characters

导出excel报错。错误日志提示&#xff1a;:The maximum length of cell contents (text) is 32767 characters 排查后&#xff0c;发现poi有单元格最大长度校验&#xff0c;超过32767会报错。 解决方案&#xff1a; 通过java反射机制&#xff0c;设置单元格最大校验限制为Int…

Skywalking 应用笔记

概念 Skywalking是一款分布式的系统 性能监视工具&#xff0c;专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。SkyWalking是一款 观察性的分析平台和应用性能管理系统&#xff0c;提供了 分布式追踪、性能指标分析、应用服务依赖分析、可视化一体化等解决方…

CV | SAM在医学影像上的模型调研【20240207更新版】

本文主要是SAM&#xff08;Segment Anything&#xff09;在医学影像上的数据集&#xff0c;模型及评估方法调研【持续更新】~ 1.开源数据集 可参考这篇【数据集 | 基于计算机视觉的医学影像处理数据集_CSDN博客】 2.算法模型 2023.04_SAM 论文&#xff1a;2018.08.05v_Segm…

Win32 SDK Gui编程系列之--弹出式菜单

1.弹出式菜单 例如,在命令提示窗口中点击鼠标右键,会出现如下图所示的弹出菜单(下拉菜单)。 这种弹出式菜单的实现很简单。不创建菜单栏,用CreatePopupMenu函数创建的菜单是最顶端的菜单就可以了。 菜单的显示使用TrackPopupMenu函数进行。 例如,点击鼠标右键显示弹出…

尚硅谷 Java 基础实战—Bank 项目—实验题目 3

实验题目 修改 withdraw 方法以返回一个布尔值&#xff0c;指示交易是否成功。 实验目的 使用有返回值的方法。 提示 修改 Account 类 修改 deposit 方法返回 true&#xff08;意味所有存款是成功的&#xff09;。修改 withdraw 方法来检查提款数目是否大于余额。如果amt小…

十七、vben合并行后操作按钮如何合并

上期我们说了如何在table内部合并行,行内的内容都是字符串,那么如果是多个操作按钮呢,他们是如何合并的,事件是怎么触发的,怎么写呢。 先看效果图 数据上也是和上期一样有9条信息。 下面来看一下我们的具体实现 一、在template里面写table <BasicTable:showIndexCol…

【网络技术】【Kali Linux】Nmap 嗅探(一)简单扫描

一、实验环境 本次实验进行简单的Nmap扫描&#xff0c;实验使用 Kali Linux 虚拟机和 Ubuntu Linux 虚拟机完成&#xff0c;主机操作系统为 Windows 11&#xff0c;虚拟化平台选择 Oracle VM VirtualBox&#xff0c;如下图所示。 二、实验步骤 1、相关配置 Kali Linux 虚拟机…

【python】python实现代码雨【附源码】

欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 系列文章 1新年烟花代码https://blog.csdn.net/m0_73367097/article/details/1354817792爱心代码https://blog.csdn.net/m0_73367097/article/details/136017032 一、效果图&#xff1a; 二、准备工作 &#xff08;1…

第二证券:股市的国家队是谁?股市国家队包括哪些机构?

在a股商场上&#xff0c;投资者大致能够分为散户、游资、主力、组织、国家队这几大类&#xff0c;那么&#xff0c;股市的国家队是谁&#xff1f;股市国家队包含哪些组织&#xff1f; 国家队主要是指以下五大类&#xff1a; 1、中心汇金 中心汇金的全称为中心汇金投资有限责…

【STM32F103】PWM驱动舵机(SG90MG995)

PWM 关于如何发出PWM可以参考我之前的文章。 【STM32F103】TIM定时器&PWM-CSDN博客 SG90&MG995 以这两款舵机为例是因为我手上碰巧只有这两款舵机。不过实际上舵机的操作基本上差不了多少&#xff0c;基本上都是给频率为50Hz的PWM&#xff0c;然后就可以让舵机旋转…

详解C++类和对象(中(类的6个默认成员函数))

文章目录 写在前面1. 类的6个默认成员函数2. 构造函数2.1 构造函数的引入2.1 构造函数的特性 3. 析构函数3.1 析构函数的引入3.2 析构函数的特性 4. 拷贝构造函数4.1 拷贝构造函数概念4.2 拷贝构造函数的特性4.3 拷贝构造函数典型调用场景 5. 赋值运算符重载5.1 运算符重载5.2 …

Javaweb之SpringBootWeb案例之登录校验功能的详细解析

2. 登录校验 2.1 问题分析 我们已经完成了基础登录功能的开发与测试&#xff0c;在我们登录成功后就可以进入到后台管理系统中进行数据的操作。 但是当我们在浏览器中新的页面上输入地址&#xff1a;http://localhost:9528/#/system/dept&#xff0c;发现没有登录仍然可以进…

寻迹模块——红外循迹模式使用介绍

目录 循迹模式——红外循迹模式使用介绍 红外循迹模块介绍 接线 循迹小车原理 安装与接线 实验程序 实验效果 循迹模式——红外循迹模式使用介绍 实验效果&#xff1a; 寻迹模块-CSDN直播 红外循迹模块介绍 传感器的红外发射二极管不断发射红外线&#xff0c;当发射出…

RabbitMQ-5.消费者的可靠性

消费者的可靠性 5.消费者的可靠性5.1.消费者确认机制5.2.失败重试机制5.3.失败处理策略5.4.业务幂等性5.4.1.唯一消息ID5.4.2.业务判断 5.5.兜底方案 5.消费者的可靠性 当RabbitMQ向消费者投递消息以后&#xff0c;需要知道消费者的处理状态如何。因为消息投递给消费者并不代表…

jvm几个常见面试题整理

1. Full GC触发机制有如下5种情况。 (1)调用System.gc()时&#xff0c;系统建议执行Full GC&#xff0c;但是不必然执行。(2)老年代空间不足。(3)方法区空间不足。(4)老年代的最大可用连续空间小于历次晋升到老年代对象的平均大小就会进行Full GC。(5)由Eden区、S0(From)区向S…