10.7. Események küldése az irányítópultra

Az irányítópultnak abban a pillanatban kell értesülnie, amint a mozgás aktiválódik – nem a következő lekérdezéskor. Ezt teszik a Server-Sent Events: egyetlen HTTP-kapcsolat a böngészőtől a kameráig, és a kamera lefelé küldi rajta az eseményeket, amikor csak megtörténnek.

10.7.1. Egy mozgásérzékelő korutin

Egy harmadik legfelső szintű feladat fut a felvételi ciklus és a HTTP-szerver mellett. Minden új képkockára vár, különbséget számol az előző képkockához képest, és – amikor a változás a beállított küszöbérték felett van – növel egy számlálót, és eseményt jelez:

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)

A compute_change megvalósítása túlmutat ennek a fejezetnek a hatókörén – a képfeldolgozási szakasz tárgyalja megfelelően a képkocka-különbségképzést. Egyelőre kezeld helykitöltőként, amely egy számot ad vissza.

Add hozzá az új feladatot a main függvényhez:

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

10.7.2. A /events útvonal

A microdot.sse.with_sse() úgy dekorál egy async kezelőt, hogy a microdot elvégezze az SSE-kézfogást (200-as állapot, Content-Type: text/event-stream, nincs pufferelés), és átad a kezelőnek egy SSE objektumot. A kezelő addig marad ébren, ameddig a böngésző nyitva tartja a kapcsolatot:

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)

A send() egy eseményt ír a vezetékre, és visszaadja a vezérlést az eseményhuroknak. Az event='motion' megnevezi az eseménytípust, hogy a böngészőoldali EventSource figyelőt regisztrálhasson éppen arra a névre. Az event_id= (nem látható) beállítja az id: sort, hogy a böngésző egy ismert eltolásról folytathassa az újrakapcsolódáskor a Last-Event-ID fejléc révén.

A 15 másodperces időtúllépés + comment=True küldés a keep-alive trükk. A megjegyzéssorok : karakterrel kezdődnek, és a böngésző teljesen figyelmen kívül hagyja őket, de a vezetéken mozgó bájtok megakadályozzák, hogy a köztes proxyk és NAT-dobozok megöljenek egy tétlen kapcsolatot.

10.7.3. Az irányítópult fogyasztja az eseményeket

Add hozzá ezt az app.js fájlhoz:

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);
});

A böngésző egy állandó HTTP-kapcsolatot nyit a /events útvonalra, és bármilyen lekapcsolódáskor automatikusan újranyitja. Minden motion esemény, amelyet a kamera küld, új <li> elemként jelenik meg a lista tetején.

A tulajdonos most már abban a pillanatban látja a mozgásészlelési eseményeket, amint aktiválódnak.