12.6. Benannte Channels

Die Channel-ID im Header jedes Pakets ermöglicht es bis zu 32 unabhängigen Streams, denselben physischen Transport zu teilen. Die Channel-Ebene verwandelt diese numerischen IDs in benannte, für die Anwendung sichtbare Endpunkte, auf die der Host-Code per Zeichenkette verweisen kann.

Ein Transportkabel auf der linken Seite, das sich auf der Cam-Seite in vier beschriftete Channels auffächert -- stdin, stdout, stream und einen vom Benutzer registrierten Status-Channel -- jeder dargestellt als eigenständiger Kasten.

12.6.1. Die vier eingebauten Channels

Die Cam registriert beim Booten vier Channels, bevor irgendein Anwendungscode läuft:

  • stdin – Skript-Bytes, die der Host zur Ausführung an die Cam schiebt. Die IDE nutzt diesen Channel, um das gerade bearbeitete Skript zu senden; exec() im Host-SDK ist der entsprechende Aufruf aus einem Python-Programm.

  • stdout – Bytes von print()-Aufrufen der Cam und Tracebacks nicht abgefangener Ausnahmen. Die serielle Konsole der IDE liest diesen Channel.

  • stream – der Live-Vorschau-Channel. Die IDE zieht JPEG-Frames daraus; jedes Host-Skript kann dasselbe mit read_frame() tun.

  • profile – Profiler-Ereignisse, nur vorhanden, wenn die Cam mit aktiviertem Profiling gebaut wurde. Die meisten Release-Builds lassen ihn weg.

Anwendungscode muss selten einen der eingebauten Channels anfassen; die interessante Arbeit geschieht auf Channels, die die Anwendung selbst registriert.

12.6.2. Einen Channel registrieren

Ein Cam-seitiges Skript registriert einen neuen Channel, indem es protocol.register() mit einem Namen und einem Python-Backend-Objekt aufruft:

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())

Die Methoden des Backend-Objekts bestimmen, was der Channel tun kann. Ein Backend mit nur size und read ist ein schreibgeschützter Datenchannel; fügt man write hinzu, wird er bidirektional; fügt man poll hinzu, kann der Host fragen, ob neue Daten bereit sind, bevor er für einen Lesevorgang zahlt. Das Abtasten der Daten innerhalb von size ist das einfachste Muster, wenn die Nutzlast klein genug ist, um in ein Fragment zu passen – der Puffer wird bei Bedarf erzeugt, niemals zwischengespeichert, niemals in einer Race-Condition. Größere Nutzlasten – Bild-Frames, Sensor-Spuren – benötigen ein Latch-Muster, das den Puffer hält, bis der Host seinen Lesevorgang über mehrere Fragmente abgeschlossen hat, was beim Frame-Channel behandelt wird.

Ein wenig Buchführung geschieht automatisch:

  • Die Bibliothek weist die nächste freie Channel-ID zu (zwischen 0 und 31).

  • Die Fähigkeits-Flags werden aus den vorhandenen Methoden abgeleitet: CHANNEL_FLAG_READ, wenn read definiert ist, CHANNEL_FLAG_WRITE, wenn write definiert ist, CHANNEL_FLAG_LOCK, wenn lock / unlock definiert sind.

  • Ein CHANNEL_REGISTERED-Ereignispaket wird an jeden verbundenen Host gesendet, damit dessen Channel-Liste aktualisiert wird.

Der Rückgabewert ist ein protocol.ProtocolChannel-Handle, das die Anwendung behalten kann. Die Methode send_event() des Handles ist der Cam-seitige Hook, um dem Host mitzuteilen „auf diesem Channel ist etwas passiert, ohne dass sich die lesbaren Daten geändert haben“ – ein Trigger wurde ausgelöst, eine Taste wurde gedrückt, ein Meilenstein bei der Anzahl der Abtastungen wurde erreicht.

12.6.3. Channels vom Host aus lesen

