Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类

在之前的文章中,我们讨论了使用单例模式的SerialPortEventListener类。然而,这种模式在某些情况下并不理想,因为它会导致重复创建监听器,从而无法正确获取串口返回的数据。那么,如何实现SerialPortEventListener的复用呢?

首先,我们需要了解什么是SerialPortEventListener类。SerialPortEventListener是一个用于监听串口事件的类,可以接收串口事件通知,并在事件发生时执行相应的操作。例如,当有数据可读时,它可以帮助我们进行数据缓存和处理。

setListenerToSerialPort函数用于建立监听,前端使用一个定时器不断地请求receiveDataTest()来获取数据,而后端则不断返回数据。但是,这个方法有一个问题,就是后端会重复创建监听器,导致每次都拿不到一个监听器的数据。

一、失败方案

原本获取串口返回的信息是这样写的:

串口监听工具

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);
    }
}

Controller层

@GetMapping("/open")
    public String openSerialPort(@RequestParam() String port) throws NoSuchPortException, PortInUseException, UnsupportedCommOperationException, TooManyListenersException {
        port = "COM" + port;
        log.debug(port);
        SerialPort serialPort = remoteService.openSerialPortTest(port);
        return "Serial Port Opened";
    }
    @PostMapping("/send")
    public String sendData(@RequestBody String data) throws TooManyListenersException, UnsupportedCommOperationException, NoSuchPortException, PortInUseException {
        remoteService.sendData(data);
        return "Data Sent";
    }
    @GetMapping("/receive")
    public String receiveData() throws TooManyListenersException {
        return remoteService.getCurrentListener().getData();
    }
    @GetMapping("/close")
    public String closeSerialPort() throws TooManyListenersException, NoSuchPortException, PortInUseException, UnsupportedCommOperationException {
        remoteService.closeSerialPort();
        return "Serial Port Closed";
    }

MySerialPortEventListener

public class MySerialPortEventListener implements SerialPortEventListener {
    private RedisCache redisCache;
    private Queue<String> dataQueue = new LinkedList<>();
    private String latestData = "";
    public MySerialPortEventListener(SerialPort serialPort, int eventType) throws TooManyListenersException {
        this.serialPort = serialPort;
        this.eventType = eventType;
        this.redisCache = new RedisCache();
        //给串口添加事件监听
        serialPort.addEventListener(this);
        //串口有数据监听
        serialPort.notifyOnDataAvailable(true);
        //中断事件监听
        serialPort.notifyOnBreakInterrupt(true);
    }
    private SerialPort serialPort;
    
    private int eventType;
    private static ConcurrentHashMap<Long, MySerialPortEventListener> instanceMap = new ConcurrentHashMap<>();
    public static synchronized MySerialPortEventListener getInstance(SerialPort serialPort, int eventType) throws TooManyListenersException {
        long threadId = Thread.currentThread().getId();
        MySerialPortEventListener instance = instanceMap.get(threadId);
        if (instance == null) {
            instance = new MySerialPortEventListener(serialPort, eventType);
            instanceMap.put(threadId, instance);
        }
        return instance;
    }
    public static ConcurrentHashMap<Long, MySerialPortEventListener> getInstanceMap() {
        return instanceMap;
    }
    @Override
    public void serialEvent(SerialPortEvent arg0) {
        if (arg0.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
            // 数据通知
            byte[] bytes = SerialPortTool.getDataFromComPort(serialPort);
            System.out.println("收到的数据长度:" + bytes.length);
            String str = new String(bytes);
            latestData = str;
            System.out.println("收到的数据:" + str);
            eventType = arg0.getEventType(); // 将事件类型赋值给eventType
        }
    }
    public String getData() {
        if (latestData != null) {
            String data = latestData;
            latestData = null; // 清空最新数据,避免重复获取
            return data;
        } else {
            return "";
        }
    }
    public int getEventType() {
        return eventType;
    }
    public SerialPort getSerialPort() {
        return serialPort;
    }
}

impl

