树莓派4B串口助手开发全记录:从PyQt5安装到打包成可执行文件

在嵌入式开发和物联网项目的原型验证阶段,一个稳定可靠的串口调试工具是开发者手中的“瑞士军刀”。虽然市面上有众多成熟的串口助手软件,但当你的工作核心转移到像树莓派4B这样的单板计算机上,直接在设备上运行一个量身定制的工具,往往能带来意想不到的便利——无论是实时监控设备日志,还是与自制硬件进行双向通信。本文将带你完整走一遍在树莓派4B上,从零开始构建一个基于PyQt5的图形化串口助手,并最终打包成独立可执行文件的全部流程。这个过程不仅涉及Python环境配置的“坑”与“桥”,更融合了针对ARM架构的本土化实践和高效开发技巧,旨在为嵌入式开发者和物联网爱好者提供一份可直接复用的实战指南。

1. 开发环境搭建与依赖项部署

在树莓派上开始任何Python项目,第一步永远是确保你的基础环境是稳固且高效的。树莓派OS(原Raspbian)虽然预装了Python,但直接使用系统Python和默认的软件源进行开发,可能会在后续遇到依赖冲突或网络超时的困扰。因此,建立一个清晰、可控的独立开发环境是明智之举。

我强烈建议从系统级别的更新和换源开始。打开终端,执行以下命令更新软件包列表并升级现有软件:

sudo apt update
sudo apt full-upgrade -y

接下来,将软件源更换为国内镜像以大幅提升下载速度。编辑APT源列表文件:

sudo nano /etc/apt/sources.list

将文件中所有 deb http://archive.raspberrypi.org/debian/deb http://raspbian.raspberrypi.org/raspbian/ 开头的URL,替换为国内镜像站地址,例如中科大的镜像。替换后文件内容大致如下:

deb http://mirrors.ustc.edu.cn/raspbian/raspbian/ bullseye main contrib non-free rpi
deb http://mirrors.ustc.edu.cn/archive.raspberrypi.org/debian/ bullseye main

保存退出后,再次运行 sudo apt update 使更改生效。这个步骤能为你后续安装任何软件节省大量等待时间,是提升在树莓派上开发体验的关键一步。

1.1 创建并使用Python虚拟环境

为了避免项目依赖与系统Python环境相互污染,使用venv创建独立的虚拟环境是最佳实践。树莓派OS通常已预装python3-venv,如果没有,请先安装:

sudo apt install python3-venv -y

然后,在你的项目目录(例如 ~/projects/serial_assistant)中创建虚拟环境:

cd ~
mkdir -p projects/serial_assistant
cd projects/serial_assistant
python3 -m venv venv

激活虚拟环境:

source venv/bin/activate

激活后,你的命令行提示符前会出现 (venv) 标识,表示后续所有Python包都将安装在这个隔离的环境中。你可以通过 which python3which pip 命令来确认当前使用的是虚拟环境内的解释器和包管理器。

1.2 安装PyQt5及其依赖

在树莓派的ARM架构上安装PyQt5,与在x86的Windows或Linux上略有不同。直接使用 pip install PyQt5 可能会因为缺少底层Qt库或架构不兼容而失败。最可靠的方式是通过树莓派OS的包管理器apt来安装系统级的PyQt5绑定。

首先,确保虚拟环境已激活,然后通过apt安装python3-pyqt5及相关图形和串口支持包:

sudo apt install python3-pyqt5 python3-pyqt5.qtserialport -y

注意:这里使用sudo apt install安装的包是系统全局的,但Python的site-packages目录可以被虚拟环境识别。安装完成后,在虚拟环境中尝试导入PyQt5PyQt5.QtSerialPort来验证。

为了在虚拟环境中也能方便地管理这些通过apt安装的包(尽管它们不在venv的site-packages里),并且安装其他纯Python依赖,我们还需要在虚拟环境中安装pyserial这个核心的串口通信库:

pip install pyserial

为了加速pip下载,可以临时使用国内PyPI镜像源:

pip install pyserial -i https://pypi.tuna.tsinghua.edu.cn/simple

至此,图形界面和串口通信的核心依赖已就绪。你可以创建一个简单的测试脚本 test_import.py 来验证:

import sys
import PyQt5
from PyQt5 import QtWidgets, QtSerialPort
import serial

print("PyQt5 version:", PyQt5.__version__)
print("PySerial version:", serial.__version__)
print("All imports successful!")

运行 python test_import.py,如果没有报错,则环境配置成功。

2. 串口助手核心功能设计与编码

