MicroPython: ESP32 – Getting Started with Bluetooth Low Energy (BLE)

This tutorial is a getting started guide to Bluetooth Low Energy (BLE) with the ESP32 programmed with MicroPython firmware. We’ll introduce you to BLE basic concepts and run some simple examples: advertise and expose data to be read by other BLE devices; and detect when another BLE device writes some data on the ESP32 characteristics.

ESP32 with MicroPython Getting Started with Bluetooth Low Energy BLE

Are you using Arduino IDE? Follow this tutorial instead: ESP32 with Bluetooth and Bluetooth Low Energy: The Ultimate Guide

Prerequisites

Before proceeding with this tutorial, make sure you check the following prerequisites.

MicroPython Firmware

micorpython logo

To follow this tutorial you need MicroPython firmware installed in your ESP32 or ESP8266 boards. You also need an IDE to write and upload the code to your board. We suggest using Thonny IDE:

Learn more about MicroPython: MicroPython Programming with ESP32 and ESP8266

Introducing Bluetooth Low Energy (BLE)

Before diving into the examples, we’ll explain some basic concepts about Bluetooth and Bluetooth Low Energy.

What is Bluetooth?

Introducing Bluetooth Low Energy BLE

Bluetooth is a wireless technology that enables devices to communicate over short distances. This variant of Bluetooth is also referred to as “Bluetooth Classic” or simply “Bluetooth”. It was designed for high-speed data transmission and it’s commonly used for connecting devices like headphones to phones, linking a keyboard or mouse to a computer, or transferring files between devices.

What is Bluetooth Low Energy?

Bluetooth Low Energy, BLE for short (also called Bluetooth Smart), is a power‑conserving variant of Bluetooth. BLE’s primary application is short-distance transmission of small amounts of data (low bandwidth). Unlike Bluetooth which is always on, BLE remains in sleep mode constantly except when a connection is initiated. This makes it consume very little power. BLE consumes approximately 100x less power than Bluetooth (depending on the use case).

Bluetooth Classic vs Bluetooth Low Energy

So, what are the main differences between Bluetooth Classic and Bluetooth Low Energy?

Bluetooth Classic is known for higher data transfer rates, making it suitable for applications like audio streaming and file transfer. It consumes more power, making it less ideal for battery-operated devices. Usually, Bluetooth Classic is easier to understand and implement for beginners, while Bluetooth Low Energy might take a little more time to understand basic concepts. That’s why many people still prefer using Bluetooth Classic instead of BLE in their IoT projects.

On the other hand, BLE (Bluetooth Low Energy) is designed for low power consumption, making it perfect for devices like IoT gadgets and wearables, and is also a great solution for the ESP32 in IoT and Home Automation applications. BLE operates with lower data transfer rates but is energy-efficient and works well in short-range scenarios.

Another big difference between the two versions of Bluetooth is the way to transfer data. Bluetooth Classic uses something similar to Serial Communication (Serial Port Profile), while Bluetooth Low Energy uses a client-server model, where it employs the GATT (Generic Attribute Profile) to structure data.

You can check this website to learn in more detail about the main differences between Bluetooth and Bluetooth Low Energy.

 Bluetooth ClassicBluetooth Low Energy (BLE)
Power ConsumptionHigher power consumptionLow power consumption
Data Transfer RateHigher data transfer ratesLower data transfer rates
RangeLonger rangeShorter range
ApplicationsAudio streaming, file transferIoT devices, wearables, smart home
Data TransferSerial Port Profile (SPP)Generic Attribute Profile (GATT)

Bluetooth Low Energy Basic Concepts

Before proceeding, it’s important to get familiar with some basic BLE concepts.

BLE Peripheral and Controller (Central Device)

When using Bluetooth Low Energy (BLE), it’s important to understand the roles of BLE Peripheral and BLE Controller (also referred to as the Central Device).

Bluetooth Low Energy Basic Concepts Example Overivew

The ESP32 can act either as a Peripheral or as a central device. When it acts as a peripheral it sets up a GATT profile and advertises its service with characteristics that the central devices can read or interact with. On the other hand, when it is set as a central device, it can connect to other BLE devices to read or interact with their profiles and read their characteristics.

In the above diagram, the ESP32 takes the role of the BLE Peripheral, serving as the device that provides data or services. Your smartphone or computer acts as the BLE Controller, managing the connection and communication with the ESP32.

