12.6. Canaux nommés

L’identifiant de canal présent dans l’en-tête de chaque paquet permet à jusqu’à 32 flux indépendants de partager le même transport physique. La couche de canaux transforme ces identifiants numériques en points de terminaison nommés et visibles par l’application, auxquels le code hôte peut faire référence par une chaîne de caractères.

Un seul fil de transport à gauche se ramifiant en quatre canaux étiquetés du côté caméra -- stdin, stdout, stream et un canal de statut enregistré par l'utilisateur -- chacun apparaissant comme une boîte indépendante.

12.6.1. Les quatre canaux intégrés

La caméra enregistre quatre canaux au démarrage, avant l’exécution de tout code applicatif :

  • stdin – les octets de script que l’hôte envoie à la caméra pour exécution. L’IDE utilise ce canal pour envoyer le script en cours d’édition ; exec() sur le SDK hôte est l’appel équivalent depuis un programme Python.

  • stdout – les octets provenant des appels print() de la caméra et des traces d’exceptions non capturées. La console série de l’IDE lit ce canal.

  • stream – le canal d’aperçu en direct. L’IDE en récupère des trames JPEG ; tout script hôte peut faire de même avec read_frame().

  • profile – les événements du profileur, présents uniquement lorsque la caméra a été compilée avec le profilage activé. La plupart des versions de production l’omettent.

Le code applicatif a rarement besoin de toucher à l’un des canaux intégrés ; le travail intéressant se déroule sur les canaux que l’application enregistre elle-même.

12.6.2. Enregistrement d’un canal

Un script côté caméra enregistre un nouveau canal en appelant protocol.register() avec un nom et un objet backend Python

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

Les méthodes de l’objet backend décident de ce que le canal peut faire. Un backend doté uniquement de size et read est un canal de données en lecture seule ; ajoutez write et il devient bidirectionnel ; ajoutez poll et l’hôte peut demander si de nouvelles données sont prêtes avant de payer le coût d’une lecture. Échantillonner les données à l’intérieur de size est le modèle le plus simple lorsque la charge utile est suffisamment petite pour tenir dans un seul fragment – le tampon est généré à la demande, jamais mis en cache, jamais sujet à une situation de concurrence. Les charges utiles plus importantes – trames d’image, traces de capteur – nécessitent un modèle de verrouillage qui maintient le tampon jusqu’à ce que l’hôte termine sa lecture sur plusieurs fragments, abordé avec le canal de trames.

Une petite quantité de comptabilité se produit automatiquement :

  • La bibliothèque attribue le prochain identifiant de canal libre (entre 0 et 31).

  • Les indicateurs de capacité sont déduits des méthodes présentes : CHANNEL_FLAG_READ si read est défini, CHANNEL_FLAG_WRITE si write est défini, CHANNEL_FLAG_LOCK si lock / unlock sont définis.

  • Un paquet d’événement CHANNEL_REGISTERED est envoyé à tout hôte connecté afin que sa liste de canaux se mette à jour.

La valeur de retour est un descripteur protocol.ProtocolChannel que l’application peut conserver. La méthode send_event() du descripteur est le point d’accroche côté caméra permettant d’indiquer à l’hôte « quelque chose s’est produit sur ce canal sans modifier les données lisibles » – un déclencheur s’est activé, un bouton a été pressé, un palier de nombre d’échantillons a été franchi.

12.6.3. Lecture des canaux depuis l’hôte

Le SDK hôte est distribué sous forme du paquet openmv sur PyPI (pip install openmv), construit sur pyserial pour le transport. Sa classe openmv.camera.Camera expose les canaux nommés de la caméra au moyen de méthodes de haut niveau

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)

Avertissement

Le paquet openmv requiert CPython 3.12 ou plus récent. Les interpréteurs antérieurs ne disposent pas des fonctionnalités dont dépend le SDK ; installez une version 3.12+ avant pip install openmv.

Quelques points à noter concernant la configuration :

  • La chaîne du port série – /dev/ttyACM0 ici – est de style COM3 sous Windows, /dev/cu.usbmodemXXXX sous macOS et /dev/ttyACM* sous Linux. Le numéro réel dépend du port sous lequel la caméra a été énumérée.

  • Le débit en bauds est la valeur magique du protocole 921600, que la pile USB-CDC de la caméra reconnaît comme « ce client parle le protocole, pas le REPL ». Tout autre débit retombe sur une simple ligne série.

  • Le gestionnaire de contexte with Camera(...) as cam: ouvre le transport, exécute PROTO_SYNC, échange les capacités et, à la sortie, ferme proprement le port. L’appel explicite update_channels() après l’entrée rafraîchit la liste locale des canaux avec tout canal que l’application a enregistré après le démarrage.

channel_size() et channel_read() sont les méthodes de travail principales ; channel_write() effectue un aller-retour d’un tampon vers la caméra si le backend possède une méthode write ; has_channel() est le moyen sûr de vérifier qu’un nom est enregistré avant de l’utiliser. Le nom du canal est résolu une fois en l’identifiant de canal que la caméra a attribué lors de register, puis utilisé dans chaque paquet par la suite.

Chaque paire channel_size() / channel_read() coûte deux allers-retours : un paquet pour demander la taille, un pour demander les octets. Sur USB-CDC, les deux se terminent en environ une milliseconde au total ; sur UART, le même échange prend plus de temps proportionnellement au débit en bauds de la ligne série. Le code applicatif qui lit dans une boucle serrée ne devrait appeler channel_size() que lorsque la taille peut réellement changer – pour des données de taille fixe, la taille du premier appel peut être mise en cache.

12.6.4. Indépendance entre les canaux

Trois choses méritent d’être connues sur la façon dont les canaux interagissent :

  • Contrôle de flux indépendant. Chaque canal possède son propre état de lecture en attente, ses propres données et ses propres fonctions de rappel size / read / write. Une lecture de longue durée sur le canal stream ne bloque pas les lectures sur le canal config de l’application.

  • Séquentiel par canal. Au sein d’un même canal, les paquets sont livrés dans l’ordre. La couche de fiabilité le garantit même lorsque des retransmissions sont impliquées.

  • Transport partagé, budget de retransmission partagé. Tous les canaux partagent l’unique liaison physique ; ainsi, un flot de trafic sur un canal ralentit les autres en monopolisant le fil. Le mécanisme CHANNEL_LOCK permet à un canal de réserver le fil pour une lecture atomique sur plusieurs paquets ; le backend y adhère en implémentant les fonctions de rappel lock / unlock.

Un canal est la surface minimale sur laquelle un programme hôte et un programme caméra conviennent de coopérer. Le nom, le sens de circulation (lecture, écriture ou les deux), les méthodes de rappel côté caméra et les appels de méthode correspondants côté hôte constituent l’intégralité du contrat.