[pyqt5-pyserial实现一个串口调试工具记录]

遇到的问题

1. 程序结构

  • 创建一个主窗口的类:在里面主要实现窗口UI的绘制,并定义一些槽函数接口
  • 创建主框架类:该类继承自主窗口类,并实现主窗口类中的槽函数,在该类中创建串口接收线程
  • 串口接收线程类:该类继承自QtCore.QThread类,主要进行串口接收处理

2. 多线程
在程序结构上,需要建立两个线程:主线程串口接收线程;主线程在程序启动时就存在;串口接收线程主要负责在后台不断的读取串口接收缓存中的数据,判断是否有数据到来。多线程通过创立一个继承QtCore.QThread的类来实现;为什么没有使用threading.Thread?因为没有threading.Thread类中找到终止线程的API,所以改用QtCore.QThread。
终止线程:
self.serialThread.quit() # serialThread为我创建的线程实例对象

  1. 点击窗口的"X"关闭窗口时,需要对线程资源进行清理
    当点击窗口的"X"时,我们可以通过在我们的窗口类中重写closeEvent()方法实现最后的资源清理,在该串口程序中,实现的功能如下:
# 重写关闭窗口事件
    def closeEvent(self, event):
        if self.serialPara['serialPt'].isOpen() == True:  # 如果串口打开了,说明线程正在运行,需要终止线程
            self.serialThread.quit()  # 终止线程
  1. 对接收的bytes数据进行显示处理
    通过串口接收到的数据是bytes类型,类似于:b'\x12\xde\x7f' 这种形式

对于0~ 0x7f之间的数据可以使用decode()进行解码,但是0x7f之后的数据使用decode()解码时,会提示不能对utf-8的字符使用decode()进行解码,因为ascii码的范围在0 ~ 0x7f之间

解决方法:在程序中,需要对发送的数据进行格式选择:hex发送或ascii码发送两种形式,

  1. 当使用ascii码形式发送数据时直接使用decode()对接收到的数据进行解码
  2. 当使用hex码形式发送数据时,我们的串口工具需要将数据一个一个的取出来,然后使用hex()转换为hex形式的数据,然后去除0x,再将它们以空格为分割符拼接在一起;代码如下:
def run(self):
        print ("启动线程")
        while True:
            # 获得接收到的字符
            count = self.Ser.inWaiting()
            if count != 0:
                dealStr = ""
                # 读串口数据
                recv = self.Ser.read(count)
                recv = recv.upper()
                # 在这里将接收到数据进行区分:hex 或 字符串
                # hex 格式:\xYY\xYY\xYY,如果接收到的字符是这种格式,则说明是hex字符,我们需要将
                # \x去除掉,取出YY,然后组成字符串返回
                # 如果接收到的是字符串,则使用decode进行解码
                print ("接收到的数据 %s \n类型为: %s\n" % (recv,  type(recv)))
                # 尝试使用decode解码,如果失败,则表示接收到的数据为hex发送过来的数据
                try:
                    dealStr = recv.decode()
                except (TypeError,  UnicodeDecodeError):
                    for i in range(len(recv)):
                        print (hex(recv[i])[2:])
                        dealStr += hex(recv[i])[2:]
                        dealStr +=' '
                    dealStr.rstrip(' ')
                print ("处理后的数据 %s \n类型为: %s\n" % (dealStr,  type(dealStr)))
                
                # 显示接收到的数据
                self.dispContent(dealStr)
                # 清空接收缓冲区
                self.Ser.flushInput()
            time.sleep(0.1)
            if self.Ser.isOpen() == False:
                print ("关闭线程")
                self.quit()
                return

程序代码如下:

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

from PyQt5 import QtGui
from PyQt5 import QtCore
from PyQt5 import QtWidgets 
import sys
import serial
import win32api
import win32com
import binascii
import struct
import time
import threading
import codecs  

