10.8. Controle bidirecional com WebSockets¶
Os Server-Sent Events são apenas de envio. Quando o proprietário toca em “salvar um snapshot agora” ou “redefinir o contador de disparos” no painel, o painel tem que enviar uma mensagem para a câmera. Isso são os WebSockets – um socket TCP, mensagens enquadradas fluindo em ambas as direções.
10.8.1. A rota /control¶
microdot.websocket.with_websocket() executa o handshake de upgrade do WebSocket e entrega ao handler um objeto WebSocket. O handler entra em loop 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() retorna uma string para quadros de texto e bytes para quadros binários. O WebSocket.send(...) do lado do navegador envia texto por padrão, então comandos codificados em JSON são a escolha natural.
send() aceita strings, bytes ou qualquer coisa serializável em JSON – um dict é enviado como um quadro de texto JSON.
WebSocketError é lançada quando o cliente se desconecta (fechamento limpo, queda de rede ou erro de protocolo). O handler sai do loop e retorna; o microdot limpa o socket.
10.8.2. O painel envia comandos¶
Dois botões vão para o index.html ao lado do slider:
<button id="snap-btn">Save snapshot</button>
<button id="reset-btn">Reset counter</button>
e o app.js abre o WebSocket uma vez e conecta 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 http:// versus https:// – o WebSocket herda o mesmo tratamento de TLS. Com HTTPS no lugar, o painel se conecta automaticamente via wss://.
10.8.3. Quando escolher SSE versus WebSockets¶
Use SSE quando a câmera envia e o navegador apenas escuta – notificações, telemetria, mudanças de status. O transporte é HTTP simples, o lado do cliente é uma linha (new EventSource) e a reconexão é automática.
Use WebSockets quando o navegador também precisa enviar – botões, sliders que enviam atualizações na velocidade de digitação, qualquer coisa em que esperar pela próxima requisição seria lento demais. A conexão é bidirecional e enquadrada, mas a API é mais elaborada em ambos os lados.
A câmera agora é algo interativo – observa, envia eventos para fora e aceita comandos de entrada.