10.8. Control bidireccional con WebSockets¶
Los eventos enviados por el servidor (Server-Sent Events) son solo de envío. Cuando el propietario toca «guardar una captura ahora mismo» o «reiniciar el contador de disparos» en el panel de control, el panel tiene que enviar un mensaje a la cámara. Eso son los WebSockets: un único socket TCP, con mensajes enmarcados fluyendo en ambas direcciones.
10.8.1. La ruta /control¶
microdot.websocket.with_websocket() realiza el apretón de manos de actualización a WebSocket y entrega al manejador un objeto WebSocket. El manejador entra en un bucle infinito, leyendo comandos y enviando confirmaciones:
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() devuelve una cadena para los marcos de texto y bytes para los marcos binarios. El WebSocket.send(...) del lado del navegador envía texto de forma predeterminada, así que los comandos codificados en JSON son la opción natural.
send() acepta cadenas, bytes o cualquier cosa serializable a JSON; un diccionario se envía como un marco de texto JSON.
WebSocketError se lanza cuando el cliente se desconecta (cierre limpio, caída de red o error de protocolo). El manejador sale del bucle y retorna; microdot limpia el socket.
10.8.2. El panel de control envía comandos¶
Dos botones van en index.html junto al deslizador:
<button id="snap-btn">Save snapshot</button>
<button id="reset-btn">Reset counter</button>
y app.js abre el WebSocket una vez y conecta ambos botones 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 elección entre ws:// y wss:// refleja la de http:// frente a https://: el WebSocket hereda el mismo manejo de TLS. Con HTTPS en su sitio, el panel de control se conecta automáticamente vía wss://.
10.8.3. Cuándo elegir SSE frente a WebSockets¶
Usa SSE cuando la cámara envía datos y el navegador solo escucha: notificaciones, telemetría, cambios de estado. La comunicación es HTTP simple, el lado del cliente es una sola línea (new EventSource) y la reconexión es automática.
Usa WebSockets cuando el navegador también necesita enviar: botones, deslizadores que envían actualizaciones a ritmo de pulsación de tecla, cualquier cosa donde esperar a la siguiente solicitud sería demasiado lento. La conexión es bidireccional y enmarcada, pero la API es más compleja en ambos lados.
La cámara es ahora una cosa interactiva: observa, envía eventos hacia fuera y acepta comandos hacia dentro.