Über den Beitrag
In diesem Beitrag möchte ich euch den Einsatz von BLE (Bluetooth® Low Energy) auf ESP32-basierten Boards näherbringen. Während sich das klassische Bluetooth mit der BluetoothSerial-Bibliothek des Arduino-ESP32-Boardpakets sehr einfach nutzen lässt, erfordert BLE mit der gleichnamigen Bibliothek etwas mehr Einarbeitung und ein grundlegendes Verständnis der zugrunde liegenden Konzepte.
Ich beschränke mich in diesem Beitrag auf den ESP32, weil sich die Bibliotheken anderer Boards wie etwa ArduinoBLE für BLE-fähige Arduino-Boards (Arduino Nano 33 IoT/BLE, Arduino UNO R4 WiFi etc.) doch erheblich unterscheiden. Alles in einem Beitrag zu behandeln, wäre zu unübersichtlich und zu lang geworden.
Folgendes erwartet euch:
- Ein kurzer Überblick über BLE
- Ein erster Minimal-Sketch
- Callbacks
- Notify und Deskriptoren
- Zwei ESP32 über BLE verbinden
- Weitere Anwendungsbeispiele
- BLE-„HC-05“-Module
- Anhang
Ein kurzer Überblick über BLE
Ich muss zunächst einige BLE-relevante Grundbegriffe einführen, die euch zu Beginn verwirren dürften (ging mir nicht anders!). Mit den Beispielsketchen wird sich der Nebel hoffentlich auflösen.
Peripheral und Central
Bluetooth® Low Energy (BLE) wurde mit Bluetooth 4.0 eingeführt und ist für Anwendungen mit geringem Energieverbrauch und niedrigen Datenraten ausgelegt. Dadurch können batteriebetriebene Geräte, etwa Sensoren oder Bluetooth-Tracker, oft monatelang oder sogar jahrelang mit einer Knopfzelle betrieben werden.
Bei BLE gibt es zwei Rollen: Peripheral und Central. Das Peripheral-Gerät (im Folgenden kurz: das Peripheral) stellt Informationen bereit, während das Central-Gerät (kurz: das Central) diese Informationen sucht, eine Verbindung aufbaut und liest oder verändert. Man kann sich das Peripheral als Server und das Central als Client vorstellen. Typische Peripherals sind Sensoren oder Mikrocontroller, während Smartphones oder PCs häufig die Rolle des Centrals übernehmen.
Services und Characteristics

