基于Python+PyQt5實(shí)現(xiàn)串口數(shù)據(jù)采集和顯示
本節(jié)我們將會(huì)通過(guò)PyQt5
實(shí)現(xiàn)串口數(shù)據(jù)采集和實(shí)時(shí)通信,涉及到的技術(shù)棧包括:Python
、PyQt5
。
一、環(huán)境搭建
1.1 Python 3.X安裝
直接從官網(wǎng)下載安裝包:Index of /ftp/python/;
這里我下載的包為https://www.python.org/ftp/python/3.9.6/python-3.9.6-amd64.exe
,安裝版本:python 3.9.6
。
雙擊開(kāi)始安裝的時(shí)候,一定要把下面的 Add Path
勾上 (表示添加到環(huán)境變量,這樣cmd
也能使用了),其他一路Next
安裝完成。默認(rèn)會(huì)安裝一鍵式工具pip
。
pip
工具鏡像源配置。配置方法如下:
- 在
cmd
窗口下執(zhí)行echo %HOMEPATH%
獲取用戶HOME
目錄,并在該目錄下創(chuàng)建pip
目錄; - 在
pip
目錄下創(chuàng)建pip.ini
文件。記住,后綴必須是.ini
格式。并在該文件中寫入如下內(nèi)容;
內(nèi)容如下:
[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host = pypi.tuna.tsinghua.edu.cn
1.2 安裝Pycharm
官方網(wǎng)站:http://www.jetbrains.com/pycharm/,提供以下安裝版本:
Professional
:專業(yè)版(收費(fèi),網(wǎng)上一大堆破解方法)Community
:社區(qū)版(免費(fèi),我用的這個(gè)),下載版本為pycharm-community-2023.3.4.exe
;
1.3 PyQt5安裝
使用pip
工具安裝PyQt5
工具,執(zhí)行:
pip install pyqt5
如果慢,用國(guó)內(nèi)源:
pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple
使用pip
工具安裝PyQt5-tools
工具,執(zhí)行:
pip install pyqt5-tools
如果慢,用國(guó)內(nèi)源:
pip install pyqt5-tools -i https://pypi.tuna.tsinghua.edu.cn/simple
工具安裝完成后的路徑在E:\Program Files\Python\Lib\site-packages
。
PyQt5
主要有三個(gè)部分:
QtCore
: 包含了核心的非GUI
的功能。主要和時(shí)間、文件與文件夾、各種數(shù)據(jù)、模型、流、URLs
、mime
類文件、進(jìn)程與線程一起使用;QtGui
: 包含了窗口系統(tǒng)、事件處理、2D圖像、基本繪畫、字體和文字類;QtWidgets
: 包含了一些創(chuàng)建桌面的UI
元素和控件;
1.4 環(huán)境配置
PyCharm
是開(kāi)發(fā)Python
程序主流常用的IDE。為方便調(diào)用Qt Designer
實(shí)現(xiàn)界面開(kāi)發(fā)和編譯相應(yīng)完成,可以在PyCharm
配置Qt Designer
和PyUIC
、Pyrcc
。
其中Qt Designer
是Qt
設(shè)計(jì)師,PyUics
是把UI
界面轉(zhuǎn)換成py
文件,Pyrcc
是資源系統(tǒng)轉(zhuǎn)換。
打開(kāi)PyCharm
, 新建一個(gè)項(xiàng)目,項(xiàng)目名稱為serial_port
。
這里Python path
選擇我們之前安裝的python 3.9.6
的路徑:E:\Program Files\Python\python.exe
。
1.4.1 配置Qt Designer
菜單File
->Settings
-> Tools
-> External Tools
-> +
號(hào),進(jìn)行添加。 參數(shù)配置說(shuō)明:
- Name:填入Qt Designer,實(shí)際可以任意取值;
- Program:designer.exe程序絕對(duì)路徑。根據(jù)實(shí)際安裝路徑填寫,這里我配置的是E:\Program Files\Python\Lib\site-packages\qt5_applications\Qt\bin\designer.exe;
- Working directory: 填入$FileDir$,固定取值;
具體如下:
1.4.2 配置PyUIC
該工具是用于將Qt Designer
工具開(kāi)發(fā)完成的.ui
文件轉(zhuǎn)化為.py
文件。配置打開(kāi)路徑同Qt Designer
,參數(shù)配置說(shuō)明:
Name
:填入PyUIC
,實(shí)際可以任意取值。Program
:python.exe
程序絕對(duì)路徑,根據(jù)實(shí)際安裝路徑填寫,這里我配置的是E:\Program Files\Python\python.exe
;Arguments
:-m PyQt5.uic.pyuic $FileName$ -o $FileNameWithoutExtension$.py
;Working directory
: 填入$FileDir$
,固定取值;
具體如下:
1.4.3 配置Pyqcc
配置打開(kāi)路徑同Qt Designer
。參數(shù)配置說(shuō)明:
Name
:填入Pyqcc
,實(shí)際可以任意取值。Program
:這里我配置的是E:\Program Files\Python\Scripts\pyrcc5.exe
;Arguments
:$FileName$ -o $FileNameWithoutExtension$_rc.py
;Working directory
: 填入$FileDir$
,固定取值;
具體如下:
1.5 測(cè)試
測(cè)試Qt Designer
和PyUIC
、Pyqcc
配置是否成功。打開(kāi)路徑:菜單欄Tools
->External Tools
->Qt Designer
/PyUIC
/Pyqcc
;
1.5.1 ui_serial_port.ui
點(diǎn)擊Qt Designer
,打開(kāi)Designer
程序主主界面,會(huì)彈出一個(gè)窗口,這里一般是選擇Main Window
或者Widget
,其中Main Window
繼承自Widget
,添加了一些內(nèi)容,本質(zhì)二者差不多。這里選擇的是Main Window
;
將左側(cè)Widget Box
中Push button
空間拖到主界面,Ctrl + S
保存名稱ui_serial_port.ui
,默認(rèn)后綴就是.ui
。
1.5.2 ui_serial_port.py
選中ui_serial_port.ui
文件,同理點(diǎn)擊PyUIC
,自動(dòng)完成ui_serial_port.ui
文件的轉(zhuǎn)換,生成文件名為ui_serial_port.py
。
1.5.3 serial_port.py
除ui
界面代碼,還需要有一個(gè)邏輯代碼,而邏輯代碼個(gè)人感覺(jué)使用類的形式來(lái)組織更加方便,也更優(yōu)雅。
還記得創(chuàng)建ui
時(shí)選擇的類嗎?是Widget
還是Main Window
,邏輯代碼類最好是繼承這個(gè)這個(gè)類,即QWidget
或QMainWindow
。一般的代碼結(jié)構(gòu)如下所示:
from PyQt5.QtWidgets import QMainWindow # 導(dǎo)入設(shè)計(jì)的ui界面轉(zhuǎn)換成的py文件 from ui_serial_port import Ui_MainWindow class SerialPort(QMainWindow): """ 串口行為 """ def __init__(self): # QMainWindow構(gòu)造函數(shù)初始化 super().__init__() self.ui = Ui_MainWindow() # 這個(gè)函數(shù)本身需要傳遞一個(gè)MainWindow類,而該類本身就繼承了這個(gè),所以可以直接傳入self self.ui.setupUi(self)
1.5.4 main.py
在當(dāng)前項(xiàng)目下,新建main.py
文件;
import sys from serial_port import SerialPort from PyQt5.QtWidgets import QApplication, QMainWindow if __name__ == '__main__': # 先建立一個(gè)app app = QApplication(sys.argv) # 初始化一個(gè)對(duì)象,調(diào)用init函數(shù),已加載設(shè)計(jì)的ui文件 ui = SerialPort() # 顯示這個(gè)ui ui.show() # 運(yùn)行界面,響應(yīng)按鈕等操作 sys.exit(app.exec_())
運(yùn)行程序:
二、程序設(shè)計(jì)
2.1 需求
客戶這里有一款慣性設(shè)備,在慣性裝置的AXS31
接口,里面有兩路數(shù)據(jù),一路稱為導(dǎo)航解算,一路稱為原始信息。我們通過(guò)串口讀取該設(shè)備的數(shù)據(jù)并在界面顯示處理,同時(shí)還需要將讀取到的數(shù)據(jù)保存到文本中。
實(shí)際在測(cè)試時(shí)候發(fā)現(xiàn)只能接收到導(dǎo)航解算報(bào)文,因此猜測(cè)原始信息已經(jīng)被傳感器內(nèi)部轉(zhuǎn)換為導(dǎo)航解算報(bào)文了。
2.1.1 導(dǎo)航解算報(bào)文
波特率:230400,數(shù)據(jù)位8,停止位1,無(wú)校驗(yàn);
字節(jié) | 意義 | 類型 | 所占字節(jié) | 備注 |
---|---|---|---|---|
1-2 | 報(bào)文頭 | 2字節(jié) | 5a 5a | |
3 | 工作狀態(tài) | 1字節(jié) | 0xFF等待對(duì)準(zhǔn) 0x00碼頭對(duì)準(zhǔn) 0x01海上對(duì)準(zhǔn) 0x02牽引對(duì)準(zhǔn) 0x03 是無(wú)阻尼 0x04是慣導(dǎo)阻尼 0x05 點(diǎn)校 0x06 綜合校正 0x07 位置組合 | |
4 | 參數(shù)狀態(tài) | 1字節(jié) | B8 =0手動(dòng) B8=1自動(dòng) | |
5-8 | 運(yùn)行時(shí)間 | 4字節(jié) | 單位0.05s | |
9-11 | 緯度 | 3字節(jié) | 量綱93206.75556 | |
12-14 | 經(jīng)度 | 3字節(jié) | 量綱46603.37778 | |
15-16 | 升沉 | 2字節(jié) | 最小量綱100m | |
17-18 | 東速 | 2字節(jié) | 最小量綱100kn | |
19-20 | 北速 | 2字節(jié) | 最小量綱100kn | |
21-22 | 垂速 | 2字節(jié) | 最小量綱100m/s | |
23-25 | 姿態(tài)角1 | 3字節(jié) | 最小量綱0.25*93206.75556 | |
26-28 | 姿態(tài)角2 | 3字節(jié) | 最小量綱93206.75556 | |
29-31 | 姿態(tài)角3 | 3字節(jié) | 最小量綱93206.75556 | |
32-34 | 姿態(tài)角速率1(縱搖角速率) | 3字節(jié) | 93206.75556 度每秒 | |
35-37 | 姿態(tài)角速率2 (橫搖角速率) | 3字節(jié) | 93206.75556 度每秒 | |
38-40 | 姿態(tài)角速率3(航向角速率) | 3字節(jié) | 93206.75556度每秒 | |
41 | 故障碼 | 1字節(jié) | B0=1 IMU接收錯(cuò) B1=1 測(cè)角采樣錯(cuò) B2=1 接收緩存錯(cuò) B3=1 測(cè)角控制板錯(cuò) B4=1 驅(qū)動(dòng)錯(cuò) B5=1 測(cè)角錯(cuò) B6=1 激磁錯(cuò) B7=1 轉(zhuǎn)臺(tái)保護(hù)錯(cuò) | |
42-43 | IMUtime | 2字節(jié) | ||
44-47 | 備用 | 4字節(jié) | ||
48 | 應(yīng)答標(biāo)志 | 1字節(jié) | 可忽略 | |
49 | 校驗(yàn)和 | 1字節(jié) | 3-48字節(jié)累加和 |
2.1.2 原始信息報(bào)文
波特率:230400,數(shù)據(jù)位8,停止位1,無(wú)校驗(yàn);
字節(jié) | 意義 | 類型 | 所占字節(jié) | 備注 |
---|---|---|---|---|
1-2 | 報(bào)文頭 | Int | 2字節(jié) | 5a 5a |
3-6 | IMUtime | Int | 4字節(jié) | 整型時(shí)戳 |
7-10 | GYROX | float | 4字節(jié) | 直接浮點(diǎn)數(shù) |
11-14 | GYROY | float | 4字節(jié) | 直接浮點(diǎn)數(shù) |
15-18 | GYROZ | float | 4字節(jié) | 直接浮點(diǎn)數(shù) |
19-22 | ACCEX | float | 4字節(jié) | 直接浮點(diǎn)數(shù) |
23-26 | ACCEY | float | 4字節(jié) | 直接浮點(diǎn)數(shù) |
27-30 | ACCEZ | float | 4字節(jié) | 直接浮點(diǎn)數(shù) |
31-34 | 備用 | Int | 4字節(jié) | |
35-37 | 轉(zhuǎn)臺(tái)角1 | Int | 3字節(jié) | 量綱2.330168888888889*e4° |
38-40 | 轉(zhuǎn)臺(tái)角2 | Int | 3字節(jié) | 量綱2.330168888888889*e4° |
41-43 | GPS經(jīng)度 | Int | 3字節(jié) | 量綱93206.75556 |
44-46 | GPS緯度 | Int | 3字節(jié) | 量綱46603.37778 |
47-48 | Para4 | Int | 2字節(jié) | |
49-50 | Para[2]/para[5] | Int | 2字節(jié) | |
51-52 | Para[3]/para[6] | Int | 2字節(jié) | |
53-54 | Para[7] | Int | 2字節(jié) | |
55-56 | Para[8] | Int | 2字節(jié) | |
57 | comdatavalid | Int | 1字節(jié) | |
58 | 校驗(yàn)和 | Int | 1字節(jié) | 3-57和校驗(yàn) |
2.2 界面設(shè)計(jì)
首先,我們?cè)O(shè)計(jì)一個(gè)簡(jiǎn)單的用戶界面,包括:
- 串口配置區(qū)域:【串口】、【波特率】、【數(shù)據(jù)位】、【停止位】的設(shè)置,以及一個(gè)按鈕用于開(kāi)始打開(kāi)和關(guān)閉串口;
- 接收設(shè)置區(qū)域:用于設(shè)置接收和發(fā)送的數(shù)據(jù)格式,支持
16
進(jìn)制以及ASCII
兩種格式;這里為了簡(jiǎn)單起見(jiàn),程序中發(fā)送/接收采用一樣的數(shù)據(jù)格式;16
進(jìn)制:例如:5a 5a 02 03 5a
;ASCII
格式:例如:DDR V1.12 52218f4949 cym 23/07/0
;
- 數(shù)據(jù)發(fā)送區(qū)域:由一個(gè)文本框和一個(gè)發(fā)送按鈕組成;
- 數(shù)據(jù)接收區(qū)域:由一個(gè)文本域組成;
- 如果接收到的數(shù)據(jù)時(shí)導(dǎo)航解算或者原始信息報(bào)文,則將解析后的數(shù)據(jù)顯示在導(dǎo)航結(jié)算和原始信息區(qū)域;
界面效果如下(參考網(wǎng)上串口助手工具);
三、程序?qū)崿F(xiàn)
我們將界面原型劃分成了五個(gè)區(qū)域;
- 串口設(shè)置區(qū)域;
- 接收設(shè)置區(qū)域;
- 數(shù)據(jù)發(fā)送區(qū)域;
- 數(shù)據(jù)接收區(qū)域;
- 導(dǎo)航解算和原始信息區(qū)域;
我們針對(duì)這五個(gè)區(qū)域編寫相關(guān)實(shí)現(xiàn)代碼,其具體流程如下;
- 使用
Qt Designer
工具按照界面原型設(shè)置窗口,主要使用到一些基礎(chǔ)控件,比如按鈕、文本框、文本域、單選框、復(fù)選框等;‘ - 針對(duì)窗體各個(gè)區(qū)域中的控件進(jìn)行初始化工作;
我們將界面相關(guān)的代碼均放置在serial_port.py
文件中,該文件主要包含了如下功能;
- 界面初始化工作;
- 打開(kāi)串口;
- 接收數(shù)據(jù);
- 發(fā)送數(shù)據(jù)。
3.1 初始化工作
3.1.1 初始化串口設(shè)置區(qū)域
串口設(shè)置區(qū)域主要由【串口】、【波特率】、【校驗(yàn)位】、【數(shù)據(jù)位】、【停止位】下拉列表以及【打開(kāi)串口】按鈕組成。
首先需要初始化【串口】、【波特率】、【校驗(yàn)位】、【數(shù)據(jù)位】、【停止位】下拉列表;
- 串口:下拉列表加載系統(tǒng)當(dāng)前可用的串口;
- 波特率:下拉列表設(shè)置常見(jiàn)的波特率,比如
1200
、2400
、9600
、4800
,9600
,19200
,384000
,57600
,115200
,460800
,921600
,230400
,1500000
等; - 校驗(yàn)位:下拉列表設(shè)置校驗(yàn)位為
None
、Odd
、Even
、Mark
、Space
; - 數(shù)據(jù)位:下拉列表設(shè)置為
5
、6
、7
、8
; - 停止位:下拉列表設(shè)置為
1
、2
;
設(shè)置【打開(kāi)串口】按鈕點(diǎn)擊事件對(duì)應(yīng)的槽函數(shù)為self.open_serial_connection
,當(dāng)點(diǎn)擊【打開(kāi)串口】按鈕時(shí)將會(huì)執(zhí)行該函數(shù);
代碼位于serial_port.py
中__init_serial_setting__
,具體如下;
def __init_serial_setting__(self): """ 初始化串口設(shè)置相關(guān)控件默認(rèn)參數(shù) 設(shè)置下拉列表自動(dòng)補(bǔ)全 https://blog.csdn.net/xuleisdjn/article/details/51434118 :return: """ # 加載可用串口 self.ui.cbx_com.setEditable(False) self.ui.cbx_com.setMaxVisibleItems(10) # 設(shè)置最大顯示下列項(xiàng) 超過(guò)要使用滾動(dòng)條拖拉 self.ui.cbx_com.setInsertPolicy(QComboBox.InsertAfterCurrent) # 設(shè)置插入方式\ port_list = list(serial.tools.list_ports.comports()) # 獲取當(dāng)前的所有串口,得到一個(gè)列表 for port in port_list: self.ui.cbx_com.addItem(port.device) # 初始化波特率列表 self.ui.cbx_baud_rate.setEditable(False) self.ui.cbx_baud_rate.setMaxVisibleItems(10) # 設(shè)置最大顯示下列項(xiàng) 超過(guò)要使用滾動(dòng)條拖拉 self.ui.cbx_baud_rate.setInsertPolicy(QComboBox.InsertAfterCurrent) # 設(shè)置插入方式 for baud_rate in [1200, 2400, 4800, 9600, 19200, 384000, 57600, 115200, 460800, 921600, 230400, 1500000]: self.ui.cbx_baud_rate.addItem(str(baud_rate), baud_rate) # 設(shè)置默認(rèn)值 self.ui.cbx_baud_rate.setCurrentIndex(7) # 初始化校驗(yàn)位列表 self.ui.cbx_parity_bit.setEditable(False) self.ui.cbx_parity_bit.setMaxVisibleItems(10) # 設(shè)置最大顯示下列項(xiàng) 超過(guò)要使用滾動(dòng)條拖拉 self.ui.cbx_parity_bit.setInsertPolicy(QComboBox.InsertAfterCurrent) # 設(shè)置插入方式 for (key, value) in {'None': serial.PARITY_NONE, 'Odd': serial.PARITY_ODD, 'Even': serial.PARITY_EVEN, 'Mark': serial.PARITY_MARK, 'Space': serial.PARITY_SPACE}.items(): self.ui.cbx_parity_bit.addItem(key, value) # 設(shè)置默認(rèn)值 self.ui.cbx_parity_bit.setCurrentIndex(0) # 初始化數(shù)據(jù)位列表 self.ui.cbx_data_bit.setEditable(False) self.ui.cbx_data_bit.setMaxVisibleItems(10) # 設(shè)置最大顯示下列項(xiàng) 超過(guò)要使用滾動(dòng)條拖拉 self.ui.cbx_data_bit.setInsertPolicy(QComboBox.InsertAfterCurrent) # 設(shè)置插入方式 for data_bit in [serial.FIVEBITS, serial.SIXBITS, serial.SEVENBITS, serial.EIGHTBITS]: self.ui.cbx_data_bit.addItem(str(data_bit), data_bit) # 設(shè)置默認(rèn)值 self.ui.cbx_data_bit.setCurrentIndex(3) # 初始化停止位列表 self.ui.cbx_stop_bit.setEditable(False) self.ui.cbx_stop_bit.setMaxVisibleItems(10) # 設(shè)置最大顯示下列項(xiàng) 超過(guò)要使用滾動(dòng)條拖拉 self.ui.cbx_stop_bit.setInsertPolicy(QComboBox.InsertAfterCurrent) # 設(shè)置插入方式 for data_bit in [serial.STOPBITS_ONE, serial.STOPBITS_TWO]: self.ui.cbx_stop_bit.addItem(str(data_bit), data_bit) # 設(shè)置默認(rèn)值 self.ui.cbx_stop_bit.setCurrentIndex(0) # 設(shè)置點(diǎn)擊打開(kāi)串口按鈕對(duì)應(yīng)的槽函數(shù) self.ui.btx_start.clicked.connect(self.open_serial_connection)
3.1.2 初始化串口接收區(qū)域
串口接收區(qū)域主要由【Hex
】、【ASCII
】單選框以及【顯示時(shí)間】復(fù)選框組成;
設(shè)置發(fā)送/接收的數(shù)據(jù)格式,支持Hex
和ASCII
,Hex
和ASCII
是互斥的,默認(rèn)選中Hex
,這里我們?cè)O(shè)置:
- 【
Hex
】單選框點(diǎn)擊事件對(duì)應(yīng)的槽函數(shù)為self.rbn_data_format_hex_clicked
,當(dāng)點(diǎn)擊【Hex
】單選框時(shí)將會(huì)執(zhí)行該函數(shù),在該函數(shù)內(nèi)會(huì)記錄當(dāng)前選中的是hex
; - 【
ASCII
】單選框點(diǎn)擊事件對(duì)應(yīng)的槽函數(shù)為self.rbn_data_format_ascii_clicked
,當(dāng)點(diǎn)擊【ASCII
】單選框時(shí)將會(huì)執(zhí)行該函數(shù),在該函數(shù)內(nèi)出記錄當(dāng)前選中的是ascii
;
【顯示時(shí)間】復(fù)選框用于設(shè)置串口接收到數(shù)據(jù)時(shí),是否在接收區(qū)域輸出當(dāng)前時(shí)間;
代碼位于serial_port.py
中__init_recv_setting__
,具體如下;
def __init_recv_setting__(self): """ 接收設(shè)置初始化 :return: """ self.ui.rbn_data_format_hex.clicked.connect(self.rbn_data_format_hex_clicked) self.ui.rbn_data_format_ascii.clicked.connect(self.rbn_data_format_ascii_clicked)
其中rbn_data_format_hex_clicked
函數(shù);
def rbn_data_format_hex_clicked(self): """ 接收數(shù)據(jù)格式發(fā)生變化 :return: """ if self.ui.rbn_data_format_hex.isChecked(): self.ui.rbn_data_format_ascii.setChecked(False) if self.serial_thread: self.serial_thread.date_format = 'hex'
其中rbn_data_format_ascii_clicked
函數(shù);
def rbn_data_format_ascii_clicked(self): """ 接收數(shù)據(jù)格式發(fā)生變化 :return: """ if self.ui.rbn_data_format_ascii.isChecked(): self.ui.rbn_data_format_hex.setChecked(False) if self.serial_thread: self.serial_thread.date_format = 'ascii'
3.1.3 初始化串口數(shù)據(jù)接收區(qū)域
串口數(shù)據(jù)接收區(qū)域由一個(gè)【文本域控件】組成,用于存放串口接收到的數(shù)據(jù),數(shù)據(jù)長(zhǎng)度默認(rèn)最長(zhǎng)為5000字符,超過(guò)5000字符自動(dòng)清空;
這里初始化【文本域控件】為只讀,并且串口接收到數(shù)據(jù)時(shí),自動(dòng)將滾動(dòng)條移動(dòng)至【文本域控件】的最低端;
代碼位于serial_port.py
中__init_recv_setting__
,具體如下;
def __init_recv_data_viewer__(self): """ 初始化串口數(shù)據(jù)接收區(qū)域 :return: """ self.ui.txt_recv_data_viewer.setReadOnly(True) self.ui.txt_recv_data_viewer.textChanged.connect( lambda: self.ui.txt_recv_data_viewer.moveCursor(QTextCursor.End))
3.1.4 初始化串口數(shù)據(jù)發(fā)送區(qū)域
串口數(shù)據(jù)接收區(qū)域由一個(gè)【文本輸入框】和一個(gè)【發(fā)送】按鈕組成,文本輸入框用于輸入要發(fā)送的內(nèi)容,點(diǎn)擊發(fā)送按鈕,會(huì)將文本輸入框的內(nèi)容通過(guò)當(dāng)前打開(kāi)的串口發(fā)送出去;
代碼位于serial_port.py
中__init_send_data_viewer__
,具體如下;
def __init_send_data_viewer__(self): """ 初始化串口數(shù)據(jù)發(fā)送區(qū)域 :return: """ self.ui.btn_send.clicked.connect(self.send_serial_data)
設(shè)置【發(fā)送】按鈕點(diǎn)擊事件對(duì)應(yīng)的槽函數(shù)為self.send_serial_data
,當(dāng)點(diǎn)擊【發(fā)送】按鈕時(shí)將會(huì)執(zhí)行該函數(shù)進(jìn)行串口數(shù)據(jù)的發(fā)送。
3.1.5 初始化報(bào)文解析區(qū)域
報(bào)文解析區(qū)域主要由兩部分組成;
- 導(dǎo)航結(jié)算區(qū)域:該部分主要由【報(bào)文頭】、【工作狀態(tài)】、【參數(shù)狀態(tài)】、【運(yùn)行時(shí)間】等文本輸入框組成,其與導(dǎo)航解算報(bào)文字段一一對(duì)應(yīng);
- 原始信息區(qū)域:該部分主要由【報(bào)文頭】、【
IMUTime
】、【GYROX
】、【GYROZ
】等文本輸入框組成,其與原始信息報(bào)文字段一一對(duì)應(yīng);
代碼位于serial_port.py
中__init_package_setting__
,具體如下;
def __init_package_setting__(self): self.ui.let_nav_header.setReadOnly(True) self.ui.let_nav_work_state.setReadOnly(True) self.ui.let_nav_param_state.setReadOnly(True) self.ui.let_nav_run_time.setReadOnly(True) self.ui.let_nav_latitude.setReadOnly(True) self.ui.let_nav_longitude.setReadOnly(True) self.ui.let_nav_heave.setReadOnly(True) self.ui.let_nav_east_speed.setReadOnly(True) self.ui.let_nav_north_speed.setReadOnly(True) self.ui.let_nav_vertical_speed.setReadOnly(True) self.ui.let_nav_attitude_angle1.setReadOnly(True) self.ui.let_nav_attitude_angle2.setReadOnly(True) self.ui.let_nav_attitude_angle3.setReadOnly(True) self.ui.let_nav_attitude_velocity1.setReadOnly(True) self.ui.let_nav_attitude_velocity2.setReadOnly(True) self.ui.let_nav_attitude_velocity3.setReadOnly(True) self.ui.let_nav_fault_code.setReadOnly(True) self.ui.let_nav_imu_time.setReadOnly(True) self.ui.let_nav_reserved.setReadOnly(True) self.ui.let_nav_response_flag.setReadOnly(True) self.ui.let_nav_check_sum.setReadOnly(True) self.ui.let_raw_header.setReadOnly(True) self.ui.let_raw__imu_time.setReadOnly(True) self.ui.let_raw_gyrox.setReadOnly(True) self.ui.let_raw_gyroy.setReadOnly(True) self.ui.let_raw_gyroz.setReadOnly(True) self.ui.let_raw_accex.setReadOnly(True) self.ui.let_raw_accey.setReadOnly(True) self.ui.let_raw_accez.setReadOnly(True) self.ui.let_raw_reserved.setReadOnly(True) self.ui.let_raw_turntable_angle1.setReadOnly(True) self.ui.let_raw_turntable_angle2.setReadOnly(True) self.ui.let_raw_longitude.setReadOnly(True) self.ui.let_raw_latitude.setReadOnly(True) self.ui.let_raw_para4.setReadOnly(True) self.ui.let_raw_para2_5.setReadOnly(True) self.ui.let_raw_para3_6.setReadOnly(True) self.ui.let_raw_para7.setReadOnly(True) self.ui.let_raw_para8.setReadOnly(True) self.ui.let_raw_comdata_valid.setReadOnly(True) self.ui.let_raw_check_sum.setReadOnly(True)
這里我們僅僅是將上述這些控件設(shè)置為只讀狀態(tài)。
3.2 打開(kāi)串口
根據(jù)硬件設(shè)備參數(shù)在串口設(shè)置區(qū)域進(jìn)行配置【串口】、【波特率】、【校驗(yàn)位】、【數(shù)據(jù)位】、【停止位】,配置完成后,當(dāng)點(diǎn)擊【打開(kāi)串口】按鈕,將會(huì)執(zhí)行open_serial_connection
函數(shù);
在open_serial_connection
函數(shù)內(nèi)主要會(huì)進(jìn)行如下工作;
- 獲取串口設(shè)置區(qū)域配置的參數(shù),并對(duì)參數(shù)進(jìn)行校驗(yàn),具體是由
__validate_setting__
函數(shù)完成; - 調(diào)用
SerialThread
創(chuàng)建串口線程,并將串口設(shè)置區(qū)域配置參數(shù)傳遞給線程; - 設(shè)置串口線程接收到數(shù)據(jù)時(shí)對(duì)應(yīng)的槽函數(shù)為
handle_data_received
,即串口接收到數(shù)據(jù)時(shí)會(huì)執(zhí)行handle_data_received
函數(shù); - 設(shè)置串口線程發(fā)生異常時(shí)對(duì)應(yīng)的槽函數(shù)為
handler_serial_error
,即串口線程發(fā)生異常時(shí),會(huì)將錯(cuò)誤信息發(fā)送給主線程,主線程接收到錯(cuò)誤信息,將會(huì)在界面顯示;
核心代碼如下:
def open_serial_connection(self): """ 打開(kāi)串口 :return: """ if not self.serial_thread or not self.serial_thread.isRunning(): # 參數(shù)校驗(yàn) if not self.__validate_setting__(): return # 建立一個(gè)串口 self.serial_thread = SerialThread(self.ui.cbx_com.currentText(), self.ui.cbx_baud_rate.currentData(), self.ui.cbx_data_bit.currentData(), self.ui.cbx_parity_bit.currentData(), self.ui.cbx_stop_bit.currentData(), 'hex' if self.ui.rbn_data_format_hex.isChecked() else 'ascii') self.serial_thread.data_received.connect(self.handle_data_received) self.serial_thread.serial_error.connect(self.handler_serial_error) self.serial_thread.start() self.ui.btx_start.setText("關(guān)閉串口") else: self.serial_thread.stop() # 打開(kāi)串口操作 self.ui.btx_start.setText("打開(kāi)串口")
3.2.1 __validate_setting__
__validate_setting__
函數(shù)主要就是校驗(yàn)【串口】、【波特率】、【校驗(yàn)位】、【數(shù)據(jù)位】、【停止位】下拉列表是否已經(jīng)配置,如果未配置將會(huì)彈出警告提示信息;
def __validate_setting__(self): """ 校驗(yàn)串口設(shè)置參數(shù) :return: """ # 參數(shù)校驗(yàn) if self.ui.cbx_com.currentIndex() == -1: QMessageBox.warning(self, "Warning", "請(qǐng)選擇串口!") return False if self.ui.cbx_baud_rate.currentIndex() == -1: QMessageBox.warning(self, "Warning", "請(qǐng)選擇波特率!") return False if self.ui.cbx_parity_bit.currentIndex() == -1: QMessageBox.warning(self, "Warning", "請(qǐng)選擇校驗(yàn)位!") return False if self.ui.cbx_data_bit.currentIndex() == -1: QMessageBox.warning(self, "Warning", "請(qǐng)選擇數(shù)據(jù)位!") return False if self.ui.cbx_data_bit.currentIndex() == -1: QMessageBox.warning(self, "Warning", "請(qǐng)選擇停止位!") return False return True
3.2.2 SerialThread
SerialThread
類的實(shí)現(xiàn)位于serial_thread.py
文件中,這塊我們后面單獨(dú)接收。
3.2.3 handle_data_received
當(dāng)串口接收到數(shù)據(jù)時(shí)會(huì)執(zhí)行handle_data_received
函數(shù),這塊我們后面單獨(dú)接收。
3.2.4 handler_serial_error
串口線程發(fā)生異常時(shí),會(huì)將錯(cuò)誤信息發(fā)送給主線程,主線程接收到錯(cuò)誤信息,將會(huì)在界面顯示;具體實(shí)現(xiàn)函數(shù)為handler_serial_error
;
def handler_serial_error(self, error): """ 串口接收線程異常 :param error: :return: """ QMessageBox.critical(self, '錯(cuò)誤', error)
3.3 接收數(shù)據(jù)
上面我們提到,當(dāng)串口線程接收到數(shù)據(jù)時(shí),會(huì)將數(shù)據(jù)通過(guò)信號(hào)與槽機(jī)制傳遞給主線程,將會(huì)由主線程handle_data_received
函數(shù)進(jìn)行處理;
在handle_data_received
函數(shù)內(nèi)主要會(huì)進(jìn)行如下工作;
- 獲取當(dāng)前時(shí)間;
- 串口數(shù)據(jù)接收區(qū)域【文本域控件】?jī)?nèi)容超過(guò)
5000
個(gè)字符將會(huì)清空; - 如果配置了顯示當(dāng)前時(shí)間,將會(huì)在接收到的數(shù)據(jù)前面 插入當(dāng)前時(shí)間,并將數(shù)據(jù)追加到串口數(shù)據(jù)接收區(qū)域【文本域控件】中;
- 如果串口接收區(qū)域配置的數(shù)據(jù)格式為
hex
,將會(huì)對(duì)接收到的報(bào)文信息進(jìn)行解析;- 解析由
PackageParse
類完成,解析類會(huì)根據(jù)報(bào)文數(shù)據(jù)長(zhǎng)度、報(bào)文頭、以及校驗(yàn)和等參數(shù)判定當(dāng)前報(bào)文格式為導(dǎo)航結(jié)算還是原始信息; - 如果為導(dǎo)航結(jié)算報(bào)文,保存報(bào)文到
nav_data.csv
,同時(shí)將報(bào)文解析到的字段一一填入界面導(dǎo)航結(jié)算區(qū)域?qū)?yīng)的控件中; - 如果為原始信息報(bào)文,保存報(bào)文到
raw_data.csv
,同時(shí)將報(bào)文解析到的字段一一填入界面原始信息區(qū)域?qū)?yīng)的控件中;
- 解析由
- 移除已經(jīng)解析的報(bào)文,截取未解析的報(bào)文,重復(fù)報(bào)文解析的流程,直至報(bào)文內(nèi)容不滿足導(dǎo)航結(jié)算、原始信息報(bào)文格式;
handle_data_received
代碼如下:
def handle_data_received(self, data): """ 接收數(shù)據(jù) :param data:接收到的數(shù)據(jù) :return: """ # 獲取時(shí)間 current_time = QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss") # 超過(guò)5000字符清空 if len(self.ui.txt_recv_data_viewer.toPlainText()) > 5000: self.ui.txt_recv_data_viewer.clear() # 更新顯示區(qū)域中的數(shù)據(jù) if self.ui.cbx_show_time.isChecked(): self.ui.txt_recv_data_viewer.insertPlainText(f"[{current_time}] {data}") else: self.ui.txt_recv_data_viewer.insertPlainText(f"[{data}") # 必須是hex格式 if self.ui.rbn_data_format_ascii.isChecked(): return # 如果報(bào)文最后兩位是換行符,則移除 data = data.replace('0d 0a', '') data = bytes.fromhex(data.replace(' ', '')) # 解析data while data is not None: package_parse = PackageParse(data) data_format = package_parse.get_data_format() if data_format == PackageParse.Type.NAl_SOL: # 解析數(shù)據(jù) dict_msg = package_parse.get_nav_sol() # 寫入當(dāng)前時(shí)間,并保存到文件 dict_msg["current_time"] = current_time save_csv('nav_data.csv', dict_msg) # 界面顯示 self.ui.let_nav_header.setText(dict_msg["header"]) self.ui.let_nav_work_state.setText(dict_msg["work_state"]) self.ui.let_nav_param_state.setText(dict_msg["param_state"]) self.ui.let_nav_run_time.setText(str(dict_msg["run_time"])) self.ui.let_nav_latitude.setText(str(dict_msg["latitude"])) self.ui.let_nav_longitude.setText(str(dict_msg["longitude"])) self.ui.let_nav_heave.setText(str(dict_msg["heave"])) self.ui.let_nav_east_speed.setText(str(dict_msg["east_speed"])) self.ui.let_nav_north_speed.setText(str(dict_msg["north_speed"])) self.ui.let_nav_vertical_speed.setText(str(dict_msg["vertical_speed"])) self.ui.let_nav_attitude_angle1.setText(str(dict_msg["attitude_angle1"])) self.ui.let_nav_attitude_angle2.setText(str(dict_msg["attitude_angle2"])) self.ui.let_nav_attitude_angle3.setText(str(dict_msg["attitude_angle3"])) self.ui.let_nav_attitude_velocity1.setText(str(dict_msg["attitude_velocity1"])) self.ui.let_nav_attitude_velocity2.setText(str(dict_msg["attitude_velocity2"])) self.ui.let_nav_attitude_velocity3.setText(str(dict_msg["attitude_velocity3"])) self.ui.let_nav_fault_code.setText(dict_msg["fault_code"]) self.ui.let_nav_imu_time.setText(dict_msg["imu_time"]) self.ui.let_nav_reserved.setText(dict_msg["reserved"]) self.ui.let_nav_response_flag.setText(dict_msg["response_flag"]) self.ui.let_nav_check_sum.setText(dict_msg["check_sum"]) if data_format == PackageParse.Type.RAW: # 解析數(shù)據(jù) data = package_parse.get_raw() # 寫入當(dāng)前時(shí)間,并保存到文件 data["current_time"] = current_time save_csv('raw_data.csv', data) # 界面顯示 self.ui.let_raw_header.setText(dict_msg["header"]) self.ui.let_raw__imu_time.setText(dict_msg["imu_time"]) self.ui.let_raw_gyrox.setText(dict_msg["gyrox"]) self.ui.let_raw_gyroy.setText(dict_msg["gyroy"]) self.ui.let_raw_gyroz.setText(dict_msg["gyroz"]) self.ui.let_raw_accex.setText(dict_msg["accex"]) self.ui.let_raw_accey.setText(dict_msg["accey"]) self.ui.let_raw_accez.setText(dict_msg["accez"]) self.ui.let_raw_reserved.setText(dict_msg["reserved"]) self.ui.let_raw_turntable_angle1.setText(dict_msg["turntable_angle1"]) self.ui.let_raw_turntable_angle2.setText(dict_msg["turntable_angle2"]) self.ui.let_raw_longitude.setText(dict_msg["longitude"]) self.ui.let_raw_latitude.setText(dict_msg["latitude"]) self.ui.let_raw_para4.setText(dict_msg["para4"]) self.ui.let_raw_para2_5.setText(dict_msg["para2_5"]) self.ui.let_raw_para3_6.setText(dict_msg["para3_6"]) self.ui.let_raw_para7.setText(dict_msg["para7"]) self.ui.let_raw_para8.setText(dict_msg["para8"]) self.ui.let_raw_comdata_valid.setText(dict_msg["comdata_valid"]) self.ui.let_raw_check_sum.setText(dict_msg["check_sum"]) # 一次收到若干個(gè)數(shù)據(jù)包 if data_format != PackageParse.Type.UNkNOWN and len(data) > data_format.value: data = data[data_format.value:] else: data = None
文件保存函數(shù)save_csv
:
def save_csv(file_name, dict: {}): """ 保存到csv文件 :param file_name:文件名 :param dict: 數(shù)據(jù),字典格式 :return: """ file_exists = os.path.exists(file_name) # Save data to CSV file with open(file_name, 'a', newline='') as csvfile: writer = csv.writer(csvfile) # 如果文件不存在,則寫入列名 if not file_exists: writer.writerow(dict.keys()) # 寫入數(shù)據(jù) writer.writerow(dict.values())
3.4 報(bào)文解析
報(bào)文解析是由PackageParse
類實(shí)現(xiàn)的,代碼位于package_parse.py
,在該類內(nèi)部主要實(shí)現(xiàn)了一下功能;
判斷當(dāng)前報(bào)文格式是導(dǎo)航結(jié)算還是原始信息,由get_data_format
函數(shù)實(shí)現(xiàn);實(shí)現(xiàn)導(dǎo)航結(jié)算報(bào)文的解析,由get_nav_sol
函數(shù)實(shí)現(xiàn);實(shí)現(xiàn)原始信息報(bào)文的解析,由get_raw
函數(shù)實(shí)現(xiàn);
3.4.1 get_data_format
這里判斷報(bào)文格式的方法很簡(jiǎn)單;
- 首先就是長(zhǎng)度判定,如果是導(dǎo)航結(jié)算報(bào)文,長(zhǎng)度至少為49個(gè)字符;如果是原始信息報(bào)文,長(zhǎng)度至少是58個(gè)字符;
- 接著判斷報(bào)文前兩個(gè)字節(jié)是否為
0x5a 0X5a
; - 然后判定第58個(gè)字節(jié)是否為
3~57
個(gè)字節(jié)的累加和,如果滿足條件就是原始信息報(bào)文; - 最后判定第49個(gè)字節(jié)是否為
3~48
個(gè)字節(jié)的累加和,如果滿足條件就是導(dǎo)航結(jié)算報(bào)文;
具體代碼如下:
def validate_nal_sol(self): """ 校驗(yàn)和 導(dǎo)航結(jié)算:第49個(gè)字節(jié)為3-48字節(jié)累加和 """ # 取出第3到第48字節(jié) selected_bytes = self.__byte_array[2:48] # 計(jì)算累加和并只保留低兩位數(shù)字 checksum = sum(selected_bytes) & 0xFF return checksum == self.__byte_array[48] def validate_raw(self): """ 校驗(yàn)和 原始信息:第58個(gè)字節(jié)為3-57字節(jié)累加和 """ # 取出第3到第48字節(jié) selected_bytes = self.__byte_array[2:57] # 計(jì)算累加和并只保留低兩位數(shù)字 checksum = sum(selected_bytes) & 0xFF return checksum == self.__byte_array[57] def get_data_format(self): """ 返回?cái)?shù)據(jù)類型:在慣性裝置的AXS31接口,里面有兩路數(shù)據(jù),一路稱為導(dǎo)航解算,一路稱為原始數(shù) :return: 0: 導(dǎo)航解算 1:原始信息 2:非法數(shù)據(jù) """ # 原始信息報(bào)文 if (len(self.__byte_array) >= 58 and self.__byte_array[0] == 0x5a and self.__byte_array[1] == 0x5a and self.validate_raw()): return PackageParse.Type.RAW # 導(dǎo)航解算的報(bào)文內(nèi)容 if (len(self.__byte_array) >= 49 and self.__byte_array[0] == 0x5a and self.__byte_array[1] == 0x5a and self.validate_nal_sol()): return PackageParse.Type.NAl_SOL return PackageParse.Type.UNkNOWN
3.4.2 get_nav_sol
如果報(bào)文格式是導(dǎo)航結(jié)算,那么調(diào)用該方法實(shí)現(xiàn)導(dǎo)航結(jié)算報(bào)文的解析,在函數(shù)內(nèi)部根據(jù)字段單位、量綱對(duì)原始字節(jié)進(jìn)行了轉(zhuǎn)換;
def translate(hex_array: [], unit=None): """ 將字節(jié)數(shù)組轉(zhuǎn)換為16進(jìn)制字符串 [0x0a,0x04]-> 0x0a 0x04 :param hex_array: 字節(jié)數(shù)組 :param unit 量綱 :return: """ if unit is not None: # 將字節(jié)數(shù)組按照小端格式轉(zhuǎn)換為數(shù)字 num = int.from_bytes(hex_array, byteorder='little') return num / unit return ' '.join(f'0x{val:02X}' for val in hex_array) def get_nav_sol(self): """ 獲取導(dǎo)航解算數(shù)據(jù) :return: """ if self.get_data_format() != PackageParse.Type.NAl_SOL: print("數(shù)據(jù)格式錯(cuò)誤,非導(dǎo)航解算報(bào)文!") return None message_dict = {} # 解析報(bào)文頭 message_dict['header'] = translate(self.__byte_array[0:2]) # 解析工作狀態(tài) message_dict['work_state'] = translate(self.__byte_array[2:3]) # 解析參數(shù)狀態(tài) message_dict['param_state'] = translate(self.__byte_array[3:4]) # 解析運(yùn)行時(shí)間 message_dict['run_time'] = translate(self.__byte_array[4:8], 20) # 解析緯度 message_dict['latitude'] = translate(self.__byte_array[8:11], 93206.75556) # 解析經(jīng)度 message_dict['longitude'] = translate(self.__byte_array[11:14], 46603.37778) # 解析升沉 message_dict['heave'] = translate(self.__byte_array[14:16], 100) # 解析東速 message_dict['east_speed'] = translate(self.__byte_array[16:18], 100) # 解析北速 message_dict['north_speed'] = translate(self.__byte_array[18:20], 100) # 解析垂速 message_dict['vertical_speed'] = translate(self.__byte_array[20:22], 100) # 解析姿態(tài)角1 message_dict['attitude_angle1'] = translate(self.__byte_array[22:25], 0.25 * 93206.75556) # 解析姿態(tài)角2 message_dict['attitude_angle2'] = translate(self.__byte_array[25:28], 93206.75556) # 解析姿態(tài)角3 message_dict['attitude_angle3'] = translate(self.__byte_array[28:31], 93206.75556) # 姿態(tài)角速率1 message_dict['attitude_velocity1'] = translate(self.__byte_array[31:34], 93206.75556) # 姿態(tài)角速率2 message_dict['attitude_velocity2'] = translate(self.__byte_array[34:37], 93206.75556) # 姿態(tài)角速率3 message_dict['attitude_velocity3'] = translate(self.__byte_array[37:40], 93206.75556) # 故障碼 message_dict['fault_code'] = translate(self.__byte_array[40:41]) # IMU時(shí)間 message_dict['imu_time'] = translate(self.__byte_array[41:43]) # 備用 message_dict['reserved'] = translate(self.__byte_array[43:47]) # 應(yīng)答標(biāo)志 message_dict['response_flag'] = translate(self.__byte_array[47:48]) # 校驗(yàn)和 message_dict['check_sum'] = translate(self.__byte_array[48:49]) return message_dict
3.4.3 get_raw
如果報(bào)文格式是原始信息,那么調(diào)用該方法實(shí)現(xiàn)原始信息報(bào)文的解析;
def get_raw(self): """ 獲取原始信息報(bào)文 :return: """ if self.get_data_format() != PackageParse.Type.RAW: print("數(shù)據(jù)格式錯(cuò)誤,非原始信息報(bào)文!") return None message_dict = {} # 解析報(bào)文頭 message_dict['header'] = translate(self.__byte_array[0:2]) # IMUtime message_dict['imu_time'] = translate(self.__byte_array[2:6]) # GYROX message_dict['gyrox'] = translate(self.__byte_array[6:10]) # GYROY message_dict['gyroy'] = translate(self.__byte_array[10:14]) # GYROZ message_dict['gyroz'] = translate(self.__byte_array[14:18]) # ACCEX message_dict['accex'] = translate(self.__byte_array[18:22]) # ACCEY message_dict['accey'] = translate(self.__byte_array[22:26]) # ACCEZ message_dict['accez'] = translate(self.__byte_array[26:30]) # 備用 message_dict['reserved'] = translate(self.__byte_array[30:34]) # 轉(zhuǎn)臺(tái)角1 message_dict['turntable_angle1'] = translate(self.__byte_array[34:37]) # 轉(zhuǎn)臺(tái)角2 message_dict['turntable_angle2'] = translate(self.__byte_array[37:40]) # GPS經(jīng)度 message_dict['longitude'] = translate(self.__byte_array[40:43]) # GPS緯度 message_dict['latitude'] = translate(self.__byte_array[43:46]) # Para[4](電磁/牽引時(shí)為航向) message_dict['para4'] = translate(self.__byte_array[46:48]) # Para[2]/para[5] message_dict['para2_5'] = translate(self.__byte_array[48:50]) # Para[3]/para[6] message_dict['para3_6'] = translate(self.__byte_array[50:52]) # Para[7] message_dict['para7'] = translate(self.__byte_array[52:54]) # Para[8] message_dict['para8'] = translate(self.__byte_array[54:56]) # comdatavalid message_dict['comdata_valid'] = translate(self.__byte_array[56:57]) # 校驗(yàn)和 message_dict['check_sum'] = translate(self.__byte_array[57:58]) return message_dict
3.5 發(fā)送數(shù)據(jù)
當(dāng)用戶在串口數(shù)據(jù)接收區(qū)域的【文本輸入框】錄入內(nèi)容,點(diǎn)擊【發(fā)送】按鈕時(shí),會(huì)調(diào)用send_serial_data
方法:
def send_serial_data(self, data: str): """ 發(fā)送數(shù)據(jù) :return: """ if not self.serial_thread or not self.serial_thread.isRunning(): QMessageBox.warning(self, "Warning", "請(qǐng)先打開(kāi)串口!") return data = self.ui.let_send_data_viewer.text() if data != "": self.serial_thread.send_data(data) self.ui.let_send_data_viewer.clear()
函數(shù)內(nèi)部首先校驗(yàn)串口是否已經(jīng)打開(kāi),如果串口已經(jīng)打開(kāi)并且【文本輸入框】輸入了內(nèi)容,將調(diào)用串口線程的send_data
方法來(lái)進(jìn)行數(shù)據(jù)發(fā)送。
3.6 串口線程
PyQt
已經(jīng)為我們提供了串口控件,控件名稱為QtSerialPort
,使用方法比較簡(jiǎn)單,主要是兩個(gè)模塊:QSerialPort
, QSerialPortInfo
,但是這個(gè)控件提供的能力有限。
這里我們使用另一個(gè)串口包pyserial
來(lái)實(shí)現(xiàn):
pip install pyserial -i https://pypi.tuna.tsinghua.edu.cn/simple
為了實(shí)現(xiàn)串口數(shù)據(jù)的讀取和發(fā)送,我們創(chuàng)建了一個(gè)繼承自QThread
的SerialThread
類;
from PyQt5.QtCore import QThread,pyqtSignal import serial class SerialThread(QThread): """ 創(chuàng)建一個(gè)繼承自QThread的SerialThread類,實(shí)現(xiàn)串口數(shù)據(jù)的讀取/發(fā)送 """ # 用于發(fā)送串口數(shù)據(jù)接收信號(hào) data_received = pyqtSignal(str) # 串口打開(kāi)/接收異常 serial_error = pyqtSignal(str) .......
3.6.1 初始化
在首次點(diǎn)擊【打開(kāi)串口】按鈕時(shí)會(huì)創(chuàng)建SerialThread
實(shí)例,即調(diào)用串口線程初始化方法;
def __init__(self, port, baud_rate, data_bits, parity_bits, stop_bits, data_format): """ 初始化 :param port: 串口號(hào) :param baud_rate: 波特率 :param data_bits: 數(shù)據(jù)位 :param stop_bits: 停止位 :param parity_bits: 奇偶校驗(yàn)位 """ super().__init__() self.port = port self.baud_rate = baud_rate self.data_bits = data_bits self.stop_bits = stop_bits self.parity_bits = parity_bits # 串口已經(jīng)運(yùn)行標(biāo)志位 self.running = False # 串口 self.serial = None # 數(shù)據(jù)格式 self.__date_format = data_format # 發(fā)送開(kāi)啟追加換行 self.__auto_line = True @property def date_format(self): """ #把一個(gè)getter方法變成屬性 :return: """ return self.__date_format @date_format.setter def date_format(self, value): """ # 負(fù)責(zé)把一個(gè)setter方法變成屬性賦值 :param value: :return: """ if not isinstance(value, str): raise ValueError('date_format must be an str') if value not in ['hex', 'ascii']: raise ValueError('date_format must in [hex,ascii]') self.__date_format = value
在這段代碼中,主要就是保存構(gòu)造函數(shù)傳遞過(guò)來(lái)的參數(shù),比如串口號(hào)、波特率等。
3.6.2 線程運(yùn)行
串口線程初始化完成后,調(diào)用start
方法,將會(huì)執(zhí)行run
函數(shù);
def run(self): """ 打開(kāi)串口 :return: """ # 串口已經(jīng)運(yùn)行 if self.running: return try: # 建立一個(gè)串口 with serial.Serial(port=self.port, baudrate=self.baud_rate, parity=self.parity_bits, bytesize=self.data_bits, stopbits=self.stop_bits, timeout=2) as self.serial: self.running = True while self.running: data = self.__read_data__() if data: self.data_received.emit(data) except Exception as e: print("打開(kāi)串口時(shí)失敗:", e) self.serial_error.emit("打開(kāi)串口時(shí)失??!")
在函數(shù)內(nèi)部我們實(shí)際上就是調(diào)用了pyserial
提供的Serial
類去實(shí)現(xiàn)串口的打開(kāi),并在while
循環(huán)中,調(diào)用__read_data__
獲取串口發(fā)送過(guò)來(lái)的數(shù)據(jù),如果接受到數(shù)據(jù)將串口數(shù)據(jù)通過(guò)信號(hào)與槽機(jī)制推送給主線程。
3.6.3 接受數(shù)據(jù)
串口數(shù)據(jù)的讀取是由__read_data__
函數(shù)完成的,在函數(shù)內(nèi)部我們調(diào)用self.serial.readline()
依次讀取一行的數(shù)據(jù),這里讀取到的是字節(jié)數(shù)組,我們根據(jù)我們串口接收區(qū)域設(shè)置的數(shù)據(jù)格式來(lái)進(jìn)行解析;
def __read_data__(self): """ 按行接收數(shù)據(jù) :return: """ if self.serial is None or not self.serial.isOpen(): return try: # 讀取串口數(shù)據(jù) 例如:b'DDR V1.12 52218f4949 cym 23/07/0' byte_array = self.serial.readline() if len(byte_array) == 0: return None # ascii顯示 if self.__date_format == 'ascii': # 串口接收到的字符串為b'ABC',要轉(zhuǎn)化成unicode字符串才能輸出到窗口中去 data_str = byte_array.decode('utf-8') else: # 串口接收到的字符串為b'ZZ\x02\x03Z',要轉(zhuǎn)換成16進(jìn)制字符串顯示 data_str = ' '.join(format(x, '02x') for x in byte_array) if self.__auto_line: data_str += '\r\n' return data_str except Exception as e: print("接收數(shù)據(jù)異常:", e) self.serial_error.emit("接收數(shù)據(jù)異常!")
3.6.4 發(fā)送數(shù)據(jù)
當(dāng)用戶在串口數(shù)據(jù)接收區(qū)域的【文本輸入框】錄入內(nèi)容,點(diǎn)擊【發(fā)送】按鈕時(shí),最終調(diào)用的就是串口線程的send_data
方法;我們根據(jù)我們串口接收區(qū)域設(shè)置的數(shù)據(jù)格式處理將要發(fā)送的數(shù)據(jù),然后調(diào)用self.serial.write
實(shí)現(xiàn)串口數(shù)據(jù)的發(fā)送;
def send_data(self, data: str): """ 發(fā)送數(shù)據(jù) :return: """ if not self.running: self.serial_error.emit("請(qǐng)先打開(kāi)串口!") return # hex發(fā)送 比如:5a 5a 02 03 5a -> b'ZZ\x02\x03Z' if self.__date_format == 'hex': data_str = data.strip() send_list = [] while data_str != '': try: num = int(data_str[0:2], 16) except ValueError: self.serial_error.emit('請(qǐng)輸入十六進(jìn)制數(shù)據(jù),以空格分開(kāi)!') return data_str = data_str[2:].strip() send_list.append(num) if self.__auto_line: send_list.append(0x0d) send_list.append(0x0a) byte_array = bytes(send_list) else: if self.__auto_line: data += '\r\n' # ascii發(fā)送 比如:'ABC' -> b'ABC' byte_array = data.encode('utf-8') try: self.serial.write(byte_array) except Exception as e: print("發(fā)送失敗", e) self.serial_error.emit('發(fā)送失敗!')
以上就是基于Python+PyQt5實(shí)現(xiàn)串口數(shù)據(jù)采集和顯示的詳細(xì)內(nèi)容,更多關(guān)于Python PyQt5數(shù)據(jù)采集和顯示的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Python進(jìn)行中文繁簡(jiǎn)轉(zhuǎn)換的實(shí)現(xiàn)代碼
這篇文章主要介紹了使用Python進(jìn)行中文繁簡(jiǎn)轉(zhuǎn)換的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10使用python爬蟲實(shí)現(xiàn)抓取動(dòng)態(tài)加載數(shù)據(jù)
這篇文章主要給大家介紹了如何用python爬蟲抓取豆瓣電影“分類排行榜”中的電影數(shù)據(jù),比如輸入“犯罪”則會(huì)輸出所有犯罪影片的電影名稱、評(píng)分,文中通過(guò)代碼示例和圖文介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01通過(guò)PYTHON來(lái)實(shí)現(xiàn)圖像分割詳解
這篇文章主要介紹了通過(guò)PYTHON來(lái)實(shí)現(xiàn)圖像分割詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06Python本地cache不當(dāng)使用導(dǎo)致內(nèi)存泄露的問(wèn)題分析與解決
最近在項(xiàng)目開(kāi)發(fā)中遇到了本地cache不當(dāng)使用導(dǎo)致的一個(gè)內(nèi)存泄露問(wèn)題,所以本文主要分析了問(wèn)題出現(xiàn)的原因已經(jīng)解決方法,需要的小伙伴可以參考下2023-08-08python3+PyQt5使用數(shù)據(jù)庫(kù)窗口視圖
這篇文章主要為大家詳細(xì)介紹了python3+PyQt5使用數(shù)據(jù)庫(kù)窗口視圖,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04Python編程中對(duì)文件和存儲(chǔ)器的讀寫示例
這篇文章主要介紹了Python編程中對(duì)文件和存儲(chǔ)器的讀寫示例,包括使用cPickle儲(chǔ)存器存儲(chǔ)對(duì)象的例子,需要的朋友可以參考下2016-01-01Python數(shù)據(jù)分析中常見(jiàn)統(tǒng)計(jì)方法詳解
數(shù)據(jù)分析是現(xiàn)代社會(huì)中不可或缺的一部分,通過(guò)對(duì)數(shù)據(jù)的統(tǒng)計(jì)和分析,我們可以得出有用的信息和見(jiàn)解,本文將介紹在?Python?中常見(jiàn)的數(shù)據(jù)統(tǒng)計(jì)方法,希望對(duì)大家有所幫助2024-02-02Python?不設(shè)計(jì)?do-while?循環(huán)結(jié)構(gòu)的理由
Python作為一種語(yǔ)言不支持do-while循環(huán)。?但是,我們可以采用一種變通方法來(lái)模擬do-while循環(huán)?。下面通過(guò)本文給大家分享下Python?不設(shè)計(jì)do-while?循環(huán)結(jié)構(gòu)的理由,需要的朋友可以參考下2022-01-01