matlab制作以太网数据接收上位机_Python制作串口通讯上位机
????串口通訊具有簡(jiǎn)單易用的特點(diǎn)廣泛應(yīng)用于測(cè)試設(shè)備的通訊和數(shù)據(jù)傳遞、單片機(jī)與計(jì)算機(jī)的通訊等,本案例基于Python語(yǔ)言制作一個(gè)用于接收燃油質(zhì)量流量計(jì)的串口通訊上位機(jī),實(shí)現(xiàn)數(shù)據(jù)的讀取和保存。
1. 相關(guān)知識(shí)點(diǎn):
1.1 Python GUI庫(kù)
GUI開(kāi)發(fā)是開(kāi)發(fā)具有用戶圖形界面的程序,在打包成可執(zhí)行文件.exe之后,具有用戶界面的程序具有更好地交互性和易用性,Python中常用的GUI庫(kù)如下:
Tkinter:是Python內(nèi)置的GUI庫(kù),小巧簡(jiǎn)單,著名的Python IDLE就是用tkinter實(shí)現(xiàn)的,在Windows, MacOS和Linux平臺(tái)均可使用,適合用于開(kāi)發(fā)界面簡(jiǎn)單的程序。
PyQt: 功能強(qiáng)大的GUI開(kāi)發(fā)庫(kù),具有方便的周邊工具支持,如QtDesigner, Eric等;但由于其功能強(qiáng)大,因此安裝較為繁瑣,運(yùn)行也較為龐大,此外還需掌握一定的C++知識(shí),PyQt同樣可應(yīng)用于Windows,Mac OS和Linux平臺(tái);該GUI庫(kù)適合開(kāi)發(fā)界面復(fù)雜、功能強(qiáng)大的程序。
wxPython: wxPython則是tkinter和PyQt的一個(gè)折中選擇,功能也介于兩者之間,也具有與PyQt類似的可視化開(kāi)發(fā)工具。
對(duì)于GUI庫(kù)的選擇,需要根據(jù)自己的需求而定,本例中由于制作的串口通訊上位機(jī)界面和功能都較為簡(jiǎn)單,因此選擇最易上手的tkinter庫(kù)制作。
1.2 類和對(duì)象
?????? 面向?qū)ο蟮某绦蛟O(shè)計(jì)往往是GUI程序設(shè)計(jì)的基礎(chǔ),讓程序具有更好地封裝性。類(class)是對(duì)象的一種抽象,描述了對(duì)象的特征,包括數(shù)據(jù)和操作;對(duì)象(object)是類的一個(gè)具體化,是由數(shù)據(jù)及能對(duì)其實(shí)施的操作所構(gòu)成的封裝體。也就是說(shuō),類不占用內(nèi)存,而對(duì)象占用內(nèi)存。例如:“狗”這個(gè)概念即可看作一個(gè)類,而名叫“小黃”的這條狗則是“狗”這個(gè)類的具體化對(duì)象,它具有類的特征,占用“內(nèi)存“。
? ?
?圖片來(lái)源:中國(guó)大學(xué)Mooc—用Python玩轉(zhuǎn)數(shù)據(jù)
1.3 串口通訊
?????? 串行接口簡(jiǎn)稱串口(COM口),是采用串行通信方式的擴(kuò)展接口。串行接口(Serial Interface)是指數(shù)據(jù)一位一位地順序傳送。其特點(diǎn)是通信線路簡(jiǎn)單,只要一對(duì)傳輸線就可以實(shí)現(xiàn)雙向通信,從而大大降低了成本,特別適用于遠(yuǎn)距離通信,但傳送速度較慢。目前常用的串口標(biāo)準(zhǔn)有RS-232、RS-422和RS-485,在功能上主要差別體現(xiàn)在抗干擾能力、最大傳輸速度和最大傳輸距離上。在目前計(jì)算機(jī)上使用串口通訊需要配備一根USB轉(zhuǎn)串口線(如下圖),在正確安裝通訊線的驅(qū)動(dòng)后,可在計(jì)算機(jī)的設(shè)備管理器中看到相應(yīng)的COM口。
????在Python編程語(yǔ)言中,pyserial庫(kù)封裝了串口通訊模塊,可以像文件讀寫(xiě)一樣操作串口,如用read,write等函數(shù),極大地簡(jiǎn)化了串口的操作。使用pyserial之前需要對(duì)這個(gè)庫(kù)進(jìn)行安裝,方法非常簡(jiǎn)單,打開(kāi)cmd命令提示符界面,輸入pip install pyserial等待片刻即可自動(dòng)安裝好pyserial庫(kù)。
1.4 線程
線程(英語(yǔ):thread)是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。一條線程指的是進(jìn)程中一個(gè)單一順序的控制流,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程,每條線程并行執(zhí)行不同的任務(wù)。(上述內(nèi)容摘抄自百度百科)
一個(gè)線程中可以對(duì)另一個(gè)線程進(jìn)行操作,如創(chuàng)建、停止等,同一進(jìn)程中的多個(gè)線程之間可以并發(fā)執(zhí)行。Threading模塊是python眾多線程模塊中功能強(qiáng)大,易于使用的線程管理模塊,對(duì)線程的支持較為完善,絕大多數(shù)情況下,只需要使用 threading 這個(gè)高級(jí)模塊就夠了。
本例制作用于接收串口信號(hào)的GUI程序至少需要兩個(gè)線程,一個(gè)線程用于響應(yīng)用于對(duì)于GUI程序的界面操作,如點(diǎn)擊按鈕,另一個(gè)線程用于不停地接收并顯示串口數(shù)據(jù)。
1.5 通訊協(xié)議
通訊協(xié)議指的是數(shù)據(jù)通訊的方式,即通訊雙方約定好的數(shù)據(jù)解讀方法,若通訊協(xié)議不正確會(huì)導(dǎo)致接收到的數(shù)據(jù)沒(méi)有被正確地“翻譯”而無(wú)法獲取所需的信息。正確的通訊協(xié)議包括了正確地?cái)?shù)據(jù)線接線、正確的波特率和正確的數(shù)據(jù)格式等。本例中采用RS422的5線制接線方式,波特率為2400,1位起始位,8位數(shù)據(jù)位,1位校驗(yàn)位(奇校驗(yàn)),1位停止位。具體來(lái)說(shuō)就是在起始位和停止位之間的8位數(shù)據(jù)位用于傳輸一個(gè)字符信息,典型地為ASCII碼字符,而一組完整的數(shù)據(jù)往往由多個(gè)ASCII碼字符組成,也就是需要進(jìn)行多次傳輸才能完整地傳輸出一組完整信息。在本例中,數(shù)據(jù)格式如下圖所示,每位數(shù)據(jù)位之間間隔20 ms,每一組數(shù)據(jù)以回車結(jié)束;一組數(shù)據(jù)發(fā)送完后間隔50 ms:
? ? ? ?由于串口通訊的特性,數(shù)據(jù)只能一位一位地傳輸,因此我們讀取串口數(shù)據(jù)也需要從緩存區(qū)一位一位地讀取,可以根據(jù)每位數(shù)據(jù)傳輸?shù)臅r(shí)間間隔來(lái)設(shè)定緩存區(qū)的讀取間隔,如在本例中可采用10 ms讀取一次的方式,讀取到有數(shù)據(jù)之后將數(shù)據(jù)傳送到程序中,并清空緩存區(qū),等待下一次讀取。
2 串口通訊上位機(jī)制作流程
2.1 最終結(jié)果
?????? 首先看一下本例中制作的串口通訊上位機(jī)的最終結(jié)果,如下圖所示。該上位機(jī)程序包含四個(gè)功能模塊:左上角的串口通訊,左下角的原始數(shù)據(jù)顯示,右上角的實(shí)時(shí)數(shù)據(jù)顯示,右下角的文件保存。
????具體的使用流程和功能如下圖:啟動(dòng)軟件后,首先需要選擇或者輸入串口號(hào),這里利用下拉菜單動(dòng)態(tài)識(shí)別可用串口;點(diǎn)擊開(kāi)始采集之后,開(kāi)始采集以及串口配置區(qū)域變?yōu)椴豢捎?#xff0c;停止采集按鈕變?yōu)榭捎?#xff0c;同時(shí)左下角的數(shù)據(jù)接收框開(kāi)始顯示接收到的原始數(shù)據(jù),右上角的數(shù)據(jù)顯示框顯示轉(zhuǎn)換之后的數(shù)據(jù)信息;在想要保存采集數(shù)據(jù)時(shí),設(shè)置好右下角區(qū)域的路徑、文件名、數(shù)據(jù)名和采集數(shù)量之后,點(diǎn)擊記錄數(shù)據(jù)即可將當(dāng)前數(shù)據(jù)寫(xiě)入到文件。在程序運(yùn)行過(guò)程中,原始數(shù)據(jù)區(qū)域會(huì)不斷地接收信號(hào),為了避免長(zhǎng)時(shí)間的數(shù)據(jù)堆積導(dǎo)致運(yùn)行速度變慢,在數(shù)據(jù)滿300行或者點(diǎn)擊清空內(nèi)容按鈕時(shí),該數(shù)據(jù)框?qū)崿F(xiàn)一次清空操作;當(dāng)然,也可手動(dòng)點(diǎn)擊清空按鈕。
????在上面這張操作演示圖中,由于手邊沒(méi)有硬件設(shè)備,這里用了虛擬串口(左下軟件)和串口調(diào)試助手(左上軟件)來(lái)模擬實(shí)際的儀器所發(fā)送的串口數(shù)據(jù);右上角軟件為本文開(kāi)發(fā)的采集軟件,其用到的串口也事先由虛擬串口配置好了。右下角是保存數(shù)據(jù)所在的文件夾。
2.2 堆代碼的枯燥過(guò)程
2.2.1 類的定義
?????? 對(duì)于GUI編程,通常將界面的控件、變量和函數(shù)封裝成一個(gè)類,在使用時(shí)實(shí)例化一個(gè)該類的具體對(duì)象,進(jìn)行相關(guān)的操作。通常而言,一個(gè)類的定義需要包含一個(gè)初始化函數(shù)和若干變量以及函數(shù),在類的對(duì)象實(shí)例化時(shí)自動(dòng)執(zhí)行初始化函數(shù)中的內(nèi)容;為了保證無(wú)論在類中哪個(gè)函數(shù)下定義的變量都具有整個(gè)類的作用域,往往需要在定義的變量前加self進(jìn)行聲明。如self.x這個(gè)變量的作用域是整個(gè)類,而直接定義的變量x作用域只有當(dāng)前函數(shù)中。在本例中類進(jìn)行如下的定義。更具體地,在后面將分模塊、分功能對(duì)代碼進(jìn)行解讀。
class myGUI: def__init__(self): #定義窗口界面、控件、變量、執(zhí)行的操作 #Self.varname defReadUART(self): #串口讀取操作 defSavetofile(self): #保存文件的操作GUI = myGUI() #實(shí)例化對(duì)象2.2.2 窗口和控件的定義
?????? 在Tkinter中,界面的生成需要依靠代碼實(shí)現(xiàn),一個(gè)控件大約需要2-4行代碼即可,一般界面和控件的生成需要在初始化函數(shù)中完成,界面生成和部分控件的生成代碼如下:
def__init__(self): self.window = tk.Tk() self.window.title("油耗采集_byJianxiong Hua") self.APPlabel = ttk.Label(self.window, text = "FCM油耗采集軟件",font = ('黑體', 20)) self.APPlabel.grid(row = 1, column = 1, rowspan = 2, columnspan = 4,sticky = tk.N) self.frame_COMinf = tk.Frame(self.window) self.frame_COMinf.grid(row=3, column=1) self.RunInf = tk.StringVar(value = '請(qǐng)選擇串口號(hào)!') # TK變量,儲(chǔ)存提示信息 self.labelInf = ttk.Label(self.frame_COMinf, textvariable = self.RunInf) self.labelInf.grid(row = 3, column = 2, padx = 5, pady = 3)????在上述代碼中,首先用self.window=tk.Tk()生成一個(gè)窗口,用于承載所有的控件,然后設(shè)置其title特性;然后以self.window為載體定義了一個(gè)叫做self.APPlabel的標(biāo)簽(Label)控件,該控件僅用于顯示信息,可對(duì)其text內(nèi)容和字體等進(jìn)行設(shè)置,然后用grid函數(shù)將其固定(若不固定則不會(huì)顯示在window上);同理,接下來(lái)定義了一個(gè)名叫self.frame_COMinf的Frame框,然后以這個(gè)Frame又定義了一個(gè)用于顯示運(yùn)行狀態(tài)信息的label。這里涉及到控件的變量傳遞:在Tkinter中控件之間傳遞變量需要用到TK變量,這里的tk.StringVar就是一個(gè)字符類型的TK變量,它與self.labelInf中的textvariable特征關(guān)聯(lián)起來(lái)了,在程序運(yùn)行中若想要更改self.labelInf顯示的信息只需要修改對(duì)應(yīng)的TK變量,這里是名叫self.RunInf的這個(gè)StringVar變量。更多的變量傳遞用法在后面有更詳細(xì)地介紹。
????除了label控件,還需要用到輸入框(Entry), 按鈕(Button), 下拉框(Combobox)等。值得一提的是,本例中的Entry, Button, Label和Combobox控件用的是ttk而非tk,二者在用法上幾乎相同,而ttk的界面顯示更加美觀,符合win7和win10的系統(tǒng)風(fēng)格。下面以Combobox為例,再介紹一下控件的定義:
???????self.labelBaudrate?=?ttk.Label(self.frame_COMinf,?text?=?'波特率:') self.Baudrate = tk.IntVar(value = 2400) #定義TK中的整數(shù)變量,存儲(chǔ)波特率 self.comboBaudrate = ttk.Combobox(self.frame_COMinf, width = 12,textvariable = self.Baudrate) self.comboBaudrate["values"] = (100, 300, 600, 1200, 2400,4800, 9600, 14400, 19200, 38400, 56000) self.labelBaudrate.grid(row = 5, column = 1, padx = 5, pady = 3) self.comboBaudrate.grid(row = 5, column =2, padx = 5, pady = 3)?????? 上述代碼中combobox默認(rèn)的值為2400,若點(diǎn)擊下拉箭頭將出現(xiàn)數(shù)組中定義的100,300,……,56000等數(shù)值。
2.2.3 自動(dòng)查詢可用串口號(hào)
?????? 自動(dòng)查詢可用串口號(hào)即點(diǎn)擊串口的下拉框時(shí)動(dòng)態(tài)刷新可用串口,要想實(shí)現(xiàn)該功能需要在初始化函數(shù)中定義combobox時(shí)將對(duì)應(yīng)的函數(shù)與下拉框動(dòng)作相綁定,然后將動(dòng)態(tài)刷新串口功能在對(duì)應(yīng)的函數(shù)中實(shí)現(xiàn),具體如下:
def__init__(self): …… self.COM = tk.StringVar(value = '') #定義TK中的字符變量,存儲(chǔ)一個(gè)串口號(hào) self.comboCOM = ttk.Combobox(self.frame_COMinf, width = 12, textvariable= self.COM, postcommand = self.Port_List) #單機(jī)下拉時(shí)觸發(fā)self.Port_List方法 self.comboCOM.grid(row = 4, column = 2, padx = 5, pady = 3) …… def Port_List(self): port_list = list(serial.tools.list_ports.comports()) port_serial = [] #*******以下提取COM口的端口號(hào)******* if len(port_list) <= 0: self.RunInf.set("未找到端口!") else: for i in range(len(port_list)): port_serial.append(list(port_list[i])[0]) self.comboCOM["values"] = port_serial?????? 上述代碼中涉及到兩個(gè)函數(shù),即__initial__()和Port_List(),前者為類的初始化函數(shù),后者為動(dòng)態(tài)獲取可用串口號(hào)并將其賦值給相應(yīng)的下拉框的功能函數(shù)。該函數(shù)通過(guò)postcommand=self.Port_List語(yǔ)句與下拉框的下拉操作相綁定。
2.2.3 按鈕的執(zhí)行函數(shù)綁定和串口打開(kāi)
?????? 與下拉框綁定執(zhí)行函數(shù)的方法類似,對(duì)于按鈕控件則必須綁定相應(yīng)的函數(shù),以執(zhí)行點(diǎn)擊按鈕時(shí)的操作。這里以開(kāi)始采集和停止采集按鈕為例進(jìn)行說(shuō)明,具體代碼如下:
def __init__(self): …… #開(kāi)始和停止按鈕 self.buttonStart = ttk.Button(self.window, text = "開(kāi)始采集", command = self.Start) self.buttonStart.grid(row = 8, column = 1, padx = 5, pady = 3, sticky =tk.E) self.buttonStop = ttk.Button(self.window, text = "停止采集", command = self.Stop) self.buttonStop.grid(row = 8, column = 3, padx = 5, pady = 3) self.buttonStop.configure(state = 'disabled') self.ser = serial.Serial() #串口變量 …… def Start(self): self.ser.port = self.COM.get() #端口號(hào) self.ser.baudrate = self.Baudrate.get() #波特率 self.ser.timeout = 1 #超時(shí)設(shè)置,1s未讀取到數(shù)據(jù)則返回結(jié)果 strParity = self.Parity.get() #校驗(yàn)形式 if (strParity == "NONE"): self.ser.parity = serial.PARITY_NONE elif (strParity=="ODD"): self.ser.parity = serial.PARITY_ODD elif(strParity=="EVEN"): self.ser.parity = serial.PARITY_EVEN elif(strParity=="MARK"): self.ser.parity = serial.PARITY_MARK elif(strParity=="SPACE"): self.ser.parity = serial.PARITY_SPACE strStopbits = self.Stopbits.get() #停止位 if (strStopbits == "1"): self.ser.stopbits = serial.STOPBITS_ONE elif (strStopbits == "1.5"): self.ser.stopbits =serial.STOPBITS_ONE_POINT_FIVE elif (strStopbits == "2"): self.ser.stopbits = serial.STOPBITS_TWO self.RunInf.set("串口打開(kāi)失敗!") self.ser.open() #打開(kāi)串口 if (self.ser.isOpen()): #判斷是否成功打開(kāi) self.buttonStart.configure(state = 'disabled') self.buttonStop.configure(state = 'normal') self.comboCOM.configure(state = 'disabled') self.comboBaudrate.configure(state = 'disabled') self.comboParity.configure(state ='disabled') self.comboStopbits.configure(state = 'disabled') self.RunInf.set("已成功打開(kāi)串口") self.uartState = True self.ReadUART() #調(diào)用讀取串口的程序 def Stop(self): #關(guān)閉串口 self.t.cancel() #停止定時(shí)器 if (self.ser.isOpen()): self.ser.close() self.buttonStop.configure(state = 'disabled') self.buttonStart.configure(state = 'normal') self.comboCOM.configure(state = 'normal') self.comboBaudrate.configure(state = 'normal') self.comboParity.configure(state = 'normal') self.comboStopbits.configure(state = 'normal') self.RunInf.set("已關(guān)閉串口!") self.uartState = False? ? ? ?這段代碼列出了兩個(gè)按鈕(開(kāi)始采集和停止采集)的功能函數(shù),具體通過(guò)定義Button控件時(shí)的command=self.Start/Stop屬性將Start/Stop函數(shù)與對(duì)應(yīng)的按鈕進(jìn)行了綁定,這樣在點(diǎn)擊按鈕時(shí)即可執(zhí)行對(duì)應(yīng)的函數(shù)。在initial函數(shù)中還需要聲明一個(gè)串口變量ser,然后再Start函數(shù)中對(duì)ser變量中的端口號(hào)port、波特率baudrate、校驗(yàn)parity、停止位stopbits、超時(shí)時(shí)間等進(jìn)行賦值,然后通過(guò)ser.open()打開(kāi)串口,在判斷串口成功打開(kāi)后,執(zhí)行串口讀取函數(shù)self.ReadUART()。該函數(shù)涉及到線程的使用,在2.2.4節(jié)進(jìn)行介紹。在Stop函數(shù)中需要執(zhí)行的操作就簡(jiǎn)單得多,即停止掉線程的定時(shí)器以結(jié)束串口讀取線程,然后關(guān)閉串口,更改按鈕等控件的狀態(tài)。Normal為可用狀態(tài),disabled為不可更改的狀態(tài)(灰色)。
2.2.4 多線程
?????? 前面已經(jīng)提到,線程是在一個(gè)主程序下并行執(zhí)行的一些操作,在執(zhí)行時(shí)不受主線程的影響,尤其是在主程序需要執(zhí)行循環(huán)語(yǔ)句的情況下,往往需要啟動(dòng)新的線程去執(zhí)行其他任務(wù)。這里簡(jiǎn)單介紹兩種基于python的多線程方法。
方法一:
import threadingimport timedef test_thread (): print(“testing thread”) time.sleep(0.5)thread1 = threading.Thread(target=test_thread)thread1.start()?????? 方法一首先需要定義一個(gè)需要新線程執(zhí)行的函數(shù),這里是test_thread()函數(shù),然后利用threading.Thread語(yǔ)句聲明一個(gè)線程,目標(biāo)屬性為test_thread函數(shù),再用start方法啟動(dòng)該線程即可。線程啟動(dòng)后即反復(fù)不停地執(zhí)行test_thread()函數(shù),在主程序停止時(shí)線程也停止。可以用相應(yīng)方法
方法二:
import threadingdef test_thread (): print(“testing thread”) globaltimer1 timer1= threading.Timer(0.5, test_thread) timer1.start()timer1 = threading.Timer(0.5, test_thread)timer.start()?????? 在方法二中,并沒(méi)有像方法一中那樣聲明線程對(duì)象,而是利用構(gòu)建線程定時(shí)器的方法實(shí)現(xiàn)新線程的構(gòu)建。在主程序中首先第一次構(gòu)建線程定時(shí)器并啟動(dòng)后,在執(zhí)行函數(shù)中也需要構(gòu)建線程定時(shí)器,這樣每次函數(shù)執(zhí)行完畢后都會(huì)有一個(gè)線程定時(shí)器對(duì)象對(duì)函數(shù)自身進(jìn)行調(diào)用,這樣也實(shí)現(xiàn)了線程的功能。這種方式較為靈活,用于簡(jiǎn)單的線程操作,還可以用timer1.cancel()方法終止該線程。在本例中即采用的這種方式構(gòu)建串口線程。
? ? 本例中的串口線程關(guān)鍵代碼如下:
def Start(self): …… #打開(kāi)串口的操作 ……????self.ReadUART()??#調(diào)用讀取串口的程序def ReadUART(self): …… #讀取串口數(shù)據(jù)的操作 …… self.t=threading.Timer(0.005,self.ReadUART) self.t.start() #啟動(dòng)定時(shí)器????同理,檢測(cè)數(shù)據(jù)接收框長(zhǎng)度并自動(dòng)清零的線程構(gòu)建如下:
def __init__(self):???????……??????#控件和變量的定義???????……???????self.MonitorText()??#啟動(dòng)文本框監(jiān)視器線程,到達(dá)一定數(shù)量后自動(dòng)清零???????self.window.mainloop() def MonitorText(self): if len(self.OutputText.get('1.0',tk.END)) > 500: #超過(guò)一定長(zhǎng)度后自動(dòng)清零 self.OutputText.delete(1.0,tk.END) self.monitor=threading.Timer(0.1,self.MonitorText) self.monitor.start()2.2.5 原始數(shù)據(jù)接收框
?????? 程序的左下角的文本框即為原始數(shù)據(jù)接收框,用于接收并顯示原始信號(hào),這是一個(gè)簡(jiǎn)單的控件、按鈕、線程的結(jié)合使用。點(diǎn)擊清空內(nèi)容按鈕可清空接收框的內(nèi)容,或者在接收框接收到的數(shù)據(jù)長(zhǎng)度超過(guò)500時(shí)自動(dòng)清空內(nèi)容。具體代碼如下:
???def?__init__(self): …… #其他控件和變量的定義操作 …… #**********************數(shù)據(jù)接收框************ self.frame_Recv = ttk.Frame(self.window) self.frame_Recv.grid(row = 9, column = 1) self.labelRecvName = ttk.Label(self.frame_Recv, text = '接收到的原始數(shù)據(jù):') self.labelRecvName.grid(row = 1, column = 1, padx = 5, pady = 1, sticky= tk.W) self.frameTransSon = tk.Frame(self.frame_Recv) #同一個(gè)frame用了grid不能用pack,因此建立一個(gè)子frame self.frameTransSon.grid(row = 10, column =1, rowspan = 6, columnspan =2, padx = 5, pady = 1) self.scrollbarTrans = tk.Scrollbar(self.frameTransSon) self.scrollbarTrans.pack(side = tk.RIGHT, fill = tk.Y) self.OutputText = tk.Text(self.frameTransSon, wrap = tk.WORD, width =30, height = 8, yscrollcommand = self.scrollbarTrans.set, font =('TimesNewRoman', 8)) self.OutputText.pack() self.buttonClearText = ttk.Button(self.frame_Recv, text = "清空內(nèi)容", command = self.ClearText) self.buttonClearText.grid(row = 17, column = 1, columnspan = 2, padx =5, pady = 1, sticky = tk.N) ……???????self.MonitorText()??#啟動(dòng)文本框監(jiān)視器線程,到達(dá)一定數(shù)量后自動(dòng)清零???????…… def ClearText(self): self.OutputText.delete(1.0,tk.END) def MonitorText(self): if len(self.OutputText.get('1.0',tk.END)) > 500: #文本超過(guò)一定長(zhǎng)度后自動(dòng)清零 self.OutputText.delete(1.0,tk.END) self.monitor=threading.Timer(0.1,self.MonitorText) self.monitor.start() #啟動(dòng)定時(shí)器monitor2.2.6 數(shù)據(jù)傳遞和計(jì)算
?????? 在本例的串口程序中,數(shù)據(jù)傳遞主要包括兩類,一類是控件間的數(shù)據(jù)傳遞,即將數(shù)據(jù)傳遞給控件用于顯示或者獲取控件上顯示的數(shù)據(jù);另一類是讀取串口緩存區(qū)的數(shù)據(jù),并做相應(yīng)的類型轉(zhuǎn)換后進(jìn)行相關(guān)的計(jì)算操作。
?????? 控件間的數(shù)據(jù)傳遞主要依靠的是一類叫做TK變量的特殊數(shù)據(jù)類型,其定義方式為a=tk.StringVar(value=’astring’),然后將該TK變量與相應(yīng)控件的textvariable屬性相綁定,如label1=ttk.Label(window, textvariable=a)。如此一來(lái),在程序中無(wú)論何時(shí)何處只要對(duì)變量a (TK變量)進(jìn)行了修改,那么在label1上顯示的內(nèi)容也會(huì)隨之更改。除了StringVar類型外,TK變量還有tk.DoubleVar,tk.IntVar等類型,在使用時(shí)應(yīng)該注意給這些變量賦值時(shí)需要數(shù)據(jù)類型正確,否則會(huì)出錯(cuò)。
?????? 對(duì)于TK變量值的修改和讀取與傳統(tǒng)的變量有所不同,需要用到set和get方法。例如將剛才定義的a變量進(jìn)行值的修改,應(yīng)寫(xiě)為:a.set(‘this is a string’),要獲取a的數(shù)據(jù)則應(yīng)寫(xiě)為:b = a.get()。另外,除了TK變量外,對(duì)于包含內(nèi)容的控件來(lái)說(shuō),也可以直接用get和set方法來(lái)獲取和修改對(duì)應(yīng)控件的值。
?????? 對(duì)于第二類串口緩存區(qū)的變量,則需要用到python中的串口操作,用.read()函數(shù)讀取緩存區(qū)的變量,一般而言串口以SACII碼方式發(fā)送數(shù)據(jù),因此需要將數(shù)據(jù)解碼,然后進(jìn)行后續(xù)的操作。本例中的串口讀取部分代碼如下:
?? def?__init__(self): …… self.ser = serial.Serial() self.data = [] …… def ReadUART(self): if (self.uartState): try: ch = self.ser.read() self.RunInf.set("正在采集數(shù)據(jù)") ch =ch.decode('ASCII') #油耗儀中數(shù)據(jù)傳輸為ASCII,需要解碼 self.OutputText.insert(tk.END,ch) if ch == 'F': self.ch_flag = 1 #串口數(shù)據(jù)標(biāo)志位為1,表示是需要的數(shù)據(jù) if self.ch_flag == 0: self.data.clear() if self.ch_flag == 1: #如果是需要的數(shù)據(jù)則執(zhí)行以下操作 if ch != 'T': #讀取到T為止 self.data.append(ch) if ch == 'T': #所需要的數(shù)據(jù)是T之前的5位 self.ch_flag =0 #把標(biāo)志位置0 except: self.RunInf.set("發(fā)生錯(cuò)誤!") self.t=threading.Timer(0.005,self.ReadUART) self.t.start() #啟動(dòng)定時(shí)器? ? ? ?在這部分代碼中,在初始化函數(shù)中聲明了一個(gè)串口操作變量ser,利用該變量可對(duì)串口進(jìn)行操作(2.2.3中介紹了如何打開(kāi)串口)。在ReadUART函數(shù)中用ser.read()方法進(jìn)行串口讀取,一次讀取一個(gè)字符,然后用decode(‘ASCII’)函數(shù)進(jìn)行解碼(內(nèi)置的函數(shù)),將其插入到數(shù)據(jù)接收框中。接下來(lái)判斷讀取到的數(shù)據(jù)是否在后續(xù)計(jì)算中會(huì)用到,如果會(huì)用到則將其添加到data列表變量中(data.append(ch))。這樣一次讀取一個(gè)字符,然后該線程一直重復(fù)該過(guò)程即完成了串口數(shù)據(jù)的傳遞和計(jì)算。
2.2.7 文件保存
?????? 在本例中文件保存的邏輯如下:點(diǎn)擊保存按鈕后,在與之綁定的函數(shù)中將保存標(biāo)志位置為1;由于串口線程獨(dú)立于主線程并且無(wú)限循環(huán),因此在串口線程中檢測(cè)保存標(biāo)志位是否為1,否則不保存,是1則保存當(dāng)前的數(shù)據(jù)。具體地,保存數(shù)據(jù)時(shí)需要獲取路徑、文件名、寫(xiě)入的內(nèi)容、寫(xiě)入的數(shù)量等參數(shù),然后每保存一次,采集計(jì)數(shù)+1,直到保存的數(shù)量與設(shè)定的數(shù)量相等時(shí),將保存標(biāo)志位置0。具體代碼如下:
def ReadUART(self): if (self.uartState): try: ch = self.ser.read() self.RunInf.set("正在采集數(shù)據(jù)") …… if self.issave== 1: #判斷保存標(biāo)志位 self.Savefile() #執(zhí)行Savefile函數(shù) self.testCount.set(int(self.testCount.get())+ 1) ifint(self.testCount.get()) == int(self.testNum.get()): self.testCount.set('0') self.issave = 0 self.buttonSavetofile.config(state = 'normal') self.data.clear() #接收完數(shù)據(jù)后清空 except: self.RunInf.set("發(fā)生錯(cuò)誤!") self.t=threading.Timer(0.005,self.ReadUART) self.t.start() #啟動(dòng)定時(shí)器 def Savefile(self): filepath = self.filePath.get() filename = self.fileName.get() wholename = os.path.join(filepath, filename) #完整的路徑+文件名(無(wú)后綴) condition = self.conditionName.get() if os.path.exists(filepath) == False: os.mkdir(filepath) with open(wholename+'.txt', 'a') as f1: ls = [condition + '\t' + str(self.AvgFuel.get()) + '\n'] f1.writelines(ls)#===========保存按鈕執(zhí)行的函數(shù)============= def StartSave(self): self.issave = 1 self.buttonSavetofile.config(state = 'disabled')2.2.8 生成可執(zhí)行程序
?????? 生成exe需要用到pyinstaller,因此需要首先利用pip install pyinstaller安裝該庫(kù),具體生成exe步驟:
1. 啟動(dòng)命令提示符;
2. 在命令提示符中進(jìn)入到包含.py文件和.co文件的文件夾;(直接輸入E:可進(jìn)入磁盤(pán),輸入cd 文件夾路徑即可進(jìn)入文件夾)
3. 輸入pyinstaller -D-i ?圖標(biāo)名稱.ico ?文件名稱.py --noconsole? (--noconsole作用是生成的exe文件在執(zhí)行時(shí)不出現(xiàn)黑色的dos界面)
4. 在dist文件夾中即可找到生成的可執(zhí)行文件,注意-D是生成一個(gè)包含多個(gè)文件的文件夾,若將-D替換為-F即可生成只有一個(gè)exe的可執(zhí)行文件,但其啟動(dòng)速度比-D的文件夾形式更慢.
在后臺(tái)回復(fù)“FCM”或“串口GUI”或“串口程序”可獲取本例的完整代碼、可執(zhí)行程序、虛擬串口軟件和串口調(diào)試助手的下載鏈接。
留言區(qū)
總結(jié)
以上是生活随笔為你收集整理的matlab制作以太网数据接收上位机_Python制作串口通讯上位机的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
 
                            
                        - 上一篇: python服务器稳定性,一种基于Pyt
- 下一篇: r语言 rgl 强制过程中_一个R语言中
