13.3.1.4. 自定义通道

通道(channel)是摄像头端脚本与主机之间的一个具名双向字节流。摄像头注册一个通道并提供产生或消费数据的回调;主机按名称读取和写入该通道。软件包内部用于承载帧的 stream 通道、承载脚本输出的 stdout 通道以及承载脚本上传的 stdin 通道所使用的正是同一套机制,并且该机制对用户脚本也是开放的,因此主机所需的任何应用特定数据都可以搭载同一条 USB 连接,而无需另行发明第二套协议。

这是该软件包最有用的功能,也是标准文档介绍得最不充分的功能,因此本页将端到端地完整讲解它。

13.3.1.4.1. 两个组成部分

自定义通道需要在两端编写协作的代码。摄像头端脚本导入 protocol,定义一个带有三个方法(size()read()poll())外加一个可选的 write() 的类,并调用 protocol.register(name=..., backend=...) 以所选名称发布该通道:

import protocol
import time

class TicksChannel:
    def size(self):
        return 10

    def read(self, offset, size):
        return f'{time.ticks_ms():010d}'

    def poll(self):
        return True

protocol.register(name='ticks', backend=TicksChannel())

size() 方法返回该通道当前可用的字节数。read() 是生产者:给定主机请求的 offsetsize,它返回这些字节(或一个由协议层编码的字符串)。poll() 在有内容可读时返回 True——协议层据此在 read_status() 中将该通道标记为就绪。

主机端程序使用四个 openmv.Camera 方法:has_channel() 检查通道是否存在,channel_size() 查询有多少数据在等待,channel_read() 拉取字节,以及 channel_write() 推送字节。read_status() 一次性轮询每个通道:

from openmv import Camera

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('ticks_cam.py').read())

    while True:
        status = cam.read_status()

        if status.get('ticks'):
            data = cam.channel_read('ticks')
            print(f"ticks: {data.decode()}")

主机循环轮询 read_status();当 ticks 通道就绪时,它会调用不带 sizechannel_read() 以拉取任何可用的数据。摄像头的 TicksChannel.poll() 在每次检查时都返回 True,因此该通道始终处于“就绪”状态,主机每次轮询都会获得一个新的 tick 值。

13.3.1.4.2. 双向通道

对于需要回推数据的主机,摄像头端的类需添加一个接受传入字节的 write() 方法:

import protocol

class CommandChannel:
    def __init__(self):
        self.last_command = b''
        self.replied = False

    def size(self):
        return len(self.last_command)

    def read(self, offset, size):
        self.replied = True
        return self.last_command

    def write(self, offset, data):
        self.last_command = b'echo: ' + bytes(data)
        self.replied = False

    def poll(self):
        return not self.replied and len(self.last_command) > 0

protocol.register(name='echo', backend=CommandChannel())

主机用 channel_write() 向通道写入数据,并通过常规的 read_status() / channel_read() 模式读回响应:

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('echo_cam.py').read())

    cam.channel_write('echo', b'hello')

    while True:
        if cam.read_status().get('echo'):
            print(cam.channel_read('echo').decode())
            break

13.3.1.4.3. 应用程序能从中获得什么

每当应用程序希望利用现有的 USB 连接传输非帧、非打印的数据时,自定义通道都是合适的工具:遥测计数器、从主机 UI 实时流式传输的配置旋钮、反向发送的控制命令、摄像头计算出但不符合 stream 通道所假定的“图像”格式的测量结果。协议层负责处理分帧、分片、确认和重试;脚本只需实现这四个方法的后端,主机只需知道通道名称和数据形态。

CLI 的 --channel NAME 标志是一种从终端快速验证自定义通道而无需编写主机端程序的方式:CLI 会轮询具名通道并打印每次更新的前十个字节。

单次 channel_read()channel_write() 调用的大小限制是协议协商出的 max_payload——默认 4096 字节。主机端方法会自动将较大的写入拆分为相应数量的数据包,因此应用程序可以传递任意大的缓冲区;分片过程是不可见的。