基于Python的Qt开发之Pyside6 串口接收数据被分割的解决方案

在串口通信中,数据被分割接收是一个常见问题,主要原因是:

1、readyRead 信号触发时机:当串口缓冲区有数据就绪时立即触发,不保证一帧数据完全到达2、网络/硬件延迟:长数据帧可能分批到达接收缓冲区

3、缓冲区大小限制:单次读取可能无法获取完整数据包

解决方案:

方案1:针对固定帧头帧尾,添加数据缓冲区和帧识别机制

python 复制代码
def __init__(self):
    super().__init__()
    # 初始化界面(调用生成的setupUi方法,加载.ui定义的界面)
    self.setupUi(self)

    self.serial_manager = SerialPortManager()
    
    # 添加接收数据缓冲区
    self.recv_buffer = bytearray()
    # 添加帧开始和结束标志(根据实际协议调整)
    self.frame_start_marker = b'\xFF\xAA'  # 示例起始标志
    self.frame_end_marker = b'\xEE\xEE'    # 示例结束标志

def read_serial_data(self):
    """读取串口接收的数据(响应 readyRead 信号)"""
    if not self.serial_manager.serial_port.isOpen():
        return

    # 读取所有可用数据(QByteArray -> bytes)
    recv_data = self.serial_manager.serial_port.readAll()
    if recv_data.isEmpty():
        return

    # 转换为 Python 字节类型并添加到缓冲区
    new_data = bytes(recv_data.data())
    self.recv_buffer.extend(new_data)

    # 尝试从缓冲区中提取完整帧
    complete_frames = self._extract_complete_frames()
    
    # 处理完整的帧数据
    for frame in complete_frames:
        recv_hex_spaced = ' '.join(frame.hex()[i:i + 2].upper() for i in range(0, len(frame.hex()), 2))
        self._append_recv_text(f"接收:{recv_hex_spaced}")

def _extract_complete_frames(self):
    """从缓冲区中提取完整的数据帧"""
    complete_frames = []
    
    while True:
        # 查找帧结束标志
        end_pos = self.recv_buffer.find(self.frame_end_marker)
        if end_pos == -1:
            # 没有找到结束标志,等待更多数据
            break
            
        # 查找对应帧开始标志
        start_pos = self.recv_buffer.rfind(self.frame_start_marker, 0, end_pos + len(self.frame_end_marker))
        if start_pos == -1:
            # 没有找到对应的开始标志,可能是数据错误
            # 移除前面的数据,保留可能的完整帧
            self.recv_buffer = self.recv_buffer[end_pos + len(self.frame_end_marker):]
            continue
            
        # 提取完整帧(包含开始和结束标志)
        complete_frame = self.recv_buffer[start_pos:end_pos + len(self.frame_end_marker)]
        complete_frames.append(bytearray(complete_frame))
        
        # 从缓冲区移除已处理的帧
        self.recv_buffer = self.recv_buffer[end_pos + len(self.frame_end_marker):]
    
    return complete_frames

方案2:针对固定长度字段,如果协议中有长度字段,可以使用基于长度的帧识别

python 复制代码
def read_serial_data(self):
    """读取串口接收的数据(响应 readyRead 信号)"""
    if not self.serial_manager.serial_port.isOpen():
        return

    # 读取所有可用数据(QByteArray -> bytes)
    recv_data = self.serial_manager.serial_port.readAll()
    if recv_data.isEmpty():
        return

    # 转换为 Python 字节类型并添加到缓冲区
    new_data = bytes(recv_data.data())
    self.recv_buffer.extend(new_data)

    # 尝试解析完整帧
    while self._has_complete_frame():
        frame = self._get_next_frame()
        if frame:
            recv_hex_spaced = ' '.join(frame.hex()[i:i + 2].upper() for i in range(0, len(frame.hex()), 2))
            self._append_recv_text(f"接收:{recv_hex_spaced}")