class MainDialog(QtWidgets.QDialog):
    # ---定义属性
    # 发送buf
    def __init__(self, parent = None):
        super(MainDialog, self).__init__()
        self.setWindowTitle(self.tr("串口助手"))

        self.serialNo = ("COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9")
        self.serialBaud = (1200, 2400, 4800, 9600, 14400, 19200, 38400, 56000, 57600, 115200)
        self.serialChk = {"None": serial.PARITY_NONE,
                          "Odd": serial.PARITY_EVEN,
                          "Even": serial.PARITY_ODD}
        self.serialChkCode = ("None", "Odd", "Even")
        self.serialStopBitCode = (serial.STOPBITS_ONE, serial.STOPBITS_ONE_POINT_FIVE, serial.STOPBITS_TWO)
        self.serialPara  = {
                          "comNo": 0,
                          "baud": 9600,
                          "stopBit": 1,
                          "dataBit": 8,
                          "chk": "odd",
                          "serialPt": "串口1"}
        # 串口状态,打开/关闭
        self.isSerialOpen = False
        self.sendBuf = []
        self.initUI()


    def initUI(self):
        layout = QtWidgets.QVBoxLayout(self)
        # 内容显示区
        self.contentDispText = QtWidgets.QTextEdit()
        self.contentDispText.setReadOnly(True)
        # 设置区
        line1_inputLabel = QtWidgets.QLabel(self.tr("输入框:"))
        self.line1_inputLineEdit = QtWidgets.QLineEdit()
        self.line1_sendButton = QtWidgets.QPushButton(self.tr("发送"))
        self.line1_clearButton = QtWidgets.QPushButton(self.tr("清除"))
        self.line1_hexdispCheckBox = QtWidgets.QCheckBox(self.tr("HEX显示"))

        # 第二行
        line2_serialNoLabel = QtWidgets.QLabel(self.tr("串口号:"))
        self.line2_serialComboBox = QtWidgets.QComboBox()
        for uart in self.serialNo:
            self.line2_serialComboBox.insertItem(self.serialNo.index(uart), self.tr(uart))

        line2_serialBaudLabel = QtWidgets.QLabel(self.tr("波特率:"))
        self.line2_serialBaudComboBox = QtWidgets.QComboBox()
        for baud in self.serialBaud:
            self.line2_serialBaudComboBox.insertItem(self.serialBaud.index(baud), self.tr(str(baud)))
        self.line2_serialBaudComboBox.setCurrentIndex(3)

        self.line2_OpenSerialButton = QtWidgets.QPushButton(self.tr("打开串口"))

        # 此处还要添加一个指示灯,考虑是否可以使用图片表示工作状态

        self.line2_hexSendCheckBox = QtWidgets.QCheckBox(self.tr("HEX发送"))
        self.line2_hexSendCheckBox.setChecked(True)

        # 第三行
        line3_dataBitLabel = QtWidgets.QLabel(self.tr("数据位:"))
        self.line3_dataBitComboBox = QtWidgets.QComboBox()
        i = 0
        for bit in range(5, 9):
            self.line3_dataBitComboBox.insertItem(i, self.tr(str(bit)))
            i += 1
        self.line3_dataBitComboBox.setCurrentIndex(3)
        line3_checkLabel = QtWidgets.QLabel(self.tr("校验位:"))
        self.line3_checkComboBox = QtWidgets.QComboBox()
        for chk in self.serialChkCode:
            self.line3_checkComboBox.insertItem(self.serialChkCode.index(chk), self.tr(chk))
        line3_stopBitLabel = QtWidgets.QLabel(self.tr("停止位:"))
        self.line3_stopBitComboBox = QtWidgets.QComboBox()
        self.line3_stopBitComboBox.insertItem(0, self.tr('1'))
        self.line3_stopBitComboBox.insertItem(1, self.tr('1.5'))
        self.line3_stopBitComboBox.insertItem(2, self.tr('2'))
        self.line3_stopBitComboBox.setCurrentIndex(0)

        # 此处还需要添加一个扩展功能
        setHbox = QtWidgets.QHBoxLayout()
        setGridLayout =  QtWidgets.QGridLayout()
        setGridLayout.setContentsMargins(0,  0,  0,  0)
        setGridLayout.setSpacing(10)
        setGridLayout.addWidget(line1_inputLabel, 0, 0)
        setGridLayout.addWidget(self.line1_inputLineEdit, 0, 1, 1, 3)
        setGridLayout.addWidget(self.line1_sendButton, 0, 4)
        setGridLayout.addWidget(self.line1_clearButton, 0, 5)
        setGridLayout.addWidget(self.line1_hexdispCheckBox, 0, 6)

        setGridLayout.addWidget(line2_serialNoLabel, 1, 0)
        setGridLayout.addWidget(self.line2_serialComboBox, 1, 1)
        setGridLayout.addWidget(line2_serialBaudLabel, 1, 2)
        setGridLayout.addWidget(self.line2_serialBaudComboBox, 1, 3)
        setGridLayout.addWidget(self.line2_OpenSerialButton, 1, 4)
        setGridLayout.addWidget(self.line2_hexSendCheckBox, 1, 6)

        setGridLayout.addWidget(line3_dataBitLabel, 2, 0)
        setGridLayout.addWidget(self.line3_dataBitComboBox, 2, 1)
        setGridLayout.addWidget(line3_checkLabel, 2, 2)
        setGridLayout.addWidget(self.line3_checkComboBox, 2, 3)
        setGridLayout.addWidget(line3_stopBitLabel, 2, 4)
        setGridLayout.addWidget(self.line3_stopBitComboBox, 2, 5)

        setHbox.addLayout(setGridLayout)
        setHbox.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
        layout.addWidget(self.contentDispText)
        layout.addLayout(setHbox)

        # -----对控件进行初始化----
        self.line1_sendButton.setEnabled(False)     # 在没打开串口时,设置串口为无法操作的状态
        # -----为按钮添加事件-----
        # 串口开关操作
        #self.line2_OpenSerialButton.clicked.connect(self.serialState)
        # 这里省略了receiver,使用的是connect中的一个重载函数,receiver默认为this
        # 在PYQT4.5之后,这是一种新的信号和槽的 API,老的例子API:  button.clicked.connect(self.onClicked)
        self.line2_OpenSerialButton.clicked.connect(self.serialState)
        #self.connect(self.line2_OpenSerialButton, QtCore.SIGNAL("clicked()"),  self.serialState)
        # 发送数据操作
        self.line1_sendButton.clicked.connect(self.writeStr)
        #self.connect(self.line1_sendButton, QtCore.SIGNAL("clicked()"),   self.writeStr)
        # 清除按钮
        self.line1_clearButton.clicked.connect(self.contentDispText.clear)
        #self.connect(self.line1_clearButton, QtCore.SIGNAL("clicked()"), self.contentDispText, QtCore.SLOT("clear()"))
        # 窗口的关闭按钮

        #self.connect(self, QtCore.SIGNAL(""))
    def setSerialState(self, state):
        pass

    # 显示收发数据
    def dispContent(self, argvStr):
        pass
        #self.contentDispText.append(r'\n')

    # 处理输入的数据
    def dealInputData(self, argv):
        pass

    def writeStr(self):
        pass

    def serialState(self):
        pass


