Über den Beitrag
Nachdem ich im letzten Beitrag über Bluetooth Low Energy (BLE) auf dem ESP32 berichtet habe, möchte ich hier nun erklären, wie ihr BLE mithilfe der Bibliothek ArduinoBLE auf Arduino-Boards nutzt. Natürlich gibt es viele Ähnlichkeiten bei der Nutzung von BLE auf ESP32- und Arduino-Boards, aber die Bibliotheken unterscheiden sich doch so sehr, dass ich eine Trennung in zwei separate Beiträge für sinnvoll hielt. Die Struktur dieses Beitrages ist jedoch im Grunde dieselbe.
Die Grundbegriffe von BLE wie etwa „Service“, „Characteristic“, „Peripheral“ und „Central“ werde ich hier nicht noch einmal erläutern. Bei Bedarf schaut hier.
Was euch erwartet:
- ArduinoBLE Minimal-Sketch
- Event-Handler (Callbacks)
- Notify und Deskriptoren
- Zwei Arduinos über BLE verbinden
- Anhang – Strukturen versenden
Vorbereitung
Um die Sketche dieses Beitrages nachvollziehen zu können, braucht ihr zunächst einmal zwei BLE-fähige Boards, die von der ArduinoBLE-Bibliothek unterstützt werden. Das sind, Stand heute und meines Wissens, die folgenden Arduino-Boards:
- UNO WiFi Rev2, UNO R4 WiFi
- MKR WiFi 1010
- Nano 33 IoT, Nano 33 BLE (einschl. Sense und Sense Rev2), Nano RP2040
- Nicla Sense ME, Nicla Voice, Nicla Vision
- Opta
- Portenta H7, Portenta C33
Die Kommunikation klappt auch zwischen unterschiedlichen Boards. Ihr braucht also nicht zwei gleiche Modelle. Aber natürlich habe ich nicht alle Modelle und Kombinationen selbst gestestet.
Bei einigen Boards müsst ihr evtl. die Firmware NINA-W102 aktualisieren. Ihr erkennt das Problem an entsprechenden Fehlermeldungen beim Kompilieren. Das Update ist aber keine große Sache: Wählt das richtige Board und den richtigen Port in der Arduino IDE aus. Dann geht auf Werkzeuge → Firmware-Updater → Board wählen → Nach Updates suchen → Version wählen (>3.0.0) → Installieren.
Dann benötigt ihr die ArduinoBLE Bibliothek, die ihr ganz einfach über die Bibliotheksverwaltung der Arduino IDE installieren könnt.
Und schließlich braucht ihr noch ein Smartphone mit geeigneter BLE-App wie etwa LightBlue oder nRF Connect (für Android / für Apple). Alternativ könnt ihr auch einen PC/Laptop mit Bluetooth-Adapter verwenden. Auch da braucht ihr geeignete Apps wie etwa Bluetooth LE Explorer. Auf einige Smartphone- und PC-Apps bin ich im letzten Beitrag näher eingegangen.
ArduinoBLE Minimal-Sketch
Wir beginnen ganz einfach und machen aus unserem Arduino ein Peripheral, dem wir einen Service und eine Characteristic verleihen. Die Characteristic soll vom Client beschreibbar sein. Als Client nutzen wir eine geeignete App auf PC oder Smartphone. Hier der Sketch:
#include <ArduinoBLE.h>
BLEService myService("19B10000-E8F2-537E-4F6C-D104768A1214");
BLEStringCharacteristic myCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite, 40);
void setup() {
Serial.begin(115200);
while (!Serial);
if (!BLE.begin()) {
Serial.println("Starting BLE module failed!");
while (1);
}
BLE.setLocalName("BLE Minimal"); // "Advertising Name"
// BLE.setDeviceName("BLE Minimal"); // Device Name
BLE.setAdvertisedService(myService);
myService.addCharacteristic(myCharacteristic);
BLE.addService(myService);
myCharacteristic.writeValue("Hello Smartphone!");
BLE.advertise();
Serial.println("BLE Minimal Sketch");
}
void loop() {
BLEDevice central = BLE.central();
if(central) {
Serial.print("Connected to central: ");
Serial.println(central.address());
while (central.connected()) {
if (myCharacteristic.written()) {
if (myCharacteristic.value()) {
Serial.println(myCharacteristic.value());
}
}
}
Serial.println("Disconnected from central.");
}
}
Ladet den Sketch hoch, geht in eure BLE-App wie etwa LightBlue und sucht dort nach dem Peripheral „BLE Minimal“. Verbindet euch und geht dann auf die zugehörige Characteristic. Tippt oder klickt ihr auf „Read“, dann solltet ihr den Wert der Characteristic sehen, nämlich „Hello Smartphone“. Falls ihr nur Zahlen seht, dann müsst ihr das Format auf UTF-8 ändern. Bei nRF Connect müsst ihr vor der Characteristic noch auf den Service tippen, und der Pfeil nach unten entspricht „Read“.
Dann geht auf „Write New Value“ (nRF Connect: Pfeil nach oben) und schreibt einen neuen Wert, z.B. „Hello Arduino“. Gegebenenfalls müsst ihr auch hier das Format auf UTF-8 ändern. Im seriellen Monitor seht ihr den neuen Wert sofort, auf dem Client (Central) seht ihr den Wert beim nächsten Read.
So sah die Ausgabe bei mir auf dem Smartphone mit LightBlue aus (vor Änderung der Characteristic):
Und das war die Ausgabe auf dem seriellen Monitor nach Beschreiben der Characteristic und Abmeldung des Centrals vom Peripheral:

Erläuterungen zum Sketch
Nach dem Einbinden der ArduinoBLE-Bibliothek erzeugt ihr mit BLEService myService(uuid); das Objekt „myService“ und übergebt die (frei wählbare) UUID. BLEStringCharacteristic myCharacteristic(uuid, permissions); erzeugt das Characteristic-Objekt „myCharacteristic“. Auch seine UUID ist frei wählbar (sofern nicht schon vergeben).
Ein Objekt der Klasse BLEStringCharacteristic erwartet als Wert einen String. Für alle wichtigen Datentypen gibt es eine eigene Klasse, wie etwa BLEFloatCharacteristic oder BLEBooleanCharacteristic. Ihr findet die Klassen (außer der String-Klasse) in der ArduinoBLE-Datei BLETypedCharacteristics.h.
Neben der UUID übergebt ihr eurem Characteristics-Objekt seine Eigenschaften (Berechtigungen). Die wichtigsten sind:
- BLERead: Characteristic-Wert kann gelesen werden.
- BLEWrite: Characteristic-Wert darf beschrieben werden.
- BLEWriteWithoutResponse: Schreiben ohne Bestätigung (benötigt z.B. für Bluetooth LE Lab)
- BLENotify: Das Peripheral darf das Client-Gerät über geänderte Characteristic-Werte informieren.
Eine Auflistung aller Eigenschaften findet ihr im Enum „BLEproperty“ in BLEProperty.h.
Mit BLE.begin() initialisiert ihr die BLE-Funktionen eures Arduinos.
Den für das Advertising verwendeten Namen vergebt ihr mit setLocalName(), d. h., ihr seht diesen Namen in der Liste der verfügbaren Peripherals. Interessant ist, dass euer Peripheral ihr nach der Trennung vom Client evtl. unter einem anderen Namen in der Liste auftaucht. Bei mir war das „Arduino“. „Arduino“ war der voreingestellte „Device-Name“. Wenn ihr das Verhalten ändern wollt, dann ändert den „Device-Name“ mit setDeviceName().
Außerdem, erklärt in aller Kürze:
- Mit
BLE.setAdvertisedService(myService);wird die UUID vonmyServicein die Advertising-Daten aufgenommen. - Mit
myService.addCharacteristic(myCharacteristic);fügen wir die CharacteristicmyCharacteristicdem ServicemyServicehinzu. BLE.addService(myService);fügt den Service dem lokalen GATT-Server hinzu.myCharacteristic.writeValue()aktualisiert den Wert der Characteristic.BLEDevice central = BLE.central();erzeugt ein BLEDevice-Objekt namens „central“. Ist ein Central verbunden, repräsentiert dieses Objekt das verbundene Central. Andernfalls wird es bei einer Abfrage mitif (central)als false ausgewertet.central.address();liefert die MAC-Adresse des Centrals.- Die Funktion
central.connected()prüft, ob das Central noch verbunden ist. Falls nicht, verlassen wir die while‑Schleife. Weilcentralwieder neu erzeugt wird, ergibt die Prüfungif (central)wieder false, bis eine neue Verbindung steht. - Mit
myCharacteristic.written();prüfen wir, ob das Central der Characteristic einen neuen Wert gegeben hat.
Event-Handler (Callbacks)
Im nächsten Beispiel werden wir die Write-Funktion nutzen, um die Board-LED des Arduino-Boards ein- bzw. auszuschalten. Eigentlich eine einfache Aufgabe: Ein bestimmter Wert der Characteristic bedeutet „schalte die LED ein“, ein anderer bedeutet „schalte die LED aus“. Den Wert immer wieder abzufragen, um zu prüfen, ob er sich geändert hat, ist allerdings etwas unpraktisch. Eleganter wäre es, wenn diese Aktionen automatisch ablaufen würden. Und genau dafür gibt es in der ArduinoBLE-Bibliothek die Event-Handler-Funktionen. Sie entsprechen im Prinzip den Callback-Funktionen, die ich im letzten Beitrag über BLE mit dem ESP32 vorgestellt habe.
Und da wir gerade bei den Event-Handler-Funktionen sind, wollen wir diese auch dazu nutzen, um informiert zu werden, wenn sich ein Central mit unserem Arduino verbindet oder die Verbindung getrennt wird.
Hier zunächst der Sketch:
#include <ArduinoBLE.h>
BLEService ledService("19B10000-E8F2-537E-4F6C-D104768A1214");
BLEByteCharacteristic switchCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite);
const int ledPin = LED_BUILTIN;
void setup() {
Serial.begin(115200);
while (!Serial);
pinMode(ledPin, OUTPUT);
if (!BLE.begin()) {
Serial.println("Starting BLE module failed!");
while (1);
}
BLE.setLocalName("LED_Switch");
BLE.setDeviceName("LED_Switch");
BLE.setAdvertisedService(ledService);
ledService.addCharacteristic(switchCharacteristic);
BLE.addService(ledService);
BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);
switchCharacteristic.setEventHandler(BLEWritten, switchCharacteristicWritten);
switchCharacteristic.setValue(0);
BLE.advertise();
Serial.println(("BLE device active, waiting for connections..."));
}
void loop() {
BLE.poll();
}
void blePeripheralConnectHandler(BLEDevice central) {
Serial.print("Connected event, central: ");
Serial.println(central.address());
}
void blePeripheralDisconnectHandler(BLEDevice central) {
Serial.print("Disconnected event, central: ");
Serial.println(central.address());
}
void switchCharacteristicWritten(BLEDevice central, BLECharacteristic characteristic) {
// unused parameters
(void)central;
(void)characteristic;
Serial.print("Characteristic event, written: ");
if (switchCharacteristic.value()) {
Serial.println("LED on");
digitalWrite(ledPin, HIGH);
} else {
Serial.println("LED off");
digitalWrite(ledPin, LOW);
}
}
Wenn ihr euer Arduino-Board mit dem Central verbindet, die Characteristic einmal mit 1 (Format: hexadezimal) beschreibt, dann mit 0 und dann die Verbindung trennt, so solltet ihr eine Ausgabe wie die unten abgebildete auf dem seriellen Monitor sehen. Auf dem Board seht ihr, wie die Board-LED an- und wieder ausgeht.