有了稳固的环境,我们就可以开始构思和编写串口助手本身了。一个基础的串口助手通常需要包含以下几个核心功能模块:串口参数配置(端口、波特率、数据位等)、打开/关闭连接、发送数据、接收并显示数据、以及清空显示等。我们将使用PyQt5来构建图形界面,并用PyQt5.QtSerialPort(它是对pyserial的Qt封装,更适合与GUI事件循环集成)来处理串口通信。

2.1 设计图形用户界面

我们使用Qt Designer进行界面设计,这是一个可视化拖拽工具,可以生成.ui文件,再通过pyuic5工具转换为Python代码。首先安装Qt Designer:

sudo apt install qttools5-dev-tools -y

安装后,可以在终端输入 designer 启动。设计一个简单的界面,包含以下关键控件:

  • QComboBox:用于选择可用串口端口。
  • 多个QComboBox:分别用于选择波特率(如9600, 115200)、数据位(8)、停止位(1)、校验位(None)。
  • QPushButton: “打开串口”/“关闭串口”按钮。
  • QTextEditQPlainTextEdit:用于显示接收到的数据。
  • QLineEditQPushButton:用于输入要发送的文本和触发发送。
  • QPushButton: “清空接收区”按钮。

设计完成后,保存为 serial_ui.ui 文件。然后,在项目目录下使用 pyuic5 将其转换为Python代码:

pyuic5 -o ui_serial.py serial_ui.ui

生成的 ui_serial.py 文件包含了界面控件的定义和布局,我们将在主程序中导入并使用它。

2.2 编写主程序逻辑

接下来,我们创建主程序文件 serial_assistant.py。代码结构大致如下:

import sys
import serial.tools.list_ports
from PyQt5 import QtWidgets, QtCore, QtSerialPort
from ui_serial import Ui_MainWindow  # 假设UI类名为Ui_MainWindow

class SerialAssistant(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # 初始化串口对象
        self.serial = QtSerialPort.QSerialPort()
        self.serial.readyRead.connect(self.read_data)

        # 初始化界面
        self.refresh_ports()
        self.setup_baudrate_combo()
        self.ui.btn_refresh.clicked.connect(self.refresh_ports)
        self.ui.btn_open.clicked.connect(self.toggle_serial)
        self.ui.btn_send.clicked.connect(self.send_data)
        self.ui.btn_clear.clicked.connect(self.clear_received)

        # 定时刷新端口列表(可选)
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.refresh_ports)
        self.timer.start(2000)  # 每2秒刷新一次

    def refresh_ports(self):
        """刷新可用串口列表"""
        self.ui.cb_port.clear()
        ports = serial.tools.list_ports.comports()
        for port in ports:
            self.ui.cb_port.addItem(port.device)

    def setup_baudrate_combo(self):
        """设置常用的波特率选项"""
        baudrates = ['9600', '19200', '38400', '57600', '115200', '230400', '460800', '921600']
        self.ui.cb_baud.addItems(baudrates)
        self.ui.cb_baud.setCurrentText('115200')  # 默认115200

    def toggle_serial(self):
        """打开或关闭串口"""
        if not self.serial.isOpen():
            # 打开串口
            port = self.ui.cb_port.currentText()
            baud = int(self.ui.cb_baud.currentText())
            self.serial.setPortName(port)
            self.serial.setBaudRate(baud)
            # 设置其他参数(数据位、停止位、校验位),根据UI中的ComboBox值设置
            # self.serial.setDataBits(...)
            # self.serial.setParity(...)
            # self.serial.setStopBits(...)

            if self.serial.open(QtCore.QIODevice.ReadWrite):
                self.ui.btn_open.setText('关闭串口')
                self.ui.statusbar.showMessage(f'已连接 {port} @ {baud} bps')
            else:
                QtWidgets.QMessageBox.critical(self, '错误', '无法打开串口!')
        else:
            # 关闭串口
            self.serial.close()
            self.ui.btn_open.setText('打开串口')
            self.ui.statusbar.showMessage('串口已关闭')

    def read_data(self):
        """读取串口数据并显示"""
        if self.serial.isOpen():
            data = self.serial.readAll()
            if data:
                text = data.data().decode('utf-8', errors='ignore')  # 根据实际编码调整
                self.ui.text_received.insertPlainText(text)
                # 自动滚动到底部
                cursor = self.ui.text_received.textCursor()
                cursor.movePosition(cursor.End)
                self.ui.text_received.setTextCursor(cursor)

    def send_data(self):
        """发送数据到串口"""
        if self.serial.isOpen():
            text = self.ui.line_send.text()
            if text:
                # 处理换行符,例如将\n替换为回车
                if self.ui.cb_newline.isChecked():
                    text += '\r\n'
                self.serial.write(text.encode())
                self.ui.line_send.clear()
        else:
            QtWidgets.QMessageBox.warning(self, '警告', '请先打开串口!')

    def clear_received(self):
        """清空接收显示区"""
        self.ui.text_received.clear()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = SerialAssistant()
    window.show()
    sys.exit(app.exec_())

