12.9. Przepływ dwukierunkowy

Kanały nie są jednokierunkowe. Backend implementujący write pozwala hostowi wysyłać bajty do kamery, a kamera reaguje. To wzorzec stojący za każdym prawdziwym narzędziem interaktywnym: operator przekręca pokrętło w interfejsie graficznym hosta, host zapisuje nową wartość do kanału konfiguracji, a kamera odczytuje ją przy następnym przechwyceniu.

12.9.1. Kanał konfiguracji

Dodając do skryptu strumieniującego po stronie kamery, udostępnij drugi kanał dla jakości 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)

Pętla przechwytywania odczytuje z config.quality za każdym razem, gdy kompresuje ramkę:

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

Host ma teraz pokrętło. Ustaw je na 50, a następna ramka będzie mniejsza (i brzydsza); ustaw je na 95, a następna ramka będzie większa (i ostrzejsza). Kamera nadal przechwytuje bez ponownego uruchamiania; host nie musi przesyłać nowego skryptu.

12.9.2. Wywołanie zapisu z hosta

Po stronie hosta channel_write() wysyła bajty do nazwanego kanału:

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

SDK hosta koduje bajty jako pojedynczy (lub pofragmentowany) pakiet CHANNEL_WRITE, warstwa protokołu dostarcza go do kamery, na kamerze uruchamia się write(offset=0, data=...), a strona kamery potwierdza odbiór. Zanim wywołanie powróci, kamera odebrała i zaakceptowała nową wartość.

Zapis jest atomowy z punktu widzenia kamery – biblioteka protokołu gwarantuje, że write backendu wykona się do końca, zanim rozpocznie się jakakolwiek inna operacja na tym kanale. Kod aplikacji może odczytywać config.quality z wnętrza pętli przechwytywania, nie martwiąc się, że host nadpisze ją w trakcie wykonywania zrzutu obrazu.

12.9.3. Zaślepka rozmiaru i odczyt na kanale tylko do zapisu

Czysty kanał zapisu nadal potrzebuje zdefiniowanych size i read, nawet jeśli są to zaślepki zwracające 0 i b''. Biblioteka używa obecności metod do wyprowadzenia flag możliwości kanału; backend, w którym brakuje read, nie otrzyma ustawionej flagi CHANNEL_FLAG_READ, a host odmówi próby odczytu.

Bajty zwracane z read na kanale tylko do zapisu są jednak przydatne do innego celu: odsyłania bieżącej wartości, aby host, który właśnie się podłączył, mógł zapytać kamerę „jakie jest bieżące ustawienie?”, zamiast zaczynać od wartości domyślnej. Aby to działało, obie strony muszą uzgodnić serializację. Parsowanie surowych bajtów int(bytes(data)) z wcześniejszego przykładu działa dla pojedynczego pola całkowitego, ale nie będzie się skalować, gdy pojawi się drugie pokrętło do ustawienia. Przełączenie write na parsowanie JSON i sparowanie go z read zwracającym odpowiadający zrzut JSON zmienia kanał w prawdziwą dwukierunkową pamięć konfiguracji:

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)

Host zapisuje teraz cam.channel_write('config', b'{"quality": 50}'), aby ustawić wartość, oraz cam.channel_read('config'), aby odczytać bieżący stan. Kamera serializuje świeży zrzut JSON przy każdym odczycie, więc host zawsze widzi najnowsze wartości, a dodanie kolejnego pokrętła (threshold, exposure, orientation) to jedna linia w słowniku JSON po każdej stronie.

12.9.4. Kompletna pętla

Mając kanał ramki dla danych kamera → host, kanał konfiguracji dla sterowania host → kamera oraz odrobinę kodu spajającego, aplikacja staje się narzędziem interaktywnym:

  • Host otwiera kamerę, zaczyna pobierać ramki i wyświetla je w oknie.

  • Gdy operator przeciągnie suwak, host zapisuje nową wartość na config.

  • Pętla przechwytywania kamery podejmuje wartość przy następnej ramce.

  • Nowe ramki przepływają przez ten sam kanał frame.

To cały model. Dwa kanały, po dwa wywołania zwrotne każdy, pętla przechwytywania na kamerze, pętla odczytu i zapisu na hoście. Żadnej widocznej logiki ramkowania, żadnej widocznej obsługi błędów – biblioteka protokołu sprawia, że niezawodne przenoszenie bajtów znika.

Wszystko poza tym punktem to kod aplikacji. Dodanie trzeciego kanału dla histogramu, czwartego dla telemetrii lub piątego dla wyzwalaczy sensora to ten sam przepis z klasą backendu i protocol.register, powtórzony. Gdy projekt kamery osiągnie ten punkt, protokół przestaje być interesującym problemem; staje się nim własna logika aplikacji.