You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于JSSC实现多COM端口并发读取与聚合输出技术问询

解决多串口数据聚合时的丢包问题

看来你遇到了多串口数据聚合时的丢包坑——循环轮询端口的方式本质是阻塞式的,你盯着一个端口读数据时,其他端口的 incoming 数据要么被串口缓冲区溢出丢弃,要么因为没及时读取而被后续数据覆盖。结合你用 JSSC 做串口操作的场景,给你几个落地可行的解决方案:


1. 给每个输入串口分配独立异步读取线程

这是最直接的破局思路:给4个4800波特率、1个34800波特率的输入端口各开一个专属读取线程,每个线程只负责监听自己的串口数据。当数据到来时,线程把完整的NEMA帧存入线程安全的队列,再由输出端口的线程统一从队列取数据发送。

核心实现要点:

  • 改造RS232Control类,给每个串口实例绑定一个读取线程,用JSSC自带的SerialPortEventListener做事件驱动监听(比主动轮询高效得多)。
  • 必须处理粘包/半包问题:串口数据可能分多次到达,比如一次只收到帧的一部分,所以每个串口要维护一个缓冲区,把收到的字节拼接起来,直到提取出以$开头、CR/LF结尾的完整NEMA帧,再加入队列。

示例伪代码片段:

public class RS232Control {
    private SerialPort serialPort;
    private BlockingQueue<String> dataQueue;
    private StringBuilder frameBuffer = new StringBuilder(); // 缓存半包数据

    public RS232Control(String portName, int baudRate, BlockingQueue<String> queue) throws SerialPortException {
        this.dataQueue = queue;
        serialPort = new SerialPort(portName);
        serialPort.openPort();
        serialPort.setParams(baudRate, 8, 1, 0);
        
        // 注册串口数据监听事件
        serialPort.addEventListener(event -> {
            if (event.isRXCHAR() && event.getEventValue() > 0) {
                try {
                    byte[] receivedBytes = serialPort.readBytes();
                    frameBuffer.append(new String(receivedBytes));
                    // 提取所有完整的NEMA帧
                    extractCompleteFrames();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, SerialPort.MASK_RXCHAR);
    }

    private void extractCompleteFrames() {
        String bufferContent = frameBuffer.toString();
        int endIndex;
        while ((endIndex = bufferContent.indexOf("\r\n")) != -1) {
            String completeFrame = bufferContent.substring(0, endIndex + 2);
            if (completeFrame.startsWith("$")) { // 校验NEMA帧格式
                dataQueue.put(completeFrame);
            }
            // 保留剩余的半包数据
            frameBuffer = new StringBuilder(bufferContent.substring(endIndex + 2));
            bufferContent = frameBuffer.toString();
        }
    }
}

2. 输出端口用独立线程做队列消费

输出端口(192k波特率)单独开一个线程,从共享的线程安全队列里取数据并发送。因为输入总带宽(4*4800 + 34800 = 54k)远小于输出带宽(192k),理论上不会出现消费跟不上生产的情况,但要做好队列的边界处理。

示例输出线程代码:

public class OutputPortSender implements Runnable {
    private final RS232Control outputPort;
    private final BlockingQueue<String> dataQueue;

    public OutputPortSender(RS232Control outputPort, BlockingQueue<String> dataQueue) {
        this.outputPort = outputPort;
        this.dataQueue = dataQueue;
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                String frame = dataQueue.take(); // 队列为空时自动阻塞等待
                outputPort.sendData(frame.getBytes());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 优雅终止线程
            } catch (SerialPortException e) {
                e.printStackTrace();
            }
        }
    }
}

3. 额外优化建议

  • 队列容量限制:给LinkedBlockingQueue设置固定容量,当队列满时,可选择丢弃最旧的帧(用offer方法,失败时移除队首再尝试),避免内存溢出。
  • 资源优雅释放:程序退出时,要关闭所有串口、中断所有线程,比如添加JVM shutdown hook。
  • 帧校验增强:除了开头结尾的校验,可选择性验证NEMA帧的校验和(如果你的数据源支持),进一步过滤无效数据。

这种异步事件驱动+线程安全队列的模式,能让每个串口的数据都被及时捕获和处理,彻底解决轮询切换导致的丢包问题。你可以基于现有的RS232Control、NEMA_aggregrator类做改造,替换掉原来的循环轮询逻辑即可。

内容的提问来源于stack exchange,提问作者ian5142

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 07:35:44