12.9. Flux bidirecțional

Canalele nu sunt unidirecționale. Un backend care implementează write permite gazdei să trimită octeți către cameră, iar camera reacționează. Acesta este tiparul din spatele fiecărui instrument interactiv real: operatorul rotește un buton pe interfața grafică a gazdei, gazda scrie noua valoare într-un canal de configurare, iar camera o citește data viitoare când captează.

12.9.1. Un canal de configurare

Adăugând la scriptul de transmitere în flux de pe partea camerei, expune un al doilea canal pentru calitatea 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)

Bucla de captare citește din config.quality ori de câte ori comprimă un cadru:

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

Gazda are acum un buton de reglare. Setează-l la 50 și următorul cadru este mai mic (și mai urât); setează-l la 95 și următorul cadru este mai mare (și mai clar). Camera continuă să capteze fără repornire; gazda nu trebuie să trimită un script nou.

12.9.2. Apelul de scriere de la gazdă

Pe partea gazdei, channel_write() trimite octeți către un canal denumit:

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

SDK-ul gazdei codifică octeții ca un singur pachet CHANNEL_WRITE (sau fragmentat), stratul de protocol îl livrează camerei, funcția write(offset=0, data=...) a camerei rulează, iar partea camerei confirmă. Până când apelul revine, camera a primit și a acceptat noua valoare.

Scrierea este atomică din punctul de vedere al camerei – biblioteca protocolului garantează că funcția write a backendului rulează până la finalizare înainte ca orice altă operațiune pe acel canal să continue. Codul de aplicație poate citi config.quality din interiorul buclei de captare fără a se îngrijora că gazda va interveni în mijlocul unui instantaneu.

12.9.3. Dimensiunea-substituent și citirea pe un canal exclusiv de scriere

Un canal exclusiv de scriere are nevoie totuși de size și read definite, chiar dacă sunt substituenți care returnează 0 și b''. Biblioteca folosește prezența metodelor pentru a deriva indicatorii de capabilitate ai canalului; un backend căruia îi lipsește read nu va avea setat CHANNEL_FLAG_READ, iar gazda va refuza o tentativă de citire.

Octeții returnați de read pe un canal exclusiv de scriere sunt totuși utili într-un alt scop: returnarea valorii curente prin ecou, astfel încât o gazdă care tocmai s-a atașat să poată întreba camera „care este setarea curentă?” în loc să pornească de la o valoare implicită. Pentru ca acest lucru să funcționeze, ambele direcții trebuie să convină asupra unei serializări. Analiza octeților bruți int(bytes(data)) din exemplul anterior funcționează pentru un singur câmp întreg, dar nu se va scala odată ce există un al doilea buton de reglat. Trecerea funcției write la analiza JSON și asocierea ei cu o funcție read care returnează exportul JSON corespunzător transformă canalul într-un veritabil depozit de configurare cu dus-întors:

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)

Gazda scrie acum cam.channel_write('config', b'{"quality": 50}') pentru a seta o valoare și cam.channel_read('config') pentru a citi înapoi starea curentă. Camera serializează un export JSON proaspăt la fiecare citire, astfel încât gazda să vadă întotdeauna cele mai recente valori, iar adăugarea unui alt buton (threshold, exposure, orientation) reprezintă o singură linie în dicționarul JSON de pe fiecare parte.

12.9.4. O buclă completă

Cu un canal de cadre pentru datele cameră → gazdă, un canal de configurare pentru controlul gazdă → cameră și o cantitate mică de cod de legătură, aplicația este un instrument interactiv:

  • Gazda deschide camera, începe să extragă cadre și le afișează într-o fereastră.

  • Când operatorul trage un cursor glisant, gazda scrie noua valoare pe config.

  • Bucla de captare a camerei preia valoarea la următorul cadru.

  • Noile cadre curg prin același canal frame.

Acesta este întregul model. Două canale, câte două funcții de retroapelare (callback) fiecare, o buclă de captare pe cameră și o buclă de citire-și-scriere pe gazdă. Nicio logică de încadrare vizibilă, nicio gestionare a erorilor vizibilă – biblioteca protocolului face ca mișcarea fiabilă a octeților să dispară.

Tot ce urmează dincolo de acest punct este cod de aplicație. Adăugarea unui al treilea canal pentru o histogramă, a unui al patrulea pentru telemetrie sau a unui al cincilea pentru declanșatoarele de senzori este aceeași rețetă de clasă-backend-și-protocol.register, repetată. Odată ce un proiect de cameră ajunge la acest punct, protocolul încetează să mai fie problema interesantă; logica proprie a aplicației o devine.