10.8. Tvåvägsstyrning med WebSockets

Server-Sent Events är endast push. När ägaren trycker på ”spara en stillbild just nu” eller ”nollställ utlösningsräknaren” på instrumentpanelen måste instrumentpanelen skicka ett meddelande till kameran. Det är WebSockets – en TCP-socket, inramade meddelanden som flödar åt båda hållen.

10.8.1. Rutten /control

microdot.websocket.with_websocket() utför WebSocket-uppgraderingshandskakningen och överlämnar ett WebSocket-objekt till hanteraren. Hanteraren loopar i all evighet, läser kommandon och skickar bekräftelser:

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() returnerar en sträng för textramar och byte för binära ramar. Webbläsarsidans WebSocket.send(...) skickar text som standard, så JSON-kodade kommandon är det naturliga valet.

send() accepterar strängar, byte eller vad som helst som är JSON-serialiserbart – en dict skickas som en JSON-textram.

WebSocketError genereras när klienten kopplar från (ren stängning, nätverksavbrott eller protokollfel). Hanteraren lämnar slingan och returnerar; microdot städar upp socketen.

10.8.2. Instrumentpanelen skickar kommandon

Två knappar läggs till i index.html bredvid reglaget:

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

och app.js öppnar WebSocketen en gång och kopplar båda knapparna till 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));
});

Valet mellan ws:// och wss:// speglar http:// mot https:// – WebSocketen ärver samma TLS-hantering. Med HTTPS på plats ansluter instrumentpanelen automatiskt via wss://.

10.8.3. När man väljer SSE kontra WebSockets

Använd SSE när kameran skickar och webbläsaren bara lyssnar – aviseringar, telemetri, statusändringar. Tråden är vanlig HTTP, klientsidan är en rad (new EventSource) och återanslutning sker automatiskt.

Använd WebSockets när webbläsaren också behöver skicka – knappar, reglage som skickar uppdateringar i tangenttryckningstakt, allt där väntan på nästa förfrågan vore för långsam. Anslutningen är dubbelriktad och inramad, men API:et är mer omfattande på båda sidor.

Kameran är nu en interaktiv sak – titta, skicka ut händelser, ta emot kommandon.