13.3.1.5. Eventos

As páginas anteriores chamam a câmara: carregam um script, leem um fotograma, escrevem num canal. Todas essas operações são iniciadas pelo anfitrião – o anfitrião pergunta, a câmara responde. O protocolo também funciona na direção inversa. A câmara pode enviar eventos para o anfitrião sem ser solicitada, e o SDK do anfitrião entrega cada um a um callback que a aplicação pode substituir.

Esta é a ferramenta certa sempre que a aplicação quer reagir a algo que a câmara detetou antes de perguntar. Sem eventos, a única forma de descobrir é continuar a chamar read_status() num ciclo.

13.3.1.5.1. O callback predefinido

Camera já subscreve eventos internamente. _handle_event() é o callback que o transporte executa sempre que chega um pacote de evento. O predefinido trata de três eventos do sistema:

  • CHANNEL_REGISTERED – apareceu um novo canal na câmara após a ligação do anfitrião. A estrutura atualiza a sua cache de canais para que a próxima pesquisa has_channel() o encontre.

  • CHANNEL_UNREGISTERED – um canal desapareceu.

  • SOFT_REBOOT – a câmara reiniciou por conta própria (watchdog, falha grave, machine.reset() intencional).

Também rastreia o evento de fotograma pronto do canal stream para o caminho de transmissão e o início/fim do script do canal stdin para o buffering de stdout. A predefinição events=True do construtor mantém tudo isto ativo; uma aplicação que não queira nada disto pode passar events=False para Camera e o subsistema de eventos fica silencioso.

13.3.1.5.2. Criar subclasses para reagir

Para tratar eventos específicos da aplicação que a câmara levanta, crie uma subclasse de Camera e substitua _handle_event(). Chame primeiro o pai para manter o comportamento predefinido, depois despache os eventos que a aplicação tem interesse:

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

A assinatura é (channel_id, event). channel_id é 0 para eventos do sistema e, caso contrário, o ID numérico do canal que o levantou; event é um inteiro que o script na câmara escolheu. O enum EventType dá nomes aos três eventos do sistema; os eventos de canal usam os valores que o backend na câmara definir.

Os eventos de canal chegam indexados pelo ID numérico, não pelo nome. O dicionário channels_by_id em cache é o que o método substituído acima usa para procurar o nome; channels_by_name é o seu espelho, indexado ao contrário.

13.3.1.5.3. A metade na câmara

O script na câmara levanta um evento chamando send_event() no handle devolvido por 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)

O número do evento é um inteiro escolhido pela aplicação. Qualquer valor que o método substituído do anfitrião esteja preparado para tratar é válido; a camada de protocolo trata-o como payload opaco. Por predefinição a chamada é do tipo «disparar e esquecer»; passe wait_ack=True para bloquear até o anfitrião reconhecer, quando saber que o evento chegou importa mais do que a latência da viagem de ida e volta.

Um canal que apenas levanta eventos e não contém dados legíveis é um padrão válido – size devolve 0 e read devolve bytes vazios. A biblioteca de protocolo ainda precisa de ambos os métodos presentes para marcar o canal como legível; o script na câmara simplesmente nunca coloca dados nele.

13.3.1.5.4. Manter o caminho de receção ativo quando inativo

Os eventos chegam na mesma ligação que tudo o resto, pelo que qualquer chamada do anfitrião que envie ou receba bytes dá ao transporte uma oportunidade de processar eventos pendentes inline. Um ciclo de consulta que já chama read_status() ou read_frame() uma vez por ciclo não precisa de nada extra.

Para programas que ficam minutos sem outra E/S, poll_events() executa o caminho de receção uma vez sem enviar um comando. Retorna assim que o buffer de entrada estiver vazio, pelo que um ciclo fechado à sua volta – ou um temporizador curto num ciclo de eventos GUI – é o que mantém os handlers reativos.

13.3.1.5.5. Um ciclo completo

Do início ao fim, o padrão é: o script na câmara regista um canal e chama send_event() quando algo acontece; a subclasse no anfitrião substitui _handle_event() e despacha. Um ciclo no anfitrião que apenas serve eventos tem este aspeto:

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

    while True:
        cam.poll_events()

A câmara captura, decide e levanta eventos. O anfitrião permanece dentro de poll_events() até que um chegue, depois on_motion é executado. Nenhuma chamada a read_status() é feita quando nada aconteceu, e nenhum fotograma é transferido via USB quando a câmara nada tem a reportar.