12.9. Flusso bidirezionale

I canali non sono unidirezionali. Un backend che implementa write consente all’host di inviare byte verso la cam, e la cam reagisce. È questo lo schema alla base di ogni strumento interattivo reale: l’operatore ruota una manopola nella GUI host, l’host scrive il nuovo valore su un canale config, la cam lo legge la volta successiva in cui acquisisce.

12.9.1. Un canale config

Aggiungendo allo script lato cam per lo streaming, esponi un secondo canale per la qualità 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)

Il ciclo di acquisizione legge da config.quality ogni volta che comprime un frame:

while True:
    img = csi0.snapshot()
    latest_jpeg = bytes(
        img.compress(quality=config.quality).bytearray()
    )
    ch.send_event(0x01)

Ora l’host ha una manopola. Impostala a 50 e il frame successivo sarà più piccolo (e più brutto); impostala a 95 e il frame successivo sarà più grande (e più nitido). La cam continua ad acquisire senza riavviarsi; l’host non deve inviare un nuovo script.

12.9.2. La chiamata di scrittura dall’host

Sul lato host, channel_write() invia byte a un canale con nome:

cam.channel_write('config', b'50')

L’SDK host codifica i byte come un singolo pacchetto CHANNEL_WRITE (o frammentato), il livello di protocollo lo consegna alla cam, viene eseguito il metodo write(offset=0, data=...) della cam, e il lato cam invia il riscontro. Nel momento in cui la chiamata ritorna, la cam ha ricevuto e accettato il nuovo valore.

La scrittura è atomica dal punto di vista della cam – la libreria del protocollo garantisce che il metodo write del backend venga eseguito fino al completamento prima che qualsiasi altra operazione su quel canale proceda. Il codice applicativo può leggere config.quality dall’interno del ciclo di acquisizione senza preoccuparsi che l’host intervenga a metà di uno snapshot.

12.9.3. Stub di size e read su un canale di sola scrittura

Un canale di sola scrittura necessita comunque della definizione di size e read, anche se sono stub che restituiscono 0 e b''. La libreria utilizza la presenza dei metodi per ricavare i flag di capacità del canale; un backend privo di read non otterrà l’impostazione di CHANNEL_FLAG_READ e l’host rifiuterà un tentativo di lettura.

I byte restituiti da read su un canale di sola scrittura sono però utili a uno scopo diverso: restituire il valore corrente in modo che un host appena collegato possa chiedere alla cam «qual è l’impostazione attuale?» anziché partire da un valore predefinito. Per farlo funzionare, entrambe le direzioni devono concordare su una serializzazione. L’analisi dei byte grezzi int(bytes(data)) dell’esempio precedente funziona per un singolo campo intero, ma non scalerà non appena ci sarà una seconda manopola da impostare. Modificare write affinché analizzi JSON e abbinarlo a un read che restituisce il corrispondente dump JSON trasforma il canale in un vero archivio di configurazione a doppio senso:

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)

Ora l’host scrive cam.channel_write('config', b'{"quality": 50}') per impostare un valore e cam.channel_read('config') per leggere indietro lo stato corrente. La cam serializza un nuovo dump JSON a ogni lettura, così l’host vede sempre i valori più recenti, e aggiungere un’altra manopola (threshold, exposure, orientation) è una sola riga nel dizionario JSON su ciascun lato.

12.9.4. Un ciclo completo

Con un canale frame per i dati cam → host, un canale config per il controllo host → cam e una piccola quantità di collante, l’applicazione è uno strumento interattivo:

  • L’host apre la cam, inizia a estrarre frame e li visualizza in una finestra.

  • Quando l’operatore trascina un cursore, l’host scrive il nuovo valore su config.

  • Il ciclo di acquisizione della cam recepisce il valore al frame successivo.

  • I nuovi frame fluiscono attraverso lo stesso canale frame.

Questo è l’intero modello. Due canali, due callback ciascuno, un ciclo di acquisizione sulla cam, un ciclo di lettura e scrittura sull’host. Nessuna logica di framing visibile, nessuna gestione degli errori visibile – la libreria del protocollo fa scomparire lo spostamento affidabile dei byte.

Tutto ciò che segue è codice applicativo. Aggiungere un terzo canale per un istogramma, un quarto per la telemetria o un quinto per i trigger dei sensori è la stessa ricetta a base di classe backend e protocol.register, ripetuta. Una volta che un progetto con cam raggiunge questo punto, il protocollo smette di essere il problema interessante; lo diventa la logica propria dell’applicazione.