[pyqt5+pyserial]python实现modbus串口通信

本文通过PyQt5构建GUI界面,并结合pyserial库,详细介绍了如何使用Python实现Modbus串口通信。同时,利用QThread确保UI线程不被阻塞,保证了用户体验。Func.py文件中包含了具体的Modbus交互指令和CRC校验功能。

Test.py实现GUI界面和大部分逻辑 

# -*- coding: utf-8 -*-

from PyQt5.QtCore import QDateTime, Qt, QTimer, pyqtSignal, QThread
from PyQt5.QtWidgets import (QApplication, QCheckBox, QComboBox, QDateTimeEdit,
        QDial, QDialog, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
        QProgressBar, QPushButton, QRadioButton, QScrollBar, QSizePolicy,
        QSlider, QSpinBox, QStyleFactory, QTableWidget, QTabWidget, QTextEdit,
        QVBoxLayout, QWidget, QMessageBox)
from PyQt5.QtGui import (QIntValidator)
import serial
import serial.tools.list_ports
import crcmod.predefined
import Func
import time
import threading

class WidgetGallery(QDialog):

    # global dict1
    # dict1 = {'SUM': '0000'}
    sig = pyqtSignal(str, str, float, str, str, str)

    def __init__(self, parent=None):
        super(WidgetGallery, self).__init__(parent)
        self.originalPalette = QApplication.palette()

        self.comComboBox = QComboBox()
        # print(self.getPort())


        # self.comComboBox.currentIndexChanged.connect(self.selectionchange)
        # print(self.comComboBox.currentText())
        self.pasg = '1'  # 默认通道为1,不然初始化bug
        self.serFlag = False
        comLabel = QLabel("串口:")
        comLabel.setBuddy(self.comComboBox)


        self.bpsComboBox = QComboBox()
        self.bpsComboBox.addItems(self.getBps())

        bpsLabel = QLabel("波特率:")
        bpsLabel.setBuddy(self.bpsComboBox)

        self.crcComboBox = QComboBox()
        self.crcComboBox.addItems(self.getCrc())

        crcLabel = QLabel("校验:")
        crcLabel.setBuddy(self.crcComboBox)

        self.openButton = QPushButton("打开串口")
        self.openButton.setDefault(False)
        self.openButton.clicked.connect(self.openSer)
        self.openButton.setCheckable(True)
        # self.openButton.setAutoExclusive(True)
        # self.openButton.setEnabled(False)
        # print(self.getPort())
        self.comComboBox.addItems(self.getPort())


        # self.open1Button = QPushButton("go03")
        # self.open1Button.setDefault(False)
        # self.staNum = '01'
        # self.funCode = '03'
        # self.keyName = 'SUM'
        # self.open1Button.clicked.connect(self.sendFunction03(staNum, funCode, keyName))
        # self.open1Button.clicked.connect(self.Print)
        # self.open1Button.clicked.connect(self.sendFunction03)

        self.setWindowFlags(Qt.Widget)
        self.setWindowTitle("ST05D校准软件")
        self.resize(600, 400)
        self.setFixedSize(600, 400)
        self.changeStyle('windowsvista')

        topLayout = QHBoxLayout()
        topLayout.addWidget(comLabel)
        topLayout.addWidget(self.comComboBox)
        topLayout.addWidget(bpsLabel)
        topLayout.addWidget(self.bpsComboBox)
        topLayout.addWidget(crcLabel)
        topLayout.addWidget(self.crcComboBox)
        topLayout.addSpacing(50)
        topLayout.addWidget(self.openButton)
        # topLayout.addWidget(self.open1Button)
        topLayout.addStretch(1)

        self.createTopLeftGroupBox()
        self.createTopRightGroupBox()
        self.createMiddleLeftGroupBox()
        self.createMiddleRightGroupBox()
        self.createBottomLeftGroupBox()
        self.createBottomRightGroupBox()

        mainLayout = QGridLayout()
        mainLayout.addLayout(topLayout, 0, 0, 1, 2)
        mainLayout.addWidget(self.topLeftGroupBox, 1, 0)
        mainLayout.addWidget(self.topRightGroupBox, 1, 1)
        mainLayout.addWidget(self.middleLeftGroupBox, 2, 0)
        mainLayout.addWidget(self.middleRightGroupBox, 2, 1)
        mainLayout.addWidget(self.bottomLeftGroupBox, 3, 0)
        mainLayout.addWidget(self.bottomRightGroupBox, 3, 1)
        mainLayout.setColumnStretch(0, 1)
        mainLayout.setColumnStretch(1, 1)

        self.setLayout(mainLayout)


    # def Print(self):
    #     print('print')


    def closeEvent(self, event):
        # self.c.terminate()
        print('thread=sta', self.readthread._running)
        self.readthread.terminate()
        print('close')

    def openSer(self):
        print('open')
        print(self.openButton.isChecked())
        if self.openButton.isChecked():
            self.ser = serial.Serial(self.comComboBox.currentText(), self.bpsComboBox.currentText(), timeout=0)
            self.comComboBox.setEnabled(False)
            self.bpsComboBox.setEnabled(False)
            self.crcComboBox.setEnabled(False)
            self.openButton.setText('关闭串口')
        else:
            print('no')
            self.ser.close()
            self.openButton.setText('打开串口')
            self.comComboBox.setEnabled(True)
            self.bpsComboBox.setEnabled(True)
            self.crcComboBox.setEnabled(True)
        # if not self.serFlag:
        #     self.ser = serial.Serial(self.comComboBox.currentText(), self.bpsComboBox.currentText(), timeout=0)
        #     self.serFlag = True
        #     print(self.comComboBox.currentText(), self.bpsComboBox.currentText())
        # else:
        #     self.ser.close()
        #     self.serFlag = False
        #     self.openSer()

    # def sendFunction03(self, staNum, funCode, keyName):
    def send03(self, keyName):
        try:
            staNum = self.staNum
            # funCode = self.funCode
            # keyName = self.keyName
        except Exception:
            staNum = '00'
        # keyName = self.keyName
        funCode = '03'
        print('033')
        if self.openButton.isChecked():
            list1 = Func.sendFunction03(staNum, funCode, keyName)
            # list1 = []
            # dict1 = Func.addressDict03
            # list1.append(int('0x' + staNum, 16))
            # list1.append(int('0x' + funCode, 16))
            # list1.append(int('0x' + dict1[keyName][0:2], 16))
            # list1.append(int('0x' + dict1[keyName][2:4], 16))
            # list1.append(int('0x00', 16))
            # list1.append(int('0x02', 16))
            # crc16_func = crcmod.predefined.mkCrcFun('modbus')
            # # ba = bytearray([0o01, 0o03, 0o00, 0o00, 0o00, 0o02])
            # a = bytes().fromhex(staNum + funCode + dict1[keyName] + '0002')
            # # print(a)
            # reslist = hex(crc16_func(a))[2:].zfill(4)
            # # print(type(reslist[2:].upper()), reslist[:2].upper())
            # list1.append(int('0x' + reslist[2:], 16))
            # list1.append(int('0x' + reslist[:2], 16))
            print(list1)
            try:
                self.ser.write(list1)
            except Exception:
                print('not ok')
                reply = QMessageBox.warning(self,
                                            "串口设置错误",
                                            "请检查串口号及波特率设置",
                                            QMessageBox.Yes | QMessageBox.No)
                # self.echo(reply)
            else:
                print(type(self.ser.read()))
     
