13.3.1.4. Canales personalizados

Un canal es un flujo de bytes bidireccional con nombre entre un script del lado de la cámara y el anfitrión. La cámara registra un canal y proporciona funciones de retorno (callbacks) que producen o consumen datos; el anfitrión lee de ese canal y escribe en él por nombre. El mismo mecanismo que el paquete usa internamente para el canal stream que transporta los fotogramas, el canal stdout que transporta la salida del script y el canal stdin que transporta la subida del script se expone a los scripts de usuario, de modo que cualquier dato específico de la aplicación que el anfitrión necesite puede viajar por la misma conexión USB sin inventar un segundo protocolo.

Esta es la característica más útil del paquete y la que la documentación estándar cubre peor, así que esta página la desarrolla de extremo a extremo.

13.3.1.4.1. Las dos mitades

Un canal personalizado necesita código cooperante en ambos lados. El script del lado de la cámara importa protocol, define una clase con tres métodos (size(), read(), poll()) más un write() opcional, y llama a protocol.register(name=..., backend=...) para publicar el canal bajo un nombre elegido:

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

El método size() devuelve cuántos bytes tiene disponibles actualmente el canal. read() es el productor: dado un offset y un size solicitados por el anfitrión, devuelve los bytes (o una cadena que la capa de protocolo codifica). poll() devuelve True cuando hay algo que leer; la capa de protocolo usa esto para marcar el canal como listo en read_status().

El programa del lado del anfitrión usa cuatro métodos de openmv.Camera: has_channel() para comprobar que el canal existe, channel_size() para preguntar cuántos datos están esperando, channel_read() para extraer bytes, y channel_write() para introducir bytes. read_status() sondea todos los canales a la vez:

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

El bucle del anfitrión sondea read_status(); cuando el canal ticks está listo, llama a channel_read() sin size para extraer todo lo que esté disponible. El método TicksChannel.poll() de la cámara devuelve True en cada comprobación, de modo que el canal siempre está «listo» y el anfitrión obtiene un valor de tick fresco en cada sondeo.

13.3.1.4.2. Un canal bidireccional

Para un anfitrión que necesita devolver datos, la clase del lado de la cámara añade un método write() que acepta los bytes entrantes:

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

El anfitrión escribe en el canal con channel_write() y lee la respuesta a través del patrón habitual de 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. Qué aporta esto a la aplicación

Los canales personalizados son la herramienta adecuada siempre que una aplicación quiera aprovechar la conexión USB existente para datos que no son fotogramas ni impresiones: contadores de telemetría, perillas de configuración transmitidas en vivo desde una interfaz en el anfitrión, comandos de control enviados en sentido contrario, resultados de una medición que la cámara calculó y que no encajan en el marco de «imagen» que asume el canal stream. La capa de protocolo se encarga del enmarcado, la fragmentación, el acuse de recibo y el reintento; el script solo necesita implementar el backend de cuatro métodos, y el anfitrión solo necesita conocer el nombre del canal y la forma de los datos.

El indicador --channel NAME de la CLI es una forma rápida de verificar un canal personalizado desde la terminal sin escribir un programa del lado del anfitrión: la CLI sondea el canal nombrado e imprime los primeros diez bytes de cada actualización.

El límite de tamaño en una sola llamada a channel_read() o channel_write() es el max_payload negociado por el protocolo: 4096 bytes por defecto. Los métodos del lado del anfitrión dividen automáticamente las escrituras más grandes en el número correcto de paquetes, de modo que la aplicación puede pasar búferes arbitrariamente grandes; la fragmentación es invisible.