Руководства · 14 мин чтения

Bluetooth на ESP32: Classic и BLE, управление светодиодом и датчики

Bluetooth на ESP32: управляем светодиодом с телефона через BluetoothSerial, создаём BLE-сервер с сервисами и UUID, проверяем в nRF Connect и отправляем данные датчика.

Bluetooth на ESP32 использует встроенный в чип беспроводной радиомодуль, чтобы подключить ESP32 к телефону, планшету или компьютеру без дополнительных модулей. Оригинальная ESP32 поддерживает сразу два разных вида Bluetooth: Bluetooth Classic, который работает как последовательный порт, и Bluetooth Low Energy (BLE), который потребляет очень мало энергии. Для связи Arduino Uno с телефоном нужен отдельный модуль HC-05, а на ESP32 хватит нескольких строк кода. В этом руководстве шаг за шагом настроим оба способа, включим светодиод с телефона, напишем собственный BLE-сервис и отправим данные датчика на телефон.

Выбор способа зависит от твоего телефона и платы. Если у тебя Android-смартфон и классическая плата ESP32 DevKit, быстрее всего начать с библиотеки BluetoothSerial. Если у тебя iPhone или плата ESP32-S3 либо ESP32-C3, классический Bluetooth не поможет — нужен BLE. Ближе к концу есть и тот же BLE-проект для Raspberry Pi Pico W на MicroPython. Если ESP32 в твоих руках впервые, сначала закончи настройку Arduino IDE из статьи Что такое ESP32?.

Отладочная плата ESP32 с Bluetooth и BLE
Отладочная плата ESP32: Wi-Fi, Bluetooth Classic и BLE находятся внутри одного чипа, поэтому для подключения к телефону внешний Bluetooth-модуль не нужен.

Что такое Bluetooth на ESP32? Classic и BLE

Bluetooth Classic — старый и привычный вид Bluetooth, который используется в беспроводных наушниках и модулях вроде HC-05. ESP32 работает с ним через SPP (Serial Port Profile): байты идут один за другим, словно между телефоном и платой протянут невидимый последовательный кабель. Сначала выполни сопряжение устройства в настройках Bluetooth телефона, затем подключись через приложение-терминал. BLE же — отдельная система, которая появилась в 2010 году вместе с Bluetooth 4.0 и создана для устройств, месяцами работающих от батарейки. В BLE нет непрерывного потока данных: ESP32 хранит небольшую таблицу значений (GATT), а телефон читает их, записывает или подписывается на них, чтобы узнавать об изменениях. Несмотря на похожие названия, эти два вида не могут общаться друг с другом: приложение, написанное для BLE, не подключится к устройству с классическим Bluetooth.

ПараметрBluetooth Classic (SPP)Bluetooth Low Energy (BLE)
Библиотека ArduinoBluetoothSerial (входит в пакет ESP32)BLEDevice (входит в пакет ESP32) или NimBLE-Arduino
Модель данныхНепрерывный поток байтов, как у последовательного портаТаблица GATT из сервисов и характеристик
Объём данныхБольшой, подходит для непрерывных потоковНебольшие пакеты; по умолчанию 20 байт на уведомление
ЭнергопотреблениеПостоянно высокое во время подключенияОчень низкое; устройство может месяцами работать от батарейки
СопряжениеВыполняется в настройках Bluetooth телефонаНе нужно; подключение идёт внутри приложения
AndroidПоддерживаетсяПоддерживается
iPhone (iOS)Не поддерживаетсяПоддерживается
Какие ESP32 поддерживаютТолько оригинальная ESP32 (WROOM-32, WROVER)ESP32, S3, C3, C6 и H2 (кроме S2)
Типичное применениеПоследовательный терминал, робот-машинка, замена HC-05Датчики, носимые устройства, умный дом, маяки (beacon)

Практическое правило такое: если нужно отправлять и получать текст с Android-смартфона и быстро увидеть результат, бери Classic; если нужна поддержка iPhone, низкое энергопотребление или одна из новых плат ESP32 — BLE. Для нового проекта BLE стоит изучить: он работает на обоих типах телефонов и есть во всех новых чипах Espressif с Bluetooth. Если с HC-05 уже доводилось работать, станет видно, что логика команд из статьи Bluetooth-модуль HC-05 в BluetoothSerial точно такая же.

Какой Bluetooth есть в какой плате ESP32?

Не у всех чипов семейства ESP32 одинаковое Bluetooth-оборудование. Самая частая ошибка — пытаться загрузить найденный в интернете пример BluetoothSerial в плату ESP32-S3 или ESP32-C3. У Espressif классический Bluetooth есть только в оригинальном чипе ESP32; более поздние чипы с Bluetooth поддерживают только BLE. Какой чип стоит на плате, видно по надписи на металлическом экране модуля (например, ESP32-WROOM-32 или ESP32-S3-WROOM-1).

