ESP32:Примеры/BLE-сервер и BLE-клиент при помощи ESP32

Материал из Онлайн справочника
Версия от 22:55, 16 января 2019; Myagkij (обсуждение | вклад) (→‎Сервер)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Черновик


BLE-сервер и BLE-клиент при помощи ESP32

В этом примере мы научимся настраивать Bluetooth-соединение между двумя платами ESP32. Одна из этих ESP32 будет работать в качестве сервера, а другая – в качестве клиента. В проекте, который мы сделаем ниже, к серверу будет подключен датчик температуры и влажности (DHT11/DHT22), и этот сервер каждые 10 секунд будет отправлять клиенту самые последние данные, считанные с датчика. Кроме того, клиент будет печатать эти данные в подключенном к нему OLED-дисплее.

Итак, на одну ESP32 будет загружен серверный скетч, а на другую – клиентский. Клиент начнет сканировать близлежащие устройства и, найдя другую ESP32, подключится к ней. Спустя несколько секунд ESP32-клиент начнет получать данные о температуре и влажности.

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

Устанавливаем библиотеку «DHT Sensor»

Чтобы считывать данные с датчика DHT11/DHT22 при помощи IDE Arduino, нам понадобится установить библиотеку «DHT Sensor». Для этого проделайте следующее:

  1. Кликните тут, чтобы скачать ZIP-архив библиотеки;
  2. Распакуйте скачанный архив. У вас должна получиться папка под названием «DHT-sensor-library-master»;
  3. Переименуйте ее на «DHT_sensor»;
  4. Переместите папку «DHT_sensor» в папку «libraries», которая находится внутри папки, куда установлена IDE Arduino;
  5. Откройте IDE Arduino;

Устанавливаем библиотеку «Adafruit Sensor»

Нам также нужно установить библиотеку «Adafruit Sensor». Для этого проделайте следующее:

  1. Кликните тут, чтобы скачать ZIP-архив библиотеки;
  2. Распакуйте скачанный архив. У вас должна получиться папка «Adafruit_sensor-master»;
  3. Переименуйте ее на «Adafruit_Sensor»;
  4. Переместите папку «Adafruit_Sensor» в папку «libraries», которая находится внутри папки, где установлена IDE Arduino;
  5. Откройте IDE Arduino;

Устанавливаем библиотеку «Adafruit SSD1306»

Библиотека «Adafruit SSD1306» упрощает печать текста на OLED-дисплее при помощи IDE Arduino. Чтобы установить эту библиотеку в IDE Arduino, проделайте следующее:

  1. Кликните тут, чтобы скачать ZIP-архив библиотеки
  2. Распакуйте этот архив. У вас должна появиться папка «Adafruit_SSD1306-master»
  3. Переименуйте ее на «Adafruit_SSD1306»
  4. Переместите папку «Adafruit_SSD1306» в папку «libraries», которая находится внутри папки, где установлена ваша IDE Arduino
  5. Откройте IDE Arduino

Устанавливаем библиотеку «Adafruit GFX»

Нам также понадобится библиотека «Adafruit GFX». Чтобы установить ее в IDE Arduino, проделайте следующее:

  1. Кликните тут, чтобы скачать ZIP-архив библиотеки
  2. Распакуйте этот архив. У вас должна получиться папка «Adafruit-GFX-library-master»
  3. Переименуйте ее на «Adafruit_GFX_library»
  4. Переместите папку «Adafruit_GFX_library» в папку «libraries», которая находится в папке, где установлена ваша IDE Arduino
  5. Откройте IDE Arduino

Как работает код

Серверный код

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

Примечание: Для этого скетча необходимо, чтобы в IDE Arduino были установлены библиотеки «ESP32 BLE Arduino», «DHT sensor» и «Adafruit Sensor».

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

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

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include "DHT.h"

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

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

// закомментируйте строчку ниже, если вам нужны градусы Фаренгейта:
#define temperatureCelsius

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

Задаем название для BLE-сервера

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

#define bleServerName "dhtESP32"

Выбираем тип датчика

Во этом фрагменте мы выбираем тип используемого датчика. В данном случае выбран DHT11:

#define DHTTYPE DHT11 // DHT 11
//#define DHTTYPE DHT21 // DHT 21 (AM2301)
//#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321

Задаем UUID

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

#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"
#ifdef temperatureCelsius
BLECharacteristic dhtTemperatureCelsiusCharacteristics("cba1d466-344c4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor dhtTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
BLECharacteristic dhtTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor dhtTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2901));
#endif
BLECharacteristic dhtHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor dhtHumidityDescriptor(BLEUUID((uint16_t)0x2903));

Задаем номер контакта

В следующей строчке задаем номер контакта, к которому подключен датчик DHT. В нашем случае датчик будет подключен к цифровому контакту GPIO14. Если вы подключили его к какому-то другому контакту, не забудьте поменять и эту строчку кода:

const int DHTPin = 14;

setup()

Блок setup() начинаем с запуска датчика DHT:

// запускаем датчик DHT:
dht.begin();

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

// запускаем последовательную коммуникацию:
Serial.begin(115200);

Создаем новое BLE-устройство, которому даем заданное ранее название «bleServerName».

// создаем BLE-устройство:
BLEDevice::init(bleServerName);

Делаем это BLE-устройство сервером и привязываем к нему функции обратного вызова.

// создаем BLE-сервер:
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

Функция MyServerCallbacks() содержит две функции обратного вызова, переключающие значение в булевой переменной «deviceConnected» между «true» и «false» в зависимости от текущего статуса подключения BLE-устройства. Это значит, что если клиент подключен к серверу, этим значением будет «true», а если не подключен, то «false». Ниже – фрагмент кода, задающий то, как работает функция MyServerCallbacks():

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

Далее в блоке setup() запускаем BLE-устройство с заданным ранее UUID.

// создаем BLE-сервис:
BLEService *dhtService = pServer->createService(SERVICE_UUID);

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

#ifdef temperatureCelsius
    dhtService->addCharacteristic(&dhtTemperatureCelsiusCharacteristics);
    dhtTemperatureCelsiusDescriptor.setValue("DHT temperature Celsius");
                                         //  "Температура в Цельсиях"
    dhtTemperatureCelsiusCharacteristics.addDescriptor(new BLE2902());

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

#else
    dhtService->addCharacteristic(&dhtTemperatureFahrenheitCharacteristics);
    dhtTemperatureFahrenheitDescriptor.setValue("DHT temperature Fahrenheit");
                                            //  "Температура в Фаренгейтах"
    dhtTemperatureFahrenheitCharacteristics.addDescriptor(new BLE2902());

После этого ESP32-сервер запустит характеристику для данных о влажности:

dhtService->addCharacteristic(&dhtHumidityCharacteristics);
dhtHumidityDescriptor.setValue("DHT humidity"); 
                             //  "Влажность"
dhtHumidityCharacteristics.addDescriptor(new BLE2902());

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

// запускаем сервис:
dhtService->start();

// запускаем рассылку оповещений:
pServer->getAdvertising()->start();

loop()

Код в блоке loop() достаточно прост. Мы просто постоянно проверяем, подключено ли устройство или нет. Если подключено, считываем текущую температуру и влажность:

if (deviceConnected) {
    // считываем температуру в градусах Цельсия (по умолчанию):
    float t = dht.readTemperature();
    // считываем температуру в градусах Фаренгейта
    // (isFahrenheit = true):
    float f = dht.readTemperature(true);
    // считываем влажность:
    float h = dht.readHumidity();

Также задаем условие, проверяющее, корректны ли считанные данные или нет.

if (isnan(h) || isnan(t) || isnan(f)) {
  Serial.println("Failed to read from DHT sensor!");
             //  "Не удалось прочесть данные с датчика DHT!"
  return;

Если считанные данные корректны, код продолжит работать.

Если вы используете температуру в Цельсиях, то запустится фрагмент кода ниже. В нем данные о температуре сохраняются во временную переменную «temperatureCTemp».

#ifdef temperatureCelsius
  static char temperatureCTemp[7];
  dtostrf(t, 6, 2, temperatureCTemp);

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

dhtTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
dhtTemperatureCelsiusCharacteristics.notify();

Три строчки ниже печатают в мониторе порта данные о температуре (в отладочных целях).

Serial.print("Temperature Celsius: ");
         //  "Температура в градусах Цельсия: "
Serial.print(t);
Serial.print(" *C");

Далее используем аналогичный способ для отправки температуры в градусах Фаренгейта.

#else
  static char temperatureFTemp[7];
  dtostrf(f, 6, 2, temperatureFTemp);
  // задаем значение для температурной характеристики (Фаренгейт)
  // и отправляем уведомление подключенному клиенту:
  dhtTemperatureFahrenheitCharacteristics.setValue(temperatureFTemp);
  dhtTemperatureFahrenheitCharacteristics.notify();
  Serial.print("Temperature Fahrenheit: ");
           //  "Температура в градусах Фаренгейта: "
  Serial.print(f);
  Serial.print(" *F");
#endif

И тот же способ используем для отправки данных о влажности.

// отправляем уведомление о том,
// что с датчика DHT считаны данные о влажности:
static char humidityTemp[7];
dtostrf(h, 6, 2, humidityTemp);
// задаем значение для влажностной характеристики
// и отправляем уведомление подключенному клиенту:
dhtHumidityCharacteristics.setValue(humidityTemp);
dhtHumidityCharacteristics.notify();   
Serial.print(" - Humidity: ");
         //  " - Влажность: "
Serial.print(h);
Serial.println(" %");

Функция delay() задает 10-секундную паузу между считываниями данных с датчика.

delay(10000);

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

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

Загрузите код на плату. Затем возьмите смартфон и откройте приложение nRF connect от Nordic.

Убедитесь, что ESP32 включена, затем включите Bluetooth на смартфоне и начните сканирование. В результате вы должны найти устройство под названием «dhtESP32» – это название ESP32-сервера, заданное нами ранее.

Подключитесь к нему и откройте сервис, созданный нами в скетче.

Активируйте свойства «Notify» для температуры и влажности.

После этого в приложении каждые 10 секунд должны начать появляться новые данные от датчика.

Если все в порядке, это значит, что BLE-сервер на базе ESP32 работает как надо!

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

Клиентский код

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

Вначале подключаем необходимые библиотеки:

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

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

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

// по умолчанию температура будет в градусах Цельсия,
// но если вам нужны градусы Фаренгейта, закомментируйте строчку ниже:
#define temperatureCelsius

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

Задаем название и UUID для BLE-сервера

Как уже говорилось ранее, лучше использовать те UUID и название BLE-сервера, что стоят в коде по умолчанию – так они будут совпадать с теми, что заданы в серверном скетче.

Таким образом, название BLE-сервера должно быть следующим:

#define bleServerName "dhtESP32"

UUID должны быть такими:

// UUID для сервиса:
static BLEUUID dhtServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");

#ifdef temperatureCelsius
  // UUID для температурной характеристики (градусы Цельсия):
  static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
  // UUID для температурной характеристики (градусы Фаренгейта):
  static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif

// UUID для влажностной характеристики:
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");

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

Далее объявляем несколько переменных, которые понадобятся нам в дальнейшем:

// переменные, используемые для определения того,
// нужно ли начинать подключение или завершено ли подключение:
static boolean doConnect = false;
static boolean connected = false;

// адрес периферийного устройства;
// (он должен быть найден во время сканирования):
static BLEAddress *pServerAddress;
 
// характеристики, данные которых мы хотим прочесть:
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;

// включение/выключение уведомлений:
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};

setup()

Блок setup() начинаем, задав правильные настройки для OLED-дисплея:

Wire.begin();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.setTextSize(1);
//display.setBackgroundcolor(BLACK);
display.setTextColor(WHITE,0);

Затем печатаем на OLED-дисплее первое сообщение: «DHT READINGS» («Данные от DHT-датчика").

display.setCursor(30,0);
display.print("DHT READINGS");  //  "Данные от DHT-датчика"
display.display();

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

Serial.begin(115200);

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

BLEDevice::init("");

Сканируем близлежащие устройства

Фрагмент кода ниже предназначен для сканирования находящихся рядом устройств.

BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(30);

Создаем функцию MyAdvertisedDeviceCallbacks()

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

// функция обратного вызова, которая будет вызвана
// при получении оповещения от другого устройства:
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // проверяем, совпадает ли название 
    // BLE-сервера, рассылающего оповещения:
    if (advertisedDevice.getName() == bleServerName) {
      // мы нашли, что искали, 
      // поэтому сканирование можно завершить:
      advertisedDevice.getScan()->stop(); 
      // сохраняем адрес устройства, рассылающего оповещения: 
      pServerAddress = new BLEAddress(advertisedDevice.getAddress()); 
      // задаем индикатор, дающий понять,
      // что мы готовы подключиться:
      doConnect = true;
      Serial.println("Device found. Connecting!");
                 //  "Устройство найдено. Подключаемся!"
    }
  }
};

