12.9. Bidirektionaler Fluss¶
Kanäle sind nicht einseitig. Ein Backend, das write implementiert, lässt den Host Bytes zur Kamera schicken, und die Kamera reagiert. Das ist das Muster hinter jedem echten interaktiven Werkzeug: Der Bediener dreht an einem Regler in der Host-GUI, der Host schreibt den neuen Wert in einen Konfigurationskanal, und die Kamera liest ihn beim nächsten Erfassen.
12.9.1. Ein Konfigurationskanal¶
Erweitern Sie das kameraseitige Streaming-Skript, um einen zweiten Kanal für die JPEG-Qualität bereitzustellen:
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)
Die Erfassungsschleife liest aus config.quality, wann immer sie ein Einzelbild komprimiert:
while True:
img = csi0.snapshot()
latest_jpeg = bytes(
img.compress(quality=config.quality).bytearray()
)
ch.send_event(0x01)
Der Host hat nun einen Regler. Stellen Sie ihn auf 50, und das nächste Einzelbild ist kleiner (und hässlicher); stellen Sie ihn auf 95, und das nächste Einzelbild ist größer (und schärfer). Die Kamera erfasst weiter, ohne neu zu starten; der Host muss kein neues Skript übertragen.
12.9.2. Der Schreibaufruf vom Host¶
Auf der Hostseite sendet channel_write() Bytes an einen benannten Kanal:
cam.channel_write('config', b'50')
Das Host-SDK kodiert die Bytes als ein einzelnes (oder fragmentiertes) CHANNEL_WRITE-Paket, die Protokollschicht liefert es an die Kamera, die write(offset=0, data=...) der Kamera läuft, und die Kameraseite bestätigt. Wenn der Aufruf zurückkehrt, hat die Kamera den neuen Wert empfangen und akzeptiert.
Der Schreibvorgang ist aus Sicht der Kamera atomar – die Protokollbibliothek garantiert, dass das write des Backends vollständig abläuft, bevor ein anderer Vorgang auf diesem Kanal fortschreitet. Anwendungscode kann config.quality aus der Erfassungsschleife heraus lesen, ohne befürchten zu müssen, dass der Host mitten im Schnappschuss dazwischenfunkt.
12.9.3. Stub-Größe und Lesen auf einem reinen Schreibkanal¶
Ein reiner Schreibkanal benötigt trotzdem definierte size- und read-Methoden, selbst wenn es Stubs sind, die 0 und b'' zurückgeben. Die Bibliothek nutzt das Vorhandensein von Methoden, um die Fähigkeits-Flags des Kanals abzuleiten; ein Backend, dem read fehlt, erhält kein gesetztes CHANNEL_FLAG_READ, und der Host verweigert einen Leseversuch.
Die von read auf einem reinen Schreibkanal zurückgegebenen Bytes sind jedoch für einen anderen Zweck nützlich: das Zurückspiegeln des aktuellen Werts, damit ein gerade verbundener Host die Kamera fragen kann „was ist die aktuelle Einstellung?“, statt von einem Standardwert auszugehen. Damit das funktioniert, müssen sich beide Richtungen auf eine Serialisierung einigen. Das Roh-Byte-Parsen int(bytes(data)) im früheren Beispiel funktioniert für ein einzelnes Ganzzahlfeld, skaliert aber nicht, sobald es einen zweiten Regler einzustellen gibt. Wenn man write so umstellt, dass es JSON parst, und es mit einem read paart, das den passenden JSON-Dump zurückgibt, wird der Kanal zu einem echten Round-Trip-Konfigurationsspeicher:
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)
Der Host schreibt nun cam.channel_write('config', b'{"quality": 50}'), um einen Wert zu setzen, und cam.channel_read('config'), um den aktuellen Zustand zurückzulesen. Die Kamera serialisiert bei jedem Lesen einen frischen JSON-Dump, sodass der Host stets die aktuellsten Werte sieht, und das Hinzufügen eines weiteren Reglers (threshold, exposure, orientation) ist auf jeder Seite eine Zeile im JSON-Dict.
12.9.4. Eine vollständige Schleife¶
Mit einem Frame-Kanal für Daten von der Kamera zum Host, einem Konfigurationskanal für die Steuerung vom Host zur Kamera und ein wenig Klebecode ist die Anwendung ein interaktives Werkzeug:
Der Host öffnet die Kamera, beginnt Einzelbilder abzurufen und zeigt sie in einem Fenster an.
Wenn der Bediener einen Schieberegler zieht, schreibt der Host den neuen Wert auf
config.Die Erfassungsschleife der Kamera nimmt den Wert beim nächsten Einzelbild auf.
Die neuen Einzelbilder fließen durch denselben
frame-Kanal.
Das ist das gesamte Modell. Zwei Kanäle, je zwei Callbacks, eine Erfassungsschleife auf der Kamera, eine Lese-und-Schreib-Schleife auf dem Host. Keine sichtbare Framing-Logik, keine sichtbare Fehlerbehandlung – die Protokollbibliothek lässt die zuverlässige Byte-Bewegung verschwinden.
Alles ab diesem Punkt ist Anwendungscode. Das Hinzufügen eines dritten Kanals für ein Histogramm, eines vierten für Telemetrie oder eines fünften für Sensor-Trigger ist dasselbe Rezept aus Backend-Klasse und protocol.register, nur wiederholt. Sobald ein Kameraprojekt diesen Punkt erreicht, hört das Protokoll auf, das interessante Problem zu sein; die eigene Logik der Anwendung übernimmt.