10.8. 以 WebSockets 進行雙向控制

Server-Sent Events 是僅供推播的。當擁有者在儀表板上點選「立即儲存一張快照」或「重設觸發計數器」時,儀表板必須送出一則訊息 相機。這就是 WebSockets——一個 TCP socket,兩個方向皆有框架化訊息流動。

10.8.1. /control 路由

microdot.websocket.with_websocket() 會執行 WebSocket 升級交握,並把一個 WebSocket 物件交給處理常式。處理常式會永遠循環,讀取指令並送出確認訊息:

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() 對文字框架回傳字串,對二進位框架回傳位元組。瀏覽器端的 WebSocket.send(...) 預設送出文字,因此以 JSON 編碼的指令是自然的選擇。

send() 接受字串、位元組,或任何可序列化為 JSON 的物件——一個 dict 會以 JSON 文字框架送出。

WebSocketError 會在用戶端中斷連線時拋出(正常關閉、網路斷線或協定錯誤)。處理常式會跳出迴圈並回傳;microdot 則會收拾這個 socket。

10.8.2. 儀表板送出指令

兩個按鈕放進 index.html 中、緊鄰滑桿:

<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——通知、遙測、狀態變更。傳輸線路是純粹的 HTTP,用戶端只需一行(new EventSource),且會自動重新連線。

當瀏覽器也需要推播時,請使用 WebSockets——按鈕、會以按鍵速率送出更新的滑桿,或任何等待下一次請求會太慢的場景。連線是雙向且框架化的,但兩端的 API 都較為繁複。

相機現在是一個可互動的裝置了——觀看、向外推播事件、接收進來的指令。