На практике часто возникают задачи, в рамках которых требуется организовать сбор данных или управление по беспроводному каналу связи. Например, мониторинг температуры и влажности. Для выполнения этой задачи отлично подойдёт Bluetooth. В рамках этой статьи рассмотрим организацию передачи данных между модулем ESP32 и смартфоном при помощи технологии Bluetooth Low Energy (BLE).

В течение последних 15 лет индустрия Интернет-вещей (IoT — Internet of Things) значительно развивалась благодаря распространению и удешевлению мобильного Интернета. Но мощнейшим импульсом для развития стало появление удобных и доступных аппаратных и программных средств, таких как микроконтроллеры из семейства ESP8266 и ESP32 от компании Espressif Systems.
На сегодняшний день контроллер ESP32 за счёт невысокой стоимости и простоты программирования стал настоящим хитом не только среди радиолюбителей, но и профессионалов.
В рамках этой статьи мы подробно рассмотрим процесс организации передачи полезной информации (значения температуры и влажности с цифрового датчика SHT30, который является упрощённой версией сенсора SHT31) между отладочной платой NodeMCU-32S (38 pin) на базе модуля ESP-WROOM-32 и смартфоном, используя технологию BLE.
Bluetooth Low Energy является частью стандартов Bluetooth 4.0+ (начиная с 2010 года). BLE отличается от классического варианта Basic Rate / Enhanced Data Rate (BR/EDR — Classic Bluetooth) уменьшенным энергопотреблением за счёт коротких сеансов связи (устройство “просыпается”, передает информацию и “засыпает”), малых объёмов данных и малой скорости. Это идеально для построения IoT-устройств, таких как медицинские датчики, фитнесс-трекеры, компоненты «умного» дома, работающие от батарейного питания.
Схема включения
В качестве измерительного канала воспользуемся цифровым модулем датчика температуры и влажности SHT30, у которого заявленная производителем погрешность измерений температуры и влажности составляет ±0,3 ℃ и ±2 % соответственно. Ниже представлены схема подключения и фотография собранного макета:

Программный код (скетч)
В качестве среды разработки воспользуемся расширением PlatformIO для Visual Studio Code. Предлагаемый программный код будет одинаково работать и в Arduino IDE, необходимо только правильно выбрать отладочную плату, а также установить в менеджере библиотек необходимые пакеты для работы с BLE и сенсором Adafruit_SHT31.
Процесс установки и настройки PlatformIO достаточно трудоёмкий, но в Интернете можно найти довольно много качественных и подробных инструкций. Важно уделить внимание установке Python и указанию PATH. Также отмечу, что работать с PlatformIO нужно без подключения к WSL (Windows Subsystem for Linux).
Создаём новый проект. При выборе отладочной платы необходимо указать DOIT ESP32 DEVKIT V1, которая соответствует NodeMCU-32S (38 pin) на базе модуля ESP-WROOM-32:

Применяемый фреймворк — Arduino, чтобы обеспечить совместимость соответствующей IDE. В коде будут явные вставки на C++, поскольку Arduino C — это сочетание C++ и фреймворка Wiring.
После создания проекта нужно подключить к проекту библиотеку для работы с нашим датчиком температуры и влажности (Adafruit SHT31 Library):

В файле platformio.ini необходимо прописать базовые моменты проекта:
[env:esp32doit-devkit-v1]
platform = espressif32
monitor_speed = 9600
board = esp32doit-devkit-v1
framework = arduino
lib_deps = adafruit/Adafruit SHT31 Library@^2.2.2

