1. 程式人生 > 其它 >一個串列埠測試工具的開發總結

一個串列埠測試工具的開發總結

一些問題總結

之前用pyqt設計了一個串列埠連線工具,實現了裝置的自動化除錯。流程大概如下:HDMI連線裝置,執行軟體,通過串列埠連線後,選擇電腦上編寫的一個測試指令碼,匯入它,執行腳本里面的函式。同時可以對多個裝置進行連線操作。總結一下遇到的一些問題。

pyqt槽函式 傳遞引數

pyqt設計介面的時候,常用按鈕連線的槽函式幾乎都有額外引數,connect連線的時候藉助partial來實現引數的傳遞。

def onClicked(self, i):
    pass

def onClicked_no_arg(self):
    pass
btn.clicked.connect(onClicked_no_arg)  # 正常

# 而如果繫結的槽函式有額外引數,可以使用functools中的偏函式partial封裝
btn.clicked.connect(partial(self.onClicked, 1))  
# 另外網上搜到使用lambda函式,但是對我使用的pyqt版本無效,不清楚是否舊版本可以
btn.clicked.connect(lambda : self.onClicked(1))   

partial

偏函式是模組functools中的一個類,它的作用類似於將一個函式的一些引數給"凍結"住,從而形成一個更少引數的函式。比如下面定義一個函式fun1a,b兩個引數,

def fun1(a,b):
    print("a=",a)
    print("b=",b)
    print(a+b)

可以用partial來封裝函式fun1,比如當fun1的某個引數經常不變的時候。

  1. partial封裝的時候,不凍結住引數 。那麼使用的時候就得傳入所有引數,這樣跟使用原來的fun1沒有區別

    g = partial(fun1)  # 用partial "包裝"fun1 ,可以把g也當做一個函式來呼叫 
    g(1,2)  # 使用時就得傳入兩個引數,否則報錯缺少引數
    
  2. partial封裝的時候,把一個引數凍結住,呼叫的時候就傳入剩下的引數即可。

    g = partial(fun1, 10)  # 相當於固定了fun1的第一個引數a=10
    g(5)  # 這時候的 5 傳入給了第二個引數b
    '''
    g(5)執行結果是:
    a=10
    b=5
    15
    '''
    

    在凍住第一個引數的情況下,如果呼叫g傳入兩個引數就會報錯。

    g = partial(fun1, 10)  # 相當於固定了fun1的第一個引數a=10
    g(2,5)  
    '''
    TypeError: fun1() takes 2 positional arguments but 3 were given
    '''
    
  3. partial

    凍結的時候,預設是按順序凍結引數,即從左往右給位置引數賦值,如果需要指定凍結哪個,直接手動指明即可

    g = partial(fun1,b=10,a=2)  # 甚至不需要遵從原來的fun1引數順序
    g()
    
    g = partial(fun1,b=10) 
    g(25)  #現在傳入的25是傳遞給了fun1的引數 a
    
  4. partial凍結的時候,如果原來的函式引數有預設值,凍結該引數會覆蓋掉

    def fun1(a,b=10):
        print("a=",a)
        print("b=",b)
        print(a+b)
    g = partial(fun1,b=5)
    g(20)
    '''
    輸出資訊如下,可以看到引數b被覆蓋掉了
    a=20
    b=5
    25
    '''
    
  5. 其他情況,如果函式引數有可變引數啥的和普通位置引數的情形一樣

原來的槽函式需要傳遞一個引數,使用partial將它"封裝"了一次,仍然是個Callable物件,可以看做一個沒有額外引數的函數了。因此把它當做一個新的"槽函式"連線到相應的控制元件訊號上,就解決了引數傳遞的問題。lambda的原理也類似,只是不清楚哪些版本的pyqt才有效,我嘗試用lambda,引數傳遞沒有成功。

多執行緒

對於每一個串列埠,我們編寫一個指令碼進行測試該裝置的功能,有多個串列埠的時候,就要執行多個指令碼檔案。因此用了多執行緒。但是python的多執行緒實際是假的多執行緒,因為全域性直譯器鎖GIL的存在,多個執行緒也只能使用單核。好像多程序可以解決,但沒嘗試,以後再討論。

對每個串列埠的指令碼,使用者手動選擇一個python檔案後,我用__import__手動匯入該檔案,然後執行該檔案以testcase開頭的函式,當然需要指定執行哪個函式。該函式的執行放入一個單獨的執行緒中,

from threading import Thread
serialThread = Thread(target=func,args=(ser, cap))  # 預設要執行的函式,必須有串列埠物件例項和攝像頭物件例項兩個引數,方便在自己的測試指令碼中呼叫一些主程式的資源等  func就是使用者選擇的指令碼中的函式
serialThread.setDaemon(True)
serialThread.start()

因為涉及IO比較多,因此使用多執行緒還是可以提高效率,另外我們的測試指令碼本身是while死迴圈,不得不單獨將它放在一個執行緒執行。

存在的問題是,python對執行緒的結束設計並不友好,如上面的工具介面所示,儘管每個指令碼在單獨的執行緒執行,但是得有個測試停止按鈕,去手動暫停或者直接結束該指令碼的執行。

這個串列埠工具早期是隻通過serial進行串列埠通訊,指令碼放在裝置上的指定目錄的,然後serial傳送一個,比如 Test /data/fun1.lua就能把這個測試指令碼跑起來(就是圖上第五行的第一個下拉選單選擇裝置上的檔案,第二個執行lua檔案按鈕就傳送這條指令 ) 。而要停止掉就很方便,直接ctrl c就行,因此通過串列埠傳送一個chr(0x03)就可以。原先的方案測試指令碼實際是在裝置上執行的。

現在的問題是,測試指令碼的執行是在電腦上執行,如果涉及到操作裝置的部分,我們重新將那部分相關封裝了一下,從而在串列埠直接用 Test fun1 arg1這種方式呼叫,就是上一篇部落格提到的用C++把相關模組的函式封裝了一遍。回到正題,指令碼的執行是在電腦上單獨開的一個執行緒執行它,如果要結束,得手動去觸發。

網上搜到了很多python去結束一個執行緒的方法,有一個是通過設定flag,手動在子執行緒的執行過程中檢查flag,狀態變化的時候就停止掉。但是這樣需要手動繼承一下Thread,然後處理函式的執行。當然該方法有效,但我懶得去繼承Thread重寫這部分。在Stack Overflow [https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread]上看到一個比較方便的方法,手動丟擲異常來結束執行緒。

丟擲異常的部分程式碼在很多部落格上也有解釋,通過ctypes模組來實現。

import ctypes

def terminate_thread(thread):
    """Terminates a python thread from another thread.

    :param thread: a threading.Thread instance
    """
    if not thread.isAlive():
        return

    exc = ctypes.py_object(SystemExit)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
        ctypes.c_long(thread.ident), exc)
    if res == 0:
        raise ValueError("nonexistent thread id")
    elif res > 1:
        # """if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"""
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
        raise SystemError("PyThreadState_SetAsyncExc failed")

上面的方法確實可以退出該執行緒的執行,但是在使用中我發現一個現象,我將不同串列埠要執行的指令碼放在不同的執行緒裡面執行,然後殺死指定的某個執行緒後,打印出當前的執行緒資訊,該執行緒例項仍然存在,但是隔一會重新執行或者選擇另外一個指令碼執行的時候,是建立一個新的執行緒,之前殺死的那個執行緒就不在了。按理殺死後再列印執行緒資訊是看不到那個執行緒的,不知道為什麼,有空再深入研究。