12.9. Flujo bidireccional¶
Los canales no son unidireccionales. Un backend que implementa write permite que el host envíe bytes a la cámara, y la cámara reacciona. Ese es el patrón que hay detrás de toda herramienta interactiva real: el operador gira un mando en la GUI del host, el host escribe el nuevo valor en un canal de configuración y la cámara lo lee la próxima vez que captura.
12.9.1. Un canal de configuración¶
Ampliando el script del lado de la cámara para streaming, expón un segundo canal para la calidad 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)
El bucle de captura lee de config.quality cada vez que comprime un fotograma:
while True:
img = csi0.snapshot()
latest_jpeg = bytes(
img.compress(quality=config.quality).bytearray()
)
ch.send_event(0x01)
Ahora el host tiene un mando. Ponlo a 50 y el siguiente fotograma será más pequeño (y más feo); ponlo a 95 y el siguiente fotograma será más grande (y más nítido). La cámara sigue capturando sin reiniciarse; el host no tiene que enviar un nuevo script.
12.9.2. La llamada de escritura desde el host¶
En el lado del host, channel_write() envía bytes a un canal con nombre:
cam.channel_write('config', b'50')
El SDK del host codifica los bytes como un único paquete CHANNEL_WRITE (o fragmentado), la capa de protocolo lo entrega a la cámara, se ejecuta el write(offset=0, data=...) de la cámara y el lado de la cámara confirma la recepción. Para cuando la llamada retorna, la cámara ha recibido y aceptado el nuevo valor.
La escritura es atómica desde el punto de vista de la cámara: la biblioteca del protocolo garantiza que el write del backend se ejecute por completo antes de que prosiga cualquier otra operación sobre ese canal. El código de aplicación puede leer config.quality desde dentro del bucle de captura sin preocuparse de que el host la pise a mitad de una captura.
12.9.3. Tamaño ficticio (stub) y lectura en un canal de solo escritura¶
Un canal de escritura pura aún necesita tener definidos size y read, aunque sean stubs que devuelvan 0 y b''. La biblioteca utiliza la presencia de los métodos para derivar las banderas de capacidad del canal; un backend al que le falte read no tendrá activada la bandera CHANNEL_FLAG_READ y el host rechazará un intento de lectura.
Sin embargo, los bytes devueltos por read en un canal de solo escritura son útiles para otro propósito: devolver como eco el valor actual para que un host que acaba de conectarse pueda preguntar a la cámara «¿cuál es la configuración actual?» en lugar de partir de un valor por defecto. Para que eso funcione, ambas direcciones tienen que ponerse de acuerdo en una serialización. El análisis de bytes en bruto int(bytes(data)) del ejemplo anterior funciona para un único campo entero, pero no escala en cuanto hay un segundo mando que ajustar. Cambiar write para que analice JSON y emparejarlo con un read que devuelva el volcado JSON correspondiente convierte el canal en un auténtico almacén de configuración de ida y vuelta:
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)
Ahora el host escribe cam.channel_write('config', b'{"quality": 50}') para establecer un valor y cam.channel_read('config') para leer de vuelta el estado actual. La cámara serializa un volcado JSON nuevo en cada lectura, de modo que el host siempre ve los valores más recientes, y añadir otro mando (threshold, exposure, orientation) es una sola línea en el diccionario JSON de cada lado.
12.9.4. Un bucle completo¶
Con un canal de fotogramas para los datos de cámara → host, un canal de configuración para el control de host → cámara y una pequeña cantidad de código de pegamento, la aplicación es una herramienta interactiva:
El host abre la cámara, empieza a extraer fotogramas y los muestra en una ventana.
Cuando el operador arrastra un control deslizante, el host escribe el nuevo valor en
config.El bucle de captura de la cámara recoge el valor en el siguiente fotograma.
Los nuevos fotogramas fluyen a través del mismo canal
frame.
Ese es todo el modelo. Dos canales, dos funciones de retorno (callbacks) cada uno, un bucle de captura en la cámara y un bucle de lectura y escritura en el host. Sin lógica de tramado visible, sin manejo de errores visible: la biblioteca del protocolo hace desaparecer el movimiento fiable de bytes.
Todo lo que hay a partir de este punto es código de aplicación. Añadir un tercer canal para un histograma, un cuarto para telemetría o un quinto para disparadores de sensores es la misma receta de clase-de-backend-y-protocol.register, repetida. Una vez que un proyecto de cámara llega a este punto, el protocolo deja de ser el problema interesante; lo es la propia lógica de la aplicación.