BLE Server and Client

With Bluetooth Low Energy, there are two types of devices: the server and the client. The ESP32 can act either as a client or as a server. In the picture below it acts as a server, exposing its GATT structure containing data. The BLE Server acts as a provider of data or services, while the BLE Client consumes or uses these services.

Bluetooth Low Energy Basic Concepts BLE Server and Client

The server advertises its existence, so it can be found by other devices and contains data that the client can read or interact with. The client scans the nearby devices, and when it finds the server, it is looking for, it establishes a connection and can interact with that device by reading or writing on its characteristics.

The BLE server is basically the BLE peripheral before establishing a connection. The BLE Client is the BLE controller before establishing a connection. Many times, these terms are used interchangeably.

GATT

GATT, which stands for Generic Attribute Profile, is a fundamental concept in Bluetooth Low Energy (BLE) technology. Essentially, it serves as a blueprint for how BLE devices communicate with each other. Think of it as a structured language that two BLE devices use to exchange information seamlessly.

GATT Generic Attribute Profile Bluetooth Low Energy (BLE) technology
  • Profile: standard collection of services for a specific use case;
  • Service: collection of related information, like sensor readings, battery level, heart rate, etc.;
  • Characteristic: it is where the actual data is saved on the hierarchy (value);
  • Descriptor: metadata about the data;
  • Properties: describe how the characteristic value can be interacted with. For example: read, write, notify, broadcast, indicate, etc.

Let’s take a more in-depth look at the BLE Service and Characteristics.

BLE Service

The top level of the hierarchy is a profile and is composed of one or more services. Usually, a BLE device contains more than one service, like battery service and heart rate service.

Every service contains at least one characteristic. There are predefined services for several types of data defined by the SIG (Bluetooth Special Interest Group) like: Battery Level, Blood Pressure, Heart Rate, Weight Scale, Environmental Sensing, etc. You can check on the following link for the predefined services:

UUID

A UUID is a unique digital identifier used in BLE and GATT to distinguish and locate services, characteristics, and descriptors. It’s like a distinct label that ensures every component in a Bluetooth device has a unique name.

Each service, characteristic, and descriptor has a UUID (Universally Unique Identifier). A UUID is a unique 128-bit (16 bytes) number. For example:

55072829-bc9e-4c53-938a-74a6d4c78776

There are shortened and default UUIDs for services, and characteristics specified in the SIG (Bluetooth Special Interest Group). This means, that if you have a BLE device that uses the default UUIDs for its services and characteristics, you’ll know exactly how to interact with that device to get or interact with the information you’re looking for.

You can also generate your own custom UUIDs if you don’t want to stick with predefined values or if the data you’re exchanging doesn’t fit in any of the categories. You can generate custom UUIDs using this UUID generator website.

Communication between BLE Devices

Here are the usual steps that describe the communication between BLE Devices.

ESP32 BLE Server and Client Communicaton
  1. The BLE Peripheral (server) advertises its existence (ESP32).
  2. The BLE Central Device (client) scans for BLE devices.
  3. When the central device finds the peripheral it is looking for, it connects to it.
  4. After connecting, it reads the GATT profile of the peripheral and searches for the service it is looking for (for example: environmental sensing).
  5. If it finds the service, it can now interact with the characteristics. For example, reading the temperature value.

Installing the aioble Package

To write code to use Bluetooth with the ESP32, we’ll install the aioble package—that’s currently the recommended library for BLE communication with MicroPython.

Before proceeding to the actual examples, you need to install it on your board.

  1. Connect the board (with MicroPython installed—check the prerequisites) to your computer and connect it to Thonny IDE.
  2. On Thonny IDE, go to Tools > Manage Packages…
  3. Search for aioble and click on the aioble option.
Installing aioble Bluetooth Library MicroPython ESP32

Finally, click the Install button.

Installing aioble Bluetooth Library MicroPython ESP32

Wait a few seconds while it installs. After installing, you can proceed to the examples.

The ESP32 as a BLE Peripheral

