{"id":26335,"date":"2026-08-06T13:57:14","date_gmt":"2026-08-06T13:57:14","guid":{"rendered":"https:\/\/wolles-elektronikkiste.de\/?p=26335"},"modified":"2026-08-06T13:57:21","modified_gmt":"2026-08-06T13:57:21","slug":"ble-with-the-esp32-an-introduction","status":"publish","type":"post","link":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction","title":{"rendered":"BLE with the ESP32 \u2013 An Introduction"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">About this Post<\/h2>\n<p>In this post, I would like to walk you through using BLE (Bluetooth\u00ae Low Energy) on ESP32-based boards. While classic Bluetooth is very easy to use with the <a href=\"https:\/\/github.com\/espressif\/arduino-esp32\/tree\/master\/libraries\/BluetoothSerial\" target=\"_blank\" rel=\"noopener\">BluetoothSerial library<\/a> from the Arduino ESP32 board package, BLE \u2014 using the <a href=\"https:\/\/github.com\/espressif\/arduino-esp32\/tree\/master\/libraries\/BLE\" target=\"_blank\" rel=\"noopener\">library of the same name<\/a> \u2014 requires a bit more familiarization and a basic understanding of the underlying concepts. <\/p>\n<p>I will limit myself to the ESP32, because the libraries for other boards \u2014 such as <a href=\"https:\/\/github.com\/arduino-libraries\/ArduinoBLE\" target=\"_blank\" rel=\"noopener\">ArduinoBLE<\/a> for BLE-enabled Arduino boards (Arduino Nano 33 IoT\/BLE, Arduino UNO R4 WiFi, etc.) \u2014 differ significantly. Covering everything in a single post would have made it too confusing and too long. <\/p>\n<p>This is what you can expect in this article:<\/p>\n<ul>\n<li><a href=\"#overview\">A Brief Overview of BLE<\/a><\/li>\n<li><a href=\"#minimal_sketch\">A First Minimal Sketch<\/a><\/li>\n<li><a href=\"#callbacks\">Callbacks<\/a><\/li>\n<li><a href=\"#notify_descriptors\">Notify and Descriptors<\/a><\/li>\n<li><a href=\"#two_esp32\">Connecting Two ESP32s via BLE<\/a>\n<ul>\n<li><a href=\"#server\">The Server (Peripheral)<\/a><\/li>\n<li><a href=\"#client\">The Client (Central)<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#additional_applications\">Additional Examples of Use<\/a>\n<ul>\n<li><a href=\"#battery_check\">Checking the Battery Level of BLE Devices<\/a><\/li>\n<li><a href=\"#smartphone_hid\">Control your Smartphone via BLE\/HID &#8211; Remote Shutter Release for Cameras<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#ble_hc05\">BLE &#8220;HC-05&#8221; Modules<\/a><\/li>\n<li><a href=\"#appendix\">Appendix<\/a>\n<ul>\n<li><a href=\"#nrf_connect\">nRF Connect Smartphone BLE App<\/a><\/li>\n<li><a href=\"#windows_ble_apps\">More Windows BLE Apps<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>&nbsp;<\/p>\n\n<h2 id=\"overview\" class=\"wp-block-heading\">A Brief Overview of BLE<\/h2>\n<p>First, I need to introduce a few basic BLE-related concepts that might confuse you at first (I felt the same way!). Hopefully, the example sketches will clear things up. <\/p>\n\n<h4 class=\"wp-block-heading\">Peripheral and Central<\/h4>\n<p>Bluetooth\u00ae Low Energy (BLE) was introduced with Bluetooth 4.0 and is designed for applications with low power consumption and low data rates. This allows battery-powered devices, such as sensors or Bluetooth trackers, to often operate for months or even years on a single button cell battery. <\/p>\n<p>In BLE, there are two roles: peripheral and central. The peripheral device (hereinafter referred to as the peripheral) provides information, while the central device (hereinafter referred to as the central) retrieves this information, establishes a connection, and reads or modifies it. You can think of the peripheral as a server and the central as a client. Typical peripherals are sensors or microcontrollers, while smartphones or PCs often take on the role of the central.   <\/p>\n\n<h4 class=\"wp-block-heading\">Services and Characteristics<\/h4>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/ble_scheme-1024x334.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"334\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/ble_scheme-1024x334.png\" alt=\"\" class=\"wp-image-26214\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/ble_scheme-1024x334.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/ble_scheme-300x98.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/ble_scheme-768x250.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/ble_scheme-1320x430.png 1320w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/ble_scheme.png 1405w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">BLE Schematic<\/figcaption><\/figure>\n<p>The data provided by the peripheral is organized into services. Each service contains one or more characteristics in which the actual data is stored. Each characteristic and each service has a unique identifier (UUID = Universally Unique Identifier). Characteristics can have different properties. For example, they can be read-only, writable, or enabled for automatic notifications (notify or indicate).    <\/p>\n<p>Notify is particularly useful: If the peripheral changes the value of a characteristic, it can notify the central unit. This means the central unit does not have to repeatedly query the data. This simplifies the code and is ideal for measured values such as temperature, acceleration, or heart rate.  <\/p>\n<p>In order for a central device to locate a peripheral device at all, the peripheral regularly transmits short advertising packets. These packets contain, among other things, the device name and the services it offers. Only after the peripheral has been located is a connection established and the desired data exchanged.  <\/p>\n\n<h4 class=\"wp-block-heading\">Standardized vs. Non-Standardized Services and Characteristics<\/h4>\n<p>Many BLE devices have similar features, such as a rechargeable battery. The Bluetooth SIG (Special Interest Group) has standardized services and characteristics for such features. 16-bit UUIDs have been defined for these standardized services, for example:  <\/p>\n<ul>\n<li>Battery Service\n<ul>\n<li>UUID: 0x180F<\/li>\n<\/ul>\n<\/li>\n<li>Battery Level Characteristics\n<ul>\n<li>UUID: 0x2A19<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>How you define and organize your own services and characteristics is up to you. You define the corresponding UUIDs yourself. Since you typically assign 128-bit UUIDs for this purpose, the risk of collisions \u2014 that is, identical UUIDs \u2014 is extremely low. Let us consider two buttons on a microcontroller board that serves as a peripheral. The button states (pressed\/released) are transmitted via BLE. The UUIDs could then look like this:     <\/p>\n<ul>\n<li>Button Service\n<ul>\n<li>UUID:<strong>19B10000-E8F2-537E-4F6C-D104768A1214<\/strong> <\/li>\n<\/ul>\n<\/li>\n<li>Button 1 Characteristic\n<ul>\n<li>UUID:<strong>19B10001-E8F2-537E-4F6C-D104768A1214<\/strong> <\/li>\n<\/ul>\n<\/li>\n<li>Button 2 Characteristic\n<ul>\n<li>UUID:<strong>19B10002-E8F2-537E-4F6C-D104768A1214<\/strong> <\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>The position of the hyphens is fixed, as is the length of the UUIDs, which is 32 hexadecimal digits.  <\/p>\n\n<h4 class=\"wp-block-heading\">Advertising and General Advertising Profile (GAP)<\/h4>\n<p>Using the General Advertising Profile (GAP), BLE devices regularly send small advertising packets. These include, among other things, the device name and the UUIDs of the services being offered. This allows other devices to discover the peripheral and decide whether they want to connect to it.  <\/p>\n<p>Since advertising packets offer only a limited amount of storage space, typically only a single 128-bit service UUID can be transmitted. Although additional services may be present, they are not recognized until after the connection is established. <\/p>\n\n<h4 class=\"wp-block-heading\">Generic Attribute Profile (GATT)<\/h4>\n<p>The Generic Attribute Profile (GATT) defines how data is organized and exchanged in the form of services and characteristics. Using GATT, characteristic values can be read, written, or transmitted via Notify or Indicate. <\/p>\n<p>In most applications, the peripheral is the GATT server, since it provides the services and characteristics. The central, accordingly, is the GATT client that accesses this data. However, the terms \u201cserver\u201d and \u201cclient\u201d are independent of the roles of peripheral and central. In specific applications, a device can also take on both roles simultaneously.   <\/p>\n\n<h2 id=\"minimal_sketch\" class=\"wp-block-heading\">A First Minimal Sketch<\/h2>\n<p>The following sketch turns the ESP32 into a peripheral that offers one service and one characteristic. A suitable program on a smartphone or PC serves as the central device, which is authorized to read and modify the value of the characteristic.  <\/p>\n<p>Before you upload the sketch, check whether your ESP32 supports Bluetooth. Most do, but not all. The ESP32S2 is one of the models that does not support Bluetooth.  <\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"minimal_example.ino\" data-enlighter-title=\"minimal_example.ino\">#include &lt;BLEDevice.h&gt;\n\/\/#include &lt;BLEServer.h&gt; \/\/ should be included automatically\n\/\/#include &lt;BLEUtils.h&gt; \/\/ should be included automatically\n\n#define SERVICE_UUID        \"12345678-1234-1234-1234-1234567890ab\"\n#define CHARACTERISTIC_UUID \"abcd1234-1234-1234-1234-abcdef123456\"\n\nBLECharacteristic *pCharacteristic;\n\nvoid setup() {\n  Serial.begin(115200);\n\n  BLEDevice::init(\"ESP32 BLE Minimal\");\n\n  BLEServer *pServer = BLEDevice::createServer();\n  BLEService *pService = pServer-&gt;createService(SERVICE_UUID);\n  pServer-&gt;advertiseOnDisconnect(true);\n\n  pCharacteristic = pService-&gt;createCharacteristic(\n    CHARACTERISTIC_UUID,\n    BLECharacteristic::PROPERTY_READ |\n    BLECharacteristic::PROPERTY_WRITE\n    \/* NR = no response, needed for Bluetooth LE Lab *\/\n    \/\/ | BLECharacteristic::PROPERTY_WRITE_NR \n  );\n\n  pCharacteristic-&gt;setValue(\"Hello Smartphone!\");\n\n  pService-&gt;start();\n\n  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();\n  pAdvertising-&gt;addServiceUUID(SERVICE_UUID);\n  pAdvertising-&gt;start();\n\n  Serial.println(\"BLE Server started\");\n}\n\nvoid loop() {\n  String value = pCharacteristic-&gt;getValue();\n\n  if (value.length() &gt; 0) {\n    Serial.println(value);\n    delay(2000);\n  }\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n\n<h4 class=\"wp-block-heading\">Explanations<\/h4>\n<p>First, you need to include BLEDevice.h. This library file should include all other files required by the sketch.  <\/p>\n<p>Our sketch has only one service and one characteristic. The UUIDs can be chosen freely. <\/p>\n<p>With <code>BLECharacteristic *pCharacteristic;<\/code>, you define a pointer to a characteristic object. The &#8220;p&#8221; is there to remind you that this is a <strong>p<\/strong>ointer. The object itself is created later. Since we will also need the characteristic later in <code>loop()<\/code>, it is defined globally.    <\/p>\n<p><code>BLEDevice::init();<\/code> initializes the BLE device and sets its name. A lot happens in the background to accomplish this, such as initializing the ESP32&#8217;s Bluetooth hardware. BLEDevice is not a class that you create an object from; rather, it contains a set of functions. That is why you call the init function using the scope operator \u201c::\u201d (class::function()) and not, as you are probably more used to, using the dot operator (object.function()).   <\/p>\n<p>Use <code>BLEServer *pServer = BLEDevice::createServer();<\/code> to create a GATT server or a pointer to it. The server is assigned a service with the UUID defined at the beginning using <code>BLEService *pService = pServer-&gt;createService(SERVICE_UUID);<\/code>. Since &#8220;pServer&#8221; is a pointer, you use the arrow operator instead of the dot operator to access <code>createService()<\/code>. Using <code>advertiseOnDisconnect(true)<\/code> ensures that you can reconnect immediately after a disconnect. Otherwise, you would have to restart the ESP32.    <\/p>\n<p>You can then add a characteristic to the service using <code>createCharacteristic()<\/code>. To do this, you pass the UUID and the properties. In this example, the value of the characteristic can be both read and written.   <\/p>\n<p><code>setValue()<\/code> accepts either an Arduino-<code data-start=\"395\" data-end=\"403\">String<\/code> or a pointer to a byte array (<code data-start=\"442\" data-end=\"451\">uint8_t<\/code>) with the data length specified (<code>setValue(const uint8_t *pData, size_t length)<\/code>). This allows both text and any binary data to be transmitted. <\/p>\n<p>Finally, the service still needs to be started.<\/p>\n<p>With <code>getAdvertising()<\/code>, you get a pointer to the advertising object (there is only one). Only with <code data-start=\"1106\" data-end=\"1124\">addServiceUUID()<\/code> does the service become visible during the scanning process. Without this specification, a client can still discover the service via the GATT server after establishing a connection.  <\/p>\n<p>In <code>loop()<\/code>, the value of the characteristic is regularly queried using <code>getValue()<\/code> and displayed.<\/p>\n<p>&nbsp;<\/p>\n\n<h4 class=\"wp-block-heading\">But what am I supposed to do with this now?<\/h4>\n<p>To get started with the peripheral you just created, you will need a central device. A smartphone or a PC works well for this. I will focus on smartphones here, but <a href=\"#windows_ble_apps\">in the appendix<\/a> I will also introduce some (Windows) PC apps for BLE.  <\/p>\n<p>There are various BLE apps available for smartphones; I have tried <a href=\"https:\/\/play.google.com\/store\/apps\/details?id=com.punchthrough.lightblueexplorer\" target=\"_blank\" rel=\"noopener\">LightBlue\u00ae<\/a> and <a href=\"https:\/\/play.google.com\/store\/apps\/details?id=no.nordicsemi.android.mcp\" target=\"_blank\" rel=\"noopener\">nRF Connect<\/a> on my Android smartphone. Both apps are also available for Apple smartphones. For this post, I am primarily using LightBlue, as I find its interface a bit more user-friendly. In the appendix<a href=\"#nrf_connect\">,<\/a> I will briefly discuss nRF Connect.    <\/p>\n<p>Open LightBlue, select the &#8220;ESP32 BLE Minimal&#8221; peripheral, and connect. Then go to the characteristic. In the upper-right corner of the screen, you will likely see \u201cHEX.\u201d Tap it, select \u201cUTF-8 String,\u201d and tap \u201cSave.\u201d Use \u201cRead\u201d to view the characteristic\u2019s value and \u201cWrite\u201d to change it.     <\/p>\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_start-473x1024.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"473\" height=\"1024\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_start-473x1024.jpg\" alt=\"\" class=\"wp-image-26236\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_start-473x1024.jpg 473w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_start-139x300.jpg 139w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_start.jpg 500w\" sizes=\"auto, (max-width: 473px) 100vw, 473px\" \/><\/a><figcaption class=\"wp-element-caption\">Start screen<\/figcaption><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_peripheral-473x1024.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"473\" height=\"1024\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_peripheral-473x1024.webp\" alt=\"\" class=\"wp-image-26240\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_peripheral-473x1024.webp 473w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_peripheral-139x300.webp 139w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_peripheral.webp 500w\" sizes=\"auto, (max-width: 473px) 100vw, 473px\" \/><\/a><figcaption class=\"wp-element-caption\">Peripheral screen<\/figcaption><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_characteristic-473x1024.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"473\" height=\"1024\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_characteristic-473x1024.webp\" alt=\"\" class=\"wp-image-26239\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_characteristic-473x1024.webp 473w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_characteristic-139x300.webp 139w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_light_blue_characteristic.webp 500w\" sizes=\"auto, (max-width: 473px) 100vw, 473px\" \/><\/a><figcaption class=\"wp-element-caption\">Read\/write Screen<\/figcaption><\/figure>\n<\/div>\n<\/div>\n<p>If you write something like &#8220;Hi ESP32!&#8221; as the new value, you will see that both the output from the next &#8220;Read&#8221; and the output on the serial monitor change accordingly:<\/p>\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"511\" height=\"112\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_minimal_example.png\" alt=\"\" class=\"wp-image-26308\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_minimal_example.png 511w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_minimal_example-300x66.png 300w\" sizes=\"auto, (max-width: 511px) 100vw, 511px\" \/><figcaption class=\"wp-element-caption\">Output of minimal_example.ino<\/figcaption><\/figure>\n\n<h2 id=\"callbacks\" class=\"wp-block-heading\">Callbacks<\/h2>\n\n<h3 class=\"wp-block-heading\">Characteristic Callbacks<\/h3>\n<p>The first example was meant to illustrate the concept, but it has only limited practical value. That is why we are now going to switch an LED on the peripheral via the central unit. It is actually a simple task: when the characteristic has a certain value, turn the LED on; when it has another specific value, turn it off.  <\/p>\n<p>It would be much more convenient if we didn&#8217;t have to keep querying the value of the characteristic, but instead if writing to the characteristic would automatically trigger the associated actions. And that is exactly what callbacks are for. Let us take a look at an example:  <\/p>\n<\/p>\n<div class=\"scroll-paragraph-long\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"switch_led_and_call_back.ino\" data-enlighter-title=\"switch_led_and_call_back.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEServer.h&gt;\n#include &lt;BLEUtils.h&gt;\n\n#define SERVICE_UUID        \"11111111-1111-1111-1111-111111111111\"\n#define CHARACTERISTIC_UUID \"11111112-1111-1111-1111-111111111111\"\n\nconst int ledPin = 17;\n\n\/\/ Callback-Class\nclass MyCallbacks : public BLECharacteristicCallbacks {\n\n  void onWrite(BLECharacteristic *pCharacteristic) override{\n\n    String value = pCharacteristic-&gt;getValue();\/\/.c_str(); \/\/uncomment if necessary\n\n    Serial.print(\"Received: \");\n    Serial.println(value);\n\n    if (value == \"ON\") {\n      digitalWrite(ledPin, HIGH);\n      Serial.println(\"LED ON\");\n    }\n    else if (value == \"OFF\") {\n      digitalWrite(ledPin, LOW);\n      Serial.println(\"LED OFF\");\n    }\n  }\n};\n\nvoid setup() {\n\n  Serial.begin(115200);\n\n  pinMode(ledPin, OUTPUT);\n  digitalWrite(ledPin, LOW);\n\n  BLEDevice::init(\"ESP32 BLE LED\");\n\n  BLEServer *pServer = BLEDevice::createServer();\n\n  BLEService *pService =\n      pServer-&gt;createService(SERVICE_UUID);\n  pServer-&gt;advertiseOnDisconnect(true);\n\n  BLECharacteristic *pCharacteristic =\n      pService-&gt;createCharacteristic(\n          CHARACTERISTIC_UUID,\n          BLECharacteristic::PROPERTY_READ |\n          BLECharacteristic::PROPERTY_WRITE\n      );\n\n  pCharacteristic-&gt;setValue(\"OFF\");\n  pCharacteristic-&gt;setCallbacks(new MyCallbacks());\n\n  pService-&gt;start();\n\n  BLEAdvertising *pAdvertising =\n      BLEDevice::getAdvertising();\n\n  pAdvertising-&gt;addServiceUUID(SERVICE_UUID);\n  pAdvertising-&gt;setScanResponse(true);\n  pAdvertising-&gt;start();\n\n  Serial.println(\"BLE Server started\");\n}\n\nvoid loop() {\n  \/\/ nothing to do\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n<p>Connect an LED to pin 17. Then take your BLE Central device, connect to the &#8220;ESP32 BLE LED&#8221; peripheral, and select its only characteristic. If you type \u201cON,\u201d the LED will light up; if you type \u201cOFF,\u201d it will turn off again. If you type anything else, such as \u201cBlabla,\u201d nothing will happen. In the serial monitor, it looks like this:   <\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/output_switch_led_and_call_back.png\"><img loading=\"lazy\" decoding=\"async\" width=\"524\" height=\"112\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/output_switch_led_and_call_back.png\" alt=\"\" class=\"wp-image-26255\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/output_switch_led_and_call_back.png 524w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/output_switch_led_and_call_back-300x64.png 300w\" sizes=\"auto, (max-width: 524px) 100vw, 524px\" \/><\/a><figcaption class=\"wp-element-caption\">Output of switch_led_and_callback.ino<\/figcaption><\/figure>\n\n<h4 class=\"wp-block-heading\">Explanation of the Sketch <\/h4>\n<p>The only truly new feature here is the callback. For readers with limited experience in C++, things are about to get a little challenging. &#8220;BLECharacteristicCallbacks&#8221; is a class with the member function <code>onWrite()<\/code>, which is called in the event of a write operation. Since the library\u2019s developers couldn\u2019t know exactly what should happen when <code>onWrite()<\/code> is called, they defined the function as &#8220;virtual.&#8221;   <\/p>\n<p>Virtual functions are defined when they are to be overridden in derived classes. And that is precisely what we are doing here. Using <code>class MyCallbacks : public BLECharacteristicCallbacks {....<\/code>, we derive the class MyCallbacks from the predefined class BLECharacteristicCallbacks and define our own <code>onWrite()<\/code> function. The body of the function can be modified, but its parameter cannot. It expects a pointer to a BLECharacteristic object.     <\/p>\n<p>Using <code>setCallbacks(new MyCallbacks())<\/code>, we create an object of the MyCallbacks class, which we assign to pCharacteristics. <\/p>\n<p>And then you might have come across the following line:<\/p>\n<p><code>pAdvertising-&gt;setScanResponse(true);<\/code><\/p>\n<p>It is important to note that a BLE advertising packet is tiny (a maximum of 31 bytes for standard advertising). If these 31 bytes are not enough, a second packet can be used \u2014 the scan response. In other words, the line means: \u201cIf a scanner requests it, send an additional scan response packet.\u201d This way, you will be on the safe side. <\/p>\n\n<h4 class=\"wp-block-heading\">Additional Callback Classes and Callback Functions<\/h4>\n<p>In the example sketch, we set up a callback for a characteristic and defined the <code>onWrite()<\/code> function. In addition to <code>onWrite()<\/code>, there are other functions such as <code>onRead()<\/code>, <code>onNotify()<\/code>, and <code>onStatus()<\/code>. Furthermore, there are other callback functions with their own functions. Here is an (incomplete!) list of some callback classes with selected callback functions:   <\/p>\n<ul>\n<li><strong>BLEServerCallbacks<\/strong>: Client connects (<code>onConnect()<\/code>) or disconnects (<code>onDisconnect()<\/code>)<\/li>\n<li><strong>BLEClientCallbacks<\/strong>: Client Connection to the Server (<code>onConnect()<\/code>, <code>onDisconnect()<\/code>)<\/li>\n<li><strong>BLECharacteristicCallbacks<\/strong>: A characteristic is being written or read (<code>onRead()<\/code>, <code>onWrite()<\/code>).<\/li>\n<li><strong>BLEDescriptorCallbacks<\/strong>: Accessing a descriptor (<code>onRead()<\/code>, <code>onWrite()<\/code>)<\/li>\n<li><strong>BLESecurityCallbacks<\/strong>: e.g., authentication or authorization (<code>onAuthenticationComplete()<\/code>, <code> onAuthorizationRequest()<\/code>) <\/li>\n<\/ul>\n<p>You can find the callback class and function definitions in the BLE library files of the Arduino ESP32 board package (<a href=\"https:\/\/github.com\/espressif\/arduino-esp32\/tree\/master\/libraries\/BLE\/src\" target=\"_blank\" rel=\"noopener\">here)<\/a>. There is also some additional information in the example sketches in the BLE library, which you can find under File \u2192 Examples \u2192 Examples for <em>your board<\/em> \u2192 BLE. <\/p>\n<p>To illustrate this, I extended the BLECharacteristicCallbacks class from the previous example with a <code>onRead()<\/code> function (more precisely, I redefined the function). I also defined a BLEServerCallbacks class that uses <code>onConnect()<\/code> and <code>onDisconnect()<\/code> to indicate whether a client has connected or disconnected: <\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"switch_led_and_call_back_extended.ino\" data-enlighter-title=\"switch_led_and_call_back_extended.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEServer.h&gt;\n#include &lt;BLEUtils.h&gt;\n\n#define SERVICE_UUID        \"11111111-1111-1111-1111-111111111111\"\n#define CHARACTERISTIC_UUID \"11111112-1111-1111-1111-111111111111\"\n\nconst int ledPin = 17;\n\n\/\/ Callback-Class\nclass MyCharacteristicsCBs : public BLECharacteristicCallbacks {\n\n  void onWrite(BLECharacteristic *pCharacteristic) override{\n\n    String value = pCharacteristic-&gt;getValue();\/\/.c_str(); \/\/uncomment if necessary\n\n    Serial.print(\"Received: \");\n    Serial.println(value);\n\n    if (value == \"ON\") {\n      digitalWrite(ledPin, HIGH);\n      Serial.println(\"LED ON\");\n    }\n    else if (value == \"OFF\") {\n      digitalWrite(ledPin, LOW);\n      Serial.println(\"LED OFF\");\n    }\n  }\n\n  void onRead(BLECharacteristic *pCharacteristic) override{\n\n      Serial.print(\"Characteristic \");\n      Serial.print(pCharacteristic-&gt;getUUID().toString());\n      Serial.print(\" was read\");\n  }\n};\n\nclass MyServerCBs : public BLEServerCallbacks {\n    void onConnect(BLEServer *pServer) override {\n      Serial.println(\"Client is connected\");\n    }\n\n    void onDisconnect(BLEServer *pServer) override {\n      Serial.println(\"Client is disconnected\");\n    }\n};\n\n\n\nvoid setup() {\n\n  Serial.begin(115200);\n\n  pinMode(ledPin, OUTPUT);\n  digitalWrite(ledPin, LOW);\n\n  BLEDevice::init(\"ESP32 BLE LED\");\n\n  BLEServer *pServer = BLEDevice::createServer();\n\n  BLEService *pService =\n      pServer-&gt;createService(SERVICE_UUID);\n  pServer-&gt;advertiseOnDisconnect(true);\n  pServer-&gt;setCallbacks(new MyServerCBs());\n\n  BLECharacteristic *pCharacteristic =\n      pService-&gt;createCharacteristic(\n          CHARACTERISTIC_UUID,\n          BLECharacteristic::PROPERTY_READ |\n          BLECharacteristic::PROPERTY_WRITE\n      );\n\n  pCharacteristic-&gt;setValue(\"OFF\");\n  pCharacteristic-&gt;setCallbacks(new MyCharacteristicsCBs());\n\n  pService-&gt;start();\n\n  BLEAdvertising *pAdvertising =\n      BLEDevice::getAdvertising();\n\n  pAdvertising-&gt;addServiceUUID(SERVICE_UUID);\n  pAdvertising-&gt;setScanResponse(true);\n  pAdvertising-&gt;start();\n\n  Serial.println(\"BLE Server started\");\n}\n\nvoid loop() {\n  \/\/ nothing to do\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n<p>And while I was at it, I also added the function <code>getUUID()<\/code>, which returns the UUID of a characteristic. With <code>toString()<\/code>, you can convert it into a string that you can display on the serial monitor.  <\/p>\n\n<h4 class=\"wp-block-heading\">Multiple Characteristic Callbacks<\/h4>\n<p>You might be wondering how to handle this when setting up callbacks for multiple characteristics, where different actions are to be performed for each characteristic. Do I then have to create a separate callback class and callback object for each characteristic? No, not necessarily.   <\/p>\n<p>In the line <code><span class=\"\u037c11\">void<\/span> <span class=\"\u037c11\">onWrite<\/span>(<span class=\"\u037c11\">BLECharacteristic<\/span> *<span class=\"\u037c11\">pCharacteristic<\/span>)<\/code>, the function receives as a parameter a pointer to the exact characteristic that was just written to. If you have created the characteristics pChar1 and pChar2, you can check within the function which characteristic was just written to, for example, <code>if(pCharacteristic == pChar1) {.....}<\/code>. <\/p>\n<p>To make this work, assign the same callback object to both characteristics:<\/p>\n<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">class MyCallbacks : public BLECharacteristicCallbacks {....};\n....\nBLECharacteristic *pChar1;\nBLECharacteristic *pChar2;\n....\nMyCallbacks *callbacks = new MyCallbacks();\n....\npChar1-&gt;setCallbacks(callbacks);\npChar2-&gt;setCallbacks(callbacks);<\/pre>\n<p>\n<p>If the tasks to be performed by the callback are very different, it might be clearer to create separate callback classes.<\/p>\n\n<h2 id=\"notify_descriptors\" class=\"wp-block-heading\">Notify and Descriptors<\/h2>\n\n<h4 class=\"wp-block-heading\">What is this for?<\/h4>\n<p>In the examples so far, the action always originated from the central device: it set the value of a characteristic, which the peripheral then evaluated and reacted to accordingly.<\/p>\n<p>In the next example, we will reverse this principle. The action will now be initiated on the peripheral side, and the central device reacts. To do this, we will connect two pushbuttons to the ESP32 and want to see immediately on the smartphone when one of the buttons is pressed or released.  <\/p>\n<p>In principle, this could already be achieved using the techniques described so far. For each button, we create a separate characteristic and change its value according to the button\u2019s state. The central could then read this value at regular intervals. However, this constant polling is cumbersome and also carries the risk that a brief button press between two read operations might go unnoticed.   <\/p>\n<p>It is more elegant to have the central automatically notified as soon as the value of a characteristic changes. That is precisely what the notify property is for. <\/p>\n<p>However, simply setting the notify property is not enough. The characteristic must also be enabled for notifications. This is done using the <strong>Client<\/strong> <strong>Characteristic<\/strong> <strong>Configuration<\/strong> <strong>Descriptor<\/strong>(CCCD). This standardized descriptor has the UUID 0x2902 and allows the Central to subscribe to notifications (or indications) for a characteristic.   <\/p>\n<p>Since we are already using descriptors, we might as well use them to solve a second problem. Because our example uses two characteristics, we would have to distinguish between them on the central side based on their UUIDs \u2014 which is not very convenient. The Characteristic User Description descriptor (UUID 0x2901), on the other hand, allows us to assign a freely chosen name to a characteristic. This makes it much easier to distinguish between the two characteristics.   <\/p>\n\n<h4 class=\"wp-block-heading\">Example Sketch for Notify and Descriptors<\/h4>\n<p>To prepare, connect two pushbuttons to the appropriate pins on the ESP32. Connect the other ends of the two pushbuttons to GND. <\/p>\n<p>Here is the sketch:<\/p>\n<\/p>\n<div class=\"scroll-paragraph-long\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"notify_and_descriptors.ino\" data-enlighter-title=\"notify_and_descriptors.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEServer.h&gt;\n#include &lt;BLEUtils.h&gt;\n#include &lt;BLE2901.h&gt;\n#include &lt;BLE2902.h&gt;\n\n#define SERVICE_UUID \"7a3e0001-9b52-4b24-91c9-123456789abc\"\n#define BUTTON1_UUID \"7a3e0002-9b52-4b24-91c9-123456789abc\"\n#define BUTTON2_UUID \"7a3e0003-9b52-4b24-91c9-123456789abc\"\n\nconst int button1Pin = 18;\nconst int button2Pin = 19;\n\nBLECharacteristic *button1Char;\nBLECharacteristic *button2Char;\n\nbool lastButton1 = HIGH;\nbool lastButton2 = HIGH;\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(button1Pin, INPUT_PULLUP);\n  pinMode(button2Pin, INPUT_PULLUP);\n\n  BLEDevice::init(\"ESP32 Two Button Server\"); \/\/new name\n\n  BLEServer *pServer = BLEDevice::createServer();\n  pServer-&gt;advertiseOnDisconnect(true);\n  BLEService *pService = pServer-&gt;createService(SERVICE_UUID);\n\n  button1Char = pService-&gt;createCharacteristic(\n    BUTTON1_UUID,\n    BLECharacteristic::PROPERTY_READ |\n    BLECharacteristic::PROPERTY_NOTIFY\n  );\n\n  button2Char = pService-&gt;createCharacteristic(\n    BUTTON2_UUID,\n    BLECharacteristic::PROPERTY_READ |\n    BLECharacteristic::PROPERTY_NOTIFY\n  );\n\n  BLE2901 *desc1 = new BLE2901();\n  desc1-&gt;setDescription(\"Button 1 state\");\n  button1Char-&gt;addDescriptor(desc1);\n  button1Char-&gt;addDescriptor(new BLE2901());\n  button1Char-&gt;addDescriptor(new BLE2902());\n  button1Char-&gt;setValue(\"Button 1 not pressed\");\n\n  BLE2901 *desc2 = new BLE2901();\n  desc2-&gt;setDescription(\"Button 2 state\");\n  button2Char-&gt;addDescriptor(desc2);\n  button2Char-&gt;addDescriptor(new BLE2902());\n  button2Char-&gt;setValue(\"Button 2 not pressed\");\n\n  pService-&gt;start();\n\n  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();\n  pAdvertising-&gt;addServiceUUID(SERVICE_UUID);\n  pAdvertising-&gt;setScanResponse(true);\n  pAdvertising-&gt;start();\n\n  Serial.println(\"BLE Server started\");\n}\n\nvoid loop() {\n  bool button1 = digitalRead(button1Pin);\n  bool button2 = digitalRead(button2Pin);\n\n  if (lastButton1 == HIGH &amp;&amp; button1 == LOW) {\n    button1Char-&gt;setValue(\"Button 1 pressed\");\n    button1Char-&gt;notify();\n\n    Serial.println(\"Button 1 pressed\");\n    delay(200);\n  }\n  else if(lastButton1 == LOW &amp;&amp; button1 == HIGH) {\n    button1Char-&gt;setValue(\"Button 1 released\");\n    button1Char-&gt;notify();\n\n    Serial.println(\"Button 1 released\");\n    delay(200);\n  }\n  if (lastButton2 == HIGH &amp;&amp; button2 == LOW) {\n    button2Char-&gt;setValue(\"Button 2 pressed\");\n    button2Char-&gt;notify();\n\n    Serial.println(\"Button 2 pressed\");\n    delay(200);\n  }\n  else if (lastButton2 == LOW &amp;&amp; button2 == HIGH) {\n    button2Char-&gt;setValue(\"Button 2 released\");\n    button2Char-&gt;notify();\n\n    Serial.println(\"Button 2 released\");\n    delay(200);\n  }\n\n  lastButton1 = button1;\n  lastButton2 = button2;\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n\n<h4 class=\"wp-block-heading\">Output<\/h4>\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-vertically-aligned-center is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:66.66%\">\n<p>As before, open your favorite BLE app, select the BLE peripheral (in this case: &#8220;ESP32 Two Button Server&#8221;), and then choose one of the two characteristics: &#8220;Button 1 state&#8221; or &#8220;Button 2 state.&#8221;<\/p>\n<p>Then you will need to subscribe to the notification. There is a &#8220;Subscribe&#8221; button for that. <\/p>\n<p>If you press either of the two buttons now, you will receive the corresponding message in your app.<\/p>\n<p>You can see what that looks like in LightBlue in the image on the right. <\/p>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:33.33%\">\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/screen_lightblue_notify_example.png\"><img loading=\"lazy\" decoding=\"async\" width=\"400\" height=\"651\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/screen_lightblue_notify_example.png\" alt=\"\" class=\"wp-image-26268\" style=\"aspect-ratio:0.6144486781491199;width:226px;height:auto\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/screen_lightblue_notify_example.png 400w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/screen_lightblue_notify_example-184x300.png 184w\" sizes=\"auto, (max-width: 400px) 100vw, 400px\" \/><\/a><\/figure>\n<\/div>\n<\/div>\n\n<h4 class=\"wp-block-heading\">Explanations<\/h4>\n<p>To use these two descriptors, you must include the corresponding header files, BLE2901.h and BLE2902.h.<\/p>\n<p>This time, we will create two characteristics. I think the procedure is self-explanatory. The notify property is set with <code>BLECharacteristic::PROPERTY_NOTIFY<\/code>.  <\/p>\n<p>Next, we will handle the &#8220;Characteristic User Description&#8221; descriptor for the characteristic of the first button. For this, we will create a new BLE2901 object using <code>BLE2901 *desc1 = new BLE2901();<\/code> and store its address in the pointer <code>desc1<\/code>. <\/p>\n<p>Next, we assign the desired name to the descriptor using <code>desc1-&gt;setDescription(\"Button 1 state\");<\/code>.<\/p>\n<p>The descriptor is assigned to the characteristic using <code>button1Char-&gt;addDescriptor(desc1);<\/code>. We will do the same for the second button below.  <\/p>\n<p>Assigning the CCCD is easier: <code>button1Char-&gt;addDescriptor(new BLE2902());<\/code>.<\/p>\n<p>In loop(), we continuously check the status of the buttons. If a status changes, we update the value of the characteristic using <code>buttonxChar-&gt;setValue(\"Button x .....\");<\/code>. <\/p>\n<div>Notifications are sent via <code>buttonxChar-&gt;notify();<\/code>.<\/div>\n\n<h4 class=\"wp-block-heading\">A few more comments on the sketch<\/h4>\n<p>This sketch is for illustrative purposes only and has not been optimized. To ensure that button presses in rapid succession are not lost, I would use interrupts. We could also get by with just one characteristic covering buttons 1 and 2. However, I wanted to show how to set up two characteristics.    <\/p>\n\n<h2 id=\"two_esp32\" class=\"wp-block-heading\">Connecting Two ESP32s via BLE<\/h2>\n<p>For didactic reasons, the client side has so far been a smartphone, PC, or laptop. Now we are taking the next step by having two ESP32s communicate via BLE. A message entered in the serial monitor of the server ESP32 (peripheral) should be displayed on the serial monitor of the client ESP32, and vice versa.   <\/p>\n\n<h3 id=\"server\" class=\"wp-block-heading\">The Server (Peripheral)<\/h3>\n<p>On the server side, there is not really anything new regarding BLE. However, we are combining some elements from previous examples into a single sketch. When the message is sent to the client (more specifically, when we change the value of the characteristic), we use notify so that the client does not have to constantly check for the value in <code>loop()<\/code>. To be notified of a message arrival (= write event) on the client side without having to constantly check &#8220;manually,&#8221; we use the characteristic callback function <code>onWrite()<\/code>.    <\/p>\n<p>We also use the server callback functions <code>onConnect()<\/code> and <code>onDisconnect()<\/code> to keep track of the connection status. <\/p>\n<p>We use two characteristics for the messages we send and receive. RX_UUID is the UUID for receiving (R = receive), and TX_UUID is the UUID for sending (T = transmit). <\/p>\n<p>Here is the server sketch:<\/p>\n<\/p>\n<div class=\"scroll-paragraph-long\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"two_esp32_server.ino\" data-enlighter-title=\"two_esp32_server.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEServer.h&gt;\n#include &lt;BLEUtils.h&gt;\n#include &lt;BLE2902.h&gt;\n\n#define SERVICE_UUID \"11111111-1111-1111-1111-111111111111\"\n#define RX_UUID      \"22222222-2222-2222-2222-222222222222\" \/\/ Client -&gt; Server\n#define TX_UUID      \"33333333-3333-3333-3333-333333333333\" \/\/ Server -&gt; Client\n\nBLECharacteristic *txCharacteristic;\nbool deviceConnected = false;\n\nclass ServerCallbacks : public BLEServerCallbacks {\n  void onConnect(BLEServer *pServer) {\n    deviceConnected = true;\n    Serial.println(\"Client connected\");\n  }\n\n  void onDisconnect(BLEServer *pServer) {\n    deviceConnected = false;\n    Serial.println(\"Client diconnected\");\n    pServer-&gt;startAdvertising();\n  }\n};\n\nclass RxCallbacks : public BLECharacteristicCallbacks {\n  void onWrite(BLECharacteristic *pCharacteristic) {\n    String value = pCharacteristic-&gt;getValue();\n\n    if (value.length() &gt; 0) {\n      Serial.print(\"Received: \");\n      Serial.println(value);\n    }\n  }\n};\n\nvoid setup() {\n  Serial.begin(115200);\n\n  BLEDevice::init(\"ESP32 BLE Server\");\n\n  BLEServer *pServer = BLEDevice::createServer();\n  pServer-&gt;setCallbacks(new ServerCallbacks());\n\n  BLEService *pService = pServer-&gt;createService(SERVICE_UUID);\n\n  BLECharacteristic *rxCharacteristic = pService-&gt;createCharacteristic(\n    RX_UUID,\n    BLECharacteristic::PROPERTY_WRITE\n  );\n  rxCharacteristic-&gt;setCallbacks(new RxCallbacks());\n\n  txCharacteristic = pService-&gt;createCharacteristic(\n    TX_UUID,\n    BLECharacteristic::PROPERTY_NOTIFY\n  );\n  txCharacteristic-&gt;addDescriptor(new BLE2902());\n\n  pService-&gt;start();\n\n  BLEAdvertising *advertising = BLEDevice::getAdvertising();\n  advertising-&gt;addServiceUUID(SERVICE_UUID);\n  advertising-&gt;setScanResponse(true);\n  advertising-&gt;start();\n\n  Serial.println(\"Server started\");\n}\n\nvoid loop() {\n  if (deviceConnected &amp;&amp; Serial.available()) {\n    String text = Serial.readStringUntil('\\n');\n\n    if (text.length() &gt; 0) {\n      txCharacteristic-&gt;setValue(text);\n      txCharacteristic-&gt;notify();\n\n      Serial.print(\"Sent: \");\n      Serial.println(text);\n    }\n  }\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n<p>If input is entered via the serial monitor and the server is connected to the client (<code>if (deviceConnected && Serial.available())<\/code>), the input is set as the new value, and the client is notified via <code>notify()<\/code>.<\/p>\n\n<h3 id=\"client\" class=\"wp-block-heading\">The Client (Central)<\/h3>\n<p>The client side is new and includes a whole range of new features. Until now, the software (like LightBlue) has handled scanning for available BLE devices. We had then selected the device from the list and established the connection. Now we need to teach the central ESP32 how to do all of this. Since that involves quite a few things at once, I have added an intermediate step: a central sketch that initially just scans and searches for the desired service.     <\/p>\n\n<h4 class=\"wp-block-heading\">Intermediate Step: The Client scans<\/h4>\n<p>So here is the scanner sketch:<\/p>\n<\/p>\n<div class=\"scroll-paragraph-long\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"two_esp32_client_scan_sketch.ino\" data-enlighter-title=\"two_esp32_client_scan_sketch.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEUtils.h&gt;\n\n#define SERVICE_UUID \"11111111-1111-1111-1111-111111111111\"\n\nvoid scanForDevices() {\n  Serial.println(\"Searching server...\");\n\n  BLEScan *pScan = BLEDevice::getScan();\n  pScan-&gt;setActiveScan(true);\n\n  BLEScanResults *pResults = pScan-&gt;start(5);\n\n  for (int i = 0; i &lt; pResults-&gt;getCount(); i++) {\n    BLEAdvertisedDevice device = pResults-&gt;getDevice(i);\n\n    Serial.println(device.toString());\n\n    if (device.haveServiceUUID() &amp;&amp;\n        device.isAdvertisingService(BLEUUID(SERVICE_UUID))) {\n      Serial.println(\"Matching server found!\");\n    }\n\n    Serial.println();\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  BLEDevice::init(\"ESP32 BLE Client\");\n\n  scanForDevices();\n}\n\nvoid loop() {\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n<p>Upload this sketch and the server sketch to two ESP32 boards. On the client side, you should see output like this: <\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_two_esp32_client_scanner.png\"><img loading=\"lazy\" decoding=\"async\" width=\"976\" height=\"202\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_two_esp32_client_scanner.png\" alt=\"\" class=\"wp-image-26281\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_two_esp32_client_scanner.png 976w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_two_esp32_client_scanner-300x62.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_two_esp32_client_scanner-768x159.png 768w\" sizes=\"auto, (max-width: 976px) 100vw, 976px\" \/><\/a><figcaption class=\"wp-element-caption\">Output from two_esp32_client_scan_sketch.ino<\/figcaption><\/figure>\n<p><code>BLEScan *pScan = BLEDevice::getScan();<\/code> creates a pointer to the scan object. <code>pScan-&gt;setActiveScan(true);<\/code> makes the scan active, meaning that the scanner not only listens to what the BLE devices\u2019 advertising packets offer but also sends a scan request to devices of interest. The devices respond with a scan response packet, which may contain additional data. If you pass <code>false<\/code> instead, no scan request is sent. With <code>BLEScanResults *pResults = scan-&gt;start(5);<\/code>, you start a five-second scan and create a pointer to the result. <\/p>\n<div>\n<div><code>pResults-&gt;getCount();<\/code> returns the number of BLE devices found. Use <code>BLEAdvertisedDevice device = pResults-&gt;getDevice(i);<\/code> to &#8220;extract&#8221; the device with the corresponding sequence number. Use <code>device.toString()<\/code> to retrieve the device information as a string, which you can then output using <code>Serial.print()<\/code>.   <\/div>\n<\/div>\n<div> <\/div>\n<div>\n<div>\n<div>The condition <code>device.haveServiceUUID() && device.isAdvertisingService(BLEUUID(SERVICE_UUID))<\/code> checks whether the device has a service and whether that service has the UUID &#8220;SERVICE_UUID&#8221;. The function <code>isAdvertisingService()<\/code> expects a BLEUUID object as a parameter, which it generates from the UUID. <\/div>\n<\/div>\n<\/div>\n\n<h4 class=\"wp-block-heading\">The Complete Client Sketch<\/h4>\n<\/p>\n<div class=\"scroll-paragraph-long\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"two_esp32_client_complete.ino\" data-enlighter-title=\"two_esp32_client_complete.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEUtils.h&gt;\n#include &lt;BLEClient.h&gt;\n\n#define SERVICE_UUID \"11111111-1111-1111-1111-111111111111\"\n#define RX_UUID      \"22222222-2222-2222-2222-222222222222\" \/\/ Client -&gt; Server\n#define TX_UUID      \"33333333-3333-3333-3333-333333333333\" \/\/ Server -&gt; Client\n\nBLERemoteCharacteristic *rxCharacteristic;\nBLERemoteCharacteristic *txCharacteristic;\n\nbool connected = false;\n\nvoid notifyCallback(\n  BLERemoteCharacteristic *characteristic,\n  uint8_t *data,\n  size_t length,\n  bool isNotify\n) {\n  Serial.print(\"Received: \");\n\n  for (size_t i = 0; i &lt; length; i++) {\n    Serial.print((char)data[i]);\n  }\n\n  Serial.println();\n}\n\nclass ClientCallbacks : public BLEClientCallbacks {\n  void onConnect(BLEClient *client) {\n    Serial.println(\"Connected to server\");\n  }\n\n  void onDisconnect(BLEClient *client) {\n    connected = false;\n    Serial.println(\"Disconnected from server\");\n  }\n};\n\nbool connectToServer() {\n  Serial.println(\"Serching server...\");\n\n  BLEScan *pScan = BLEDevice::getScan();\n  pScan-&gt;setActiveScan(true);\n\n  BLEScanResults *pResults = pScan-&gt;start(5);\n\n  BLEAdvertisedDevice *pServerDevice = nullptr;\n\n  for (int i = 0; i &lt; pResults-&gt;getCount(); i++) {\n    BLEAdvertisedDevice device = pResults-&gt;getDevice(i);\n\n    if (device.haveServiceUUID() &amp;&amp;\n        device.isAdvertisingService(BLEUUID(SERVICE_UUID))) {\n      pServerDevice = new BLEAdvertisedDevice(device);\n      break;\n    }\n  }\n\n  if (pServerDevice == nullptr) {\n    Serial.println(\"Server not found\");\n    return false;\n  }\n\n  BLEClient *pClient = BLEDevice::createClient();\n  pClient-&gt;setClientCallbacks(new ClientCallbacks());\n\n  if (!pClient-&gt;connect(pServerDevice)) {\n    Serial.println(\"Connection failed\");\n    return false;\n  }\n\n  BLERemoteService *pService =\n      pClient-&gt;getService(BLEUUID(SERVICE_UUID));\n\n  if (pService == nullptr) {\n    Serial.println(\"Service not found\");\n    pClient-&gt;disconnect();\n    return false;\n  }\n\n  rxCharacteristic =\n      pService-&gt;getCharacteristic(BLEUUID(RX_UUID));\n\n  txCharacteristic =\n      pService-&gt;getCharacteristic(BLEUUID(TX_UUID));\n\n  if (rxCharacteristic == nullptr || txCharacteristic == nullptr) {\n    Serial.println(\"Characteristic not found\");\n    pClient-&gt;disconnect();\n    return false;\n  }\n\n  txCharacteristic-&gt;registerForNotify(notifyCallback);\n\n  connected = true;\n  Serial.println(\"Ready. Type in some text and press enter.\");\n  return true;\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  BLEDevice::init(\"ESP32 BLE Client\");\n\n  connectToServer();\n}\n\nvoid loop() {\n  if (!connected) {\n    static unsigned long lastTry = 0;\n\n    if (millis() - lastTry &gt; 5000) {\n      lastTry = millis();\n      connectToServer();\n    }\n\n    return;\n  }\n\n  if (Serial.available()) {\n    String text = Serial.readStringUntil('\\n');\n\n    if (text.length() &gt; 0) {\n      rxCharacteristic-&gt;writeValue(text, text.length());\n\n      Serial.print(\"Sent: \");\n      Serial.println(text);\n    }\n  }\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n<p>The first thing you might notice in the client sketch is the two BLERemoteCharacteristics. On the server side, the services and characteristics are actually created. They are therefore represented by objects of type BLEService and BLECharacteristic. The client does not create these objects again. After establishing a connection, it searches for the offered services and characteristics based on their UUIDs. To access these remote objects, the library uses the BLERemoteService and BLERemoteCharacteristic classes. A BLERemoteCharacteristic is thus the local representation of a characteristic located on the BLE server.      <\/p>\n<p>Next, I would like to focus on the <code>connectToServer()<\/code> function. I explained the first few lines in the intermediate step above.  <code>BLEClient *pClient = BLEDevice::createClient();<\/code> creates a pointer to your client object. Then, using <code>pClient-&gt;setClientCallbacks(new ClientCallbacks())<\/code>, a callback object of the ClientCallbacks class is assigned to the client object. The callback functions <code>onConnect()<\/code> and <code>onDisconnect()<\/code> allow us to be notified of changes in the connection status and to automatically reconnect later in <code>loop()<\/code>.   <\/p>\n<p><code>pClient-&gt;connect(pServerDevice);<\/code> attempts to connect the client to the server and reports back whether it was successful. <\/p>\n<p><code>rxCharacteristic = pService-&gt;getCharacteristic(BLEUUID(RX_UUID));<\/code> returns the address of rxCharacteristic. We follow the same procedure for txCharacteristic. If this fails, the pointer is still a null pointer (nullptr). We can check for this and, if necessary, disconnect the client from the server.   <\/p>\n<p>Just as we subscribed to notifications on our smartphones earlier, we basically need to do the same thing here \u2014 specifically, with <code>txCharacteristic-&gt;registerForNotify(notifyCallback);<\/code>.<\/p>\n<p>Hopefully, the rest of the sketch is clear. Now you can send messages via the serial monitors of the server and client ESP32s. <\/p>\n\n<h2 id=\"additional_applications\" class=\"wp-block-heading\">Additional Examples of Use<\/h2>\n\n<h3 id=\"battery_check\" class=\"wp-block-heading\">Checking the Battery Level of BLE Devices<\/h3>\n<p>I would like to return to the topic of standardized services and characteristics. A complete list is available <a href=\"https:\/\/www.bluetooth.com\/wp-content\/uploads\/Files\/Specification\/HTML\/Assigned_Numbers\/out\/en\/Assigned_Numbers.pdf\" target=\"_blank\" rel=\"noopener\">here<\/a> on the official website of the Bluetooth Special Interest Group. You can easily find out which standardized services and characteristics are implemented in your BLE peripherals using apps like LightBlue.   <\/p>\n<p>My first example shows how you can read the battery charge level of a BLE device using the ESP32 \u2014 provided the device offers the Battery Service with the UUID 0x180F and has the Battery Level Characteristic with the UUID 0x2A19. <\/p>\n<p>This is the sketch:<\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"battery_level_check.ino\" data-enlighter-title=\"battery_level_check.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEUtils.h&gt;\n#include &lt;BLEClient.h&gt;\n\n#define BATTERY_SERVICE_UUID \"180F\"\n#define BATTERY_LEVEL_UUID   \"2A19\"\n\n\/\/ MAC-Address from LightBlue or nRF Connect:\nBLEAddress headphoneAddress(\"80:C3:BA:96:56:DC\");\n\nBLERemoteCharacteristic *batteryCharacteristic;\nbool connected = false;\n\nbool connectToHeadphones() {\n  Serial.println(\"Connect to headphone...\");\n\n  BLEClient *pClient = BLEDevice::createClient();\n\n  if (!pClient-&gt;connect(headphoneAddress)) {\n    Serial.println(\"Connection failed\");\n    return false;\n  }\n\n  Serial.println(\"Connected\");\n\n  BLERemoteService *batteryService =\n      pClient-&gt;getService(BLEUUID(BATTERY_SERVICE_UUID));\n\n  if (batteryService == nullptr) {\n    Serial.println(\"Battery service not found\");\n    pClient-&gt;disconnect();\n    return false;\n  }\n\n  batteryCharacteristic =\n      batteryService-&gt;getCharacteristic(BLEUUID(BATTERY_LEVEL_UUID));\n\n  if (batteryCharacteristic == nullptr) {\n    Serial.println(\"Battery level characteristic not found\");\n    pClient-&gt;disconnect();\n    return false;\n  }\n\n  connected = true;\n  Serial.println(\"Battery characteristic found\");\n  return true;\n}\n\nvoid readBatteryLevel() {\n  if (!batteryCharacteristic-&gt;canRead()) {\n    Serial.println(\"Cannot read battery level\");\n    return;\n  }\n\n  String value = batteryCharacteristic-&gt;readValue();\n\n  if (value.length() &gt; 0) {\n    uint8_t batteryLevel = value[0];\n\n    Serial.print(\"Battery level: \");\n    Serial.print(batteryLevel);\n    Serial.println(\" %\");\n  }\n  else {\n    Serial.println(\"No value received\");\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  delay(1000);\n\n  BLEDevice::init(\"ESP32 BLE Battery Client\");\n\n  if (connectToHeadphones()) {\n    readBatteryLevel();\n  }\n}\n\nvoid loop() {\n  static unsigned long lastRead = 0;\n\n  if (connected &amp;&amp; millis() - lastRead &gt; 10000) {\n    lastRead = millis();\n    readBatteryLevel();\n  }\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n<p>I selected my Bluetooth headphones as the BLE device. I used LightBlue to find the corresponding MAC address. There, I also saw that the device offers the Battery Service.   <\/p>\n<p>What is new in this sketch is that the function <code>connect()<\/code> function is passed a MAC address instead of a pointer to a BLEAdvertisedDevice object. Both approaches work. In addition, we use the function <code>canRead()<\/code> here to check whether we can read the characteristic.   <\/p>\n<p>The rest of the sketch should sound familiar to you. Here is the output. <\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_battery_level_check.png\"><img loading=\"lazy\" decoding=\"async\" width=\"591\" height=\"102\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_battery_level_check.png\" alt=\"\" class=\"wp-image-26290\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_battery_level_check.png 591w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/output_battery_level_check-300x52.png 300w\" sizes=\"auto, (max-width: 591px) 100vw, 591px\" \/><\/a><figcaption class=\"wp-element-caption\">Output from battery_level_check.ino<\/figcaption><\/figure>\n<p><strong>Now for the bad news regarding standardized services<\/strong>: Although the Bluetooth SIG defines numerous standardized services, in practice many BLE devices implement a wide variety of manufacturer-specific services and characteristics instead. For example, in headphones, features such as active noise cancellation, equalizers, or firmware updates are usually implemented via proprietary BLE services. The standardized battery service is the exception rather than the rule here.  <\/p>\n\n<h3 id=\"smartphone_hid\" class=\"wp-block-heading\">Control your Smartphone via BLE\/HID &#8211; Remote Shutter Release for Cameras<\/h3>\n<p>If you can interact with a smartphone via BLE, isn\u2019t it also possible to control certain smartphone functions using an ESP32? Yes, it is possible, but not that simple. One option would be to write your own smartphone app. However, that\u2019s likely beyond the capabilities of most people. Alternatively, you can turn the ESP32 into an HID input device (HID = Human Interface Device). While this is easier than writing a smartphone app, it is still complex enough that I could fill an entire blog post with it. Since I am not an expert on this topic either, I turned to ChatGPT for help. The task was to write a sketch that would let me trigger the camera on my Android smartphone using a button on the ESP32. It worked right away.         <\/p>\n<p>This is the sketch:<\/p>\n<\/p>\n<div class=\"scroll-paragraph-long\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"hid_camera_control.ino\" data-enlighter-title=\"hid_camera_control.ino\">#include &lt;BLEDevice.h&gt;\n#include &lt;BLEServer.h&gt;\n#include &lt;BLEHIDDevice.h&gt;\n#include &lt;BLEUtils.h&gt;\n\nconst int buttonPin = 17;   \nbool lastState = HIGH;\nbool connected = false;\n\nBLEHIDDevice *hid;\nBLECharacteristic *inputReport;\n\n\/\/ HID Report Map f\u00fcr Consumer Control: Volume Up \/ Volume Down\nconst uint8_t reportMap[] = {\n  0x05, 0x0C,       \/\/ Usage Page: Consumer\n  0x09, 0x01,       \/\/ Usage: Consumer Control\n  0xA1, 0x01,       \/\/ Collection: Application\n  0x85, 0x01,       \/\/ Report ID: 1\n\n  0x15, 0x00,       \/\/ Logical Minimum: 0\n  0x25, 0x01,       \/\/ Logical Maximum: 1\n  0x75, 0x01,       \/\/ Report Size: 1 Bit\n  0x95, 0x02,       \/\/ Report Count: 2 Bits\n  0x09, 0xE9,       \/\/ Usage: Volume Up\n  0x09, 0xEA,       \/\/ Usage: Volume Down\n  0x81, 0x02,       \/\/ Input: Data, Variable, Absolute\n\n  0x75, 0x01,       \/\/ Report Size: 1 Bit\n  0x95, 0x0E,       \/\/ Report Count: 14 Bits Padding\n  0x81, 0x03,       \/\/ Input: Constant\n\n  0xC0              \/\/ End Collection\n};\n\nclass ServerCallbacks : public BLEServerCallbacks {\n  void onConnect(BLEServer *server) {\n    connected = true;\n    Serial.println(\"Smartphone connected\");\n  }\n\n  void onDisconnect(BLEServer *server) {\n    connected = false;\n    Serial.println(\"Smartphone disconnected\");\n    BLEDevice::startAdvertising();\n  }\n};\n\nvoid sendVolumeDown() {\n  \/\/ Bit 0 = Volume Up, Bit 1 = Volume Down\n  uint8_t press[]   = { 0x02, 0x00 };\n  uint8_t release[] = { 0x00, 0x00 };\n\n  inputReport-&gt;setValue(press, sizeof(press));\n  inputReport-&gt;notify();\n\n  delay(80);\n\n  inputReport-&gt;setValue(release, sizeof(release));\n  inputReport-&gt;notify();\n\n  Serial.println(\"Sent volume down\");\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(buttonPin, INPUT_PULLUP);\n\n  BLEDevice::init(\"ESP32 Camera Button\");\n\n  BLEServer *server = BLEDevice::createServer();\n  server-&gt;setCallbacks(new ServerCallbacks());\n\n  hid = new BLEHIDDevice(server);\n  inputReport = hid-&gt;inputReport(1);\n\n  hid-&gt;manufacturer()-&gt;setValue(\"My ESP32\");\n  hid-&gt;pnp(0x02, 0x1234, 0x5678, 0x0100);\n  hid-&gt;hidInfo(0x00, 0x01);\n\n  hid-&gt;reportMap((uint8_t *)reportMap, sizeof(reportMap));\n  hid-&gt;startServices();\n\n  BLEAdvertising *advertising = BLEDevice::getAdvertising();\n  advertising-&gt;setAppearance(0x03C1);  \n  advertising-&gt;addServiceUUID(hid-&gt;hidService()-&gt;getUUID());\n  advertising-&gt;setScanResponse(true);\n  advertising-&gt;start();\n\n  Serial.println(\"BLE HID started\");\n  Serial.println(\"Pair with your phone and open camera app.\");\n}\n\nvoid loop() {\n  bool state = digitalRead(buttonPin);\n\n  if (lastState == HIGH &amp;&amp; state == LOW) {\n    if (connected) {\n      sendVolumeDown();\n    }\n    delay(250); \/\/ debouncing\n  }\n\n  lastState = state;\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n<p>Then I asked ChatGPT for a &#8220;high-level&#8221; explanation, with the following result:<\/p>\n<p><em>The sketch makes the ESP32 appear to a smartphone as a Bluetooth input device. It does this by using the standardized Bluetooth HID profile, which is also used by keyboards, mice, and remote controls, for example. The so-called HID Report Map defines which input commands the ESP32 can send; in this case, these are \u201cVolume Up\u201d and \u201cVolume Down.\u201d Although both commands are defined, the sketch uses only \u201cVolume Down,\u201d since many camera apps interpret this key press as the shutter button.   <\/em><\/p>\n<p><em>Upon startup, the ESP32 creates a BLE server and sets up the HID service on it. It then begins advertising so that the smartphone can find and pair with the device under the name &#8220;ESP32 Camera Button.&#8221; Callback functions are used to detect whether a smartphone is connected; if the connection is lost, advertising automatically restarts. In the main loop, the ESP32 monitors the button on GPIO 17 and detects the transition from the unpressed to the pressed state. If a smartphone is connected, the <code>sendVolumeDown()<\/code> function first sends the button press and, shortly thereafter, the button release. To the smartphone, the ESP32 thus behaves like a Bluetooth remote control whose volume button is used as a camera shutter button.     <\/em><\/p>\n\n<h2 id=\"ble_hc05\" class=\"wp-block-heading\">BLE &#8220;HC-05 Modules&#8221;<\/h2>\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:25%\"><\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:50%\">\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/hc05_module-1024x346.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"346\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/hc05_module-1024x346.webp\" alt=\"\" class=\"wp-image-26295\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/hc05_module-1024x346.webp 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/hc05_module-300x102.webp 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/hc05_module-768x260.webp 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/hc05_module.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">BLE &#8220;HC-05&#8221; Module<\/figcaption><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:25%\"><\/div>\n<\/div>\n<p>In one of my first posts, I discussed the HC-05 and HC-06 modules (<a href=\"https:\/\/wolles-elektronikkiste.de\/en\/hc-05-and-hc-06-bluetooth-modules\" target=\"_blank\" rel=\"noopener\">link to the post<\/a>). These modules enable simple communication via Bluetooth Classic from one MCU board to another or from an MCU board to a smartphone. Recently, BLE versions of these modules have become increasingly common. They are sold as \u201cstandard\u201d HC-05 or HC-06 modules, so you do not realize what you have bought until your order arrives. The AT command <code>AT+VERSION?<\/code> returns <code>hc05V2.3_le<\/code> or something similar. The modules do not connect to each other. Configuration options are limited, as the modules can only be controlled via AT commands.       <\/p>\n<p>I think these modules are fake, since there are no HC-05 or HC-06 modules that support BLE on the <a href=\"https:\/\/www.hc01.com\/goods\" target=\"_blank\" rel=\"noopener\">manufacturer&#8217;s website<\/a> (which is only in Chinese \u2014 you will need to use your browser&#8217;s translation feature). <\/p>\n<p>So, what can you do with it? After all, you can use apps like LightBlue to send messages back and forth between your smartphone and the module \u2014 though that doe not make much sense for most ESP32 boards, since you can do that without the module anyway. If you still want to give it a try, connect the module to Serial1, for example (GND-GND, VCC-3.3V, RX-TX1, TX-RX1). Pair the module with your smartphone and then select it in LightBlue or another BLE app. There you will find a Write\/Notify characteristic that you can use to communicate.     <\/p>\n<p>Here is a sketch you can use: <\/p>\n<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"hc05_ble_sketch.ino\" data-enlighter-title=\"hc05_ble_sketch.ino\">#define RX1 16\n#define TX1 17\n\nvoid setup() {\n  Serial.begin(38400);\n  Serial1.begin(38400, SERIAL_8N1, RX1, TX1);\n  \n  if(Serial1){\n    Serial.println(\"Serial1 successfully set up\");\n  }\n}\n\nvoid loop() { \/\/ run over and over\n  if (Serial1.available()) {\n    Serial.write(Serial1.read());\n  }\n  if (Serial.available()) {\n    Serial1.write(Serial.read());\n  }\n}\n<\/pre>\n<p>\u00a0<\/p>\n<p>\n\n<h2 class=\"wp-block-heading\">If you want to dive deeper<\/h2>\n<p>This post is comprehensive, but it still only scratches the surface of BLE. If you want to dive deeper, I recommend going through the examples in the BLE library. <\/p>\n<p>It is also worth taking a look at the header files for the BLE library\u2014you can find them <a href=\"https:\/\/github.com\/espressif\/arduino-esp32\/tree\/master\/libraries\/BLE\/src\" target=\"_blank\" rel=\"noopener\">here<\/a>. For example, if you want to know what functions a BLEAdvertisedDevice object has, check the BLEAdvertisedDevice.h file. <\/p>\n<p>And if you ever get stuck on your BLE projects and cannot figure out the problem on your own, I recommend ChatGPT. The AI is very knowledgeable about this topic (which is not the case for all topics!).  <\/p>\n\n<h2 class=\"wp-block-heading\">How you can support me<\/h2>\n<p>Did you enjoy this post? And did you notice that there are no annoying ads here? To keep it that way, you can support me. You can find out how <a href=\"https:\/\/wolles-elektronikkiste.de\/en\/support-me\" target=\"_blank\" rel=\"noopener\">here<\/a>.   <\/p>\n\n<h2 id=\"appendix\" class=\"wp-block-heading\">Appendix<\/h2>\n\n<h3 id=\"nrf_connect\" class=\"wp-block-heading\">nRF Connect Smartphone BLE App<\/h3>\n<p>An alternative to LightBlue is the <a href=\"https:\/\/play.google.com\/store\/apps\/details?id=no.nordicsemi.android.mcp\" target=\"_blank\" rel=\"noopener\">nRF Connect<\/a> app. You can find the available Bluetooth devices under &#8220;Scanner.&#8221; Tap &#8220;Connect&#8221; and then go to your device&#8217;s tab. There, tap the characteristic of your choice. The down arrow lets you read the characteristic, while the up arrow opens a write dialog. Select the data type, enter the value or text, and tap \u201cSend.\u201d Here is what it looked like for me using the sketch minimal_example.ino:      <\/p>\n<p>&nbsp;<\/p>\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_scanner-473x1024.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"473\" height=\"1024\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_scanner-473x1024.webp\" alt=\"\" class=\"wp-image-26232\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_scanner-473x1024.webp 473w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_scanner-139x300.webp 139w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_scanner.webp 500w\" sizes=\"auto, (max-width: 473px) 100vw, 473px\" \/><\/a><figcaption class=\"wp-element-caption\">Scanner Screen<\/figcaption><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_characteristic-473x1024.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"473\" height=\"1024\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_characteristic-473x1024.webp\" alt=\"\" class=\"wp-image-26233\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_characteristic-473x1024.webp 473w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_characteristic-139x300.webp 139w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_characteristic.webp 500w\" sizes=\"auto, (max-width: 473px) 100vw, 473px\" \/><\/a><figcaption class=\"wp-element-caption\">Device Screen<\/figcaption><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_write-473x1024.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"473\" height=\"1024\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_write-473x1024.webp\" alt=\"\" class=\"wp-image-26234\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_write-473x1024.webp 473w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_write-139x300.webp 139w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/screen_nrfconnect_write.webp 500w\" sizes=\"auto, (max-width: 473px) 100vw, 473px\" \/><\/a><figcaption class=\"wp-element-caption\">Write Screen<\/figcaption><\/figure>\n<\/div>\n<\/div>\n<p>nRF Connect is also available as a PC program. During installation, it asked me to install additional utilities. It all seemed a bit too much for my needs, so I decided not to go through with it.  <\/p>\n\n<h3 id=\"windows_ble_apps\" class=\"wp-block-heading\">More Windows BLE Apps<\/h3>\n\n<h4 class=\"wp-block-heading\">Bluetooth LE Explorer<\/h4>\n<p>There are also various BLE programs available for PCs and laptops. For Windows systems, I recommend &#8220;Bluetooth LE Explorer&#8221; from the Microsoft Store. There are certainly plenty of options for Mac and Linux systems as well (e.g., LightBlue for Mac), but since I happen to have a Windows PC, I was not able to try them out.   <\/p>\n<p>In Bluetooth LE Explorer, first go to &#8220;Discover and Pair&#8221; and then select &#8220;Start&#8221;:<\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_start-1024x408.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"408\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_start-1024x408.png\" alt=\"\" class=\"wp-image-26261\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_start-1024x408.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_start-300x120.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_start-768x306.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_start.png 1202w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Bluetooth LE Explorer &#8211; Home Screen<\/figcaption><\/figure>\n<p>Your BLE device should then appear in the list (in this case: ESP32 BLE Minimal). Click on the device to go to the following overview: <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_ble_minimal-1024x570.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"570\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_ble_minimal-1024x570.png\" alt=\"\" class=\"wp-image-26262\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_ble_minimal-1024x570.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_ble_minimal-300x167.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_ble_minimal-768x427.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_ble_minimal.png 1202w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">BLE Explorer &#8211; ESP32 Minimal<\/figcaption><\/figure>\n<p>If you are using the &#8220;ESP32 BLE Minimal&#8221; example, you can now click on the characteristic to read its value or write a new value. Be sure to set the correct data format. <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_chracteristic-1024x341.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"341\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_chracteristic-1024x341.png\" alt=\"\" class=\"wp-image-26263\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_chracteristic-1024x341.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_chracteristic-300x100.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_chracteristic-768x256.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_explorer_chracteristic.png 1202w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">BLE Explorer &#8211; Characteristics Page<\/figcaption><\/figure>\n<p>With PCs and laptops, keep in mind that the device remembers the name of the peripheral, among other things. If you create a new peripheral with a different name using your ESP32, you will likely still see the old name. Apparently, the PC or laptop recognizes the device by its MAC address and does not re-read all the information but retrieves it from a cache. To fix this, go to Windows settings \u2192 \u201cBluetooth &#038; Devices\u201d and remove the device there. If that does not help, briefly turn Bluetooth off and then back on again on your computer.     <\/p>\n\n<h4 class=\"wp-block-heading\">Bluetooth LE Lab<\/h4>\n<p>I also tried &#8220;Bluetooth LE Explorer&#8221; from the Microsoft Store. In my opinion, I would not recommend it. With this program, you first have to pair the Bluetooth device. You can do this via Windows Settings \u2192 Bluetooth &#038; devices \u2192 Add a device \u2192 Bluetooth \u2192 All devices. Then launch &#8220;Bluetooth LE Lab&#8221; and select the device.     <\/p>\n<p><strong>Also important<\/strong>: You must assign the &#8220;PROPERTY_WRITE_NR&#8221; property (write without acknowledgment) to the characteristic and check the corresponding box in &#8220;Bluetooth LE Lab&#8221; when writing (see below). If you forgot to do this on your first attempt, simply uploading the modified sketch again will not be enough. Yoo will also need to unpair the device, briefly turn Bluetooth off and then back on, and pair it again.   <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/bluetooth_le_lab-1024x768.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/bluetooth_le_lab-1024x768.png\" alt=\"\" class=\"wp-image-26245\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/bluetooth_le_lab-1024x768.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/bluetooth_le_lab-300x225.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/bluetooth_le_lab-768x576.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/07\/bluetooth_le_lab.png 1291w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Bluetooth LE Lab &#8211; User Interface<\/figcaption><\/figure>\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>After a brief overview of BLE (Bluetooth Low Energy), I will walk you through, step by step, how to use this technology on ESP32-based boards.<\/p>\n","protected":false},"author":1,"featured_media":26334,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[568,548],"tags":[556,2935,2936,2944,2952,2939,2937,2950,1626,2947,1044,2942,596,2758,558,2943,2945,2949,2948,2940,1627,2938,2951,1660,2941,2946],"class_list":["post-26335","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-bluetooth-en","category-wireless","tag-arduino-en-2","tag-ble","tag-bluetooth-low-energy","tag-callback","tag-callbacks","tag-central","tag-characteristic","tag-characteristics","tag-client-en","tag-descriptor","tag-esp32-en","tag-gatt","tag-hc-05-en","tag-hid-en","tag-library-en-2","tag-lightblue","tag-notify","tag-nrf-connect","tag-pc","tag-peripheral","tag-server-en","tag-service","tag-services","tag-smartphone-en","tag-uuid","tag-write-2"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.2 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>BLE with the ESP32 \u2013 An Introduction &#8226; Wolles Elektronikkiste<\/title>\n<meta name=\"description\" content=\"After a brief overview, I will walk you through, step by step, how to use BLE (Bluetooth Low Energy) on ESP32-based boards.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"BLE with the ESP32 \u2013 An Introduction &#8226; Wolles Elektronikkiste\" \/>\n<meta property=\"og:description\" content=\"After a brief overview, I will walk you through, step by step, how to use BLE (Bluetooth Low Energy) on ESP32-based boards.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction\" \/>\n<meta property=\"og:site_name\" content=\"Wolles Elektronikkiste\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-06T13:57:14+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-06T13:57:21+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_post_image.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1000\" \/>\n\t<meta property=\"og:image:height\" content=\"1000\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Wolfgang Ewald\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Wolfgang Ewald\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"36 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction\"},\"author\":{\"name\":\"Wolfgang Ewald\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\"},\"headline\":\"BLE with the ESP32 \u2013 An Introduction\",\"datePublished\":\"2026-08-06T13:57:14+00:00\",\"dateModified\":\"2026-08-06T13:57:21+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction\"},\"wordCount\":5084,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\"},\"image\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/ble_post_image.jpg\",\"keywords\":[\"Arduino\",\"BLE\",\"Bluetooth Low Energy\",\"callback\",\"Callbacks\",\"central\",\"characteristic\",\"characteristics\",\"client\",\"descriptor\",\"ESP32\",\"GATT\",\"HC-05\",\"HID\",\"Library\",\"LightBlue\",\"notify\",\"nRF Connect\",\"PC\",\"peripheral\",\"server\",\"service\",\"services\",\"smartphone\",\"UUID\",\"write\"],\"articleSection\":[\"Bluetooth\",\"Wireless\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction\",\"name\":\"BLE with the ESP32 \u2013 An Introduction &#8226; Wolles Elektronikkiste\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/ble_post_image.jpg\",\"datePublished\":\"2026-08-06T13:57:14+00:00\",\"dateModified\":\"2026-08-06T13:57:21+00:00\",\"description\":\"After a brief overview, I will walk you through, step by step, how to use BLE (Bluetooth Low Energy) on ESP32-based boards.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#primaryimage\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/ble_post_image.jpg\",\"contentUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/ble_post_image.jpg\",\"width\":1000,\"height\":1000},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/ble-with-the-esp32-an-introduction#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Startseite\",\"item\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"BLE with the ESP32 \u2013 An Introduction\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#website\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\",\"name\":\"Wolles Elektronikkiste\",\"description\":\"Die wunderbare Welt der Elektronik\",\"publisher\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\",\"name\":\"Wolfgang Ewald\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\",\"contentUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\",\"width\":512,\"height\":512,\"caption\":\"Wolfgang Ewald\"},\"logo\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"BLE with the ESP32 \u2013 An Introduction &#8226; Wolles Elektronikkiste","description":"After a brief overview, I will walk you through, step by step, how to use BLE (Bluetooth Low Energy) on ESP32-based boards.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction","og_locale":"en_US","og_type":"article","og_title":"BLE with the ESP32 \u2013 An Introduction &#8226; Wolles Elektronikkiste","og_description":"After a brief overview, I will walk you through, step by step, how to use BLE (Bluetooth Low Energy) on ESP32-based boards.","og_url":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction","og_site_name":"Wolles Elektronikkiste","article_published_time":"2026-08-06T13:57:14+00:00","article_modified_time":"2026-08-06T13:57:21+00:00","og_image":[{"width":1000,"height":1000,"url":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_post_image.jpg","type":"image\/jpeg"}],"author":"Wolfgang Ewald","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Wolfgang Ewald","Est. reading time":"36 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#article","isPartOf":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction"},"author":{"name":"Wolfgang Ewald","@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46"},"headline":"BLE with the ESP32 \u2013 An Introduction","datePublished":"2026-08-06T13:57:14+00:00","dateModified":"2026-08-06T13:57:21+00:00","mainEntityOfPage":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction"},"wordCount":5084,"commentCount":0,"publisher":{"@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46"},"image":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#primaryimage"},"thumbnailUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_post_image.jpg","keywords":["Arduino","BLE","Bluetooth Low Energy","callback","Callbacks","central","characteristic","characteristics","client","descriptor","ESP32","GATT","HC-05","HID","Library","LightBlue","notify","nRF Connect","PC","peripheral","server","service","services","smartphone","UUID","write"],"articleSection":["Bluetooth","Wireless"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#respond"]}]},{"@type":"WebPage","@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction","url":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction","name":"BLE with the ESP32 \u2013 An Introduction &#8226; Wolles Elektronikkiste","isPartOf":{"@id":"https:\/\/wolles-elektronikkiste.de\/en#website"},"primaryImageOfPage":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#primaryimage"},"image":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#primaryimage"},"thumbnailUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_post_image.jpg","datePublished":"2026-08-06T13:57:14+00:00","dateModified":"2026-08-06T13:57:21+00:00","description":"After a brief overview, I will walk you through, step by step, how to use BLE (Bluetooth Low Energy) on ESP32-based boards.","breadcrumb":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#primaryimage","url":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_post_image.jpg","contentUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2026\/08\/ble_post_image.jpg","width":1000,"height":1000},{"@type":"BreadcrumbList","@id":"https:\/\/wolles-elektronikkiste.de\/en\/ble-with-the-esp32-an-introduction#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Startseite","item":"https:\/\/wolles-elektronikkiste.de\/en"},{"@type":"ListItem","position":2,"name":"BLE with the ESP32 \u2013 An Introduction"}]},{"@type":"WebSite","@id":"https:\/\/wolles-elektronikkiste.de\/en#website","url":"https:\/\/wolles-elektronikkiste.de\/en","name":"Wolles Elektronikkiste","description":"Die wunderbare Welt der Elektronik","publisher":{"@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/wolles-elektronikkiste.de\/en?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46","name":"Wolfgang Ewald","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png","url":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png","contentUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png","width":512,"height":512,"caption":"Wolfgang Ewald"},"logo":{"@id":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png"}}]}},"_links":{"self":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts\/26335","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/comments?post=26335"}],"version-history":[{"count":2,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts\/26335\/revisions"}],"predecessor-version":[{"id":26338,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts\/26335\/revisions\/26338"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/media\/26334"}],"wp:attachment":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/media?parent=26335"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/categories?post=26335"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/tags?post=26335"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}