10.8. Двостороннє керування за допомогою WebSockets

Server-Sent Events підтримують лише надсилання. Коли власник натискає «зробити знімок зараз» або «скинути лічильник спрацювань» на панелі моніторингу, панель має надіслати повідомлення на камеру. Це WebSockets – один 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 проти WebSockets

Використовуйте SSE, коли камера надсилає дані, а браузер лише слухає – сповіщення, телеметрія, зміни стану. Дротяна частина – це звичайний HTTP, сторона клієнта – один рядок (new EventSource), і повторне підключення відбувається автоматично.

Використовуйте WebSockets, коли браузеру також потрібно надсилати – кнопки, слайдери, що надсилають оновлення зі швидкістю натискань клавіш, все, де очікування наступного запиту буде надто повільним. З’єднання є двостороннім і кадрованим, але API є більш складним з обох сторін.

Камера тепер є інтерактивним пристроєм – спостерігає, надсилає події назовні, приймає команди всередину.