全網最適合入門的物件導向程式設計教程:60 Python物件導向綜合例項-感測器資料實時繪圖器

FreakStudio發表於2024-12-08

全網最適合入門的物件導向程式設計教程:60 Python 物件導向綜合例項-感測器資料實時繪圖器

image

摘要:

本文將結合之前內容實現模擬一個感測器系統軟體,包括三個執行緒:感測器執行緒生成資料並透過串列埠傳送給主機程序;主機程序透過串列埠接收指令,進行資料濾波和處理後,將處理結果傳送給繪圖執行緒;繪圖執行緒負責接收資料並繪製更新資料曲線。

原文連結:

FreakStudio的部落格

往期推薦:

學嵌入式的你,還不會物件導向??!

全網最適合入門的物件導向程式設計教程:00 物件導向設計方法導論

全網最適合入門的物件導向程式設計教程:01 物件導向程式設計的基本概念

全網最適合入門的物件導向程式設計教程:02 類和物件的 Python 實現-使用 Python 建立類

全網最適合入門的物件導向程式設計教程:03 類和物件的 Python 實現-為自定義類新增屬性

全網最適合入門的物件導向程式設計教程:04 類和物件的Python實現-為自定義類新增方法

全網最適合入門的物件導向程式設計教程:05 類和物件的Python實現-PyCharm程式碼標籤

全網最適合入門的物件導向程式設計教程:06 類和物件的Python實現-自定義類的資料封裝

全網最適合入門的物件導向程式設計教程:07 類和物件的Python實現-型別註解

全網最適合入門的物件導向程式設計教程:08 類和物件的Python實現-@property裝飾器

全網最適合入門的物件導向程式設計教程:09 類和物件的Python實現-類之間的關係

全網最適合入門的物件導向程式設計教程:10 類和物件的Python實現-類的繼承和里氏替換原則

全網最適合入門的物件導向程式設計教程:11 類和物件的Python實現-子類呼叫父類方法

全網最適合入門的物件導向程式設計教程:12 類和物件的Python實現-Python使用logging模組輸出程式執行日誌

全網最適合入門的物件導向程式設計教程:13 類和物件的Python實現-視覺化閱讀程式碼神器Sourcetrail的安裝使用

全網最適合入門的物件導向程式設計教程:全網最適合入門的物件導向程式設計教程:14 類和物件的Python實現-類的靜態方法和類方法

全網最適合入門的物件導向程式設計教程:15 類和物件的 Python 實現-__slots__魔法方法

全網最適合入門的物件導向程式設計教程:16 類和物件的Python實現-多型、方法重寫與開閉原則

全網最適合入門的物件導向程式設計教程:17 類和物件的Python實現-鴨子型別與“file-like object“

全網最適合入門的物件導向程式設計教程:18 類和物件的Python實現-多重繼承與PyQtGraph串列埠資料繪製曲線圖

全網最適合入門的物件導向程式設計教程:19 類和物件的 Python 實現-使用 PyCharm 自動生成檔案註釋和函式註釋

全網最適合入門的物件導向程式設計教程:20 類和物件的Python實現-組合關係的實現與CSV檔案儲存

全網最適合入門的物件導向程式設計教程:21 類和物件的Python實現-多檔案的組織:模組module和包package

全網最適合入門的物件導向程式設計教程:22 類和物件的Python實現-異常和語法錯誤

全網最適合入門的物件導向程式設計教程:23 類和物件的Python實現-丟擲異常

全網最適合入門的物件導向程式設計教程:24 類和物件的Python實現-異常的捕獲與處理

全網最適合入門的物件導向程式設計教程:25 類和物件的Python實現-Python判斷輸入資料型別

全網最適合入門的物件導向程式設計教程:26 類和物件的Python實現-上下文管理器和with語句

全網最適合入門的物件導向程式設計教程:27 類和物件的Python實現-Python中異常層級與自定義異常類的實現

全網最適合入門的物件導向程式設計教程:28 類和物件的Python實現-Python程式設計原則、哲學和規範大彙總

