ESP32 BLE Сервер и Клиент (Bluetooth Low Energy)

Узнайте, как создать BLE (Bluetooth Low Energy) соединение между двумя платами ESP32. Одна ESP32 будет сервером, а другая ESP32 будет клиентом. BLE-сервер объявляет характеристики, содержащие показания датчиков, которые клиент может считывать. BLE-клиент ESP32 считывает значения этих характеристик (температуру и влажность) и отображает их на OLED-дисплее.

ESP32 BLE Сервер и Клиент Bluetooth Low Energy Arduino IDE

Рекомендуемое чтение: Начало работы с ESP32 Bluetooth Low Energy (BLE)

Что такое Bluetooth Low Energy?

Прежде чем переходить непосредственно к проекту, важно кратко рассмотреть некоторые основные концепции BLE, чтобы вы могли лучше понять проект в дальнейшем. Если вы уже знакомы с BLE, можете перейти к разделу Обзор проекта.

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

Это позволяет потреблять очень мало энергии. BLE потребляет примерно в 100 раз меньше энергии, чем Bluetooth (в зависимости от варианта использования). Вы можете ознакомиться с основными различиями между Bluetooth и Bluetooth Low Energy здесь.

BLE Сервер и Клиент

В Bluetooth Low Energy существует два типа устройств: сервер и клиент. ESP32 может выступать как в роли клиента, так и в роли сервера.

Сервер объявляет о своём существовании, чтобы другие устройства могли его найти, и содержит данные, которые клиент может считывать. Клиент сканирует ближайшие устройства и, когда находит нужный сервер, устанавливает соединение и слушает входящие данные. Это называется коммуникацией «точка-точка».

BLE Клиент Сервер - сервер объявляет о себе

Существуют и другие возможные режимы связи, такие как режим широковещания и ячеистая сеть (mesh network), которые не рассматриваются в этом руководстве.

GATT

GATT расшифровывается как Generic Attributes (общие атрибуты) и определяет иерархическую структуру данных, которая предоставляется подключённым BLE-устройствам. Это означает, что GATT определяет способ, которым два BLE-устройства отправляют и получают стандартные сообщения. Понимание этой иерархии важно, поскольку оно облегчит использование BLE с ESP32.

Иерархия GATT - пример ESP32 BLE Сервер Клиент
  • Профиль (Profile): стандартная коллекция сервисов для конкретного варианта использования;

  • Сервис (Service): коллекция связанной информации, например, показания датчиков, уровень заряда батареи, частота сердечных сокращений и т.д.;

  • Характеристика (Characteristic): именно здесь в иерархии хранятся фактические данные (значение);

  • Дескриптор (Descriptor): метаданные о данных;

  • Свойства (Properties): описывают, как можно взаимодействовать со значением характеристики. Например: чтение (read), запись (write), уведомление (notify), широковещание (broadcast), индикация (indicate) и т.д.

В нашем примере мы создадим сервис с двумя характеристиками. Одна для температуры, а другая для влажности. Фактические показания температуры и влажности сохраняются в значении соответствующих характеристик. Каждая характеристика имеет свойство notify (уведомление), так что она уведомляет клиента всякий раз, когда значения изменяются.

UUID

Каждый сервис, характеристика и дескриптор имеют UUID (Universally Unique Identifier — универсальный уникальный идентификатор). UUID — это уникальное 128-битное (16 байт) число. Например:

55072829-bc9e-4c53-938a-74a6d4c78776

Существуют сокращённые UUID для всех типов, сервисов и профилей, указанных в SIG (Bluetooth Special Interest Group).

Но если вашему приложению нужен собственный UUID, вы можете сгенерировать его с помощью этого сайта-генератора UUID.

Таким образом, UUID используется для уникальной идентификации информации. Например, он может идентифицировать конкретный сервис, предоставляемый Bluetooth-устройством.

Для более подробного введения в BLE прочитайте наше руководство по началу работы:

Обзор проекта

В этом руководстве вы узнаете, как создать BLE-соединение между двумя платами ESP32. Одна ESP32 будет BLE-сервером, а другая ESP32 — BLE-клиентом.

Демонстрация ESP32 BLE Клиент Сервер с OLED-дисплеем

BLE-сервер ESP32 подключён к датчику BME280 и обновляет значения характеристик температуры и влажности каждые 30 секунд.

