13.4.1.5. Eventi#

Le pagine viste finora effettuano chiamate verso la cam: caricano uno script, leggono un frame, scrivono su un canale. Ognuna di queste operazioni è avviata dall’host: l’host chiede, la cam risponde. Il protocollo funziona anche nella direzione opposta. La cam può inviare eventi all’host senza che le siano richiesti, e l’SDK host consegna ciascuno di essi a un callback che l’applicazione può sovrascrivere.

Questo è lo strumento giusto ogni volta che l’applicazione vuole reagire a qualcosa che la cam ha notato prima ancora di chiederlo. Senza eventi, l’unico modo per scoprirlo è continuare a chiamare read_status() in un ciclo.

13.4.1.5.1. Il callback predefinito#

Camera si iscrive già internamente agli eventi. _handle_event() è il callback che il transport esegue ogni volta che arriva un pacchetto di evento. Quello predefinito gestisce tre eventi di sistema:

  • CHANNEL_REGISTERED – un nuovo canale è apparso sulla cam dopo la connessione dell’host. Il framework aggiorna la sua cache dei canali in modo che la successiva ricerca has_channel() lo trovi.

  • CHANNEL_UNREGISTERED – un canale è scomparso.

  • SOFT_REBOOT – la cam si è riavviata da sola (watchdog, hard fault, machine.reset() intenzionale).

Tiene inoltre traccia dell’evento di frame pronto del canale stream per il percorso di streaming e dell’avvio / arresto dello script del canale stdin per il buffering dello stdout. Il valore predefinito events=True del costruttore mantiene attivo tutto questo; un’applicazione che non ne vuole nulla può passare events=False a Camera e il sottosistema degli eventi rimane silenzioso.

13.4.1.5.2. Sottoclassi per reagire#

Per gestire gli eventi specifici dell’applicazione generati dalla cam, crea una sottoclasse di Camera e sovrascrivi _handle_event(). Chiama prima il metodo padre per mantenere il comportamento predefinito, quindi smista gli eventi a cui l’applicazione tiene:

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 firma è (channel_id, event). channel_id è 0 per gli eventi di sistema e altrimenti l’ID numerico del canale che lo ha generato; event è un intero scelto dallo script lato cam. L’enum EventType assegna i nomi ai tre eventi di sistema; gli eventi di canale usano qualsiasi valore definito dal backend lato cam.

Gli eventi di canale vengono restituiti con chiave l’ID numerico, non il nome. Il dizionario channels_by_id in cache è ciò che l’override sopra usa per cercare il nome; channels_by_name è il suo speculare, con chiavi nell’altra direzione.

13.4.1.5.3. La metà lato cam#

Lo script lato cam genera un evento chiamando send_event() sull’handle restituito da 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)

Il numero dell’evento è un intero scelto dall’applicazione. Qualsiasi valore che l’override dell’host è pronto a gestire è ammissibile; il livello di protocollo lo tratta come payload opaco. Per impostazione predefinita la chiamata invia e dimentica; passa wait_ack=True per bloccare fino a quando l’host conferma, quando sapere che l’evento è arrivato conta più della latenza del viaggio di andata e ritorno.

Un canale che genera solo eventi e non trasporta dati leggibili è uno schema valido: size restituisce 0 e read restituisce byte vuoti. La libreria del protocollo richiede comunque la presenza di entrambi i metodi per contrassegnare il canale come leggibile; lo script lato cam semplicemente non vi inserisce mai dati.

13.4.1.5.4. Pilotare il percorso di ricezione durante l’inattività#

Gli eventi arrivano sulla stessa connessione di tutto il resto, quindi qualsiasi chiamata host che invia o riceve byte dà al transport la possibilità di elaborare gli eventi in sospeso in linea. Un ciclo di polling che chiama già read_status() o read_frame() una volta per ciclo non necessita di nulla di aggiuntivo.

Per i programmi che restano minuti senza altro I/O, poll_events() esegue il percorso di ricezione una volta senza inviare un comando. Ritorna non appena il buffer in entrata è vuoto, quindi un ciclo serrato attorno ad esso, o un breve timer in un ciclo di eventi GUI, è ciò che mantiene reattivi i gestori.

13.4.1.5.5. Un ciclo completo#

Dall’inizio alla fine, lo schema è: lo script lato cam registra un canale e chiama send_event() quando accade qualcosa; la sottoclasse lato host sovrascrive _handle_event() e smista. Un ciclo host che non fa altro che servire gli eventi ha questo aspetto:

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

    while True:
        cam.poll_events()

La cam acquisisce, decide e genera eventi. L’host resta all’interno di poll_events() finché non ne arriva uno, quindi viene eseguito on_motion. Nessuna chiamata a read_status() viene eseguita quando non è successo nulla, e nessun frame viene trasferito su USB quando la cam non ha nulla da segnalare.