10.7. Invio di eventi in push alla dashboard¶
La dashboard deve essere avvisata nell’istante in cui viene rilevato del movimento, non al polling successivo. È proprio ciò che fanno i Server-Sent Events: una connessione HTTP dal browser alla camera, e la camera vi invia gli eventi in push ogni volta che si verificano.
10.7.1. Una coroutine per il rilevatore di movimento¶
Un terzo task di primo livello viene eseguito insieme al ciclo di acquisizione e al server HTTP. Attende ogni nuovo frame, calcola la differenza rispetto al frame precedente e – quando la variazione supera la soglia configurata – incrementa un contatore e segnala un evento:
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)
L’implementazione di compute_change esula dall’ambito di questo capitolo: la sezione sull’elaborazione delle immagini tratta in modo approfondito la differenza tra frame. Per ora consideralo un segnaposto che restituisce un numero.
Aggiungi il nuovo task a main:
async def main():
await asyncio.gather(
capture_loop(),
motion_detector(),
app.start_server(host='0.0.0.0', port=80),
)
10.7.2. La route /events¶
microdot.sse.with_sse() decora un handler asincrono in modo che microdot esegua l’handshake SSE (stato 200, Content-Type: text/event-stream, nessun buffering) e passi all’handler un oggetto SSE. L’handler resta attivo finché il browser mantiene aperta la connessione:
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() scrive un evento sul canale e restituisce il controllo all’event loop. event='motion' assegna un nome al tipo di evento, così che l”EventSource lato browser possa registrare un listener solo per quel nome. event_id= (non mostrato) imposta la riga id: in modo che il browser possa riprendere da un offset noto alla riconnessione tramite l’header Last-Event-ID.
Il timeout di 15 secondi più l’invio di comment=True è il trucco del keep-alive. Le righe di commento iniziano con : e il browser le ignora completamente, ma i byte che transitano sul canale impediscono ai proxy intermedi e ai box NAT di interrompere una connessione inattiva.
10.7.3. La dashboard consuma gli eventi¶
Aggiungi questo ad 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);
});
Il browser apre un’unica connessione HTTP persistente verso /events e la riapre automaticamente a ogni disconnessione. Ogni evento motion che la camera invia in push compare come un nuovo <li> in cima all’elenco.
Il proprietario ora vede gli eventi di movimento nell’istante stesso in cui si verificano.