Подключаемся к серверу

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

// функция для подключения к BLE-серверу,
// у которого есть название, сервис и характеристики:
bool connectToServer(BLEAddress pAddress) {
   BLEClient* pClient = BLEDevice::createClient();

  // подключаемся к удаленному BLE-серверу:
  pClient->connect(pAddress);
  Serial.println(" - Connected to server");
             //  " – Подключились к серверу"
 
  // считываем UUID искомого сервиса:
  BLERemoteService* pRemoteService = pClient->getService(dhtServiceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
             //  "Не удалось найти UUID нашего сервиса: "
    Serial.println(dhtServiceUUID.toString().c_str());
    return (false);
  }

  // считываем UUID искомых характеристик:
  temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
  humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);

  if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
    Serial.print("Failed to find our characteristic UUID");
             //  "Не удалось найти UUID нашей характеристики"
    return false;
  }
  Serial.println(" - Found our characteristics");
             //  " – Наши характеристики найдены"

Также присваиваем характеристикам функции обратного вызова, ответственные за то, что будет происходить при получении новых данных.

  // присваиваем характеристикам функции обратного вызова:
  temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
  humidityCharacteristic->registerForNotify(humidityNotifyCallback);
}

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

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-экране и их печать в мониторе порта.

// функция обратного вызова, которая будет запущена,
// если BLE-сервер пришлет вместе с уведомлением
// корректные данные о температуре:
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
  display.setCursor(34,10);
  display.print((char*)pData);
  Serial.print("Temperature: ");  //  "Температура: "
  Serial.print((char*)pData);
  #ifdef temperatureCelsius
    // температура в градусах Цельсия:
    display.print(" *C");
    Serial.print(" *C");
  #else
    // температура в градусах Фаренгейта:
    display.print(" *F");
    Serial.print(" *F");
  #endif  
  display.display();
}

// функция обратного вызова, которая будет запущена,
// если BLE-сервер пришлет вместе с уведомлением
// корректные данные о влажности:
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
  display.setCursor(34,20);
  display.print((char*)pData);
  display.print(" %");
  display.display();
  Serial.print(" Humidity: ");  //  " Влажность: "
  Serial.print((char*)pData); 
  Serial.println(" %");
}

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

Тестируем проект

С кодом все. Можете загрузить его на ESP32.

Загрузив код, запитайте ESP32, настроенную ранее (ESP32-сервер), а затем запитайте ESP32, настроенную в этом Разделе (ESP32-клиент). Клиент начнет сканировать близлежащие устройства и, найдя другую ESP32, установит с ней Bluetooth-соединение. Если все настроено правильно, ESP32-клиент будет каждые 10 секунд показывать на OLED-дисплее самые последние данные, считанные с датчика.

Возможные проблемы и их решение

Если на OLED-дисплее не печатаются никакие данные, то причины у этого, как правило, две:

  • Датчик DHT не смог прочесть данные о температуре и влажности. Откройте монитор порта для BLE-сервера, чтобы посмотреть, печатаются ли в нем какие-нибудь данные
  • У клиента не получается подключиться к серверу. Перезагрузите ESP32, на которой работает клиентский скетч

Проект готов! Теперь вы знаете, как создать два BLE-устройства, обменивающихся полезными данными. Смело экспериментируйте с этими скетчами и пробуйте подключить другие датчики.

Необходимое оборудование

Схема

Сервер