ESP32-клиент подключается к BLE-серверу и получает уведомления о значениях характеристик температуры и влажности. Этот ESP32 подключён к OLED-дисплею и выводит последние показания.

Этот проект разделён на две части:

  • Часть 1 — ESP32 BLE-сервер

  • Часть 2 — ESP32 BLE-клиент

Необходимые компоненты

Вот список компонентов, необходимых для выполнения этого проекта:

ESP32 BLE-сервер:

ESP32 BLE-клиент:

Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!

1) ESP32 BLE-сервер

В этой части мы настроим BLE-сервер, который объявляет сервис, содержащий две характеристики: одну для температуры и другую для влажности. Эти характеристики имеют свойство Notify для уведомления клиента о новых значениях.

ESP32 BLE Сервер подключён к приложению nRF Connect

Схема подключения

BLE-сервер ESP32 будет объявлять характеристики с температурой и влажностью от датчика BME280. Вы можете использовать любой другой датчик, если добавите необходимые строки в код.

Мы будем использовать I2C-связь с модулем датчика BME280. Для этого подключите датчик к стандартным выводам ESP32 SCL (GPIO 22) и SDA (GPIO 21), как показано на следующей схеме подключения.

Схема подключения ESP32 к BME280

Рекомендуемое чтение: Справочник по распиновке ESP32: Какие GPIO-выводы следует использовать?

Установка библиотек BME280

Как упоминалось ранее, мы будем передавать показания датчика BME280. Поэтому вам необходимо установить библиотеки для взаимодействия с датчиком BME280.

Вы можете установить библиотеки с помощью менеджера библиотек Arduino. Перейдите в Sketch > Include Library > Manage Libraries и найдите нужную библиотеку по имени.

Установка библиотек (VS Code + PlatformIO)

Если вы используете VS Code с расширением PlatformIO, скопируйте следующее в файл platformio.ini для подключения библиотек.

lib_deps = adafruit/Adafruit Unified Sensor @ ^1.1.4
            adafruit/Adafruit BME280 Library @ ^2.1.2

Код ESP32 BLE-сервера

Подготовив схему и установив необходимые библиотеки, скопируйте следующий код в Arduino IDE или в файл main.cpp, если вы используете VS Code.

