13.3.1.5. Événements¶
Les pages précédentes appellent la caméra : téléverser un script, lire une trame, écrire sur un canal. Chacune de ces opérations est initiée par l’hôte : l’hôte demande, la caméra répond. Le protocole fonctionne aussi dans l’autre sens. La caméra peut envoyer des événements à l’hôte sans qu’on le lui demande, et le SDK de l’hôte transmet chacun d’eux à une fonction de rappel que l’application peut remplacer.
C’est l’outil adapté chaque fois que l’application souhaite réagir à quelque chose que la caméra a remarqué avant qu’elle ne le demande. Sans les événements, le seul moyen de le savoir est d’appeler en boucle read_status().
13.3.1.5.1. La fonction de rappel par défaut¶
Camera s’abonne déjà aux événements en interne. _handle_event() est la fonction de rappel que le transport exécute chaque fois qu’un paquet d’événement arrive. Le comportement par défaut gère trois événements système :
CHANNEL_REGISTERED: un nouveau canal est apparu sur la caméra après la connexion de l’hôte. Le framework actualise son cache de canaux afin que la prochaine recherchehas_channel()le trouve.CHANNEL_UNREGISTERED: un canal a disparu.SOFT_REBOOT: la caméra a redémarré d’elle-même (chien de garde, faute matérielle,machine.reset()intentionnel).
Il suit aussi l’événement « trame prête » du canal stream pour le chemin de diffusion ainsi que le démarrage et l’arrêt de script du canal stdin pour la mise en tampon de stdout. La valeur par défaut events=True du constructeur maintient tout cela actif ; une application qui n’en veut aucun peut passer events=False à Camera et le sous-système d’événements reste silencieux.
13.3.1.5.2. Dériver une sous-classe pour réagir¶
Pour gérer les événements propres à l’application que la caméra déclenche, dérivez une sous-classe de Camera et redéfinissez _handle_event(). Appelez d’abord la classe parente pour conserver le comportement par défaut, puis répartissez les événements qui intéressent l’application:
from openmv import Camera
class MyCamera(Camera):
def _handle_event(self, channel_id, event):
super()._handle_event(channel_id, event)
name = self.channels_by_id.get(
channel_id, {}).get('name')
if name == 'motion' and event == 1:
self.on_motion()
def on_motion(self):
print("motion detected")
La signature est (channel_id, event). channel_id vaut 0 pour les événements système et sinon l’identifiant numérique du canal qui l’a déclenché ; event est un entier choisi par le script côté caméra. L’énumération EventType donne des noms aux trois événements système ; les événements de canal utilisent les valeurs que définit le backend côté caméra.
Les événements de canal reviennent indexés par identifiant numérique, et non par nom. Le dictionnaire mis en cache channels_by_id est ce que la redéfinition ci-dessus utilise pour rechercher le nom ; channels_by_name en est le miroir, indexé dans l’autre sens.
13.3.1.5.3. La moitié côté caméra¶
Le script côté caméra déclenche un événement en appelant send_event() sur le descripteur renvoyé par protocol.register():
import protocol
class MotionChannel:
def size(self):
return 0
def read(self, offset, size):
return b''
def poll(self):
return False
ch = protocol.register(
name='motion', backend=MotionChannel())
while True:
if detect_motion():
ch.send_event(1)
Le numéro d’événement est un entier choisi par l’application. Toute valeur que la redéfinition de l’hôte est prête à gérer est acceptable ; la couche protocole la traite comme une charge utile opaque. Par défaut, l’appel se déclenche et oublie ; passez wait_ack=True pour bloquer jusqu’à ce que l’hôte accuse réception, lorsque savoir que l’événement est bien arrivé compte plus que la latence de l’aller-retour.
Un canal qui ne déclenche que des événements et ne transporte aucune donnée lisible est un schéma valide : size renvoie 0 et read renvoie des octets vides. La bibliothèque de protocole a tout de même besoin que les deux méthodes soient présentes pour marquer le canal comme lisible ; le script côté caméra n’y place simplement jamais de données.
13.3.1.5.4. Piloter le chemin de réception en période d’inactivité¶
Les événements arrivent sur la même connexion que tout le reste, de sorte que tout appel de l’hôte qui envoie ou reçoit des octets donne au transport l’occasion de traiter les événements en attente en ligne. Une boucle d’interrogation qui appelle déjà read_status() ou read_frame() une fois par cycle n’a besoin de rien de plus.
Pour les programmes qui restent des minutes sans autre entrée/sortie, poll_events() exécute le chemin de réception une fois sans envoyer de commande. Elle retourne dès que le tampon entrant est vide, de sorte qu’une boucle serrée autour d’elle, ou un court minuteur dans une boucle d’événements d’interface graphique, est ce qui maintient les gestionnaires réactifs.
13.3.1.5.5. Une boucle complète¶
De bout en bout, le schéma est le suivant : le script côté caméra enregistre un canal et appelle send_event() lorsqu’un événement survient ; la sous-classe côté hôte redéfinit _handle_event() et répartit. Une boucle d’hôte qui ne fait rien d’autre que traiter les événements ressemble à ceci:
with MyCamera('/dev/ttyACM0') as cam:
cam.stop()
cam.exec(open('motion_cam.py').read())
while True:
cam.poll_events()
La caméra capture, décide et déclenche des événements. L’hôte reste à l’intérieur de poll_events() jusqu’à ce qu’un événement arrive, puis on_motion s’exécute. Aucun appel à read_status() ne s’exécute lorsque rien ne s’est produit, et aucune trame n’est transférée via USB lorsque la caméra n’a rien à signaler.