Давайте начнем с подключения компонентов друг к другу. Подключите датчик DHT11/DHT22 к ESP32 при помощи резистора на 4.7 кОм (см. схему ниже).

На этой схеме изображена 36-контактная версия ESP32 DEVKIT DOIT V1. Если вы используете другую модель, обязательно сверьтесь с ее распиновкой.


Датчик DHT11/DHT22 можно подключить к любому цифровому контакту ESP32. Потом вам нужно будет лишь поменять в скетче номер контакта, к которому подключен датчик.

Клиент

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

OLED-дисплею нужно 3.3-вольтовое питание, контакт SCL нужно подключить к GPIO22, а контакт SDA – к GPIO21.

Код

Скетч для сервера

/*********
  Руи Сантос
  Более подробно о проекте на: http://randomnerdtutorials.com  
*********/

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include "DHT.h"

// по умолчанию температура будет в градусах Цельсия;
// закомментируйте строчку ниже, если вам нужны градусы Фаренгейта:
#define temperatureCelsius

// даем название BLE-серверу:
#define bleServerName "dhtESP32"

// оставьте незакомментированной строчку,
// соответствующую используемому вами типу датчика:
#define DHTTYPE DHT11   // DHT 11
//#define DHTTYPE DHT21   // DHT 21 (AM2301)
//#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321

// для генерирования UUID можно воспользоваться этим сайтом:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"

#ifdef temperatureCelsius
  BLECharacteristic dhtTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor dhtTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
  BLECharacteristic dhtTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor dhtTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2901));
#endif

BLECharacteristic dhtHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor dhtHumidityDescriptor(BLEUUID((uint16_t)0x2903));

// контакт, к которому подключен датчик DHT:
const int DHTPin = 14;

// инициализируем датчик DHT:
DHT dht(DHTPin, DHTTYPE);

bool deviceConnected = false;

// задаем функции обратного вызова onConnect() и onDisconnect():
class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
  }
};

void setup() {
  // запускаем датчик DHT:
  dht.begin();

  // запускаем последовательную коммуникацию:
  Serial.begin(115200);

  // создаем BLE-устройство:
  BLEDevice::init(bleServerName);

  // создаем BLE-сервер:
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // создаем BLE-сервис:
  BLEService *dhtService = pServer->createService(SERVICE_UUID);

  // создаем BLE-характеристики и BLE-дескриптор: bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml       

  #ifdef temperatureCelsius
    dhtService->addCharacteristic(&dhtTemperatureCelsiusCharacteristics);
    dhtTemperatureCelsiusDescriptor.setValue("DHT temperature Celsius");
                                         //  "Температура в Цельсиях"
    dhtTemperatureCelsiusCharacteristics.addDescriptor(new BLE2902());
  #else
    dhtService->addCharacteristic(&dhtTemperatureFahrenheitCharacteristics);
    dhtTemperatureFahrenheitDescriptor.setValue("DHT temperature Fahrenheit");
                                         //  "Температура в Фаренгейтах"
    dhtTemperatureFahrenheitCharacteristics.addDescriptor(new BLE2902());
  #endif  
  dhtService->addCharacteristic(&dhtHumidityCharacteristics);
  dhtHumidityDescriptor.setValue("DHT humidity"); 
                             //  "Влажность"
  dhtHumidityCharacteristics.addDescriptor(new BLE2902());
  
  // запускаем сервис:
  dhtService->start();

  // запускаем рассылку оповещений:
  pServer->getAdvertising()->start();
  Serial.println("Waiting a client connection to notify...");
             //  "Ждем подключения клиента, чтобы отправить уведомление..."
}

