10.7. Tapahtumien työntäminen kojelaudalle

Kojelaudalle täytyy kertoa heti kun liike laukeaa – ei vasta seuraavalla kyselyllä. Juuri tätä Server-Sent Events tekevät: yksi HTTP-yhteys selaimesta kameralle, ja kamera työntää tapahtumia sitä pitkin aina kun niitä tapahtuu.

10.7.1. Liiketunnistinkorutiini

Kolmas ylätason tehtävä ajetaan kaappaussilmukan ja HTTP-palvelimen rinnalla. Se odottaa jokaista uutta kehystä, suorittaa eron edelliseen kehykseen ja – kun muutos on määritetyn kynnysarvon yläpuolella – kasvattaa laskuria ja signaloi tapahtuman:

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)

compute_change-toteutus on tämän luvun laajuuden ulkopuolella – kuvankäsittelyosio käsittelee kehysten erotuksen kunnolla. Toistaiseksi käsittele sitä paikanvaraajana, joka palauttaa luvun.

Lisää uusi tehtävä funktioon main:

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

10.7.2. /events-reitti

microdot.sse.with_sse() dekoroi asynkronisen käsittelijän niin, että microdot suorittaa SSE-kättelyn (status 200, Content-Type: text/event-stream, ei puskurointia) ja antaa käsittelijälle SSE-olion. Käsittelijä pysyy hereillä niin kauan kuin selain pitää yhteyden auki:

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() kirjoittaa yhden tapahtuman siirtotielle ja luovuttaa vuoron takaisin tapahtumasilmukalle. event='motion' nimeää tapahtumatyypin, jotta selainpuolen EventSource voi rekisteröidä kuuntelijan juuri sille nimelle. event_id= (ei näytetty) asettaa id:-rivin, jotta selain voi jatkaa tunnetusta kohdasta uudelleenyhdistettäessä Last-Event-ID-otsakkeen kautta.

15 sekunnin aikakatkaisu + comment=True-lähetys on keep-alive-temppu. Kommenttirivit alkavat merkillä : ja selain jättää ne kokonaan huomiotta, mutta siirtotiellä liikkuvat tavut estävät välissä olevia välityspalvelimia ja NAT-laatikoita tappamasta jouten olevaa yhteyttä.

10.7.3. Kojelauta kuluttaa tapahtumia

Lisää tämä tiedostoon 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);
});

Selain avaa yhden pysyvän HTTP-yhteyden reitille /events ja avaa sen uudelleen automaattisesti jokaisen yhteyskatkoksen yhteydessä. Jokainen motion-tapahtuma, jonka kamera työntää, ilmestyy uutena <li>-elementtinä listan yläosaan.

Omistaja näkee nyt liiketapahtumat sillä hetkellä kun ne laukeavat.