<think> 我们正在讨论使用PyQt5实现串口通信上位机的方法。以下是实现的基本步骤和关键代码示例: 1. **环境准备**: - 安装PyQt5:`pip install PyQt5` - 安装串口通信库:`pip install pyserial` 2. **设计界面**: - 使用Qt Designer设计界面(包括串口设置、打开/关闭串口按钮、数据发送和接收区域等)。 - 将.ui文件转换为.py文件:`pyuic5 -x design.ui -o design.py` 3. **编写逻辑代码**: - 导入必要的模块:`PyQt5`的相关模块和`serial`。 - 创建主窗口类,并加载界面。 - 实现串口扫描、打开、关闭、发送和接收数据等功能。 下面是一个简单的示例代码框架: ```python import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox from PyQt5.QtCore import QIODevice, QTimer from serial import Serial, SerialException # 假设通过Qt Designer生成的界面文件为Ui_MainWindow from design import Ui_MainWindow class SerialPortGUI(QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) # 串口对象 self.serial = Serial() # 定时器,用于周期读取串口数据 self.timer = QTimer() self.timer.timeout.connect(self.read_data) # 初始化界面 self.init_ui() # 绑定信号槽 self.btn_open.clicked.connect(self.open_port) self.btn_send.clicked.connect(self.send_data) self.btn_close.clicked.connect(self.close_port) self.btn_scan.clicked.connect(self.scan_ports) def init_ui(self): # 初始化串口设置下拉框等 self.scan_ports() # 设置波特率下拉框默认值 self.cb_baud.setCurrentText("9600") def scan_ports(self): """扫描可用串口""" self.cb_port.clear() ports = ["COM{}".format(i+1) for i in range(256)] # 简单列举COM1~COM256 available_ports = [] for port in ports: try: s = Serial(port) s.close() available_ports.append(port) except (SerialException, OSError): pass self.cb_port.addItems(available_ports) def open_port(self): """打开串口""" if self.serial.is_open: self.serial.close() port = self.cb_port.currentText() baud = self.cb_baud.currentText() try: self.serial = Serial(port, int(baud), timeout=0.1) self.timer.start(100) # 100ms读取一次 self.btn_open.setEnabled(False) self.btn_close.setEnabled(True) except SerialException as e: QMessageBox.critical(self, "错误", str(e)) def close_port(self): """关闭串口""" if self.serial.is_open: self.serial.close() self.timer.stop() self.btn_open.setEnabled(True) self.btn_close.setEnabled(False) def send_data(self): """发送数据""" if not self.serial.is_open: return data = self.text_send.toPlainText() if data: # 如果是以十六进制发送,则转换 if self.cb_hex_send.isChecked(): data = bytes.fromhex(data) else: data = data.encode() self.serial.write(data) def read_data(self): """读取串口数据""" if self.serial and self.serial.is_open: try: n = self.serial.inWaiting() if n: data = self.serial.read(n) # 十六进制显示 if self.cb_hex_recv.isChecked(): data = data.hex().upper() # 每两个字符加一个空格 data = ' '.join([data[i:i+2] for i in range(0, len(data), 2)]) else: try: data = data.decode() except: data = data.hex() # 追加显示 self.text_recv.append(data) except SerialException as e: self.close_port() QMessageBox.critical(self, "错误", str(e)) if __name__ == '__main__': app = QApplication(sys.argv) gui = SerialPortGUI() gui.show() sys.exit(app.exec_()) ``` **注意事项**: - 串口操作需要在独立的线程中进行,但上述示例使用定时器轮询方式,简单但可能不够高效。对于实时性要求高的场景,建议使用多线程。 - 串口数据的解析需要根据实际协议进行,上述示例仅提供了十六进制和文本两种显示方式。 - 错误处理需要完善,比如串口拔出的异常处理等。 **引用说明**: PyQt5提供了强大的GUI功能,而pyserial库则用于串口通信。两者结合可以构建出功能完善的上位机软件[^1]。 §§相关问题§§ 1. 如何优化PyQt5串口通信的实时性? 2. 串口通信中如何处理二进制数据? 3. 如何实现多串口同时通信? 4. 如何保存串口接收的数据到文件? 5. 如何实现自定义的串口通信协议? </think> ### 使用PyQt5实现串口通信上位机的方法 以下是实现串口通信上位机的完整步骤和代码框架: #### 1. **核心依赖库** ```python pip install PyQt5 pyserial # 安装必要库 ``` #### 2. **基础代码框架** ```python import sys import serial from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QComboBox, QPushButton, QTextEdit from PyQt5.QtCore import QTimer class SerialApp(QMainWindow): def __init__(self): super().__init__() self.serial = serial.Serial() self.initUI() self.timer = QTimer(self) self.timer.timeout.connect(self.read_data) def initUI(self): # 串口配置区域 self.port_label = QLabel("端口:", self) self.port_combo = QComboBox(self) self.baud_label = QLabel("波特率:", self) self.baud_combo = QComboBox(self) self.baud_combo.addItems(["9600", "115200", "57600"]) # 控制按钮 self.connect_btn = QPushButton("连接", self) self.connect_btn.clicked.connect(self.toggle_connection) self.send_btn = QPushButton("发送", self) self.send_btn.clicked.connect(self.send_data) # 数据区域 self.receive_text = QTextEdit(self) self.send_text = QTextEdit(self) # 布局代码需根据实际界面设计添加... self.setGeometry(300, 300, 800, 600) self.setWindowTitle('串口调试助手') self.show() def toggle_connection(self): if self.serial.is_open: self.close_serial() else: self.open_serial() def open_serial(self): try: self.serial.port = self.port_combo.currentText() self.serial.baudrate = int(self.baud_combo.currentText()) self.serial.open() self.timer.start(50) # 50ms读取周期 self.connect_btn.setText("断开") except serial.SerialException as e: print(f"打开失败: {e}") def close_serial(self): self.timer.stop() self.serial.close() self.connect_btn.setText("连接") def read_data(self): if self.serial.in_waiting: data = self.serial.read_all().decode('utf-8', errors='ignore') self.receive_text.append(data) def send_data(self): if self.serial.is_open: text = self.send_text.toPlainText() self.serial.write(text.encode()) def closeEvent(self, event): self.close_serial() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) ex = SerialApp() sys.exit(app.exec_()) ``` #### 3. **关键实现细节** 1. **串口扫描**: ```python import serial.tools.list_ports def scan_ports(self): self.port_combo.clear() ports = serial.tools.list_ports.comports() for port in ports: self.port_combo.addItem(port.device) ``` 2. **数据格式处理**: ```python # 十六进制发送 if self.hex_send.isChecked(): data = bytes.fromhex(self.send_text.toPlainText().replace(' ', '')) else: data = self.send_text.toPlainText().encode() ``` 3. **错误处理增强**: ```python try: self.serial.write(data) except serial.SerialTimeoutException: self.receive_text.append("<发送超时>") except serial.SerialException as e: self.receive_text.append(f"<错误: {str(e)}>") ``` #### 4. **高级功能扩展** - **实时波形显示**:使用`PyQtGraph`库绘制传感器数据 - **多线程处理**:通过`QThread`分离串口读写和UI操作 - **协议解析**:添加MODBUS/CAN等协议解析层 - **数据记录**:集成SQLite数据库保存历史数据 #### 5. **推荐开发资源** 1. PyQt5官方文档:[https://doc.qt.io/qtforpython/](https://doc.qt.io/qtforpython/) 2. PySerial文档:[https://pyserial.readthedocs.io](https://pyserial.readthedocs.io) 3. 开源参考项目:QT上位机模板库[^1] > **注意**:实际开发中需注意线程安全,建议使用`QSerialPort`替代PySerial实现更好的Qt集成[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值