10.7. Odesílání událostí na dashboard

Dashboardu je třeba dát vědět ve chvíli, kdy se spustí pohyb – ne až při dalším dotazování. To je přesně to, co dělají Server-Sent Events: jedno HTTP spojení z prohlížeče do kamery, a kamera jím odesílá události dolů, kdykoli nastanou.

10.7.1. Korutina detektoru pohybu

Třetí úloha nejvyšší úrovně běží vedle snímací smyčky a HTTP serveru. Čeká na každý nový snímek, spustí rozdíl proti předchozímu snímku a – když je změna nad nakonfigurovaným prahem – zvýší počítadlo a signalizuje událost:

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)

Implementace compute_change je mimo rozsah této kapitoly – sekce o zpracování obrazu se rozdílovému porovnávání snímků věnuje pořádně. Prozatím s ní zacházejte jako se zástupným kódem, který vrací číslo.

Přidejte novou úlohu do main:

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

10.7.2. Cesta /events

microdot.sse.with_sse() dekoruje asynchronní handler tak, aby microdot provedl SSE handshake (stav 200, Content-Type: text/event-stream, žádné bufferování) a předal handleru objekt SSE. Handler zůstává vzhůru tak dlouho, dokud prohlížeč udržuje spojení otevřené:

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() zapíše jednu událost na drát a předá řízení zpět smyčce událostí. event='motion' pojmenuje typ události, takže EventSource na straně prohlížeče může zaregistrovat posluchače právě pro toto jméno. event_id= (není zde zobrazeno) nastaví řádek id:, takže prohlížeč může při opětovném připojení pokračovat ze známého offsetu pomocí hlavičky Last-Event-ID.

Patnáctisekundový timeout + odeslání comment=True je trik pro udržení spojení (keep-alive). Řádky komentářů začínají dvojtečkou : a prohlížeč je zcela ignoruje, ale bajty putující po drátě brání mezilehlým proxy a NAT zařízením v ukončení nečinného spojení.

10.7.3. Dashboard konzumuje události

Přidejte toto do 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);
});

Prohlížeč otevře jedno trvalé HTTP spojení na /events a při jakémkoli odpojení jej automaticky znovu otevře. Každá událost motion, kterou kamera odešle, se objeví jako nový <li> na vrcholu seznamu.

Majitel nyní vidí události pohybu ve chvíli, kdy se spustí.