13.3.1.5. Gebeurtenissen

De pagina’s tot nu toe roepen de cam aan: een script uploaden, een frame lezen, naar een kanaal schrijven. Elk van die operaties wordt door de host geïnitieerd – de host vraagt, de cam antwoordt. Het protocol werkt ook in de andere richting. De cam kan gebeurtenissen naar de host duwen zonder daarom gevraagd te worden, en de host-SDK levert elke gebeurtenis af aan een callback die de applicatie kan overschrijven.

Dit is het juiste hulpmiddel wanneer de applicatie wil reageren op iets dat de cam heeft opgemerkt voordat het ernaar vraagt. Zonder gebeurtenissen is de enige manier om erachter te komen het herhaaldelijk aanroepen van read_status() in een lus.

13.3.1.5.1. De standaard-callback

Camera abonneert zich intern al op gebeurtenissen. _handle_event() is de callback die het transport uitvoert telkens wanneer er een gebeurtenispakket binnenkomt. De standaardversie handelt drie systeemgebeurtenissen af:

  • CHANNEL_REGISTERED – een nieuw kanaal verscheen op de cam nadat de host verbinding had gemaakt. Het framework vernieuwt zijn kanaalcache zodat de volgende has_channel() opzoeking het vindt.

  • CHANNEL_UNREGISTERED – een kanaal verdween.

  • SOFT_REBOOT – de cam herstartte uit zichzelf (watchdog, hard fault, opzettelijke machine.reset()).

Het volgt ook de frame-ready-gebeurtenis van het stream kanaal voor het streamingpad en de start / stop van het script van het stdin kanaal voor stdout-buffering. De events=True standaard van de constructor houdt dit allemaal ingeschakeld; een applicatie die er niets van wil, kan events=False doorgeven aan Camera en het gebeurtenissubsysteem blijft stil.

13.3.1.5.2. Subclassen om te reageren

Om applicatiespecifieke gebeurtenissen die de cam veroorzaakt af te handelen, maak je een subclass van Camera en overschrijf je _handle_event(). Roep eerst de ouder aan om het standaardgedrag te behouden en verdeel daarna de gebeurtenissen waar de applicatie om geeft:

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

De signatuur is (channel_id, event). channel_id is 0 voor systeemgebeurtenissen en anders de numerieke ID van het kanaal dat ze veroorzaakte; event is een geheel getal dat het cam-zijdige script heeft gekozen. De EventType enum geeft namen aan de drie systeemgebeurtenissen; kanaalgebeurtenissen gebruiken welke waarden de cam-zijdige backend ook definieert.

Kanaalgebeurtenissen komen terug gerangschikt op numerieke ID, niet op naam. De gecachete channels_by_id dict is wat de bovenstaande override gebruikt om de naam op te zoeken; channels_by_name is de spiegel daarvan, andersom gerangschikt.

13.3.1.5.3. De cam-zijdige helft

Het cam-zijdige script veroorzaakt een gebeurtenis door send_event() aan te roepen op de handle die door protocol.register() wordt teruggegeven:

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)

Het gebeurtenisnummer is een geheel getal dat door de applicatie wordt gekozen. Elke waarde die de override van de host bereid is af te handelen is geldig; de protocollaag behandelt het als ondoorzichtige payload. Standaard vuurt de aanroep af en vergeet hij; geef wait_ack=True door om te blokkeren tot de host bevestigt, wanneer het weten dat de gebeurtenis is aangekomen belangrijker is dan de latentie van de heen-en-terugreis.

Een kanaal dat alleen gebeurtenissen afvuurt en geen leesbare data draagt is een geldig patroon – size geeft 0 terug en read geeft lege bytes terug. De protocolbibliotheek heeft nog steeds beide methoden nodig om het kanaal als leesbaar te markeren; het cam-zijdige script stopt er gewoon nooit data in.

13.3.1.5.4. Het ontvangstpad aansturen tijdens inactiviteit

Gebeurtenissen arriveren op dezelfde verbinding als al het andere, dus elke hostaanroep die bytes verzendt of ontvangt geeft het transport de kans om in behandeling zijnde gebeurtenissen inline te verwerken. Een peilende lus die al eenmaal per cyclus read_status() of read_frame() aanroept heeft niets extra’s nodig.

Voor programma’s die minuten zonder andere I/O doorgaan, voert poll_events() het ontvangstpad eenmaal uit zonder een commando te verzenden. Het keert terug zodra de inkomende buffer leeg is, dus een strakke lus eromheen – of een korte timer in een GUI-gebeurtenislus – is wat de handlers reactief houdt.

13.3.1.5.5. Een complete lus

Van begin tot eind is het patroon: het cam-zijdige script registreert een kanaal en roept send_event() aan wanneer er iets gebeurt; de host-zijdige subclass overschrijft _handle_event() en verdeelt. Een hostlus die niets anders doet dan gebeurtenissen bedienen ziet er zo uit:

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

    while True:
        cam.poll_events()

De cam legt vast, beslist en veroorzaakt gebeurtenissen. De host blijft binnen poll_events() tot er een arriveert, waarna on_motion draait. Er wordt geen read_status() aanroep uitgevoerd wanneer er niets is gebeurd, en er wordt geen frame over USB opgehaald wanneer de cam niets te melden heeft.