class ReceiveThread(QtCore.QThread):
    def __init__(self, Ser, dispContent):
        super(ReceiveThread, self).__init__()
        self.Ser = Ser
        self.dispContent = dispContent
        print ("创建线程")

    def run(self):
        print ("启动线程")
        while True:
            # 获得接收到的字符
            count = self.Ser.inWaiting()
            if count != 0:
                dealStr = ""
                # 读串口数据
                recv = self.Ser.read(count)
                # 在这里将接收到数据进行区分:hex 或 字符串
                # hex 格式:\xYY\xYY\xYY,如果接收到的字符是这种格式,则说明是hex字符,我们需要将
                # \x去除掉,取出YY,然后组成字符串返回
                # 如果接收到的是字符串,则使用decode进行解码
                print ("接收到的数据 %s \n类型为: %s\n" % (recv,  type(recv)))
                try:
                    dealStr = recv.decode()
                except (TypeError,  UnicodeDecodeError):
                    for i in range(len(recv)):
                        print ("不可以吗")
                        print (hex(recv[i])[2:])
                        dealStr += hex(recv[i])[2:]
                        dealStr +=' '
                    
                print ("处理后的数据 %s \n类型为: %s\n" % (dealStr,  type(dealStr)))
                
                # 显示接收到的数据
                self.dispContent(dealStr)
                # 清空接收缓冲区
                self.Ser.flushInput()
            time.sleep(0.1)
            if self.Ser.isOpen() == False:
                print ("关闭线程")
                self.quit()
                return


