12.9. Flux bidirectionnel

Les canaux ne sont pas unidirectionnels. Un backend qui implémente write permet à l’hôte d’envoyer des octets vers la caméra, et la caméra réagit. C’est le schéma derrière chaque véritable outil interactif : l’opérateur tourne un bouton dans l’interface graphique de l’hôte, l’hôte écrit la nouvelle valeur sur un canal de configuration, et la caméra la lit lors de sa prochaine capture.

12.9.1. Un canal de configuration

En complétant le script de diffusion côté caméra, exposez un second canal pour la qualité 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)

La boucle de capture lit config.quality chaque fois qu’elle compresse une trame

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

L’hôte dispose maintenant d’un bouton. Réglez-le sur 50 et la trame suivante sera plus petite (et plus laide) ; réglez-le sur 95 et la trame suivante sera plus grande (et plus nette). La caméra continue de capturer sans redémarrer ; l’hôte n’a pas à envoyer un nouveau script.

12.9.2. L’appel d’écriture depuis l’hôte

Côté hôte, channel_write() envoie des octets vers un canal nommé

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

Le SDK hôte encode les octets sous la forme d’un seul paquet CHANNEL_WRITE (ou fragmenté), la couche de protocole le délivre à la caméra, le write(offset=0, data=...) de la caméra s’exécute, et le côté caméra accuse réception. Au moment où l’appel retourne, la caméra a reçu et accepté la nouvelle valeur.

L’écriture est atomique du point de vue de la caméra – la bibliothèque du protocole garantit que le write du backend s’exécute jusqu’à son terme avant qu’aucune autre opération sur ce canal ne se poursuive. Le code applicatif peut lire config.quality depuis l’intérieur de la boucle de capture sans craindre que l’hôte n’interfère en plein milieu d’une capture.

12.9.3. Taille fictive et lecture sur un canal en écriture seule

Un canal en écriture pure a tout de même besoin que size et read soient définis, même s’il s’agit de stubs renvoyant 0 et b''. La bibliothèque utilise la présence des méthodes pour déduire les drapeaux de capacité du canal ; un backend auquel il manque read n’obtiendra pas le drapeau CHANNEL_FLAG_READ et l’hôte refusera toute tentative de lecture.

Les octets renvoyés par read sur un canal en écriture seule sont toutefois utiles à une autre fin : renvoyer en écho la valeur actuelle afin qu’un hôte qui vient de se connecter puisse demander à la caméra « quel est le réglage actuel ? » plutôt que de partir d’une valeur par défaut. Pour que cela fonctionne, les deux directions doivent s’accorder sur une sérialisation. L’analyse brute des octets int(bytes(data)) de l’exemple précédent fonctionne pour un seul champ entier mais ne tiendra pas la charge dès qu’il y a un second bouton à régler. Faire en sorte que write analyse du JSON et l’associer à un read qui renvoie le dump JSON correspondant transforme le canal en un véritable magasin de configuration aller-retour

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)

L’hôte écrit désormais cam.channel_write('config', b'{"quality": 50}') pour définir une valeur et cam.channel_read('config') pour relire l’état actuel. La caméra sérialise un nouveau dump JSON à chaque lecture afin que l’hôte voie toujours les valeurs les plus récentes, et ajouter un autre bouton (threshold, exposure, orientation) tient en une ligne dans le dictionnaire JSON de chaque côté.

12.9.4. Une boucle complète

Avec un canal de trame pour les données caméra → hôte, un canal de configuration pour le contrôle hôte → caméra, et un peu de colle, l’application devient un outil interactif :

  • L’hôte ouvre la caméra, commence à récupérer des trames et les affiche dans une fenêtre.

  • Lorsque l’opérateur déplace un curseur, l’hôte écrit la nouvelle valeur sur config.

  • La boucle de capture de la caméra récupère la valeur à la trame suivante.

  • Les nouvelles trames circulent par le même canal frame.

C’est tout le modèle. Deux canaux, deux fonctions de rappel chacun, une boucle de capture sur la caméra, une boucle de lecture-écriture sur l’hôte. Aucune logique de tramage visible, aucune gestion d’erreur visible – la bibliothèque du protocole fait disparaître le déplacement fiable des octets.

Tout ce qui suit est du code applicatif. Ajouter un troisième canal pour un histogramme, un quatrième pour la télémétrie, ou un cinquième pour les déclencheurs de capteur suit la même recette de classe backend et de protocol.register, répétée. Une fois qu’un projet de caméra atteint ce point, le protocole cesse d’être le problème intéressant ; c’est la logique propre à l’application qui le devient.