def _has_complete_frame(self):
    """检查缓冲区是否有完整帧"""
    if len(self.recv_buffer) < 4:  # 至少需要4字节(例如:起始符+长度)
        return False
    
    # 根据具体协议检查帧完整性
    # 这里假设第3-4字节表示数据长度
    start_marker = self.recv_buffer[:2]
    if start_marker != b'\xFF\xAA':  # 根据实际协议调整
        return False
    
    if len(self.recv_buffer) >= 4:
        data_len = int.from_bytes(self.recv_buffer[2:4], byteorder='big')
        expected_frame_size = 4 + data_len + 2  # 开始标志+长度+数据+校验/CRC
        return len(self.recv_buffer) >= expected_frame_size
    
    return False

def _get_next_frame(self):
    """从缓冲区获取下一帧并移除"""
    if not self._has_complete_frame():
        return None
        
    # 解析帧长度
    data_len = int.from_bytes(self.recv_buffer[2:4], byteorder='big')
    expected_frame_size = 4 + data_len + 2  # 根据实际协议调整
    
    if len(self.recv_buffer) >= expected_frame_size:
        frame = self.recv_buffer[:expected_frame_size]
        self.recv_buffer = self.recv_buffer[expected_frame_size:]
        return bytearray(frame)
    
    return None

方案3:针对不确定协议格式,使用超时机制

python 复制代码
from PySide6.QtCore import QTimer

def __init__(self):
    super().__init__()
    # ... 其他初始化代码 ...
    
    # 添加超时定时器用于处理分包
    self.frame_timeout_timer = QTimer()
    self.frame_timeout_timer.timeout.connect(self._process_partial_frame)
    self.frame_timeout_timer.setSingleShot(True)
    self.partial_frame_timeout = 100  # 100ms 超时
    
def read_serial_data(self):
    """读取串口接收的数据(响应 readyRead 信号)"""
    if not self.serial_manager.serial_port.isOpen():
        return

    # 读取所有可用数据
    recv_data = self.serial_manager.serial_port.readAll()
    if recv_data.isEmpty():
        return

    new_data = bytes(recv_data.data())
    
    # 如果定时器正在运行,说明正在接收一帧数据
    if self.frame_timeout_timer.isActive():
        # 继续添加数据到现有缓冲区
        self.recv_buffer.extend(new_data)
        # 重启定时器
        self.frame_timeout_timer.start(self.partial_frame_timeout)
    else:
        # 新的接收开始
        self.recv_buffer = bytearray(new_data)
        # 启动定时器
        self.frame_timeout_timer.start(self.partial_frame_timeout)

def _process_partial_frame(self):
    """处理接收到的部分帧数据"""
    if len(self.recv_buffer) > 0:
        recv_hex_spaced = ' '.join(self.recv_buffer.hex()[i:i + 2].upper() 
                                  for i in range(0, len(self.recv_buffer.hex()), 2))
        self._append_recv_text(f"接收:{recv_hex_spaced}")
        # 清空缓冲区
        self.recv_buffer.clear()
相关推荐
AI探索者21 小时前
LangGraph StateGraph 实战:状态机聊天机器人构建指南
python
AI探索者21 小时前
LangGraph 入门:构建带记忆功能的天气查询 Agent
python
FishCoderh1 天前
Python自动化办公实战:批量重命名文件,告别手动操作
python
躺平大鹅1 天前
Python函数入门详解(定义+调用+参数)
python
曲幽1 天前
我用FastAPI接ollama大模型,差点被asyncio整崩溃(附对话窗口实战)
python·fastapi·web·async·httpx·asyncio·ollama
两万五千个小时1 天前
落地实现 Anthropic Multi-Agent Research System
人工智能·python·架构
哈里谢顿1 天前
Python 高并发服务限流终极方案:从原理到生产落地(2026 实战指南)
python
用户8356290780512 天前
无需 Office:Python 批量转换 PPT 为图片
后端·python
markfeng82 天前
Python+Django+H5+MySQL项目搭建
python·django
GinoWi2 天前
Chapter 2 - Python中的变量和简单的数据类型
python