Die vom Peripheral angebotenen Daten sind in Services organisiert. Jeder Service enthält eine oder mehrere Characteristics, in denen die eigentlichen Daten gespeichert werden. Jede Characteristic und jeder Service besitzen eine eindeutige Kennung (UUID = Universally Unique Identifier). Die Characteristics können unterschiedliche Eigenschaften haben. So können sie beispielsweise nur lesbar (Read), beschreibbar (Write) oder für automatische Benachrichtigungen (Notify oder Indicate) freigegeben sein.
Besonders praktisch ist Notify: Ändert das Peripheral den Wert einer Characteristic, dann kann es das Central darüber informieren. Dieses muss die Daten also nicht wiederholt abfragen. Das vereinfacht den Code und eignet sich hervorragend für Messwerte wie Temperatur, Beschleunigung oder Herzfrequenz.
Damit ein Central ein Peripheral überhaupt finden kann, sendet dieses regelmäßig kurze Advertising-Pakete aus. Darin stehen unter anderem der Gerätename und die angebotenen Services. Erst nach dem Auffinden wird bei Bedarf eine Verbindung aufgebaut und die gewünschten Daten ausgetauscht.
Standardisierte vs. nicht-standardisierte Services und Characteristics
Viele BLE-Geräte haben ähnliche Merkmale, wie etwa einen Akku. Für solche Merkmale hat die Bluetooth-SIG (Special Interest Group) Services und Characteristics standardisiert. Für diese standardisierten Dienste wurden 16-Bit-UUIDs festgelegt, beispielsweise:
- Battery Service
- UUID: 0x180F
- Battery Level Characteristic
- UUID: 0x2A19
Wie ihr eigene Services und Characteristics definiert bzw. organisiert, ist euch überlassen. Die zugehörigen UUIDs legt ihr selbst fest. Da ihr dafür in der Regel 128-Bit-UUIDs vergebt, ist die Gefahr von Kollisionen, also identischer UUIDs, überaus gering. Betrachten wir zwei Taster an einem Mikrocontrollerboard, das als Peripheral dient. Die Tasterzustände (gedrückt / nicht gedrückt) werden per BLE übertragen. Die UUIDs könnten dann wie folgt aussehen:
- Taster-Service
- UUID: 19B10000-E8F2-537E-4F6C-D104768A1214
- Taster 1 Characteristic
- UUID: 19B10001-E8F2-537E-4F6C-D104768A1214
- Taster 2 Characteristic
- UUID: 19B10002-E8F2-537E-4F6C-D104768A1214
Die Position der Bindestriche ist fix, ebenso die Länge der UUIDs, nämlich 32 Hexadezimalziffern.
Advertising und General Advertising Profile (GAP)
Über das General Advertising Profile (GAP) senden BLE-Geräte regelmäßig kleine Werbepakete (Advertising). Diese enthalten unter anderem den Gerätenamen sowie die UUIDs der angebotenen Services. So können andere Geräte das Peripheral finden und entscheiden, ob sie sich verbinden möchten.
Da Advertising-Pakete nur wenig Speicherplatz bieten, kann meist nur eine einzige 128-Bit-Service-UUID übertragen werden. Weitere Services können zwar vorhanden sein, werden aber erst nach dem Verbindungsaufbau erkannt.
Generic Attribute Profile (GATT)
Das Generic Attribute Profile (GATT) definiert, wie Daten in Form von Services und Characteristics organisiert und ausgetauscht werden. Über GATT können Characteristic-Werte gelesen, geschrieben oder per Notify bzw. Indicate übertragen werden.
In den meisten Anwendungen ist das Peripheral der GATT-Server, da es die Services und Characteristics bereitstellt. Das Central ist entsprechend der GATT-Client, der auf diese Daten zugreift. Die Begriffe Server und Client sind jedoch unabhängig von den Rollen Peripheral und Central. In speziellen Anwendungen kann ein Gerät auch beide Rollen gleichzeitig übernehmen.
Ein erster Minimal-Sketch
Der gleich folgende Sketch macht aus dem ESP32 ein Peripheral, das einen Service und eine Characteristic anbietet. Als Central-Gerät, das den Wert der Characteristic lesen und verändern darf, dienen geeignete Programme auf Smartphone oder PC.
Bevor ihr den Sketch hochladet, prüft, ob euer ESP32 Bluetooth unterstützt. Das ist bei den meisten der Fall, aber nicht bei allen. Zu denen, die kein Bluetooth unterstützen, gehört unter anderem der ESP32S2.
#include <BLEDevice.h>
//#include <BLEServer.h> // should be included automatically
//#include <BLEUtils.h> // should be included automatically
#define SERVICE_UUID "12345678-1234-1234-1234-1234567890ab"
#define CHARACTERISTIC_UUID "abcd1234-1234-1234-1234-abcdef123456"
BLECharacteristic *pCharacteristic;
void setup() {
Serial.begin(115200);
BLEDevice::init("ESP32 BLE Minimal");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
pServer->advertiseOnDisconnect(true);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
/* NR = no response, needed for Bluetooth LE Lab */
// | BLECharacteristic::PROPERTY_WRITE_NR
);
pCharacteristic->setValue("Hello Smartphone!");
pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->start();
Serial.println("BLE Server started");
}
void loop() {
String value = pCharacteristic->getValue();
if (value.length() > 0) {
Serial.println(value);
delay(2000);
}
}
Erklärungen
Zunächst müsst ihr BLEDevice.h inkludieren. Diese Bibliotheksdatei sollte alle weiteren, vom Sketch benötigten Dateien einbinden.
Unser Sketch hat nur einen Service und eine Characteristic. Die UUIDs sind frei wählbar.
Mit BLECharacteristic *pCharacteristic; definiert ihr einen Zeiger auf ein Characteristic-Objekt. Das „p“ soll daran erinnern, dass es sich um einen Zeiger (Pointer) handelt. Das Objekt selbst wird erst später erzeugt. Da wir die Characteristic später auch in loop() benötigen, wird sie global definiert.
BLEDevice::init(); initiiert das BLE-Gerät und legt seinen Namen fest. Im Hintergrund passiert dazu eine ganze Menge, wie etwa das Starten der Bluetooth-Hardware des ESP32. BLEDevice ist keine Klasse, die ein Objekt erzeugt, sondern sie enthält eine Reihe von Funktionen. Deswegen greift ihr auf die init-Funktion mit dem Bereichsauflösungoperator „::“ zu (Klasse::Funktion()) und nicht wie eher gewohnt über den Punktoperator (Objekt.Funktion()).
Mit BLEServer *pServer = BLEDevice::createServer(); erschafft ihr einen GATT-Server bzw. den Zeiger darauf. Der Server erhält durch BLEService *pService = pServer->createService(SERVICE_UUID); einen Service mit der zu Beginn definierten UUID. Da pServer ein Zeiger ist, greift ihr mit dem Pfeil- anstelle des Punktoperators auf createService() zu. Mit advertiseOnDisconnect(true) erreicht ihr, dass ihr euch nach einem Disconnect sofort wieder verbinden könnt. Ansonsten müsstet ihr den ESP32 neu starten.
Dem Service wiederum spendiert ihr mit createCharacteristic() eine Characteristic. Dabei übergebt ihr die UUID und die Eigenschaften (properties). In diesem Beispiel darf der Wert der Characteristic sowohl gelesen als auch geschrieben werden.
setValue() akzeptiert entweder einen Arduino-String oder einen Zeiger auf ein Byte-Array (uint8_t) mit Angabe der Datenlänge (setValue(const uint8_t *pData, size_t length)). Dadurch können sowohl Text als auch beliebige Binärdaten übertragen werden.
Schließlich muss der Service noch gestartet werden.
Mit getAdvertising() erhaltet ihr einen Zeiger auf das Advertising Objekt (es gibt nur eins). Erst durch addServiceUUID() wird der Service bereits während des Scannens sichtbar. Ohne diese Angabe kann ein Client den Service dennoch nach dem Verbindungsaufbau über den GATT-Server entdecken.
In loop() wird der Wert der Characteristic regelmäßig mit getValue() abgefragt und ausgegeben.
Aber was kann ich damit nun anfangen?
Um mit eurem gerade erschaffenem Peripheral etwas anzufangen, braucht ihr ein Central. Dafür bieten sich ein Smartphone oder ein PC an. Ich konzentriere mich hier auf das Smartphone. Im Anhang stelle ich aber auch (Windows-)PC Apps für BLE vor.
Für das Smartphone gibt es verschiedene BLE-Apps, von denen ich LightBlue® und nRF Connect auf meinem Android-Smartphone ausprobiert habe. Beide Apps gibt es auch für Apple-Smartphones. Ich nutze für den Beitrag primär LightBlue, da es für meinen Geschmack ein etwas „freundlicheres“ Erscheinungsbild hat. Im Anhang gehe ich hier kurz auf nRF Connect ein.
Öffnet LightBlue, wählt das Peripheral „ESP32 BLE Minimal“ aus und verbindet euch. Dann geht ihr auf die Characteristic. Oben rechts auf dem Bildschirm steht bei auch wahrscheinlich „HEX“. Tippt darauf, wählt „UTF-8 String“ aus und geht auf „Save“. Über „Read lest ihr den Wert der Characteristic, mittels Write könnt ihr ihn ändern.
Schreibt ihr etwa „Hi ESP32!“ als neuen Wert, dann seht ihr, wie sich sowohl die Ausgabe beim nächsten „Read“ als auch die Ausgabe auf dem seriellen Monitor entsprechend ändert:

Callbacks
Characteristic Callbacks
Das erste Beispiel diente der Anschauung, aber es hat nur begrenzten praktischen Wert. Deswegen schalten wir jetzt eine LED am Peripheral über das Central. Eigentlich eine einfache Aufgabenstellung: Wenn die Characteristic einen bestimmten Wert annimmt, dann schalte die LED an, bei einem anderen bestimmten Wert schalte sie aus.
Nun wäre es deutlich bequemer, wenn wir den Wert der Characteristic nicht immer wieder abfragen müssten, sondern wenn ein Schreiben der Characteristic die damit verbundenen Aktionen automatisch auslösen würde. Und genau dafür gibt es Callbacks. Am besten schauen wir uns das an einem Beispiel an:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define CHARACTERISTIC_UUID "11111112-1111-1111-1111-111111111111"
const int ledPin = 17;
// Callback-Class
class MyCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) override{
String value = pCharacteristic->getValue();//.c_str(); //uncomment if necessary
Serial.print("Received: ");
Serial.println(value);
if (value == "ON") {
digitalWrite(ledPin, HIGH);
Serial.println("LED ON");
}
else if (value == "OFF") {
digitalWrite(ledPin, LOW);
Serial.println("LED OFF");
}
}
};
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
BLEDevice::init("ESP32 BLE LED");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService =
pServer->createService(SERVICE_UUID);
pServer->advertiseOnDisconnect(true);
BLECharacteristic *pCharacteristic =
pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharacteristic->setValue("OFF");
pCharacteristic->setCallbacks(new MyCallbacks());
pService->start();
BLEAdvertising *pAdvertising =
BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->start();
Serial.println("BLE Server started");
}
void loop() {
// nothing to do
}
Hängt eine LED an Pin 17. Dann nehmt ihr euer BLE-Central-Gerät zur Hand, verbindet euch mit dem Peripheral „ESP32 BLE LED“ und wählt seine einzige Characteristic aus. Wenn ihr ein „ON“ schreibt, dann leuchtet die LED; schreibt ihr ein „OFF“ dann geht sie wieder aus. Schreibt ihr irgendetwas anderes, wie etwa „Blabla“, dann passiert nichts. Im seriellen Monitor sieht das so aus:

Erklärungen zum Sketch
Wirklich neu ist hier nur der Callback. Für Leser mit begrenzter Erfahrung in C++ wird es jetzt etwas herausfordernd. „BLECharacteristicCallbacks“ ist eine Klasse mit der Mitgliedsfunktion onWrite(), welche im Fall eines Schreib-Events aufgerufen wird. Da die Entwickler der Bibliothek nicht wissen konnten, was genau beim Aufruf von onWrite() passieren soll, haben sie die Funktion als „virtual“ definiert.
Virtuelle Funktionen werden definiert, wenn sie in abgeleiteten Klassen ersetzt werden sollen. Und genau das machen wir hier. Mit class MyCallbacks : public BLECharacteristicCallbacks {.... leiten wir die Klasse MyCallbacks aus der vorgefertigten Klasse BLECharacteristicCallbacks ab und definieren eine eigene onWrite() Funktion. Der Inhalt der Funktion ist veränderbar, nicht aber ihr Parameter. Erwartet wird der Zeiger auf ein BLECharacteristic-Objekt.
Mit setCallbacks(new MyCallbacks()) erzeugen wir ein Objekt aus der Klasse MyCallbacks, das wir pCharacteristics zuordnen.
Und dann seid ihr vielleicht noch über folgende Zeile gestolpert:
pAdvertising->setScanResponse(true);
Man muss dazu wissen, dass ein BLE-Advertising-Paket nur sehr klein ist (maximal 31 Byte bei klassischem Advertising). Reichen diese 31 Byte nicht aus, kann ein zweites Paket verwendet werden – die Scan Response. Mit anderen Worten bewirkt die Zeile: „Falls ein Scanner danach fragt, sende zusätzlich ein Scan-Response-Paket.“ Damit seid ihr auf der sicheren Seite.
Weitere Callback-Klassen und Callback-Funktionen
In dem Beispielsketch haben wir einen Callback für eine Characteristic eingerichtet und haben die onWrite() Funktion definiert. Neben onWrite() gibt es aber auch weitere Funktionen wie onRead(), onNotify() oder onStatus(). Überdies gibt es weitere Callback-Funktionen mit eigenen Funktionen. Hier eine (unvollständige!) Auflistung einiger Callback-Klassen mit ausgewählten Callback-Funktionen:
- BLEServerCallbacks: Client verbindet sich (
onConnect()) oder trennt sich (onDisconnect()) - BLEClientCallbacks: Verbindung des Clients zum Server (
onConnect(),onDisconnect()) - BLECharacteristicCallbacks: Characteristic wird geschrieben oder gelesen (
onRead(),onWrite()). - BLEDescriptorCallbacks: Zugriff auf einen Deskriptor (
onRead(),onWrite()) - BLESecurityCallbacks: z.B. Authentifizierung oder Autorisierung (
onAuthenticationComplete(),onAuthorizationRequest())
Ihr findet die Callback-Klassen- und Funktions-Definitionen in den BLE-Bibliotheksdateien des Arduino ESP32 Board Paketes, also hier. Außerdem gibt es einiges dazu in den Beispielsketchen der BLE-Bibliothek, also unter Datei → Beispiele → Beispiele für euer Board → BLE.
Um das zu veranschaulichen, habe ich die BLECharacteristicCallbacks-Klasse des letzten Beispiels um eine onRead() Funktion erweitert (genauer: ich habe die Funktion neu definiert). Außerdem habe ich eine BLEServerCallbacks-Klasse definiert, die über onConnect() und onDisconnect() darüber informiert, ob ein Client sich verbunden oder getrennt hat:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define CHARACTERISTIC_UUID "11111112-1111-1111-1111-111111111111"
const int ledPin = 17;
// Callback-Class
class MyCharacteristicsCBs : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) override{
String value = pCharacteristic->getValue();//.c_str(); //uncomment if necessary
Serial.print("Received: ");
Serial.println(value);
if (value == "ON") {
digitalWrite(ledPin, HIGH);
Serial.println("LED ON");
}
else if (value == "OFF") {
digitalWrite(ledPin, LOW);
Serial.println("LED OFF");
}
}
void onRead(BLECharacteristic *pCharacteristic) override{
Serial.print("Characteristic ");
Serial.print(pCharacteristic->getUUID().toString());
Serial.print(" was read");
}
};
class MyServerCBs : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) override {
Serial.println("Client is connected");
}
void onDisconnect(BLEServer *pServer) override {
Serial.println("Client is disconnected");
}
};
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
BLEDevice::init("ESP32 BLE LED");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService =
pServer->createService(SERVICE_UUID);
pServer->advertiseOnDisconnect(true);
pServer->setCallbacks(new MyServerCBs());
BLECharacteristic *pCharacteristic =
pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharacteristic->setValue("OFF");
pCharacteristic->setCallbacks(new MyCharacteristicsCBs());
pService->start();
BLEAdvertising *pAdvertising =
BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->start();
Serial.println("BLE Server started");
}
void loop() {
// nothing to do
}
Und ganz nebenbei habe ich noch die Funktion getUUID() eingeführt, die die UUID einer Characteristic zurückgibt. Mit toString() macht ihr daraus einen String, den ihr auf dem seriellen Monitor ausgeben könnt.
Mehrere Characteristic Callbacks
Vielleicht stellt ihr euch die Frage, wie man damit umgeht, wenn man Callbacks für mehrere Characteristics einrichtet, wobei für jede Characteristic unterschiedliche Aktionen ausgeführt werden sollen. Muss ich dann für jede Characteristic eine eigene Callback-Klasse und eigenes Callback-Objekt erstellen? Nein, nicht zwingendermaßen.
In der Zeile void onWrite(BLECharacteristic *pCharacteristic) bekommt die Funktion als Parameter einen Zeiger auf genau die Characteristic, die gerade beschrieben wurde. Wenn ihr die Characteristics pChar1 und pChar2 erstellt habt, dann könnt ihr innerhalb der Funktion prüfen, welche Characteristic gerade beschrieben wurde, also z.B. if(pCharacteristic == pChar1) {.....}.
Damit das funktioniert, weist ihr beiden Characteristics dasselbe Callback-Objekt zu:
class MyCallbacks : public BLECharacteristicCallbacks {....};
....
BLECharacteristic *pChar1;
BLECharacteristic *pChar2;
....
MyCallbacks *callbacks = new MyCallbacks();
....
pChar1->setCallbacks(callbacks);
pChar2->setCallbacks(callbacks);
Wenn die Aufgaben, die mit dem Callback-Aufruf erledigt werden sollen, sehr unterschiedlich sind, dann könnte es übersichtlicher sein, separate Callback-Klassen zu erstellen.
Notify und Deskriptoren
Wozu braucht man das?
In den bisherigen Beispielen ging die Aktion stets vom Central-Gerät aus: Es beschrieb den Wert einer Characteristic, den das Peripheral anschließend auswertete und entsprechend darauf reagierte.
Im nächsten Beispiel kehren wir dieses Prinzip um. Die Aktion findet nun auf der Peripheralseite statt, die Reaktion auf der Centralseite. Dazu schließen wir zwei Taster an den ESP32 an und möchten auf dem Smartphone unmittelbar sehen, wann einer der Taster gedrückt oder wieder losgelassen wird.
Grundsätzlich ließe sich das bereits mit den bisher vorgestellten Techniken realisieren. Für jeden Taster legen wir eine eigene Characteristic an und ändern deren Wert entsprechend des Tasterzustands. Das Central könnte diesen Wert dann regelmäßig auslesen. Dieses ständige Abfragen ist jedoch umständlich und birgt zudem die Gefahr, dass ein kurzer Tastendruck zwischen zwei Lesevorgängen unbemerkt bleibt.
Eleganter ist es, wenn das Central automatisch benachrichtigt wird, sobald sich der Wert einer Characteristic ändert. Genau dafür gibt es die Eigenschaft Notify.
Allein das Setzen der Eigenschaft Notify genügt allerdings noch nicht. Die Characteristic muss zusätzlich für Benachrichtigungen freigeschaltet werden. Dies geschieht mithilfe des Client Characteristic Configuration Descriptors (CCCD). Dieser standardisierte Deskriptor besitzt die UUID 0x2902 und ermöglicht es dem Central, Notifications (oder Indications) für eine Characteristic zu abonnieren.
Wenn wir schon Deskriptoren verwenden, können wir damit gleich noch ein zweites Problem lösen. Da unser Beispiel mit zwei Characteristics arbeitet, müssten wir sie auf der Centralseite anhand ihrer UUIDs unterscheiden – wenig komfortabel. Der Characteristic User Description Deskriptor (UUID 0x2901) erlaubt es dagegen, einer Characteristic einen frei wählbaren Namen zuzuweisen. Dadurch lassen sich die beiden Characteristics wesentlich leichter auseinanderhalten.
Beispielsketch für Notify und Deskriptoren
Als Vorbereitung hängt ihr zwei Taster and geeignete Pins des ESP32. Die anderen Seiten der beiden Taster verbindet ihr mit GND.
Hier zunächst der Sketch:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2901.h>
#include <BLE2902.h>
#define SERVICE_UUID "7a3e0001-9b52-4b24-91c9-123456789abc"
#define BUTTON1_UUID "7a3e0002-9b52-4b24-91c9-123456789abc"
#define BUTTON2_UUID "7a3e0003-9b52-4b24-91c9-123456789abc"
const int button1Pin = 18;
const int button2Pin = 19;
BLECharacteristic *button1Char;
BLECharacteristic *button2Char;
bool lastButton1 = HIGH;
bool lastButton2 = HIGH;
void setup() {
Serial.begin(115200);
pinMode(button1Pin, INPUT_PULLUP);
pinMode(button2Pin, INPUT_PULLUP);
BLEDevice::init("ESP32 Two Button Server"); //new name
BLEServer *pServer = BLEDevice::createServer();
pServer->advertiseOnDisconnect(true);
BLEService *pService = pServer->createService(SERVICE_UUID);
button1Char = pService->createCharacteristic(
BUTTON1_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
button2Char = pService->createCharacteristic(
BUTTON2_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
BLE2901 *desc1 = new BLE2901();
desc1->setDescription("Button 1 state");
button1Char->addDescriptor(desc1);
button1Char->addDescriptor(new BLE2901());
button1Char->addDescriptor(new BLE2902());
button1Char->setValue("Button 1 not pressed");
BLE2901 *desc2 = new BLE2901();
desc2->setDescription("Button 2 state");
button2Char->addDescriptor(desc2);
button2Char->addDescriptor(new BLE2902());
button2Char->setValue("Button 2 not pressed");
pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->start();
Serial.println("BLE Server started");
}
void loop() {
bool button1 = digitalRead(button1Pin);
bool button2 = digitalRead(button2Pin);
if (lastButton1 == HIGH && button1 == LOW) {
button1Char->setValue("Button 1 pressed");
button1Char->notify();
Serial.println("Button 1 pressed");
delay(200);
}
else if(lastButton1 == LOW && button1 == HIGH) {
button1Char->setValue("Button 1 released");
button1Char->notify();
Serial.println("Button 1 released");
delay(200);
}
if (lastButton2 == HIGH && button2 == LOW) {
button2Char->setValue("Button 2 pressed");
button2Char->notify();
Serial.println("Button 2 pressed");
delay(200);
}
else if (lastButton2 == LOW && button2 == HIGH) {
button2Char->setValue("Button 2 released");
button2Char->notify();
Serial.println("Button 2 released");
delay(200);
}
lastButton1 = button1;
lastButton2 = button2;
}
Ausgabe
Wie zuvor geht ihr in eure Lieblings-BLE-App, wählt dort das BLE-Peripheral (hier: ESP32 Two Button Server) und dann eine der beiden Characteristics aus, nämlich „Button 1 state“ oder „Button 2 state“.
Dann müsst ihr die Notification noch abonnieren. Dazu gibt es einen „Subscribe“-Button.
Drückt ihr jetzt einen der beiden Buttons, bekommt ihr die entsprechende Nachricht in eurer App.
Wie das in LightBlue aussieht, seht ihr auf dem Bild rechts.
Erklärungen
Um die beiden Deskriptoren verwenden zu können, müsst ihr die zugehörigen Bibliotheksdateien BLE2901.h und BLE2902.h einbinden.
Wir erstellen dieses Mal zwei Characteristics. Ich denke, die Vorgehensweise ist selbsterklärend. Die Eigenschaft Notify wird mit BLECharacteristic::PROPERTY_NOTIFY festgelegt.
Als Nächstes kümmern wir uns um den Descriptor „Characteristic User Description“ für die Characteristic des ersten Buttons. Zunächst erzeugen wir mit BLE2901 *desc1 = new BLE2901(); ein neues BLE2901-Objekt und speichern dessen Adresse in dem Zeiger desc1.
Anschließend weisen wir dem Descriptor mit desc1->setDescription("Button 1 state"); den gewünschten Namen zu.
Der Deskriptor wird mit button1Char->addDescriptor(desc1); der Characteristic zugeordnet. Dasselbe machen wir weiter unten für den zweiten Button.
Die Zuordnung des CCCD ist einfacher: button1Char->addDescriptor(new BLE2902());.
In loop() fragen wir den Zustand der Taster permanent ab. Ändert sich ein Zustand, passen wir den Wert der Characteristic mit buttonxChar->setValue("Button x ....."); an.
buttonxChar->notify();.Noch ein paar Anmerkungen zum Sketch
Der Sketch dient nur der Anschauung und ist nicht optimiert. Damit Tasterdrücke in kurzer Folge nicht verloren gehen, würde ich mit Interrupts arbeiten. Außerdem könnten wir hier auch mit einer Characteristic auskommen, die Button 1 und 2 abdeckt. Ich wollte aber einmal zeigen, wie man zwei Characteristics einrichtet.
Zwei ESP32 über BLE verbinden
Aus didaktischen Gründen war die Clientseite bisher das Smartphone oder der PC bzw. Laptop. Nun gehen wir den nächsten Schritt, indem wir zwei ESP32 über BLE kommunizieren lassen. Die Eingabe einer Nachricht im seriellen Monitor des Server-ESP32 (Peripheral) soll auf dem seriellen Monitor des Client-ESP32 ausgegeben werden und umgekehrt.
Der Server (Peripheral)
Auf der Server-Seite kommt hinsichtlich BLE nichts wirklich Neues hinzu. Allerdings vereinigen wir einige Elemente aus bisherigen Beispielen in einem Sketch. Wenn die Nachricht an den Client geschickt wird (genauer gesagt ändern wir den Wert der Characteristic), verwenden wir Notify, damit der Client den Wert nicht ständig in loop() abfragen muss. Um auch auf der Clientseite ohne ständige „manuelle“ Abfrage über das Eintreffen einer Nachricht (= Write-Event) informiert zu werden, nutzen wir die Characteristic-Callback-Funktion onWrite().
Außerdem nutzen wir die Server-Callback-Funktionen onConnect() und onDisconnect(), um uns über den Verbindungsstatus auf dem Laufenden zu halten.
Für die Nachrichten, die wir senden und die wir erhalten, nutzen wir zwei Characteristics. RX_UUID ist die UUID für den Empfang (R = Receive), TX_UUID ist die UUID für den Versand (T = Transmit).
Hier also der Server-Sketch:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define RX_UUID "22222222-2222-2222-2222-222222222222" // Client -> Server
#define TX_UUID "33333333-3333-3333-3333-333333333333" // Server -> Client
BLECharacteristic *txCharacteristic;
bool deviceConnected = false;
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) {
deviceConnected = true;
Serial.println("Client connected");
}
void onDisconnect(BLEServer *pServer) {
deviceConnected = false;
Serial.println("Client diconnected");
pServer->startAdvertising();
}
};
class RxCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
String value = pCharacteristic->getValue();
if (value.length() > 0) {
Serial.print("Received: ");
Serial.println(value);
}
}
};
void setup() {
Serial.begin(115200);
BLEDevice::init("ESP32 BLE Server");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
BLECharacteristic *rxCharacteristic = pService->createCharacteristic(
RX_UUID,
BLECharacteristic::PROPERTY_WRITE
);
rxCharacteristic->setCallbacks(new RxCallbacks());
txCharacteristic = pService->createCharacteristic(
TX_UUID,
BLECharacteristic::PROPERTY_NOTIFY
);
txCharacteristic->addDescriptor(new BLE2902());
pService->start();
BLEAdvertising *advertising = BLEDevice::getAdvertising();
advertising->addServiceUUID(SERVICE_UUID);
advertising->setScanResponse(true);
advertising->start();
Serial.println("Server started");
}
void loop() {
if (deviceConnected && Serial.available()) {
String text = Serial.readStringUntil('\n');
if (text.length() > 0) {
txCharacteristic->setValue(text);
txCharacteristic->notify();
Serial.print("Sent: ");
Serial.println(text);
}
}
}
Wenn eine Eingabe über den seriellen Monitor erfolgt und der Server mit dem Client verbunden ist (if (deviceConnected && Serial.available())), dann wird die Eingabe als neuer Wert gesetzt und der Client mittels notify() informiert.
Der Client (Central)
Die Clientseite ist neu und beinhaltet eine ganze Reihe neuer Funktionen. Bisher hat die Software (wie LightBlue) das Scannen der verfügbaren BLE-Geräte übernommen. Wir haben dann das Gerät aus der Liste ausgewählt und die Verbindung hergestellt. All das müssen wir dem Central-ESP32 erst beibringen. Da das ziemlich viele Dinge auf einmal sind, habe ich einen Zwischenschritt eingefügt, nämlich einen Central-Sketch, der erst einmal nur scannt und nach dem gewünschten Service sucht.
Zwischenschritt: Der Client scannt
Hier also der Scanner-Sketch:
#include <BLEDevice.h>
#include <BLEUtils.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
void scanForDevices() {
Serial.println("Searching server...");
BLEScan *pScan = BLEDevice::getScan();
pScan->setActiveScan(true);
BLEScanResults *pResults = pScan->start(5);
for (int i = 0; i < pResults->getCount(); i++) {
BLEAdvertisedDevice device = pResults->getDevice(i);
Serial.println(device.toString());
if (device.haveServiceUUID() &&
device.isAdvertisingService(BLEUUID(SERVICE_UUID))) {
Serial.println("Matching server found!");
}
Serial.println();
}
}
void setup() {
Serial.begin(115200);
BLEDevice::init("ESP32 BLE Client");
scanForDevices();
}
void loop() {
}
Ladet diesen und den Server-Sketch auf zwei ESP32-Boards. Auf der Clientseite solltet ihr eine Ausgabe wie diese erhalten:

Mit BLEScan *pScan = BLEDevice::getScan(); erzeugt ihr einen Zeiger auf das Scan-Objekt. pScan->setActiveScan(true); bewirkt, dass der Scan aktiv ist, d. h., der Scanner hört nicht nur auf das, was ihm die Advertising-Pakete der BLE-Geräte anbieten, sondern sendet auch einen Scan-Request an interessante Geräte. Die Geräte antworten darauf mit einem Scan-Response-Paket, das ggf. weitere Daten enthält. Übergebt ihr stattdessen false, wird kein Scan-Request gesendet. Mit BLEScanResults *pResults = scan->start(5); startet ihr einen fünfsekündigen Scan und erzeugt einen Zeiger zum Ergebnis.
pResults->getCount(); liefert euch die Anzahl der gefundenen BLE-Geräte. Mit BLEAdvertisedDevice device = pResults->getDevice(i); „extrahiert“ ihr das jeweilige Gerät mit der laufenden Nummer. Mit device.toString() erhaltet ihr die Geräteinformationen als String, den ihr mit Serial.print() ausgeben könnt. device.haveServiceUUID() && device.isAdvertisingService(BLEUUID(SERVICE_UUID)) prüft, ob das Gerät einen Service hat und ob der Service die gesuchte UUID SERVICE_UUID hat. Die Funktion isAdvertisingService() erwartet als Parameter ein BLEUUID-Objekt, das ihr aus der UUID erzeugt.Der komplette Client-Sketch
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEClient.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define RX_UUID "22222222-2222-2222-2222-222222222222" // Client -> Server
#define TX_UUID "33333333-3333-3333-3333-333333333333" // Server -> Client
BLERemoteCharacteristic *rxCharacteristic;
BLERemoteCharacteristic *txCharacteristic;
bool connected = false;
void notifyCallback(
BLERemoteCharacteristic *characteristic,
uint8_t *data,
size_t length,
bool isNotify
) {
Serial.print("Received: ");
for (size_t i = 0; i < length; i++) {
Serial.print((char)data[i]);
}
Serial.println();
}
class ClientCallbacks : public BLEClientCallbacks {
void onConnect(BLEClient *client) {
Serial.println("Connected to server");
}
void onDisconnect(BLEClient *client) {
connected = false;
Serial.println("Disconnected from server");
}
};
bool connectToServer() {
Serial.println("Serching server...");
BLEScan *pScan = BLEDevice::getScan();
pScan->setActiveScan(true);
BLEScanResults *pResults = pScan->start(5);
BLEAdvertisedDevice *pServerDevice = nullptr;
for (int i = 0; i < pResults->getCount(); i++) {
BLEAdvertisedDevice device = pResults->getDevice(i);
if (device.haveServiceUUID() &&
device.isAdvertisingService(BLEUUID(SERVICE_UUID))) {
pServerDevice = new BLEAdvertisedDevice(device);
break;
}
}
if (pServerDevice == nullptr) {
Serial.println("Server not found");
return false;
}
BLEClient *pClient = BLEDevice::createClient();
pClient->setClientCallbacks(new ClientCallbacks());
if (!pClient->connect(pServerDevice)) {
Serial.println("Connection failed");
return false;
}
BLERemoteService *pService =
pClient->getService(BLEUUID(SERVICE_UUID));
if (pService == nullptr) {
Serial.println("Service not found");
pClient->disconnect();
return false;
}
rxCharacteristic =
pService->getCharacteristic(BLEUUID(RX_UUID));
txCharacteristic =
pService->getCharacteristic(BLEUUID(TX_UUID));
if (rxCharacteristic == nullptr || txCharacteristic == nullptr) {
Serial.println("Characteristic not found");
pClient->disconnect();
return false;
}
txCharacteristic->registerForNotify(notifyCallback);
connected = true;
Serial.println("Ready. Type in some text and press enter.");
return true;
}
void setup() {
Serial.begin(115200);
BLEDevice::init("ESP32 BLE Client");
connectToServer();
}
void loop() {
if (!connected) {
static unsigned long lastTry = 0;
if (millis() - lastTry > 5000) {
lastTry = millis();
connectToServer();
}
return;
}
if (Serial.available()) {
String text = Serial.readStringUntil('\n');
if (text.length() > 0) {
rxCharacteristic->writeValue(text, text.length());
Serial.print("Sent: ");
Serial.println(text);
}
}
}
Als Erstes fallen euch im Client-Sketch vielleicht die beiden BLERemoteCharacteristics auf. Auf der Serverseite werden die Services und Characteristics tatsächlich angelegt. Sie werden deshalb durch Objekte vom Typ BLEService und BLECharacteristic repräsentiert. Der Client erzeugt diese Objekte nicht noch einmal. Nach dem Verbindungsaufbau sucht er die angebotenen Services und Characteristics anhand ihrer UUIDs. Für den Zugriff auf diese entfernten Objekte verwendet die Bibliothek die Klassen BLERemoteService und BLERemoteCharacteristic. Eine BLERemoteCharacteristic ist somit die lokale Repräsentation einer Characteristic, die sich auf dem BLE-Server befindet.
Dann möchte ich das Augenmerk auf die Funktion connectToServer() richten. Die ersten Zeilen habe ich im obigen Zwischenschritt erklärt. BLEClient *pClient = BLEDevice::createClient(); erzeugt einen Zeiger zu eurem Client-Objekt. Dann wird dem Client-Objekt mit pClient->setClientCallbacks(new ClientCallbacks()) ein Callback der Klasse ClientCallbacks zugeordnet. Die Callback-Funktionen onConnect() und onDisconnect() erlauben es uns, über Änderungen des Verbindungsstatus informiert zu werden und später in loop() automatisch zu reconnecten (eigentlich schrecklich, dieses denglisch).
pClient->connect(pServerDevice); versucht, den Client mit dem Server zu verbinden, und meldet zurück, ob es geklappt hat.
Mit rxCharacteristic = pService->getCharacteristic(BLEUUID(RX_UUID)); erhält rxCharacteristic seine Adresse. Entsprechend verfahren wir mit der txCharacteristic. Sollte das scheitern, dann ist der Zeiger noch ein Nullzeiger (nullptr). Das können wir prüfen und trennen ggf. den Client vom Server.
So, wie wir zuvor auf dem Smartphone Notifications abonniert haben, müssen wir hier im Prinzip dasselbe tun, und zwar mit txCharacteristic->registerForNotify(notifyCallback);.
Der Rest des Sketches ist hoffentlich verständlich. Nun könnt ihr euch über die seriellen Monitore des Server- und des Client-ESP32 Nachrichten zusenden.
Weitere Anwendungsbeispiele
Akkuladezustand von BLE-Geräten prüfen
Ich komme zurück auf standardisierte Services und Characteristics. Eine vollständige Liste gibt es hier auf den offiziellen Seiten der Bluetooth Special Interest Group. Welche standardisierten Services und Characteristics in euren BLE-Peripherals implementiert sind, könnt ihr mit Apps wie LightBlue einfach herausbekommen.
Mein erstes Beispiel zeigt, wie ihr den Akku-Ladezustand eines BLE-Gerätes über den ESP32 auslesen könnt – sofern das Gerät den Battery Service mit der UUID 0x180F anbietet und über die Battery Level Characteristic mit der UUID 0x2A19 verfügt.
Hier der Sketch:
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEClient.h>
#define BATTERY_SERVICE_UUID "180F"
#define BATTERY_LEVEL_UUID "2A19"
// MAC-Address from LightBlue or nRF Connect:
BLEAddress headphoneAddress("80:C3:BA:96:56:DC");
BLERemoteCharacteristic *batteryCharacteristic;
bool connected = false;
bool connectToHeadphones() {
Serial.println("Connect to headphone...");
BLEClient *pClient = BLEDevice::createClient();
if (!pClient->connect(headphoneAddress)) {
Serial.println("Connection failed");
return false;
}
Serial.println("Connected");
BLERemoteService *batteryService =
pClient->getService(BLEUUID(BATTERY_SERVICE_UUID));
if (batteryService == nullptr) {
Serial.println("Battery service not found");
pClient->disconnect();
return false;
}
batteryCharacteristic =
batteryService->getCharacteristic(BLEUUID(BATTERY_LEVEL_UUID));
if (batteryCharacteristic == nullptr) {
Serial.println("Battery level characteristic not found");
pClient->disconnect();
return false;
}
connected = true;
Serial.println("Battery characteristic found");
return true;
}
void readBatteryLevel() {
if (!batteryCharacteristic->canRead()) {
Serial.println("Cannot read battery level");
return;
}
String value = batteryCharacteristic->readValue();
if (value.length() > 0) {
uint8_t batteryLevel = value[0];
Serial.print("Battery level: ");
Serial.print(batteryLevel);
Serial.println(" %");
}
else {
Serial.println("No value received");
}
}
void setup() {
Serial.begin(115200);
delay(1000);
BLEDevice::init("ESP32 BLE Battery Client");
if (connectToHeadphones()) {
readBatteryLevel();
}
}
void loop() {
static unsigned long lastRead = 0;
if (connected && millis() - lastRead > 10000) {
lastRead = millis();
readBatteryLevel();
}
}
Als BLE-Gerät habe ich meine Bluetooth-Kopfhörer ausgewählt. Die zugehörige MAC-Adresse habe ich mit LightBlue ermittelt. Dort habe ich auch gesehen, dass das Gerät den Battery Service anbietet.
Neu ist in diesem Sketch, dass der Funktion connect() kein Zeiger zu einem BLEAdvertisedDevice-Objekt, sondern eine MAC-Adresse übergeben wird. Beides funktioniert. Überdies nutzen wir hier die Funktion canRead(), um zu prüfen, ob wir die Characteristic lesen können.
Der Rest des Sketches sollte euch bekannt vorkommen. Hier noch die Ausgabe:

Nun kommt die schlechte Nachricht zu den standardisierten Services: Die Bluetooth SIG definiert zwar zahlreiche standardisierte Services, in der Praxis implementieren viele BLE-Geräte jedoch zusätzlich eine Vielzahl herstellerspezifischer Services und Characteristics. So werden bei Kopfhörern beispielsweise Funktionen wie Active Noise Cancelling, Equalizer oder Firmware-Updates meist über proprietäre BLE-Services realisiert. Der standardisierte Battery-Service bildet hier eher die Ausnahme als die Regel.
Das Smartphone über BLE/HID steuern – Kamerafernauslöser
Wenn man doch per BLE mit dem Smartphone interagieren kann, ist es dann nicht auch möglich, bestimmte Funktionen des Smartphones per ESP32 zu steuern? Ja, es geht, aber nicht so einfach. Eine Möglichkeit wäre, eine eigene Smartphone-App zu schreiben. Das dürfte aber die Fähigkeit der meisten übersteigen. Alternativ lässt sich aus dem ESP32 ein HID-Eingabegerät machen (HID = Human Interface Device). Das ist zwar einfacher, als eine Smartphone-App zu schreiben, aber immer noch so komplex, dass ich damit einen ganzen Blogbeitrag füllen könnte. Da ich mich damit auch nicht im Detail auskenne, habe ich mir Hilfe bei ChatGPT geholt. Die Aufgabe lautete, einen Sketch zu schreiben, mit dem ich die Kamera meines Android-Smartphones per Taster am ESP32 auslösen kann. Das hat auf Anhieb geklappt.
Hier der Sketch:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEHIDDevice.h>
#include <BLEUtils.h>
const int buttonPin = 17;
bool lastState = HIGH;
bool connected = false;
BLEHIDDevice *hid;
BLECharacteristic *inputReport;
// HID Report Map für Consumer Control: Volume Up / Volume Down
const uint8_t reportMap[] = {
0x05, 0x0C, // Usage Page: Consumer
0x09, 0x01, // Usage: Consumer Control
0xA1, 0x01, // Collection: Application
0x85, 0x01, // Report ID: 1
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x75, 0x01, // Report Size: 1 Bit
0x95, 0x02, // Report Count: 2 Bits
0x09, 0xE9, // Usage: Volume Up
0x09, 0xEA, // Usage: Volume Down
0x81, 0x02, // Input: Data, Variable, Absolute
0x75, 0x01, // Report Size: 1 Bit
0x95, 0x0E, // Report Count: 14 Bits Padding
0x81, 0x03, // Input: Constant
0xC0 // End Collection
};
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *server) {
connected = true;
Serial.println("Smartphone connected");
}
void onDisconnect(BLEServer *server) {
connected = false;
Serial.println("Smartphone disconnected");
BLEDevice::startAdvertising();
}
};
void sendVolumeDown() {
// Bit 0 = Volume Up, Bit 1 = Volume Down
uint8_t press[] = { 0x02, 0x00 };
uint8_t release[] = { 0x00, 0x00 };
inputReport->setValue(press, sizeof(press));
inputReport->notify();
delay(80);
inputReport->setValue(release, sizeof(release));
inputReport->notify();
Serial.println("Sent volume down");
}
void setup() {
Serial.begin(115200);
pinMode(buttonPin, INPUT_PULLUP);
BLEDevice::init("ESP32 Camera Button");
BLEServer *server = BLEDevice::createServer();
server->setCallbacks(new ServerCallbacks());
hid = new BLEHIDDevice(server);
inputReport = hid->inputReport(1);
hid->manufacturer()->setValue("My ESP32");
hid->pnp(0x02, 0x1234, 0x5678, 0x0100);
hid->hidInfo(0x00, 0x01);
hid->reportMap((uint8_t *)reportMap, sizeof(reportMap));
hid->startServices();
BLEAdvertising *advertising = BLEDevice::getAdvertising();
advertising->setAppearance(0x03C1);
advertising->addServiceUUID(hid->hidService()->getUUID());
advertising->setScanResponse(true);
advertising->start();
Serial.println("BLE HID started");
Serial.println("Pair with your phone and open camera app.");
}
void loop() {
bool state = digitalRead(buttonPin);
if (lastState == HIGH && state == LOW) {
if (connected) {
sendVolumeDown();
}
delay(250); // debouncing
}
lastState = state;
}
Dann habe ich ChatGPT noch um eine „High-Level“ Erklärung gebeten, mit folgendem Ergebnis:
Der Sketch lässt den ESP32 gegenüber einem Smartphone wie ein Bluetooth-Eingabegerät erscheinen. Dazu wird das standardisierte Bluetooth-HID-Profil verwendet, das beispielsweise auch von Tastaturen, Mäusen und Fernbedienungen genutzt wird. Im sogenannten HID Report Map wird festgelegt, welche Eingaben der ESP32 senden kann; in diesem Fall sind das „Lautstärke erhöhen“ und „Lautstärke verringern“. Obwohl beide Befehle definiert sind, verwendet der Sketch nur „Lautstärke verringern“, da viele Kamera-Apps diesen Tastendruck als Auslöser interpretieren.
Beim Start erzeugt der ESP32 einen BLE-Server und richtet darauf den HID-Service ein. Anschließend beginnt er mit dem Advertising, sodass das Smartphone das Gerät unter dem Namen „ESP32 Camera Button“ finden und koppeln kann. Über Callback-Funktionen wird registriert, ob ein Smartphone verbunden ist; nach einem Verbindungsabbruch wird das Advertising automatisch erneut gestartet. In der Hauptschleife überwacht der ESP32 den Taster an GPIO 17 und erkennt den Übergang vom nicht gedrückten zum gedrückten Zustand. Ist ein Smartphone verbunden, sendet die Funktion sendVolumeDown() zunächst den Tastendruck und kurz danach das Loslassen der Taste. Für das Smartphone verhält sich der ESP32 damit wie eine Bluetooth-Fernbedienung, deren Lautstärketaste als Kameraauslöser genutzt wird.
BLE-„HC-05-Module“
In einem meiner ersten Beiträge habe ich mich mit den HC-05- und HC-06-Modulen beschäftigt (Link zum Beitrag). Diese ermöglichen eine einfache Kommunikation über Bluetooth Classic von MCU-Board zu MCU-Board oder von MCU-Board zum Smartphone. In den letzten Jahren sind vermehrt BLE-Varianten dieser Module aufgetaucht. Sie werden als „normale“ HC-05- oder HC-6-Module angeboten, sodass man erst bei Eintreffen der Bestellung merkt, was man da gekauft hat. Das AT-Kommando AT+VERSION? liefert hc05V2.3_le oder ähnlich. Untereinander verbinden sich die Module nicht. Einstellungen sind nur begrenzt möglich, da man die Module nur über die AT-Kommandos steuern kann.
Ich denke diese Module sind Fake, da es auf den Seiten des Herstellers (nur Chinesisch, Browser muss übersetzen) keine HC-05 oder HC-06-Module gibt, die BLE beherrschen.
Also, was kann man damit anfangen? Immerhin kann man z.B. über LightBlue & Co. Nachrichten zwischen Smartphone und Modulhin- und herschicken, was bei den meisten ESP32-Boards allerdings wenig Sinn macht, da das auch ohne Modul geht. Wenn ihr es trotzdem probieren wollt, dann schließt das Modul z.B. an Serial1 an (GND-GND, VCC-3.3V, RX-TX1, TX-RX1). Koppelt das Modul mit dem Smartphone und wählt es dann in LightBlue oder einer anderen BLE-App aus. Dort findet ihr eine Write/Notify-Characteristic, über die ihr kommunizieren könnt.
Hier ein Sketch dazu:
#define RX1 16
#define TX1 17
void setup() {
Serial.begin(38400);
Serial1.begin(38400, SERIAL_8N1, RX1, TX1);
if(Serial1){
Serial.println("Serial1 successfully set up");
}
}
void loop() { // run over and over
if (Serial1.available()) {
Serial.write(Serial1.read());
}
if (Serial.available()) {
Serial1.write(Serial.read());
}
}
Wenn ihr tiefer einsteigen wollt
Dieser Beitrag ist umfangreich, kratzt aber trotzdem nur an der Oberfläche. Wenn ihr tiefer einsteigen wollt, dann empfehle ich euch, die Beispiele der BLE-Bibliothek durchzugehen.
Außerdem lohnt es sich, einmal in die Headerdateien der BLE-Bibliothek zu schauen, also hier. Wenn ihr beispielsweise wissen wollt, welche Funktionen ein BLEAdvertisedDevice-Objekt hat, dann schaut in die Datei BLEAdvertisedDevice.h.
Und falls ihr bei euren BLE-Projekten mal feststeckt und das Problem nicht selbst gelöst bekommt, empfehle ich ChatGPT. Mit diesem Thema kennt sich die KI gut aus (was nicht bei allen Themen der Fall ist!).
Unterstützung
Hat euch der Beitrag gefallen? Und ist euch aufgefallen, dass ihr hier keine nervigen Werbeanzeigen seht? Damit das so bleibt, könnt ihr mich unterstützen. Wie, das seht ihr hier.
Anhang
Smartphone-BLE-App nRF Connect
Eine Alternative zu LightBlue ist die App nRF Connect. Ihr findet die verfügbaren Bluetooth-Geräte unter „Scanner“. Tippt auf „Connect“ und geht dann auf den Tab eures Geräts. Dort tippt ihr auf die Characteristic eurer Wahl. Der nach unten weisende Pfeil lässt euch die Characteristic lesen, der nach oben weisende Pfeil öffnet einen Schreib-Dialog. Wählt den Datentyp, gebt den Wert bzw. Text ein und tippt auf „Send“. So sah das bei mir mit dem Sketch minimal_example.ino aus:
nRF Connect gibt es auch als PC-Programm. Bei der Installation wollte es ein weiteres Hilfsprogramm installieren. Irgendwie erschien mir das alles zu groß für meine Zwecke und ich habe es dann bleiben gelassen.
Weitere Windows-BLE-Apps
Bluetooth LE Explorer
Auch für PC und Laptop gibt es verschiedene BLE-Programme. Für Windows-Systeme kann ich „Bluetooth LE Explorer“ aus dem Microsoft Store empfehlen. Für MAC- und Linux-Systeme gibt es sicherlich auch einiges (z.B. LightBlue für MAC), aber da ich nun einmal einen Windows-PC habe, konnte ich das nicht ausprobieren.
Bei Bluetooth LE Explorer geht ihr zunächst auf „Discover und Pair“ und wählt dann „Start“:

Euer BLE-Gerät sollte dann gelistet werden (hier: ESP32 BLE Minimal). Klickt auf das Gerät, um zur folgenden Übersicht zu gelangen:

Wenn ihr das Beispiel „ESP32 BLE Minimal“ verwendet, könnt ihr nun auf die Characteristic klicken und den Wert lesen oder einen neuen Wert schreiben. Stellt dabei das richtige Datenformat ein.

Bei PC/Laptop ist zu beachten, dass dieser sich u. a. den Namen des Peripherals merkt. Wenn ihr mit eurem ESP32 einen neuen Peripheral mit anderem Namen erzeugt, dann seht ihr wahrscheinlich trotzdem noch den alten Namen. Offenbar erkennt der PC/Laptop das Gerät anhand seiner MAC-Adresse wieder und liest nicht alle Informationen neu, sondern aus einem Cache. Als Abhilfe geht ihr in die Windows-Einstellungen → „Bluetooth und Geräte“ und entfernt dort das Gerät. Wenn das nicht hilft, dann schaltet das Bluetooth am Rechner zusätzlich kurz aus und wieder ein.
Bluetooth LE Lab
Zudem habe ich „Bluetooth LE Explorer“ aus dem Microsoft Store ausprobiert. Es ist aus meiner Sicht nicht zu empfehlen. Bei diesem Programm muss man das Bluetooth Gerät zunächst koppeln. Das macht ihr über die Windows-Einstellungen → Bluetooth und Geräte → Geräte hinzufügen → Bluetooth → Alle Geräte. Dann startet ihr „Bluetooth LE Lab“ und wählt das Gerät aus.
Außerdem wichtig: Ihr müsst der Characteristic die Eigenschaft „PROPERTY_WRITE_NR“ (Schreiben ohne Empfangsbestätigung) verleihen und in „Bluetooth LE Lab“ beim Schreiben die entsprechende Checkbox aktivieren (s.u.). Habt ihr das beim ersten Versuch versäumt, dann reicht es nicht, den Sketch noch einmal modifiziert hochzuladen. Zusätzlich müsst ihr das Gerät entkoppeln, Bluetooth einmal kurz aus- und wieder anschalten und neu koppeln.