全網最適合入門的物件導向程式設計教程:29 類和物件的Python實現-斷言與防禦性程式設計和help函式的使用

全網最適合入門的物件導向程式設計教程:30 Python的內建資料型別-object根類

全網最適合入門的物件導向程式設計教程:31 Python的內建資料型別-物件Object和型別Type

全網最適合入門的物件導向程式設計教程:32 Python的內建資料型別-類Class和例項Instance

全網最適合入門的物件導向程式設計教程:33 Python的內建資料型別-物件Object和型別Type的關係

全網最適合入門的物件導向程式設計教程:34 Python的內建資料型別-Python常用複合資料型別:元組和命名元組

全網最適合入門的物件導向程式設計教程:35 Python的內建資料型別-文件字串和__doc__屬性

全網最適合入門的物件導向程式設計教程:36 Python的內建資料型別-字典

全網最適合入門的物件導向程式設計教程:37 Python常用複合資料型別-列表和列表推導式

全網最適合入門的物件導向程式設計教程:38 Python常用複合資料型別-使用列表實現堆疊、佇列和雙端佇列

全網最適合入門的物件導向程式設計教程:39 Python常用複合資料型別-集合

全網最適合入門的物件導向程式設計教程:40 Python常用複合資料型別-列舉和enum模組的使用

全網最適合入門的物件導向程式設計教程:41 Python常用複合資料型別-佇列(FIFO、LIFO、優先順序佇列、雙端佇列和環形佇列)

全網最適合入門的物件導向程式設計教程:42 Python常用複合資料型別-collections容器資料型別

全網最適合入門的物件導向程式設計教程:43 Python常用複合資料型別-擴充套件內建資料型別

全網最適合入門的物件導向程式設計教程:44 Python內建函式與魔法方法-重寫內建型別的魔法方法

全網最適合入門的物件導向程式設計教程:45 Python實現常見資料結構-連結串列、樹、雜湊表、圖和堆

全網最適合入門的物件導向程式設計教程:46 Python函式方法與介面-函式與事件驅動框架

全網最適合入門的物件導向程式設計教程:47 Python函式方法與介面-回撥函式Callback

全網最適合入門的物件導向程式設計教程:48 Python函式方法與介面-位置引數、預設引數、可變引數和關鍵字引數

全網最適合入門的物件導向程式設計教程:49 Python函式方法與介面-函式與方法的區別和lamda匿名函式

全網最適合入門的物件導向程式設計教程:50 Python函式方法與介面-介面和抽象基類

全網最適合入門的物件導向程式設計教程:51 Python函式方法與介面-使用Zope實現介面

全網最適合入門的物件導向程式設計教程:52 Python函式方法與介面-Protocol協議與介面

全網最適合入門的物件導向程式設計教程:53 Python字串與序列化-字串與字元編碼

全網最適合入門的物件導向程式設計教程:54 Python字串與序列化-字串格式化與format方法

全網最適合入門的物件導向程式設計教程:55 Python字串與序列化-位元組序列型別和可變位元組字串

全網最適合入門的物件導向程式設計教程:56 Python字串與序列化-正規表示式和re模組應用

全網最適合入門的物件導向程式設計教程:57 Python字串與序列化-序列化與反序列化

全網最適合入門的物件導向程式設計教程:58 Python字串與序列化-序列化Web物件的定義與實現

全網最適合入門的物件導向程式設計教程:59 Python並行與併發-並行與併發和執行緒與程序

更多精彩內容可看:

給你的 Python 加加速:一文速通 Python 平行計算

一文搞懂 CM3 微控制器除錯原理

肝了半個月,嵌入式技術棧大彙總出爐

電子計算機類比賽的“武林秘籍”

一個MicroPython的開源專案集錦:awesome-micropython,包含各個方面的Micropython工具庫

Avnet ZUBoard 1CG開發板—深度學習新選擇

SenseCraft 部署模型到Grove Vision AI V2影像處理模組

文件和程式碼獲取:

可訪問如下連結進行對文件下載:

https://github.com/leezisheng/Doc

image