private SerialPort serialPort;
@Override
    public SerialPort openSerialPort(String port) {
        serialPort = SerialPortTool.openComPort(port, 9600, 8, 1, 0);
        // 创建新线程来处理串口的打开操作
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    MySerialPortEventListener listener = MySerialPortEventListener.getInstance(serialPort, 1);
                } catch (TooManyListenersException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        return serialPort;
    }
    @Override
    public void sendData(String senData) {
        String data = senData;
        // 获取当前线程的MySerialPortEventListener实例
        MySerialPortEventListener listener = getCurrentListener();
        if (listener != null) {
            SerialPortTool.sendDataToComPort(listener.getSerialPort(), data.getBytes());
        }
    }
    @Override
    public SerialPort getSerialPort() {
        // 获取当前线程的MySerialPortEventListener实例
        MySerialPortEventListener listener = getCurrentListener();
        if (listener != null) {
            return listener.getSerialPort();
        } else {
            return null;
        }
    }
    @Override
    // 获取当前线程的MySerialPortEventListener实例
    public MySerialPortEventListener getCurrentListener() {
        long threadId = Thread.currentThread().getId();
        MySerialPortEventListener listener = MySerialPortEventListener.getInstanceMap().get(threadId);
        return listener;
    }
    // 关闭串口测试
    @Override
    public void closeSerialPort() {
        SerialPortTool.closeComPort(serialPort);
    }

现在我把发送数据,设置监听,获取数据单独拿出来了,但是获取不到数据了,我已经把listener对象作为一个成员变量保存在RemoteService类中了。

问题是每次请求数据时都使用不到同一个SerialPortEventListener对象,所以拿不到串口返回的数据

if (arg0.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
                    // 数据通知
                    byte[] bytes = SerialPortTool.getDataFromComPort(serialPort);
                    System.out.println("收到的数据长度:" + bytes.length);
                    System.out.println("收到的数据:" + new String(bytes));

因为 getCurrentListener() 方法返回了 null,导致在 receiveDataTest() 方法中调用 getData() 方法时出现了 NullPointerException。这可能是因为你没有在第二个用户访问 COM6 时创建一个新的 MySerialPortEventListener 实例,而是继续使用了第一个用户的实例,导致数据被覆盖或丢失。

二、成功方案

在这个示例中,我们在后端定义了一个MySerialPortEventListener类,每个用户访问串口时创建一个新的 MySerialPortEventListener 实例,并将其存储在一个 Map 中,以便在后续的请求中使用。这样每个用户都有自己的监听器实例,互不干扰。

串口监听工具

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;
/**
 *
 * 串口工具类
 */
public class SerialPortTool {
    private static final Logger logger = LoggerFactory.getLogger(SerialPortTool.class);//slf4j 日志记录器
    /**
     * 查找电脑上所有可用 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() + " 不是串口...");
                return null;
            }
        } 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     待发送数据
     * @return
     */
    public static String 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) + " 完成");
                return "往串口 " + serialPort.getName() + " 发送数据:" + Arrays.toString(orders) + " 完成";
            } else {
                logger.error("gnu.io.SerialPort 为null,取消数据发送...");
                return "串口为空,取消数据发送";
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
    /**
     * 向串口发送数据
     */
    public static String sendDataToPort(SerialPort serialPort, byte[] data) {
        OutputStream outputStream = null;
        try {
            if (serialPort != null) {
                outputStream = serialPort.getOutputStream();
                outputStream.write(data);
                outputStream.flush();
                logger.info("Successfully sent data to serial port " + serialPort.getName() + ": " + Arrays.toString(data));
                return "Successfully sent data to serial port " + serialPort.getName();
            } else {
                logger.error("Failed to send data to serial port: serial port is null");
                return null;
            }
        } catch (IOException e) {
            logger.error("Failed to send data to serial port " + serialPort.getName() + ": " + e.getMessage());
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    logger.error("Failed to close output stream for serial port " + serialPort.getName() + ": " + e.getMessage());
                }
            }
        }
        return null;
    }
    /**
     * 从串口获取数据
     */
    public static byte[] getDataFromPort(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 closeSerialPort(SerialPort serialPort) {
        if (serialPort != null) {
            serialPort.close();
            logger.info("Successfully closed serial port " + serialPort.getName());
        } else {
            logger.error("Failed to close serial port: serial port is null");
        }
    }
    /**
     * 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;
    }
    /**
     * 给串口设置监听
     */
    public static void setListenerToSerialPort(SerialPort serialPort, SerialPortEventListener listener) throws TooManyListenersException {
//        serialPort.removeEventListener();
        //给串口添加事件监听
        serialPort.addEventListener(listener);
        //串口有数据监听
        serialPort.notifyOnDataAvailable(true);
        //中断事件监听
        serialPort.notifyOnBreakInterrupt(true);
    }
}