/*********
  Rui Santos
  Complete instructions at https://RandomNerdTutorials.com/esp32-ble-server-client/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

//BLE server name
#define bleServerName "BME280_ESP32"

Adafruit_BME280 bme; // I2C

float temp;
float tempF;
float hum;

// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

bool deviceConnected = false;

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"

// Temperature Characteristic and Descriptor
#ifdef temperatureCelsius
  BLECharacteristic bmeTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
  BLECharacteristic bmeTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2902));
#endif

// Humidity Characteristic and Descriptor
BLECharacteristic bmeHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeHumidityDescriptor(BLEUUID((uint16_t)0x2903));

//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
  }
};

void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

void setup() {
  // Start serial communication
  Serial.begin(115200);

  // Init BME Sensor
  initBME();

  // Create the BLE Device
  BLEDevice::init(bleServerName);

  // Create the BLE Server
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Create the BLE Service
  BLEService *bmeService = pServer->createService(SERVICE_UUID);

  // Create BLE Characteristics and Create a BLE Descriptor
  // Temperature
  #ifdef temperatureCelsius
    bmeService->addCharacteristic(&bmeTemperatureCelsiusCharacteristics);
    bmeTemperatureCelsiusDescriptor.setValue("BME temperature Celsius");
    bmeTemperatureCelsiusCharacteristics.addDescriptor(&bmeTemperatureCelsiusDescriptor);
  #else
    bmeService->addCharacteristic(&bmeTemperatureFahrenheitCharacteristics);
    bmeTemperatureFahrenheitDescriptor.setValue("BME temperature Fahrenheit");
    bmeTemperatureFahrenheitCharacteristics.addDescriptor(&bmeTemperatureFahrenheitDescriptor);
  #endif

  // Humidity
  bmeService->addCharacteristic(&bmeHumidityCharacteristics);
  bmeHumidityDescriptor.setValue("BME humidity");
  bmeHumidityCharacteristics.addDescriptor(new BLE2902());

  // Start the service
  bmeService->start();

  // Start advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pServer->getAdvertising()->start();
  Serial.println("Waiting a client connection to notify...");
}

void loop() {
  if (deviceConnected) {
    if ((millis() - lastTime) > timerDelay) {
      // Read temperature as Celsius (the default)
      temp = bme.readTemperature();
      // Fahrenheit
      tempF = 1.8*temp +32;
      // Read humidity
      hum = bme.readHumidity();

      //Notify temperature reading from BME sensor
      #ifdef temperatureCelsius
        static char temperatureCTemp[6];
        dtostrf(temp, 6, 2, temperatureCTemp);
        //Set temperature Characteristic value and notify connected client
        bmeTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
        bmeTemperatureCelsiusCharacteristics.notify();
        Serial.print("Temperature Celsius: ");
        Serial.print(temp);
        Serial.print(" ºC");
      #else
        static char temperatureFTemp[6];
        dtostrf(tempF, 6, 2, temperatureFTemp);
        //Set temperature Characteristic value and notify connected client
        bmeTemperatureFahrenheitCharacteristics.setValue(temperatureFTemp);
        bmeTemperatureFahrenheitCharacteristics.notify();
        Serial.print("Temperature Fahrenheit: ");
        Serial.print(tempF);
        Serial.print(" ºF");
      #endif

      //Notify humidity reading from BME
      static char humidityTemp[6];
      dtostrf(hum, 6, 2, humidityTemp);
      //Set humidity Characteristic value and notify connected client
      bmeHumidityCharacteristics.setValue(humidityTemp);
      bmeHumidityCharacteristics.notify();
      Serial.print(" - Humidity: ");
      Serial.print(hum);
      Serial.println(" %");

      lastTime = millis();
    }
  }
}

Посмотреть исходный код

Вы можете загрузить код, и он сразу начнёт работать, объявляя свой сервис с характеристиками температуры и влажности. Продолжайте чтение, чтобы узнать, как работает код, или перейдите к разделу 2) ESP32 BLE-клиент.

В разделе Examples (Примеры) есть несколько примеров использования BLE с ESP32. В Arduino IDE перейдите в File > Examples > ESP32 BLE Arduino. Этот скетч сервера основан на примере Notify.

Импорт библиотек

Код начинается с импорта необходимых библиотек.

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Выбор единицы измерения температуры

По умолчанию ESP отправляет температуру в градусах Цельсия. Вы можете закомментировать или удалить следующую строку, чтобы отправлять температуру в градусах Фаренгейта.

//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

Имя BLE-сервера

Следующая строка задаёт имя для нашего BLE-сервера. Оставьте имя BLE-сервера по умолчанию. В противном случае имя сервера в коде клиента также нужно будет изменить (потому что они должны совпадать).

//BLE server name
#define bleServerName "BME280_ESP32"

Датчик BME280

Создайте объект Adafruit_BME280 с именем bme на стандартных выводах I2C ESP32.

Adafruit_BME280 bme; // I2C

Переменные temp, tempF и hum будут хранить температуру в градусах Цельсия, температуру в градусах Фаренгейта и влажность, считанные с датчика BME280.

float temp;
float tempF;
float hum;

Другие переменные

Следующие переменные таймера определяют, как часто мы хотим записывать в характеристики температуры и влажности. Мы установили переменную timerDelay на 30000 миллисекунд (30 секунд), но вы можете изменить это значение.

// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

Булева переменная deviceConnected позволяет нам отслеживать, подключён ли клиент к серверу.

bool deviceConnected = false;

UUID для BLE

В следующих строках мы определяем UUID для сервиса, для характеристики температуры в градусах Цельсия, для характеристики температуры в градусах Фаренгейта и для влажности.

// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"

// Temperature Characteristic and Descriptor
#ifdef temperatureCelsius
  BLECharacteristic bmeTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
  BLECharacteristic bmeTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2901));
#endif

// Humidity Characteristic and Descriptor
BLECharacteristic bmeHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeHumidityDescriptor(BLEUUID((uint16_t)0x2903));

Рекомендуется оставить все UUID по умолчанию. В противном случае вам также нужно будет изменить код на стороне клиента, чтобы клиент мог найти сервис и получить значения характеристик.

setup()

В функции setup() инициализируйте Serial Monitor и датчик BME280.

// Start serial communication
Serial.begin(115200);

// Init BME Sensor
initBME();

Создайте новое BLE-устройство с именем BLE-сервера, которое вы определили ранее:

// Create the BLE Device
BLEDevice::init(bleServerName);

Установите BLE-устройство в качестве сервера и назначьте функцию обратного вызова.

// Create the BLE Server
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

Функция обратного вызова MyServerCallbacks() изменяет булеву переменную deviceConnected на true или false в зависимости от текущего состояния BLE-устройства. Это означает, что если клиент подключён к серверу, состояние — true. Если клиент отключается, булева переменная меняется на false. Вот часть кода, определяющая функцию MyServerCallbacks().

//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
  }
};

Запустите BLE-сервис с UUID сервиса, определённым ранее.

BLEService *bmeService = pServer->createService(SERVICE_UUID);

Затем создайте BLE-характеристику температуры. Если вы используете градусы Цельсия, устанавливаются следующие характеристика и дескриптор:

#ifdef temperatureCelsius
  bmeService->addCharacteristic(&bmeTemperatureCelsiusCharacteristics);
  bmeTemperatureCelsiusDescriptor.setValue("BME temperature Celsius");
  bmeTemperatureCelsiusCharacteristics.addDescriptor(new BLE2902());

В противном случае устанавливается характеристика для Фаренгейта:

#else
  bmeService->addCharacteristic(&dhtTemperatureFahrenheitCharacteristics);
  bmeTemperatureFahrenheitDescriptor.setValue("BME temperature Fahrenheit");
  bmeTemperatureFahrenheitCharacteristics.addDescriptor(new BLE2902());
#endif

После этого устанавливается характеристика влажности:

// Humidity
bmeService->addCharacteristic(&bmeHumidityCharacteristics);
bmeHumidityDescriptor.setValue("BME humidity");
bmeHumidityCharacteristics.addDescriptor(new BLE2902());

Наконец, вы запускаете сервис, и сервер начинает объявление, чтобы другие устройства могли его найти.

// Start the service
bmeService->start();

// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pServer->getAdvertising()->start();
Serial.println("Waiting a client connection to notify...");

loop()

Функция loop() довольно проста. Вы постоянно проверяете, подключено ли устройство к клиенту или нет. Если оно подключено и прошло время timerDelay, считываются текущие значения температуры и влажности.

if (deviceConnected) {
  if ((millis() - lastTime) > timerDelay) {
    // Read temperature as Celsius (the default)
    temp = bme.readTemperature();
    // Fahrenheit
    tempF = temp*1.8 +32;
    // Read humidity
    hum = bme.readHumidity();

Если вы используете температуру в градусах Цельсия, выполняется следующий блок кода. Сначала температура преобразуется в переменную типа char (переменная temperatureCTemp). Мы должны преобразовать температуру в тип char, чтобы использовать её в функции setValue().

static char temperatureCTemp[6];
dtostrf(temp, 6, 2, temperatureCTemp);

Затем значение характеристики bmeTemperatureCelsiusCharacteristic устанавливается равным новому значению температуры (temperatureCTemp) с помощью функции setValue(). После установки нового значения мы можем уведомить подключённого клиента с помощью функции notify().

//Set temperature Characteristic value and notify connected client
bmeTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
bmeTemperatureCelsiusCharacteristics.notify();

Аналогичная процедура выполняется для температуры в градусах Фаренгейта.

#else
    static char temperatureFTemp[6];
    dtostrf(f, 6, 2, temperatureFTemp);
    //Set temperature Characteristic value and notify connected client
    bmeTemperatureFahrenheitCharacteristics.setValue(tempF);
    bmeTemperatureFahrenheitCharacteristics.notify();
    Serial.print("Temperature Fahrenheit: ");
    Serial.print(tempF);
    Serial.print(" *F");
#endif

Отправка влажности также использует тот же процесс.

//Notify humidity reading from DHT
static char humidityTemp[6];
dtostrf(hum, 6, 2, humidityTemp);
//Set humidity Characteristic value and notify connected client
bmeHumidityCharacteristics.setValue(humidityTemp);
bmeHumidityCharacteristics.notify();
Serial.print(" - Humidity: ");
Serial.print(hum);
Serial.println(" %");

Тестирование ESP32 BLE-сервера

Загрузите код на вашу плату, а затем откройте Serial Monitor. Он отобразит сообщение, как показано ниже.

Запуск ESP32 BLE-сервера - Serial Monitor

Затем вы можете проверить, правильно ли работает BLE-сервер, используя приложение для BLE-сканирования на вашем смартфоне, например nRF Connect. Это приложение доступно для Android и iOS.

После установки приложения включите Bluetooth на вашем смартфоне. Откройте приложение nRF Connect и нажмите кнопку Scan (Сканировать). Оно найдёт все Bluetooth-устройства поблизости, включая ваше устройство BME280_ESP32 (это имя BLE-сервера, которое вы определили в коде).

Сканирование BLE-устройств ESP32 в приложении nRF Connect

Подключитесь к устройству BME280_ESP32, а затем выберите вкладку клиента (интерфейс может немного отличаться). Вы можете увидеть, что оно объявляет сервис с UUID, который мы определили в коде, а также характеристики температуры и влажности. Обратите внимание, что эти характеристики имеют свойство Notify.

Характеристики ESP32 BLE-сервера в приложении nRF Connect

Ваш ESP32 BLE-сервер готов!

Перейдите к следующему разделу, чтобы создать ESP32-клиент, который подключается к серверу для получения доступа к характеристикам температуры и влажности и отображения показаний на OLED-дисплее.


2) ESP32 BLE-клиент

В этом разделе мы создадим ESP32 BLE-клиент, который установит соединение с ESP32 BLE-сервером и отобразит показания на OLED-дисплее.

Схема подключения

ESP32 BLE-клиент подключён к OLED-дисплею. Дисплей показывает показания, полученные по Bluetooth.

Подключите OLED-дисплей к ESP32, следуя приведённой ниже схеме. Вывод SCL подключается к GPIO 22, а вывод SDA — к GPIO 21.

Схема подключения ESP32 к OLED-дисплею SSD1306

Установка библиотек SSD1306, GFX и BusIO

Для взаимодействия с OLED-дисплеем необходимо установить следующие библиотеки:

Для установки библиотек перейдите в Sketch > Include Library > Manage Libraries и найдите нужные библиотеки по имени.

Установка библиотек (VS Code + PlatformIO)

Если вы используете VS Code с расширением PlatformIO, скопируйте следующее в файл platformio.ini для подключения библиотек.

lib_deps =
     adafruit/Adafruit GFX Library@^1.10.12
     adafruit/Adafruit SSD1306@^2.4.6

Код ESP32 BLE-клиента

Скопируйте скетч BLE-клиента в Arduino IDE или в файл main.cpp, если вы используете VS Code с PlatformIO.

/*********
  Rui Santos
  Complete instructions at https://RandomNerdTutorials.com/esp32-ble-server-client/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/

#include "BLEDevice.h"
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>

//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

//BLE Server name (the other ESP32 name running the server sketch)
#define bleServerName "BME280_ESP32"

/* UUID's of the service, characteristic that we want to read*/
// BLE Service
static BLEUUID bmeServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");