In this example, we’ll set the ESP32 as a BLE Peripheral. We’ll show you how to advertise services and change the value of characteristics and how to detect if another BLE device wrote on the ESP32 characteristics. Here’s a quick breakdown of how the project works:

  • In this example, the ESP32 will act as a BLE Peripheral/BLE Server that advertises its existence.
  • The ESP32 GATT structure will have one service with two characteristics. One characteristic (let’s call it sensor characteristic) will be the place to save a value that changes over time (like sensor readings).
  • The ESP32 will write a new value to the sensor characteristic periodically.
  • The other characteristic (let’s call it LED characteristic) will be the place to save the state of a GPIO. By changing the value of that characteristic, we’ll be able to control an LED connected to that GPIO.
  • We’ll use an app called nRF Connect to connect to the ESP32 BLE device to read the sensor characteristic and write on the LED characteristic to control the onboard LED.
The ESP32 as a BLE Peripheral

ESP32 BLE Peripheral MicroPython Code

Copy the following code to Thonny IDE.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp32-bluetooth-low-energy-ble/

from micropython import const
import asyncio
import aioble
import bluetooth
import struct
from machine import Pin
from random import randint

# Init LED
led = Pin(2, Pin.OUT)
led.value(0)

# Init random value
value = 0

# See the following for generating UUIDs:
# https://www.uuidgenerator.net/
_BLE_SERVICE_UUID = bluetooth.UUID('19b10000-e8f2-537e-4f6c-d104768a1214')
_BLE_SENSOR_CHAR_UUID = bluetooth.UUID('19b10001-e8f2-537e-4f6c-d104768a1214')
_BLE_LED_UUID = bluetooth.UUID('19b10002-e8f2-537e-4f6c-d104768a1214')
# How frequently to send advertising beacons.
_ADV_INTERVAL_MS = 250_000

# Register GATT server, the service and characteristics
ble_service = aioble.Service(_BLE_SERVICE_UUID)
sensor_characteristic = aioble.Characteristic(ble_service, _BLE_SENSOR_CHAR_UUID, read=True, notify=True)
led_characteristic = aioble.Characteristic(ble_service, _BLE_LED_UUID, read=True, write=True, notify=True, capture=True)

# Register service(s)
aioble.register_services(ble_service)

# Helper to encode the data characteristic UTF-8
def _encode_data(data):
    return str(data).encode('utf-8')

# Helper to decode the LED characteristic encoding (bytes).
def _decode_data(data):
    try:
        if data is not None:
            # Decode the UTF-8 data
            number = int.from_bytes(data, 'big')
            return number
    except Exception as e:
        print("Error decoding temperature:", e)
        return None

# Get sensor readings
def get_random_value():
    return randint(0,100)

# Get new value and update characteristic
async def sensor_task():
    while True:
        value = get_random_value()
        sensor_characteristic.write(_encode_data(value), send_update=True)
        print('New random value written: ', value)
        await asyncio.sleep_ms(1000)
        
# Serially wait for connections. Don't advertise while a central is connected.
async def peripheral_task():
    while True:
        try:
            async with await aioble.advertise(
                _ADV_INTERVAL_MS,
                name="ESP32",
                services=[_BLE_SERVICE_UUID],
                ) as connection:
                    print("Connection from", connection.device)
                    await connection.disconnected()             
        except asyncio.CancelledError:
            # Catch the CancelledError
            print("Peripheral task cancelled")
        except Exception as e:
            print("Error in peripheral_task:", e)
        finally:
            # Ensure the loop continues to the next iteration
            await asyncio.sleep_ms(100)

async def wait_for_write():
    while True:
        try:
            connection, data = await led_characteristic.written()
            print(data)
            print(type)
            data = _decode_data(data)
            print('Connection: ', connection)
            print('Data: ', data)
            if data == 1:
                print('Turning LED ON')
                led.value(1)
            elif data == 0:
                print('Turning LED OFF')
                led.value(0)
            else:
                print('Unknown command')
        except asyncio.CancelledError:
            # Catch the CancelledError
            print("Peripheral task cancelled")
        except Exception as e:
            print("Error in peripheral_task:", e)
        finally:
            # Ensure the loop continues to the next iteration
            await asyncio.sleep_ms(100)
            
# Run tasks
async def main():
    t1 = asyncio.create_task(sensor_task())
    t2 = asyncio.create_task(peripheral_task())
    t3 = asyncio.create_task(wait_for_write())
    await asyncio.gather(t1, t2)
    
asyncio.run(main())

View raw code

How the Code Works

Let’s take a quick look at the relevant parts of the code for this example.

Include Libraries

You need to include the aioble and the bluetooth libraries to use Bluetooth with the ESP32. We also import the randint method from the random module to generate random numbers.Our code will be asynchronous. For that, we’ll use the asyncio library.

