13.3.1.4. Benutzerdefinierte Kanäle

Ein Kanal ist ein benannter, bidirektionaler Byte-Stream zwischen einem kameraseitigen Skript und dem Host. Die Kamera registriert einen Kanal und stellt Callbacks bereit, die Daten erzeugen oder verbrauchen; der Host liest aus diesem Kanal und schreibt in ihn anhand des Namens. Derselbe Mechanismus, den das Paket intern für den stream-Kanal (der Einzelbilder transportiert), den stdout-Kanal (der die Skriptausgabe transportiert) und den stdin-Kanal (der den Skript-Upload transportiert) verwendet, steht Benutzerskripten zur Verfügung. So können beliebige anwendungsspezifische Daten, die der Host benötigt, dieselbe USB-Verbindung nutzen, ohne dass ein zweites Protokoll erfunden werden muss.

Dies ist das nützlichste Feature des Pakets und gleichzeitig dasjenige, das die Standarddokumentation am schlechtesten abdeckt. Daher arbeitet diese Seite es von Anfang bis Ende durch.

13.3.1.4.1. Die beiden Hälften

Ein benutzerdefinierter Kanal benötigt zusammenarbeitenden Code auf beiden Seiten. Das kameraseitige Skript importiert protocol, definiert eine Klasse mit drei Methoden (size(), read(), poll()) sowie einer optionalen write()-Methode und ruft protocol.register(name=..., backend=...) auf, um den Kanal unter einem gewählten Namen zu veröffentlichen:

import protocol
import time

class TicksChannel:
    def size(self):
        return 10

    def read(self, offset, size):
        return f'{time.ticks_ms():010d}'

    def poll(self):
        return True

protocol.register(name='ticks', backend=TicksChannel())

Die size()-Methode gibt zurück, wie viele Bytes der Kanal aktuell zur Verfügung hat. read() ist der Produzent: Bei einem vom Host angeforderten offset und size gibt sie die Bytes zurück (oder eine Zeichenkette, die die Protokollschicht kodiert). poll() gibt True zurück, wenn es etwas zu lesen gibt – die Protokollschicht verwendet dies, um den Kanal in read_status() als bereit zu kennzeichnen.

Das hostseitige Programm verwendet vier openmv.Camera-Methoden: has_channel(), um zu prüfen, ob der Kanal existiert, channel_size(), um abzufragen, wie viele Daten warten, channel_read(), um Bytes herauszuziehen, und channel_write(), um Bytes hineinzuschieben. read_status() fragt alle Kanäle auf einmal ab:

from openmv import Camera

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('ticks_cam.py').read())

    while True:
        status = cam.read_status()

        if status.get('ticks'):
            data = cam.channel_read('ticks')
            print(f"ticks: {data.decode()}")

Die Host-Schleife fragt read_status() ab; wenn der ticks-Kanal bereit ist, ruft sie channel_read() ohne size auf, um alles abzuholen, was verfügbar ist. Die TicksChannel.poll()-Methode der Kamera gibt bei jeder Prüfung True zurück, sodass der Kanal immer „bereit“ ist und der Host bei jeder Abfrage einen frischen Tick-Wert erhält.

13.3.1.4.2. Ein bidirektionaler Kanal

Für einen Host, der Daten zurückschieben muss, fügt die kameraseitige Klasse eine write()-Methode hinzu, die die eingehenden Bytes annimmt:

import protocol

class CommandChannel:
    def __init__(self):
        self.last_command = b''
        self.replied = False

    def size(self):
        return len(self.last_command)

    def read(self, offset, size):
        self.replied = True
        return self.last_command

    def write(self, offset, data):
        self.last_command = b'echo: ' + bytes(data)
        self.replied = False

    def poll(self):
        return not self.replied and len(self.last_command) > 0

protocol.register(name='echo', backend=CommandChannel())

Der Host schreibt mit channel_write() in den Kanal und liest die Antwort über das übliche Muster aus read_status() / channel_read() zurück:

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('echo_cam.py').read())

    cam.channel_write('echo', b'hello')

    while True:
        if cam.read_status().get('echo'):
            print(cam.channel_read('echo').decode())
            break

13.3.1.4.3. Was die Anwendung dadurch gewinnt

Benutzerdefinierte Kanäle sind das richtige Werkzeug, wann immer eine Anwendung die bestehende USB-Verbindung für Daten nutzen möchte, die weder Einzelbilder noch Ausgaben sind: Telemetriezähler, Konfigurationsregler, die live von einer Benutzeroberfläche auf dem Host gestreamt werden, Steuerbefehle, die in die andere Richtung gesendet werden, Ergebnisse einer von der Kamera berechneten Messung, die nicht in das „Bild“-Format passen, das der Stream-Kanal voraussetzt. Die Protokollschicht kümmert sich um Framing, Fragmentierung, Bestätigung und Wiederholung; das Skript muss nur das Vier-Methoden-Backend implementieren, und der Host muss nur den Kanalnamen und die Datenform kennen.

Das --channel NAME-Flag des CLI ist ein schneller Weg, um einen benutzerdefinierten Kanal vom Terminal aus zu überprüfen, ohne ein hostseitiges Programm zu schreiben: Das CLI fragt den benannten Kanal ab und gibt die ersten zehn Bytes jeder Aktualisierung aus.

Die Größenbeschränkung für einen einzelnen Aufruf von channel_read() oder channel_write() ist das vom Protokoll ausgehandelte max_payload – standardmäßig 4096 Bytes. Die hostseitigen Methoden teilen größere Schreibvorgänge automatisch in die richtige Anzahl von Paketen auf, sodass die Anwendung beliebig große Puffer übergeben kann; die Fragmentierung ist unsichtbar.