// BLE Characteristics
#ifdef temperatureCelsius
  //Temperature Celsius Characteristic
  static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
  //Temperature Fahrenheit Characteristic
  static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif

// Humidity Characteristic
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");

//Flags stating if should begin connecting and if the connection is up
static boolean doConnect = false;
static boolean connected = false;

//Address of the peripheral device. Address will be found during scanning...
static BLEAddress *pServerAddress;

//Characteristicd that we want to read
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;

//Activate notify
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

//Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

//Variables to store temperature and humidity
char* temperatureChar;
char* humidityChar;

//Flags to check whether new temperature and humidity readings are available
boolean newTemperature = false;
boolean newHumidity = false;

//Connect to the BLE Server that has the name, Service, and Characteristics
bool connectToServer(BLEAddress pAddress) {
   BLEClient* pClient = BLEDevice::createClient();

  // Connect to the remove BLE Server.
  pClient->connect(pAddress);
  Serial.println(" - Connected to server");

  // Obtain a reference to the service we are after in the remote BLE server.
  BLERemoteService* pRemoteService = pClient->getService(bmeServiceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
    Serial.println(bmeServiceUUID.toString().c_str());
    return (false);
  }

  // Obtain a reference to the characteristics in the service of the remote BLE server.
  temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
  humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);

  if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
    Serial.print("Failed to find our characteristic UUID");
    return false;
  }
  Serial.println(" - Found our characteristics");

  //Assign callback functions for the Characteristics
  temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
  humidityCharacteristic->registerForNotify(humidityNotifyCallback);
  return true;
}