from micropython import const
import asyncio
import aioble
import bluetooth
import struct
from machine import Pin
from random import randint

Define UUIDs and Register the GATT Service and Characteristic

We define the UUIDs for the service and its characteristics. One characteristic will hold the LED value and another for the hypothetical sensor reading (in this case, a random value).

_BLE_SERVICE_UUID = bluetooth.UUID('19b10000-e8f2-537e-4f6c-d104768a1214')
_BLE_SENSOR_CHAR_UUID = bluetooth.UUID('19b10001-e8f2-537e-4f6c-d104768a1214')
_BLE_LED_UUID = bluetooth.UUID('19b10002-e8f2-537e-4f6c-d104768a1214')

Those UUIDs were created using the uuidgenerator website. You can generate your own UUIDs for your application, but for this example, we recommend using the same UUIDs we’re using.

Then, register the GATT service and characteristics.

# Register GATT server, the service and characteristics
ble_service = aioble.Service(_BLE_SERVICE_UUID)
sensor_characteristic = aioble.Characteristic(ble_service, _BLE_SENSOR_CHAR_UUID, read=True, notify=True)
led_characteristic = aioble.Characteristic(ble_service, _BLE_LED_UUID, read=True, write=True, notify=True, capture=True)

# Register service(s)
aioble.register_services(ble_service)

When creating the sensor_characteristic, we set the read and notify arguments to True. This defines the way that the central device can interact with the characteristic. It can read the characteristic and be notified when it changes.

For the led_characteristic, we have an additional property. The capture property set to True. This indicates that other BLE devices can write to that characteristic—this is how other devices can control the ESP32 LED.

led_characteristic = aioble.Characteristic(ble_service, _BLE_LED_UUID, read=True, write=True, notify=True, capture=True)

Encode and Decode Data

The data to be written on the characteristic needs to be in a specific format. The _encode_data() function converts the data to UTF-8 format.

# Helper to encode the data characteristic UTF-8
def _encode_data(data):
    return str(data).encode('utf-8')

When other devices write to the led_characteristic, the data will be in byte format. The following function converts the bytes to an integer.

# Helper to decode the LED characteristic encoding (bytes).
def _decode_data(data):
    try:
        if data is not None:
            # Decode the UTF-8 data
            number = int.from_bytes(data, 'big')
            return number
    except Exception as e:
        print("Error decoding temperature:", e)
        return None

Get a New Value and Write on Characteristic

The sensor_task() is an asynchronous function that gets a new random value and writes on the characteristic using the write() method on the sensor_characteristic.This task is repeated continuously every second. You can adjust the delay time as needed.

# Get new value and update characteristic
async def sensor_task():
    while True:
        value = get_random_value()
        sensor_characteristic.write(_encode_data(value), send_update=True)
        #print('New random value written: ', value)
        await asyncio.sleep_ms(1000)

The get_random_value() function, in a real-world scenario, should be replaced with a function that will get sensor data—like temperature from a DS18B20 temperature sensor, for example.

Advertising

Besides writing to the sensor characteristic, we also need to advertise the ESP32 as a BLE service. For that, we use the peripheral_task() function.

# Serially wait for connections. Don't advertise while a central is connected.
async def peripheral_task():
    while True:
        try:
            async with await aioble.advertise(
                _ADV_INTERVAL_MS,
                name="ESP32",
                services=[_BLE_SERVICE_UUID],
                ) as connection:
                    print("Connection from", connection.device)
                    await connection.disconnected()             
        except asyncio.CancelledError:
            # Catch the CancelledError
            print("Peripheral task cancelled")
        except Exception as e:
            print("Error in peripheral_task:", e)
        finally:
            # Ensure the loop continues to the next iteration
            await asyncio.sleep_ms(100)

In that function, we define the BLE device name (‘ESP32‘). You can change its name if you want to. But, to follow our examples, we recommend that you’ll leave that name.

Waiting for a Write

In the wait_for_write() function, we’re continuously checking if the led_characteristic was written on. When it is, we decode the data and turn the ESP32 onboard LED on or off accordingly.

