【问题标题】:Timers cannot be stopped from another thread (short example with nidaqmx-python and callbacks)无法从另一个线程停止计时器(使用 nidaqmx-python 和回调的简短示例)
【发布时间】:2020-07-16 00:38:00
【问题描述】:

我在这个论坛上看到了关于该主题的其他问题,但没有一个问题能帮助我理解如何处理这个问题。在我看来,它们中的大多数也是关于相当复杂和冗长的代码。我相信我正在做一些相当简单的事情/想做一些相当简单的事情。我希望有人能帮帮忙!下面是广泛的解释,然后是我当前的代码。

注意:请不要删除此问题。我对以下内容进行了很多思考,并仔细浏览了相关线程,但无济于事。我也相信发布这个是有道理的,因为它部分与一个更通用的问题有关:如何在后台运行回调的同时实时绘图(参见最后的摘要),可以总结为我的总体目标。

设置和目标: National Instruments 采集模块(这很重要)NI cDAQ9178,通过 nidaqmx-python 接口,由 NI 维护的包,文档为 here。那里输入了一些模拟信号,目标是以一定的采样率(大约 1000 Hz)连续采集它(直到我决定停止采集),同时实时绘制信号。绘图不需要经常刷新(10Hz 刷新率甚至可以)。我在 conda 虚拟环境中使用带有 Python 3.7 的 Windows 10,并在 PyCharm 中完成编辑。理想情况下,事情应该在 PyCharm 和任何终端中都有效。

情况: nidaqmx-python 提供高级函数,允许注册回调(定义为一个愿望),每次一定数量的样本(在我的情况下为 100,但这并不严格)填充PC缓冲区。这个想法是,下面定义的回调在该点读取缓冲区,并做一些事情(在我的例子中,为了简洁起见,我已经取出了一些低通滤波,一些存储到全局变量data,也许绘图 - 见下文)。

问题:我一直在胡闹,将实时绘制的数据包含在回调中,但使用 matplotlib 是一场噩梦,因为回调使用主线程以外的线程,并且 matplotlib 不喜欢从主线程之外的任何地方调用。我已经用谷歌搜索了其他为实时绘图而优化的库(而且,我在想​​,希望线程安全)但这并不容易:我无法让 vispy 工作,甚至无法安装 pyqtgraph,只是为了给你一些例子。然后我在网上看到了几篇关于使用 matplotlib 管理相当不错的实时动画的帖子,尽管它的开发考虑了发布而不是这些应用程序;所以我想让我们试一试。

我的看法: 因为我不能让 matplotlib 从回调内部完成工作,所以我做了以下(你在下面看到的代码):在回调之后和任务开始之后使用task.start()(特定于nidaqmx-python),我只创建了一个while 循环来绘制全局变量buffer。我认为这是一个不错的技巧:看,buffer 每 0.1 秒左右(没关系)由回调更新(称之为),另一方面,while 循环正在绘制buffer一遍又一遍地变化,每次在绘图前擦除,有效地产生一个实时的绘图。

注意:我完全知道绘图部分不如它可以制作的那么好(我可能应该使用 matplotlib 的 ax API 和subplots,更不用说动画了),但我不关心此时此刻。我稍后会处理它并对其进行改进以提高效率。

我想要的:这实际上是我想要的......除了,为了阻止它,我在 while 循环周围引入了 try:except: 语句,如您在下面的代码中看到。自然地,按下CTRL+C 确实会中断循环......但它也会中断整个运行脚本并给我留下以下错误:forrtl: error (200): program aborting due to control-C event,在 PyCharm 中,以及从终端运行时的以下精度:

Image              PC                Routine            Line        Source
libifcoremd.dll    00007FFECF413B58  Unknown               Unknown  Unknown
KERNELBASE.dll     00007FFF219F60A3  Unknown               Unknown  Unknown
KERNEL32.DLL       00007FFF23847BD4  Unknown               Unknown  Unknown
ntdll.dll          00007FFF240CCED1  Unknown               Unknown  Unknown
QObject::~QObject: Timers cannot be stopped from another thread

不便之处在于我别无选择,只能关闭 python shell(再次想到 PyCharm),并且我无法访问我宝贵的变量 data,其中包含......好吧,我的数据。

猜测:显然,回调不喜欢以这种方式停止。 nidaqmx_python 任务应该用task.stop() 停止。我尝试将task.stop() 放在KeyboardInterrupt except: 之后,但这没有帮助,因为CTRL+C 将脚本停止在顶部/ 而不是中断while 循环。我相信需要一些更复杂的方法来停止我的任务。这几天我一直在考虑这个问题,但想不出一种同时拥有这两种东西的方法:我可以停止一项任务,同时进行实时绘图。请注意,在没有绘图的情况下,很容易在ENTER 按键时停止任务:只需在最后写入

input('Press ENTER to stop task')
task.stop()

