10.8. Zweiwege-Steuerung mit WebSockets

Server-Sent Events sind reine Push-Ereignisse. Wenn der Besitzer im Dashboard auf „jetzt sofort einen Schnappschuss speichern“ oder „den Auslösezähler zurücksetzen“ tippt, muss das Dashboard eine Nachricht an die Kamera senden. Das sind WebSockets – ein TCP-Socket, gerahmte Nachrichten, die in beide Richtungen fließen.

10.8.1. Die /control-Route

microdot.websocket.with_websocket() führt den WebSocket-Upgrade-Handshake durch und übergibt dem Handler ein WebSocket-Objekt. Der Handler läuft endlos in einer Schleife, liest Befehle und sendet Bestätigungen:

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() gibt einen String für Text-Frames und Bytes für Binär-Frames zurück. Das browserseitige WebSocket.send(...) sendet standardmäßig Text, daher sind JSON-kodierte Befehle die naheliegende Wahl.

send() akzeptiert Strings, Bytes oder alles JSON-Serialisierbare – ein Dict wird als JSON-Text-Frame gesendet.

WebSocketError wird ausgelöst, wenn der Client die Verbindung trennt (sauberes Schließen, Netzwerkabbruch oder Protokollfehler). Der Handler verlässt die Schleife und kehrt zurück; microdot räumt den Socket auf.

10.8.2. Das Dashboard sendet Befehle

Zwei Schaltflächen kommen neben den Schieberegler in index.html:

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

und app.js öffnet den WebSocket einmal und verdrahtet beide Schaltflächen mit 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));
});

Die Wahl zwischen ws:// und wss:// entspricht der zwischen http:// und https:// – der WebSocket erbt dieselbe TLS-Behandlung. Mit aktiviertem HTTPS verbindet sich das Dashboard automatisch über wss://.

10.8.3. Wann SSE und wann WebSockets wählen

Verwenden Sie SSE, wenn die Kamera sendet und der Browser nur zuhört – Benachrichtigungen, Telemetrie, Statusänderungen. Die Leitung ist einfaches HTTP, die Clientseite besteht aus einer Zeile (new EventSource), und die Wiederverbindung erfolgt automatisch.

Verwenden Sie WebSockets, wenn der Browser ebenfalls senden muss – Schaltflächen, Schieberegler, die Updates in Tastendruckgeschwindigkeit senden, alles, bei dem das Warten auf die nächste Anfrage zu langsam wäre. Die Verbindung ist bidirektional und gerahmt, aber die API ist auf beiden Seiten aufwendiger.

Die Kamera ist jetzt ein interaktives Ding – beobachten, Ereignisse hinausschieben, Befehle entgegennehmen.