本文件主要介紹如何使用 Python 進行物件導向程式設計,需要讀者對 Python 語法和微控制器開發具有基本瞭解。相比其他講解 Python 物件導向程式設計的部落格或書籍而言,本文件更加詳細、側重於嵌入式上位機應用,以上位機和下位機的常見串列埠資料收發、資料處理、動態圖繪製等為應用例項,同時使用 Sourcetrail 程式碼軟體對程式碼進行視覺化閱讀便於讀者理解。

相關示例程式碼獲取連結如下:https://github.com/leezisheng/Python-OOP-Demo

正文

接下來,我們將運用之前學習過的內容實現如下任務:

  • 模擬感測器執行緒生成資料,透過串列埠協議傳輸給模擬主機程序;
  • 模擬主機程序透過串列埠傳送給感測器執行緒指令以獲取資料,進行資料濾波和處理,並將資料傳輸給繪圖執行緒;
  • 繪圖執行緒完成資料曲線繪製和更新操作。

可以注意到,我們之前為了闡述物件導向程式設計的技巧,所提及的某些類和類之間的關聯設計,在實際應用中並非最佳實踐。此外,原先的程式程式碼中,為了增強讀者的直觀感受,我們使用了大量的 print 語句進行輸出。然而,這樣的做法在程式進行並行執行時,不僅會降低整體的執行效率,而且在觀察終端輸出時,對於多執行緒或多程序的執行順序也易造成混淆,使得理解變得困難。因此,我們有必要對這部分程式碼進行最佳化調整,以提高程式的效能和可讀性。

在以下程式碼中,我們定義了一個 MasterProcess 主機多程序類,它繼承了 Process 類,用於建立和管理主機多程序。可以看到 MasterProcess 主機多程序類直接在__ReadMasterSerial 和__WriteMasterSerial 方法中實現了串列埠的讀寫,而不像原先程式碼 MasterClass 類繼承於 SerialClass 類。

示例程式碼如下:

class MasterProcess(Process):
    '''
        主機多程序類
    '''
    _# 類變數:_
    _#   START_CMD       - 開啟命令      -0_
    _#   STOP_CMD        - 關閉命令      -1_
    _#   SENDID_CMD      - 傳送ID命令    -2_
    _#   SENDVALUE_CMD   - 傳送資料命令   -3_
    START_CMD, STOP_CMD, SENDID_CMD, SENDVALUE_CMD = (0, 1, 2, 3)

    def __init__(self,
                 lock,
                 Queue,
                 simplequeue,
                 port:str = "COM17",
                 baudrate:int = 115200,
                 bytesize:int = serial.EIGHTBITS,
                 parity  :str = serial.PARITY_NONE,
                 stopbits:int = serial.STOPBITS_ONE):
        '''
        MasterProcess初始化函式
        :param lock: 互斥鎖
        :param Queue: 佇列
        :param port: 埠號
        :param baudrate: 波特率
        :param bytesize: 資料位
        :param parity: 校驗位
        :param stopbits: 停止位
        '''
        self.lock               = lock
        self.Queue              = Queue
        self.simplequeue        = simplequeue
        self.dev                = serial.Serial()
        self.dev.port           = port
        self.dev.baudrate       = baudrate
        self.dev.bytesize       = bytesize
        self.dev.parity         = parity
        self.dev.stopbits       = stopbits
        _# 設定讀取timeout超時時間_
        self.dev.timeout        = 0.3
        _# 設定寫入timeout超時時間_
        self.dev.write_timeout  = 0.3
        _# 資料快取_
        self.datalist           = []
        _# 濾波器長度_
        self.filterlength       = 3
        _# 資料處理類例項_
        self.dataprocessobj     = DateProcessClass(self.datalist,self.filterlength)
        _# Process初始化方法_
        Process.__init__(self)
    def StartMasterSerial(self):
        '''
        開啟主機串列埠
        :return: None
        '''
        self.dev.open()
    def StopMasterSerial(self):
        '''
        停止主機串列埠
        :return: None
        '''
        self.dev.close()
    def __ReadMasterSerial(self):
        '''
        讀取主機串列埠,私有方法
        :return data[int] : 讀取的資料
        '''
        _# 按行讀取_
        data = self.dev.readline()
        _# 如果接收到位元組的情況下,進行處理_
        if data != b'':
            _# 收到為二進位制資料_
            _# 用utf-8編碼將二進位制資料解碼為unicode字串_
            _# 字串轉為int型別_
            data = int(data.decode('utf-8', 'replace'))
        _# 否則,設定data為-1_
        else:
            data = -1
        return data
    def __WriteMasterSerial(self,write_data):
        '''
        寫入主機串列埠,私有方法
        :param write_data: 寫入的資料
        :return:
        '''
        _# 非阻塞方式寫入_
        self.dev.write(write_data.encode())
        _# 輸出換行符_
        _# write的輸入引數必須是bytes格式_
        _# 字串資料需要encode()函式將其編碼為二進位制資料,然後才可以順利傳送_
        _# \r\n表示換行回車_
        self.dev.write('\r\n'.encode())
    def RecvSensorID(self):
        '''
        讀取感測器ID
        :return sensorid[int] : 讀取的感測器id號
        '''
        sensorid = self.__ReadMasterSerial()
        return sensorid
    def RecvSensorValue(self):
        '''
        讀取感測器資料值
        :return data[int] : 讀取的感測器資料
        '''
        data = self.__ReadMasterSerial()
        return data
    def SendSensorCMD(self,cmd):
        '''
        主機傳送命令
        :param cmd : MasterProcess中的類變數
        :return: None
        '''
        self.__WriteMasterSerial(str(cmd))