这段代码构建了一个具备基本功能的串口助手框架。QtSerialPort.QSerialPort 提供了异步非阻塞的读写方式,通过readyRead信号触发接收函数,避免了界面卡顿。

2.3 功能增强与调试技巧

基础功能完成后,可以考虑添加一些提升体验的功能:

  1. 发送历史记录:将发送过的内容保存到一个列表中,通过下拉框或快捷键进行选择再次发送。
  2. 数据格式转换:增加Hex发送和Hex显示的功能。发送时,将输入的Hex字符串转换为字节;接收时,将字节数据以Hex格式显示。
  3. 定时发送:添加一个QSpinBox设置间隔(毫秒),一个QCheckBox启用定时发送,用一个QTimer周期性触发发送函数。
  4. 接收数据统计:显示已接收的字节数、行数,并可以手动清零计数。
  5. 日志保存:添加按钮将当前接收区的文本保存到文件中。

在树莓派上调试PyQt程序,使用Thonny IDE是一个轻量级的选择。你可以通过以下命令安装:

sudo apt install thonny -y

在Thonny中打开你的 serial_assistant.py 文件,直接点击运行按钮。Thonny的内置终端会输出Python的任何打印信息和错误回溯,对于调试非常方便。如果程序崩溃,错误信息会清晰地显示出来,帮助你快速定位问题。

3. 使用PyInstaller打包为独立可执行文件

当你的串口助手开发调试完毕,下一步就是将其打包成一个独立的可执行文件。这样,你可以在没有安装Python和PyQt5依赖的其他树莓派上直接运行它。我们选择PyInstaller来完成这个任务,但它在本地的ARM环境(树莓派)上打包,有一些特别的注意事项。

3.1 安装与配置PyInstaller

首先,在你的项目虚拟环境中安装PyInstaller:

pip install pyinstaller

安装完成后,直接在终端输入 pyinstaller 可能会提示“命令未找到”。这是因为pip install默认将可执行文件安装到了用户目录下的 .local/bin(例如 /home/pi/.local/bin),而这个路径可能不在系统的PATH环境变量中。

检查并添加PATH的方法如下:

  1. 查找pyinstaller实际安装位置:

    pip show -f pyinstaller | grep Location
    

    通常,可执行脚本在 ~/.local/bin 目录下。

  2. 将用户bin目录添加到当前shell的PATH中(临时):

    export PATH="$HOME/.local/bin:$PATH"
    

    然后运行 pyinstaller --version 测试。

  3. 为了永久生效,可以将这行export命令添加到你的shell配置文件中(如 ~/.bashrc~/.profile):

    echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
    source ~/.bashrc
    

3.2 编写打包规范文件(Spec File)

直接使用命令行参数打包PyQt5应用有时会遗漏一些依赖,特别是Qt的插件文件(如图像格式支持、平台插件等)。创建一个.spec文件能让我们更精细地控制打包过程。在项目根目录创建一个 serial_assistant.spec 文件:

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
    ['serial_assistant.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

# 添加PyQt5相关的隐藏导入和Qt插件
hiddenimports = ['PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', 'PyQt5.QtSerialPort']
for imp in hiddenimports:
    if imp not in a.hiddenimports:
        a.hiddenimports.append(imp)

# 关键步骤:收集Qt插件文件
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
pyqt5_plugins = collect_data_files('PyQt5', subdir='Qt5/plugins')
a.datas += pyqt5_plugins

# 确保平台插件被正确打包
pyqt5_libs = collect_data_files('PyQt5', subdir='Qt5/lib')
a.datas += pyqt5_libs

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='serial_assistant',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,  # 使用UPX压缩,减小体积(需安装upx)
    runtime_tmpdir=None,
    console=False,  # 设置为True则显示控制台窗口,调试时有用
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='icon.ico',  # 如果有图标文件的话
)

coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='serial_assistant',
)

这个spec文件做了几件重要的事情:

  • 明确指定了隐藏导入:确保PyQt5的核心模块被打包进去。
  • 收集Qt插件:这是图形界面能正常显示的关键。platforms插件(如libqxcb.so)必须包含,否则程序在无GUI环境的服务器上运行会崩溃。
  • 使用UPX压缩:可以显著减小可执行文件体积。如果系统未安装UPX,可以 sudo apt install upx-ucl 或选择关闭此选项。

