10.8. Controllo bidirezionale con i WebSocket

I Server-Sent Events sono solo in push. Quando il proprietario tocca «salva subito uno snapshot» o «azzera il contatore degli attivamenti» sulla dashboard, la dashboard deve inviare un messaggio alla camera. Questo è ciò che fanno i WebSocket: un unico socket TCP, con messaggi incapsulati che scorrono in entrambe le direzioni.

10.8.1. La route /control

microdot.websocket.with_websocket() esegue l’handshake di upgrade del WebSocket e passa all’handler un oggetto WebSocket. L’handler entra in un ciclo infinito, leggendo i comandi e inviando conferme:

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() restituisce una stringa per i frame di testo e byte per i frame binari. Il WebSocket.send(...) lato browser invia testo per impostazione predefinita, quindi i comandi codificati in JSON sono la scelta naturale.

send() accetta stringhe, byte o qualsiasi oggetto serializzabile in JSON: un dict viene inviato come frame di testo JSON.

WebSocketError viene sollevata quando il client si disconnette (chiusura pulita, caduta della rete o errore di protocollo). L’handler esce dal ciclo e ritorna; microdot si occupa di ripulire il socket.

10.8.2. La dashboard invia comandi

Due pulsanti vengono aggiunti in index.html accanto allo slider:

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

e app.js apre il WebSocket una sola volta e collega entrambi i pulsanti a 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));
});

La scelta tra ws:// e wss:// rispecchia quella tra http:// e https://: il WebSocket eredita la stessa gestione TLS. Con HTTPS attivo, la dashboard si connette automaticamente tramite wss://.

10.8.3. Quando scegliere SSE o WebSocket

Usa SSE quando la camera invia in push e il browser si limita ad ascoltare: notifiche, telemetria, cambiamenti di stato. Il canale è semplice HTTP, il lato client è una sola riga (new EventSource) e la riconnessione è automatica.

Usa i WebSocket quando anche il browser deve inviare in push: pulsanti, slider che inviano aggiornamenti alla velocità di battitura, qualunque cosa in cui attendere la richiesta successiva sarebbe troppo lento. La connessione è bidirezionale e incapsulata in frame, ma l’API è più articolata su entrambi i lati.

La camera è ora un dispositivo interattivo: osserva, invia eventi all’esterno e accetta comandi in ingresso.