void loop() {
  if (deviceConnected) {
    // считываем температуру в градусах Цельсия (по умолчанию):
    float t = dht.readTemperature();
    // считываем температуру в градусах Фаренгейта
    // (isFahrenheit = true):
    float f = dht.readTemperature(true);
    // считываем влажность:
    float h = dht.readHumidity();
 
    // проверяем, удалось ли прочесть данные,
    // и если нет, то выходим из loop(), чтобы попробовать снова:
    if (isnan(h) || isnan(t) || isnan(f)) {
      Serial.println("Failed to read from DHT sensor!");
                 //  "Не удалось прочесть данные с датчика DHT!"
      return;
    }
    // отправляем уведомление о том,
    // что с датчика DHT считаны данные о температуре:
    #ifdef temperatureCelsius
      static char temperatureCTemp[7];
      dtostrf(t, 6, 2, temperatureCTemp);
      // задаем значение для температурной характеристики (Цельсий)
      // и отправляем уведомление подключенному клиенту:
      dhtTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
      dhtTemperatureCelsiusCharacteristics.notify();
      Serial.print("Temperature Celsius: ");
               //  "Температура в градусах Цельсия: "
      Serial.print(t);
      Serial.print(" *C");
    #else
      static char temperatureFTemp[7];
      dtostrf(f, 6, 2, temperatureFTemp);
      // задаем значение для температурной характеристики (Фаренгейт)
      // и отправляем уведомление подключенному клиенту:
   dhtTemperatureFahrenheitCharacteristics.setValue(temperatureFTemp);
      dhtTemperatureFahrenheitCharacteristics.notify();
      Serial.print("Temperature Fahrenheit: ");
               //  "Температура в градусах Фаренгейта: "
      Serial.print(f);
      Serial.print(" *F");
    #endif
    
    // отправляем уведомление о том,
    // что с датчика DHT считаны данные о влажности:
    static char humidityTemp[7];
    dtostrf(h, 6, 2, humidityTemp);
    // задаем значение для влажностной характеристики
    // и отправляем уведомление подключенному клиенту:
    dhtHumidityCharacteristics.setValue(humidityTemp);
    dhtHumidityCharacteristics.notify();   
    Serial.print(" - Humidity: ");
             //  " - Влажность: "
    Serial.print(h);
    Serial.println(" %");
    
    delay(10000);
  }
}

Скетч для клиента

/*********
  Руи Сантос
  Более подробно о проекте на: http://randomnerdtutorials.com  
*********/

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

// по умолчанию температура будет в градусах Цельсия,
// но если вам нужны градусы Фаренгейта, закомментируйте строчку ниже:
#define temperatureCelsius

// задаем название для BLE-сервера 
// (это другая ESP32, на которой запущен серверный скетч):
#define bleServerName "dhtESP32"

// UUID для сервиса:
static BLEUUID dhtServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");

#ifdef temperatureCelsius
  // UUID для температурной характеристики (градусы Цельсия):
  static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
  // UUID для температурной характеристики (градусы Фаренгейта):
  static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif

// UUID для влажностной характеристики:
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");

// переменные, используемые для определения того,
// нужно ли начинать подключение или завершено ли подключение:
static boolean doConnect = false;
static boolean connected = false;

// адрес периферийного устройства;
// (он должен быть найден во время сканирования):
static BLEAddress *pServerAddress;
 
// характеристики, данные которых мы хотим прочесть:
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;

// включение/выключение уведомлений:
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};

#define SCREEN_WIDTH 128 // ширина OLED-дисплея (в пикселях)
#define SCREEN_HEIGHT 32 // высота OLED-дисплея (в пикселях)

// создаем дисплей SSD1306, 
// подключенный через I2C (контакты SDA и SCL):
#define OLED_RESET     4 // номер контакта для сброса
                         // (или «-1», если контакт для сброса
                         // такой же, как и у Arduino)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// подключаемся к BLE-серверу,
// у которого есть название, сервис и характеристики:
bool connectToServer(BLEAddress pAddress) {
   BLEClient* pClient = BLEDevice::createClient();
 
  // подключаемся к удаленному BLE-серверу:
  pClient->connect(pAddress);
  Serial.println(" - Connected to server");
             //  " – Подключились к серверу"
 
  // считываем UUID искомого сервиса:
  BLERemoteService* pRemoteService = pClient->getService(dhtServiceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
             //  "Не удалось найти UUID нашего сервиса: "
    Serial.println(dhtServiceUUID.toString().c_str());
    return (false);
  }
 
  // считываем UUID искомых характеристик:
  temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
  humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);

  if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
    Serial.print("Failed to find our characteristic UUID");
             //  "Не удалось найти UUID нашей характеристики"
    return false;
  }
  Serial.println(" - Found our characteristics");
             //  " – Наши характеристики найдены"
 
  // присваиваем характеристикам функции обратного вызова:
  temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
  humidityCharacteristic->registerForNotify(humidityNotifyCallback);
}

