10.7. Olayların panoya itilmesi

Panoya, bir sonraki yoklamada değil, hareketin tetiklendiği an haberdar edilmesi gerekir. Server-Sent Events tam olarak bunu yapar: tarayıcıdan kameraya tek bir HTTP bağlantısı ve kamera, gerçekleştiklerinde olayları bu bağlantı üzerinden aşağı iter.

10.7.1. Bir hareket dedektörü eş yordamı

Üçüncü bir üst düzey görev, yakalama döngüsü ve HTTP sunucusunun yanında çalışır. Her yeni çerçeveyi bekler, önceki çerçeveye karşı bir fark çalıştırır ve – değişiklik yapılandırılan eşiğin üzerinde olduğunda – bir sayacı artırır ve bir olay sinyali verir:

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 uygulaması bu bölümün kapsamı dışındadır; görüntü işleme bölümü çerçeve farkını gerektiği gibi ele alır. Şimdilik bunu bir sayı döndüren bir yer tutucu olarak değerlendirin.

Yeni görevi main öğesine ekleyin:

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

10.7.2. /events rotası

microdot.sse.with_sse(), bir eşzamansız işleyiciyi dekore eder, böylece microdot SSE el sıkışmasını gerçekleştirir (durum 200, Content-Type: text/event-stream, arabelleğe alma yok) ve işleyiciye bir SSE nesnesi verir. İşleyici, tarayıcı bağlantıyı açık tuttuğu sürece uyanık kalır:

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(), hatta bir olay yazar ve olay döngüsüne geri verir. event='motion', olay türünü adlandırır, böylece tarayıcı tarafındaki EventSource yalnızca o ad için bir dinleyici kaydedebilir. event_id= (gösterilmemiştir), id: satırını ayarlar, böylece tarayıcı Last-Event-ID başlığı aracılığıyla yeniden bağlanırken bilinen bir konumdan devam edebilir.

15 saniyelik zaman aşımı + comment=True gönderimi canlı tutma hilesidir. Yorum satırları : ile başlar ve tarayıcı bunları tamamen yok sayar, ancak hat üzerinde hareket eden baytlar, aradaki vekil sunucuların ve NAT kutularının boşta bir bağlantıyı sonlandırmasını engeller.

10.7.3. Pano olayları tüketir

Bunu app.js öğesine ekleyin:

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);
});

Tarayıcı, /events adresine tek bir kalıcı HTTP bağlantısı açar ve herhangi bir kopmada otomatik olarak yeniden açar. Kameranın ittiği her motion olayı, listenin en üstünde yeni bir <li> olarak görünür.

Sahibi artık hareket olaylarını tetiklendikleri anda görür.