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.