当前位置: 代码网 > it编程>前端脚本>Python > Python使用pyqt5写一个串口工具

Python使用pyqt5写一个串口工具

2026年08月09日 Python 我要评论
最近研究了python,发现这个语言异常强大,可以直接用来写pyqt5的应用程序,搭配ai编程助手,很快写了一个串口工具,真不错。核心开发要点使用qserialport模块:这是pyqt5自带的串口通

最近研究了python,发现这个语言异常强大,可以直接用来写pyqt5的应用程序,搭配ai编程助手,很快写了一个串口工具,真不错。

核心开发要点

使用qserialport模块:这是pyqt5自带的串口通信模块,能很好地与qt的信号槽机制配合,实现异步通信,避免界面卡顿。注意,串口的读写操作建议放在子线程中执行,以保证主界面(ui)的流畅性。

选择通信库:主要有两种方式:

  • pyqt5.qtserialport:与pyqt5集成更紧密,原生支持信号槽。
  • pyserial:一个非常流行的python串口库,功能强大,常与pyqt5配合使用。你可以根据熟悉程度来选择。

获取可用串口列表:可以通过 qserialportinfo.availableports() 或 serial.tools.list_ports.comports() 来获取当前电脑上可用的串口列表。

实现多线程:为了防止串口读写操作阻塞ui,通常会创建一个继承自qthread的类来处理所有串口通信任务,并通过pyqtsignal将接收到的数据传回主线程进行显示。

'''
this module contains the main tool class and the main function to run the tool.
main purpose of this module is to implement uart com send and receive functions via pqt5.
copyright (c) 2026 by oushaojun
'''


from pyqt5.qtcore import *
from pyqt5.qtgui import *
from pyqt5.qtwidgets import *
import os
import sys
import serial
import serial.tools.list_ports
import time
from datetime import datetime


def _log(message):
    # timestamp with milliseconds
    timestamp = datetime.now().strftime("%y-%m-%d %h:%m:%s.%f")[:-3]
    print(f"[{timestamp}] {message}")

class settingsdialog(qdialog):
    reopen_serial_port_signal = pyqtsignal()

    def __init__(self, parent=none):
        super().__init__(parent)
        self.setwindowtitle("settings")
        self.resize(400, 200)
        self.init_ui()
        self.setwindowmodality(qt.applicationmodal)

    def init_ui(self):
        layout = qvboxlayout()
        self.setlayout(layout)

        # create a form layout for the settings
        form_layout = qformlayout()
        layout.addlayout(form_layout)

        # add a line edit for the com port
        self.port_combo = qcombobox()
        ports = serial.tools.list_ports.comports()
        self.port_combo.additems([port.device for port in ports])
        form_layout.addrow("com port:", self.port_combo)

        # add a combo box for the baud rate
        self.baudrate_combo = qcombobox()
        self.baudrate_combo.additems(["9600", "19200", "38400", "57600", "115200"])
        form_layout.addrow("baudrate:", self.baudrate_combo)

        # add ok and cancel buttons
        button_box = qdialogbuttonbox(qdialogbuttonbox.ok | qdialogbuttonbox.cancel)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addwidget(button_box)

    def accept(self):
        # save the settings when the dialog is accepted
        self.parent().com_config["port"] = self.port_combo.currenttext()
        self.parent().com_config["baudrate"] = int(self.baudrate_combo.currenttext())
        self.parent().save_settings()
        self.reopen_serial_port_signal.emit()
        _log(f"settings updated: {self.parent().com_config}")
        super().accept()

    def reject(self):
        # discard changes when the dialog is rejected
        _log("settings changes discarded.")
        super().reject()


class serialreadworker(qobject):
    message_received = pyqtsignal(str)

    def __init__(self, serial_port):
        super().__init__()
        self.serial_port = serial_port
        self.running = true

    @pyqtslot()
    def run(self):
        while self.running:
            try:
                if self.serial_port and self.serial_port.in_waiting > 0:
                    message = self.serial_port.readline().decode(errors='ignore').strip()
                    if message:
                        self.message_received.emit(message)
            except exception:
                # ignore transient serial errors during read
                pass
            time.sleep(0.1)

    @pyqtslot()
    def update_serial_port(self, serial_port):
        self.serial_port = serial_port

    @pyqtslot()
    def stop(self):
        self.running = false


