Зачем
Базовая Entrixy-схема — это телефон → наш сервер → ESP. Работает везде где есть интернет, но в гараже, на парковке-минусе, в лифте сигнала может не быть. BLE-канал решает «последний метр» — телефон цепляется к ESP напрямую, без вмешательства сервера.
(против 2 с через сервер)
(годы на AA)
интернета и FCM
(настраивается порогом RSSI)
Как это работает
1. ESP-сторона
ESP32-C3 спит в deep-sleep. Раз в секунду просыпается, ~200 мс отвечает в BLE-эфире (advertise со своим device_id и подписанным счётчиком), снова в сон. Никаких WiFi, никаких MQTT. Один общий секрет с приложением хозяина в NVS.
2. Телефон-сторона
Один раз регистрируется системный BluetoothLeScanner.startScan(filters, settings, pendingIntent) с фильтром по нашему service-UUID. Дальше приложение может быть убито Android'ом — Bluetooth-чип сам сканирует на низких токах (так же работает обнаружение наушников), будит систему только когда поймал ESP. Сравнимо по потреблению с тем, что Android и так делает 24/7.
3. Срабатывание
Receiver проверяет подпись advertise, подключается к ESP, читает свежий nonce, шлёт подписанную HMAC fire-команду. ESP проверяет, дёргает реле (импульс 500 мс), обратно в сон. Весь цикл — 200–400 мс.
Получить прошивку
Самый простой путь — конфигуратор в браузере
Откройте /esp/ble/, выберите чип (ESP32-S3, C3, C6, WROOM), укажите пины и полярность реле, нажмите «Скомпилировать». Сервер за 25–60 секунд соберёт уникальный .bin под ваш конфиг, его можно скачать или прошить прямо из браузера через WebSerial. Никакого Arduino IDE, ничего ставить не надо.
Есть готовые пресеты под Shelly Plus 1 / 1PM (с дополнительными клеммами под автоматику ворот) и под самосбор с релейным модулем.
Если хочется собирать вручную — готовый Arduino-скетч, собирается из коробки в Arduino IDE 2.x с Arduino-core for ESP32 v3.x. Внешних библиотек не нужно — всё из штатного core (NimBLE, Preferences, esp_sleep, mbedtls).
Безопасность
Привязка хозяина — только физически
Pair-режим открывается на 30 секунд и только в двух случаях: при первом включении непривязанного устройства (исключение, чтобы не плясать с кнопкой при первой установке) или по физическому нажатию кнопки на уже привязанном. После того как устройство привязано к хозяину — повторная привязка возможна только через long-press 5 сек на кнопке (factory reset, чистит секрет, открывает pair заново). Удалённо инициировать pair-режим невозможно.
Подпись каждой команды
Все advertise и все fire-команды подписаны HMAC-SHA256 общим секретом. Снифферу с BLE-аналайзером ничего не достать — ключ в эфир не уходит, advertise со счётчиком не повторяется, fire-команда привязана к одноразовому nonce.
Передача гостям
Хозяин выпускает гостю подписанный токен с TTL=24ч. Токен прилетает по существующему E2EE-каналу Entrixy. За 2 часа до истечения гостевое приложение само просит у хозяина продление (если хозяин в настройках не снял галочку «BLE-доступ»). Снял галочку → токен у гостя дотягивает до истечения и умирает. Никакого physical access к ESP для отзыва не нужно. На стороне ESP — ноль данных про гостей.
Полная спецификация протокола
Эта секция — однозначная битовая ссылка для разработчика, который хочет написать прошивку своего устройства под Entrixy. Все поля в little-endian. Все строки в HKDF — ASCII без терминирующего нуля.
BLE-имена и режимы
Рабочий режим · имя entrixy. Раз в секунду ~200 мс в эфире, advertise + connectable.
Pair-режим · имя entrixy-pair. Открывается на 30 секунд по физической кнопке (либо сразу при первом включении непривязанного устройства). Не открывается удалённо.
Service UUID
656e7472-7869-7900-0000-426c45000001
Характеристики
UUID Mode Operation 656e7472-7869-7900-0001-426c45000001 Work READ — NONCE (8 байт случайных) 656e7472-7869-7900-0002-426c45000001 Work WRITE — FIRE (24 или 47 байт) 656e7472-7869-7900-0003-426c45000001 Work WRITE — TIME-SYNC (24 байт) 656e7472-7869-7900-00f0-426c45000001 Pair READ — ESP public key (32 байт) 656e7472-7869-7900-00f1-426c45000001 Pair WRITE — phone pub + did (36 байт)
Advertise — manufacturer-specific data
Всего 18 байт на wire, включая 2-байтный Bluetooth SIG company ID. Android-парсер кладёт первые 2 байта в ключ SparseArray<byte[]> и фильтрует наш эфир по нему.
Offset Size Field 0 2 Company ID (LE) = 0xE0 0x00 // на wire два байта подряд 2 4 device_id (LE) // 32-bit, выдан приложением при pair 6 4 counter (LE) // монотонно растёт, anti-replay 10 8 HMAC-SHA256(owner_secret, advertise[2..9])[0..7]
Тонкий момент: HMAC подписывает только 8 байт — это конкатенация device_id и counter. Company ID в подпись не входит.
Fire от хозяина — 24 байта
Owner шлёт WRITE на CHAR_FIRE_UUID после READ NONCE:
Offset Size Field 0 8 nonce (точно то что было прочитано из NONCE) 8 16 HMAC-SHA256(owner_secret, nonce)[0..15]
Fire от гостя — 47 байт
Гость не имеет owner_secret. Вместо него — подписанный GuestToken от хозяина и производный ключ. WRITE на тот же CHAR_FIRE_UUID, диспатч по длине payload:
Offset Size Field 0 7 GuestToken 7 16 owner_sig = HMAC-SHA256(owner_secret, token)[0..15] 23 8 nonce 31 16 guest_sig = HMAC-SHA256(guest_key, nonce)[0..15]
GuestToken (7 байт):
Offset Size Field 0 2 guest_did (LE) // 16-bit, идентифицирует гостя 2 4 valid_until (LE, UNIX epoch sec) // после этой даты ESP откажет 6 1 perms // 0x01 = fire-only, прочие зарезервированы
guest_key выводится через HKDF (см. ниже).
Time-sync — 24 байта
Хозяин подписанной командой корректирует RTC устройства. Без этого TTL гостевых токенов «уплывёт». Гости отправить time-sync не могут — подпись делается owner_secret'ом, у них только производный guest_key.
Offset Size Field 0 8 epoch_ms (LE, UNIX epoch milliseconds) 8 16 HMAC-SHA256(owner_secret, epoch_ms)[0..15]
Pair-flow — последовательность
┌──────────┐ ┌──────────┐
│ ESP │ │ Phone │
└────┬─────┘ └────┬─────┘
│ │
│ (advertise name = "entrixy-pair") │
│ ◄────── BLE discovery + connect ────── │
│ │
│ ─── READ CHAR_PAIR_PUB_UUID ──────► │
│ [esp_pub 32B] │
│ │
│ phone_priv ← random 32B (clamped по RFC 7748)
│ phone_pub ← X25519_base(phone_priv)
│ shared_z ← X25519(phone_priv, esp_pub)
│ owner_secret ← HKDF_SHA256(
│ salt = "entrixy-pair-v1",
│ ikm = shared_z,
│ info = "owner-secret",
│ L = 32)
│ │
│ ◄── WRITE CHAR_PAIR_DONE_UUID ────── │
│ [phone_pub 32B][device_id 4B LE] │
│ │
│ shared_z = X25519(esp_priv, phone_pub)│
│ owner_secret = HKDF(...) │
│ save NVS: {owner_secret, device_id, │
│ counter=0} │
│ │
│ ── BLE disconnect, leave pair-mode │
Fire-flow (хозяин) — последовательность
┌──────────┐ ┌──────────┐
│ ESP │ │ Phone │
└────┬─────┘ └────┬─────┘
│ │
│ advertise (раз в секунду 200мс) │
│ ──[mfg: company=0x00E0, did, ctr, hmac]►│
│ │
│ verify HMAC(owner_secret, did||ctr) │
│ match did → connect │
│ │
│ ◄────── connect, discover ──────── │
│ │
│ ── READ CHAR_NONCE_UUID ────────► │
│ [nonce 8B (random fresh)] │
│ │
│ hmac = HMAC(owner_secret, nonce) │
│ payload = nonce || hmac[0..15] │
│ │
│ ◄── WRITE CHAR_FIRE_UUID ─────────── │
│ [nonce 8B][hmac[0..15] 16B] │
│ │
│ verify nonce + HMAC → fire relay │
│ (опционально) WRITE TIME-SYNC для │
│ корректировки RTC │
│ │
│ ── disconnect ────────────────────► │
Криптопримитивы
HMAC-SHA256 · RFC 2104, тэг 32 байта (мы используем первые 8 или 16). Реализации: mbedtls/md.h на ESP, javax.crypto.Mac("HmacSHA256") на Android.
HKDF-SHA256 · RFC 5869. Extract-then-expand на основе HMAC-SHA256. mbedtls/hkdf.h на ESP, своя реализация поверх Mac на Android.
X25519 / Curve25519 · RFC 7748. Ключи представляются как 32-байтные little-endian X-координаты. На ESP — mbedtls_ecp_* с MBEDTLS_ECP_DP_CURVE25519. На Android (minSdk 26) — org.bouncycastle.math.ec.rfc7748.X25519 (платформенный KeyPairGenerator("XDH") только с API 31).
HKDF — параметры
1) Owner secret из ECDH shared point:
salt = "entrixy-pair-v1" (15 байт ASCII)
ikm = shared_z (32 байта)
info = "owner-secret" (12 байт ASCII)
L = 32
2) Guest key из owner_secret:
salt = null/пустой (зануляется до 32 нулей по RFC 5869)
ikm = owner_secret (32 байта)
info = "guest" || guest_did_LE (5 + 2 = 7 байт)
L = 32
Test vectors
Чтобы проверить что ваша реализация HMAC сходится с нашей:
owner_secret = 0x00000000000000000000000000000000
00000000000000000000000000000000 (32 нулевых байта)
nonce = 0x0102030405060708 (8 байт)
HMAC-SHA256(owner_secret, nonce) =
c59a063ccd78f8ef7da2e361f5027145
c495f0728f0575fe5fe55840daaefa51 (32 байта)
fire payload = nonce || HMAC[0..15] =
0102030405060708
c59a063ccd78f8ef7da2e361f5027145 (24 байт)
Проверка в Python:
import hmac, hashlib
secret = bytes(32)
nonce = bytes.fromhex("0102030405060708")
print(hmac.new(secret, nonce, hashlib.sha256).hexdigest())
Дальность
В настройках BLE-объекта в приложении — ползунок порога RSSI:
(машина рядом)
(дефолт)
(дверь подъезда)
(шлагбаум двора)
Что внутри прошивки
Скетч entrixy-ble.ino реализует полный протокол целиком:
• ECDH X25519 в pair-flow. Хозяин и ESP обмениваются временными публичными ключами через GATT, оба вычисляют общий секрет через mbedtls. В эфир ключ не уходит — снифферу с BLE-аналайзером ничего не достать.
• Гостевые токены с TTL. Отдельная ветка в fire-callback: парсит GuestToken (guest_did + valid_until + perms), проверяет owner_sig, проверяет срок против RTC, выводит производный ключ через HKDF и проверяет guest_sig над nonce.
• RTC через slow memory ESP. Время выживает deep-sleep (RTC_DATA_ATTR). Хозяин на каждом fire-коннекте может скорректировать часы подписанной командой — для проверки TTL гостей точности секунд достаточно.
• Pair-кнопка с factory reset. Короткое нажатие на непривязанном ESP — войти в pair-режим. Long-press 5 сек на привязанном — чистка NVS и заход в pair заново.
Если хочешь поучаствовать или поковыряться — скетч лежит, форкай и пиши.
Когда BLE не подходит
WiFi+интернет всегда есть на месте установки, и хочется управлять не только вблизи, а с любой точки мира — посмотрите WebSocket-вариант прошивки. Тот же контроллер на ESP32 или ESP8266, но устройство постоянно онлайн через WSS к серверу, открытие срабатывает откуда угодно с интернета.
Можно поставить и BLE и WS параллельно: BLE — для близкого срабатывания без интернета и батарейки, WS — для удалённого открытия гостям.
Не хочется ничего прошивать вообще — есть путь через веб-хук Entrixy в готовое облако или Tasmota/ESPHome устройства, см. /integrations.