Теплица обросла семью BLE-термометрами LYWSD03MMC, и настал день, когда надо было понять, кто из них вообще жив. Серверную сторону я уже собирал: в уроке про BLE-сервис плата сама рекламирует службы и характеристики. Здесь понадобилась обратная роль — слушающий: найти в эфире все устройства поблизости, отсортировать безымянных соседей от своих термометров и понять, у кого батарейка села. Первая версия сканера выдала в монитор порт несколько сотен строк за пятисекундное окно: три гаджета повторялись десятки раз, а половина строк была без имён.
Разберём, как устроен BLE-скан на ESP32: BLEScan, колбэк onResult с адресом, именем и RSSI, чем активный скан отличается от пассивного и почему без дедупликации адресов журнал превращается в свалку. Отдельно — фильтры: по имени, по UUID сервиса и по уровню сигнала, без которых даже простой вопрос «что вокруг меня» не собирается в вменяемый ответ.
Как BLE-эфир выглядит для слушающего
BLE-устройства не сидят в эфире постоянно — они кричат рекламными пакетами (advertising) с интервалом от 20 миллисекунд до 10 секунд, чаще всего 100 мс - 1 с. Сканер молча слушает три рекламных канала и ловит эти крики. Активный скан (setActiveScan(true)) после рекламного пакета досылает запрос «представься подробнее» и получает scan response — там живут полные имена; пассивный дешевле по эфиру и току, но довольствуется коротким именем из первого пакета, а часто и вовсе безымянным криком. Уровень сигнала прилетает в каждом пакете: RSSI около -40 дБм — устройство в комнате, -70 — норма через квартиру, -90 — за стеной или на границе приёма; от замера к замеру цифра скачет на 5-10 дБ, к порогам её берут с усреднением.
API в Arduino-ядре короткое. BLEDevice::init("") — инициализация, нужна даже сканеру без единого серверного сервиса. Дальше BLEDevice::getScan() отдаёт объект скана, setAdvertisedDeviceCallbacks() вешает ваш колбэк, setActiveScan(true) включает досылку запросов, start(5) слушает эфир пять секунд. Каждая находка — отдельный вызов onResult(BLEAdvertisedDevice dev): адрес через dev.getAddress().toString(), имя через dev.getName() после проверки dev.haveName(), уровень через dev.getRSSI(). Одна оговорка про версии: в ядрах 3.x сигнатуры кочуют — start() возвращает результат по значению, колбэк принимает ссылку; если компилятор ругается, сверьтесь со Scan-примерами из библиотеки, я так и поймал переезд getScanResults().
#include <BLEDevice.h>#include <BLEScan.h> BLEScan* pScan; class Found : public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice dev) override { Serial.print(dev.getAddress().toString().c_str()); Serial.print(" RSSI "); Serial.print(dev.getRSSI()); if (dev.haveName()) { // имя живёт в scan response Serial.print(" "); Serial.print(dev.getName().c_str()); } Serial.println(); }}; void setup() { Serial.begin(115200); BLEDevice::init(""); // сканеру тоже нужна инициализация pScan = BLEDevice::getScan(); pScan->setAdvertisedDeviceCallbacks(new Found()); pScan->setActiveScan(true); // спросить scan response: полные имена} void loop() { Serial.println("--- skan 5 s ---"); BLEScanResults* r = pScan->start(5, false); Serial.print("unikalnyh adresov v etom okne: "); Serial.println(r->getCount()); pScan->clearResults(); // вернуть память под результаты delay(10000); // пауза: эфир, ток и память не бесконечны}Почему в журнале сотни строк, а уникальных устройств пять: каждое BLE-устройство кричит свой пакет десятки раз за окно, и колбэк срабатывает на каждый крик. Ключ дедупликации — строка адреса из getAddress().toString(): собирайте её в свой список и печатайте находку только при первом вхождении за окно, а итоговый счёт уникальных адресов отдаёт r->getCount(). clearResults() обязателен при длинных циклах: без него буфер результатов растёт, и через часы работы сканер упирается в память. Пауза между окнами — тоже часть дизайна: скан держит радио активным на 20-30 мА, и сканировать подряд без пауз значит жечь батарейку впустую.
Фильтры превращают сырую свалку в ответ на конкретный вопрос. По имени: печатают только устройства с префиксом — мои термометры отвечают на LYWSD и ATC. По UUID сервиса: термометры Xiaomi анонсируют сервис FE95, маячки iBeacon — свой фиксированный UUID; проверка dev.haveServiceUUID() и dev.getServiceUUID().toString(). По уровню: порог «ближе -75 дБм» отсеивает соседей за стеной. На этой связке собираются бытовые сценарии: «гость пришёл» — маячок на ключах держит RSSI лучше -70 два скана из трёх; «термометр умер» — числится в списке устройств, но не анонсируется уже час. Про то, как после находки читать с устройства показания по характеристикам, — в уроке про notify и CCCD; про саму логику децибел и усреднения — в диагностике WiFi-RSSI, арифметика та же.
- дедупликация по строке адреса: печатаем первое вхождение, итог — getCount() за окно
- фильтр по имени: dev.haveName() и префикс — LYWSD, ATC, MI; пустые имена отделяем от своих
- фильтр по UUID сервиса: FE95 у термометров Xiaomi, свой UUID у iBeacon-маячков
- порог RSSI -75...-80 дБм с усреднением трёх-пяти замеров против скачков на 5-10 дБ
- окно 5 секунд и пауза 10-30 секунд: скан держит радио на 20-30 мА
- clearResults() в каждом цикле: буфер результатов без очистки съедает память за часы
- 1. Что даёт setActiveScan(true)?
- 2. Почему один и тот же адрес встречается в журнале десятки раз за окно?
- 3. RSSI -40 и -90 — что это значит?
- BLE-устройства анонсируют себя рекламными пакетами; сканер слушает их молча
- setActiveScan(true) досылает запрос на scan response — без него имена будут пустыми
- дедупликация по строке адреса: одно устройство за окно кричит десятки раз
- RSSI -40 рядом, -90 за стеной; скачки 5-10 дБ гасятся усреднением трёх-пяти замеров
- окно 5 с + пауза 10-30 с + clearResults(): экономия эфира, тока и памяти
Частые вопросы по теме урока
- Что даёт setActiveScan(true)?
- Активный скан после каждого рекламного пакета спрашивает scan response — там лежат полные имена. Пассивный дешевле по эфиру, но довольствуется укороченным анонсом, а часто и вовсе безымянным пакетом. сканер досылает запрос после рекламного пакета и получает полное имя из scan response
- Почему один и тот же адрес встречается в журнале десятки раз за окно?
- Рекламный интервал BLE-устройств — от 20 мс до 10 с, и за пятисекундное окно одно устройство успевает крикнуть десятки раз. Дедупликация ведётся по строке адреса из getAddress().toString(). устройство кричит рекламными пакетами с интервалом короче окна — колбэк срабатывает на каждый
- RSSI -40 и -90 — что это значит?
- RSSI отрицательный и измеряется в дБм: чем ближе к нулю, тем сильнее сигнал. -40 — устройство в комнате, -70 — норма через квартиру, -90 — за стеной; цифра скачет на 5-10 дБ между замерами. -40 — устройство рядом, -90 — за стеной или на границе приёма