10.8. Двусторонее управление через WebSocket

Server-Sent Events работают только на отправку. Когда владелец нажимает на панели управления «сделать снимок прямо сейчас» или «сбросить счётчик срабатываний», панель должна отправить сообщение камере. Это WebSocket – один TCP-сокет, кадрированные сообщения, идущие в обоих направлениях.

10.8.1. Маршрут /control

microdot.websocket.with_websocket() выполняет рукопожатие обновления до WebSocket и передаёт обработчику объект WebSocket. Обработчик зацикливается навсегда, читая команды и отправляя подтверждения:

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() возвращает строку для текстовых кадров и байты для двоичных кадров. На стороне браузера WebSocket.send(...) по умолчанию отправляет текст, так что команды, закодированные в JSON, – естественный выбор.

send() принимает строки, байты или любой объект, сериализуемый в JSON – словарь отправляется как текстовый кадр JSON.

WebSocketError возбуждается, когда клиент отключается (чистое закрытие, обрыв сети или ошибка протокола). Обработчик выходит из цикла и завершается; microdot прибирает за сокетом.

10.8.2. Панель управления отправляет команды

В index.html рядом со слайдером добавляются две кнопки:

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

а app.js один раз открывает WebSocket и подключает обе кнопки к 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));
});

Выбор между ws:// и wss:// отражает выбор между http:// и https:// – WebSocket наследует ту же обработку TLS. При включённом HTTPS панель управления автоматически подключается через wss://.

10.8.3. Когда выбирать SSE, а когда WebSocket

Используйте SSE, когда камера отправляет данные, а браузер только слушает – уведомления, телеметрия, изменения состояния. Канал – это обычный HTTP, клиентская сторона занимает одну строку (new EventSource), а переподключение происходит автоматически.

Используйте WebSocket, когда браузеру тоже нужно отправлять данные – кнопки, слайдеры, посылающие обновления с частотой нажатий клавиш, всё, где ожидание следующего запроса было бы слишком медленным. Соединение двунаправленное и кадрированное, но API сложнее с обеих сторон.

Теперь камера – интерактивное устройство: наблюдает, отправляет события наружу, принимает команды внутрь.