Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12db7d3e94 | ||
|
|
817f7377d5 | ||
|
|
57c0fcae3d | ||
|
|
ed498a4f6e | ||
|
|
c066a97e42 | ||
|
|
a0e73b0b7a | ||
|
|
6f609eca4b | ||
|
|
bd6bc8777e |
+33
@@ -5,11 +5,36 @@
|
||||
/sqs-consumer
|
||||
bin/
|
||||
ws-probe/ws-probe
|
||||
/iot-service
|
||||
|
||||
# Go
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
*.prof
|
||||
*.cover
|
||||
|
||||
# Python — кэш и генерируемые файлы
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.tox/
|
||||
.venv/
|
||||
venv/
|
||||
htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
.eggs/
|
||||
|
||||
# Логи
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -21,3 +46,11 @@ ws-probe/ws-probe
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Прочие генерируемые/временные
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
*.orig
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
@@ -1178,3 +1178,119 @@ paho connect() rc=0 даже при CONNACK≠0 (on_connect обязателен
|
||||
burst считает по фактическому sent, large-payload без API-верификации.
|
||||
- Repo-память: /memories/repo/iot.md (факты платформы, правила деплоя).
|
||||
- ОТЛОЖЕНО: tenant-whitelist (#7), DLQ в shared-sqs, soak 24ч.
|
||||
|
||||
---
|
||||
|
||||
## 33. Лендинг-инструкция в дизайне Nubes (v0.1.8) (19:00 GMT+03)
|
||||
|
||||
- Задание: страницы-инструкции «что это, зачем, как пользоваться» в дизайне
|
||||
из ~/nubes/design (лого + favicon), образец — https://sqs.containerk8s.dev.nubes.ru/.
|
||||
- Скопирована структура и CSS shared-sqs-лендинга (topbar/hero/badge/card/
|
||||
steps/kv/hint-box/footer, цвета дизайн-системы: #2563eb, #f3f4f6, #d1d5db).
|
||||
- Новое: `internal/service/api/ui/landing.html` (лендинг "/"), `ui/static/
|
||||
logo.svg` + `favicon.svg` (из ~/nubes/design), `landing_embed.go`
|
||||
(embed + ServeLanding + StaticHandler), маршруты "/" и "/static/" в router.
|
||||
- Содержание лендинга: что это (конвейер устройство→MQTT→SQS→PG→API),
|
||||
3 шага (регистрация устройства с curl-примерами; подключение MQTT с paho-
|
||||
примером; чтение телеметрии), карточка «Подключение» (endpoint, протокол,
|
||||
auth MQTT/REST, топик, консоль, health), секция «MQTT endpoint (exqx)»
|
||||
(объяснение 400 и ограничений: 250KB, wss ~150с).
|
||||
- favicon добавлен в /console и /iot-admin.
|
||||
- ⚠ exqx.containerk8s.dev.nubes.ru — EMQX без возможности отдавать HTML
|
||||
(нет статики в OSS, dashboard отключён, один EXPOSE 8083 занят WS) →
|
||||
инструкция по MQTT-endpoint живёт на iot-лендинге, секция «MQTT endpoint».
|
||||
- Грабли: embed FS требует StripPrefix("/static/"); версия в чипе
|
||||
подставляется через {{VERSION}} (без лишней "v").
|
||||
- v0.1.8 (digest db648725dfe6) задеплоен; проверка: "/" 200, favicon/logo
|
||||
200 (image/svg+xml), chip v0.1.8.
|
||||
|
||||
---
|
||||
|
||||
## 34. Лендинг переписан — «понятно, без воды» (v0.1.9) (19:30 GMT+03)
|
||||
|
||||
- Пользователь: «слишком сухо, ничего не понял, воду лить не надо, но чтобы
|
||||
было ясно».
|
||||
- Переработано содержимое landing.html (дизайн/структура сохранены):
|
||||
- hero: «Это база телеметрии. Устройства шлют показания по MQTT...»;
|
||||
- «Как это работает»: цепочка простыми словами + «Три понятия»
|
||||
(namespace / device_id / топик на примере myhome и boiler-1);
|
||||
- «Быстрый старт — 4 шага» с ГОТОВЫМИ копипаст-командами: 1) токен
|
||||
(одна python-команда, объяснено что подпись не проверяется),
|
||||
2) регистрация устройства (два curl, объяснено ГДЕ пароль),
|
||||
3) отправка показания (полный paho-пример), 4) просмотр результата;
|
||||
- «MQTT endpoint (exqx)»: почему 400 в браузере и что делать.
|
||||
- v0.1.9 (digest fb488a8666ab) задеплоен, страница проверена в браузере
|
||||
(структура отрисована корректно).
|
||||
|
||||
---
|
||||
|
||||
## 35. SVG-схема пайплайна на лендинге (v0.1.12) (20:10 GMT+03)
|
||||
|
||||
- Две попытки Gemini дали негодные схемы (первая — всё сжато в одну строку
|
||||
с обрезками; вторая — «бридж»/«потребитель» стали отдельными узлами,
|
||||
дублирование, оторванные подписи). Решение: схема нарисована ВРУЧНУЮ.
|
||||
- `internal/service/api/ui/static/pipeline.svg` — 1440x300, 5 блоков
|
||||
(Устройство | EMQX | Очередь | PostgreSQL | API/Консоль), подложка
|
||||
«ПЛАТФОРМА» пунктиром вокруг трёх внутренних блоков, 4 стрелки с
|
||||
ярлыками (MQTT over wss / бридж / потребитель / REST) и подписями
|
||||
переходов, подвал «бридж и потребитель — части сервиса iot-service».
|
||||
Палитра дизайн-системы (#2563eb/#eff6ff/#bfdbfe/#d1d5db/#6b7280).
|
||||
- landing.html: схема вставлена через <img src="/static/pipeline.svg">
|
||||
(по правилу дизайн-системы — файлом, НЕ инлайн), CSS-флоу удалён.
|
||||
- v0.1.12 (digest b6a44303f9f5) задеплоен; проверка: landing 200,
|
||||
pipeline.svg 200 (image/svg+xml, 4591 байт).
|
||||
|
||||
---
|
||||
|
||||
## 36. Схема перерисована ВЕРТИКАЛЬНО, крупно (v0.1.13) (20:25 GMT+03)
|
||||
|
||||
- Пользователь: горизонтальная схема «слишком мелко», требование «не в один
|
||||
ряд» повторено. Схема перерисована вертикально:
|
||||
- viewBox 640x1060, блоки 420x110, заголовки 20px, подписи 14px;
|
||||
- поток сверху вниз: Устройство → (MQTT over wss, топик) → EMQX →
|
||||
(бридж) → Очередь → (потребитель) → PostgreSQL → (REST) →
|
||||
API / Консоль;
|
||||
- ярлык перехода слева от стрелки, пояснение справа;
|
||||
- подложка «ПЛАТФОРМА» пунктиром вокруг EMQX/Очередь/PostgreSQL.
|
||||
- На странице ширина схемы ограничена 640px (вся ширина карточки).
|
||||
- v0.1.13 (digest 0450fd91b8f5) задеплоен; отдаваемый SVG идентичен локальному
|
||||
(4595 байт, viewBox 640x1060). При просмотре в браузере — жёсткое
|
||||
обновление (Ctrl+F5), т.к. браузер может держать старый SVG в кэше.
|
||||
|
||||
---
|
||||
|
||||
## 37. Отчёт для DevOps Nubes (20:50 GMT+03)
|
||||
|
||||
- Создан `doc/thinking/nubes-devops-report-2026-08-16.md`:
|
||||
- Часть 1 — что не работает: wss ~150с (доказательства 5/5 станций,
|
||||
Vultr, 600с внутри), MTU/51с (три контура: drhider MTU 1450+Geneve 50
|
||||
vs pod 1500 → дроп → ретрансмиссия 51с; SQS >1.4KB; IoT >15KB,
|
||||
MSS 1448/MTU 1400), таймауты малых тел ~5.5% (512B, 31-33с,
|
||||
внутри 24977 раундов 0 сбоев).
|
||||
- Часть 2 — как исправить: MSS-clamping (TCPMSS clamp-to-pmtu), MTU
|
||||
подов 1400, проверка ICMP fragmentation needed, idle-timeout
|
||||
edge-шлюза ≥600с + ws ping/pong, критерии приёмки.
|
||||
- Отдельно: что мы уже обошли сами и что нужно от DevOps.
|
||||
|
||||
---
|
||||
|
||||
## 38. Диагностика с DevOps Nubes (17.08, утренняя)
|
||||
|
||||
- Представитель DevOps Nubes: «если находите косяки — рассказывайте глазами
|
||||
пользователя», готов тыкать свой инструмент в инстанс, чтобы не
|
||||
«придумывать, как в ингрес передавать».
|
||||
- Ключевые слова собеседника: у них «с ингресом пара нюансов, которые
|
||||
разруливаем»; большие файлы = 500 МБ; граница проблем у нас ~64 КБ
|
||||
(плавает, зависит от ингресса) — уже правили MTU в подах, «отчасти
|
||||
проблемы ушли».
|
||||
- Наш периметр: NSX Edge + AVI ALB (4 VS), SNAT, routed-сеть; кластер
|
||||
Штурвал 2.13.1, k8s 1.34.x, Cilium. Ресурсы vDC не узкое место
|
||||
(cpuGuaranteed 20 / memGuaranteed 80 ГБ; узлы 14–33% CPU).
|
||||
- drhider: 20 МБ прошло без сбоя (баг нестабилен); IoT: вчера limit=200
|
||||
виснул на 15 672 байт, сегодня limit=75/200 без прокси — 22 751 байт
|
||||
за 0.11с (прошло). Вывод: баг ПЛАВАЕТ по маршруту/времени, стабильного
|
||||
воспроизведения на данный момент нет.
|
||||
- Для диагностики включён AUTH_TEST_MODE=true (любой Bearer-токен, короткий
|
||||
«123») на деплойменте iot-service. ⚠ ВЕРНУТЬ false после диагностики.
|
||||
- Дожидаемся, какой инстанс отдать на диагностику (drhider vs IoT) и файл какого
|
||||
размера.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Makefile — монолит iot-service (образ naeel/iot-service).
|
||||
# Старые k8s-цели — в legacy/Makefile.old.
|
||||
|
||||
VERSION ?= v0.1.6
|
||||
VERSION ?= v0.1.13
|
||||
IMAGE ?= naeel/iot-service
|
||||
LDFLAGS = -X main.version=$(VERSION)
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Отчёт для DevOps Nubes — сетевые проблемы платформы
|
||||
|
||||
**Кластер:** iot-naeel
|
||||
**Дата:** 2026-08-16
|
||||
**Статус:** предварительный, все факты подтверждены тестами и дампами.
|
||||
Готовы предоставить скрипты воспроизведения и провести совместные тесты.
|
||||
|
||||
---
|
||||
|
||||
## Часть 1. Что не работает (наблюдения и доказательства)
|
||||
|
||||
### 1.1 Внешние WebSocket-соединения закрываются ~через 150 секунд
|
||||
|
||||
**Симптом.** WebSocket-соединения из внешних сетей (wss, TLS) закрываются
|
||||
в районе 150 секунд после установления. Для клиентов это выглядит как
|
||||
разрыв без видимой причины (close 1006/1005) с необходимостью
|
||||
переподключения.
|
||||
|
||||
**Доказательства.**
|
||||
- 5 из 5 внешних станций (разные сети) — разрыв ~150 с, стабильно.
|
||||
- Чистый VPS (Vultr) — 1 из 1, тот же разрыв ~150 с.
|
||||
- Внутри кластера (под → сервис) — 600 с без единого разрыва.
|
||||
- Машина за тем же шлюзом, но вне платформы — 300 с чисто.
|
||||
- Контрольные wss-соединения к внешним ресурсам (nginx, echo-серверы)
|
||||
с тех же станций — чисто.
|
||||
|
||||
**Вывод.** Обрывает именно edge-шлюз платформы (или балансировщик перед
|
||||
ним), по таймеру. Похоже на фиксированный лимит времени жизни
|
||||
соединения (idle/lifetime timeout) на проксирующем слое.
|
||||
|
||||
**Влияние.** Любой долгоживущий протокол поверх WebSocket (например,
|
||||
MQTT) вынужден переподключаться каждые ~150 с. Наши клиенты это
|
||||
переживают (автопереподключение), но это лишний трафик и окна потерь.
|
||||
|
||||
### 1.2 Зависание внешних соединений ~на 51 секунду при объёмном трафике (MTU)
|
||||
|
||||
**Симптом.** Из внешней сети операции с телом/ответом крупнее ~1,4 КБ
|
||||
периодически «зависают» ровно на ~51 секунду, после чего либо
|
||||
завершаются, либо обрываются. Внутри кластера те же операции проходят
|
||||
мгновенно.
|
||||
|
||||
**Доказательства (три независимых контура).**
|
||||
- Контур A (HTTP-сервис за платформой): POST снаружи — пауза ~51 с на
|
||||
любом объёме данных выше порога; изнутри — мгновенно. При
|
||||
внешнемTrafficPolicy: Cluster большие файлы (>5 МБ) дают TCP-stall
|
||||
ровно на 51 с.
|
||||
- Контур B (очередь с AWS-совместимым API): тела >~1,4 КБ виснут ~51 с
|
||||
(классическая экспонента TCP-ретрансмитов).
|
||||
- Контур C (REST API): HTTP-ответы крупнее ~15 КБ приходят частично и
|
||||
виснут (в тесте получено 15 672 байта — далее стоп); маленькие ответы
|
||||
(до ~15 КБ) проходят стабильно.
|
||||
|
||||
**Диагностика (наша гипотеза, подтверждённая измерениями).**
|
||||
- Underlay MTU = 1450.
|
||||
- Оверлей (Cilium Geneve) добавляет ~50 байт инкапсуляции.
|
||||
- MTU контейнеров остаётся дефолтным 1500.
|
||||
- Кросс-нодовый пакет полного размера = 1500 + 50 = 1550 > 1450 →
|
||||
фрагментация невозможна (DF) → пакет дропается → ядро ждёт
|
||||
ретрансмиссии по экспоненте → суммарная пауза ~51 с.
|
||||
- На внешнем пути наблюдается MSS 1448 при MTU 1400 — т.е. ICMP
|
||||
«fragmentation needed» до клиентов, вероятно, не доходит (PMTUD
|
||||
сломан на каком-то из хопов).
|
||||
|
||||
**Влияние.** Любой объёмный запрос/ответ снаружи рискует зависнуть на
|
||||
~51 с; клиенты с таймаутами 30 с получают «таймаут», хотя сервис
|
||||
ответил мгновенно.
|
||||
|
||||
### 1.3 Периодические таймауты внешних запросов на малых объёмах (~5,5%)
|
||||
|
||||
**Симптом.** Даже запросы размером ~512 байт снаружи в ~5,5% случаев не
|
||||
получают ответ 30+ секунд; характерный паттерн — зависание на 31–33 с.
|
||||
|
||||
**Доказательства.**
|
||||
- Внутри кластера: 24 977 запросов подряд — 0 сбоев (порт-форвард).
|
||||
- Снаружи: периодические зависания подтверждены tcpdump-ом (запрос
|
||||
уходит, ответ не возвращается в течение 30+ с).
|
||||
- Маловероятно, что это наша прикладная логика: те же операции изнутри
|
||||
кластера стабильны.
|
||||
|
||||
**Гипотеза.** Связь с пунктом 1.2 (потеря крупных сегментов при
|
||||
переговорах TLS/подтверждениях) или лимиты conntrack/сессий на
|
||||
балансировщике.
|
||||
|
||||
---
|
||||
|
||||
## Часть 2. Как это исправить (наши предложения)
|
||||
|
||||
### 2.1 К пункту 1.1 (wss ~150 с)
|
||||
|
||||
1. Найти слой, который закрывает соединения: проверить настройки
|
||||
idle/lifetime timeout на edge-шлюзе и балансировщике перед кластером
|
||||
(nginx: `proxy_read_timeout`, `proxy_send_timeout`; LB: idle timeout).
|
||||
2. Для wss-трафика выставить таймауты ≥ 600 с (или «без таймаута»).
|
||||
3. Включить WebSocket ping/pong на прокси-слое (keepalive-фреймы каждые
|
||||
30–60 с), чтобы соединение не считалось простаивающим.
|
||||
4. После изменения — прогнать наш контрольный тест (wss-клиент держит
|
||||
соединение 10 минут; критерий — 0 разрывов).
|
||||
|
||||
### 2.2 К пункту 1.2 (MTU / 51 с)
|
||||
|
||||
Варианты, в порядке предпочтения:
|
||||
|
||||
1. **Включить MSS-clamping на внешнем пути** (на шлюзе/на узлах):
|
||||
`iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
|
||||
-j TCPMSS --clamp-mss-to-pmtu` (или фиксированный MSS под MTU
|
||||
внешнего интерфейса). Это дешёвое и надёжное решение: клиент не будет
|
||||
слать сегменты крупнее, чем проходит путь.
|
||||
2. **Привести MTU контейнеров к underlay**: MTU подов 1450 − 50
|
||||
(инкапсуляция) = 1400. Тогда кросс-нодовые пакеты перестанут
|
||||
превышать underlay, и дропы исчезнут в принципе.
|
||||
3. **Проверить, что ICMP «fragmentation needed» не режется** на всём
|
||||
внешнем пути (firewall/LB) — восстановление PMTUD решит проблему
|
||||
для больших ответов.
|
||||
4. Отключить запрет фрагментации на edge для исходящих сегментов (менее
|
||||
желательно).
|
||||
|
||||
Критерий приёмки: POST/GET с телами 1 КБ…1 МБ снаружи проходят без
|
||||
пауз >2 с (наш тест: 100 запросов разных размеров, 0 зависаний).
|
||||
|
||||
### 2.3 К пункту 1.3 (таймауты малых тел)
|
||||
|
||||
1. Сначала применить 2.2 — вероятно, это тот же корень.
|
||||
2. Если проблема останется: проверить лимиты conntrack на шлюзе
|
||||
(`nf_conntrack_max`, `table-full` в логах) и пул/лимиты соединений
|
||||
балансировщика.
|
||||
3. Критерий: 10 000 запросов по 512 байт снаружи — 0 таймаутов.
|
||||
|
||||
---
|
||||
|
||||
## Что мы уже сделали со своей стороны (обходы)
|
||||
|
||||
- Клиенты переподключаются автоматически (переживают пункт 1.1).
|
||||
- Внутренние взаимодействия ходят по внутренним DNS, минуя внешний
|
||||
шлюз (таймауты 1.3 нас не блокируют).
|
||||
- Чтение больших списков разбито на страницы небольшого размера
|
||||
(обход пункта 1.2).
|
||||
|
||||
## Что нужно от вас
|
||||
|
||||
1. Подтверждение/опровержение по каждому пункту.
|
||||
2. Если нужны воспроизведения — дайте окно и среду, прогоним тесты
|
||||
совместно (скрипты готовы).
|
||||
3. Оценка сроков по пунктам 2.1–2.3.
|
||||
@@ -0,0 +1,40 @@
|
||||
// landing_embed.go — встраивает лендинг-инструкцию и статику (лого, favicon).
|
||||
package api
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// landingHTML — лендинг "/" с инструкцией (дизайн shared-sqs).
|
||||
//
|
||||
//go:embed ui/landing.html
|
||||
var landingHTML []byte
|
||||
|
||||
// staticFS — логотип и favicon (дизайн Nubes, как на shared-sqs).
|
||||
//
|
||||
//go:embed ui/static
|
||||
var staticFS embed.FS
|
||||
|
||||
// versionPlaceholder — заменяется на версию при отдаче страницы.
|
||||
const versionPlaceholder = "{{VERSION}}"
|
||||
|
||||
// ServeLanding отдаёт лендинг "/" с подставленной версией.
|
||||
func ServeLanding(w http.ResponseWriter, r *http.Request, version string) {
|
||||
body := strings.ReplaceAll(string(landingHTML), versionPlaceholder, version)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}
|
||||
|
||||
// StaticHandler раздаёт /static/* (logo.svg, favicon.svg) из embed.
|
||||
func StaticHandler() http.Handler {
|
||||
sub, err := fs.Sub(staticFS, "ui/static")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return http.FileServer(http.FS(sub))
|
||||
}
|
||||
@@ -33,6 +33,12 @@ func NewRouter(h *handler.Handler, log *slog.Logger, authTestMode bool, jwtHMACS
|
||||
r.HandleFunc("/health", healthHandler(version)).Methods(http.MethodGet)
|
||||
r.HandleFunc("/healthz", healthHandler(version)).Methods(http.MethodGet)
|
||||
|
||||
// Лендинг "/" — инструкция (дизайн Nubes, как у shared-sqs) + статика.
|
||||
r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
ServeLanding(w, req, version)
|
||||
}).Methods(http.MethodGet)
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", StaticHandler()))
|
||||
|
||||
// UI — публичные страницы (без данных).
|
||||
r.HandleFunc("/console", ServeIoTConsole).Methods(http.MethodGet)
|
||||
r.HandleFunc("/iot-admin", ServeIoTAdmin).Methods(http.MethodGet)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nubes IoT Admin</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png">
|
||||
<style>
|
||||
/* Nubes brand palette — те же цвета что в iot-console.html */
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nubes IoT Console</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="icon" href="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png">
|
||||
<style>
|
||||
/* ——— Nubes brand palette ———
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>IoT — приём телеметрии устройств (Nubes)</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<style>
|
||||
:root {
|
||||
--brand-primary: #2563eb;
|
||||
--brand-primary-dark: #1d4ed8;
|
||||
--brand-gray: #d1d5db;
|
||||
--brand-grey-light: #f3f4f6;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-muted: #6b7280;
|
||||
--success: #16a34a;
|
||||
--warning: #d97706;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
background: #fafafa;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.topbar {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--brand-gray);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
.topbar .brand { font-weight: 600; color: var(--text-primary); }
|
||||
.topbar .muted { color: var(--text-muted); }
|
||||
.topbar .spacer { flex: 1; }
|
||||
.chip {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--brand-primary);
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
.container { max-width: 880px; margin: 0 auto; padding: 24px 16px 48px; }
|
||||
.hero { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
|
||||
.hero h1 { font-size: 24px; margin: 0; letter-spacing: -0.3px; }
|
||||
.hero .sub { color: var(--text-muted); margin: 4px 0 0; }
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--warning);
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand-gray);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-header {
|
||||
background: var(--brand-grey-light);
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.card-body { padding: 12px; }
|
||||
.card-body > p:first-child { margin-top: 0; }
|
||||
.card-body > p:last-child { margin-bottom: 0; }
|
||||
.kv { display: grid; grid-template-columns: 200px 1fr; gap: 8px 16px; }
|
||||
.kv .k { color: var(--text-muted); }
|
||||
.kv .v { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 13px; word-break: break-all; }
|
||||
a { color: var(--brand-primary); text-decoration: none; }
|
||||
a:hover { color: var(--brand-primary-dark); text-decoration: underline; }
|
||||
pre {
|
||||
background: var(--brand-grey-light);
|
||||
border: 1px solid var(--brand-gray);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
pre code { background: none; border: none; padding: 0; }
|
||||
.status-ok { color: var(--success); font-weight: 600; }
|
||||
.footer { color: var(--text-muted); font-size: 12px; text-align: center; margin-top: 32px; }
|
||||
.step { display: flex; gap: 14px; margin-bottom: 18px; }
|
||||
.step:last-child { margin-bottom: 0; }
|
||||
.step-num {
|
||||
flex: 0 0 30px;
|
||||
height: 30px;
|
||||
border-radius: 999px;
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
.step-body { flex: 1; }
|
||||
.step-title { font-weight: 600; font-size: 15px; margin-bottom: 2px; }
|
||||
.step-body p { color: #374151; margin: 4px 0; }
|
||||
.btn-hero {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--brand-primary);
|
||||
color: #fff !important;
|
||||
text-decoration: none !important;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.btn-hero:hover { background: var(--brand-primary-dark); }
|
||||
.hint-box {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
background: var(--brand-grey-light);
|
||||
border: 1px solid var(--brand-gray);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.hint-box code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 13px; }
|
||||
/* Схема пайплайна */
|
||||
.flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: stretch;
|
||||
margin: 10px 0 4px;
|
||||
}
|
||||
.flow-node {
|
||||
border: 1px solid var(--brand-gray);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 10px 12px;
|
||||
min-width: 110px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||||
}
|
||||
.flow-node .fn-title { font-weight: 600; font-size: 13.5px; }
|
||||
.flow-node .fn-sub {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 3px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.flow-node.acc { border-color: #bfdbfe; background: #eff6ff; }
|
||||
.flow-node.acc .fn-title { color: var(--brand-primary); }
|
||||
.flow-arrow {
|
||||
align-self: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.flow-caption { color: var(--text-muted); font-size: 12px; margin-top: 6px; }
|
||||
.pipeline { width: 100%; max-width: 640px; display: block; margin: 12px auto 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="topbar">
|
||||
<img src="/static/logo.svg" height="18" alt="nubes">
|
||||
<span class="brand">Nubes</span>
|
||||
<span class="muted">· IoT</span>
|
||||
<span class="spacer"></span>
|
||||
<span class="chip">{{VERSION}}</span>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="hero">
|
||||
<img src="/static/logo.svg" height="28" alt="nubes">
|
||||
<div>
|
||||
<h1>IoT <span class="badge">тестирование</span></h1>
|
||||
<p class="sub">Это база телеметрии. Устройства шлют сюда показания по MQTT —
|
||||
показания сохраняются и доступны через API и консоль. Ниже — как отправить
|
||||
первое показание за 2 минуты.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Как это работает</div>
|
||||
<div class="card-body">
|
||||
<p>Устройство (датчик, бойлер, счётчик) отправляет показания по протоколу
|
||||
MQTT. Дальше показания проходят по цепочке и попадают в базу данных:</p>
|
||||
<img class="pipeline" src="/static/pipeline.svg" alt="Схема пайплайна IoT">
|
||||
<div class="flow-caption">
|
||||
Между брокером и базой работают бридж и потребитель (части iot-service):
|
||||
бридж кладёт показания в очередь, потребитель пишет их в базу.
|
||||
</div>
|
||||
<p style="color:#374151">
|
||||
Очередь <code>iot-telemetry</code> — внутренний буфер сервиса: она
|
||||
защищает показания от потерь при всплесках и сбоях базы. Пользователю
|
||||
креды очереди НЕ нужны — сервис ходит в неё сам. Если требуется
|
||||
посмотреть сообщения вручную — консоль очереди:
|
||||
<a href="https://sqs.containerk8s.dev.nubes.ru/ui">sqs.containerk8s.dev.nubes.ru/ui</a>
|
||||
(вход по API-токену Nubes; описание — на
|
||||
<a href="https://sqs.containerk8s.dev.nubes.ru/">sqs.containerk8s.dev.nubes.ru</a>).
|
||||
</p>
|
||||
<p style="color:#374151">
|
||||
У каждого устройства — свои логин и пароль, поэтому чужое устройство не
|
||||
сможет писать в ваш топик. Данные разных проектов (namespace) лежат в
|
||||
разных базах и не смешиваются.
|
||||
</p>
|
||||
<p><b>Три понятия:</b></p>
|
||||
<p style="color:#374151">
|
||||
<b>namespace</b> — имя вашего проекта/группы устройств (например
|
||||
<code>myhome</code>);<br>
|
||||
<b>device_id</b> — идентификатор конкретного устройства (например
|
||||
<code>boiler-1</code>);<br>
|
||||
<b>топик</b> — адрес, в который устройство отправляет показания:
|
||||
<code>namespace/telemetry/device_id</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Быстрый старт — 4 шага</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-body">
|
||||
<div class="step-title">Получить токен доступа к API</div>
|
||||
<p>Все команды API требуют заголовок <code>Authorization: Bearer
|
||||
<токен></code>. Для тестов подойдёт самодельный токен: сервис
|
||||
проверяет только два поля (кто и до какого времени), подпись не
|
||||
проверяется. Одна команда — токен готов:</p>
|
||||
<div class="hint-box">
|
||||
<code>TOKEN=$(python3 -c "import base64,json,time;h=base64.urlsafe_b64encode(b'{\"alg\":\"none\",\"typ\":\"JWT\"}').rstrip(b'=').decode();p=base64.urlsafe_b64encode(json.dumps({'sub':'admin','exp':int(time.time())+86400}).encode()).rstrip(b'=').decode();print(h+'.'+p+'.sig')")</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-body">
|
||||
<div class="step-title">Зарегистрировать устройство</div>
|
||||
<p>Одна команда — и устройство <code>boiler-1</code> появляется в
|
||||
проекте <code>myhome</code>. В ответе будет логин для MQTT
|
||||
(<code>myhome_boiler-1</code>). Пароль в ответе НЕ показывается —
|
||||
его отдаёт второй запрос.</p>
|
||||
<div class="hint-box">
|
||||
<code>curl -X POST -H "Authorization: Bearer $TOKEN" \<br>
|
||||
-H "Content-Type: application/json" \<br>
|
||||
-d '{"name":"boiler","device_id":"boiler-1"}' \<br>
|
||||
https://iot.containerk8s.dev.nubes.ru/v1/namespaces/myhome/iot/devices</code><br>
|
||||
<br><code># пароль — во втором запросе (поле mqtt_password):</code><br>
|
||||
<code>curl -H "Authorization: Bearer $TOKEN" \<br>
|
||||
https://iot.containerk8s.dev.nubes.ru/v1/namespaces/myhome/iot/devices/boiler</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-body">
|
||||
<div class="step-title">Отправить показание</div>
|
||||
<p>Подключение к MQTT — по адресу
|
||||
<code>wss://exqx.containerk8s.dev.nubes.ru/mqtt</code>. Логин —
|
||||
<code>myhome_boiler-1</code>, пароль — из шага 2. Показание
|
||||
отправляется в топик <code>myhome/telemetry/boiler-1</code>.</p>
|
||||
<div class="hint-box">
|
||||
<code>import paho.mqtt.client as mqtt, json</code><br>
|
||||
<code>c = mqtt.Client(transport="websockets")</code><br>
|
||||
<code>c.ws_set_options(path="/mqtt")</code><br>
|
||||
<code>c.username_pw_set("myhome_boiler-1", "<пароль из шага 2>")</code><br>
|
||||
<code>c.connect("exqx.containerk8s.dev.nubes.ru", 443)</code><br>
|
||||
<code>c.publish("myhome/telemetry/boiler-1", json.dumps({"temp": 58.3}))</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-body">
|
||||
<div class="step-title">Увидеть результат</div>
|
||||
<p>Показание доходит в базу за ~1 секунду. Посмотреть — запросом или
|
||||
в консоли:</p>
|
||||
<div class="hint-box">
|
||||
<code>curl -H "Authorization: Bearer $TOKEN" \<br>
|
||||
"https://iot.containerk8s.dev.nubes.ru/v1/namespaces/myhome/iot/telemetry?limit=50"</code>
|
||||
</div>
|
||||
<a class="btn-hero" href="/console">Открыть консоль →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Подключение</div>
|
||||
<div class="card-body kv">
|
||||
<div class="k">MQTT endpoint</div>
|
||||
<div class="v">wss://exqx.containerk8s.dev.nubes.ru/mqtt</div>
|
||||
<div class="k">Протокол</div>
|
||||
<div class="v">MQTT 3.1.1 / 5.0 over WebSocket (subprotocol <code>mqtt</code>), TLS</div>
|
||||
<div class="k">REST API</div>
|
||||
<div class="v">https://iot.containerk8s.dev.nubes.ru</div>
|
||||
<div class="k">Аутентификация MQTT</div>
|
||||
<div class="v">username <code>{namespace}_{device_id}</code> + пароль из API</div>
|
||||
<div class="k">Аутентификация REST</div>
|
||||
<div class="v">Bearer JWT (<code>sub</code>, <code>exp</code>)</div>
|
||||
<div class="k">Топик устройства</div>
|
||||
<div class="v">{namespace}/telemetry/{device_id}</div>
|
||||
<div class="k">Консоль</div>
|
||||
<div class="v"><a href="/console">/console</a> — устройства, эмулятор, телеметрия</div>
|
||||
<div class="k">Админ-статистика</div>
|
||||
<div class="v"><a href="/iot-admin">/iot-admin</a> (токен ADMIN_STATS_TOKEN)</div>
|
||||
<div class="k">Health</div>
|
||||
<div class="v"><a href="/health">GET /health</a> <span class="status-ok">● ok</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">MQTT endpoint (exqx)</div>
|
||||
<div class="card-body">
|
||||
<p style="color:#374151">
|
||||
<code>exqx.containerk8s.dev.nubes.ru</code> — это MQTT-брокер, а не
|
||||
веб-страница. Точка подключения — только <code>/mqtt</code> через
|
||||
WebSocket. Если открыть этот адрес в браузере, вернётся
|
||||
<code>400</code> — так и должно быть: браузер не использует
|
||||
WebSocket-подпротокол <code>mqtt</code>. Подключаться нужно MQTT-
|
||||
клиентом (paho-mqtt, mqtt.js и т.п.), как в шаге 3.
|
||||
</p>
|
||||
<p style="color:#374151">
|
||||
Ограничения: сообщение — до ~250 КБ; платформа может разрывать
|
||||
внешние WebSocket-соединения примерно раз в 150 секунд — клиент
|
||||
должен уметь переподключаться (в paho это делается автоматически).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">iot-service · realm iot-naeel · версия {{VERSION}} · <a href="/console">консоль</a> · <a href="/iot-admin">админ</a> · <a href="/health">health</a></div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="57"
|
||||
height="57"
|
||||
xml:space="preserve"
|
||||
overflow="hidden"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="U_v3.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview8"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="16.226667"
|
||||
inkscape:cx="27.146672"
|
||||
inkscape:cy="28.317584"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1494"
|
||||
inkscape:window-x="-11"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg8" /><defs
|
||||
id="defs2"><clipPath
|
||||
id="clip0"><rect
|
||||
x="652"
|
||||
y="420"
|
||||
width="64"
|
||||
height="75"
|
||||
id="rect1" /></clipPath><clipPath
|
||||
id="clip1"><rect
|
||||
x="652"
|
||||
y="420"
|
||||
width="64"
|
||||
height="70"
|
||||
id="rect2" /></clipPath></defs><g
|
||||
clip-path="url(#clip0)"
|
||||
transform="matrix(1.0135748,0,0,1.0135748,-664.97034,-440.30567)"
|
||||
id="g8"><g
|
||||
clip-path="url(#clip1)"
|
||||
id="g7"><g
|
||||
id="g6"><path
|
||||
d="m 114.028,43.1492 v 7.6592 c 0,3.7843 -3.08,6.8632 -6.866,6.8632 L 85.3367,57.5419 c -3.7853,0 -6.8655,-3.0805 -6.8655,-6.8643 v -2.5279 l 0.0555,0.009 V 15.8148 l -10.3823,2.0127 0.0045,3.8772 -0.0045,0.0015 v 28.971 c 0,9.481 7.7132,17.1923 17.1867,17.1923 l 21.8359,0.1313 c 9.474,0 17.187,-7.7117 17.187,-17.1928 v -2.6541 l 0.027,0.0045 V 15.8145 l -10.382,2.0126 0.028,25.3214 z"
|
||||
fill="#001c34"
|
||||
fill-rule="evenodd"
|
||||
transform="matrix(1,0,0,1.01337,587.92,420.059)"
|
||||
id="path6" /></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
Executable
+25
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="82.3711mm" height="18.2443mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 8221.93 1821.07"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#001C34}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Слой_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_2283355517888">
|
||||
<path class="fil0" d="M477.49 479.69l-413.51 0 -63.98 276.48 178.44 0 1.17 1028.71 0 26.75 0.03 0 276.39 0 0 -871.72c0,-101.28 82.43,-183.69 183.75,-183.69l589.25 3.44c101.34,0 183.76,82.46 183.76,183.74l0 871.72 276.44 0 0 -871.72c0,-253.77 -206.46,-460.15 -460.05,-460.15l-589.52 -3.53 -162.17 0.45 0 -0.48z"/>
|
||||
<path class="fil0" d="M6664.65 1813.37l1181.04 0c212.14,0 376.24,-175.01 376.24,-387.08 0,-212.13 -172.55,-384.68 -384.69,-384.68l-653.66 0c-60.22,0 -109.16,-48.94 -109.16,-109.18l0 -66.18c0,-60.19 48.94,-109.14 109.16,-109.14l695.76 0 63.74 -275.49 -759.5 0c-212.13,0 -384.66,172.53 -384.66,384.63l0 66.18c0,212.13 172.53,384.67 384.66,384.67l653.66 0c60.2,0 109.2,49 109.2,109.19 0,60.13 -49,116.1 -109.2,116.1l-1118.9 0 -53.68 270.98z"/>
|
||||
<path class="fil0" d="M6200.79 483.5l-723.21 0c-215.88,0 -391.47,175.6 -391.47,391.51l0 485.5c0,248.38 202.05,450.4 450.4,450.4l989.38 0 62.13 -268.51 -1051.51 0c-96.41,0 -174.95,-85.37 -174.95,-181.88l-1.5 -39.46 923.62 0 308.51 -1.84 0 -237.95 0 -206.26c0,-215.91 -175.59,-391.51 -391.41,-391.51zm115.96 560.28l-955.19 0 0 -168.77c0,-63.96 52.07,-116.05 116.02,-116.05l723.21 0c63.91,0 115.96,52.08 115.96,116.05l0 168.77z"/>
|
||||
<path class="fil0" d="M4683.49 1194.24l0 168.28c0,100.92 -81.7,178.37 -182.67,178.37l-589.87 1.23c-93.18,0 -170.28,-69.96 -181.63,-160.09l0 -458.13c11.36,-90.1 88.46,-160.07 181.63,-160.07l589.44 3.5c100.97,0 183.1,82.18 183.1,183.1l0 30.42 0 213.4zm-1230.2 -345.53l-0.89 -795.03 276.91 -53.68 0 526.07c55.74,-24.16 117.03,-37.74 181.5,-37.74l589.71 3.5c252.69,0 458.42,205.72 458.42,458.59l0 30.42 0 213.4 0 168.28c0,252.86 -205.73,458.54 -458.42,458.54l-589.71 -3.5c-252.7,0 -458.41,-205.67 -458.41,-458.56l0 -171.61 0 -240.48 0.89 -98.2z"/>
|
||||
<path class="fil0" d="M3041.25 1150.84l0 204.28c0,100.93 -82.15,183.05 -183.11,183.05l-582.11 -3.46c-100.96,0 -183.11,-82.16 -183.11,-183.08l0 -67.42 1.48 0.24 0 -862.65 -276.91 53.68 0.12 103.41 -0.12 0.04 0 772.69c0,252.87 205.72,458.54 458.39,458.54l582.4 3.5c252.67,0 458.4,-205.68 458.4,-458.55l0 -70.79 0.71 0.12 0 -862.65 -276.91 53.68 0.76 675.35z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,69 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 1060" width="100%" role="img" aria-label="Схема пайплайна IoT">
|
||||
<defs>
|
||||
<style>
|
||||
text { font-family: system-ui, "Segoe UI", Roboto, sans-serif; }
|
||||
.mono { font-family: ui-monospace, Menlo, Consolas, monospace; }
|
||||
</style>
|
||||
<marker id="arr" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||
<path d="M0,0 L8,3 L0,6 Z" fill="#6b7280"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Подложка «ПЛАТФОРМА» -->
|
||||
<text x="104" y="222" font-size="12" fill="#6b7280">ПЛАТФОРМА</text>
|
||||
<rect x="100" y="232" width="440" height="568" rx="16" fill="none"
|
||||
stroke="#bfdbfe" stroke-width="1.5" stroke-dasharray="6 4"/>
|
||||
|
||||
<!-- Переход 1: Устройство → EMQX -->
|
||||
<line x1="320" y1="158" x2="320" y2="242" stroke="#6b7280" stroke-width="2" marker-end="url(#arr)"/>
|
||||
<rect x="175" y="187" width="130" height="26" rx="8" fill="#ffffff" stroke="#d1d5db"/>
|
||||
<text x="240" y="205" font-size="14" fill="#1a1a1a" text-anchor="middle">MQTT over wss</text>
|
||||
<text x="345" y="196" font-size="13" class="mono" fill="#6b7280">{namespace}/</text>
|
||||
<text x="345" y="216" font-size="13" class="mono" fill="#6b7280">telemetry/{device_id}</text>
|
||||
|
||||
<!-- Переход 2: EMQX → Очередь -->
|
||||
<line x1="320" y1="368" x2="320" y2="452" stroke="#6b7280" stroke-width="2" marker-end="url(#arr)"/>
|
||||
<rect x="200" y="397" width="80" height="26" rx="8" fill="#ffffff" stroke="#d1d5db"/>
|
||||
<text x="240" y="415" font-size="14" fill="#1a1a1a" text-anchor="middle">бридж</text>
|
||||
<text x="345" y="416" font-size="13" fill="#6b7280">кладёт показания</text>
|
||||
|
||||
<!-- Переход 3: Очередь → PostgreSQL -->
|
||||
<line x1="320" y1="578" x2="320" y2="662" stroke="#6b7280" stroke-width="2" marker-end="url(#arr)"/>
|
||||
<rect x="175" y="607" width="130" height="26" rx="8" fill="#ffffff" stroke="#d1d5db"/>
|
||||
<text x="240" y="625" font-size="14" fill="#1a1a1a" text-anchor="middle">потребитель</text>
|
||||
<text x="345" y="626" font-size="13" fill="#6b7280">пишет в базу</text>
|
||||
|
||||
<!-- Переход 4: PostgreSQL → API / Консоль -->
|
||||
<line x1="320" y1="788" x2="320" y2="872" stroke="#6b7280" stroke-width="2" marker-end="url(#arr)"/>
|
||||
<rect x="205" y="817" width="70" height="26" rx="8" fill="#ffffff" stroke="#d1d5db"/>
|
||||
<text x="240" y="835" font-size="14" fill="#1a1a1a" text-anchor="middle">REST</text>
|
||||
<text x="345" y="836" font-size="13" fill="#6b7280">чтение истории</text>
|
||||
|
||||
<!-- Блок 1: Устройство -->
|
||||
<rect x="110" y="40" width="420" height="110" rx="12" fill="#ffffff" stroke="#d1d5db"/>
|
||||
<text x="320" y="102" font-size="20" font-weight="600" fill="#1a1a1a" text-anchor="middle">Устройство</text>
|
||||
<text x="320" y="128" font-size="14" class="mono" fill="#6b7280" text-anchor="middle">датчик · MQTT-клиент</text>
|
||||
|
||||
<!-- Блок 2: EMQX -->
|
||||
<rect x="110" y="250" width="420" height="110" rx="12" fill="#eff6ff" stroke="#bfdbfe"/>
|
||||
<text x="320" y="312" font-size="20" font-weight="600" fill="#2563eb" text-anchor="middle">EMQX</text>
|
||||
<text x="320" y="338" font-size="14" class="mono" fill="#6b7280" text-anchor="middle">брокер · wss /mqtt</text>
|
||||
|
||||
<!-- Блок 3: Очередь -->
|
||||
<rect x="110" y="460" width="420" height="110" rx="12" fill="#eff6ff" stroke="#bfdbfe"/>
|
||||
<text x="320" y="522" font-size="20" font-weight="600" fill="#2563eb" text-anchor="middle">Очередь</text>
|
||||
<text x="320" y="548" font-size="14" class="mono" fill="#6b7280" text-anchor="middle">iot-telemetry</text>
|
||||
|
||||
<!-- Блок 4: PostgreSQL -->
|
||||
<rect x="110" y="670" width="420" height="110" rx="12" fill="#eff6ff" stroke="#bfdbfe"/>
|
||||
<text x="320" y="732" font-size="20" font-weight="600" fill="#2563eb" text-anchor="middle">PostgreSQL</text>
|
||||
<text x="320" y="758" font-size="14" class="mono" fill="#6b7280" text-anchor="middle">база на namespace</text>
|
||||
|
||||
<!-- Блок 5: API / Консоль -->
|
||||
<rect x="110" y="880" width="420" height="110" rx="12" fill="#ffffff" stroke="#d1d5db"/>
|
||||
<text x="320" y="942" font-size="20" font-weight="600" fill="#1a1a1a" text-anchor="middle">API / Консоль</text>
|
||||
<text x="320" y="968" font-size="14" class="mono" fill="#6b7280" text-anchor="middle">чтение истории</text>
|
||||
|
||||
<!-- Подвал -->
|
||||
<text x="320" y="1038" font-size="13" fill="#6b7280" text-anchor="middle">бридж и потребитель — части сервиса iot-service</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user