//Callback function that gets called, when another device's advertisement has been received
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    if (advertisedDevice.getName() == bleServerName) { //Check if the name of the advertiser matches
      advertisedDevice.getScan()->stop(); //Scan can be stopped, we found what we are looking for
      pServerAddress = new BLEAddress(advertisedDevice.getAddress()); //Address of advertiser is the one we need
      doConnect = true; //Set indicator, stating that we are ready to connect
      Serial.println("Device found. Connecting!");
    }
  }
};

//When the BLE Server sends a new temperature reading with the notify property
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
                                        uint8_t* pData, size_t length, bool isNotify) {
  //store temperature value
  temperatureChar = (char*)pData;
  newTemperature = true;
}

//When the BLE Server sends a new humidity reading with the notify property
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
                                    uint8_t* pData, size_t length, bool isNotify) {
  //store humidity value
  humidityChar = (char*)pData;
  newHumidity = true;
  Serial.print(newHumidity);
}

//function that prints the latest sensor readings in the OLED display
void printReadings(){

  display.clearDisplay();
  // display temperature
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("Temperature: ");
  display.setTextSize(2);
  display.setCursor(0,10);
  display.print(temperatureChar);
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  Serial.print("Temperature:");
  Serial.print(temperatureChar);
  #ifdef temperatureCelsius
    //Temperature Celsius
    display.print("C");
    Serial.print("C");
  #else
    //Temperature Fahrenheit
    display.print("F");
    Serial.print("F");
  #endif

  //display humidity
  display.setTextSize(1);
  display.setCursor(0, 35);
  display.print("Humidity: ");
  display.setTextSize(2);
  display.setCursor(0, 45);
  display.print(humidityChar);
  display.print("%");
  display.display();
  Serial.print(" Humidity:");
  Serial.print(humidityChar);
  Serial.println("%");
}

