10.7. Trimiterea evenimentelor către tabloul de bord

Tabloul de bord trebuie informat în momentul în care se declanșează mișcarea – nu la următoarea interogare. Exact asta fac Server-Sent Events: o singură conexiune HTTP de la browser la cameră, iar camera trimite evenimente pe ea ori de câte ori apar.

10.7.1. O coroutină de detectare a mișcării

O a treia sarcină de nivel superior rulează alături de bucla de captură și de serverul HTTP. Așteaptă fiecare cadru nou, rulează o diferență față de cadrul anterior și – când modificarea este peste pragul configurat – incrementează un contor și semnalează un eveniment:

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)

Implementarea compute_change este în afara domeniului acestui capitol – secțiunea de procesare a imaginilor acoperă corespunzător diferențierea cadrelor. Deocamdată trateaz-o ca pe un substituent care returnează un număr.

Adaugă noua sarcină în main:

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

10.7.2. Ruta /events

microdot.sse.with_sse() decorează un handler asincron astfel încât microdot efectuează negocierea SSE (status 200, Content-Type: text/event-stream, fără tamponare) și transmite handlerului un obiect SSE. Handlerul rămâne activ atâta timp cât browserul menține conexiunea deschisă:

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() scrie un eveniment pe transport și cedează controlul înapoi buclei de evenimente. event='motion' denumește tipul evenimentului, astfel încât EventSource din browser să poată înregistra un ascultător doar pentru acel nume. event_id= (neafișat) setează linia id: astfel încât browserul să poată relua de la un offset cunoscut la reconectare prin anteturul Last-Event-ID.

Expirarea de 15 secunde + trimiterea comment=True reprezintă trucul de menținere a conexiunii (keep-alive). Liniile de comentariu încep cu : și browserul le ignoră complet, dar octeții care circulă pe transport împiedică proxy-urile intermediare și casetele NAT să închidă o conexiune inactivă.

10.7.3. Tabloul de bord consumă evenimentele

Adaugă asta în 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);
});

Browserul deschide o singură conexiune HTTP persistentă către /events și o redeschide automat la orice deconectare. Fiecare eveniment motion pe care camera îl trimite apare ca un nou <li> în partea de sus a listei.

Proprietarul vede acum evenimentele de mișcare în clipa în care se declanșează.