但当然,仅执行上述操作并不允许我包含实时绘图部分。

总结:我无法从连续读取数据的回调中调用 matplotlib,所以我写了一个 while 循环用于在单独的块中进行实时绘图,但后来我看不到任何办法停止while 循环而不出现上述错误(我认为,它抱怨回调是从不同的线程停止的)。

我希望我说的很清楚,如果没有,请询​​问!

代码:我已经对其进行了清理,以使其尽可能接近显示问题的 MWE,尽管我当然知道你们中的大多数人没有 NI daq玩耍和连接以便能够运行它。无论如何......在这里:

import matplotlib.pyplot as plt
import numpy as np

import nidaqmx
from nidaqmx import stream_readers
from nidaqmx import constants

sfreq = 1000
bufsize = 100

with nidaqmx.Task() as task:

    # Here we set up the task ... nevermind
    task.ai_channels.add_ai_voltage_chan("cDAQ2Mod1/ai1")
    task.timing.cfg_samp_clk_timing(rate=sfreq, sample_mode=constants.AcquisitionType.CONTINUOUS,
                                    samps_per_chan=bufsize)
    # Here we define a stream to be read continuously
    stream = stream_readers.AnalogMultiChannelReader(task.in_stream)

    data = np.zeros((1, 0))  # initializing an empty numpy array for my total data
    buffer = np.zeros((1, bufsize))  # defined so that global buffer can be written to by the callback

    # This is my callback to read data continuously
    def reading_task_callback(task_idx, event_type, num_samples, callback_data):  # bufsize is passed to num_samples when this is called
        global data
        global buffer

        buffer = np.zeros((1, num_samples))

        # This is the reading part
        stream.read_many_sample(buffer, num_samples, timeout=constants.WAIT_INFINITELY)
        data = np.append(data, buffer, axis=1)  # appends buffered data to variable data

        return 0  # Absolutely needed for this callback to be well defined (see nidaqmx doc).

    # Here is the heavy lifting I believe: the above callback is registered
    task.register_every_n_samples_acquired_into_buffer_event(bufsize, reading_task_callback)
    task.start()  # The task is started (callback called periodically)

    print('Acquiring sensor data. Press CTRL+C to stop the run.\n')  # This should work ...

    fig = plt.figure()
    try:
        while True:
            # Poor's man plot updating
            plt.clf()
            plt.plot(buffer.T)
            plt.show()
            plt.pause(0.01)  # 100 Hz refresh rate
    except KeyboardInterrupt:  # stop loop with CTRL+C ... or so I thought :-(
        plt.close(fig)
        pass

    task.stop()  # I believe I never get to this part after pressing CTRL+C ...

    # Some prints at the end ... nevermind
    print('Total number of acquired samples: ', len(data.T),'\n')
    print('Sampling frequency: ', sfreq, 'Hz\n')
    print('Buffer size: ', bufsize, '\n')
    print('Acquisition duration: ', len(data.T)/sfreq, 's\n')

任何意见将不胜感激。先谢谢各位了!

编辑:在下面接受答案之后,我重写了上面的代码并提出了以下代码,现在可以按预期工作(抱歉,这次我没有清理它,有些行与当前问题无关):

# Stream read from a task that is set up to read continuously
import matplotlib.pyplot as plt
import numpy as np

import nidaqmx
from nidaqmx import stream_readers
from nidaqmx import constants

from scipy import signal

import threading

running = True

sfreq = 1000
bufsize = 100
bufsizeb = 100

global task

def askUser():  # it might be better to put this outside of task
    global running
    input("Press return to stop.")
    running = False

