10.7. Enviando eventos para o painel¶
O painel precisa ser informado no instante em que o movimento dispara – não na próxima sondagem. É isso que os Server-Sent Events fazem: uma conexão HTTP do navegador para a câmera, e a câmera envia eventos por ela sempre que acontecem.
10.7.1. Uma corrotina de detecção de movimento¶
Uma terceira tarefa de nível superior roda ao lado do loop de captura e do servidor HTTP. Ela espera cada novo quadro, executa uma diferença em relação ao quadro anterior e – quando a mudança está acima do limiar configurado – incrementa um contador e sinaliza um 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)
A implementação de compute_change está fora do escopo deste capítulo – a seção de processamento de imagem cobre a diferenciação de quadros adequadamente. Por enquanto, trate-a como um espaço reservado que retorna um número.
Adicione a nova tarefa ao main:
async def main():
await asyncio.gather(
capture_loop(),
motion_detector(),
app.start_server(host='0.0.0.0', port=80),
)
10.7.2. A rota /events¶
microdot.sse.with_sse() decora um handler assíncrono para que o microdot execute o handshake de SSE (status 200, Content-Type: text/event-stream, sem buffering) e entregue ao handler um objeto SSE. O handler permanece ativo enquanto o navegador mantiver a conexão aberta:
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() escreve um evento no transporte e devolve o controle ao loop de eventos. event='motion' nomeia o tipo de evento para que o EventSource do lado do navegador possa registrar um listener apenas para esse nome. event_id= (não mostrado) define a linha id: para que o navegador possa retomar a partir de um deslocamento conhecido na reconexão, via o cabeçalho Last-Event-ID.
O timeout de 15 segundos + o envio com comment=True é o truque de keep-alive. As linhas de comentário começam com : e o navegador as ignora completamente, mas os bytes trafegando pelo transporte impedem que proxies intermediários e caixas NAT matem uma conexão ociosa.
10.7.3. O painel consome eventos¶
Adicione isto ao 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);
});
O navegador abre uma conexão HTTP persistente para /events e a reabre automaticamente em qualquer desconexão. Cada evento motion que a câmera envia aparece como um novo <li> no topo da lista.
O proprietário agora vê os eventos de movimento no instante em que disparam.