10.7. Đẩy sự kiện lên bảng điều khiển¶
Bảng điều khiển cần được thông báo ngay lập tức khi phát hiện chuyển động -- không phải ở lần polling tiếp theo. Đó là điều Server-Sent Events làm: một kết nối HTTP từ trình duyệt đến camera, và camera đẩy các sự kiện xuống bất cứ khi nào chúng xảy ra.
10.7.1. Một coroutine phát hiện chuyển động¶
Một tác vụ cấp cao thứ ba chạy song song với vòng lặp chụp và máy chủ HTTP. Nó chờ mỗi khung hình mới, chạy so sánh với khung hình trước đó, và -- khi thay đổi vượt quá ngưỡng đã cấu hình -- tăng bộ đếm và báo hiệu một sự kiện:
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)
Việc triển khai compute_change nằm ngoài phạm vi của chương này -- phần xử lý ảnh đề cập đến phân tán khung hình đúng cách. Hiện tại hãy coi nó như một placeholder trả về một số.
Thêm tác vụ mới vào main:
async def main():
await asyncio.gather(
capture_loop(),
motion_detector(),
app.start_server(host='0.0.0.0', port=80),
)
10.7.2. Route /events¶
microdot.sse.with_sse() trang trí một handler async để microdot thực hiện quá trình bắt tay SSE (trạng thái 200, Content-Type: text/event-stream, không có buffering) và chuyển cho handler một đối tượng SSE. Handler thức dậy miễn là trình duyệt giữ kết nối mở:
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() ghi một sự kiện lên đường truyền và trả lại cho vòng lặp sự kiện. event='motion' đặt tên cho loại sự kiện để EventSource ở phía trình duyệt có thể đăng ký một listener chỉ cho tên đó. event_id= (không hiển thị) đặt dòng id: để trình duyệt có thể tiếp tục từ một offset đã biết khi kết nối lại thông qua header Last-Event-ID.
Gửi với timeout 15 giây + comment=True là thủ thuật keep-alive. Các dòng comment bắt đầu bằng : và trình duyệt bỏ qua chúng hoàn toàn, nhưng các byte chuyển qua đường truyền ngăn các proxy trung gian và hộp NAT không giết một kết nối nhàn rỗi.
10.7.3. Bảng điều khiển tiêu thụ sự kiện¶
Thêm vào 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);
});
Trình duyệt mở một kết nối HTTP liên tục đến /events và tự động mở lại khi có bất kỳ sự ngắt kết nối nào. Mỗi sự kiện motion mà camera đẩy xuất hiện như một <li> mới ở đầu danh sách.
Chủ nhân bây giờ thấy các sự kiện chuyển động ngay lập tức khi chúng kích hoạt.