# Подключение устройства к IoT (Nubes, тест-стенд) Проверено end-to-end 2026-10-11 на развёрнутом стенде (`https://iot-api.containerk8s.dev.nubes.ru`, `wss://exqx-iot.containerk8s.dev.nubes.ru/mqtt`). ## 1. Регистрация устройства (HTTP API, JWT HS256) JWT подписывается `JWT_HMAC_SECRET` (HS256), claim `sub` = ваш namespace, `exp` обязателен; `namespace` в пути **обязан совпадать** с `sub`, иначе `403`. ```bash curl -X POST "$API/v1/namespaces/$NS/iot/devices" \ -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \ -d '{"name":"dev1","device_id":"d-01"}' ``` Ответ `201` содержит **единственный раз** выдаваемый пароль: ```json {"name":"dev1","namespace":"e2e-nubes","device_id":"d-01","enabled":true, "mqtt_username":"e2e-nubes_d-01","mqtt_password":"", "telemetry_topic":"e2e-nubes/telemetry/d-01"} ``` `GET …/devices/{name}` пароль **не** возвращает. Ограничения идентификаторов: `^[a-zA-Z0-9][a-zA-Z0-9-]*$`, namespace ≤ 56, name/device_id ≤ 64 символов (иначе `400`). Дубликаты name/device_id → `409`. ## 2. Подключение по MQTT - URL: `wss://exqx-iot.containerk8s.dev.nubes.ru/mqtt` (WebSocket, subprotocol `mqtt`); внутри кластера — `ws://containerk8s..svc.cluster.local:8083/mqtt`. - username = `namespace_device_id`, пароль — из ответа на создание. - Публикация: топик `namespace/telemetry/device_id` (`telemetry_topic`). ACL: устройству разрешён **только свой** топик. Публикация/подписка куда-либо ещё (чужой namespace, другое устройство, wildcard `+`/`#`) → `deny_action=disconnect`, соединение рвётся. Также запрещены: retained-публикации (`retain_available=false`), топики глубже 10 уровней, subscribe на `+/telemetry/+` (это привилегия бриджа). Коды CONNACK: `0` — успех, `4` — неверный username/пароль, `5` — не авторизован (в т.ч. устройство выключено через `PATCH {"enabled": false}`). ## 3. Требование к payload — ВАЖНО Дедупликация выполняется по **содержимому сообщения**: `message_id = SHA256(namespace | device_id | topic | payload)` (`internal/service/bridge/handler.go`). Поэтому: - **два побайтово одинаковых payload подряд считаются дубликатом и сохраняются один раз** (это защита от повторной доставки SQS at-least-once); - ⇒ в payload обязателен **timestamp или счётчик**, иначе легитимные повторные одинаковые показания (например, `{"t":21.0}` каждую минуту) будут отброшены. Пример корректного payload: ```json {"temp": 21.5, "ts": 1791737028, "seq": 4211} ``` Payload валидный JSON сохраняется как JSON; не-JSON (текст, бинарь) оборачивается в строку. Лимиты: MQTT-пакет ≤ 1 МБ (`max_packet_size`), envelope для SQS ≤ 250 КБ (`maxSQSBodyBytes`) — сообщения больше отбрасываются мостом. QoS: `QoS0` — «выстрелил и забыл»; для гарантии доставки используйте `QoS1` (дождитесь PUBACK перед закрытием соединения). ## 4. Чтение телеметрии ```bash curl "$API/v1/namespaces/$NS/iot/telemetry?device_id=d-01&limit=50&offset=0" \ -H "Authorization: Bearer $JWT" ``` `limit` — дефолт 50, потолок 1000. Ответ: `{"count":N,"items":[{"id":…,"device_id":…,"ts":…,"payload":…}]}`. ⚠️ Чтение отдаёт payload целиком, поэтому `limit=1000` при крупных payload'ах дорого для клиента и сервиса — для больших объёмов берите меньше и используйте `offset`. ## 5. Наблюдаемое поведение (e2e, 2026-10-11) Задержка «MQTT → видно в API» недетерминирована и доходит до ~20 с: потребитель SQS работает long-poll окном (`SQS_LONG_POLL_SECONDS=20`), батч пишется в PG и только потом удаляется из очереди. Для тестов/интеграции — опрашивать с таймаутом ≥ 30 с, а не фиксированной паузой 1–5 с.