Controller层

@Slf4j
@RestController
@RequestMapping("/remote/remote")
public class RemoteController {
    @Autowired
    private IRemoteService remoteService;
    /**
     * 打开串口
     */
    @GetMapping("/open")
    public AjaxResult openSerialPort(@RequestParam() String port) {
        String portName = "COM" + port;
        try {
            SerialPort serialPort = remoteService.openSerialPort(portName);
            if (serialPort == null) {
                return AjaxResult.error("打开串口失败");
            }
            return AjaxResult.success("打开串口成功");
        } catch (Exception e) {
            return AjaxResult.error("打开串口失败: " + e.getMessage());
        }
    }
    /**
     * 向串口发送数据
     */
    @GetMapping("/send")
    public AjaxResult sendDataToPort(@RequestParam() String data, @RequestParam() String port) throws TooManyListenersException, UnsupportedCommOperationException, NoSuchPortException, PortInUseException {
        String portName = "COM" + port;
        try {
            String result = remoteService.sendDataToPort(data, portName);
            if (result != null) {
                return AjaxResult.success("发送数据成功");
            } else {
                return AjaxResult.error("发送数据失败");
            }
        } catch (Exception e) {
            return AjaxResult.error("发送数据失败: " + e.getMessage());
        }
    }
    /**
     * 从串口接收数据
     */
    @GetMapping("/receive")
    public AjaxResult receiveDataFromPort(@RequestParam() String port) {
        String portName = "COM" + port;
        try {
            String data = remoteService.receiveDataFromPort(portName);
            return AjaxResult.success(data);
        } catch (Exception e) {
            return AjaxResult.error("接收串口数据失败: " + e.getMessage());
        }
    }
    /**
     * 关闭串口
     */
    @GetMapping("/close")
    public AjaxResult closeSerialPort(@RequestParam() String port) {
        String portName = "COM" + port;
        try {
            remoteService.closeSerialPort(portName);
            return AjaxResult.success("关闭串口成功");
        } catch (Exception e) {
            return AjaxResult.warn(e.getMessage());
        }
    }
}

MySerialPortEventListener

package com.ruoyi.remote.burn;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.TooManyListenersException;
/**
 * 串口监听器
 *
 * @author Yeats
 */
public class MySerialPortEventListener implements SerialPortEventListener {
    private String latestData = "";
    private SerialPort serialPort;
    private int eventType;
    private static final Logger logger = LoggerFactory.getLogger(SerialPortTool.class);//slf4j 日志记录器
    /**
     * 构造串口监听器
     *
     * @param serialPort
     * @param eventType
     * @throws TooManyListenersException
     */
    public MySerialPortEventListener(SerialPort serialPort, int eventType) throws TooManyListenersException {
        this.serialPort = serialPort;
        this.eventType = eventType;
        //给串口添加事件监听
        serialPort.addEventListener(this);
        //串口有数据监听
        serialPort.notifyOnDataAvailable(true);
        //中断事件监听
        serialPort.notifyOnBreakInterrupt(true);
    }
    /**
     * 串口监听事件
     *
     * @param arg0
     * @throws TooManyListenersException
     */
    @Override
    public void serialEvent(SerialPortEvent arg0) {
        if (arg0.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
            // 数据通知
            byte[] bytes = SerialPortTool.getDataFromPort(serialPort);
            String str = new String(bytes);
            latestData = str;
            eventType = arg0.getEventType(); // 将事件类型赋值给eventType
            logger.info("收到的数据长度:" + bytes.length);
            logger.info("收到的数据:" + str);
        }
    }
    /**
     * 获取最新数据
     *
     * @return 最新数据
     */
    public String getData() {
        if (latestData != null) {
            // 清空最新数据,避免重复获取
            String data = " " + latestData;
            latestData = null;
            return data;
        } else {
            return "";
        }
    }
    /**
     * 获取串口对象
     *
     * @return 串口对象
     */
    public SerialPort getSerialPort() {
        return serialPort;
    }
}

