ESP32 BLE Сервер и Клиент (Bluetooth Low Energy)
Узнайте, как создать BLE (Bluetooth Low Energy) соединение между двумя платами ESP32. Одна ESP32 будет сервером, а другая ESP32 будет клиентом. BLE-сервер объявляет характеристики, содержащие показания датчиков, которые клиент может считывать. BLE-клиент ESP32 считывает значения этих характеристик (температуру и влажность) и отображает их на OLED-дисплее.
Рекомендуемое чтение: Начало работы с 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 может выступать как в роли клиента, так и в роли сервера.
Сервер объявляет о своём существовании, чтобы другие устройства могли его найти, и содержит данные, которые клиент может считывать. Клиент сканирует ближайшие устройства и, когда находит нужный сервер, устанавливает соединение и слушает входящие данные. Это называется коммуникацией «точка-точка».
Существуют и другие возможные режимы связи, такие как режим широковещания и ячеистая сеть (mesh network), которые не рассматриваются в этом руководстве.
GATT
GATT расшифровывается как Generic Attributes (общие атрибуты) и определяет иерархическую структуру данных, которая предоставляется подключённым BLE-устройствам. Это означает, что GATT определяет способ, которым два BLE-устройства отправляют и получают стандартные сообщения. Понимание этой иерархии важно, поскольку оно облегчит использование BLE с ESP32.
Профиль (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-клиентом.
BLE-сервер ESP32 подключён к датчику BME280 и обновляет значения характеристик температуры и влажности каждые 30 секунд.
ESP32-клиент подключается к BLE-серверу и получает уведомления о значениях характеристик температуры и влажности. Этот ESP32 подключён к OLED-дисплею и выводит последние показания.
Этот проект разделён на две части:
Часть 1 — ESP32 BLE-сервер
Часть 2 — ESP32 BLE-клиент
Необходимые компоненты
Вот список компонентов, необходимых для выполнения этого проекта:
ESP32 BLE-сервер:
Плата ESP32 DOIT DEVKIT V1 (читайте Лучшие платы разработки ESP32)
Смартфон с Bluetooth (необязательно)
ESP32 BLE-клиент:
Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!
1) ESP32 BLE-сервер
В этой части мы настроим BLE-сервер, который объявляет сервис, содержащий две характеристики: одну для температуры и другую для влажности. Эти характеристики имеют свойство Notify для уведомления клиента о новых значениях.
Схема подключения
BLE-сервер ESP32 будет объявлять характеристики с температурой и влажностью от датчика BME280. Вы можете использовать любой другой датчик, если добавите необходимые строки в код.
Мы будем использовать I2C-связь с модулем датчика BME280. Для этого подключите датчик к стандартным выводам ESP32 SCL (GPIO 22) и SDA (GPIO 21), как показано на следующей схеме подключения.
Рекомендуемое чтение: Справочник по распиновке 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. Он отобразит сообщение, как показано ниже.
Затем вы можете проверить, правильно ли работает BLE-сервер, используя приложение для BLE-сканирования на вашем смартфоне, например nRF Connect. Это приложение доступно для Android и iOS.
После установки приложения включите Bluetooth на вашем смартфоне. Откройте приложение nRF Connect и нажмите кнопку Scan (Сканировать). Оно найдёт все Bluetooth-устройства поблизости, включая ваше устройство BME280_ESP32 (это имя BLE-сервера, которое вы определили в коде).
Подключитесь к устройству BME280_ESP32, а затем выберите вкладку клиента (интерфейс может немного отличаться). Вы можете увидеть, что оно объявляет сервис с UUID, который мы определили в коде, а также характеристики температуры и влажности. Обратите внимание, что эти характеристики имеют свойство Notify.
Ваш ESP32 BLE-сервер готов!
Перейдите к следующему разделу, чтобы создать ESP32-клиент, который подключается к серверу для получения доступа к характеристикам температуры и влажности и отображения показаний на OLED-дисплее.
2) ESP32 BLE-клиент
В этом разделе мы создадим ESP32 BLE-клиент, который установит соединение с ESP32 BLE-сервером и отобразит показания на OLED-дисплее.
Схема подключения
ESP32 BLE-клиент подключён к OLED-дисплею. Дисплей показывает показания, полученные по Bluetooth.
Подключите OLED-дисплей к ESP32, следуя приведённой ниже схеме. Вывод SCL подключается к GPIO 22, а вывод SDA — к GPIO 21.
Установка библиотек 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 секунд дисплей обновляется последними показаниями.
Важно: не забудьте отключить ваш смартфон от BLE-сервера. В противном случае ESP32 BLE-клиент не сможет подключиться к серверу.
Заключение
В этом руководстве вы узнали, как создать BLE-сервер и BLE-клиент с помощью ESP32. Вы научились устанавливать новые значения температуры и влажности в характеристиках BLE-сервера. Затем другие BLE-устройства (клиенты) могут подключаться к этому серверу и считывать значения этих характеристик, чтобы получать актуальные значения температуры и влажности. Эти характеристики имеют свойство notify, благодаря которому клиент получает уведомление при каждом изменении значения.
Использование BLE — это ещё один протокол связи, который можно использовать с платами ESP32 помимо Wi-Fi. Надеемся, что это руководство оказалось для вас полезным. У нас есть руководства и по другим протоколам связи, которые могут быть вам полезны.
Узнайте больше об ESP32 с помощью наших ресурсов:
Источник: ESP32 BLE Server and Client (Bluetooth Low Energy)