async def wait_for_write():
    while True:
        try:
            connection, data = await led_characteristic.written()
            print(data)
            print(type)
            data = _decode_data(data)
            print('Connection: ', connection)
            print('Data: ', data)
            if data == 1:
                print('Turning LED ON')
                led.value(1)
            elif data == 0:
                print('Turning LED OFF')
                led.value(0)
            else:
                print('Unknown command')
        except asyncio.CancelledError:
            # Catch the CancelledError
            print("Peripheral task cancelled")
        except Exception as e:
            print("Error in peripheral_task:", e)
        finally:
            # Ensure the loop continues to the next iteration
            await asyncio.sleep_ms(100)

Main Function

Finally, we create an asynchronous main() function, where we’ll write the base for our code. We create three asynchronous tasks: one for advertising, another to write on the sensor characteristic, and finally, another one to control the LED when other devices write on the led_characteristic.

async def main():
    t1 = asyncio.create_task(sensor_task())
    t2 = asyncio.create_task(peripheral_task())
    t3 = asyncio.create_task(wait_for_write())
    await asyncio.gather(t1, t2)

Finally, we run the code as follows.

asyncio.run(main())

Testing the Code

Run the previous code on the ESP32. It will start writing the temperature on the sensor characteristic and it will advertise its service.

To connect to this peripheral, read its sensor characteristic, and write to the led characteristic, we’ll use the nRF connect app. You can also use our Web BLE app (only works with Android and Windows computers).

nRF Connect App

The nRF Connect app from Nordic works on Android (Google Play Store) and iOS (App Store). Go to Google Play Store or App Store, search for “nRF Connect for Mobile” and install the app on your smartphone.

nRF Connect for Mobile

Go to your smartphone, open the nRF Connect app from Nordic, and start scanning for new devices. You should find a device called ESP32—this is the BLE server name you defined earlier.

nRF Connect app from Nordic connecting to ESP32

Connect to the ESP32 device. On Thonny IDE, you’ll see that it detected a new connection.

Thonny IDE MicroPython connection from device

You’ll see that it displays the service with the UUID we defined in the code and that it contains two characteristics with the UUIDs we defined previously.

Reading the Characteristic

On the sensor characteristic, click on the arrows to read the characteristic and activate the notifications. Then, click on the icon to change the data format (set to UTF-8)—on Android devices you don’t need to change the format.

Reading the BLE Characteristic

It will start displaying the random values on the Value field. It is updated every second.

Reading the BLE Characteristic Value

Writing to the Characteristic

To write to the LED characteristic and control the LED, click on the upper arrow on the LED characteristic.

Writing to the BLE Characteristic value

Select the UnsignedInt or Bool type. Then, write 1 or 0 to turn the LED on or off. 1 turns the LED on and 0 turns the LED off.

The ESP32 will notice that the other device wrote on the LED characteristic and will read its new value.

Receive the BLE Characteristic value Thonny IDE control LED

According to the value written, it will turn the LED on or off.

Alternatively, you can use our Web BLE app to connect to the ESP32 BLE device. It works on Google Chrome on Windows and Android devices.

Web BLE app ESP32 web browser demonstration

Web Bluetooth (also sometimes referred to as Web BLE) is a relatively recent technology that allows you to connect and control BLE-enabled devices, like the ESP32 or Raspberry Pi Pico, directly from your web browser using JavaScript. You can follow this guide to learn more: Getting Started with ESP32 Web Bluetooth (BLE).

Wrapping Up

In this tutorial, you learned the basics of using Bluetooth Low Energy with the ESP32 programmed with MicroPython. We’ve shown you an example of setting the ESP32 as a peripheral device that exposes a GATT structure. In an upcoming tutorial, we’ll show you how to set the ESP32 as a central device to interact with BLE peripherals.

If you want to learn more about Bluetooth with the ESP32 programmed with Arduino IDE, you can follow the next tutorials instead:

We hope you’ve found this tutorial useful. If you want to learn more about MicroPython, check out our resources:

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

2 thoughts on “MicroPython: ESP32 – Getting Started with Bluetooth Low Energy (BLE)”

  1. Hi Sara, many thanks for this excellent description of BLE. I have not yet attempted BLE and use ESPNOW quite a lot. Do you have an idea of the difference in range between ESPNOW, BLE and standard Bluetooth?

    Reply
  2. Excellent article. By the way, the code works perfectly on the Raspberry Pi Pico W with only one tiny edit: change pin number from 2 to LED. On my iPhone, I used the LightBlue app and was able to get the numbers and control the LED.

    Reply

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.