Java jSerialComm库串口通信(USB RS-485/232) 查询/应答、主动上报模式
查询/应答模式
要在Java中通过USB RS-485接口发送和接收特定的数据帧,你需要利用适当的串行通信库。在Java中,一个常见的选择是使用RXTX或jSerialComm库。这些库允许Java应用程序与串行端口进行通信。
以下是通信过程的步骤:
1. 添加串行通信库依赖
如果你选择使用jSerialComm库,可以在你的Maven pom.xml
文件中添加以下依赖:
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.6.2</version>
</dependency>
2. 串行端口通信代码
以下是一个基本的示例代码,展示如何发送和接收数据:
import com.fazecast.jSerialComm.SerialPort;
public class RS485Communication {
public static void main(String[] args) {
SerialPort serialPort = SerialPort.getCommPort("COM3"); // 替换为你的端口名
serialPort.setComPortParameters(9600, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY); // 设置端口参数
serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 1000, 0);
if (serialPort.openPort()) {
System.out.println("Port opened successfully.");
} else {
System.out.println("Unable to open the port.");
return;
}
try {
// 发送数据
byte[] writeBuffer = new byte[]{(byte) 0xFA, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, (byte) 0xCE, (byte) 0xFA};
serialPort.writeBytes(writeBuffer, writeBuffer.length);
// 接收数据
byte[] readBuffer = new byte[1024]; // 调整数组大小以适应预期的响应长度
int numRead = serialPort.readBytes(readBuffer, readBuffer.length);
System.out.println("Read " + numRead + " bytes.");
// 将读取的字节转换为十六进制字符串
StringBuilder data = new StringBuilder();
for (int i = 0; i < numRead; i++) {
data.append(String.format("%02X ", readBuffer[i]));
}
System.out.println("Received data: " + data.toString());
} catch (Exception e) {
e.printStackTrace();
} finally {
serialPort.closePort();
}
}
}
3. 数据解析
在接收到数据后,你可能需要根据你的协议解析这些数据。例如,你可能需要检查帧头、SN码、命令字等,并从数据内容中提取所需的信息。
4. 注意事项
- 确保你的USB RS-485适配器已正确安装,并且你知道它在你的系统中的端口名称(如COM3、COM4等)。
- 适当设置串行通信参数(如波特率、数据位、停止位和奇偶校验位)以匹配你的设备要求。
- 如果你的系统不是基于Windows,串行端口名称可能会有所不同(如在Linux上通常是
/dev/ttyUSB0
)。 - 异常处理对于处理通信错误和意外情况非常重要。
- 根据实际情况调整代码中缓冲区大小。
主动上报模式(监听)
1. 实现方法
一种是轮询模式(Polling),另一种是事件监听模式(Event Listener)。以下是关于这两种方法的说明:
- 轮询模式(Polling):
- 在轮询模式下,程序会周期性地(通常使用循环)检查串口是否有可用数据。
- 使用一个循环来检查串口COM3是否有可用数据,如果有数据,则读取并处理数据。
- 这种方式比较简单,但可能会造成CPU的浪费,因为程序会不断地检查串口,即使没有数据到达。
- 事件监听模式(Event Listener):
- 在事件监听模式下,程序注册了一个事件监听器(
SerialPortEventListener
),当串口有数据到达时,事件监听器会触发相应的事件。 - 使用事件监听器来监听串口CO3,当有数据到达时,事件监听器会调用
serialEvent
方法来处理数据。 - 这种方式相对更高效,因为程序只有在有数据到达时才会执行相应的处理代码,而不需要不断地轮询串口。
- 在事件监听模式下,程序注册了一个事件监听器(
根据你的应用需求,选择轮询模式还是事件监听模式都是可以的。事件监听模式通常更加高效,特别是在需要实时处理数据或需要减少CPU占用的情况下。但需要注意的是,使用事件监听模式需要注册事件监听器,并确保程序不会在数据到达前退出。
无论哪种模式,都需要确保串口保持打开状态,以便能够接收数据。串口被打开后,程序进入一个循环或事件监听状态,以便随时接收数据。如果在数据到达之前关闭串口,数据将会丢失。
2. 事件监听模式
package com.zxbd.project.task;
import com.fazecast.jSerialComm.SerialPort;
import com.fazecast.jSerialComm.SerialPortDataListener;
import com.fazecast.jSerialComm.SerialPortEvent;
import com.zxbd.project.queue.DataQueue;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.time.Instant;
import javax.annotation.PostConstruct;
@Slf4j
public class SerialPortListener {
private volatile Instant lastDataReceivedTime = Instant.now();
private final long TIMEOUT_MILLIS = 30000; // 30秒的超时时间
@PostConstruct
public void init() {
Thread serialPortListener = new Thread(this::listenerSerialPort);
serialPortListener.setName("SerialPortListener");
serialPortListener.start();
}
public void listenerSerialPort() {
// 无限循环,以便在断开后重新尝试连接
while (!Thread.currentThread().isInterrupted()) {
try {
SerialPort serialPort = SerialPort.getCommPort("COM3");
serialPort.setComPortParameters(9600, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY);
serialPort.setComPortTimeouts(SerialPort.TIMEOUT_NONBLOCKING, 1000, 0);
if (serialPort.openPort()) {
log.info("Port opened successfully.");
lastDataReceivedTime = Instant.now();
} else {
log.info("Unable to open the port. Retrying...");
Thread.sleep(5000); // 等待一段时间后重试
continue;
}
// 创建数据监听器
serialPort.addDataListener(new SerialPortDataListener() {
@Override
public int getListeningEvents() {
return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
}
@Override
public void serialEvent(SerialPortEvent event) {
try {
if (event.getEventType() == SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
byte[] readBuffer = new byte[1024];
int numRead = serialPort.readBytes(readBuffer, readBuffer.length);
if (numRead > 0) {
StringBuilder data = new StringBuilder();
for (int i = 0; i < numRead; i++) {
// 加入阻塞队列用于其他线程后续数据处理
// DataQueue.sensorDataQueue.put(String.format("%02X", readBuffer[i]));
data.append(String.format("%02X ", readBuffer[i]));
}
log.info("Received data: " + data.toString());
}
}
// 更新收到数据的时间戳
lastDataReceivedTime = Instant.now();
} catch (Exception e) {
log.error("Error in data processing: " + e.getMessage());
throw new RuntimeException("Error in data processing", e);
}
}
});
// 接收数据超时,抛出异常
while (true) {
if (Duration.between(lastDataReceivedTime, Instant.now()).toMillis() > TIMEOUT_MILLIS) {
throw new RuntimeException("No data received for over 30 seconds");
}
Thread.sleep(1000); // 每秒检查一次
}
} catch (Exception e) {
log.warn("Error: " + e.getMessage() + ". Retrying...");
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
log.error("Error in sleeping: " + ex.getMessage());
Thread.currentThread().interrupt();
}
}
}
}
}
3. 轮询模式
3.1 实现方法
为了让程序持续监听串口并输出收到的数据,可以在一个单独的线程中运行一个循环来读取串行端口。
import com.fazecast.jSerialComm.SerialPort;
public class RS485CommunicationPolling {
public static void main(String[] args) {
SerialPort serialPort = SerialPort.getCommPort("COM3"); // 替换为你的端口名
serialPort.setComPortParameters(9600, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY); // 设置端口参数
serialPort.setComPortTimeouts(SerialPort.TIMEOUT_NONBLOCKING, 0, 0);
if (serialPort.openPort()) {
System.out.println("Port opened successfully.");
} else {
System.out.println("Unable to open the port.");
return;
}
// 创建一个新线程来处理输入
Thread thread = new Thread(() -> {
while (true) {
try {
// 接收数据
byte[] readBuffer = new byte[1024]; // 调整数组大小以适应预期的响应长度
int numRead = serialPort.readBytes(readBuffer, readBuffer.length);
if (numRead > 0) {
// 将读取的字节转换为十六进制字符串
StringBuilder data = new StringBuilder();
for (int i = 0; i < numRead; i++) {
data.append(String.format("%02X ", readBuffer[i]));
}
System.out.println("Received data: " + data.toString());
// 收到数据的处理操作,例如加入阻塞队列,另一个模块消费队列解析收到的数据
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 启动线程
thread.start();
}
}
上面创建了一个无限循环的线程来读取串行端口。它会持续检查串行端口,当有数据到达时,它会读取数据,将其转换成十六进制格式的字符串,然后输出。
请注意,这样的代码会创建一个永远不会停止的线程,除非你在代码中添加了一种方法来停止它,比如检测到特定的输入或者程序关闭时。在实际应用中,你通常需要一种机制来安全地停止线程,并在不需要它时关闭串行端口。
3.2 轮询模式是否会丢失数据?
在轮询模式下,程序会周期性地使用循环来检查串口是否有可用数据。如果在轮询的间隙内有数据到达串口,这些数据通常会被串口驱动程序缓存起来,等待程序读取。
串口驱动程序通常会提供一个输入缓冲区,用于存储从串口接收到的数据。当数据到达串口时,它们会被放入这个缓冲区中,直到程序来读取它们。如果数据到达速度比程序读取速度快,那么这些数据会在缓冲区中等待。
因此,在轮询模式下,如果程序在轮询间隙内没有及时读取串口数据,已到达但尚未读取的数据会保留在串口的输入缓冲区中,等待程序的读取。程序可以随时读取这些数据,只要它们仍然存在于缓冲区中。
需要注意的是,串口的输入缓冲区大小是有限的,如果数据到达速度非常快,缓冲区可能会被填满,导致后续到达的数据丢失。因此,程序应该以足够快的速度读取串口数据,以避免数据丢失。如果需要处理大量数据或数据到达速度非常快,可以考虑使用事件监听模式,以便在数据到达时立即处理,而不是周期性地轮询。这可以提高数据的实时性。
3.3 串口缓冲区
串口缓冲区通常由串口设备的驱动程序和操作系统共同管理,它们在计算机系统中的位置是软件实现的。
具体来说,串口缓冲区通常包括两个部分:
- 硬件缓冲区:这部分是串口硬件上的缓冲区,用于存储从外部串口接收到的数据和将要发送的数据。串口硬件上的缓冲区大小是有限的,通常是几个字节到数十个字节不等,具体取决于串口设备的规格和型号。硬件缓冲区的大小是固定的,不可更改。
- 操作系统缓冲区:这部分缓冲区位于操作系统内核中,用于管理串口数据的传输。当数据从串口硬件传输到计算机时,操作系统会将数据从硬件缓冲区复制到操作系统缓冲区中,然后提供给应用程序进行读取。同样,当应用程序要发送数据时,数据首先被写入操作系统缓冲区,然后由操作系统传输到串口硬件。
应用程序通过串口API(如Java中的javax.comm或其他串口库)与操作系统交互,操作系统负责管理硬件缓冲区和数据传输。
因此,串口缓冲区的管理是由操作系统和串口驱动程序协同工作的结果,它们确保数据能够以可靠的方式在计算机和串口设备之间传输。