363 lines
15 KiB
HTML
363 lines
15 KiB
HTML
<!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>
|