class mainwindow(qmainwindow):
    def __init__(self):
        super().__init__()
        self.init_settings()    #call first to create variable self.com_config
        self.setwindowtitle("uart com tool")
        self.resize(1200, 600)
        self.init_ui()
        self.init_menu()
        self.open_serial_port()

    def open_serial_port(self):
        try:
            if hasattr(self, 'serial_port') and self.serial_port.is_open:
                self.serial_port.close()
                _log(f"closed serial port: {self.com_config['port']}")
            
            self.serial_port = serial.serial(self.com_config["port"], self.com_config["baudrate"], timeout=1)
            _log(f"opened serial port: {self.com_config['port']} at {self.com_config['baudrate']} baud.")
        except serial.serialexception as e:
            _log(f"error opening serial port: {e}")
            self.output_text_edit.append(f"error opening serial port: {e}")
        else:
            # start a thread to read from the serial port
            if not hasattr(self, 'read_thread') or not self.read_thread.isrunning():
                self.read_thread = qthread()
                self.read_worker = serialreadworker(self.serial_port)
                self.read_worker.movetothread(self.read_thread)
                self.read_thread.started.connect(self.read_worker.run)
                self.read_thread.finished.connect(self.read_worker.stop)
                self.read_worker.message_received.connect(self.handle_received_message)
                self.read_thread.start()
                _log("started serial read thread.")
            else:
                if hasattr(self, 'read_worker') and self.read_worker is not none:
                    self.read_worker.update_serial_port(self.serial_port)
                _log("serial read thread is already running.")
            self.output_text_edit.append(f"\r\nopened serial port: {self.com_config['port']} at {self.com_config['baudrate']} baudrate.")

    def handle_received_message(self, message):
        _log(f"received message: {message}")
        timestamp = datetime.now().strftime("%y-%m-%d %h:%m:%s.%f")[:-3]
        self.output_text_edit.append(f"[{timestamp}] received: {message}")

    def init_settings(self):
        # load settings from qsettings
        self.settings = qsettings("config.ini", qsettings.iniformat)
        self.load_settings()

    def init_ui(self):
        # create a central widget
        central_widget = qwidget()
        self.setcentralwidget(central_widget)

        # create a layout for the central widget
        layout = qvboxlayout()
        central_widget.setlayout(layout)

        # add a label to the layout
        label = qlabel("output message:")
        label.setalignment(qt.alignleft)
        layout.addwidget(label)

        #add a text edit to the layout to show the output message
        self.output_text_edit = qtextedit()
        self.output_text_edit.setreadonly(true)
        layout.addwidget(self.output_text_edit)

        #add a label to the layout
        label2 = qlabel("input message:")
        label2.setalignment(qt.alignleft)
        layout.addwidget(label2)

        #add a text edit to the layout to input the message to send
        self.input_text_edit = qlineedit()
        layout.addwidget(self.input_text_edit)

        #add a button to the layout to send the message
        hbox = qhboxlayout()
        hbox.addstretch()
        self.send_button = qpushbutton("send")
        self.send_button.setfixedheight(30)
        self.send_button.setfixedwidth(100)
        self.send_button.clicked.connect(self.send_message)
        hbox.addwidget(self.send_button)
        layout.addlayout(hbox)

        #status bar to show the current com port and baudrate
        self.status_label = qlabel()
        self.status_label.setalignment(qt.alignright | qt.alignvcenter)
        self.status_bar = qstatusbar()
        self.status_bar.addpermanentwidget(self.status_label, 1)
        self.status_label.settext(
            f"com port: {self.com_config['port']}, baudrate: {self.com_config['baudrate']}"
        )
        self.setstatusbar(self.status_bar)

    def send_message(self):
        # get the message from the input text edit
        message = self.input_text_edit.text()
        timestamp = datetime.now().strftime("%y-%m-%d %h:%m:%s.%f")[:-3]
        if not message:
            _log("no message to send.")
            return

        # send the message via the configured com port and baudrate
        try:
            if not hasattr(self, 'serial_port') or not self.serial_port.is_open:
                _log("serial port is not open. attempting to open it.")
                self.open_serial_port()
                if not self.serial_port.is_open:
                    _log("failed to open serial port. cannot send message.")
                    self.output_text_edit.append(f"[{timestamp}] error: serial port is not open.")
                    return

            self.serial_port.write((message + "\n").encode())
            _log(f"sent message: {message}")
            self.output_text_edit.append(f"[{timestamp}] sent: {message}")
        except serial.serialexception as e:
            _log(f"error sending message: {e}")
            self.output_text_edit.append(f"[{timestamp}] error: {e}")
        else:
            # clear the input text edit after sending
            #self.input_text_edit.clear()
            pass

    def reopen_serial_port(self):
        _log("reopening serial port with updated settings.")
        self.open_serial_port()
        self.update_status_bar()

    def update_status_bar(self):
        self.status_label.settext(
            f"com port: {self.com_config['port']}, baudrate: {self.com_config['baudrate']}"
        )
        _log(f"status bar updated: com port: {self.com_config['port']}, baudrate: {self.com_config['baudrate']}")

    def init_menu(self):
        # create a menu bar
        menu_bar = self.menubar()

        # menu bar contains two sub-menus: "file" and "config"
        file_menu = menu_bar.addmenu("file")
        config_menu = menu_bar.addmenu("settings")

        # exit action to close the application
        exit_action = qaction("exit", self)
        exit_action.triggered.connect(self.close)

        # info action to show the tool information
        info_action = qaction("info", self)
        info_action.triggered.connect(self.show_info)

        # save output message to a file action
        save_action = qaction("save output", self)
        save_action.triggered.connect(self.save_output_to_file)

        #clear output message action
        clear_action = qaction("clear output", self)
        clear_action.triggered.connect(self.output_text_edit.clear)

        # add the exit action to the file menu
        file_menu.addaction(exit_action)
        file_menu.addaction(info_action)
        file_menu.addaction(save_action)
        file_menu.addaction(clear_action)
        file_menu.addseparator()

        # add the settings action to the config menu
        config_menu.addaction("settings")
        config_menu.triggered.connect(self.open_settings_dialog)

    def save_output_to_file(self):
        # open a file dialog to select the file to save the output message
        timestamp = datetime.now().strftime("%y-%m-%d %h:%m:%s.%f")[:-3]
        filename = f"{timestamp.replace(' ', '_').replace(':', '-')}_{self.com_config['port']}.txt"
        options = qfiledialog.options()
        options |= qfiledialog.dontusenativedialog
        file_name, _ = qfiledialog.getsavefilename(self, "save output message", filename, "text files (*.txt);;all files (*)", options=options)
        if file_name:
            try:
                with open(file_name, 'w', encoding='utf-8') as f:
                    f.write(self.output_text_edit.toplaintext())
                _log(f"output message saved to {file_name}")
            except exception as e:
                _log(f"error saving output message: {e}")
                self.output_text_edit.append(f"[{timestamp}] error saving output message: {e}")

    def load_settings(self):

        ports = serial.tools.list_ports.comports()
        _log(f"available com ports: {[port.device for port in ports]}")
        if os.name == 'nt':
            # windows: use the first available com port or default to com1
            default_port = ports[0].device if ports else "com1"
        elif os.name == 'posix':
            # linux: use the first available /dev/ttyusb* or /dev/ttys* port or default to /dev/ttyusb0
            default_port = next((port.device for port in ports if port.device.startswith("/dev/ttyusb") or port.device.startswith("/dev/ttys")), "/dev/ttyusb0")
        else:
            # other os: default to com1
            default_port = "com1"

        self.com_config = {
            "port": self.settings.value("port", default_port),
            "baudrate": int(self.settings.value("baudrate", 115200)),
        }
        _log(f"loaded settings: {self.com_config}")

    def save_settings(self):
        self.settings.setvalue("port", self.com_config["port"])
        self.settings.setvalue("baudrate", self.com_config["baudrate"])
        self.settings.sync()
        _log(f"saved settings: {self.com_config}")

    def open_settings_dialog(self):
        dialog = settingsdialog(self)
        dialog.port_combo.setcurrenttext(self.com_config["port"])
        dialog.baudrate_combo.setcurrenttext(str(self.com_config["baudrate"]))
        dialog.reopen_serial_port_signal.connect(self.reopen_serial_port)
        dialog.exec_()
        self.update_status_bar()

    def closeevent(self, event):
        # save settings when the application is closed
        self.save_settings()
        if hasattr(self, 'read_thread') and self.read_thread is not none and self.read_thread.isrunning():
            try:
                # ask the worker to stop and quit the thread
                if hasattr(self, 'read_worker') and self.read_worker is not none:
                    self.read_worker.stop()
                self.read_thread.quit()
                self.read_thread.wait()
            except exception:
                pass
        _log("application closed. settings saved.")
        event.accept()

    def show_info(self):
        # show a message box with the tool information
        msg = qmessagebox()
        msg.settext("uart com tool\nversion 1.0\ncopyright (c) 2026 by oushaojun")
        msg.setinformativetext("this tool is used for sending and receiving data via uart com ports.")
        msg.setdetailedtext("this tool is implemented using pyqt5 and provides a simple gui for uart communication. it allows users to configure com port settings, send data, and receive data in real-time.")
        msg.setstandardbuttons(qmessagebox.ok | qmessagebox.cancel)
        msg.setwindowtitle("info")
        msg.seticon(qmessagebox.information)
        msg.resize(600, 400)
        retval = msg.exec_()



if __name__ == "__main__":
    app = qapplication(sys.argv)
    app.setwindowicon(qapplication.style().standardicon(qstyle.sp_computericon))
    win = mainwindow()
    win.show()
    sys.exit(app.exec_())

















requirements.txt内容:

pyqt5==5.15.11
pyqt5_sip==12.18.0
pyserial==3.5

以上就是python使用pyqt5写一个串口工具的详细内容,更多关于python pyqt5串口工具的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com