10.7. דחיפת אירועים אל לוח הבקרה

יש ליידע את לוח הבקרה ברגע שתנועה מופעלת – לא ב-poll הבא. זה מה ש-Server-Sent Events עושים: חיבור HTTP אחד מהדפדפן אל המצלמה, והמצלמה דוחפת בו אירועים מתי שהם קורים.

10.7.1. קורוטינה של מזהה תנועה

משימה שלישית ברמה העליונה רצה לצד לולאת הלכידה ושרת ה-HTTP. היא ממתינה לכל פריים חדש, מבצעת הפרש מול הפריים הקודם, וכאשר השינוי גבוה מהסף המוגדר – מקדמת מונה ומאותתת על אירוע:

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 מחוץ לתחום פרק זה – סעיף עיבוד התמונה מכסה הפרשי פריימים כראוי. לעת עתה התייחס אליו כ-placeholder המחזיר מספר.

הוסף את המשימה החדשה אל 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() מעטר מטפל אסינכרוני כך ש-microdot מבצע את לחיצת היד של SSE (סטטוס 200, Content-Type: text/event-stream, ללא buffering) ומוסר למטפל אובייקט SSE. המטפל נשאר ער כל עוד הדפדפן משאיר את החיבור פתוח:

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='motion' נותן שם לסוג האירוע כך ש-EventSource בצד הדפדפן יכול לרשום מאזין רק לשם הזה. event_id= (לא מוצג) מגדיר את השורה id: כך שהדפדפן יכול להמשיך מהיסט ידוע בעת התחברות מחדש דרך כותרת ה-Last-Event-ID.

פסק הזמן בן 15 השניות + שליחת comment=True הם טריק ה-keep-alive. שורות הערה מתחילות ב-: והדפדפן מתעלם מהן לחלוטין, אך הבייטים הנעים על החיווט מונעים מ-proxies ביניים ומקופסאות NAT להרוג חיבור לא פעיל.

10.7.3. לוח הבקרה צורך אירועים

הוסף את זה אל 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 שהמצלמה דוחפת מופיע כ-<li> חדש בראש הרשימה.

הבעלים כעת רואה אירועי תנועה ברגע שהם מופעלים.