一個串列埠測試工具的開發總結
一些問題總結
之前用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
中的一個類,它的作用類似於將一個函式的一些引數給"凍結"住,從而形成一個更少引數的函式。比如下面定義一個函式fun1
有a,b
兩個引數,
def fun1(a,b):
print("a=",a)
print("b=",b)
print(a+b)
可以用partial
來封裝函式fun1
,比如當fun1
的某個引數經常不變的時候。
-
partial
封裝的時候,不凍結住引數 。那麼使用的時候就得傳入所有引數,這樣跟使用原來的fun1
沒有區別g = partial(fun1) # 用partial "包裝"fun1 ,可以把g也當做一個函式來呼叫 g(1,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 '''
-
partial
g = partial(fun1,b=10,a=2) # 甚至不需要遵從原來的fun1引數順序 g() g = partial(fun1,b=10) g(25) #現在傳入的25是傳遞給了fun1的引數 a
-
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 '''
-
其他情況,如果函式引數有可變引數啥的和普通位置引數的情形一樣
原來的槽函式需要傳遞一個引數,使用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")
上面的方法確實可以退出該執行緒的執行,但是在使用中我發現一個現象,我將不同串列埠要執行的指令碼放在不同的執行緒裡面執行,然後殺死指定的某個執行緒後,打印出當前的執行緒資訊,該執行緒例項仍然存在,但是隔一會重新執行或者選擇另外一個指令碼執行的時候,是建立一個新的執行緒,之前殺死的那個執行緒就不在了。按理殺死後再列印執行緒資訊是看不到那個執行緒的,不知道為什麼,有空再深入研究。