89 lines
4.9 KiB
Markdown
89 lines
4.9 KiB
Markdown
# Подключение устройства к 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":"<hex>",
|
||
"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.<uuid-инстанса EMQX>.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 с.
|