Das Host-SDK wird als openmv-Paket auf PyPI ausgeliefert (pip install openmv) und baut auf pyserial für den Transport auf. Seine Klasse openmv.camera.Camera stellt die benannten Channels der Cam über High-Level-Methoden bereit:

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)

Warnung

Das openmv-Paket erfordert CPython 3.12 oder neuer. Frühere Interpreter haben nicht die Funktionen, von denen das SDK abhängt; installieren Sie einen 3.12+-Build vor pip install openmv.

Einige Dinge, die beim Setup zu beachten sind:

  • Die Zeichenkette des seriellen Ports – hier /dev/ttyACM0 – ist unter Windows im COM3-Stil, unter macOS /dev/cu.usbmodemXXXX und unter Linux /dev/ttyACM*. Die tatsächliche Nummer hängt davon ab, als welcher Port sich die Cam angemeldet hat.

  • Die Baudrate ist der magische Wert 921600 des Protokolls, den der USB-CDC-Stack der Cam als „dieser Client spricht das Protokoll, nicht die REPL“ erkennt. Jede andere Rate fällt auf eine einfache serielle Leitung zurück.

  • Der Kontextmanager with Camera(...) as cam: öffnet den Transport, führt PROTO_SYNC aus, tauscht Fähigkeiten aus und schließt beim Verlassen den Port sauber. Der explizite Aufruf von update_channels() nach dem Eintritt aktualisiert die lokale Channel-Liste um alle Channels, die die Anwendung nach dem Booten registriert hat.

channel_size() und channel_read() sind die Arbeitspferd-Methoden; channel_write() überträgt einen Puffer zur Cam, wenn das Backend eine write-Methode hat; has_channel() ist der sichere Weg zu prüfen, ob ein Name registriert ist, bevor man ihn verwendet. Der Channel-Name wird einmal in die Channel-ID übersetzt, die die Cam während register zugewiesen hat, und von da an in jedem Paket verwendet.

Jedes Paar aus channel_size() / channel_read() kostet zwei Round-Trips: ein Paket, um die Größe abzufragen, eines, um die Bytes abzufragen. Über USB-CDC sind beide zusammen in etwa einer Millisekunde fertig; über UART dauert derselbe Austausch proportional zur Baudrate der seriellen Leitung länger. Anwendungscode, der in einer engen Schleife liest, sollte channel_size() nur aufrufen, wenn sich die Größe tatsächlich ändern kann – bei Daten fester Größe kann die Größe aus dem ersten Aufruf zwischengespeichert werden.

12.6.4. Unabhängigkeit zwischen Channels

Drei Dinge sind über das Zusammenwirken von Channels wissenswert:

  • Unabhängige Flusskontrolle. Jeder Channel hat seinen eigenen ausstehenden Lesezustand, seine eigenen Daten und seine eigenen size / read / write-Callbacks. Ein langlaufender Lesevorgang auf dem stream-Channel blockiert nicht die Lesevorgänge auf dem config-Channel der Anwendung.

  • Sequenziell pro Channel. Innerhalb eines einzelnen Channels werden Pakete in der richtigen Reihenfolge zugestellt. Die Zuverlässigkeitsebene garantiert dies selbst dann, wenn erneute Übertragungen im Spiel sind.

  • Gemeinsamer Transport, gemeinsames Budget für erneute Übertragungen. Alle Channels teilen sich die eine physische Verbindung, sodass eine Flut von Verkehr auf einem Channel die anderen verlangsamt, indem sie die Leitung belegt. Der CHANNEL_LOCK-Mechanismus erlaubt es einem Channel, die Leitung für einen atomaren Lesevorgang über mehrere Pakete zu reservieren; das Backend aktiviert dies, indem es die lock / unlock-Callbacks implementiert.

Ein Channel ist die minimale Berührungsfläche, auf der ein Host-Programm und ein Cam-Programm sich auf Zusammenarbeit einigen. Der Name, die Richtung (Lesen oder Schreiben oder beides), die Callback-Methoden auf der Cam-Seite und die passenden Methodenaufrufe auf der Host-Seite sind der gesamte Vertrag.