Two-way control with WebSockets =============================== Server-Sent Events are push-only. When the owner taps "save a snapshot right now" or "reset the trigger counter" on the dashboard, the dashboard has to send a message *to* the cam. That's WebSockets -- one TCP socket, framed messages flowing both directions. The /control route ------------------ :func:`microdot.websocket.with_websocket` performs the WebSocket upgrade handshake and hands the handler a :class:`~microdot.websocket.WebSocket` object. The handler loops forever, reading commands and sending acknowledgements: .. code-block:: python 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'}) :meth:`~microdot.websocket.WebSocket.receive` returns a string for text frames and bytes for binary frames. The browser-side ``WebSocket.send(...)`` sends text by default, so JSON-encoded commands are the natural choice. :meth:`~microdot.websocket.WebSocket.send` accepts strings, bytes, or anything JSON-serializable -- a dict is sent as a JSON text frame. :exc:`~microdot.websocket.WebSocketError` is raised when the client disconnects (clean close, network drop, or protocol error). The handler exits the loop and returns; microdot tidies up the socket. The dashboard sends commands ---------------------------- Two buttons go into ``index.html`` next to the slider: .. code-block:: html and ``app.js`` opens the WebSocket once and wires both buttons to ``send``: .. code-block:: javascript 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)); }); The ``ws://`` vs ``wss://`` choice mirrors ``http://`` vs ``https://`` -- the WebSocket inherits the same TLS handling. With HTTPS in place the dashboard automatically connects via ``wss://``. When to pick SSE vs WebSockets ------------------------------ Use SSE when the cam pushes and the browser only listens -- notifications, telemetry, status changes. The wire is plain HTTP, the client side is one line (``new EventSource``), and reconnect is automatic. Use WebSockets when the browser also needs to push -- buttons, sliders that send keystroke-rate updates, anything where waiting on the next request would be too slow. The connection is bidirectional and framed, but the API is more involved on both sides. The cam is now an interactive thing -- watch, push events out, accept commands in.