Плата / чипBluetooth ClassicBLEПримечание
ESP32 (WROOM-32, WROVER, DevKit V1)ЕстьЕсть (4.2)Единственное семейство ESP32, на котором работает BluetoothSerial
ESP32-S2НетНетТолько Wi-Fi
ESP32-S3НетЕсть (Bluetooth 5)BluetoothSerial не скомпилируется, используй BLE
ESP32-C3НетЕсть (Bluetooth 5)Маленькие и дешёвые BLE-платы
ESP32-C6НетЕсть (Bluetooth 5)Есть ещё Wi-Fi 6, Zigbee и Thread
ESP32-H2НетЕсть (Bluetooth 5)Без Wi-Fi; ориентирована на Zigbee и Thread
ESP8266 (NodeMCU, Wemos D1)НетНетДля Bluetooth нужен HC-05 или HM-10
Raspberry Pi Pico W / Pico 2 WЕсть аппаратноЕстьMicroPython поддерживает только BLE
Raspberry Pi Pico / Pico 2НетНетНужен внешний Bluetooth-модуль
Если скомпилировать код BluetoothSerial, когда в меню плат выбрана ESP32-S3, C3, C6 или H2, проверочная строка в примерах Espressif (и в нашем коде ниже) остановит сборку с ошибкой «Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip.» (Bluetooth Serial недоступен, он есть только в чипе ESP32). Это не проблема установки или библиотеки: в чипе просто нет оборудования классического Bluetooth. Если у тебя одна из этих плат, сразу переходи к примеру BLE (Nordic UART) ниже.

Bluetooth Classic: управление светодиодом с телефона через BluetoothSerial

Библиотека BluetoothSerial представляет ESP32 телефону как модуль HC-05 и использует те же команды, что и объект Serial: available(), read(), print() и println(). Библиотека входит в пакет плат ESP32, так что ставить ничего не нужно. Для схемы хватит синего встроенного светодиода, который на большинстве плат DevKit подключён к GPIO 2; если используешь внешний светодиод, подключи его к GPIO 2 через резистор 220 Ω. Номинал резистора можно проверить калькулятором резистора для светодиода, а другие безопасные пины ESP32 посмотреть в утилите распиновки. Убедись, что в «Инструменты › Плата» (Tools › Board) выбрана ESP32 Dev Module, и загрузи этот код:

C++ (Arduino IDE)
1#include "BluetoothSerial.h"
2
3// Klasik Bluetooth yalnızca ilk ESP32'de var / Classic Bluetooth exists only on the original ESP32
4#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
5#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
6#endif
7#if !defined(CONFIG_BT_SPP_ENABLED)
8#error Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip.
9#endif
10
11BluetoothSerial SerialBT;
12const int LED_PIN = 2;                 // DevKit dahili LED / DevKit onboard LED
13bool ledAcik = false;
14
15void ledAyarla(bool acik) {
16  ledAcik = acik;
17  digitalWrite(LED_PIN, acik ? HIGH : LOW);
18  SerialBT.println(acik ? "LED acik / LED on" : "LED kapali / LED off");
19}
20
21void setup() {
22  Serial.begin(115200);
23  pinMode(LED_PIN, OUTPUT);
24  SerialBT.begin("BlueGrays-ESP32");   // telefonda görünecek ad / name shown on the phone
25  Serial.println("Eslesmeye hazir / Ready to pair");
26}
27
28void loop() {
29  if (SerialBT.available()) {
30    String komut = SerialBT.readStringUntil('\n');  // satır sonuna kadar oku / read until newline
31    komut.trim();                                   // CR ve boşlukları sil / strip CR and spaces
32    komut.toLowerCase();
33
34    if (komut == "1" || komut == "ac") {
35      ledAyarla(true);
36    } else if (komut == "0" || komut == "kapat") {
37      ledAyarla(false);
38    } else if (komut == "durum") {
39      SerialBT.println(ledAcik ? "LED acik / LED on" : "LED kapali / LED off");
40    } else if (komut.length() > 0) {
41      SerialBT.print("Bilinmeyen komut / Unknown command: ");
42      SerialBT.println(komut);
43    }
44  }
45
46  if (Serial.available()) {            // Seri Monitör'den telefona / Serial Monitor to phone
47    SerialBT.write(Serial.read());
48  }
49}

Код читает каждую строку, пришедшую с телефона, как команду. readStringUntil() ждёт символа перевода строки, а trim() убирает добавленный телефоном возврат каретки (CR) и пробелы; без этой строки при отправке «1» на самом деле приходит «1» плюс невидимый символ CR, и ни одна команда не совпадает. Команды переводятся в нижний регистр, поэтому «AC» и «ac» работают одинаково. Две строки внизу пересылают на телефон всё, что набрано в мониторе порта, — так получается беспроводной чат между компьютером и телефоном. Проверки #error в начале кода останавливают сборку с понятным сообщением, если выбрана не та плата.

Подключение с телефона через Serial Bluetooth Terminal

  1. После загрузки открой монитор порта на скорости 115200 бод и найди сообщение «Eslesmeye hazir / Ready to pair».
  2. На Android-смартфоне запусти поиск новых устройств в разделе «Настройки › Bluetooth» и выполни сопряжение с BlueGrays-ESP32. По умолчанию ESP32 сопрягается без PIN-кода; если телефон показывает код подтверждения, подтверди его.
  3. Установи из Google Play приложение Serial Bluetooth Terminal от Kai Morich.
  4. В приложении открой в боковом меню раздел Devices (устройства) и выбери BlueGrays-ESP32 на вкладке Bluetooth Classic.
  5. Нажми значок штекера на верхней панели; когда на экране появится «Connected», подключение готово.
  6. Набери 1 в поле внизу и отправь: светодиод загорится, и появится ответ «LED acik / LED on». 0 выключает его, а durum показывает текущее состояние.
Долгим нажатием на макрокнопки M1 и M2 внизу Serial Bluetooth Terminal сохрани команды «1» и «0», чтобы переключать светодиод одним касанием. Параметр Send › Newline в настройках приложения по умолчанию равен CR+LF; trim() в коде уже убирает лишний CR. Если вместо светодиода подключить реле, тот же код будет включать лампу или розетку; подробности — в статье Что такое модуль реле?.

