12.9. 双向流¶
通道不是单向的。一个实现了 write 的后端可以让主机把字节推送到摄像头,摄像头随即作出反应。这是每一个真实交互工具背后的模式:操作员在主机 GUI 上转动一个旋钮,主机将新值写入一个配置通道,摄像头在下一次捕获时读取它。
12.9.1. 一个配置通道¶
在流式传输的摄像头端脚本基础上,再暴露第二个通道用于 JPEG 质量:
class ConfigChannel:
def __init__(self):
self.quality = 85
def size(self):
return 0
def read(self, offset, size):
# Not used for "host writes to cam" -- but the library
# still needs the method present.
return b''
def write(self, offset, data):
# data is a bytearray view into the protocol buffer.
# Copy out the contents before doing anything with it.
new_q = int(bytes(data))
if 1 <= new_q <= 100:
self.quality = new_q
return len(data)
config = ConfigChannel()
protocol.register(name='config', backend=config)
捕获循环在每次压缩一帧时都从 config.quality 读取:
while True:
img = csi0.snapshot()
latest_jpeg = bytes(
img.compress(quality=config.quality).bytearray()
)
ch.send_event(0x01)
现在主机有了一个旋钮。把它设为 50,下一帧就会更小(也更难看);把它设为 95,下一帧就会更大(也更清晰)。摄像头持续捕获而无需重启;主机也不必推送新脚本。
12.9.2. 来自主机的写入调用¶
在主机端,channel_write() 将字节发送到一个命名通道:
cam.channel_write('config', b'50')
主机 SDK 将这些字节编码为单个(或分片的)CHANNEL_WRITE 数据包,协议层将其递交给摄像头,摄像头的 write(offset=0, data=...) 运行,然后摄像头一方进行确认。当该调用返回时,摄像头已经收到并接受了新值。
从摄像头的角度看,写入是原子的——协议库保证在该通道上的任何其他操作进行之前,后端的 write 已运行完毕。应用程序代码可以在捕获循环内部读取 config.quality,而不必担心主机在快照中途插手。
12.9.3. 桩 size 与只写通道上的 read¶
一个纯写入通道仍然需要定义 size 和 read,即便它们只是返回 0 和 b'' 的桩。库使用方法的存在与否来推导通道的能力标志;一个缺少 read 的后端不会被设置 CHANNEL_FLAG_READ,主机也会拒绝读取尝试。
不过,只写通道上的 read 所返回的字节可用于另一个目的:回显当前值,这样一个刚刚连接的主机就可以询问摄像头“当前的设置是什么?”,而不是从默认值开始。要做到这一点,双方必须就一种序列化方式达成一致。先前示例中的原始字节 int(bytes(data)) 解析对单个整数字段有效,但一旦有了第二个要设置的旋钮就无法扩展了。把 write 切换为解析 JSON,并将其与一个返回相应 JSON 转储的 read 配对,就能把该通道变成一个真正的往返配置存储:
import json
class ConfigChannel:
def __init__(self):
self.quality = 85
self._buf = b''
def size(self):
self._buf = json.dumps({'quality': self.quality}).encode()
return len(self._buf)
def read(self, offset, size):
return self._buf[offset:offset + size]
def write(self, offset, data):
new = json.loads(bytes(data))
if 'quality' in new:
self.quality = int(new['quality'])
return len(data)
现在主机写入 cam.channel_write('config', b'{"quality": 50}') 来设置一个值,并用 cam.channel_read('config') 读回当前状态。摄像头在每次读取时都序列化一份新的 JSON 转储,因此主机总能看到最新的值,而新增另一个旋钮(threshold、exposure、orientation)在双方各自的 JSON 字典中都只是一行代码。
12.9.4. 一个完整的循环¶
有了用于摄像头 → 主机数据的帧通道、用于主机 → 摄像头控制的配置通道,再加上少量粘合代码,应用程序就成了一个交互工具:
主机打开摄像头,开始拉取帧,并在一个窗口中显示它们。
当操作员拖动滑块时,主机在
config上写入新值。摄像头的捕获循环在下一帧时取到该值。
新的帧通过同一个
frame通道流动。
这就是整个模型。两个通道、各两个回调、摄像头上的一个捕获循环、主机上的一个读写循环。没有可见的成帧逻辑,没有可见的错误处理——协议库让可靠的字节传输消于无形。
此后的一切都是应用程序代码。新增第三个通道用于直方图、第四个用于遥测、或第五个用于传感器触发,都是同样的后端类加 protocol.register 配方的重复。一旦摄像头项目到达这一步,协议就不再是有趣的问题了;有趣的是应用程序自身的逻辑。