10.7. Guranje događaja na nadzornu ploču

Nadzornoj ploči treba reći u trenutku kada se pokrene detekcija pokreta – ne pri sljedećem prozivanju. To je ono što rade Server-Sent Events: jedna HTTP veza od preglednika do kamere, a kamera gura događaje kroz nju kad god se dogode.

10.7.1. Korutina detektora pokreta

Treći zadatak najviše razine izvodi se uz petlju snimanja i HTTP poslužitelj. Čeka svaku novu sličicu, izvodi razliku u odnosu na prethodnu sličicu i – kada je promjena iznad konfiguriranog praga – povećava brojač i signalizira događaj:

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)

Implementacija compute_change izvan je opsega ovog poglavlja – odjeljak o obradi slika pravilno pokriva razlikovanje sličica. Za sada je tretirajte kao rezervirano mjesto koje vraća broj.

Dodajte novi zadatak u 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() dekorira asinkroni rukovatelj tako da microdot izvodi SSE rukovanje (status 200, Content-Type: text/event-stream, bez međuspremnika) i predaje rukovatelju SSE objekt. Rukovatelj ostaje budan sve dok preglednik drži vezu otvorenom:

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() zapisuje jedan događaj na vezu i predaje kontrolu natrag petlji događaja. event='motion' imenuje tip događaja tako da se EventSource na strani preglednika može registrirati za osluškivanje samo tog imena. event_id= (nije prikazano) postavlja redak id: tako da preglednik može nastaviti od poznatog odmaka pri ponovnom povezivanju putem zaglavlja Last-Event-ID.

Istek od 15 sekundi + slanje s comment=True trik je za održavanje veze. Komentarski reci počinju s : i preglednik ih u potpunosti ignorira, ali bajtovi koji se kreću preko veze sprječavaju posredne proxyje i NAT uređaje da prekinu neaktivnu vezu.

10.7.3. Nadzorna ploča troši događaje

Dodajte ovo u 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);
});

Preglednik otvara jednu trajnu HTTP vezu prema /events i automatski je ponovno otvara pri svakom prekidu. Svaki motion događaj koji kamera gurne pojavljuje se kao novi <li> na vrhu popisa.

Vlasnik sada vidi događaje pokreta u trenutku kada se pokrenu.