3.3 执行打包与问题排查

使用spec文件进行打包:

pyinstaller serial_assistant.spec

PyInstaller会开始分析依赖、收集文件,并在 dist 目录下生成一个名为 serial_assistant 的文件夹(或单个可执行文件,取决于spec中COLLECT的设置)。整个文件夹包含了程序运行所需的所有库。

在树莓派上测试打包好的程序,直接进入dist文件夹运行:

cd dist/serial_assistant
./serial_assistant

如果程序启动失败,最常见的错误是缺少Qt平台插件。错误信息通常类似于 "This application failed to start because it could not find or load the Qt platform plugin 'xcb'"。解决方法是在spec文件中确保正确收集了插件,或者手动检查 dist/serial_assistant/PyQt5/Qt5/plugins/platforms/ 目录下是否存在 libqxcb.so 等文件。

另一个常见问题是文件体积过大。可以通过在spec文件中excludes列表里排除不需要的模块(如PyQt5.QtWebEnginePyQt5.QtBluetooth等),以及确保使用UPX压缩来优化。

4. 部署、优化与进阶思考

成功打包出可执行文件后,你就可以将其复制到任何运行相同版本树莓派OS(最好是相同版本,如Raspbian Buster/Bullseye)的设备上运行,无需安装Python环境。为了获得更好的使用体验,还可以进行一些部署后的优化。

4.1 创建桌面快捷方式

为了让非技术用户也能方便地使用,可以在树莓派的桌面上创建一个启动器。创建一个名为 serial_assistant.desktop 的文件:

[Desktop Entry]
Type=Application
Name=串口助手
Comment=基于PyQt5的串口调试工具
Exec=/home/pi/projects/serial_assistant/dist/serial_assistant/serial_assistant
Icon=/home/pi/projects/serial_assistant/icon.png
Terminal=false
Categories=Utility;Development;

将其放到 /home/pi/Desktop/ 目录下,并赋予执行权限 chmod +x serial_assistant.desktop。双击桌面图标即可启动程序。

4.2 性能与资源优化考量

树莓派4B虽然性能强大,但作为嵌入式设备,资源仍需精打细算。你的串口助手在长时间运行、高速率接收数据时,需要注意:

  • 接收缓冲区管理:在 read_data 函数中,如果数据涌入非常快,频繁的UI更新(insertPlainText)会成为性能瓶颈。可以考虑使用一个缓冲区(如QByteArray)累积数据,然后用一个定时器(QTimer)定期(例如每100毫秒)将缓冲区的数据更新到UI中,减少UI刷新次数。
  • 线程安全:虽然QSerialPortreadyRead信号在主线程(GUI线程)中处理是安全的,但如果进行复杂的接收数据处理(如实时协议解析),最好将处理逻辑移到一个单独的QThread中,避免界面卡顿。
  • 内存使用QPlainTextEdit组件如果接收海量数据不清空,会占用大量内存。可以增加一个“自动清空”选项,当行数超过一定数量时自动清除旧数据。

4.3 扩展方向:从工具到系统组件

一个成熟的串口助手可以进一步演化为更强大的物联网网关管理工具的一部分。例如:

  • 多串口同时监控:实例化多个QSerialPort对象,在一个标签页界面中同时监控多个硬件设备的串口输出。
  • 协议解析与可视化:集成简单的协议解析器(如Modbus RTU、自定义文本协议),将接收到的原始字节流解析为有意义的字段,并以图表(可集成PyQtGraphMatplotlib)形式实时绘制数据曲线。
  • 远程访问:为串口助手添加一个简单的HTTP API或WebSocket服务器,允许通过网络从其他计算机发送命令或获取接收到的数据,实现远程调试。
  • 与硬件GPIO联动:结合树莓派的GPIO库(如RPi.GPIOgpiozero),实现通过串口命令控制LED、读取传感器,或者根据GPIO状态自动发送特定串口指令,让树莓派成为硬件交互的智能中枢。

整个开发流程走下来,你会发现,在树莓派上进行本地化Python GUI开发并打包,虽然初期在环境配置和打包环节会遇到一些架构特有的挑战,但一旦打通,其带来的便利性是巨大的——你拥有了一个完全自主可控、深度集成在目标硬件平台上的专业工具。这份记录里提到的换源、虚拟环境、spec文件配置、路径问题,都是我在实际项目中反复踩坑后总结出的有效路径。下次当你需要为树莓派项目快速定制一个调试界面时,不妨直接从这个框架开始。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