10.7. Mendorong acara ke dasbor#

Dasbor perlu diberi tahu pada saat gerakan terdeteksi -- tidak pada polling berikutnya. Itulah yang dilakukan Server-Sent Events: satu koneksi HTTP dari browser ke kamera, dan kamera mendorong acara ke bawahnya kapan pun terjadi.

10.7.1. Coroutine detektor gerakan#

Tugas tingkat atas ketiga berjalan bersama loop pengambilan dan server HTTP. Ia menunggu setiap bingkai baru, menjalankan perbedaan terhadap bingkai sebelumnya, dan -- ketika perubahan berada di atas ambang batas yang dikonfigurasi -- menaikkan penghitung dan memberi sinyal suatu acara:

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)

Implementasi compute_change berada di luar cakupan bab ini -- bagian pemrosesan citra mencakup perbedaan bingkai dengan benar. Untuk sekarang anggap saja sebagai placeholder yang mengembalikan sebuah angka.

Tambahkan tugas baru ke main:

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

10.7.2. Rute /events#

microdot.sse.with_sse() mendekorasi handler async sehingga microdot melakukan handshake SSE (status 200, Content-Type: text/event-stream, tanpa buffering) dan memberikan handler sebuah objek SSE. Handler tetap aktif selama browser menjaga koneksi tetap terbuka:

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() menulis satu acara ke kabel dan menghasilkan kembali ke event loop. event='motion' memberi nama tipe acara sehingga EventSource sisi browser dapat mendaftarkan listener hanya untuk nama itu. event_id= (tidak ditampilkan) mengatur baris id: sehingga browser dapat melanjutkan dari offset yang diketahui saat koneksi ulang melalui header Last-Event-ID.

Timeout 15 detik + pengiriman comment=True adalah trik keep-alive. Baris komentar dimulai dengan : dan browser mengabaikannya sepenuhnya, tetapi byte yang bergerak melalui kabel mencegah proxy perantara dan kotak NAT dari memutus koneksi yang diam.

10.7.3. Dasbor mengonsumsi acara#

Tambahkan ini ke 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);
});

Browser membuka satu koneksi HTTP persisten ke /events dan membukanya kembali secara otomatis pada setiap pemutusan sambungan. Setiap acara motion yang didorong kamera muncul sebagai <li> baru di bagian atas daftar.

Pemilik sekarang melihat acara gerakan pada saat mereka terjadi.