ESP32: передача данных по Bluetooh Low Energy (BLE)

           На практике часто возникают задачи, в рамках которых требуется организовать сбор данных или управление по беспроводному каналу связи. Например, мониторинг температуры и влажности. Для выполнения этой задачи отлично подойдёт 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

           Далее подробно рассмотрим каждый фрагмент кода, а потом будет представлен полный листинг программы.

Читайте также:  VL53L0X и ESP32: собираем лазерный дальномер

           Сперва подключаем необходимые библиотеки для работы с фреймворком 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-устройств.

Читайте также:  Магнитометр HMC5883L (QMC5883L): компас на ESP32

           Кроме этого, осуществляется создание сервера и объявление характеристик для передачи показаний с датчика. При объявлении характеристик задействуется дескриптор 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, можно воспользоваться специальными приложениями:

Читайте также:  Подключение TFT-дисплея ST7735 к ESP32: вывод текста, графики и изображений

           — 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» соответствуют искомым показаниям температуры и влажности от сенсора.