15.9. Bidirectional flow¶
Channels are not one-way. A backend that implements write lets
the host push bytes to the cam, and the cam reacts. That’s the
pattern behind every real interactive tool: the operator turns a
knob on the host GUI, the host writes the new value to a config
channel, the cam reads it the next time it captures.
15.9.1. A config channel¶
Adding to the streaming cam-side script, expose a second channel for the JPEG quality:
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)
The capture loop reads from config.quality whenever it
compresses a frame:
while True:
img = csi0.snapshot()
latest_jpeg = bytes(
img.compress(quality=config.quality).bytearray()
)
ch.send_event(0x01)
The host now has a knob. Set it to 50 and the next frame is smaller (and uglier); set it to 95 and the next frame is bigger (and sharper). The cam keeps capturing without restarting; the host doesn’t have to push a new script.
15.9.2. The write call from the host¶
On the host side, channel_write()
sends bytes to a named channel:
cam.channel_write('config', b'50')
The host SDK encodes the bytes as a single (or fragmented)
CHANNEL_WRITE packet, the protocol layer delivers it to the
cam, the cam’s write(offset=0, data=...) runs, and the cam’s
side acknowledges. By the time the call returns the cam has
received and accepted the new value.
The write is atomic from the cam’s point of view – the protocol
library guarantees the backend’s write runs to completion before
any other operation on that channel proceeds. Application code can
read config.quality from inside the capture loop without
worrying about the host stomping mid-snapshot.
15.9.3. Stub size and read on a write-only channel¶
A pure write channel still needs size and read defined,
even if they’re stubs returning 0 and b''. The library uses
the presence of methods to derive the channel’s capability flags;
a backend that’s missing read won’t get CHANNEL_FLAG_READ
set and the host will refuse a read attempt.
The bytes returned from read on a write-only channel are
useful for a different purpose, though: echoing back the current
value so a host that just attached can ask the cam “what’s the
current setting?” rather than starting from a default. To make
that work both directions have to agree on a serialisation. The
raw-bytes int(bytes(data)) parse in the earlier example works
for a single integer field but won’t scale once there’s a second
knob to set. Switching write to parse JSON and pairing it with
a read that returns the matching JSON dump turns the channel
into a true round-trip configuration store:
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)
The host now writes cam.channel_write('config',
b'{"quality": 50}') to set a value and cam.channel_read('config')
to read the current state back. The cam serialises a fresh JSON
dump on every read so the host always sees the latest values, and
adding another knob (threshold, exposure, orientation)
is one line in the JSON dict on each side.
15.9.4. A complete loop¶
With a frame channel for cam → host data, a config channel for host → cam control, and a small amount of glue, the application is an interactive tool:
The host opens the cam, starts pulling frames, and displays them in a window.
When the operator drags a slider, the host writes the new value on
config.The cam’s capture loop picks the value up on the next frame.
The new frames flow through the same
framechannel.
That’s the entire model. Two channels, two callbacks each, a capture loop on the cam, a read-and-write loop on the host. No framing logic visible, no error handling visible – the protocol library makes the reliable byte movement disappear.
Everything past this point is application code. Adding a third
channel for a histogram, a fourth for telemetry, or a fifth for
sensor triggers is the same backend-class-and-protocol.register
recipe, repeated. Once a cam project reaches this point the
protocol stops being the interesting problem; the application’s
own logic does.