一个串口测试工具的开发总结
一些问题总结
之前用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")
上面的方法确实可以退出该线程的执行,但是在使用中我发现一个现象,我将不同串口要执行的脚本放在不同的线程里面执行,然后杀死指定的某个线程后,打印出当前的线程信息,该线程实例仍然存在,但是隔一会重新运行或者选择另外一个脚本执行的时候,是创建一个新的线程,之前杀死的那个线程就不在了。按理杀死后再打印线程信息是看不到那个线程的,不知道为什么,有空再深入研究。