10.8. Controlo bidirecional com WebSockets

Server-Sent Events são apenas de envio. Quando o proprietário toca em «guardar uma captura de imagem agora» ou «repor o contador de ativações» no painel de controlo, o painel tem de enviar uma mensagem para a câmara. Para isso servem os WebSockets – uma única socket TCP, com mensagens enquadradas a fluir em ambas as direções.

10.8.1. A rota /control

microdot.websocket.with_websocket() realiza o handshake de atualização para WebSocket e entrega ao handler um objeto WebSocket. O handler fica em ciclo indefinidamente, lendo comandos e enviando confirmações:

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() devolve uma string para frames de texto e bytes para frames binárias. O WebSocket.send(...) do lado do browser envia texto por defeito, pelo que os comandos codificados em JSON são a escolha natural.

send() aceita strings, bytes, ou qualquer coisa serializável em JSON – um dicionário é enviado como uma frame de texto JSON.

WebSocketError é lançado quando o cliente se desliga (fecho limpo, queda de rede, ou erro de protocolo). O handler sai do ciclo e retorna; o microdot trata da socket.

10.8.2. O painel de controlo envia comandos

Dois botões são adicionados ao index.html junto ao slider:

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

e app.js abre o WebSocket uma vez e liga ambos os botões ao 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));
});

A escolha entre ws:// e wss:// espelha a de http:// vs https:// – o WebSocket herda o mesmo tratamento TLS. Com HTTPS configurado, o painel de controlo liga-se automaticamente via wss://.

10.8.3. Quando usar SSE versus WebSockets

Use SSE quando a câmara envia e o browser apenas recebe – notificações, telemetria, alterações de estado. O protocolo é HTTP simples, o lado do cliente é uma linha (new EventSource), e a reconexão é automática.

Use WebSockets quando o browser também precisa de enviar – botões, sliders que enviam atualizações à taxa de tecla premida, qualquer coisa em que aguardar o próximo pedido seria demasiado lento. A ligação é bidirecional e enquadrada, mas a API é mais complexa em ambos os lados.

A câmara é agora um objeto interativo – vigiar, enviar eventos, aceitar comandos.