12.6. Canali con nome

L’ID del canale nell’header di ogni pacchetto consente fino a 32 flussi indipendenti di condividere lo stesso trasporto fisico. Il livello dei canali trasforma quegli ID numerici in endpoint con nome, visibili all’applicazione, a cui il codice host può fare riferimento tramite stringa.

Un cavo di trasporto sulla sinistra che si dirama in quattro canali etichettati sul lato cam -- stdin, stdout, stream e un canale di stato registrato dall'utente -- ciascuno mostrato come un riquadro indipendente.

12.6.1. I quattro canali integrati

La cam registra quattro canali all’avvio, prima che venga eseguito qualsiasi codice applicativo:

  • stdin – i byte dello script che l’host invia alla cam per l’esecuzione. L’IDE usa questo canale per inviare lo script in corso di modifica; exec() sull’SDK host è la chiamata equivalente da un programma Python.

  • stdout – i byte dalle chiamate print() della cam e dai traceback delle eccezioni non gestite. La console seriale dell’IDE legge questo canale.

  • stream – il canale di anteprima live. L’IDE ne preleva i frame JPEG; qualsiasi script host può fare lo stesso con read_frame().

  • profile – gli eventi del profiler, presenti solo quando la cam è stata compilata con il profiling abilitato. La maggior parte delle build di rilascio lo omette.

Il codice applicativo raramente ha bisogno di toccare uno dei canali integrati; il lavoro interessante avviene sui canali che l’applicazione registra autonomamente.

12.6.2. Registrazione di un canale

Uno script lato cam registra un nuovo canale chiamando protocol.register() con un nome e un oggetto backend 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())

I metodi dell’oggetto backend decidono cosa può fare il canale. Un backend con solo size e read è un canale dati di sola lettura; aggiungi write e diventa bidirezionale; aggiungi poll e l’host può chiedere se sono pronti nuovi dati prima di pagare il costo di una lettura. Campionare i dati all’interno di size è il pattern più semplice quando il payload è abbastanza piccolo da entrare in un singolo frammento: il buffer viene generato su richiesta, mai memorizzato in cache, mai soggetto a race condition. Payload più grandi – frame di immagini, tracce di sensori – richiedono un pattern di latching che mantiene il buffer finché l’host non termina la sua lettura multi-frammento, trattato con il canale frame.

Una piccola quantità di gestione amministrativa avviene automaticamente:

  • La libreria assegna il successivo ID di canale libero (tra 0 e 31).

  • I flag di funzionalità sono derivati dai metodi presenti: CHANNEL_FLAG_READ se read è definito, CHANNEL_FLAG_WRITE se write è definito, CHANNEL_FLAG_LOCK se lock / unlock sono definiti.

  • Un pacchetto di evento CHANNEL_REGISTERED viene inviato a qualsiasi host connesso affinché il suo elenco di canali si aggiorni.

Il valore di ritorno è un handle protocol.ProtocolChannel che l’applicazione può conservare. Il metodo send_event() dell’handle è l’aggancio lato cam per dire all’host «è successo qualcosa su questo canale senza modificare i dati leggibili»: un trigger scattato, un pulsante premuto, il raggiungimento di un traguardo nel conteggio dei campioni.

12.6.3. Lettura dei canali dall’host

L’SDK host viene distribuito come pacchetto openmv su PyPI (pip install openmv), basato su pyserial per il trasporto. La sua classe openmv.camera.Camera espone i canali con nome della cam tramite metodi di alto livello:

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)

Avvertimento

Il pacchetto openmv richiede CPython 3.12 o più recente. Gli interpreti precedenti non dispongono delle funzionalità da cui dipende l’SDK; installa una build 3.12+ prima di pip install openmv.

Alcune cose da notare sulla configurazione:

  • La stringa della porta seriale – qui /dev/ttyACM0 – è in stile COM3 su Windows, /dev/cu.usbmodemXXXX su macOS e /dev/ttyACM* su Linux. Il numero effettivo dipende dalla porta con cui la cam è stata enumerata.

  • Il baud rate è il valore magico del protocollo 921600, che lo stack USB-CDC della cam riconosce come «questo client parla il protocollo, non il REPL.» Qualsiasi altra velocità ricade su una normale linea seriale.

  • Il context manager with Camera(...) as cam: apre il trasporto, esegue PROTO_SYNC, scambia le capacità e all’uscita chiude la porta in modo pulito. La chiamata esplicita update_channels() dopo l’ingresso aggiorna l’elenco locale dei canali con eventuali canali registrati dall’applicazione dopo l’avvio.

channel_size() e channel_read() sono i metodi di lavoro principali; channel_write() trasferisce un buffer avanti e indietro verso la cam se il backend ha un metodo write; has_channel() è il modo sicuro per verificare che un nome sia registrato prima di usarlo. Il nome del canale viene cercato una volta per ottenere l’ID di canale che la cam ha assegnato durante register e usato in ogni pacchetto da quel momento in poi.

Ogni coppia channel_size() / channel_read() costa due round-trip: un pacchetto per chiedere la dimensione, uno per chiedere i byte. Su USB-CDC entrambi si completano in circa un millisecondo combinati; su UART lo stesso scambio richiede più tempo in proporzione al baud rate della linea seriale. Il codice applicativo che legge in un ciclo serrato dovrebbe chiamare channel_size() solo quando la dimensione può effettivamente cambiare: per dati di dimensione fissa, la dimensione della prima chiamata può essere memorizzata in cache.

12.6.4. Indipendenza tra i canali

Vale la pena conoscere tre aspetti su come interagiscono i canali:

  • Controllo di flusso indipendente. Ogni canale ha il proprio stato di lettura pendente, i propri dati e i propri callback size / read / write. Una lettura di lunga durata sul canale stream non blocca le letture sul canale config dell’applicazione.

  • Sequenziale per canale. All’interno di un singolo canale, i pacchetti vengono consegnati in ordine. Il livello di affidabilità lo garantisce anche quando sono coinvolte ritrasmissioni.

  • Trasporto condiviso, budget di ritrasmissione condiviso. Tutti i canali condividono l’unico collegamento fisico, quindi un’ondata di traffico su un canale rallenta gli altri monopolizzando il cavo. Il meccanismo CHANNEL_LOCK consente a un canale di riservare il cavo per una lettura atomica multi-pacchetto; il backend vi aderisce implementando i callback lock / unlock.

Un canale è la superficie minima su cui un programma host e un programma cam concordano di cooperare. Il nome, la direzionalità (lettura o scrittura o entrambe), i metodi di callback lato cam e le corrispondenti chiamate di metodo lato host costituiscono l’intero contratto.