Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
目录
- 一、失败方案
- 串口监听工具
- Controller层
- MySerialPortEventListener
- impl
- 二、成功方案
- 串口监听工具
- Controller层
- MySerialPortEventListener
- impl
- 前端Api
在之前的文章中,我们讨论了使用单例模式的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();
},
},