13.3.1.5. 事件¶
迄今为止的各页都是向摄像头发起调用:上传脚本、读取帧、写入通道。这些操作每一个都是由主机发起的——主机请求,摄像头响应。协议也可以反方向运行。摄像头可以在未被请求的情况下向主机推送事件(event),而主机 SDK 会将每个事件交付给应用程序可以重写的回调。
每当应用程序希望在主动询问之前对摄像头注意到的某些情况作出反应时,这就是合适的工具。如果没有事件,唯一的发现方式就是在循环中不断调用 read_status()。
13.3.1.5.1. 默认回调¶
Camera 在内部已经订阅了事件。_handle_event() 是每当事件数据包到达时传输层运行的回调。默认实现处理三种系统事件:
CHANNEL_REGISTERED—— 主机连接后摄像头上出现了一个新通道。框架会刷新其通道缓存,以便下一次has_channel()查找能找到它。CHANNEL_UNREGISTERED—— 一个通道消失了。SOFT_REBOOT—— 摄像头自行重启(看门狗、硬故障、有意调用machine.reset())。
它还会跟踪用于流式传输路径的 stream 通道的帧就绪事件,以及用于 stdout 缓冲的 stdin 通道的脚本启动 / 停止事件。构造函数的 events=True 默认值会保持所有这些功能开启;不需要其中任何功能的应用程序可以向 Camera 传入 events=False,事件子系统便会保持静默。
13.3.1.5.2. 通过子类化来作出反应¶
要处理摄像头引发的应用特定事件,请子类化 Camera 并重写 _handle_event()。先调用父类以保留默认行为,然后分派应用程序关心的事件:
from openmv import Camera
class MyCamera(Camera):
def _handle_event(self, channel_id, event):
super()._handle_event(channel_id, event)
name = self.channels_by_id.get(
channel_id, {}).get('name')
if name == 'motion' and event == 1:
self.on_motion()
def on_motion(self):
print("motion detected")
签名为 (channel_id, event)。对于系统事件,channel_id 为 0,否则为引发该事件的通道的数字 ID;event 是摄像头端脚本选定的一个整数。EventType 枚举为这三种系统事件命名;通道事件则使用摄像头端后端所定义的任何值。
通道事件返回时以数字 ID 而非名称为键。上面的重写使用缓存的 channels_by_id 字典来查找名称;channels_by_name 是它的镜像,以另一个方向为键。
13.3.1.5.3. 摄像头端部分¶
摄像头端脚本通过在 protocol.register() 返回的句柄上调用 send_event() 来引发事件:
import protocol
class MotionChannel:
def size(self):
return 0
def read(self, offset, size):
return b''
def poll(self):
return False
ch = protocol.register(
name='motion', backend=MotionChannel())
while True:
if detect_motion():
ch.send_event(1)
事件编号是由应用程序选定的整数。任何主机的重写准备好处理的值都是允许的;协议层将其视为不透明的载荷。默认情况下,该调用是发后即忘的;当确认事件已送达比往返延迟更重要时,可传入 wait_ack=True 以阻塞直到主机确认。
一个只触发事件而不承载可读数据的通道是一种有效的模式——size 返回 0,read 返回空字节。协议库仍然需要这两个方法都存在才能将通道标记为可读;摄像头端脚本只是从不向其中放入数据。
13.3.1.5.4. 在空闲时驱动接收路径¶
事件与其他所有内容到达于同一条连接上,因此任何发送或接收字节的主机调用都会给传输层一个内联处理待处理事件的机会。一个已经每个周期调用一次 read_status() 或 read_frame() 的轮询循环不需要额外做任何事情。
对于会在数分钟内没有其他 I/O 的程序,poll_events() 会在不发送命令的情况下运行一次接收路径。它会在入站缓冲区一变空就返回,因此围绕它的紧凑循环——或 GUI 事件循环中的一个短定时器——就是保持处理程序响应灵敏的方式。
13.3.1.5.5. 一个完整的循环¶
端到端来看,这个模式是:摄像头端脚本注册一个通道,并在某事发生时调用 send_event();主机端子类重写 _handle_event() 并进行分派。一个除了处理事件什么都不做的主机循环看起来像这样:
with MyCamera('/dev/ttyACM0') as cam:
cam.stop()
cam.exec(open('motion_cam.py').read())
while True:
cam.poll_events()
摄像头进行捕获、判断并引发事件。主机停留在 poll_events() 中直到事件到达,随后 on_motion 运行。当什么都没发生时,不会运行任何 read_status() 调用;当摄像头没有任何要报告的内容时,也不会有帧通过 USB 拉取过来。