遇到的问题
1. 程序结构
- 创建一个主窗口的类:在里面主要实现窗口UI的绘制,并定义一些槽函数接口
- 创建主框架类:该类继承自主窗口类,并实现主窗口类中的槽函数,在该类中创建串口接收线程
- 串口接收线程类:该类继承自QtCore.QThread类,主要进行串口接收处理
2. 多线程
在程序结构上,需要建立两个线程:主线程和串口接收线程;主线程在程序启动时就存在;串口接收线程主要负责在后台不断的读取串口接收缓存中的数据,判断是否有数据到来。多线程通过创立一个继承QtCore.QThread的类来实现;为什么没有使用threading.Thread?因为没有threading.Thread类中找到终止线程的API,所以改用QtCore.QThread。
终止线程:
self.serialThread.quit() # serialThread为我创建的线程实例对象
- 点击窗口的"X"关闭窗口时,需要对线程资源进行清理
当点击窗口的"X"时,我们可以通过在我们的窗口类中重写closeEvent()方法实现最后的资源清理,在该串口程序中,实现的功能如下:
# 重写关闭窗口事件
def closeEvent(self, event):
if self.serialPara['serialPt'].isOpen() == True: # 如果串口打开了,说明线程正在运行,需要终止线程
self.serialThread.quit() # 终止线程
- 对接收的bytes数据进行显示处理
通过串口接收到的数据是bytes类型,类似于:b'\x12\xde\x7f' 这种形式
对于0~ 0x7f之间的数据可以使用decode()进行解码,但是0x7f之后的数据使用decode()解码时,会提示不能对utf-8的字符使用decode()进行解码,因为ascii码的范围在0 ~ 0x7f之间
解决方法:在程序中,需要对发送的数据进行格式选择:hex发送或ascii码发送两种形式,
- 当使用ascii码形式发送数据时直接使用decode()对接收到的数据进行解码
- 当使用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_()