12.7. Channel-Callbacks

Das Backend-Objekt, das an protocol.register() übergeben wird, ist eine Python-Klasse. Die Protokollbibliothek fragt die Klasse nicht, welche Methoden sie implementiert; sie inspiziert die Instanz und verdrahtet diejenigen, die sie findet. Diese Introspektion ist es, die die Backend-Schnittstelle flexibel macht: Das kleinste sinnvolle Backend besteht aus zwei Methoden, das aufwendigste aus zwölf, und die Anwendung aktiviert jede Fähigkeit einzeln, Methode für Methode.

12.7.1. Die Introspektionsregeln

Wenn protocol.register() läuft, durchläuft die Bibliothek eine feste Liste von aufrufbaren Namen und bindet jeden, den sie auf der Backend-Instanz findet:

  • Das Hinzufügen von read zur Klasse aktiviert CHANNEL_FLAG_READ. Ein Host-Aufruf von channel_read() erreicht das Backend nur, wenn dieses Flag gesetzt ist.

  • Das Hinzufügen von write aktiviert CHANNEL_FLAG_WRITE und ermöglicht channel_write().

  • Das Hinzufügen von lock und unlock aktiviert CHANNEL_FLAG_LOCK und ermöglicht es dem Host, den Channel für einen atomaren Lesevorgang über mehrere Pakete zu sperren.

  • Das Hinzufügen von poll ermöglicht es dem Host, kostengünstig zu fragen „ist etwas bereit?“, ohne einen vollständigen Lesevorgang zu erzwingen.

Fehlende Methoden sind keine Fehler – die Protokollbibliothek lässt die entsprechende Fähigkeit einfach deaktiviert. Ein Backend mit nur size und read ist völlig gültig; es ist ein schreibgeschützter Datenchannel.

12.7.2. Ein schreibgeschützter Sensor-Channel

Ein Sensor-Channel, der bei jeder Host-Anfrage einen frischen Messwert veröffentlicht und Host-Schreibvorgänge ablehnt, nutzt vier der Callbacks:

import protocol
import struct

class TempChannel:
    def __init__(self, read_sensor):
        self._read_sensor = read_sensor
        self._buf = b''
        self._fresh = False

    def poll(self):
        # Tell the host whether a reading is waiting.
        return self._fresh

    def size(self):
        # Sample fresh data on every host-side size query.
        value = self._read_sensor()
        self._buf = struct.pack('<f', value)
        self._fresh = True
        return len(self._buf)

    def read(self, offset, size):
        end = offset + size
        if end >= len(self._buf):
            self._fresh = False
        return self._buf[offset:end]

protocol.register(name='temp', backend=TempChannel(read_temperature))

Durchgehen, was jede Methode tut:

  • poll gibt das Aktualitäts-Flag zurück. Der Host ruft es vor dem Lesen auf und überspringt den Lesevorgang vollständig, wenn es False zurückgibt. Das spart die Round-Trip-Kosten für „noch keine neuen Daten“.

  • size erzeugt den Puffer bei Bedarf neu und meldet dessen Länge. Die Abtastung hier durchzuführen bedeutet, dass das Backend keine Hintergrundaufgabe benötigt – ein Host-Aufruf treibt jede Messung an.

  • read gibt einen Ausschnitt des Puffers zurück. Die Protokollbibliothek kann es mehr als einmal aufrufen, wenn der Puffer größer als die ausgehandelte maximale Nutzlast ist; das Argument offset durchläuft die Fragmente.

  • Kein write bedeutet, dass Host-Schreibvorgänge auf der Framing-Ebene abgelehnt werden, bevor das Backend überhaupt beteiligt ist.

12.7.3. Der vollständige Callback-Satz

Zur Referenz: jede Methode, nach der die Bibliothek auf einem Backend sucht:

Methode

Rückgabe

Zweck

init(self)

object

Optionale einmalige Initialisierung, wenn der Channel sich erstmals an einen Host bindet. Gibt bei Erfolg einen beliebigen Wert ungleich None zurück.

poll(self)

bool

Gibt True zurück, wenn Daten verfügbar sind.

lock(self)

bool

Erwirbt den Channel für eine atomare Übertragung über mehrere Pakete.

unlock(self)

bool

Gibt ein vorheriges lock frei.

size(self)

int

Anzahl der aktuell aus dem Channel lesbaren Bytes.

shape(self)

tuple

Bis zu vier Ganzzahlen, die die Datenstruktur beschreiben (z. B. Bildhöhe, -breite, Byteanzahl). Wird vom Host zum Entpacken typisierter Puffer verwendet.

read(self, offset, size)

bytes

Gibt bis zu size Bytes ab offset zurück. Wird einmal pro Fragment aufgerufen, wenn die Nutzlast das ausgehandelte Maximum überschreitet.

readp(self, offset, size)

bytes

Zero-Copy-Variante von read: Der Speicher des Puffers muss für die Dauer der Übertragung gültig bleiben.

write(self, offset, data)

int

Der Host hat data an offset geschrieben. data ist eine bytearray-Ansicht auf den Empfangspuffer der Protokollebene – kopieren Sie heraus, was Sie behalten möchten, bevor Sie zurückkehren.

ioctl(self, cmd, length, arg)

int

Anwendungsdefinierter Opcode außerhalb des Lese-/Schreibmodells. Ein negativer Rückgabewert ist ein Fehler.

flush(self)

object

Verwirft alle gepufferten Daten. Wird aufgerufen, wenn der Host den Channel zurücksetzen möchte.

is_active(self)

bool

Nur sinnvoll bei Backends, die einen physischen Transport repräsentieren (die eingebauten USB-Channels). Anwendungs-Channels benötigen dies nicht.

Das ist die gesamte Backend-Schnittstelle. Zwölf Methodennamen, alle optional, und die Protokollbibliothek entscheidet anhand der vorhandenen Methoden, was jeder Channel tun kann.