8.14. AsyncCSI

一个典型的 OpenMV Cam 脚本以 while True: img = csi0.snapshot() 结尾——这是一个阻塞式的快照循环,根本不需要 asyncio。但一旦应用程序在采集的同时还得做别的事情——监听按钮、向对端发送数据、运行后台任务——这个阻塞调用就会成为障碍。当 snapshot 正在等待下一帧时,事件循环并未运行,因此程序中其他所有协程都会被冻结,直到那一帧到来为止。

本页围绕 CSI 构建一个小型封装器,将 snapshot 变成一个对 await 友好的协程。其结果是一个可直接替换的实现,使采集循环能够与 asyncio 程序的其余部分共存。

8.14.1. 组成部分

CSI API 中的一个部分承担了大部分工作——非阻塞模式下的 snapshot()。调用 snapshot(blocking=False) 要么返回下一帧(如果有帧就绪),要么返回 None(如果没有)。第一次非阻塞调用还会启动摄像头的 DMA 采集(如果它尚未运行),因此封装器无需做任何特殊处理来进行引导启动。

另一个部分是 asyncio.sleep_ms()。封装器在循环中轮询非阻塞快照,并在每次检查之间通过 await asyncio.sleep_ms(0) 让出控制权给事件循环,这样其他每个就绪的协程都有机会在下一次轮询前运行。

8.14.2. 封装器

import asyncio
import csi


class AsyncCSI:
    def __init__(self, *args, **kwargs):
        self._csi = csi.CSI(*args, **kwargs)

    def __getattr__(self, name):
        return getattr(self._csi, name)

    async def snapshot(self):
        while True:
            img = self._csi.snapshot(blocking=False)
            if img is not None:
                return img
            await asyncio.sleep_ms(0)

构造函数封装了一个 CSI 实例。__getattr__ 会把封装器自身未定义的每个属性——resetpixformatframesize,以及所有的 传感器调节项——转发给底层的 CSI,因此除了那个关键的方法之外,封装器看起来与未封装的对象完全相同。

async def snapshot 是新增的部分。它调用 snapshot(blocking=False);如果该调用返回了一张图像,协程就返回它。否则它通过 await asyncio.sleep_ms(0) 让出控制权给事件循环,让其他协程有机会运行,然后循环回去再次尝试。第一次迭代会启动 DMA;后续的迭代则在帧可用时取走它们。

8.14.3. 有伴的快照循环

有了这个封装器之后,快照循环就能像任何其他协程一样融入更大的 asyncio 程序。下面的示例并发运行三个协程:采集循环、一个 LED 闪烁器,以及一个每秒打印一次 hello 的心跳:

import asyncio
import csi
from machine import LED


async def capture_loop(cam):
    while True:
        img = await cam.snapshot()
        # process img here

async def blinker(led, period_ms):
    while True:
        led.on()
        await asyncio.sleep_ms(period_ms)
        led.off()
        await asyncio.sleep_ms(period_ms)

async def hello(period_s):
    while True:
        print("hello")
        await asyncio.sleep(period_s)

async def main():
    cam = AsyncCSI()
    cam.reset()
    cam.pixformat(csi.RGB565)
    cam.framesize(csi.QVGA)

    asyncio.create_task(blinker(LED("LED_BLUE"), 200))
    asyncio.create_task(hello(1))
    await capture_loop(cam)

asyncio.run(main())

这三个协程都在同一个事件循环上推进。当 capture_loop 在两次非阻塞快照轮询之间让出控制权时,blinker 切换 LED 状态,hello 进行打印。当 blinkerhello 在休眠时,capture_loop 轮询摄像头。轮询间隔很短——仅一个事件循环时钟周期——因此它对应用程序看到新帧的时机所增加的延迟可以忽略不计。

采集循环不会阻塞事件循环。添加更多并发工作——例如一个 UART 客户端——只不过是在 main 中再调用一次 create_task() 而已。

备注

帧缓冲区 设置在这种结构中仍然很重要。单缓冲模式会使 snapshot(blocking=False) 返回 None,直到下一帧被采集;双缓冲或三缓冲则可平滑这一点,使封装器通常在上一帧处理完之后的第一次轮询时就能找到一个等待中的已缓冲帧。