10.7. دفع الأحداث إلى لوحة المعلومات¶
تحتاج لوحة المعلومات إلى إخبارها لحظة اكتشاف الحركة -- لا عند الاستطلاع التالي. وهذا ما تؤديه الأحداث المُرسَلة من الخادم (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 خارج نطاق هذا الفصل -- إذ يغطي قسم معالجة الصور المقارنة بين الإطارات (frame-differencing) كما ينبغي. أما الآن فاعتبره عنصرًا نائبًا يُعيد عددًا.
أضف المهمة الجديدة إلى 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، دون تخزين مؤقت) ويمرّر إلى المعالج كائن 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 هي حيلة إبقاء الاتصال حيًّا. فأسطر التعليقات تبدأ بـ : ويتجاهلها المتصفح تمامًا، لكن البايتات المتحركة عبر السلك تمنع الوسطاء (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> جديدًا في أعلى القائمة.
يرى المالك الآن أحداث الحركة لحظة وقوعها.