Как работает BLE? Сервисы, характеристики и UUID

В BLE две роли. ESP32 играет роль сервера (peripheral): хранит значения и сообщает «я здесь», отправляя через короткие интервалы небольшие рекламные пакеты (advertising) длиной не более 31 байта (на ESP32 по умолчанию каждые 20–40 мс). Телефон играет роль клиента (central): сканирует эфир, находит устройство и подключается. После подключения телефон читает таблицу GATT (Generic Attribute Profile) ESP32. Представь эту таблицу как картотечный шкаф: каждый ящик — это сервис, а каждая папка в ящике — характеристика. Характеристика хранит одно значение (например, температуру или команду для светодиода) и свойства, которые определяют, что с этим значением можно делать.

ТерминЗначениеПример в этой статье
Реклама (advertising)Короткий пакет, в котором устройство сообщает своё имя и сервисыРеклама под именем «BlueGrays-BLE»
СервисГруппа, объединяющая связанные характеристикиСервис Nordic UART, Environmental Sensing (0x181A)
ХарактеристикаОдно значение, которое читают или записываютКоманда для светодиода, температура (0x2A6E)
READ (чтение)Телефон читает значение, когда захочетПоследняя измеренная температура
WRITE / WRITE_NR (запись)Телефон записывает значение (с ответом или без ответа)Запись «1» включает светодиод
NOTIFY (уведомление)ESP32 сама сообщает телефону об изменении значенияНовая температура каждые 2 с
Дескриптор 0x2902 (CCCD)Переключатель, который включает и выключает уведомленияКнопка с тройной стрелкой в nRF Connect
UUIDИдентификационный номер сервисов и характеристик6E400001-B5A3-F393-E0A9-E50E24DCCA9E

Каждый сервис и каждая характеристика определяются своим UUID. Стандартные функции, описанные Bluetooth SIG, используют короткие 16-битные номера: например, 0x180F для сервиса батареи и 0x2A6E для характеристики температуры. Для своих данных, у которых нет стандартного аналога, сгенерируй случайный 128-битный UUID; в Python для этого достаточно import uuid; print(uuid.uuid4()). Выбирая свой UUID, не используй значения по шаблону xxxxxxxx-0000-1000-8000-00805F9B34FB: этот шаблон зарезервирован за 16-битными номерами Bluetooth SIG. А чтобы без проблем работать с готовыми приложениями, ещё удобнее взять UUID, которые знают все:

Сервис / характеристикаUUIDФормат данных
Battery Service / Battery Level (уровень заряда)0x180F / 0x2A191 байт, проценты (0–100)
Environmental Sensing / Temperature (температура)0x181A / 0x2A6E2-байтовое целое со знаком, 0,01 °C
Environmental Sensing / Humidity (влажность)0x181A / 0x2A6F2-байтовое целое без знака, 0,01%
Heart Rate / Heart Rate Measurement (пульс)0x180D / 0x2A37Байт флагов + значение пульса
Nordic UART Service (NUS)6E400001-B5A3-F393-E0A9-E50E24DCCA9ERX 6E400002 (запись), TX 6E400003 (уведомление)

BLE-сервер на ESP32: управление светодиодом с телефона (Nordic UART)

В первом BLE-проекте превратим ESP32 в сервер Nordic UART Service (NUS). NUS — сервис «последовательный порт поверх BLE» от Nordic Semiconductor, ставший стандартом де-факто: телефон пишет в характеристику RX, а ESP32 отправляет уведомления из характеристики TX. Его плюс в том, что его понимают многие готовые приложения: nRF Connect, вкладка Bluetooth LE в Serial Bluetooth Terminal и Bluefruit Connect на iPhone работают с одним и тем же кодом. Значит, код из этого раздела работает и с Android, и с iPhone, а также на платах только с BLE, таких как ESP32-S3 и C3. Библиотека BLE входит в пакет плат ESP32, так что ставить ничего не нужно.

C++ (Arduino IDE)
1#include <BLEDevice.h>
2#include <BLEServer.h>
3#include <BLEUtils.h>
4#include <BLE2902.h>
5
6// Nordic UART Service UUID'leri / Nordic UART Service UUIDs
7#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
8#define RX_UUID      "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"  // telefon -> ESP32 / phone -> ESP32
9#define TX_UUID      "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"  // ESP32 -> telefon / ESP32 -> phone
10
11const int LED_PIN = 2;
12
13BLEServer *sunucu = nullptr;
14BLECharacteristic *txKarakteristik = nullptr;
15volatile bool bagli = false;
16bool oncekiBagli = false;
17volatile int bekleyenKomut = -1;          // -1: yeni komut yok / no new command
18
19class SunucuOlaylari : public BLEServerCallbacks {
20  void onConnect(BLEServer *s) { bagli = true; }
21  void onDisconnect(BLEServer *s) { bagli = false; }
22};
23
24class RxOlaylari : public BLECharacteristicCallbacks {
25  void onWrite(BLECharacteristic *c) {
26    String deger = c->getValue().c_str(); // çekirdek 2.x ve 3.x ile uyumlu / works on core 2.x and 3.x
27    deger.trim();
28    if (deger == "1") bekleyenKomut = 1;
29    else if (deger == "0") bekleyenKomut = 0;
30  }
31};
32
33void cevapGonder(const char *mesaj) {
34  txKarakteristik->setValue((uint8_t *)mesaj, strlen(mesaj));
35  txKarakteristik->notify();              // telefona bildirim / notify the phone
36}
37
38void setup() {
39  Serial.begin(115200);
40  pinMode(LED_PIN, OUTPUT);
41
42  BLEDevice::init("BlueGrays-BLE");       // yayında görünecek ad / advertised name
43  sunucu = BLEDevice::createServer();
44  sunucu->setCallbacks(new SunucuOlaylari());
45
46  BLEService *servis = sunucu->createService(SERVICE_UUID);
47
48  txKarakteristik = servis->createCharacteristic(TX_UUID, BLECharacteristic::PROPERTY_NOTIFY);
49  txKarakteristik->addDescriptor(new BLE2902());   // bildirim anahtarı (CCCD) / notification switch (CCCD)
50
51  BLECharacteristic *rx = servis->createCharacteristic(
52    RX_UUID, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
53  rx->setCallbacks(new RxOlaylari());
54
55  servis->start();
56
57  BLEAdvertising *yayin = BLEDevice::getAdvertising();
58  yayin->addServiceUUID(SERVICE_UUID);
59  yayin->setScanResponse(true);
60  yayin->setMinPreferred(0x06);           // iPhone bağlantısına yardımcı / helps iPhone connections
61  yayin->setMaxPreferred(0x12);
62  BLEDevice::startAdvertising();
63  Serial.println("BLE yayini basladi / BLE advertising started");
64}
65
66void loop() {
67  if (bekleyenKomut != -1) {              // telefondan komut geldi / command received
68    int komut = bekleyenKomut;
69    bekleyenKomut = -1;
70    digitalWrite(LED_PIN, komut == 1 ? HIGH : LOW);
71    if (bagli) cevapGonder(komut == 1 ? "LED acik" : "LED kapali");
72  }
73
74  if (!bagli && oncekiBagli) {            // bağlantı koptu / disconnected
75    delay(500);                           // yığına zaman tanı / give the stack time
76    sunucu->startAdvertising();           // yeniden görünür ol / advertise again
77    Serial.println("Yeniden yayin / Advertising again");
78    oncekiBagli = false;
79  }
80  if (bagli && !oncekiBagli) {            // yeni bağlantı / new connection
81    Serial.println("Telefon baglandi / Phone connected");
82    oncekiBagli = true;
83  }
84}

Самая важная часть кода — классы обратных вызовов (callback). onConnect и onDisconnect отслеживают состояние подключения, а onWrite ловит значение, записанное телефоном в характеристику RX. Эти функции выполняются в собственной задаче стека Bluetooth, поэтому вместо долгой работы внутри них мы только меняем переменную, а основную работу делает loop(). Дескриптор BLE2902, добавленный к характеристике TX, нужен, чтобы телефон мог включить уведомления. Когда соединение рвётся, ESP32 сама не возобновляет рекламу; без строки startAdvertising() в loop() устройство после первого подключения больше не найти. Строки setMinPreferred и setMaxPreferred добавляют в рекламный пакет предпочтительный интервал соединения, и с iPhone подключаться становится проще.

Код в этой статье скомпилирован с версией 2.0.11 ядра ESP32 для Arduino (пакета плат). В этой версии BLE-функции вроде getValue() возвращают std::string, и большинство примеров в интернете тоже используют этот тип. С ядром 3.0 BLE-функции перешли на тип Arduino String, поэтому код с std::string на 3.x не компилируется с ошибкой «conversion from 'String' to non-scalar type 'std::string' requested» (нельзя преобразовать String в std::string). Если, как в нашем коде, присвоить результат переменной типа String и добавить в конце .c_str(), строка скомпилируется и на ядре 2.x, и на 3.x.

Пошаговая проверка BLE в nRF Connect

  1. Установи на телефон бесплатное приложение nRF Connect for Mobile от Nordic Semiconductor; оно есть для Android и iPhone.
  2. На Android приложение запрашивает доступ к Bluetooth и разрешение Устройства поблизости (Nearby devices), а на Android 11 и более ранних версиях — доступ к геолокации при включённых службах геолокации; без этого список сканирования останется пустым.
  3. На вкладке Scanner нажми SCAN и найди в списке BlueGrays-BLE. Значение RSSI рядом — это уровень сигнала: −50 dBm означает «очень близко», −90 dBm — граница дальности.
  4. Нажми CONNECT. На открывшемся экране виден Nordic UART Service; с собственными сгенерированными UUID здесь было бы написано «Unknown Service» (неизвестный сервис).
  5. Нажми значок тройной стрелки вниз рядом с характеристикой TX (6E400003), чтобы включить уведомления.
  6. Нажми значок стрелки вверх рядом с характеристикой RX (6E400002), выбери тип данных Text (UTF-8), набери 1 и отправь. Светодиод загорится, а в строке TX появится «LED acik».

Этот тест — самый быстрый способ убедиться, что сторона ESP32 работает, ещё до написания приложения для телефона. Если что-то не так, сначала проверь всё в nRF Connect: если устройства нет в списке, проблема в рекламе; если подключение есть, а сервис не виден, пропущен вызов start() сервиса; если значение записано, а светодиод не горит, проверь сравнение внутри onWrite. Тот же код можно попробовать и на вкладке Bluetooth LE в Serial Bluetooth Terminal: приложение распознаёт сервис NUS, и переписка идёт так же, как с классическим Bluetooth. Если хочется сделать собственное Android-приложение, загляни в статью MIT App Inventor и Arduino по Bluetooth.

Передача данных датчика по BLE (DHT22)

Датчик температуры и влажности DHT22
DHT22 измеряет температуру с разрешением 0,1 °C; он работает от 3,3 V, поэтому подключается к ESP32 напрямую.
Пин DHT22ESP32Примечание
VCC (+)3V3Работает от 3,3 до 5 V
DATA (OUT)GPIO 4Для голого датчика добавь резистор 10 kΩ между DATA и 3V3
GND (−)GNDОбщая земля

В этом примере используем стандартный сервис Environmental Sensing (0x181A) и его характеристики температуры (0x2A6E) и влажности (0x2A6F). Прелесть стандартных UUID в том, что приложения вроде nRF Connect распознают значение и сразу показывают его в виде «23.45 °C». Стандарт требует отправлять значения не текстом, а целыми числами, умноженными на 100: температура 23,45 °C превращается в число 2345 и отправляется в виде 2 байт, младшим байтом вперёд (little-endian). Для чтения датчика используем библиотеку Adafruit DHT sensor library, которую мы разбирали в руководстве по DHT11; её можно установить через «Менеджер библиотек» (Library Manager).

C++ (Arduino IDE)
1#include <BLEDevice.h>
2#include <BLEServer.h>
3#include <BLE2902.h>
4#include <DHT.h>
5
6#define DHT_PIN 4
7DHT dht(DHT_PIN, DHT22);
8
9BLEServer *sunucu = nullptr;
10BLECharacteristic *sicaklikKar = nullptr;
11BLECharacteristic *nemKar = nullptr;
12volatile bool bagli = false;
13bool oncekiBagli = false;
14unsigned long sonOlcum = 0;
15
16class SunucuOlaylari : public BLEServerCallbacks {
17  void onConnect(BLEServer *s) { bagli = true; }
18  void onDisconnect(BLEServer *s) { bagli = false; }
19};
20
21BLECharacteristic *karakteristikEkle(BLEService *servis, uint16_t uuid) {
22  BLECharacteristic *k = servis->createCharacteristic(
23    BLEUUID(uuid), BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
24  k->addDescriptor(new BLE2902());        // bildirim için gerekli / needed for notify
25  return k;
26}
27
28void setup() {
29  Serial.begin(115200);
30  dht.begin();
31
32  BLEDevice::init("BlueGrays-Sensor");
33  sunucu = BLEDevice::createServer();
34  sunucu->setCallbacks(new SunucuOlaylari());
35
36  BLEService *servis = sunucu->createService(BLEUUID((uint16_t)0x181A));  // Environmental Sensing
37  sicaklikKar = karakteristikEkle(servis, 0x2A6E);  // sıcaklık, 0,01 °C / temperature, 0.01 °C
38  nemKar = karakteristikEkle(servis, 0x2A6F);       // nem, %0,01 / humidity, 0.01 %
39  servis->start();
40
41  BLEAdvertising *yayin = BLEDevice::getAdvertising();
42  yayin->addServiceUUID(BLEUUID((uint16_t)0x181A));
43  yayin->setScanResponse(true);
44  BLEDevice::startAdvertising();
45}
46
47void loop() {
48  if (millis() - sonOlcum >= 2000) {      // DHT22 en fazla 2 sn'de bir / DHT22 at most every 2 s
49    sonOlcum = millis();
50    float t = dht.readTemperature();
51    float h = dht.readHumidity();
52    if (!isnan(t) && !isnan(h)) {
53      int16_t t100 = (int16_t)lroundf(t * 100);    // 23,45 °C -> 2345
54      uint16_t h100 = (uint16_t)lroundf(h * 100);  // %41,2 -> 4120
55      sicaklikKar->setValue((uint8_t *)&t100, 2);  // 2 bayt, düşük bayt önce / 2 bytes, little-endian
56      nemKar->setValue((uint8_t *)&h100, 2);
57      if (bagli) {
58        sicaklikKar->notify();
59        nemKar->notify();
60      }
61      Serial.printf("%.2f C  %.2f %%\n", t, h);
62    }
63  }
64
65  if (!bagli && oncekiBagli) {            // bağlantı koptu / disconnected
66    delay(500);
67    sunucu->startAdvertising();           // yeniden yayın / advertise again
68    oncekiBagli = false;
69  }
70  if (bagli && !oncekiBagli) oncekiBagli = true;
71}

Код делает измерение каждые две секунды, потому что DHT22 нельзя опрашивать чаще. Сначала значения записываются в характеристику через setValue(), поэтому при чтении (READ) телефон получает последнее измерение, не дожидаясь уведомления. Если телефон подключён, notify() сразу отправляет новое значение. Если nRF Connect не может расшифровать значение, видны сырые байты: для 23,45 °C на экране будет (0x) 29-09. Переставь байты в обратном порядке — получится 0x0929, то есть 2345 в десятичной системе; раздели на 100, и вот температура. В собственном приложении нужен будет тот же расчёт. Отправлять значение текстом может показаться проще, но стандартный формат занимает меньше байтов и работает с готовыми приложениями.

Отправлять данные можно и без подключения: ESP32 может поместить измерение прямо в поле данных производителя (manufacturer data) рекламного пакета, и любой телефон или шлюз поблизости прочитает его без подключения. Такой способ называется маяк (beacon), и открытый формат BTHome, который распознаёт Home Assistant, работает так же. Если ESP32 после каждого измерения уходит в глубокий сон, батарейки хватает на месяцы; как это сделать — в статье Глубокий сон ESP32.

Bluetooth ESP32 и iPhone (iOS)

Чаще всего владельцы iPhone спрашивают, почему ESP32 не видна в настройках Bluetooth. Apple открывает профиль последовательного порта классического Bluetooth (SPP) только для аксессуаров с сертификацией MFi, поэтому к ESP32 с BluetoothSerial с iPhone не подключиться. Решение — BLE, и примеры с NUS и датчиком выше отлично работают на iPhone. Есть несколько моментов, о которых стоит помнить:

  • Поиск в настройках: BLE-устройства не отображаются в настройках Bluetooth iOS; подключайся внутри приложения, например nRF Connect, LightBlue или Bluefruit Connect.
  • Кэш: iOS запоминает имя устройства и список его сервисов. Если после изменения сервисов в коде всё ещё виден старый список, выключи и снова включи Bluetooth на телефоне.
  • Нет MAC-адреса: iOS не показывает приложениям MAC-адрес устройства и присваивает каждому устройству собственный идентификатор; если плат ESP32 несколько, дай каждой своё имя.
  • Интервал соединения: не удаляй строки setMinPreferred(0x06) и setMaxPreferred(0x12); примеры Espressif тоже рекомендуют их против проблем с подключением iPhone.
  • Своё приложение: на iOS используется Core Bluetooth со Swift; сопряжение не нужно, приложение сканирует эфир сразу по UUID сервиса.

BLE на Raspberry Pi Pico W с MicroPython

Плата Raspberry Pi Pico
В семействе Raspberry Pi Pico Bluetooth есть только у плат с буквой W в названии (Pico W, Pico 2 W); у стандартной Pico беспроводной связи нет.

В семействе Raspberry Pi Pico Bluetooth есть только у Pico W и Pico 2 W. Установленный на плате чип CYW43439 аппаратно поддерживает и классический Bluetooth, и BLE, но MicroPython использует только BLE (начиная с версии 1.20). Код ниже — аналог примера NUS для ESP32 на MicroPython; он использует те же UUID, поэтому проверяется так же — через nRF Connect и Serial Bluetooth Terminal. На плату должна быть загружена прошивка MicroPython, собранная для Pico W. Код также читает встроенный датчик температуры платы и каждые 2 секунды отправляет значение на подключённый телефон.

MicroPython
1import bluetooth
2import struct
3import time
4from machine import Pin, ADC
5from micropython import const
6
7_IRQ_CENTRAL_CONNECT = const(1)
8_IRQ_CENTRAL_DISCONNECT = const(2)
9_IRQ_GATTS_WRITE = const(3)
10_FLAG_READ = const(0x0002)
11_FLAG_WRITE_NO_RESPONSE = const(0x0004)
12_FLAG_WRITE = const(0x0008)
13_FLAG_NOTIFY = const(0x0010)
14
15# Nordic UART Service: ESP32 örneğiyle aynı UUID'ler / same UUIDs as the ESP32 example
16NUS_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
17TX = (bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), _FLAG_READ | _FLAG_NOTIFY)
18RX = (bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), _FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE)
19
20led = Pin("LED", Pin.OUT)       # Pico W dahili LED / Pico W onboard LED
21sensor = ADC(4)                 # dahili sıcaklık sensörü / internal temperature sensor
22
23ble = bluetooth.BLE()
24ble.active(True)
25((tx_h, rx_h),) = ble.gatts_register_services(((NUS_UUID, (TX, RX)),))
26baglanti = None
27
28
29def alan(tur, veri):            # uzunluk + tür + veri / length + type + data
30    return struct.pack("BB", len(veri) + 1, tur) + veri
31
32
33YAYIN = alan(0x01, b"\x06") + alan(0x09, b"BlueGrays-Pico")  # bayraklar + ad / flags + name
34YANIT = alan(0x07, bytes(NUS_UUID))                          # 128 bit UUID / 128-bit UUID
35
36
37def yayinla():
38    ble.gap_advertise(100000, adv_data=YAYIN, resp_data=YANIT)  # 100 ms aralık / interval
39
40
41def olay(event, data):
42    global baglanti
43    if event == _IRQ_CENTRAL_CONNECT:
44        baglanti = data[0]
45    elif event == _IRQ_CENTRAL_DISCONNECT:
46        baglanti = None
47        yayinla()                                 # yeniden görünür ol / advertise again
48    elif event == _IRQ_GATTS_WRITE and data[1] == rx_h:
49        komut = ble.gatts_read(rx_h).decode().strip()
50        if komut == "1":
51            led.on()
52        elif komut == "0":
53            led.off()
54
55
56ble.irq(olay)
57yayinla()
58
59while True:
60    if baglanti is not None:
61        volt = sensor.read_u16() * 3.3 / 65535
62        sicaklik = 27 - (volt - 0.706) / 0.001721   # veri sayfası formülü / datasheet formula
63        try:
64            ble.gatts_notify(baglanti, tx_h, "T:{:.1f}\n".format(sicaklik).encode())
65        except OSError:
66            pass                                    # bağlantı yeni koptu / just disconnected
67    time.sleep(2)