def main():
    global running

    global data
    global buffer
    global data_filt
    global buffer_filt

    global b
    global z

    print('Acquiring sensor data...')

    with nidaqmx.Task() as task:  # maybe we can use target as above

        thread = threading.Thread(target=askUser)
        thread.start()

        task.ai_channels.add_ai_voltage_chan("cDAQ2Mod1/ai1")
        task.timing.cfg_samp_clk_timing(rate=sfreq, sample_mode=constants.AcquisitionType.CONTINUOUS,
                                        samps_per_chan=bufsize)
        # unclear samps_per_chan is needed here above or why it would be different than bufsize
        stream = stream_readers.AnalogMultiChannelReader(task.in_stream)

        data = np.zeros((1, 0))  # probably not the most elegant way of initializing an empty numpy array
        buffer = np.zeros((1, bufsizeb))  # defined so that global buffer can be written in the callback
        data_filt = np.zeros((1, 0))  # probably not the most elegant way of initializing an empty numpy array
        buffer_filt = np.zeros((1, bufsizeb))  # defined so that global buffer can be written in the callback

        b = signal.firwin(150, 0.004)
        z = signal.lfilter_zi(b, 1)

        def reading_task_callback(task_idx, event_type, num_samples, callback_data):  # bufsizeb is passed to num_samples
            global data
            global buffer
            global data_filt
            global buffer_filt
            global z
            global b

            if running:
                # It may be wiser to read slightly more than num_samples here, to make sure one does not miss any sample,
                # see: https://documentation.help/NI-DAQmx-Key-Concepts/contCAcqGen.html
                buffer = np.zeros((1, num_samples))
                stream.read_many_sample(buffer, num_samples, timeout=constants.WAIT_INFINITELY)
                data = np.append(data, buffer, axis=1)  # appends buffered data to variable data

                # IIR Filtering, low-pass
                buffer_filt = np.zeros((1, num_samples))
                for i, x in enumerate(np.squeeze(buffer)):  # squeeze required for x to be just a scalar (which lfilter likes)
                    buffer_filt[0,i], z = signal.lfilter(b, 1, [x], zi=z)

                data_filt = np.append(data_filt, buffer_filt, axis=1)  # appends buffered filtered data to variable data_filt

            return 0  # Absolutely needed for this callback to be well defined (see nidaqmx doc).

        task.register_every_n_samples_acquired_into_buffer_event(bufsizeb, reading_task_callback)  # bufsizeb instead

        task.start()
        while running:  # this is perfect: it "stops" the console just like sleep in a way that the task does not stop
            plt.clf()
            plt.plot(buffer.T)
            plt.draw()
            plt.pause(0.01)  # 100 Hz refresh rate
        # plt.close(fig)  # maybe no need to close it for now

        # task.join()  # this is for threads I guess ... (seems useless to my case?)

        # Some prints at the end ...
    print('Total number of acquired samples:', len(data.T))
    print('Sampling frequency:', sfreq, 'Hz')
    print('Buffer size:', bufsize)
    print('Acquisition duration:', len(data.T)/sfreq, 's')

if __name__ == '__main__':
    main()

请注意,我毕竟不需要task.stop(),因为连续采集任务使用此软件包的方式是读取task.start() 之后的任何代码行,而不是sleep 或类似的代码才能完成任务停止(至少这是我的理解)。

【问题讨论】:

    标签: python multithreading matplotlib except nidaqmx


    【解决方案1】:

    我做的第一件事是摆脱键盘中断循环。我用全局变量running 替换了它,另一个线程在返回时将变量设置为False

    def askUser():
      global running
      input("Press return to stop.")
      running = False
    

    然后,在while loop之前,创建了一个新线程来执行这个函数。

    askUserThread = threading.Thread(target=askUser)
    askUserThread.start()
    

    对于 while 循环,去掉 try catch 语句:

    while running:
      plt.clf()
      plt.plot(buffer.T)
      plt.draw()          # Note: this got changed because .show wasn't working.
      plt.pause(0.01)
    

    这对我仍然不起作用,因为我必须关闭绘图窗口才能显示新的绘图窗口。所以从this answer,我把它从.show改成了.draw

    我的最终代码有点不同(因为我对随机数据进行了抽样),但就是这样。

    # sampling.py
    # by Preston Hager
    
    import matplotlib.pyplot as plt
    import numpy as np
    
    import threading
    
    sfreq = 1000
    bufsize = 100
    
    running = True
    
    data = np.zeros((1, 0))  # initializing an empty numpy array for my total data
    buffer = np.zeros((1, bufsize))  # defined so that global buffer can be written to by the callback
    
    def askUser():
        global running
    
        input("Press return to stop.")
        running = False
    
    def readingTask():
        global data
        global buffer
    
        while running:
            buffer = np.random.rand(1, bufsize)
            # This is the reading part
            data = np.append(data, buffer, axis=1)  # appends buffered data to variable data
    
    def main():
        global running
    
        print('Acquiring sensor data.')
    
        thread = threading.Thread(target=askUser)
        thread.start()
        task = threading.Thread(target=readingTask)
        task.start()
    
        fig = plt.figure()
        while running:
            # Poor's man plot updating
            plt.clf()
            plt.plot(buffer.T)
            plt.draw()
            plt.pause(0.01)  # 100 Hz refresh rate
        plt.close(fig)
    
        task.join()
    
        # Some prints at the end ... nevermind
        print('Total number of acquired samples:', len(data.T))
        print('Sampling frequency:', sfreq, 'Hz')
        print('Buffer size:', bufsize)
        print('Acquisition duration:', len(data.T)/sfreq, 's')
    
    if __name__ == '__main__':
        main()
    

    【讨论】:

    • 嗯,非常感谢!确实如此(请参阅上面对我的问题的编辑)。我花了一点时间来根据我的具体情况调整你提出的建议,但我现在拥有了我想要的,没有任何缺点,我想我理解我是如何没有朝着正确的方向思考的。再次感谢!
    • 乐于助人!有时您只需要外部意见。
    猜你喜欢
    • 2020-03-31
    • 1970-01-01
    • 2014-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-13
    相关资源
    最近更新 更多