Далее подробно рассмотрим каждый фрагмент кода, а потом будет представлен полный листинг программы.
Сперва подключаем необходимые библиотеки для работы с фреймворком Arduino, инструментами Bluetooth Low Energy и датчиком температуры/влажности:
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include "Adafruit_SHT31.h"
Далее создаём объект для работы с климатическим сенсором (в эксперименте используем SHT30, но библиотека называется SHT31, хотя оба сенсора отличаются только точностью):
Adafruit_SHT31 sht31 = Adafruit_SHT31();
Объявим переменные для работы с Bluetooth Low Energy:
BLEServer* pServer = nullptr; // указатель на BLE-сервер
// указатель на характеристику температуры
BLECharacteristic* pTemperatureCharacteristic = nullptr;
// указатель на характеристику влажности
BLECharacteristic* pHumidityCharacteristic = nullptr;
// Переменные, характеризующие состояния подключения
bool deviceConnected = false;
bool oldDeviceConnected = false;
Настроим пин для встроенного на плату светодиода, который будет выполнять роль индикатора активации беспроводного подключения платы к смартфону, а также объявим переменные для временного таймера (раз в 1 секунду осуществляется передача информации на смартфон):
const int LEDq = 2; // Пин встроенного светодиода
unsigned long previousMillis = 0;
const long interval = 1000; // Интервал обновления (1 секунда)
Для организации передачи необходимо использовать UUID (Universally Unique Identifier) — стандарт идентификации, используемый для создания уникальных идентификаторов в компьютерных системах.
UUID — это 128-битное (записанное в виде строки из 32 шестнадцатеричных символов) число, используемое для уникальной идентификации информации. В нашем проекте оно используется для идентификации сервисов (у нас это BLE-сервер), а также идентификации характеристик — конкретных значений (температура и влажность). Есть специальный онлайн-сервис, с помощью которого можно сгенерировать уникальные номера.
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define TEMPERATURE_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define HUMIDITY_UUID "cba1d466-344c-4be3-ab3f-189f80dd7518"
Когда мы будем устанавливать соединение между смартфоном и отладочной платой, нам нужно чётко выделить конкретно интересующий нас девайс, который ни с чем не спутаешь именно за счёт уникального номера (SERVICE_UUID). А внутри этого сервиса будут передаваться части информации с уникальными идентификаторами (TEMPERATURE_UUID и HUMIDITY_UUID).
Далее объявим класс, включающий в себя два метода, которые обрабатывают состояния подключения. В зависимости от них будет работать светодиодная индикация:
— при подключении девайса: устанавливается флаг deviceConnected, включается светодиод.
— при отключении: сбрасывается флаг, выключается светодиод, сервер начинает рекламировать себя снова (быть видимым в списке доступных ближайших устройств при сканировании смартфоном).
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("Устройство ВКЛючено");
digitalWrite(LEDq, HIGH);
}
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("Устройство ОТКЛючено");
digitalWrite(LEDq, LOW);
pServer->startAdvertising(); // Перезапуск рекламы
}
};
В функции setup() производится инициализация последовательного порта, встроенного светодиода, датчика.
Также инициализируется сервер с именем «ESP32-SHT30-BLE», который мы должны будем увидеть на смартфоне при сканировании Bluetooth-устройств.
Кроме этого, осуществляется создание сервера и объявление характеристик для передачи показаний с датчика. При объявлении характеристик задействуется дескриптор BLE2902, который используется для управления уведомлениями (notifications) от сервера к клиенту (смартфону), что позволяет клиенту подписаться на получение уведомлений при изменении значения характеристики (показания от сенсора).
В итоге, сервис стартует, делая себя видимым для сканеров (в нашем случае — смартфон).
void setup() {
Serial.begin(115200);
pinMode(LEDq, OUTPUT);
// Инициализация датчика SHT31
if (!sht31.begin(0x44)) {
Serial.println("Ошибка подключения к датчику");
while (1) delay(1);
}
// Настройка BLE
BLEDevice::init("ESP32-SHT30-BLE");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Создание BLE-сервиса
BLEService *pService = pServer->createService(SERVICE_UUID);
// Создание характеристик для температуры и влажности
pTemperatureCharacteristic = pService->createCharacteristic(
TEMPERATURE_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
pTemperatureCharacteristic->addDescriptor(new BLE2902());
pHumidityCharacteristic = pService->createCharacteristic(
HUMIDITY_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
pHumidityCharacteristic->addDescriptor(new BLE2902());
// Запуск сервиса
pService->start();
// Начало рекламы устройства
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // Параметры для улучшения совместимости
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("BLE-сервер запущен. Ожидание подключений...");
}
В основном бесконечном цикле программы loop() выполняются следующие операции:
— проверяется состояние подключения, после чего обновляются соответствующие флаги
if (!deviceConnected && oldDeviceConnected) {
delay(500);
...
}
— проверка, прошла ли 1 секунда с последнего обновления. То есть каждую 1 секунду считываются и отправляются показания сенсора
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
— чтение информации с датчика. Если они корректны (не NaN), то выводим их в монитор порта
float t = sht31.readTemperature();
float h = sht31.readHumidity();
if (!isnan(t) && !isnan(h)) {
Serial.printf("Temp: %.2f°C, Hum: %.2f%%\n", t, h);
— если подключение к смартфону прошло успешно, то показания температуры и влажности преобразуются в строки с фиксированной точкой и устанавливаются как значения характеристик, после чего отправляются уведомления подключенному смартфону
// Отправка по BLE
if (deviceConnected)
{
char tempStr[8];
char humStr[8];
dtostrf(t, 5, 2, tempStr);
dtostrf(h, 5, 2, humStr);
pTemperatureCharacteristic->setValue(tempStr);
pTemperatureCharacteristic->notify();
pHumidityCharacteristic->setValue(humStr);
pHumidityCharacteristic->notify();
}
else
{
Serial.println("Нет подключения к устройству");
}
}
else
{
Serial.println("Ошибка чтения данных с датчика");
}
Полный листинг программы
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include "Adafruit_SHT31.h"
Adafruit_SHT31 sht31 = Adafruit_SHT31();
// Настройки BLE
BLEServer *pServer = nullptr;
BLECharacteristic *pTemperatureCharacteristic = nullptr;
BLECharacteristic *pHumidityCharacteristic = nullptr;
bool deviceConnected = false;
bool oldDeviceConnected = false;
const int LEDq = 2; // Пин предустановленного на плату светодиода
unsigned long previousMillis = 0;
const long interval = 1000; // Интервал обновления показаний (мс)
// UUID для сервиса и характеристик
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define TEMPERATURE_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define HUMIDITY_UUID "cba1d466-344c-4be3-ab3f-189f80dd7518"
// Класс для обработки событий BLE
class MyServerCallbacks : public BLEServerCallbacks
{
void onConnect(BLEServer *pServer)
{
deviceConnected = true;
Serial.println("Устройство ВКЛючено");
digitalWrite(LEDq, HIGH);
}
void onDisconnect(BLEServer *pServer)
{
deviceConnected = false;
Serial.println("Устройство ОТКЛючено");
digitalWrite(LEDq, LOW);
pServer->startAdvertising(); // Перезапуск рекламы для повторного подключения
}
};
void setup()
{
Serial.begin(115200);
pinMode(LEDq, OUTPUT);
// Инициализация датчика SHT31
if (!sht31.begin(0x44))
{
Serial.println("Ошибка подключения к датчику");
while (1)
delay(1);
}
// Настройка BLE
BLEDevice::init("ESP32-SHT30-BLE");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Создание BLE-сервиса
BLEService *pService = pServer->createService(SERVICE_UUID);
// Создание характеристик для температуры и влажности
pTemperatureCharacteristic = pService->createCharacteristic(
TEMPERATURE_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
pTemperatureCharacteristic->addDescriptor(new BLE2902());
pHumidityCharacteristic = pService->createCharacteristic(
HUMIDITY_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
pHumidityCharacteristic->addDescriptor(new BLE2902());
// Запуск сервиса
pService->start();
// Начало рекламы устройства
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // Параметры для улучшения совместимости
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("BLE-сервер запущен. Ожидание подключений...");
}
void loop()
{
// Обработка подключения/отключения
if (!deviceConnected && oldDeviceConnected)
{
delay(500); // Даем время для стабилизации соединения
oldDeviceConnected = deviceConnected;
}
if (deviceConnected && !oldDeviceConnected)
{
oldDeviceConnected = deviceConnected;
}
// Обновление по таймеру
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval)
{
previousMillis = currentMillis;
float t = sht31.readTemperature();
float h = sht31.readHumidity();
if (!isnan(t) && !isnan(h))
{
Serial.printf("Temp: %.2f°C, Hum: %.2f%%\n", t, h);
// Отправка по BLE
if (deviceConnected)
{
char tempStr[8];
char humStr[8];
dtostrf(t, 5, 2, tempStr);
dtostrf(h, 5, 2, humStr);
pTemperatureCharacteristic->setValue(tempStr);
pTemperatureCharacteristic->notify();
pHumidityCharacteristic->setValue(humStr);
pHumidityCharacteristic->notify();
}
else
{
Serial.println("Нет подключения к устройству");
}
}
else
{
Serial.println("Ошибка чтения данных с датчика");
}
}
}
Для того, чтобы скомпилировать код, необходимо нажать на пиктограмму в виде галочки в нижнем левом углу окна:

При отсутствии ошибок компиляция должна пройти успешно:

Далее прошиваем контроллер, нажав пиктограмму в виде правой стрелки:

Процесс прошивки достаточно долгий. Когда в терминале появится надпись «Connecting…», начнут появляться точки. В этот момент нужно успеть нажать на отладочной плате кнопку «BOOT», которая справа от microUSB-разъёма.


Иногда процесс прошивки сбивается. Чаще всего это происходит из-за того, что слишком длинный USB-кабель между отладочной платой и материнской платой компьютера (ноутбука). Рекомендуется кабель длиной не более 1 метра.

Приём данных на смартфон
Для того, чтобы получать показания сенсора на смартфон через BLE, можно воспользоваться специальными приложениями:
— Android: nRF Connect, BLE Scanner;
— iOS: LightBlue, BLE Tool.
Для эксперимента воспользуемся приложением BLE Scanner, который без проблем бесплатно скачивается и устанавливается через магазины приложений.
Убедившись, что прошивка успешно загрузилась в контроллер и программа начала работу, берём смартфон с заранее установленным приложением и открываем меню Bluetooth-подключений. Активируем беспроводной модуль, после чего производим сопряжение с нашей платой «ESP32-SHT30-BLE»:

Далее открываем приложение, соглашаемся с предложениями о доступе к геолокации, а потом производим сканирование округи. Видим в списке нашу отладочную плату:

Нажимаем на зелёную кнопку «CONNECT» и видим в выпадающем списке «CUSTOM SERVICE» назначенный нами в коде SERVICE_UUID = 4fafc201-1fb5-459e-8fcc-c5c9c331914b:

Раскрываем этот список, и перед нами предстают характеристики с данными. Прожимая кнопочки «N» на каждой характеристике, мы подписываемся на уведомления, которые каждую секунду будут обновляться (строка «Value»):

Обновляющиеся значения «Value» соответствуют искомым показаниям температуры и влажности от сенсора.
