13.3.1.5. Eventos

As páginas até aqui chamam a cam: enviar um script, ler um quadro, escrever em um canal. Cada uma dessas operações é iniciada pelo host – o host pede, a cam responde. O protocolo também funciona na outra direção. A cam pode enviar eventos ao host sem ser solicitada, e o SDK do host entrega cada um a um callback que a aplicação pode sobrescrever.

Esta é a ferramenta certa sempre que a aplicação quer reagir a algo que a cam percebeu antes de pedir. Sem eventos, a única maneira de descobrir é continuar chamando read_status() em um loop.

13.3.1.5.1. O callback padrão

Camera já se inscreve em eventos internamente. _handle_event() é o callback que o transporte executa sempre que um pacote de evento chega. O comportamento padrão trata três eventos de sistema:

  • CHANNEL_REGISTERED – um novo canal apareceu na cam depois que o host conectou. O framework atualiza seu cache de canais para que a próxima consulta de has_channel() o encontre.

  • CHANNEL_UNREGISTERED – um canal desapareceu.

  • SOFT_REBOOT – a cam reiniciou por conta própria (watchdog, hard fault, machine.reset() intencional).

Ele também rastreia o evento de quadro pronto do canal stream para o caminho de streaming e o início/parada de script do canal stdin para o buffering de stdout. O padrão events=True do construtor mantém tudo isso ativado; uma aplicação que não quer nada disso pode passar events=False para Camera e o subsistema de eventos permanece silencioso.

13.3.1.5.2. Criando subclasses para reagir

Para tratar eventos específicos da aplicação que a cam dispara, crie uma subclasse de Camera e sobrescreva _handle_event(). Chame o método pai primeiro para manter o comportamento padrão e, em seguida, despache os eventos que importam à aplicação:

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 de sistema e, caso contrário, o ID numérico do canal que o disparou; event é um inteiro escolhido pelo script do lado da cam. O enum EventType dá nomes aos três eventos de sistema; os eventos de canal usam quaisquer valores que o backend do lado da cam defina.

Os eventos de canal voltam indexados por ID numérico, não por nome. O dicionário channels_by_id em cache é o que a sobrescrita acima usa para procurar o nome; channels_by_name é o seu espelho, indexado no sentido oposto.

13.3.1.5.3. A metade do lado da cam

O script do lado da cam dispara um evento chamando send_event() no handle retornado 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 a sobrescrita do host esteja preparada para tratar é válido; a camada de protocolo o trata como payload opaco. Por padrão, a chamada dispara e esquece; passe wait_ack=True para bloquear até que o host reconheça, quando saber que o evento chegou importa mais do que a latência da viagem de ida e volta.

Um canal que apenas dispara eventos e não transporta dados legíveis é um padrão válido – size retorna 0 e read retorna bytes vazios. A biblioteca de protocolo ainda precisa que ambos os métodos estejam presentes para marcar o canal como legível; o script do lado da cam simplesmente nunca coloca dados nele.

13.3.1.5.4. Movimentando o caminho de recepção enquanto ocioso

Os eventos chegam pela mesma conexão que tudo o mais, então qualquer chamada do host que envie ou receba bytes dá ao transporte a chance de processar eventos pendentes em linha. Um loop de polling que já chama read_status() ou read_frame() uma vez por ciclo não precisa de nada extra.

Para programas que passam minutos sem outras operações de E/S, poll_events() executa o caminho de recepção uma vez sem enviar um comando. Ele retorna assim que o buffer de entrada está vazio, então um loop apertado em torno dele – ou um timer curto em um loop de eventos de GUI – é o que mantém os handlers reativos.

13.3.1.5.5. Um loop completo

De ponta a ponta, o padrão é: o script do lado da cam registra um canal e chama send_event() quando algo acontece; a subclasse do lado do host sobrescreve _handle_event() e despacha. Um loop de host que não faz nada além de atender eventos se parece com:

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

    while True:
        cam.poll_events()

A cam captura, decide e dispara eventos. O host fica dentro de poll_events() até que um chegue, e então on_motion é executado. Nenhuma chamada de read_status() é feita quando nada aconteceu, e nenhum quadro é puxado pelo USB quando a cam não tem nada a reportar.