Java jSerialComm库串口通信(USB RS-485/232) 查询/应答、主动上报模式

Java jSerialComm库串口通信(USB RS-485/232) 查询/应答、主动上报模式

查询/应答模式

要在Java中通过USB RS-485接口发送和接收特定的数据帧,你需要利用适当的串行通信库。在Java中,一个常见的选择是使用RXTX或jSerialComm库。这些库允许Java应用程序与串行端口进行通信。

以下是通信过程的步骤:

1. 添加串行通信库依赖

如果你选择使用jSerialComm库,可以在你的Maven pom.xml 文件中添加以下依赖:

<dependency>
    <groupId>com.fazecastgroupId>
    <artifactId>jSerialCommartifactId>
    <version>2.6.2version>
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)。以下是关于这两种方法的说明:

  1. 轮询模式(Polling)
    • 在轮询模式下,程序会周期性地(通常使用循环)检查串口是否有可用数据。
    • 使用一个循环来检查串口COM3是否有可用数据,如果有数据,则读取并处理数据。
    • 这种方式比较简单,但可能会造成CPU的浪费,因为程序会不断地检查串口,即使没有数据到达。
  2. 事件监听模式(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 串口缓冲区

串口缓冲区通常由串口设备的驱动程序和操作系统共同管理,它们在计算机系统中的位置是软件实现的。

具体来说,串口缓冲区通常包括两个部分:

  1. 硬件缓冲区:这部分是串口硬件上的缓冲区,用于存储从外部串口接收到的数据和将要发送的数据。串口硬件上的缓冲区大小是有限的,通常是几个字节到数十个字节不等,具体取决于串口设备的规格和型号。硬件缓冲区的大小是固定的,不可更改。
  2. 操作系统缓冲区:这部分缓冲区位于操作系统内核中,用于管理串口数据的传输。当数据从串口硬件传输到计算机时,操作系统会将数据从硬件缓冲区复制到操作系统缓冲区中,然后提供给应用程序进行读取。同样,当应用程序要发送数据时,数据首先被写入操作系统缓冲区,然后由操作系统传输到串口硬件。

应用程序通过串口API(如Java中的javax.comm或其他串口库)与操作系统交互,操作系统负责管理硬件缓冲区和数据传输。

因此,串口缓冲区的管理是由操作系统和串口驱动程序协同工作的结果,它们确保数据能够以可靠的方式在计算机和串口设备之间传输。

你可能感兴趣的:(编程技巧,java,开发语言,rs485,串口通信,rxtx)