impl

@Service
public class RemoteServiceImpl implements IRemoteService {
    private static final Logger logger = LoggerFactory.getLogger(SerialPortTool.class);//slf4j 日志记录器
    /**
     * 查找电脑上所有可用 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;
    }
    
  private Map<String, MySerialPortEventListener> listenerMap = new HashMap<>();
    /**
     * 打开串口
     */
    @Override
    public SerialPort openSerialPort(String portName) {
        SerialPort serialPort = SerialPortTool.openComPort(portName, 9600, 8, 1, 0);
        if (serialPort == null) {
            throw new RuntimeException("打开串口 " + portName + " 失败");
        }
        // 创建新线程来处理串口的打开操作
        Thread thread = new Thread(() -> {
            try {
                MySerialPortEventListener listener = new MySerialPortEventListener(serialPort, 1);
                listenerMap.put(portName, listener);
            } catch (TooManyListenersException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        return serialPort;
    }
    /**
     * 向串口发送数据
     */
    @Override
    public String sendDataToPort(String senData, String portName) {
        MySerialPortEventListener listener = listenerMap.get(portName);
        if (listener != null) {
            return SerialPortTool.sendDataToPort(listener.getSerialPort(), senData.getBytes());
        } else {
            throw new RuntimeException("发送串口失败,未找到该串口");
        }
    }
    /**
     * 从串口接收数据
     */
    @Override
    public String receiveDataFromPort(String portName) throws RuntimeException {
        MySerialPortEventListener listener = listenerMap.get(portName);
        if (listener != null) {
            return listener.getData();
        } else {
            return "";
        }
    }
    /**
     * 关闭串口
     */
    @Override
    public void closeSerialPort(String portName) throws RuntimeException {
        MySerialPortEventListener listener = listenerMap.get(portName);
        if (listener != null) {
            SerialPortTool.closeSerialPort(listener.getSerialPort());
            listenerMap.remove(portName);
        } else {
            throw new RuntimeException("当前串口已关闭");
        }
    }
}
import gnu.io.SerialPort;
public interface IRemoteService {
    /**
     * 打开串口
     */
    public SerialPort openSerialPort(String portName);
    /**
     * 向串口发送数据
     */
    public String sendDataToPort(String senData, String portName);
    /**
     * 从串口接收数据
     */
    public String receiveDataFromPort(String portName);
    /**
     * 关闭串口
     */
    public void closeSerialPort(String portName);
}

前端Api

// 发送数据到串口
export function sendDataToPort(data) {
  return request({
    url: '/remote/remote/send',
    method: 'get',
    params: data,
  })
}
// 接收数据从串口
export function receiveDataFromPort(data) {
  return request({
    url: '/remote/remote/receive',
    method: 'get',
    params: data,
  })
}
// 打开串口
export function openSerialPort(data) {
  return request({
    url: '/remote/remote/open',
    method: 'get',
    params: data,
  })
}
// 关闭串口
export function closeSerialPort(data) {
  return request({
    url: '/remote/remote/close',
    method: 'get',
    params: data,
  })
}

vue前端

data() {
        return {
            sendDataString: "",
            receivedDataString: "",
            timerId: null,
        };
    },
 methods: {
        // 在openSerialPort方法中调用后端接口
        openSerialPort() {
            let serialPort = {
                port: this.port51
            };
            // 调用后端接口打开串口
            openSerialPort(serialPort).then((response) => {
                // 如果成功打开串口,则开启定时器
                if (response.code == 200) {
                    this.$message.success(response.msg);
                    this.startTimer();
                } else {
                    this.$message.error(response.msg);
                }
            });
        },
        // 关闭串口
        closeSerialPort() {
            let serialPort = {
                port: this.port51
            };
            this.stopTimer();
            closeSerialPort(serialPort).then((response) => {
                if (response.code == 200) {
                    this.sendDataString = "";
                    this.receivedDataString = "";
                    this.$message.success(response.msg);
                } else {
                    this.$message.error(response.msg);
                }
            });
        },
        
        // 清空接收数据
        clearReceive() {
            this.receivedDataString = "";
        },
        
        // 发送数据
        sendDataToPort() {
            this.isSending = true;
            if (!this.sendDataString || this.sendDataString.trim().length === 0) {
                this.$message.warning("发送数据不能为空");
                this.isSending = false;
            } else {
                let serialPort = {
                    data: this.sendDataString,
                    port: this.port51
                };
                sendDataToPort(serialPort).then((response) => {
                    if (response.code == 200) {
                        this.isSending = false;
                        this.$message.success(response.msg);
                    } else {
                        this.isSending = false;
                        this.$message.error(response.msg);
                    }
                });
            }
        },
        // 定义一个函数,用于开启定时器
        startTimer() {
            // 如果定时器已经开启,则直接返回
            if (this.timerId) {
                return;
            }
            let serialPort = {
                port: this.port51
            };
            // 开启定时器,每隔100毫秒请求一次数据
            this.timerId = setInterval(() => {
                receiveDataFromPort(serialPort).then((response) => {
                    if (response.code == 200) {
                        // 将接收到的数据存储到全局变量中
                        this.receivedDataString += response.msg;
                        this.isOpenPort = true;
                        this.isOpening = false;
                    } else {
                        this.$message.error(response.msg);
                    }
                });
            }, 1000);
        },
        // 定义一个函数,用于停止定时器
        stopTimer() {
            // 如果定时器已经停止,则直接返回
            if (!this.timerId) {
                return;
            }
            clearInterval(this.timerId);
            this.timerId = null;
        },
        // 在beforeDestroy生命周期钩子中清除定时器
        beforeDestroy() {
            this.closeSerialPort();
        },
    },


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
16天前
|
数据采集 Java API
Jsoup库能处理多线程下载吗?
Jsoup库能处理多线程下载吗?
|
18天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
2月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
63 4
|
2月前
|
监控 安全 算法
线程死循环确实是多线程编程中的一个常见问题,它可能导致应用程序性能下降,甚至使整个系统变得不稳定。
线程死循环是多线程编程中常见的问题,可能导致性能下降或系统不稳定。通过代码审查、静态分析、日志监控、设置超时、使用锁机制、测试、选择线程安全的数据结构、限制线程数、使用现代并发库及培训,可有效预防和解决死循环问题。
73 1
|
2月前
|
监控 安全 算法
线程死循环是多线程编程中的常见问题,可能导致应用性能下降甚至系统不稳定。
【10月更文挑战第6天】线程死循环是多线程编程中的常见问题,可能导致应用性能下降甚至系统不稳定。为了解决这一问题,可以通过代码审查、静态分析、添加日志监控、设置超时机制、使用锁和同步机制、进行全面测试、选用线程安全的数据结构、限制线程数量、利用现代并发库,并对团队进行培训等方法来预防和减少死循环的发生。尽管如此,多线程编程的复杂性仍需要持续监控和维护以确保系统稳定。
65 3
|
3月前
|
缓存 Java 应用服务中间件
Java虚拟线程探究与性能解析
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
128 23
|
2月前
|
网络协议 安全 Java
难懂,误点!将多线程技术应用于Python的异步事件循环
难懂,误点!将多线程技术应用于Python的异步事件循环
81 0
|
3月前
|
Java Android开发 开发者
安卓应用开发中的线程管理优化技巧
【9月更文挑战第10天】在安卓开发的海洋里,线程管理犹如航行的风帆,掌握好它,能让应用乘风破浪,反之则可能遭遇性能的暗礁。本文将通过浅显易懂的语言和生动的比喻,带你探索如何优雅地处理安卓中的线程问题,从基础的线程创建到高级的线程池运用,让你的应用运行更加流畅。
|
3月前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
|
3月前
|
监控 Java
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
159 3