10.8. การควบคุมสองทิศทางด้วย WebSockets

Server-Sent Events เป็นการส่งข้อมูลทางเดียว เมื่อเจ้าของแตะ "บันทึกสแนปช็อตตอนนี้" หรือ "รีเซ็ตตัวนับ trigger" บนแดชบอร์ด แดชบอร์ดจะต้องส่งข้อความ ไปยัง กล้อง นั่นคือ WebSockets — TCP socket เดียวที่ข้อความไหลสองทิศทาง

10.8.1. เส้นทาง /control

microdot.websocket.with_websocket() ทำ WebSocket upgrade handshake และส่ง object WebSocket ให้กับ handler Handler จะวนซ้ำตลอดไปโดยอ่านคำสั่งและส่งการยืนยันกลับ:

from microdot.websocket import with_websocket
from microdot.websocket import WebSocketError
import json

@app.get('/control')
@with_websocket
async def control(request, ws):
    while True:
        try:
            msg = await ws.receive()
        except WebSocketError:
            break
        try:
            cmd = json.loads(msg)
        except ValueError:
            await ws.send({'error': 'bad json'})
            continue

        if cmd.get('cmd') == 'snapshot_now':
            if latest_jpeg:
                path = '/sdcard/snaps/manual-{}.jpg'.format(
                    int(time.time()))
                with open(path, 'wb') as f:
                    f.write(latest_jpeg)
                await ws.send({'ok': True, 'saved': path})
            else:
                await ws.send({'ok': False, 'reason': 'no frame yet'})
        elif cmd.get('cmd') == 'reset':
            state['trigger_count'] = 0
            await ws.send({'ok': True, 'counters': 'reset'})
        else:
            await ws.send({'error': 'unknown command'})

receive() คืนค่าเป็น string สำหรับ text frame และ bytes สำหรับ binary frame เบราว์เซอร์ส่ง WebSocket.send(...) เป็น text ตามค่าเริ่มต้น ดังนั้นคำสั่งที่เข้ารหัสเป็น JSON จึงเป็นตัวเลือกที่เหมาะสม

send() รับ string, bytes หรือสิ่งใดก็ตามที่ serialize เป็น JSON ได้ — dict จะถูกส่งเป็น JSON text frame

WebSocketError จะถูก raise เมื่อ client ตัดการเชื่อมต่อ (ปิดปกติ, เครือข่ายหลุด หรือ protocol error) handler จะออกจากลูปและ return กลับ microdot จัดการ socket ให้

10.8.2. แดชบอร์ดส่งคำสั่ง

เพิ่มปุ่มสองปุ่มใน index.html ถัดจาก slider:

<button id="snap-btn">Save snapshot</button>
<button id="reset-btn">Reset counter</button>

และ app.js เปิด WebSocket ครั้งเดียวและเชื่อมปุ่มทั้งสองกับ send:

const proto = location.protocol === 'https:' ? 'wss://' : 'ws://';
const ws = new WebSocket(proto + location.host + '/control');

document.getElementById('snap-btn').addEventListener('click', () =>
    ws.send(JSON.stringify({cmd: 'snapshot_now'})));
document.getElementById('reset-btn').addEventListener('click', () =>
    ws.send(JSON.stringify({cmd: 'reset'})));

ws.addEventListener('message', (e) => {
    console.log('cam:', JSON.parse(e.data));
});

การเลือกระหว่าง ws:// กับ wss:// สอดคล้องกับ http:// กับ https:// — WebSocket รับการจัดการ TLS แบบเดียวกัน เมื่อติดตั้ง HTTPS แล้ว แดชบอร์ดจะเชื่อมต่อผ่าน wss:// โดยอัตโนมัติ

10.8.3. เมื่อใดควรใช้ SSE กับ WebSockets

ใช้ SSE เมื่อกล้องส่งข้อมูลออกและเบราว์เซอร์รับฝ่ายเดียว — การแจ้งเตือน, telemetry, การเปลี่ยนสถานะ เส้นทางคือ HTTP ธรรมดา ฝั่ง client คือบรรทัดเดียว (new EventSource) และการเชื่อมต่อใหม่เป็นแบบอัตโนมัติ

ใช้ WebSockets เมื่อเบราว์เซอร์ต้องการส่งข้อมูลด้วย — ปุ่ม, slider ที่ส่งการอัปเดตทุกครั้งที่กด, สถานการณ์ใดๆ ที่การรอ request ถัดไปจะช้าเกินไป การเชื่อมต่อเป็นแบบสองทิศทางและมีการแบ่ง frame แต่ API ซับซ้อนกว่าทั้งสองฝั่ง

กล้องกลายเป็นสิ่งที่โต้ตอบได้แล้ว — เฝ้าดู ส่งเหตุการณ์ออก และรับคำสั่งเข้า