void setup() {
  //OLED display setup
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE,0);
  display.setCursor(0,25);
  display.print("BLE Client");
  display.display();

  //Start serial communication
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");

  //Init BLE device
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 30 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(30);
}

void loop() {
  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer(*pServerAddress)) {
      Serial.println("We are now connected to the BLE Server.");
      //Activate the Notify property of each Characteristic
      temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
      humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
      connected = true;
    } else {
      Serial.println("We have failed to connect to the server; Restart your device to scan for nearby BLE server again.");
    }
    doConnect = false;
  }
  //if new temperature readings are available, print in the OLED
  if (newTemperature && newHumidity){
    newTemperature = false;
    newHumidity = false;
    printReadings();
  }
  delay(1000); // Delay a second between loops.
}

Посмотреть исходный код

Продолжайте чтение, чтобы узнать, как работает код, или перейдите к разделу Демонстрация.

Импорт библиотек

Начните с импорта необходимых библиотек:

#include "BLEDevice.h"
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>

Выбор единицы измерения температуры

По умолчанию клиент будет получать температуру в градусах Цельсия. Если вы закомментируете или удалите следующую строку, он начнёт получать температуру в градусах Фаренгейта.

//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

Имя BLE-сервера и UUID

Затем определите имя BLE-сервера, к которому мы хотим подключиться, а также UUID сервиса и характеристик, которые мы хотим считать. Оставьте имя BLE-сервера и UUID по умолчанию, чтобы они совпадали с определёнными в скетче сервера.

//BLE Server name (the other ESP32 name running the server sketch)
#define bleServerName "BME280_ESP32"

/* UUID's of the service, characteristic that we want to read*/
// BLE Service
static BLEUUID bmeServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");

// BLE Characteristics
#ifdef temperatureCelsius
  //Temperature Celsius Characteristic
  static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
  //Temperature Fahrenheit Characteristic
  static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif

// Humidity Characteristic
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");

Объявление переменных

Затем необходимо объявить несколько переменных, которые будут использоваться позже в Bluetooth для проверки подключения к серверу.