Модуль bluetooth в MicroPython низкоуровневый, поэтому рекламный пакет собираем сами. Каждое поле состоит из байта длины, байта типа и данных: 0x01 — флаги, 0x09 — полное имя устройства, 0x07 — 128-битный UUID сервиса. Рекламный пакет вмещает не более 31 байта, поэтому 18-байтовое поле UUID мы поместили в ответ на сканирование (resp_data). gatts_register_services() возвращает хэндлы (handle) характеристик; по этим хэндлам мы читаем значение, записанное телефоном, и отправляем уведомления. Если хочется кода покороче, можно установить через mip библиотеку aioble на основе asyncio. Чтобы запустить код на ESP32 под MicroPython, достаточно заменить строку светодиода на Pin(2, Pin.OUT) и читать, например, DHT11 вместо встроенного датчика температуры.

Wi-Fi и Bluetooth вместе: память, дальность и питание

На ESP32 Wi-Fi и Bluetooth делят один радиомодуль 2,4 GHz. Чип работает с ними по очереди, с разделением времени, поэтому использовать их можно одновременно, но каждый становится немного медленнее, а классический Bluetooth вместе с интенсивным трафиком Wi-Fi — самый тяжёлый сценарий. Однако настоящая проблема обычно в памяти: стек Bluetooth большой, и вместе с Wi-Fi программа может превысить стандартный раздел приложения размером 1,2 МБ (1 310 720 байт); даже один только BLE-пример из этой статьи занимает около 80% этого места. Если сборка заканчивается ошибкой «Sketch too big» (скетч слишком большой) или «text section exceeds available space in board» (код не помещается в память платы) и обновление прошивки по воздуху (OTA) не используется, выбери Huge APP (3MB No OTA/1MB SPIFFS) в «Инструменты › Partition Scheme» (Tools › Partition Scheme). Хорошая альтернатива — библиотека NimBLE-Arduino, которой нужно меньше флеш-памяти и RAM.

