10.7. Gebeurtenissen naar het dashboard pushen

Het dashboard moet op het moment dat beweging wordt gedetecteerd worden geïnformeerd – niet bij de volgende poll. Dat is wat Server-Sent Events doen: één HTTP-verbinding van de browser naar de cam, en de cam pusht er gebeurtenissen doorheen wanneer ze zich voordoen.

10.7.1. Een bewegingsdetector-coroutine

Een derde taak op het hoogste niveau draait naast de capture-lus en de HTTP-server. Hij wacht op elk nieuw frame, voert een verschil uit ten opzichte van het vorige frame en – wanneer de verandering boven de geconfigureerde drempelwaarde ligt – verhoogt een teller en signaleert een gebeurtenis:

import time

motion_event = asyncio.Event()
last_motion = None

async def motion_detector():
    global last_motion
    prev = None
    while True:
        await new_frame.wait()
        change = compute_change(prev, latest_jpeg)
        if change > state['threshold']:
            state['trigger_count'] += 1
            last_motion = {
                'ts': time.time(),
                'count': state['trigger_count'],
                'change': change,
            }
            motion_event.set()
        prev = latest_jpeg
        await asyncio.sleep_ms(50)

De implementatie van compute_change valt buiten de reikwijdte van dit hoofdstuk – de sectie over beeldverwerking behandelt frame-differencing grondig. Behandel het voorlopig als een placeholder die een getal teruggeeft.

Voeg de nieuwe taak toe aan main:

async def main():
    await asyncio.gather(
        capture_loop(),
        motion_detector(),
        app.start_server(host='0.0.0.0', port=80),
    )

10.7.2. De /events-route

microdot.sse.with_sse() decoreert een async-handler zodat microdot de SSE-handshake uitvoert (status 200, Content-Type: text/event-stream, geen buffering) en de handler een SSE-object aanreikt. De handler blijft wakker zolang de browser de verbinding open houdt:

from microdot.sse import with_sse

@app.get('/events')
@with_sse
async def events(request, sse):
    while True:
        try:
            await asyncio.wait_for(motion_event.wait(), timeout=15)
            motion_event.clear()
            if last_motion:
                await sse.send(last_motion, event='motion')
        except asyncio.TimeoutError:
            await sse.send('keepalive', comment=True)

send() schrijft één gebeurtenis naar de verbinding en geeft de controle terug aan de event loop. event='motion' benoemt het gebeurtenistype zodat de EventSource aan de browserzijde een luisteraar voor precies die naam kan registreren. event_id= (niet getoond) stelt de id:-regel in zodat de browser bij herverbinden kan hervatten vanaf een bekende offset via de Last-Event-ID-header.

De time-out van 15 seconden + de comment=True-verzending is de keep-alive-truc. Commentaarregels beginnen met : en de browser negeert ze volledig, maar de bytes die over de verbinding bewegen voorkomen dat tussenliggende proxy’s en NAT-boxen een inactieve verbinding afbreken.

10.7.3. Het dashboard verwerkt gebeurtenissen

Voeg dit toe aan app.js:

const events = document.getElementById('events');
const source = new EventSource('/events');
source.addEventListener('motion', (e) => {
    const data = JSON.parse(e.data);
    const li = document.createElement('li');
    const t = new Date(data.ts * 1000).toLocaleTimeString();
    li.textContent = t + ' -- change ' + data.change;
    events.prepend(li);
});

De browser opent één persistente HTTP-verbinding naar /events en heropent die automatisch bij elke verbreking. Elke motion-gebeurtenis die de cam pusht verschijnt als een nieuwe <li> bovenaan de lijst.

De eigenaar ziet bewegingsgebeurtenissen nu op het moment dat ze zich voordoen.