15.3.1.4. Custom channels

A channel is a named, bidirectional byte stream between a cam-side script and the host. The cam registers a channel and provides callbacks that produce or consume data; the host reads from and writes to that channel by name. The same mechanism the package uses internally for the stream channel that carries frames, the stdout channel that carries script output, and the stdin channel that carries script upload is exposed to user scripts, so any application-specific data the host needs can ride the same USB connection without inventing a second protocol.

This is the most useful feature of the package and the one the standard documentation covers least well, so this page works through it end to end.

15.3.1.4.1. The two halves

A custom channel needs cooperating code on both sides. The cam-side script imports protocol, defines a class with three methods (size(), read(), poll()) plus an optional write(), and calls protocol.register(name=..., backend=...) to publish the channel under a chosen name:

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())

The size() method returns how many bytes the channel currently has available. read() is the producer: given an offset and size requested by the host, it returns the bytes (or a string the protocol layer encodes). poll() returns True when there is something to read – the protocol layer uses this to flag the channel as ready in read_status().

The host-side program uses four openmv.Camera methods: has_channel() to check the channel exists, channel_size() to ask how much data is waiting, channel_read() to pull bytes out, and channel_write() to push bytes in. read_status() polls every channel at once:

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()}")

The host loop polls read_status(); when the ticks channel is ready it calls channel_read() with no size to pull whatever is available. The cam’s TicksChannel.poll() returns True on every check, so the channel is always “ready” and the host gets a fresh tick value every poll.

15.3.1.4.2. A bidirectional channel

For a host that needs to push data back, the cam-side class adds a write() method that accepts the incoming bytes:

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())

The host writes to the channel with channel_write() and reads the reply back through the usual read_status() / channel_read() pattern:

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

15.3.1.4.3. What this gets the application

Custom channels are the right tool whenever an application wants to ride the existing USB connection for non-frame, non-print data: telemetry counters, configuration knobs streamed live from a UI on the host, control commands sent the other way, results of a measurement the cam computed that does not fit the “image” framing the stream channel assumes. The protocol layer handles the framing, fragmentation, acknowledgement, and retry; the script only needs to implement the four-method backend, and the host only needs to know the channel name and the data shape.

The CLI’s --channel NAME flag is a quick way to verify a custom channel from the terminal without writing a host-side program: the CLI polls the named channel and prints the first ten bytes of each update.

The size limit on a single channel_read() or channel_write() call is the protocol’s negotiated max_payload – 4096 bytes by default. The host-side methods automatically split larger writes into the right number of packets, so the application can pass arbitrarily large buffers; the fragmentation is invisible.