遇到的問(wèn)題
1. 程序結(jié)構(gòu)
- 創(chuàng)建一個(gè)主窗口的類(lèi):在里面主要實(shí)現(xiàn)窗口UI的繪制,并定義一些槽函數(shù)接口
- 創(chuàng)建主框架類(lèi):該類(lèi)繼承自主窗口類(lèi),并實(shí)現(xiàn)主窗口類(lèi)中的槽函數(shù),在該類(lèi)中創(chuàng)建串口接收線(xiàn)程
- 串口接收線(xiàn)程類(lèi):該類(lèi)繼承自QtCore.QThread類(lèi)脆贵,主要進(jìn)行串口接收處理
2. 多線(xiàn)程
在程序結(jié)構(gòu)上,需要建立兩個(gè)線(xiàn)程:主線(xiàn)程和串口接收線(xiàn)程嘉汰;主線(xiàn)程在程序啟動(dòng)時(shí)就存在丹禀;串口接收線(xiàn)程主要負(fù)責(zé)在后臺(tái)不斷的讀取串口接收緩存中的數(shù)據(jù),判斷是否有數(shù)據(jù)到來(lái)。多線(xiàn)程通過(guò)創(chuàng)立一個(gè)繼承QtCore.QThread的類(lèi)來(lái)實(shí)現(xiàn)双泪;為什么沒(méi)有使用threading.Thread持搜?因?yàn)闆](méi)有threading.Thread類(lèi)中找到終止線(xiàn)程的API,所以改用QtCore.QThread焙矛。
終止線(xiàn)程:
self.serialThread.quit() # serialThread為我創(chuàng)建的線(xiàn)程實(shí)例對(duì)象
- 點(diǎn)擊窗口的"X"關(guān)閉窗口時(shí)葫盼,需要對(duì)線(xiàn)程資源進(jìn)行清理
當(dāng)點(diǎn)擊窗口的"X"時(shí),我們可以通過(guò)在我們的窗口類(lèi)中重寫(xiě)closeEvent()方法實(shí)現(xiàn)最后的資源清理村斟,在該串口程序中贫导,實(shí)現(xiàn)的功能如下:
# 重寫(xiě)關(guān)閉窗口事件
def closeEvent(self, event):
if self.serialPara['serialPt'].isOpen() == True: # 如果串口打開(kāi)了,說(shuō)明線(xiàn)程正在運(yùn)行蟆盹,需要終止線(xiàn)程
self.serialThread.quit() # 終止線(xiàn)程
- 對(duì)接收的bytes數(shù)據(jù)進(jìn)行顯示處理
通過(guò)串口接收到的數(shù)據(jù)是bytes類(lèi)型孩灯,類(lèi)似于:b'\x12\xde\x7f' 這種形式
對(duì)于0~ 0x7f之間的數(shù)據(jù)可以使用decode()進(jìn)行解碼,但是0x7f之后的數(shù)據(jù)使用decode()解碼時(shí)逾滥,會(huì)提示不能對(duì)utf-8的字符使用decode()進(jìn)行解碼峰档,因?yàn)閍scii碼的范圍在0 ~ 0x7f之間
解決方法:在程序中,需要對(duì)發(fā)送的數(shù)據(jù)進(jìn)行格式選擇:hex發(fā)送或ascii碼發(fā)送兩種形式寨昙,
- 當(dāng)使用ascii碼形式發(fā)送數(shù)據(jù)時(shí)直接使用decode()對(duì)接收到的數(shù)據(jù)進(jìn)行解碼
- 當(dāng)使用hex碼形式發(fā)送數(shù)據(jù)時(shí)讥巡,我們的串口工具需要將數(shù)據(jù)一個(gè)一個(gè)的取出來(lái),然后使用hex()轉(zhuǎn)換為hex形式的數(shù)據(jù)舔哪,然后去除0x欢顷,再將它們以空格為分割符拼接在一起;代碼如下:
def run(self):
print ("啟動(dòng)線(xiàn)程")
while True:
# 獲得接收到的字符
count = self.Ser.inWaiting()
if count != 0:
dealStr = ""
# 讀串口數(shù)據(jù)
recv = self.Ser.read(count)
recv = recv.upper()
# 在這里將接收到數(shù)據(jù)進(jìn)行區(qū)分:hex 或 字符串
# hex 格式:\xYY\xYY\xYY捉蚤,如果接收到的字符是這種格式抬驴,則說(shuō)明是hex字符,我們需要將
# \x去除掉外里,取出YY怎爵,然后組成字符串返回
# 如果接收到的是字符串特石,則使用decode進(jìn)行解碼
print ("接收到的數(shù)據(jù) %s \n類(lèi)型為: %s\n" % (recv, type(recv)))
# 嘗試使用decode解碼盅蝗,如果失敗,則表示接收到的數(shù)據(jù)為hex發(fā)送過(guò)來(lái)的數(shù)據(jù)
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 ("處理后的數(shù)據(jù) %s \n類(lèi)型為: %s\n" % (dealStr, type(dealStr)))
# 顯示接收到的數(shù)據(jù)
self.dispContent(dealStr)
# 清空接收緩沖區(qū)
self.Ser.flushInput()
time.sleep(0.1)
if self.Ser.isOpen() == False:
print ("關(guān)閉線(xiàn)程")
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):
# ---定義屬性
# 發(fā)送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"}
# 串口狀態(tài)姆蘸,打開(kāi)/關(guān)閉
self.isSerialOpen = False
self.sendBuf = []
self.initUI()
def initUI(self):
layout = QtWidgets.QVBoxLayout(self)
# 內(nèi)容顯示區(qū)
self.contentDispText = QtWidgets.QTextEdit()
self.contentDispText.setReadOnly(True)
# 設(shè)置區(qū)
line1_inputLabel = QtWidgets.QLabel(self.tr("輸入框:"))
self.line1_inputLineEdit = QtWidgets.QLineEdit()
self.line1_sendButton = QtWidgets.QPushButton(self.tr("發(fā)送"))
self.line1_clearButton = QtWidgets.QPushButton(self.tr("清除"))
self.line1_hexdispCheckBox = QtWidgets.QCheckBox(self.tr("HEX顯示"))
# 第二行
line2_serialNoLabel = QtWidgets.QLabel(self.tr("串口號(hào):"))
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("打開(kāi)串口"))
# 此處還要添加一個(gè)指示燈墩莫,考慮是否可以使用圖片表示工作狀態(tài)
self.line2_hexSendCheckBox = QtWidgets.QCheckBox(self.tr("HEX發(fā)送"))
self.line2_hexSendCheckBox.setChecked(True)
# 第三行
line3_dataBitLabel = QtWidgets.QLabel(self.tr("數(shù)據(jù)位:"))
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("校驗(yàn)位:"))
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)
# 此處還需要添加一個(gè)擴(kuò)展功能
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)
# -----對(duì)控件進(jìn)行初始化----
self.line1_sendButton.setEnabled(False) # 在沒(méi)打開(kāi)串口時(shí),設(shè)置串口為無(wú)法操作的狀態(tài)
# -----為按鈕添加事件-----
# 串口開(kāi)關(guān)操作
#self.line2_OpenSerialButton.clicked.connect(self.serialState)
# 這里省略了receiver逞敷,使用的是connect中的一個(gè)重載函數(shù)狂秦,receiver默認(rèn)為this
# 在PYQT4.5之后,這是一種新的信號(hào)和槽的 API推捐,老的例子API: button.clicked.connect(self.onClicked)
self.line2_OpenSerialButton.clicked.connect(self.serialState)
#self.connect(self.line2_OpenSerialButton, QtCore.SIGNAL("clicked()"), self.serialState)
# 發(fā)送數(shù)據(jù)操作
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()"))
# 窗口的關(guān)閉按鈕
#self.connect(self, QtCore.SIGNAL(""))
def setSerialState(self, state):
pass
# 顯示收發(fā)數(shù)據(jù)
def dispContent(self, argvStr):
pass
#self.contentDispText.append(r'\n')
# 處理輸入的數(shù)據(jù)
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 ("創(chuàng)建線(xiàn)程")
def run(self):
print ("啟動(dòng)線(xiàn)程")
while True:
# 獲得接收到的字符
count = self.Ser.inWaiting()
if count != 0:
dealStr = ""
# 讀串口數(shù)據(jù)
recv = self.Ser.read(count)
# 在這里將接收到數(shù)據(jù)進(jìn)行區(qū)分:hex 或 字符串
# hex 格式:\xYY\xYY\xYY裂问,如果接收到的字符是這種格式,則說(shuō)明是hex字符,我們需要將
# \x去除掉堪簿,取出YY痊乾,然后組成字符串返回
# 如果接收到的是字符串,則使用decode進(jìn)行解碼
print ("接收到的數(shù)據(jù) %s \n類(lèi)型為: %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 ("處理后的數(shù)據(jù) %s \n類(lèi)型為: %s\n" % (dealStr, type(dealStr)))
# 顯示接收到的數(shù)據(jù)
self.dispContent(dealStr)
# 清空接收緩沖區(qū)
self.Ser.flushInput()
time.sleep(0.1)
if self.Ser.isOpen() == False:
print ("關(guān)閉線(xiàn)程")
self.quit()
return
class SerialFrame(MainDialog):
def __init__(self, parent = None):
super(SerialFrame, self).__init__(parent)
# 重寫(xiě)關(guān)閉窗口事件
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("關(guān)閉串口"))
else:
self.line2_OpenSerialButton.setText(self.tr("打開(kāi)串口"))
# 設(shè)置串口其他參數(shù)的控件為不可設(shè)置狀態(tài)
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) # 在沒(méi)打開(kāi)串口時(shí)椭更,設(shè)置串口為無(wú)法操作的狀態(tài)
# 顯示收發(fā)數(shù)據(jù)
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')
# 處理輸入的數(shù)據(jù)
def dealInputData(self, argv):
# 將QString轉(zhuǎn)換為string哪审,因?yàn)橐褂胮ython內(nèi)置的函數(shù),必須進(jìn)行轉(zhuǎn)換
strList = str(argv)
if len(strList) == 0:
QtWidgets.QMessageBox.information(self, "警告", "請(qǐng)輸入字符后虑瀑,再發(fā)送湿滓!", QtWidgets.QMessageBox.Ok)
return "InPuT eRRoR"
else:
# 1. 判斷是HEX發(fā)送還是字符串發(fā)送
isHex = self.line2_hexSendCheckBox.isChecked()
if isHex == True: # HEX發(fā)送
strList = strList.strip() # 去除兩邊的空格
strList = strList.upper() # 轉(zhuǎn)換為大寫(xiě)字母
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]
# 到這一步時(shí)字符已經(jīng)被處理為:ff55aacc01
# 通過(guò)decode("hex")進(jìn)行處理后的數(shù)據(jù)就是兩個(gè)字符為一組的十進(jìn)制數(shù)據(jù)
# 進(jìn)行異常處理舌狗,當(dāng)進(jìn)行數(shù)據(jù)格式轉(zhuǎn)換時(shí)叽奥,因?yàn)榭赡苡泻芏喾N情況導(dǎo)致轉(zhuǎn)換失敗,所以此處使用異常處理
try:
return codecs.decode(getStr, "hex_codec")
except ValueError:
print ("abdc")
QtWidgets.QMessageBox.information(self, "警告", "請(qǐng)輸入十六進(jìn)制字符!", QtWidgets.QMessageBox.Ok)
return "InPuT eRRoR"
else:
# 字符串發(fā)送痛侍,轉(zhuǎn)換為utf-8格式
return strList.encode("utf-8")
def writeStr(self):
# 讀取數(shù)據(jù)并返回
inputStr = self.line1_inputLineEdit.text()
# 處理數(shù)據(jù)
list = self.dealInputData(inputStr)
if list == "InPuT eRRoR":
return
else:
# 發(fā)送數(shù)據(jù)
self.serialPara["serialPt"].write(list)
# 將發(fā)送的數(shù)據(jù)顯示在文本框中
self.dispContent(inputStr)
def serialState(self):
if self.isSerialOpen == False:
try:
# 獲取選擇的串口編號(hào)
self.serialPara["comNo"] = self.line2_serialComboBox.currentIndex()
# 獲取串口參數(shù)信息:波特率而线、數(shù)據(jù)位、校驗(yàn)位恋日、停止位
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()]
# 打開(kāi)串口
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()
# 啟動(dòng)線(xiàn)程
self.serialThread = ReceiveThread(self.serialPara["serialPt"], self.dispContent)
self.serialThread.start()
except Exception:
QtWidgets.QMessageBox.information(self, "警告", "串口打開(kāi)失敗", QtWidgets.QMessageBox.Ok)
return
# print "端口號(hào): %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_()