一个串口测试工具的开发总结

一些问题总结

之前用pyqt设计了一个串口连接工具,实现了设备的自动化调试。流程大概如下:HDMI连接设备,运行软件,通过串口连接后,选择电脑上编写的一个测试脚本,导入它,执行脚本里面的函数。同时可以对多个设备进行连接操作。总结一下遇到的一些问题。

image

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")

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

posted @ 2021-08-30 15:38  木易123  阅读(316)  评论(0编辑  收藏  举报