class SerialFrame(MainDialog):
    def __init__(self, parent = None):
        super(SerialFrame, self).__init__(parent)

    # 重写关闭窗口事件
    def closeEvent(self, event):
        if self.serialPara['serialPt'].isOpen() == True:
            self.serialThread.quit()

    def getSerialPt(self):
        return self.serialPara['serialPt']

    def setSerialState(self, state):
        self.isSerialOpen = state
        if self.isSerialOpen == True:
            self.line2_OpenSerialButton.setText(self.tr("关闭串口"))
        else:
            self.line2_OpenSerialButton.setText(self.tr("打开串口"))
        # 设置串口其他参数的控件为不可设置状态
        self.line2_serialComboBox.setDisabled(state)
        self.line2_serialBaudComboBox.setDisabled(state)
        self.line3_stopBitComboBox.setDisabled(state)
        self.line3_checkComboBox.setDisabled(state)
        self.line3_dataBitComboBox.setDisabled(state)
        self.line1_sendButton.setEnabled(state)  # 在没打开串口时,设置串口为无法操作的状态

    # 显示收发数据
    def dispContent(self, argvStr):
        isHexDisp = self.line1_hexdispCheckBox.isChecked()
        argvStr = str(argvStr)
        if isHexDisp == False:
            self.contentDispText.append(argvStr)
        else:
            s = ""
            for i in range(len(argvStr)):
                hval = ord(argvStr[i])
                hhex = "%02x" % hval
                s += hhex + ' '
            self.contentDispText.append(s)
            # self.contentDispText.append(r'\n')

    # 处理输入的数据
    def dealInputData(self, argv):
        # 将QString转换为string,因为要使用python内置的函数,必须进行转换
        strList = str(argv)
        if len(strList) == 0:
            QtWidgets.QMessageBox.information(self, "警告", "请输入字符后,再发送!", QtWidgets.QMessageBox.Ok)
            return "InPuT eRRoR"
        else:
            # 1. 判断是HEX发送还是字符串发送
            isHex = self.line2_hexSendCheckBox.isChecked()
            if isHex == True:  # HEX发送
                strList = strList.strip()  # 去除两边的空格
                strList = strList.upper()  # 转换为大写字母
                list = []
                list = strList.split()  # 以空格为分隔符分隔字符串,并存入list列表中
                getStr = ""
                # 假如输入的字符是:ff 55 aa cc 01
                for i in range(len(list)):
                    if len(list[i]) < 2:
                        list[i] = '0' + list[i]
                    getStr += list[i]
                # 到这一步时字符已经被处理为:ff55aacc01
                # 通过decode("hex")进行处理后的数据就是两个字符为一组的十进制数据
                # 进行异常处理,当进行数据格式转换时,因为可能有很多种情况导致转换失败,所以此处使用异常处理
                try:
                    return codecs.decode(getStr, "hex_codec")
                except ValueError:
                    print ("abdc")
                    QtWidgets.QMessageBox.information(self, "警告", "请输入十六进制字符!", QtWidgets.QMessageBox.Ok)
                    return "InPuT eRRoR"
            else:
                # 字符串发送,转换为utf-8格式
                return strList.encode("utf-8")

    def writeStr(self):
        # 读取数据并返回
        inputStr = self.line1_inputLineEdit.text()
        # 处理数据
        list = self.dealInputData(inputStr)
        if list == "InPuT eRRoR":
            return
        else:
            # 发送数据
            self.serialPara["serialPt"].write(list)
            # 将发送的数据显示在文本框中
            self.dispContent(inputStr)

    def serialState(self):
        if self.isSerialOpen == False:
            try:
                # 获取选择的串口编号
                self.serialPara["comNo"] = self.line2_serialComboBox.currentIndex()
                # 获取串口参数信息:波特率、数据位、校验位、停止位
                self.serialPara["baud"] = self.serialBaud[self.line2_serialBaudComboBox.currentIndex()]
                self.serialPara["dataBit"] = int(self.line3_dataBitComboBox.currentText())
                self.serialPara["chk"] = self.serialChk[self.serialChkCode[self.line3_checkComboBox.currentIndex()]]
                self.serialPara["stopBit"] = self.serialStopBitCode[self.line3_stopBitComboBox.currentIndex()]

                # 打开串口
                self.serialPara["serialPt"] = serial.Serial()
                self.serialPara["serialPt"].baudrate = self.serialPara["baud"]
                self.serialPara["serialPt"].parity = self.serialPara["chk"]
                self.serialPara["serialPt"].stopbits = self.serialPara["stopBit"]
                self.serialPara["serialPt"].bytesize = self.serialPara["dataBit"]
                self.serialPara["serialPt"].port = self.serialPara["comNo"]

                self.serialPara["serialPt"].open()
                # 启动线程
                self.serialThread = ReceiveThread(self.serialPara["serialPt"], self.dispContent)
                self.serialThread.start()

            except Exception:
                QtWidgets.QMessageBox.information(self, "警告", "串口打开失败", QtWidgets.QMessageBox.Ok)
                return

                # print "端口号: %d" % self.serialPara["comNo"]
        else:
            self.serialPara["serialPt"].close()
        self.setSerialState(not self.isSerialOpen)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    m = SerialFrame()
    #event = SerialOperate(m)
    m.show()
    app.exec_()



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,216评论 11 349
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,952评论 1 18
  • 关键词:易到 定位 速度 人性 字数:1840字,建议阅读时间8分钟 8月21日,艾问.人物采访了前易到CEO周航...
    Tonytoni阅读 828评论 0 3
  • 1 她的姐姐出生于1989年,那时候经济与科技还没有今天的辉煌与成就,由金庸先生所著的,94版《射雕英雄传》及9...
    熏风习习阅读 264评论 0 0