10.8. WebSocketによる双方向制御

Server-Sent Eventsはプッシュ専用です。所有者がダッシュボードで「今すぐスナップショットを保存」や「トリガーカウンターをリセット」をタップすると、ダッシュボードはカメラメッセージを送信しなければなりません。それがWebSocketです。1つのTCPソケットで、フレーム化されたメッセージが双方向に流れます。

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シリアライズ可能な任意のものを受け付けます。辞書はJSONテキストフレームとして送信されます。

WebSocketError は、クライアントが切断したとき(正常なクローズ、ネットワーク切断、またはプロトコルエラー)に送出されます。ハンドラはループを抜けて戻り、microdotがソケットを後始末します。

10.8.2. ダッシュボードからコマンドを送信する

スライダーの隣に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とWebSocketのどちらを選ぶか

カメラがプッシュし、ブラウザは受信するだけの場合はSSEを使います。通知、テレメトリ、ステータス変更などです。通信は素のHTTPで、クライアント側は1行(new EventSource)で済み、再接続は自動です。

ブラウザもプッシュする必要がある場合はWebSocketを使います。ボタン、キー入力レートで更新を送るスライダー、次のリクエストを待っていては遅すぎるものなどです。接続は双方向でフレーム化されますが、APIは両側でより複雑になります。

カメラは今やインタラクティブなものになりました。監視し、イベントを外へプッシュし、コマンドを受け付けます。