Дальность зависит от конструкции антенны платы и окружения. С ESP32 с печатной антенной (PCB) на плате рассчитывай на 10–20 м в помещении в зависимости от стен, на открытом пространстве — больше; модули с разъёмом для внешней антенны (например, ESP32-WROOM-32U) бьют ещё дальше. Если закрыть антенну металлическим корпусом, батареей или проводами, дальность резко падает. При включении радиомодуля ток ненадолго возрастает, поэтому из-за слабого USB-кабеля плата может перезагружаться с сообщением «Brownout detector was triggered»; способы исправления — в статье Brownout и Guru Meditation на ESP32.

Идеи проектов с Bluetooth на ESP32

  • Bluetooth-машинка: передавай команды F, B, L и R, пришедшие через BluetoothSerial, на драйвер двигателей L298N; механику смотри в статье Как сделать робот-машинку на Arduino.
  • Умная розетка: замени светодиод в примере NUS модулем реле и включай лампу с iPhone.
  • Беспроводной термометр: добавь к примеру с DHT22 OLED-дисплей, чтобы значение было видно и на экране, и на телефоне.
  • Цветная атмосферная подсветка: меняй цвет ленты WS2812B NeoPixel значениями R, G и B, отправленными с телефона.
  • Своё Android-приложение: за несколько часов собери в App Inventor приложение-пульт с кнопками и ползунками.
  • Связь между двумя ESP32: чтобы две платы общались без телефона, попробуй вместо Bluetooth более простой и быстрый ESP-NOW.