// функция обратного вызова, которая будет вызвана
// при получении оповещения от другого устройства:
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // проверяем, совпадает ли название 
    // BLE-сервера, рассылающего оповещения:
    if (advertisedDevice.getName() == bleServerName) {
      // мы нашли, что искали, 
      // поэтому сканирование можно завершить:
      advertisedDevice.getScan()->stop(); 
      // сохраняем адрес устройства, рассылающего оповещения: 
      pServerAddress = new BLEAddress(advertisedDevice.getAddress()); 
      // задаем индикатор, дающий понять,
      // что мы готовы подключиться:
      doConnect = true;
      Serial.println("Device found. Connecting!");
                 //  "Устройство найдено. Подключаемся!"
    }
  }
};
 
// функция обратного вызова, которая будет запущена,
// если BLE-сервер пришлет вместе с уведомлением
// корректные данные о температуре:
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, 
                                        uint8_t* pData, size_t length, bool isNotify) {
  display.setCursor(34,10);
  display.print((char*)pData);
  Serial.print("Temperature: ");  //  "Температура: "
  Serial.print((char*)pData);
  #ifdef temperatureCelsius
    // температура в градусах Цельсия:
    display.print(" *C");
    Serial.print(" *C");
  #else
    // температура в градусах Фаренгейта:
    display.print(" *F");
    Serial.print(" *F");
  #endif  
  display.display();
}

// функция обратного вызова, которая будет запущена,
// если BLE-сервер пришлет вместе с уведомлением
// корректные данные о влажности:
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, 
                                    uint8_t* pData, size_t length, bool isNotify) {
  display.setCursor(34,20);
  display.print((char*)pData);
  display.print(" %");
  display.display();
  Serial.print(" Humidity: ");  //  " Влажность: "
  Serial.print((char*)pData); 
  Serial.println(" %");
}

void setup() {
  // настраиваем OLED-дисплей;
  // параметр «SSD1306_SWITCHCAPVCC» в функции begin() задает,
  // что напряжение для дисплея будет генерироваться
  // от внутренней 3.3-вольтовой цепи,
  // а параметр «0x3C» означает «128x32»:
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
                 //  "Не удалось настроить SSD1306"
    for(;;); // дальше не продолжаем,
             // навечно оставшись в блоке loop()
  }
  
  display.clearDisplay();
  display.setTextSize(1);
  //display.setBackgroundcolor(BLACK);
  display.setTextColor(WHITE,0);
  display.setCursor(30,0);
  display.print("DHT READINGS");  //  "Данные от DHT-датчика"
  display.display();
  
  // запускаем последовательную коммуникацию:
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
             //  "Запуск клиентского BLE-приложения... "

  // инициализируем BLE-устройство:
  BLEDevice::init("");
 
  // создаем экземпляр класса «BLEScan» для сканирования
  // и задаем для этого объекта функцию обратного вызова,
  // которая будет информировать о том, найдено ли новое устройство;
  // дополнительно указываем, что нам нужно активное сканирование,
  // а потом запускаем 30-секундное сканирование:
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(30);
}

void loop() {
  // если в переменной «doConnect» значение «true»,
  // то это значит, что сканирование завершено,
  // и мы нашли нужный BLE-сервер, к которому хотим подключиться; 
  // теперь пора, собственно, подключиться к нему;
  // подключившись, мы записываем в «connected» значение «true»:
  if (doConnect == true) {
    if (connectToServer(*pServerAddress)) {
      Serial.println("We are now connected to the BLE Server.");
                 //  "Подключение к BLE-серверу прошло успешно."
      // активируем свойство «notify» у каждой характеристики:
      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.");
                 //  "Подключиться к серверу не получилось.
                 //   Перезапустите устройство, чтобы снова
                 //   просканировать ближайший BLE-сервер."
    }
    doConnect = false;
  }
  delay(1000); // делаем секундную задержку между циклами loop()
}

См.также

Внешние ссылки