//Flags stating if should begin connecting and if the connection is up
static boolean doConnect = false;
static boolean connected = false;

Создайте переменную типа BLEAddress, которая ссылается на адрес сервера, к которому мы хотим подключиться. Этот адрес будет найден при сканировании.

//Address of the peripheral device. Address will be found during scanning...
static BLEAddress *pServerAddress;

Установите характеристики, которые мы хотим считывать (температуру и влажность).

//Characteristicd that we want to read
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;

OLED-дисплей

Также необходимо объявить несколько переменных для работы с OLED. Определите ширину и высоту OLED:

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

Создайте экземпляр OLED-дисплея с определённой ранее шириной и высотой.

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire);

Переменные температуры и влажности

Определите переменные типа char для хранения значений температуры и влажности, полученных от сервера.

//Variables to store temperature and humidity
char* temperatureChar;
char* humidityChar;

Следующие переменные используются для проверки наличия новых показаний температуры и влажности и необходимости обновления OLED-дисплея.

//Flags to check whether new temperature and humidity readings are available
boolean newTemperature = false;
boolean newHumidity = false;

printReadings()

Мы создали функцию printReadings(), которая отображает показания температуры и влажности на OLED-дисплее.

void printReadings(){

  display.clearDisplay();
  // display temperature
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("Temperature: ");
  display.setTextSize(2);
  display.setCursor(0,10);
  display.print(temperatureChar);
  display.print(" ");
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  Serial.print("Temperature:");
  Serial.print(temperatureChar);
  #ifdef temperatureCelsius
    //Temperature Celsius
    display.print("C");
    Serial.print("C");
  #else
    //Temperature Fahrenheit
    display.print("F");
    Serial.print("F");
  #endif

  //display humidity
  display.setTextSize(1);
  display.setCursor(0, 35);
  display.print("Humidity: ");
  display.setTextSize(2);
  display.setCursor(0, 45);
  display.print(humidityChar);
  display.print("%");
  display.display();
  Serial.print(" Humidity:");
  Serial.print(humidityChar);
  Serial.println("%");
}

Рекомендуемое чтение: ESP32 OLED-дисплей с Arduino IDE

setup()

В функции setup() запустите OLED-дисплей.

//OLED display setup
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
  Serial.println(F("SSD1306 allocation failed"));
  for(;;); // Don't proceed, loop forever
}

Затем выведите сообщение в первой строке «BME SENSOR».

display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE,0);
display.setCursor(0,25);
display.print("BLE Client");
display.display();

Запустите последовательную связь на скорости 115200 бод.

Serial.begin(115200);

И инициализируйте BLE-устройство.

//Init BLE device
BLEDevice::init("");

Сканирование ближайших устройств

Следующие методы сканируют ближайшие устройства.

// Retrieve a Scanner and set the callback we want to use to be informed when we
// have detected a new device.  Specify that we want active scanning and start the
// scan to run for 30 seconds.
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(30);

Функция MyAdvertisedDeviceCallbacks()

Обратите внимание, что функция MyAdvertisedDeviceCallbacks() при обнаружении BLE-устройства проверяет, совпадает ли имя найденного устройства с именем BLE-сервера. Если совпадает, сканирование прекращается, и булева переменная doConnect изменяется на true. Таким образом, мы знаем, что нашли нужный сервер, и можем начать установку соединения.

//Callback function that gets called, when another device's advertisement has been received
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    if (advertisedDevice.getName() == bleServerName) { //Check if the name of the advertiser matches
      advertisedDevice.getScan()->stop(); //Scan can be stopped, we found what we are looking for
      pServerAddress = new BLEAddress(advertisedDevice.getAddress()); //Address of advertiser is the one we need
      doConnect = true; //Set indicator, stating that we are ready to connect
      Serial.println("Device found. Connecting!");
    }
  }
};

Подключение к серверу

Если переменная doConnect равна true, происходит попытка подключения к BLE-серверу. Функция connectToServer() обрабатывает соединение между клиентом и сервером.

