10.8. Control bidirecțional cu WebSockets

Server-Sent Events sunt doar de tip push. Când proprietarul apasă „salvează un instantaneu chiar acum” sau „resetează contorul declanșatorului” pe tabloul de bord, tabloul de bord trebuie să trimită un mesaj către cameră. Aceasta înseamnă WebSockets – un singur socket TCP, mesaje încadrate care circulă în ambele direcții.

10.8.1. Ruta /control

microdot.websocket.with_websocket() efectuează negocierea de upgrade WebSocket și transmite handlerului un obiect WebSocket. Handlerul rulează la nesfârșit, citind comenzi și trimițând confirmări:

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() returnează un șir de caractere pentru cadrele text și octeți pentru cadrele binare. WebSocket.send(...) din browser trimite text în mod implicit, așa că comenzile codificate JSON sunt alegerea naturală.

send() acceptă șiruri de caractere, octeți sau orice obiect serializabil JSON – un dicționar este trimis ca un cadru text JSON.

WebSocketError este ridicată când clientul se deconectează (închidere curată, pierderea rețelei sau eroare de protocol). Handlerul iese din buclă și revine; microdot face curățenia socketului.

10.8.2. Tabloul de bord trimite comenzi

Două butoane sunt adăugate în index.html lângă glisor:

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

iar app.js deschide WebSocketul o singură dată și conectează ambele butoane la 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));
});

Alegerea între ws:// și wss:// reflectă alegerea între http:// și https:// – WebSocketul moștenește aceeași gestionare TLS. Cu HTTPS activat, tabloul de bord se conectează automat prin wss://.

10.8.3. Când să alegi SSE versus WebSockets

Folosește SSE când camera trimite (push), iar browserul doar ascultă – notificări, telemetrie, schimbări de stare. Transportul este HTTP simplu, partea de client este o singură linie (new EventSource), iar reconectarea este automată.

Folosește WebSockets când și browserul trebuie să trimită – butoane, glisoare care trimit actualizări la rata apăsărilor de taste, orice unde așteptarea cererii următoare ar fi prea lentă. Conexiunea este bidirecțională și încadrată, dar API-ul este mai complex pe ambele părți.

Camera este acum un lucru interactiv – urmărește, trimite evenimente în exterior, acceptă comenzi în interior.