12.6. Canales con nombre

El ID de canal en la cabecera de cada paquete permite que hasta 32 flujos independientes compartan el mismo transporte físico. La capa de canal convierte esos ID numéricos en puntos finales con nombre, visibles para la aplicación, a los que el código del host puede referirse mediante una cadena.

Un único cable de transporte a la izquierda que se ramifica en cuatro canales etiquetados en el lado de la cámara -- stdin, stdout, stream, y un canal de estado registrado por el usuario -- cada uno mostrándose como una caja independiente.

12.6.1. Los cuatro canales integrados

La cámara registra cuatro canales al arrancar, antes de que se ejecute cualquier código de aplicación:

  • stdin – bytes de script que el host envía a la cámara para ejecutar. El IDE usa este canal para enviar el script que se está editando; exec() en el SDK del host es la llamada equivalente desde un programa de Python.

  • stdout – bytes de las llamadas print() de la cámara y de los rastreos de excepciones no capturadas. La consola serie del IDE lee este canal.

  • stream – el canal de vista previa en vivo. El IDE extrae fotogramas JPEG de él; cualquier script del host puede hacer lo mismo con read_frame().

  • profile – eventos del generador de perfiles, presente solo cuando la cámara se compiló con la generación de perfiles habilitada. La mayoría de las compilaciones de lanzamiento lo omiten.

El código de aplicación rara vez necesita tocar alguno de los canales integrados; el trabajo interesante ocurre en los canales que la aplicación registra por sí misma.

12.6.2. Registrar un canal

Un script del lado de la cámara registra un nuevo canal llamando a protocol.register() con un nombre y un objeto backend de Python:

import json
import protocol
import time

trigger_count = 0

class StatusChannel:
    def size(self):
        # Refresh the snapshot on every host query.
        self._buf = json.dumps({
            'uptime_s': time.ticks_ms() // 1000,
            'triggers': trigger_count,
        }).encode()
        return len(self._buf)

    def read(self, offset, size):
        return self._buf[offset:offset + size]

protocol.register(name='status', backend=StatusChannel())

Los métodos del objeto backend deciden lo que puede hacer el canal. Un backend con solo size y read es un canal de datos de solo lectura; añade write y se vuelve bidireccional; añade poll y el host puede preguntar si hay datos nuevos listos antes de pagar por una lectura. Muestrear los datos dentro de size es el patrón más simple cuando la carga útil es lo suficientemente pequeña para caber en un fragmento: el búfer se genera bajo demanda, nunca se almacena en caché, nunca hay condiciones de carrera. Las cargas útiles más grandes – fotogramas de imagen, trazas de sensores – necesitan un patrón de retención que mantenga el búfer hasta que el host termine su lectura de varios fragmentos, lo cual se trata con el canal de fotogramas.

Una pequeña cantidad de contabilidad ocurre automáticamente:

  • La biblioteca asigna el siguiente ID de canal libre (entre 0 y 31).

  • Los indicadores de capacidad se derivan de los métodos presentes: CHANNEL_FLAG_READ si se define read, CHANNEL_FLAG_WRITE si se define write, CHANNEL_FLAG_LOCK si se definen lock / unlock.

  • Se envía un paquete de evento CHANNEL_REGISTERED a cualquier host conectado para que su lista de canales se actualice.

El valor de retorno es un manejador protocol.ProtocolChannel que la aplicación puede conservar. El método send_event() del manejador es el punto de enganche del lado de la cámara para decirle al host «ocurrió algo en este canal sin que cambien los datos legibles»: se disparó un activador, se presionó un botón, se alcanzó un hito de recuento de muestras.

12.6.3. Leer canales desde el host

El SDK del host se distribuye como el paquete openmv en PyPI (pip install openmv), construido sobre pyserial para el transporte. Su clase openmv.camera.Camera expone los canales con nombre de la cámara a través de métodos de alto nivel:

from openmv.camera import Camera

with Camera('/dev/ttyACM0', baudrate=921600) as cam:
    cam.update_channels()
    if cam.has_channel('status'):
        size = cam.channel_size('status')
        data = cam.channel_read('status', size)

Advertencia

El paquete openmv requiere CPython 3.12 o más reciente. Los intérpretes anteriores carecen de características de las que depende el SDK; instala una compilación 3.12+ antes de pip install openmv.

Algunas cosas que conviene observar sobre la configuración:

  • La cadena del puerto serie – aquí /dev/ttyACM0 – tiene el estilo COM3 en Windows, /dev/cu.usbmodemXXXX en macOS y /dev/ttyACM* en Linux. El número real depende del puerto con el que se enumeró la cámara.

  • La velocidad en baudios es el valor mágico del protocolo 921600, que la pila USB-CDC de la cámara reconoce como «este cliente habla el protocolo, no el REPL». Cualquier otra velocidad recurre a una línea serie simple.

  • El administrador de contexto with Camera(...) as cam: abre el transporte, ejecuta PROTO_SYNC, intercambia capacidades y, al salir, cierra el puerto de forma limpia. La llamada explícita a update_channels() tras la entrada actualiza la lista local de canales con cualquier canal que la aplicación haya registrado después del arranque.

channel_size() y channel_read() son los métodos de trabajo principales; channel_write() envía un búfer de ida y vuelta a la cámara si el backend tiene un método write; has_channel() es la forma segura de comprobar que un nombre está registrado antes de usarlo. El nombre del canal se busca una vez para obtener el ID de canal que la cámara asignó durante register y se usa en cada paquete a partir de entonces.

Cada par channel_size() / channel_read() cuesta dos viajes de ida y vuelta: un paquete para pedir el tamaño, otro para pedir los bytes. Sobre USB-CDC ambos terminan en aproximadamente un milisegundo combinados; sobre UART el mismo intercambio tarda más en proporción a la velocidad en baudios de la línea serie. El código de aplicación que lee en un bucle cerrado debería llamar a channel_size() solo cuando el tamaño pueda cambiar realmente: para datos de tamaño fijo, el tamaño de la primera llamada puede almacenarse en caché.

12.6.4. Independencia entre canales

Hay tres cosas que vale la pena saber sobre cómo interactúan los canales:

  • Control de flujo independiente. Cada canal tiene su propio estado de lectura pendiente, sus propios datos y sus propias funciones de retorno size / read / write. Una lectura de larga duración en el canal stream no bloquea las lecturas en el canal config de la aplicación.

  • Secuencial por canal. Dentro de un solo canal, los paquetes se entregan en orden. La capa de fiabilidad lo garantiza incluso cuando hay retransmisiones de por medio.

  • Transporte compartido, presupuesto de retransmisión compartido. Todos los canales comparten el único enlace físico, por lo que un torrente de tráfico en un canal ralentiza a los demás al acaparar el cable. El mecanismo CHANNEL_LOCK permite que un canal reserve el cable para una lectura atómica de varios paquetes; el backend opta por ello implementando las funciones de retorno lock / unlock.

Un canal es la mínima superficie de contacto sobre la que un programa del host y un programa de la cámara acuerdan cooperar. El nombre, la direccionalidad (lectura o escritura o ambas), los métodos de retorno del lado de la cámara y las llamadas a métodos correspondientes del lado del host son todo el contrato.