//Connect to the BLE Server that has the name, Service, and Characteristics
bool connectToServer(BLEAddress pAddress) {
   BLEClient* pClient = BLEDevice::createClient();

  // Connect to the remove BLE Server.
  pClient->connect(pAddress);
  Serial.println(" - Connected to server");

  // Obtain a reference to the service we are after in the remote BLE server.
  BLERemoteService* pRemoteService = pClient->getService(bmeServiceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
    Serial.println(bmeServiceUUID.toString().c_str());
    return (false);
  }

  // Obtain a reference to the characteristics in the service of the remote BLE server.
  temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
  humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);

  if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
    Serial.print("Failed to find our characteristic UUID");
    return false;
  }
  Serial.println(" - Found our characteristics");

  //Assign callback functions for the Characteristics
  temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
  humidityCharacteristic->registerForNotify(humidityNotifyCallback);
  return true;
}

Также назначаются функции обратного вызова, отвечающие за обработку при получении нового значения.

//Assign callback functions for the Characteristics
temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
humidityCharacteristic->registerForNotify(humidityNotifyCallback);

После подключения BLE-клиента к серверу необходимо активировать свойство notify для каждой характеристики. Для этого используйте метод writeValue() для дескриптора.

if (connectToServer(*pServerAddress)) {
  Serial.println("We are now connected to the BLE Server.");
  //Activate the Notify property of each Characteristic
  temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
  humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);

Уведомление о новых значениях

Когда клиент получает новое уведомление, вызываются две функции: temperatureNotifyCallback() и humidityNotifyCallback(), которые отвечают за получение нового значения, обновление OLED новыми показаниями и их вывод в Serial Monitor.

//When the BLE Server sends a new temperature reading with the notify property
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
                                        uint8_t* pData, size_t length, bool isNotify) {
  //store temperature value
  temperatureChar = (char*)pData;
  newTemperature = true;
}
//When the BLE Server sends a new humidity reading with the notify property
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
                                    uint8_t* pData, size_t length, bool isNotify) {
  //store humidity value
  humidityChar = (char*)pData;
  newHumidity = true;
  Serial.print(newHumidity);
}

Эти две функции выполняются каждый раз, когда BLE-сервер уведомляет клиента новым значением, что происходит каждые 30 секунд. Эти функции сохраняют полученные значения в переменных temperatureChar и humidityChar. Они также изменяют переменные newTemperature и newHumidity на true, чтобы мы знали, что получили новые показания.

Отображение новых показаний температуры и влажности

В функции loop() есть условие if, которое проверяет, доступны ли новые показания. Если новые показания есть, мы устанавливаем переменные newTemperature и newHumidity в false, чтобы позже можно было получить новые показания. Затем вызываем функцию printReadings() для отображения показаний на OLED.

//if new temperature readings are available, print in the OLED
if (newTemperature && newHumidity){
  newTemperature = false;
  newHumidity = false;
  printReadings();
}

Тестирование проекта

На этом код готов. Вы можете загрузить его на вашу плату ESP32.

После загрузки кода включите питание ESP32 BLE-сервера, затем включите питание ESP32 со скетчем клиента. Клиент начнёт сканирование ближайших устройств, и когда найдёт другой ESP32, установит Bluetooth-соединение. Каждые 30 секунд дисплей обновляется последними показаниями.

Демонстрация ESP32 BLE Клиент Сервер с OLED-дисплеем

Важно: не забудьте отключить ваш смартфон от BLE-сервера. В противном случае ESP32 BLE-клиент не сможет подключиться к серверу.

ESP32 BLE-клиент подключён к ESP32 BLE-серверу - Serial Monitor

Заключение

В этом руководстве вы узнали, как создать BLE-сервер и BLE-клиент с помощью ESP32. Вы научились устанавливать новые значения температуры и влажности в характеристиках BLE-сервера. Затем другие BLE-устройства (клиенты) могут подключаться к этому серверу и считывать значения этих характеристик, чтобы получать актуальные значения температуры и влажности. Эти характеристики имеют свойство notify, благодаря которому клиент получает уведомление при каждом изменении значения.

Использование BLE — это ещё один протокол связи, который можно использовать с платами ESP32 помимо Wi-Fi. Надеемся, что это руководство оказалось для вас полезным. У нас есть руководства и по другим протоколам связи, которые могут быть вам полезны.

Узнайте больше об ESP32 с помощью наших ресурсов:


Источник: ESP32 BLE Server and Client (Bluetooth Low Energy)