Частые проблемы с Bluetooth на ESP32 и их решения

Симптом / ошибкаПричинаРешение
«Serial Bluetooth not available or not enabled» (Bluetooth Serial недоступен)Выбрана плата ESP32-S3, C3, C6 или H2, в которой нет классического BluetoothИспользуй классическую ESP32 или перейди на пример BLE (NUS)
Ошибка преобразования типа «conversion from 'String' to non-scalar type 'std::string' requested»Код для ядра 2.x (std::string) компилируется на ядре 3.xПрисвой getValue().c_str() переменной типа String
«Sketch too big» (скетч слишком большой)Bluetooth вместе с Wi-Fi не помещается в раздел приложенияPartition Scheme: выбери Huge APP или используй NimBLE-Arduino
iPhone не находит ESP32Классический Bluetooth (SPP) не поддерживается в iOSИспользуй BLE и подключайся внутри приложения
Устройство пропадает после первого подключенияПосле разрыва соединения реклама не перезапущенаВызывай startAdvertising() после разрыва соединения
Уведомления не приходятУведомления не включены на телефоне или нет BLE2902Нажми тройную стрелку в nRF Connect; добавь BLE2902 к характеристике
«read failed, socket might closed or timeout» (ошибка чтения, сокет закрыт или таймаут)Устаревшие данные сопряжения или уже подключён другой телефонУдали устройство на телефоне и выполни сопряжение заново; SPP принимает только одно подключение за раз
Конец длинного сообщения обрезаетсяMTU по умолчанию — 23 байта, то есть 20 байт данных на уведомлениеРазбей сообщение на части по 20 байт или разреши MTU побольше через BLEDevice::setMTU(185)
Изменения сервисов не видны на телефонеТелефон хранит старую таблицу GATT в кэшеВыключи и включи Bluetooth на телефоне; на Android удали устройство из сопряжённых
При поиске причин проблем с подключением выбери Debug или Verbose в «Инструменты › Core Debug Level» (Tools › Core Debug Level). Тогда монитор порта покажет журналы подключения, отключения и ошибок стека Bluetooth, по которым быстро видно, где проблема — в телефоне или в ESP32. А повтор того же теста на телефоне через nRF Connect подскажет, не в твоём ли приложении причина.

С Bluetooth на ESP32 превратить телефон в пульт, а ESP32 — в беспроводной датчик или умное устройство можно буквально несколькими строками кода. Для быстрого старта с Android подходит BluetoothSerial, а для поддержки iPhone и низкого энергопотребления — BLE. Если хочется продолжить с беспроводными проектами на ESP32, в приложении BlueGrays есть проекты со светодиодами и RGB-светодиодами, измерение температуры с DHT22 и «Машинка на L298N с управлением по Bluetooth» — у каждого пошаговая схема подключения и готовый код.

Частые вопросы

Есть ли у ESP32 Bluetooth?

Да. Оригинальный чип ESP32 поддерживает и классический Bluetooth (BR/EDR), и BLE по стандарту Bluetooth 4.2 и не требует дополнительного модуля. ESP32-S3, C3, C6 и H2 поддерживают только BLE, а у ESP32-S2 Bluetooth нет вовсе.

Какая дальность у Bluetooth на ESP32?

С ESP32 с печатной антенной на плате в помещении обычно получается 10–20 м; на это расстояние влияют стены, металлические предметы и ориентация антенны. На открытом пространстве дальность растёт, а модули с внешней антенной (например, WROOM-32U) бьют ещё дальше.

Можно ли подключить ESP32 к iPhone по Bluetooth?

Да, по BLE, но не по классическому Bluetooth (BluetoothSerial): iOS открывает профиль последовательного порта (SPP) только для аксессуаров с сертификацией MFi. На iPhone к BLE-сервису ESP32 можно подключиться через приложение вроде nRF Connect, LightBlue или Bluefruit Connect.

Работает ли BluetoothSerial на ESP32-S3 и ESP32-C3?

Нет. В этих чипах нет оборудования классического Bluetooth, поэтому BluetoothSerial не скомпилируется. Чтобы получить ту же логику последовательного порта поверх BLE, используй Nordic UART Service (NUS); его понимают приложения вроде Serial Bluetooth Terminal.

Может ли ESP32 использовать Wi-Fi и Bluetooth одновременно?

Да, они работают вместе, по очереди деля один радиомодуль 2,4 GHz. Правда, скорость немного падает, а программа становится больше; при ошибке «Sketch too big» (скетч слишком большой) выбери Huge APP в Partition Scheme или используй более лёгкую библиотеку NimBLE-Arduino.

Нужен ли HC-05 для ESP32?

Обычно нет. Библиотека BluetoothSerial на классической ESP32 выполняет работу HC-05 внутри чипа и работает с теми же приложениями-терминалами. HC-05 нужен только на платах без Bluetooth (Arduino Uno, ESP8266, стандартная Pico, ESP32-S2) или если проекту нужен классический Bluetooth, а в наличии ESP32 только с BLE.

Все проекты
Мигание встроенным светодиодом ESP32
#106Новичок

Мигание встроенным светодиодом ESP32

Мигание встроенным синим светодиодом на ESP32 DevKit V1. Внешние компоненты не нужны! Первый проект, который стоит попробовать после покупки ESP32. Встроенный светодиод подключён к GPIO 2.

4.8(29)
53
Мигающий светодиод на ESP32
#103Новичок

Мигающий светодиод на ESP32

Учит базовой работе с выходами GPIO на ESP32. Эффект мигания получается включением и выключением светодиода с цифрового пина. Это ESP32-версия классического проекта мигания на Arduino UNO; базовые команды мигания в среде Arduino те же, но расположение пинов ESP32 и уровни GPIO 3,3 V отличаются от Arduino UNO.

4.7(37)
99
Управление светодиодом кнопкой на ESP32 (переключение)
#105Новичок

Управление светодиодом кнопкой на ESP32 (переключение)

При каждом нажатии кнопки состояние светодиода меняется: если горел — гаснет, если не горел — загорается. Классический шаблон переключения. Антидребезг убирает механический шум кнопки, чтобы одно нажатие давало одно переключение.

4.6(39)
57
Управление светодиодом с телефона по Bluetooth
#34Новичок
Premium

Управление светодиодом с телефона по Bluetooth

Небольшая репетиция того, как включать и выключать свет в комнате с телефона. Bluetooth-модуль передаёт в Arduino буквы, отправленные с телефона; одна буква включает красный светодиод, другая — зелёны

4.8(68)
89
Машинка на L298N с управлением по Bluetooth
#80Продвинутый
Premium

Машинка на L298N с управлением по Bluetooth

Преврати телефон в пульт и управляй своей машинкой-роботом. Arduino получает команды из приложения-пульта на телефоне через Bluetooth-модуль и вращает колёса через драйвер моторов; машинка едет вперёд

5.0(38)
65
Управление RGB-светодиодом на ESP32
#111Новичок

Управление RGB-светодиодом на ESP32

Бесконечное число цветов одним RGB-светодиодом вместо 3 отдельных. Как в Arduino, радужный переход получается заданием значений R/G/B через analogWrite(). Теория цвета (например, красный + зелёный = жёлтый) видна на практике.

4.6(29)
51
Температура и влажность с DHT22 на ESP32 (с LCD-дисплеем)
#117Средний

Температура и влажность с DHT22 на ESP32 (с LCD-дисплеем)

Этот проект измеряет температуру и влажность воздуха датчиком DHT22 и в реальном времени показывает данные на I2C LCD 16x2. Данные читаются с датчика по однопроводному протоколу через библиотеку Adafruit DHT. Проект служит основой для IoT-метеостанций, умного дома или систем мониторинга среды в теплицах. Связь с DHT22 идёт через пин GPIO 4 у ESP32.

4.8(38)
56
Управление лентой WS2812B NeoPixel на ESP32
#126Средний
Premium

Управление лентой WS2812B NeoPixel на ESP32

Адресуемые светодиоды WS2812B (NeoPixel) — умные RGB-светодиоды, которыми управляют цепочкой через один пин данных. В этом проекте лента WS2812B из 8 светодиодов управляется библиотекой Adafruit_NeoPixel и показывает плавную радужную анимацию. Логика 3,3V у ESP32 работает с большинством WS2812B, но на случай нестабильности показано решение с последовательным резистором 470Ω и конденсатором 1000µF. Анимация получается сдвигом значения оттенка.

4.9(19)
30