10.8. Dwukierunkowe sterowanie za pomocą WebSockets

Server-Sent Events są tylko wypychające. Gdy właściciel naciśnie na panelu „zapisz zrzut obrazu teraz” lub „zresetuj licznik wyzwoleń”, panel musi wysłać wiadomość do kamery. To właśnie WebSockets – jedno gniazdo TCP, ramkowane wiadomości płynące w obu kierunkach.

10.8.1. Trasa /control

microdot.websocket.with_websocket() wykonuje uściskanie dłoni uaktualniające do WebSocket i przekazuje handlerowi obiekt WebSocket. Handler działa w nieskończonej pętli, odczytując polecenia i wysyłając potwierdzenia:

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() zwraca łańcuch znaków dla ramek tekstowych i bajty dla ramek binarnych. Po stronie przeglądarki WebSocket.send(...) domyślnie wysyła tekst, więc polecenia zakodowane w JSON są naturalnym wyborem.

send() przyjmuje łańcuchy znaków, bajty lub dowolny obiekt serializowalny do JSON – słownik jest wysyłany jako tekstowa ramka JSON.

WebSocketError jest zgłaszany, gdy klient się rozłączy (czyste zamknięcie, utrata sieci lub błąd protokołu). Handler wychodzi z pętli i wraca; microdot porządkuje gniazdo.

10.8.2. Panel wysyła polecenia

Do index.html obok suwaka trafiają dwa przyciski:

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

a app.js raz otwiera WebSocket i podpina oba przyciski do 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));
});

Wybór między ws:// a wss:// odzwierciedla wybór między http:// a https:// – WebSocket dziedziczy tę samą obsługę TLS. Gdy HTTPS jest na miejscu, panel automatycznie łączy się przez wss://.

10.8.3. Kiedy wybrać SSE, a kiedy WebSockets

Użyj SSE, gdy kamera wypycha dane, a przeglądarka tylko nasłuchuje – powiadomienia, telemetria, zmiany stanu. Transport to zwykły HTTP, strona kliencka to jedna linia (new EventSource), a ponowne łączenie jest automatyczne.

Użyj WebSockets, gdy przeglądarka również musi wypychać dane – przyciski, suwaki wysyłające aktualizacje z częstotliwością naciśnięć klawiszy, wszystko, gdzie czekanie na kolejne żądanie byłoby zbyt wolne. Połączenie jest dwukierunkowe i ramkowane, ale API jest bardziej rozbudowane po obu stronach.

Kamera jest teraz interaktywna – obserwuje, wypycha zdarzenia na zewnątrz i przyjmuje polecenia.