10.7. การส่ง event ไปยังแดชบอร์ด

แดชบอร์ดต้องได้รับแจ้งทันทีที่ตรวจพบการเคลื่อนไหว — ไม่ใช่รอ poll ถัดไป นั่นคือสิ่งที่ Server-Sent Events ทำ: การเชื่อมต่อ HTTP จากเบราว์เซอร์ไปยังกล้องหนึ่งเส้นทาง และกล้องส่ง event ลงไปเมื่อเกิดขึ้น

10.7.1. coroutine ตรวจจับการเคลื่อนไหว

task ระดับบนสุดที่สามทำงานควบคู่กับ capture loop และ HTTP server มันรอทุกเฟรมใหม่ เรียกใช้การเปรียบเทียบกับเฟรมก่อนหน้า และเมื่อการเปลี่ยนแปลงเกินค่า threshold ที่กำหนด จะเพิ่มตัวนับและส่งสัญญาณ event:

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)

การ implement compute_change อยู่นอกขอบเขตของบทนี้ — ส่วนการประมวลผลภาพจะครอบคลุม frame-differencing อย่างละเอียด ตอนนี้ให้ถือว่าเป็น placeholder ที่คืนค่าตัวเลข

เพิ่ม task ใหม่ใน 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

microdot.sse.with_sse() ตกแต่ง async handler เพื่อให้ microdot ทำ SSE handshake (status 200, Content-Type: text/event-stream, ไม่มีการ buffer) และส่ง object SSE ให้กับ handler Handler จะคงการทำงานตราบเท่าที่เบราว์เซอร์ยังเปิดการเชื่อมต่ออยู่:

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() เขียน event หนึ่งรายการลงบนเส้นทางและ yield กลับไปยัง event loop event='motion' ตั้งชื่อประเภท event เพื่อให้ EventSource ฝั่งเบราว์เซอร์ลงทะเบียน listener เฉพาะชื่อนั้น event_id= (ไม่แสดง) ตั้งค่าบรรทัด id: เพื่อให้เบราว์เซอร์กลับมาต่อจาก offset ที่รู้จักเมื่อเชื่อมต่อใหม่ผ่าน header Last-Event-ID

15-second timeout + การส่ง comment=True คือ trick keep-alive บรรทัด comment เริ่มด้วย : และเบราว์เซอร์ไม่สนใจเลย แต่ bytes ที่เคลื่อนที่บนเส้นทางป้องกัน proxy กลางและ NAT box ไม่ให้ตัดการเชื่อมต่อที่ไม่มีการใช้งาน

10.7.3. แดชบอร์ดรับ event

เพิ่มโค้ดนี้ใน 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);
});

เบราว์เซอร์เปิดการเชื่อมต่อ HTTP แบบต่อเนื่องหนึ่งเส้นทางไปที่ /events และเปิดใหม่อัตโนมัติเมื่อตัดการเชื่อมต่อ ทุก motion event ที่กล้องส่งมาจะปรากฏเป็น <li> ใหม่ที่ด้านบนของรายการ

เจ้าของตอนนี้เห็น motion event ทันทีที่เกิดขึ้น