同時,我們在其中定義了一個 run 方法,這個方法在多程序啟動後被呼叫。在這個方法中,首先開啟串列埠,然後傳送獲取 ID 的指令,接收感測器的 ID 號。然後進入一個無限迴圈,每 9 次迴圈,計算一次最大值和最小值,並列印出來。然後傳送獲取資料的指令,接收感測器的資料,將資料放入佇列和列表中,然後對資料進行濾波處理,將濾波後的資料放入另一個佇列。最後,列印出接收到的感測器資料,並讓當前程序休眠 0.5 秒。

示例程式碼如下:

def run(self):
        '''
        多程序start後執行的方法
        :return: None
        '''
        _# 執行計數變數_
        count = 0
        _# 檔案儲存索引計數變數_
        index = 0
        _# 開啟串列埠_
        self.StartMasterSerial()
        self.lock.acquire()
        print(" Master Process Started ")
        self.lock.release()

        _# 傳送獲取ID指令_
        self.SendSensorCMD(self.SENDID_CMD)
        _# 獲取感測器ID號_
        id = self.RecvSensorID()

        self.lock.acquire()
        print(" Recv Sensor ID : ", id)
        self.lock.release()

        while True:
            if count == 9:
                maxvalue = self.dataprocessobj.DateCalMax()
                minvalue = self.dataprocessobj.DateCalMin()
                self.lock.acquire()
                print("----------------------------------")
                print("Max Value: ", maxvalue)
                print("Min Value: ", minvalue)
                print("----------------------------------")
                self.lock.release()
                count = 0
            else:
                count = count + 1

            _# 傳送獲取資料指令_
            self.SendSensorCMD(self.SENDVALUE_CMD)

            self.lock.acquire()
            print("Master Send SENDVALUE_CMD")
            self.lock.release()

            _# 接收感測器資料值_
            data = self.RecvSensorValue()
            self.Queue.put(data)

            self.datalist.append(data)
            filterdata,filterdatalist = self.dataprocessobj.DateFilter()
            self.simplequeue.put(filterdata)

            self.lock.acquire()
            print("  Recv Sensor Data : ",data)
            self.lock.release()

            time.sleep(0.5)

同時,我們需要注意 MasterProcess 主機程序類和 DateProcessClass 資料處理類為組合關係,使用如下語句實現:

_# 資料快取_
        self.datalist           = []
        _# 濾波器長度_
        self.filterlength       = 3
        _# 資料處理類例項_
        self.dataprocessobj     = DateProcessClass(self.datalist,self.filterlength)

image

同時我們重寫了 DateProcessClass 資料處理類,為方便理解程式執行情況,我們去掉了異常類、日誌和 print 輸出語句,同時在 DateFilter 方法中將計算出的平均值也進行返回。

class DateProcessClass():
    def __init__(self, DateList: List[int], FilterLength: int):
        self.DateList = DateList
        self.FilterLength = FilterLength
        self.TempList = [0] * (self.FilterLength)

    def DateFilter(self) -> List:
        _# 遍歷DateList_
        for index, value in enumerate(self.DateList):
            _# 把每個值都當成傳入的新的感測器的值_
            NowValue = value
            _# 表示列表求和的變數_
            sum = 0
            for i in range(self.FilterLength - 1):
                _# 實現列表的移位操作_
                self.TempList[i] = self.TempList[i + 1]
                _# 實現列表求和_
                sum += self.TempList[i]
            self.TempList[self.FilterLength - 1] = NowValue
            sum += self.TempList[self.FilterLength - 1]
            _# 求平均值_
            average = sum / self.FilterLength
            _# 將計算得到的平均值替換原始值_
            self.DateList[index] = average
        _# 計算完成後將TempList中元素清零_
        self.TempList = [0] * (self.FilterLength)
        return average,self.DateList

    def DateCalMax(self) -> int:
        max_value = max(self.DateList)
        return int(max_value)

    def DateCalMin(self) -> int:
        min_value = min(self.DateList)
        return int(min_value)

同時,我們重寫了繪圖類,該類包含了初始化、資料更新和定時更新等方法。在初始化方法中,程式建立了一個 Qt 應用例項物件、一個多皮膚圖形視窗物件以及兩個繪圖曲線物件。資料更新方法用於接收感測器資料並將其新增到快取列表中,然後將資料轉化為圖形。定時更新方法則用於定時進行曲線更新。

這裡主要對 GetValue 方法和 DataUpdate 方法進行改寫,同時我們可以看到在初始化方法和繪圖曲線中增加了濾波後資料的相關屬性,用於檢視濾波效果。

示例程式碼如下:

class PlotThread:
    def __init__(self,lock,queue,simplequeue,wintitle:str="Basic plotting examples",plottitle:str="Updating plot",width:int=1000,height:int=600):
        '''
        用於初始化PlotThread類
        :param wintitle:  視窗標題
        :param plottitle: 圖層標題
        :param width:     視窗寬度
        :param height:    視窗高度
        '''
        self.lock               = lock
        self.queue              = queue
        self.simplequeue        = simplequeue
        _# Qt應用例項物件_
        self.app                = None
        _# 視窗物件_
        self.win                = None
        _# 設定視窗標題_
        self.title              = wintitle
        _# 設定視窗尺寸_
        self.width              = width
        self.height             = height
        _# 感測器資料_
        self.value              = 0
        _# 存放濾波後資料_
        self.filtervalue        = 0
        _# 計數變數_
        self.__count            = 0
        _# 感測器資料快取列表_
        self.valuelist          = []
        _# 感測器濾波資料快取列表_
        self.filtervaluelist    = []
        _# 繪圖曲線_
        self.curve              = None
        _# 濾波後繪圖曲線_
        self.filtercurve        = None
        _# 圖層物件_
        self.plotob             = None
        _# 圖層標題_
        self.plottitle          = plottitle
        _# 定時器物件_
        self.timer              = QtCore.QTimer()
        _# 定時時間_
        self.time               = 0
        _# Qt應用和視窗初始化_
        self.appinit()

        self.lock.acquire()
        print(" PlotClass Object Init Complete ")
        self.lock.release()

    def appinit(self):
        '''
        用於qt應用程式初始化,新增視窗、曲線和圖層
        :return: None
        '''
        _# 建立一個Qt應用,並返回該應用的例項物件_
        self.app = pg.mkQApp("Plotting Example")
        _# 生成多皮膚圖形_
        _# show:(bool) 如果為 True,則在建立小部件後立即顯示小部件。_
        _# title:(str 或 None)如果指定,則為此小部件設定視窗標題。_
        self.win = pg.GraphicsLayoutWidget(show=True, title=self.title)
        _# 設定視窗尺寸_
        self.win.resize(self.width, self.height)
        _# 進行視窗全域性設定,setConfigOptions一次性配置多項引數_
        _# antialias啟用抗鋸齒,useNumba對影像進行加速_
        pg.setConfigOptions(antialias=True, useNumba=True)

        _# 新增圖層_
        self.plotob = self.win.addPlot(title=self.plottitle)
        _# 新增曲線_
        _# 原始資料-黃色曲線_
        self.curve = self.plotob.plot(pen='y')
        _# 濾波後資料-紅色曲線_
        self.filtercurve  = self.plotob.plot(pen='r')

    def GetValue(self,value,filtervalue):
        '''
        用於接收感測器資料,加入快取列表
        :param value: 感測器資料
        :param filtervalue: 感測器濾波後資料
        :return: None
        '''
        self.value          = value
        self.valuelist.append(self.value)
        self.filtervalue    = filtervalue
        self.filtervaluelist.append(self.filtervalue)

    def DataUpdate(self):
        '''
        用於定時進行曲線更新,這裡模擬繪製正弦曲線
        :return: None
        '''
        self.value = self.queue.get()
        self.filtervalue = self.simplequeue.get()
        self.GetValue(self.value,self.filtervalue)
        _# 將資料轉化為圖形_
        self.curve.setData(self.valuelist)
        self.filtercurve.setData(self.filtervaluelist)

    def SetUpdate(self,time:int = 100):
        '''
        設定定時更新任務
        :param time: 定時的時間
        :return: None
        '''
        _# 定時器結束,觸發DataUpdate方法_
        self.timer.timeout.connect(self.DataUpdate)
        _# 啟動定時器_
        self.timer.start(time)
        _# 定時時間_
        self.time = time
        _# 進入主事件迴圈並等待_
        pg.exec()

GetValue 方法用於接收感測器資料並將其加入快取列表,DataUpdate 方法定時執行,它使用佇列接收 MasterProcess 程序中產生和傳輸的資料,並將更新後的資料透過 setData 方法實現繪圖曲線的更新。可以看到,資料的繪圖和更新都是在主執行緒中進行的,這是由於在使用 PyQtGraph 繪圖時,如果在主執行緒之外進行繪圖操作,可能會出現繪圖不生效或程式崩潰的問題。

image

image

在主執行緒中完成建立互斥鎖、訊息佇列和程序例項,然後建立感測器執行緒和繪圖執行緒例項,最後啟動程序和執行緒。示例程式碼如下:

if __name__ == "__main__":
    _# 建立互斥鎖_
    lock    = Lock()
    _# 建立訊息佇列_
    queue   = Queue(5)
    _# 建立訊息佇列_
    simplequeue = SimpleQueue()

    _# 建立程序例項_
    m_process = MasterProcess(lock,queue,simplequeue,port = "COM17")
    _# 建立執行緒例項_
    s_thread  = SensorThread(lock,port="COM11", id=0, state=SensorThread.WORK_MODE["RESPOND_MODE"])
    _# 建立繪圖類例項_
    p_thread  = PlotThread(lock,queue,simplequeue)

    _# 啟動程序_
    m_process.start()
    _# 開啟執行緒,start方法以併發方式執行_
    s_thread.start()
    _# 啟動p_thread的定時任務_
    p_thread.SetUpdate(600)

接下來,我們執行程式:

image

可以看到整個程式可以並行執行,對於並行執行的 Python 程式來說,我們可以使用 Profile/CProfile 等工具進行分析除錯,或者利用 viztracer/SnakeViz 等視覺化工具生成執行緒/程序執行的火焰圖,這些工具可以完成記錄函式的入口/出口,函式引數/返回值、任意變數的值以及執行緒/程序的執行順序等操作。

image

相關文章