Erklärungen zum Sketch
Vieles aus dem Sketch ist schon bekannt. Zum Schalten der LED definieren wir den Service ledService mit der Characteristic switchCharacteristic. Neu ist, dass wir für die Characteristic den Datentyp byte verwenden (BLEByteCharacteristic)
Die Event-Handler definieren wir mit der Funktion setEventHandler(). Es gibt eine Version für das BLE-Objekt (BLE.setEventHandler()) und eine für die Characteristics (switchCharacteristic.setEventHandler()) anwenden. Ihr übergebt der Funktion das Event und den frei wählbaren Namen der Event-Handler-Funktion. Das Prinzip kennt ihr von Interrupts.
Für die Characteristics sind folgende Events definiert:
- BLESubscribed / BLEUnsubscribed: Ein Central hat die Characteristic abonniert oder „das Abo gekündigt“.
- BLERead, BLEWritten (alias BLEUpdated): Ein Central hat die Characteristic gelesen oder beschrieben.
Für das BLE-Objekt sind die folgenden Events definiert:
- BLEConnected / BLEDisconnected: Ein Central bzw. ein Peripheral sind verbunden oder getrennt worden.
- BLEDiscovered: Der Arduino als Central hat beim Scannen ein Peripheral entdeckt.
Den BLE-Event-Handlern wird das verbundene Central übergeben. Der Event-Handler für die Characteristic bekommt außer dem Central auch die auslösende Characteristic übergeben. Trotzdem nutzen wir in der Event-Handler-Funktion switchCharacteristic und nicht den übergebenen Parameter characteristic. Der Grund dafür ist, dass der Datentyp des übergebenen Parameters die BLECharacteristic-Basisklasse ist und nicht BLEByteCharacteristic. Die Funktion value() der Basisklasse (also BLECharacteristic::value()) ist definiert als const uint8_t* value() const;. Ihr Rückgabewert ist also ein Zeiger und das würde die Dinge (für C++ Einsteiger) verkomplizieren. Auf der Centralseite müssen wir uns später dieser Herausforderung stellen.
Die Ausdrücke (void)characteristics; und (void)central; könntet ihr auch weglassen. Nur erhaltet ihr unter Umständen eine Warnmeldung über „unused parameters“. Wir machen hier einen sogenannten „Cast auf void“ und teilen dem Compiler damit sinngemäß mit: Ich nutze die Parameter absichtlich nicht.
BLE.poll() prüft und verarbeitet anstehende BLE-Ereignisse und ruft ggf. dafür die registrierten Event-Handler auf.
Warum muss ich hier pollen und beim ESP32 nicht?
Ganz so automatisch wie von mir angekündigt laufen die Callbacks also offenbar doch nicht, sonst müssten wir nicht „pollen“. Und wer meinen letzten Beitrag über BLE mit dem ESP32 gelesen hat, mag sich fragen, warum dort kein Pendant zu BLE.poll() verwendet werden musste. Die Antwort lautet: auch der ESP32 „pollt“, nur passiert das automatisch im Hintergrund mithilfe von FreeRTOS.
Stellt sich als nächste Frage, was passiert, wenn der Arduino zwischendurch in loop() mit anderen, zeitintensiven Dingen beschäftigt ist. Verpasst er etwas durch ein verspätetes BLE.poll()? Ich habe dazu einmal verschiedene Delays (Delayzeit) in loop() eingefügt. Daraufhin dauerte der Verbindungsaufbau in LightBlue ca. 14 x Delayzeit. Hier wird anscheinend mehrfach „hin- und herverhandelt“ und jeder Verhandlungsschritt wird durch das Delay verzögert. Das Schalten der LED hatte eine Verzögerung bis maximal 1 x Delayzeit.
Kurz aufeinanderfolgende Schaltanweisungen (Periode < Delayzeit) gingen in meinen Versuchen nicht verloren, wurden aber einzeln im Abstand von DelayInSekunden abgearbeitet. Besser also, ihr haltet loop() so schlank wie möglich.
Notify und Deskriptoren
Im vorherigen Beispiel hat das Central den Characteristic-Wert geändert und wir haben uns auf dem Peripheral mithilfe einer Callback-Funktion darüber automatisch informieren lassen. Im folgenden Beispiel drehen wir das Ganze um. Das Peripheral ändert die Characteristic und wir wollen uns darüber automatisch auf dem Central informieren lassen. Dafür nutzen wir die Characteristic-Eigenschaft „Notify“.
Das Ereignis, das das Beschreiben der Charakteristik auslöst, ist das Drücken bzw. Loslassen eines Tasters. Und damit es nicht so langweilig ist, nehmen wir gleich zwei Taster. Für jeden Taster richten wir eine Characteristic ein – nicht weil das so sein muss oder praktisch wäre, sondern nur um einmal mit zwei Characteristics zu arbeiten.
Da es etwas unbequem ist, die Characteristics auf dem Central nach ihrer UUID auszuwählen, geben wir ihnen einen Namen, nämlich „Button 1 state“ bzw. „Button 2 state“. Um diese Namen einzurichten, nutzen wir einen Deskriptor. So, wie es durch die Bluetooth-SIG vordefinierte Characteristics gibt, gibt es ebenso vordefinierte Deskriptoren. Der Deskriptor für die Benennung von Characteristics heißt „Characteristic User Description“ und hat die UUID 0x2901.
Zum Ausprobieren des folgenden Sketches installiert ihr zwei Taster mit einer ihrer Seiten an den Pins 6 und 7 eures Arduino-Boards. Die anderen Seiten der beiden Taster verbindet ihr mit GND.
Hier zunächst der Sketch:
#include <ArduinoBLE.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 = 6;
const int button2Pin = 7;
BLEService buttonService(SERVICE_UUID);
BLEStringCharacteristic button1Char(
BUTTON1_UUID, BLERead | BLENotify, 25);
BLEStringCharacteristic button2Char(
BUTTON2_UUID, BLERead | BLENotify, 25);
BLEDescriptor button1Desc("2901", "Button 1 state");
BLEDescriptor button2Desc("2901", "Button 2 state");
bool lastButton1 = HIGH;
bool lastButton2 = HIGH;
void setup() {
Serial.begin(115200);
while (!Serial);
pinMode(button1Pin, INPUT_PULLUP);
pinMode(button2Pin, INPUT_PULLUP);
if (!BLE.begin()) {
Serial.println("Starting BLE module failed!");
while (1);
}
BLE.setLocalName("Arduino Two Buttons");
BLE.setDeviceName("Arduino Two Buttons");
BLE.setAdvertisedService(buttonService);
buttonService.addCharacteristic(button1Char);
buttonService.addCharacteristic(button2Char);
button1Char.addDescriptor(button1Desc);
button2Char.addDescriptor(button2Desc);
BLE.addService(buttonService);
button1Char.writeValue("Button 1 not yet pressed");
button2Char.writeValue("Button 2 not yet pressed");
BLE.advertise();
Serial.println("BLE device active, waiting for connections...");
}
void loop() {
BLE.poll();
bool button1 = digitalRead(button1Pin);
bool button2 = digitalRead(button2Pin);
if (lastButton1 == HIGH && button1 == LOW) {
button1Char.writeValue("Button 1 pressed");
Serial.println("Button 1 pressed");
delay(200); // debouncing
}
else if (lastButton1 == LOW && button1 == HIGH) {
button1Char.writeValue("Button 1 released");
Serial.println("Button 1 released");
delay(200);
}
if (lastButton2 == HIGH && button2 == LOW) {
button2Char.writeValue("Button 2 pressed");
Serial.println("Button 2 pressed");
delay(200);
}
else if (lastButton2 == LOW && button2 == HIGH) {
button2Char.writeValue("Button 2 released");
Serial.println("Button 2 released");
delay(200);
}
lastButton1 = button1;
lastButton2 = button2;
}
Öffnet ihr LightBlue o.ä., dann könnt ihr sehen, dass die Characteristics jetzt benannt sind (Bild unten, Mitte). Wählt eine der Characteristics und ändert das Format von HEX auf UTF-8. Dann tippt ihr auf Subscribe (abonnieren). Wenn ihr jetzt den entsprechenden Taster drückt, bekommt ihr eine Ausgabe wie unten rechts. Ihr müsst also nicht mehr auf „Read“ tippen.
Erklärungen zum Sketch
Ich gehe nur auf die neuen Aspekte ein:
- Mit
BLERead | BLENotifyverleihen wir den Characteristics die Eigenschaften „Read“ und „Notify“. BLEDescriptor button1Desc("2901", "Button 1 state");erzeugt das Deskriptor-Objektbutton1Desc.- Der Parameter „2901“ legt fest, dass es sich um den Deskriptor „Characteristic User Description“ handelt.
- Der zweite Parameter legt den Namen der Characteristic fest.
- Die Zuordnung des Deskriptors zur Characteristic
button1Charerfolgt durchbutton1Char.addDescriptor(button1Desc);
Und das war es auch schon im Wesentlichen. In loop() werden die Tasterzustände fortlaufend abgefragt und mit den Zuständen des vorherigen Durchlaufes verglichen. Hat sich etwas geändert, wird der Wert der zuständigen Characteristic aktualisiert.
Wer meinen letzten Beitrag über BLE mit dem ESP32 gelesen hat, mag sich fragen, warum wir den Characteristics in diesem Sketch nicht auch den CCCD (Client Characteristic Configuration Descriptor) mit der UUID 0x2902 zuordnen müssen, damit der Client (Central) die Characteristics abonnieren kann. Bei der ESP32‑BLE-Bibliothek reichte Notify allein nicht aus. Die Antwort ist: Die Bibliothek ArduinoBLE ist in der Hinsicht komfortabler und macht das automatisch.
Weitere 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 bequem(er) mit einer Characteristic auskommen, die Button 1 und 2 abdeckt. Aber ich wollte nun einmal zeigen, wie man zwei Characteristics einrichtet.
Zwei Arduinos ü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 Arduino-Boards über BLE kommunizieren lassen. Die Eingabe einer Nachricht im seriellen Monitor des Server-Arduinos (Peripheral) soll auf dem seriellen Monitor des Client-Arduinos ausgegeben werden und umgekehrt. Ferner sollen sich die beiden Arduinos automatisch (wieder) verbinden.
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-Funktion setEventHandler() mit dem Event BLEWritten.
Außerdem nutzen wir die Funktion unseres Peripherals setEventHandler() mit den Parametern BLEConnected bzw. BLEDisconnected, 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).
#include <ArduinoBLE.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define RX_UUID "22222222-2222-2222-2222-222222222222" // Central -> Peripheral
#define TX_UUID "33333333-3333-3333-3333-333333333333" // Peripheral -> Central
BLEService textService(SERVICE_UUID);
BLEStringCharacteristic rxCharacteristic(
RX_UUID, BLEWrite, 100);
BLEStringCharacteristic txCharacteristic(
TX_UUID, BLERead | BLENotify, 100);
void setup() {
Serial.begin(115200);
while (!Serial);
if (!BLE.begin()) {
Serial.println("Starting BLE failed!");
while (1);
}
BLE.setLocalName("Arduino BLE Peripheral");
BLE.setDeviceName("Arduino BLE Peripheral");
BLE.setAdvertisedService(textService);
textService.addCharacteristic(rxCharacteristic);
textService.addCharacteristic(txCharacteristic);
BLE.addService(textService);
BLE.setEventHandler(BLEConnected, connectHandler);
BLE.setEventHandler(BLEDisconnected, disconnectHandler);
rxCharacteristic.setEventHandler(BLEWritten, rxWrittenHandler);
BLE.advertise();
Serial.println("Peripheral started");
}
void loop() {
BLE.poll();
if (BLE.connected() && Serial.available()) {
String text = Serial.readStringUntil('\n');
if (text.length() > 0) {
txCharacteristic.writeValue(text);
Serial.print("Sent: ");
Serial.println(text);
}
}
}
void connectHandler(BLEDevice central) {
Serial.print("Connected to central: ");
Serial.println(central.address());
Serial.println("Ready. Type in some text and press enter.");
}
void disconnectHandler(BLEDevice central) {
Serial.print("Disconnected from central: ");
Serial.println(central.address());
}
void rxWrittenHandler(BLEDevice central, BLECharacteristic characteristic) {
(void)central;
(void)characteristic;
Serial.print("Received: ");
Serial.println(rxCharacteristic.value());
}
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-Arduino erst beibringen. Da das ziemlich viele neue Dinge auf einmal sind, habe ich einen Zwischenschritt eingefügt, nämlich einen Central-Sketch, der erst einmal nur scannt und das Scan-Ergebnis ausgibt.
Zwischenschritt: Der Client scannt
Der folgende Sketch ist im Wesentlichen Scan.ino aus den Beispielen der ArduinoBLE‑Bibliothek. Ich habe ihn lediglich ein wenig erweitert.
#include <ArduinoBLE.h>
/* scan for specific devices or UUIDs - adjust and uncomment */
//#define SPECIFIC_UUID "11111111-1111-1111-1111-111111111111"
//#define SPECIFIC_NAME "Arduino BLE Peripheral"
//#define SPECIFIC_ADDRESS "3c:71:bf:95:da:ba"
void setup() {
Serial.begin(9600);
while (!Serial);
// begin initialization
if (!BLE.begin()) {
Serial.println("starting Bluetooth® Low Energy module failed!");
while (1);
}
Serial.println("Bluetooth® Low Energy Central scan");
// start scanning for peripheral
BLE.scan();
/* scan for specific devices or UUIDs - adjust and uncomment*/
//BLE.scanForUuid(SPECIFIC_UUID);
//BLE.scanForName(SPECIFIC_NAME);
//BLE.scanForAddress(SPECIFIC_ADDRESS);
}
void loop() {
// check if a peripheral has been discovered
BLEDevice peripheral = BLE.available();
if (peripheral) {
// discovered a peripheral
Serial.println("Discovered a peripheral");
Serial.println("-----------------------");
// print address
Serial.print("Address: ");
Serial.println(peripheral.address());
// print the local name, if present
if (peripheral.hasLocalName()) {
Serial.print("Local Name: ");
Serial.println(peripheral.localName());
}
// print the advertised service UUIDs, if present
if (peripheral.hasAdvertisedServiceUuid()) {
Serial.print("Service UUIDs: ");
for (int i = 0; i < peripheral.advertisedServiceUuidCount(); i++) {
Serial.print(peripheral.advertisedServiceUuid(i));
Serial.print(" ");
}
Serial.println();
}
// print the RSSI
Serial.print("RSSI: ");
Serial.println(peripheral.rssi());
Serial.println();
}
}
Ladet diesen und den Peripheral-Sketch auf zwei Arduino-Boards. Damit der Scanner nicht nur das Arduino-Board findet, habe ich zusätzlich meine BT‑Kopfhörer angeschaltet und dann folgende Ausgabe erhalten:

Der Scan wird mit BLE.scan() im Setup gestartet. Dabei ist wichtig zu wissen:
- Der Scan läuft im Hintergrund munter weiter, bis ihr ihn mit
BLE.stopScan()stoppt. Deswegen könnt ihr mit diesem Sketch auch BLE‑Geräte finden, die erst später verfügbar sind. - Bevor ihr ein Peripheral verbindet, müsst ihr den Scan stoppen. Da wir in diesem Sketch kein Gerät verbinden, stoppen wir den Scan nicht.
Den Funktionsnamen available() in BLEDevice peripheral = BLE.available(); finde ich etwas unglücklich gewählt, da er die Funktionsweise nicht unmittelbar erkennen lässt. Ein Name wie „nextDiscoveredDevice()“ wäre aus meiner Sicht passender. Sinngemäß bewirkt BLE.available() nämlich: „Gib mir das nächste beim Scan entdeckte BLE-Gerät zurück, das noch nicht von available() zurückgegeben wurde.“
Wenn ein weiteres BLE-Gerät gefunden wurde, wird der Ausdruck if (peripheral) als true interpretiert und die Schleife wird durchlaufen. Die weiteren Zeilen sind eigentlich selbsterklärend:
peripheral.address()liefert die MAC-Adresse.peripheral.hasLocalName()prüft, ob das BLE-Gerät einen Namen hat.peripheral.localName()gibt den Namen des Gerätes zurück.peripheral.hasAdvertisedServiceUuid())prüft, ob das Gerät „advertised“ Services anbietet.peripheral.advertisedServiceUuidCount()verrät euch die Zahl der „advertised“ Services.peripheral.advertisedServiceUuid(i)liefert euch die UUID des i-ten Services.peripheral.rssi()gibt die Stärke (Received Signal Strength Indicator) des empfangenen BLE-Signals zurück.
BLE.scanForUuid();filtert nach der UUID.BLE.scanForName();sucht nach dem Gerätenamen (Local Name).BLE.scanForAddress();scannt nach der MAC-Adresse.
Kommentiert bzw. entkommentiert die relevanten Zeilen und passt sie ggf. an, um die Funktionen auszuprobieren.
Der vollständige Client-Sketch
#include <ArduinoBLE.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define RX_UUID "22222222-2222-2222-2222-222222222222" // Central -> Peripheral
#define TX_UUID "33333333-3333-3333-3333-333333333333" // Peripheral -> Central
void setup() {
Serial.begin(115200);
while (!Serial);
if (!BLE.begin()) {
Serial.println("Starting BLE failed!");
while (1);
}
Serial.println("Arduino BLE Central");
Serial.println("Searching peripheral...");
BLE.scanForUuid(SERVICE_UUID);
}
void loop() {
BLEDevice peripheral = BLE.available();
if (peripheral) {
Serial.print("Found peripheral: ");
Serial.println(peripheral.address());
BLE.stopScan();
communicateWithPeripheral(peripheral);
// communicateWithPeripheral() returns after disconnect
Serial.println("Searching peripheral again...");
BLE.scanForUuid(SERVICE_UUID);
}
}
void communicateWithPeripheral(BLEDevice peripheral) {
Serial.println("Connecting...");
if (!peripheral.connect()) {
Serial.println("Connection failed");
return;
}
Serial.println("Connected to peripheral");
if (!peripheral.discoverService(SERVICE_UUID)) {
Serial.println("Service discovery failed");
peripheral.disconnect();
return;
}
BLECharacteristic rxCharacteristic =
peripheral.characteristic(RX_UUID);
BLECharacteristic txCharacteristic =
peripheral.characteristic(TX_UUID);
if (!rxCharacteristic || !txCharacteristic) {
Serial.println("Characteristic not found");
peripheral.disconnect();
return;
}
if (!rxCharacteristic.canWrite()) {
Serial.println("RX characteristic is not writable");
peripheral.disconnect();
return;
}
if (!txCharacteristic.canSubscribe()) {
Serial.println("TX characteristic is not subscribable");
peripheral.disconnect();
return;
}
if (!txCharacteristic.subscribe()) {
Serial.println("Subscription failed");
peripheral.disconnect();
return;
}
Serial.println("Subscribed to TX characteristic");
Serial.println("Ready. Type in some text and press enter.");
while (peripheral.connected()) {
BLE.poll();
// Text from peripheral received?
if (txCharacteristic.valueUpdated()) {
printReceivedText(txCharacteristic);
}
// Text entered in Serial Monitor?
if (Serial.available()) {
String text = Serial.readStringUntil('\n');
if (text.length() > 0) {
sendText(rxCharacteristic, text);
Serial.print("Sent: ");
Serial.println(text);
}
}
}
Serial.print("Disconnected from peripheral: ");
Serial.println(peripheral.address());
}
void printReceivedText(BLECharacteristic characteristic) {
char text[101];
int length = characteristic.readValue(text, 100);
text[length] = '\0'; // add null terminator
Serial.print("Received: ");
Serial.println(text);
}
void sendText(BLECharacteristic characteristic, const String &text) {
characteristic.writeValue(
text.c_str(),
text.length()
);
}
Ladet die Sketche auf zwei BLE-fähige Arduinos und öffnet die zugehörigen seriellen Monitore. Die Arduinos sollten sich automatisch verbinden. Nun könnt ihr auf beiden Seiten Nachrichten eingeben, die auf der anderen Seite ausgegeben werden. Hier eine Beispielausgabe:


Erklärungen zum Sketch
Zunächst definieren wir, genau wie auf der Peripheralseite, eine Service-UUID und die Characteristic-UUIDs für den Empfang und den Versand von Nachrichten (aus der Perspektive des Peripherals!).
In setup() initiieren wir die BLE-Funktion und starten den Scan, wobei wir spezifisch nach der Service-UUID suchen. In loop() prüfen wir, ob das Peripheral mit der gesuchten Service-UUID gefunden wurde. Wenn ja, dann wird der Scan gestoppt und wir starten die Kommunikation mit dem Peripheral in communicateWithPeripheral(), wobei wir das gefundene Peripheral an die Funktion übergeben. Sollte die Verbindung getrennt werden, startet der Scan erneut.
In communicateWithPeripheral() wird zunächst mit peripheral.connect() die Verbindung aufgebaut. Schlägt das fehl, geht es mit return zurück.
peripheral.discoverService(SERVICE_UUID) werden die Characteristics des Services „entdeckt“. Das ist essenziell. Mit peripheral.characteristic(RX_UUID) greifen wir auf die zuvor beim Peripheral entdeckte Characteristic mit der angegebenen UUID zu. Das zurückgegebene BLECharacteristic-Objekt dient dabei als lokale Repräsentation dieser entfernten Characteristic. Dasselbe passiert mit der TX-Characteristic.if (!rxCharacteristic || !txCharacteristic)prüft, ob die Characteristics tatsächlich vorhanden sind.rxCharacteristic.canWrite()ermittelt, ob die RX-Characteristic beschreibbar ist.txCharacteristic.canSubscribe()verrät euch, ob die TX-Characteristic abonnierbar ist.
Dann abonnieren wir die TX-Characteristic mit txCharacteristic.subscribe().
Solange das Central mit dem Peripheral verbunden ist (while (peripheral.connected())), wird in jedem Schleifendurchlauf:
- „gepollt“:
BLE.poll(), - auf eine aktualisierte TX-Characteristic geprüft:
txCharacteristic.valueUpdated() - und ggf. zu sendende Eingaben in den seriellen Monitor verarbeitet: (
if (Serial.available())) .
Verarbeitung der Characterisitics
Ich möchte noch einmal das Augenmerk auf die Funktionen printReceivedText() und sendText() lenken.
Bei der Verarbeitung der Characteristics ist wichtig zu wissen, dass es sich auf der Central-Seite um den Datentyp BLECharacteristic handelt und nicht etwa um BLEStringCharacteristic. Wie weiter oben bereits erläutert, liefert value() bei einer BLECharacteristic einen Zeiger vom Typ const uint8_t*.
Die Funktion readValue() ist überladen, d. h., es gibt mehrere Varianten mit unterschiedlichen Argumenten, zum Beispiel:
int readValue(uint8_t value[], int length);int readValue(void* value, int length);int readValue(xxxxx& value);mitxxxxx=uint8_t,int8_t,uint16_t,int16_t,uint32_toderint32_t.
Besonders praktisch ist die zweite Variante. Sie ist dafür verantwortlich, dass readValue(text, 100) überhaupt funktioniert. Ein void* ist ein typunabhängiger Zeiger und kann daher auf Speicherbereiche unterschiedlicher Datentypen zeigen. So könnt ihr beispielsweise auch die Adresse einer float-Variable direkt an readValue() übergeben: readValue(&floatVariable, sizeof(floatVariable)) übergeben. Im Anhang zeige ich, wie ihr diesbezüglich mit Strukturen umgeht.
Auch für writeValue() gibt es verschiedene Varianten, darunter int writeValue(const void* value, int length, bool withResponse = true);. Da text zuvor als String-Objekt eingelesen wurde und diese von writeValue() nicht angenommen werden, verwenden wir text.c_str(). Diese Funktion liefert einen const char* auf die nullterminierte Zeichenkette des String-Objekts, die anschließend an writeValue() übergeben werden kann. Alles klar? Für nicht so erfahrene Arduinoisten mag das alles etwas verwirrend sein. Wer sich etwas näher mit Strings und Character-Arrays beschäftigen möchte, kann meinen Artikel zu diesem Thema lesen.
Alternativer Client-Sketch, nicht blockierend
Der Client-Sketch hat einen gewissen Nachteil. Wenn der Client noch andere Aufgaben zu erledigen hat, wie etwa eine LED blinken zu lassen oder einen Tasterzustand (am Client) permanent zu prüfen, wo baue ich das ein? Zu loop() kehrt der Sketch nur bei einem Verbindungsabbruch zurück. Hingegen verbleibt der Sketch im Falle einer erfolgreichen Verbindung in while(peripheral.connected()), aber eben nur dann. Wenn ihr euch die Beispielsketche der ArduinoBLE-Bibliothek LEDControl.ino oder SensorTagButton.ino anschaut, dann seht ihr, dass sie ähnlich strukturiert sind und deshalb dasselbe (potenzielle!) Problem haben.
Der folgende Sketch löst das Problem, indem er immer wieder zu loop() zurückkehrt, egal ob eine Verbindung besteht oder nicht. Da der Sketch keine neuen Funktionen enthält, spare ich mir weitere Erklärungen.
#include <ArduinoBLE.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define RX_UUID "22222222-2222-2222-2222-222222222222"
#define TX_UUID "33333333-3333-3333-3333-333333333333"
BLEDevice peripheral;
BLECharacteristic rxCharacteristic;
BLECharacteristic txCharacteristic;
bool connected = false;
bool scanning = false;
void setup() {
Serial.begin(115200);
while (!Serial);
if (!BLE.begin()) {
Serial.println("Starting BLE failed!");
while (1);
}
startScan();
}
void loop() {
BLE.poll();
if (!connected) {
handleConnection();
}
else {
handleCommunication();
}
// doSomethingElse();
}
void startScan() {
BLE.scanForUuid(SERVICE_UUID);
scanning = true;
Serial.println("Searching peripheral...");
}
void handleConnection() {
if (!scanning) {
startScan();
}
peripheral = BLE.available();
if (!peripheral) {
return;
}
BLE.stopScan();
scanning = false;
Serial.println("Peripheral found");
Serial.println("Connecting...");
if (!peripheral.connect()) {
Serial.println("Connection failed");
return;
}
if (!peripheral.discoverService(SERVICE_UUID)) {
Serial.println("Service discovery failed");
peripheral.disconnect();
return;
}
rxCharacteristic = peripheral.characteristic(RX_UUID);
txCharacteristic = peripheral.characteristic(TX_UUID);
if (!rxCharacteristic || !txCharacteristic) {
Serial.println("Characteristic not found");
peripheral.disconnect();
return;
}
if (!txCharacteristic.subscribe()) {
Serial.println("Subscription failed");
peripheral.disconnect();
return;
}
connected = true;
Serial.println("Connected");
Serial.println("Ready. Type in some text and press enter.");
}
void handleCommunication() {
if (!peripheral.connected()) {
connected = false;
Serial.println("Disconnected from peripheral");
startScan();
return;
}
if (txCharacteristic.valueUpdated()) {
printReceivedText(txCharacteristic);
}
if (Serial.available()) {
String text = Serial.readStringUntil('\n');
text.trim();
if (text.length() > 0) {
sendText(rxCharacteristic, text);
Serial.print("Sent: ");
Serial.println(text);
}
}
}
void printReceivedText(BLECharacteristic characteristic) {
char text[101];
int length = characteristic.readValue(text, 100);
text[length] = '\0';
Serial.print("Received: ");
Serial.println(text);
}
void sendText(BLECharacteristic characteristic, String text) {
characteristic.writeValue(
text.c_str(),
text.length()
);
}
Anhang – Strukturen versenden
Zur Vertiefung möchte ich noch zeigen, wie ihr Strukturen versendet oder genauer gesagt, wie ihr Strukturen als Werte eine Characteristic verwendet. Als Beispiel haben wir auf der Peripheralseite eine Wetterstation, die die Temperatur (float) und die Luftfeuchtigkeit (int) misst und mitteilt, ob es gerade regnet (bool). Die Daten werden alle 10 Sekunden aktualisiert und über BLE dem Client zur Verfügung gestellt. Da es hier nur um das Prinzip geht und wir nicht wirklich eine Wetterstation am Peripheral haben, arbeiten wir mit festen Wetterdaten.
Wetterstation – Peripheral-Sketch
Hier zunächst der Peripheral Sketch:
#include <ArduinoBLE.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define WEATHER_UUID "11111112-1111-1111-1111-111111111111"
struct WeatherData {
float temperature;
int humidity;
bool raining;
};
WeatherData weatherData;
BLEService weatherService(SERVICE_UUID);
BLECharacteristic weatherCharacteristic(
WEATHER_UUID,
BLERead | BLENotify,
sizeof(WeatherData)
);
const unsigned long updateInterval = 10000;
unsigned long lastUpdate = 0;
void setup() {
Serial.begin(115200);
while (!Serial);
if (!BLE.begin()) {
Serial.println("Starting BLE failed!");
while (1);
}
BLE.setLocalName("Weather Station");
BLE.setDeviceName("Weather Station");
BLE.setAdvertisedService(weatherService);
weatherService.addCharacteristic(weatherCharacteristic);
BLE.addService(weatherService);
BLE.setEventHandler(BLEConnected, connectHandler);
BLE.setEventHandler(BLEDisconnected, disconnectHandler);
updateWeatherData();
BLE.advertise();
Serial.println("Weather Station Peripheral");
Serial.println("Advertising...");
}
void loop() {
BLE.poll();
if (millis() - lastUpdate >= updateInterval) {
lastUpdate = millis();
updateWeatherData();
weatherCharacteristic.writeValue(
&weatherData,
sizeof(weatherData)
);
Serial.println("Weather data updated");
}
}
void updateWeatherData() {
weatherData.temperature = 21.7;
weatherData.humidity = 63;
weatherData.raining = false;
}
void connectHandler(BLEDevice central) {
Serial.print("Connected to central: ");
Serial.println(central.address());
}
void disconnectHandler(BLEDevice central) {
Serial.print("Disconnected from central: ");
Serial.println(central.address());
}
Da es keine BLEStructCharacteristic o.ä. gibt, verwenden wir die allgemeine BLECharacteristic als Datentyp für unsere weatherCharacteristic. Ihr wird die WEATHER_UUID, die Eigenschaften Read und Notify und die Länge übergeben. Die Wetterdaten werden in der Struktur weatherData zusammengefasst. Ansonsten hat der Sketch keine neuen Elemente, auf die ich näher eingehen müsste.
Wetterstation: Central-Sketch
Und hier die Gegenseite:
#include <ArduinoBLE.h>
#define SERVICE_UUID "11111111-1111-1111-1111-111111111111"
#define WEATHER_UUID "11111112-1111-1111-1111-111111111111"
struct WeatherData {
float temperature;
int humidity;
bool raining;
};
BLEDevice peripheral;
BLECharacteristic weatherCharacteristic;
bool connected = false;
bool scanning = false;
void setup() {
Serial.begin(115200);
while (!Serial);
if (!BLE.begin()) {
Serial.println("Starting BLE failed!");
while (1);
}
Serial.println("Weather Station Central");
startScan();
}
void loop() {
BLE.poll();
if (!connected) {
handleConnection();
}
else {
handleCommunication();
}
// add additional code here
}
void startScan() {
BLE.scanForUuid(SERVICE_UUID);
scanning = true;
Serial.println("Searching for Weather Station...");
}
void handleConnection() {
if (!scanning) {
startScan();
}
peripheral = BLE.available();
if (!peripheral) {
return;
}
BLE.stopScan();
scanning = false;
Serial.print("Peripheral found: ");
Serial.println(peripheral.address());
if (!peripheral.connect()) {
Serial.println("Connection failed");
return;
}
Serial.println("Connected");
if (!peripheral.discoverService(SERVICE_UUID)) {
Serial.println("Service discovery failed");
peripheral.disconnect();
return;
}
weatherCharacteristic =
peripheral.characteristic(WEATHER_UUID);
if (!weatherCharacteristic) {
Serial.println("Weather characteristic not found");
peripheral.disconnect();
return;
}
if (!weatherCharacteristic.canSubscribe()) {
Serial.println("Weather characteristic cannot be subscribed");
peripheral.disconnect();
return;
}
if (!weatherCharacteristic.subscribe()) {
Serial.println("Subscription failed");
peripheral.disconnect();
return;
}
connected = true;
Serial.println("Subscribed to weather data");
printWeatherData();
}
void handleCommunication() {
if (!peripheral.connected()) {
connected = false;
Serial.println("Disconnected from peripheral");
startScan();
return;
}
if (weatherCharacteristic.valueUpdated()) {
printWeatherData();
}
}
void printWeatherData() {
WeatherData weatherData;
int length = weatherCharacteristic.readValue(
&weatherData,
sizeof(weatherData)
);
if (length != sizeof(weatherData)) {
Serial.println("Invalid weather data");
return;
}
Serial.print("Temperature: ");
Serial.print(weatherData.temperature, 1);
Serial.println(" °C");
Serial.print("Humidity: ");
Serial.print(weatherData.humidity);
Serial.println(" %");
Serial.print("Raining: ");
Serial.println(weatherData.raining ? "yes" : "no");
Serial.println();
}
Auch hier gibt es nicht viel Neues. Der entscheidende Punkt ist, wie die Wetterdaten gelesen werden, nämlich per readValue(. Erstaunlich einfach, nicht wahr?
&weatherData, sizeof(weatherData));
Hier noch die Ausgaben der Sketche:








