12.6. 命名通道¶
每个数据包头部中的通道 ID 让多达 32 个独立的流共享同一个物理传输。通道层将这些数字 ID 转换为命名的、应用程序可见的端点,主机代码可以通过字符串来引用它们。
12.6.1. 四个内置通道¶
摄像头在启动时、任何应用代码运行之前注册四个通道:
stdin—— 主机推送给摄像头执行的脚本字节。IDE 使用此通道发送正在编辑的脚本;主机 SDK 上的exec()是来自 Python 程序的等效调用。stdout—— 来自摄像头print()调用以及未捕获异常回溯的字节。IDE 的串口控制台读取此通道。stream—— 实时预览通道。IDE 从中拉取 JPEG 帧;任何主机脚本都可以用read_frame()做同样的事。profile—— 性能分析事件,仅当摄像头是在启用性能分析的情况下构建时才存在。大多数发布版构建会省略它。
应用代码很少需要触及任何内置通道;有意思的工作发生在应用程序自己注册的通道上。
12.6.2. 注册通道¶
摄像头端的脚本通过调用 protocol.register() 并传入一个名称和一个 Python 后端 对象来注册新通道:
import json
import protocol
import time
trigger_count = 0
class StatusChannel:
def size(self):
# Refresh the snapshot on every host query.
self._buf = json.dumps({
'uptime_s': time.ticks_ms() // 1000,
'triggers': trigger_count,
}).encode()
return len(self._buf)
def read(self, offset, size):
return self._buf[offset:offset + size]
protocol.register(name='status', backend=StatusChannel())
后端对象的方法决定了通道能做什么。一个仅有 size 和 read 的后端是一个 只读数据通道;加上 write 它就变成双向的;加上 poll 主机就可以在付出读取代价之前询问是否有新数据就绪。当有效载荷足够小,能装进一个分片时,在 size 内部采样数据是最简单的模式——缓冲区按需生成,从不缓存,从不竞争。更大的有效载荷——图像帧、传感器轨迹——则需要一种锁存模式,将缓冲区保持到主机完成其多分片读取为止,这一点将在帧通道中介绍。
少量的簿记工作会自动进行:
库分配下一个空闲的通道 ID(0 到 31 之间)。
能力标志由存在的方法推导得出:如果定义了
read则有CHANNEL_FLAG_READ,如果定义了write则有CHANNEL_FLAG_WRITE,如果定义了lock/unlock则有CHANNEL_FLAG_LOCK。会向任何已连接的主机发送一个
CHANNEL_REGISTERED事件数据包,以便其通道列表更新。
返回值是一个 protocol.ProtocolChannel 句柄,应用程序可以持有它。该句柄的 send_event() 方法是摄像头端的钩子,用于告诉主机“此通道上发生了某事,但可读数据没有改变”——触发了一个触发器、按下了一个按钮、达到了某个样本计数里程碑。
12.6.3. 从主机读取通道¶
主机 SDK 在 PyPI 上以 openmv 包的形式提供(pip install openmv),基于 pyserial 进行传输。它的 openmv.camera.Camera 类通过高级方法暴露摄像头的命名通道:
from openmv.camera import Camera
with Camera('/dev/ttyACM0', baudrate=921600) as cam:
cam.update_channels()
if cam.has_channel('status'):
size = cam.channel_size('status')
data = cam.channel_read('status', size)
警告
openmv 包需要 CPython 3.12 或更新版本。更早的解释器缺少 SDK 所依赖的特性;请在 pip install openmv 之前安装一个 3.12+ 的构建版本。
关于这个设置有几点需要注意:
串口字符串——此处为
/dev/ttyACM0——在 Windows 上是COM3形式,在 macOS 上是/dev/cu.usbmodemXXXX,在 Linux 上是/dev/ttyACM*。实际编号取决于摄像头枚举为哪个端口。波特率是协议的魔数值
921600,摄像头的 USB-CDC 栈将其识别为“此客户端使用协议,而非 REPL”。任何其他速率都会回退到普通的串行线路。with Camera(...) as cam:上下文管理器会打开传输、运行PROTO_SYNC、交换能力,并在退出时干净地关闭端口。进入后显式调用update_channels()会用应用程序在启动后注册的任何通道刷新本地通道列表。
channel_size() 和 channel_read() 是主力方法;如果后端有 write 方法,channel_write() 会向摄像头往返一个缓冲区;has_channel() 是在使用某个名称之前安全地检查它是否已注册的方法。通道名称会被查找一次以得到摄像头在 register 期间分配的通道 ID,此后每个数据包都使用该 ID。
每对 channel_size() / channel_read() 调用都需要两次往返:一个数据包询问大小,一个询问字节。通过 USB-CDC,两者合计约一毫秒完成;通过 UART,同样的交换则按串行线路波特率成比例地耗时更长。在紧凑循环中读取的应用代码应仅在大小确实可能变化时才调用 channel_size()——对于固定大小的数据,可以缓存第一次调用得到的大小。
12.6.4. 通道之间的独立性¶
关于通道如何相互作用,有三点值得了解:
独立的流控制。 每个通道都有自己的待处理读取状态、自己的数据,以及自己的
size/read/write回调。stream通道上一次长时间运行的读取不会阻塞应用程序config通道上的读取。每通道顺序保证。 在单个通道内,数据包按顺序传递。即使涉及重传,可靠性层也保证这一点。
共享传输,共享重传预算。 所有通道共享一条物理链路,因此某个通道上的大量流量会因占用线路而拖慢其他通道。
CHANNEL_LOCK机制让一个通道能够为原子的多数据包读取保留线路;后端通过实现lock/unlock回调来启用它。
通道是主机程序与摄像头程序为协作而达成一致的最小接触面。名称、方向性(读或写或两者)、摄像头端的回调方法,以及主机端相匹配的方法调用,就是全部的约定。