In this tutorial, we’ll learn how to manage and test BLE (Bluetooth Low Energy) communication on an ESP32 with MicroPython.
Equipment
- An ESP32 module
- A computer with Python installed
- USB cable for ESP32-computer connection
- An Android device
IDE environment and configuration
To communicate and program your ESP32 in Python, you can follow this previous tutorial for using MicroPython.
You can also install the BLE Terminal application on your Android phone to test BLE communication.
Activation du Bluetooth LE
To activate BLE on your EPS32, copy the following micropython code into the Thonny IDE editor and export it to your module. In this example, we use the bluetooth library (alternative option ubluetooth)
import bluetooth #https://docs.micropython.org/en/latest/library/bluetooth.html import ubinascii def main(): BLE = bluetooth.BLE() BLE.active(True) #Advertise name = bytes("ESP32BLEmPy", 'UTF-8') adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name BLE.gap_advertise(100, adv_data) #get MAC address mac = BLE.config('mac')[1] print("device MAC address is: "+ubinascii.hexlify(mac).decode()) #print("device MAC address is: "+mac.hex()) if __name__ == "__main__": main()
N.B.: it is possible to retrieve the MAC address in hexadecimal format using the hex() function or the ubinascii library.
Once the code has been loaded and the announcement activated, you can find the device in the BLE Terminal application.
IMPORTANT: annunciation must be activated after each disconnection in order to reconnect the ESP32.
Register services
BLE communication is based on the concept of services and features with certain access rights (read, write, notify). There are default services (Nordic UART service (NUS), Heart rate (HR) or you can create your own services with unique UUID identifiers.
A service has a unique UUID and can contain different features. Each feature is defined by a unique UUID and different access rights.
- The bluetooth.UUID function is used to define service addresses and features.
- bluetooth.FLAG_READ gives read access to the client
- bluetooth.FLAG_NOTIFY notifies the customer without any action on his part
- bluetooth.FLAG_WRITE gives write access to the client
#register HR_UUID = bluetooth.UUID(0x180D) HR_CHAR = (bluetooth.UUID(0x2A37), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) HR_SERVICE = (HR_UUID, (HR_CHAR,),) UART_UUID = bluetooth.UUID('6E400001-B5A3-F393-E0A9-E50E24DCCA9E') UART_TX = (bluetooth.UUID('6E400003-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) UART_RX = (bluetooth.UUID('6E400002-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_WRITE,) UART_SERVICE = (UART_UUID, (UART_TX, UART_RX,),) MY_UUID = bluetooth.UUID("0bd62591-0b10-431a-982e-bd136821f35b") SEN_CHAR = (bluetooth.UUID("0bd62592-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) CMD_CHAR = (bluetooth.UUID("0bd62593-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_WRITE,) MY_SERVICE = (MY_UUID, (SEN_CHAR, CMD_CHAR,),) SERVICES = (HR_SERVICE, UART_SERVICE, MY_SERVICE) ( (hr,), (tx, rx,), (sen,cmd,), ) = BLE.gatts_register_services(SERVICES) BLE.gatts_write(sen, str(43.256), False) #init val to be read
N.B: advertising must be stopped before registering services.
Define event functions
To manage the BLE module correctly, we will create callback functions to detect and act on various events.
To do this, we need to know the different event identifiers
from micropython import const _IRQ_CENTRAL_CONNECT = const(1) _IRQ_CENTRAL_DISCONNECT = const(2) _IRQ_GATTS_WRITE = const(3) _IRQ_GATTS_READ_REQUEST = const(4) _IRQ_SCAN_RESULT = const(5) _IRQ_SCAN_DONE = const(6) _IRQ_PERIPHERAL_CONNECT = const(7) _IRQ_PERIPHERAL_DISCONNECT = const(8) _IRQ_GATTC_SERVICE_RESULT = const(9) _IRQ_GATTC_SERVICE_DONE = const(10) _IRQ_GATTC_CHARACTERISTIC_RESULT = const(11) _IRQ_GATTC_CHARACTERISTIC_DONE = const(12) _IRQ_GATTC_DESCRIPTOR_RESULT = const(13) _IRQ_GATTC_DESCRIPTOR_DONE = const(14) _IRQ_GATTC_READ_RESULT = const(15) _IRQ_GATTC_READ_DONE = const(16) _IRQ_GATTC_WRITE_DONE = const(17) _IRQ_GATTC_NOTIFY = const(18) _IRQ_GATTC_INDICATE = const(19) _IRQ_GATTS_INDICATE_DONE = const(20) _IRQ_MTU_EXCHANGED = const(21) _IRQ_L2CAP_ACCEPT = const(22) _IRQ_L2CAP_CONNECT = const(23) _IRQ_L2CAP_DISCONNECT = const(24) _IRQ_L2CAP_RECV = const(25) _IRQ_L2CAP_SEND_READY = const(26) _IRQ_CONNECTION_UPDATE = const(27) _IRQ_ENCRYPTION_UPDATE = const(28) _IRQ_GET_SECRET = const(29) _IRQ_SET_SECRET = const(30)
We can then use it to define actions for each event
#event callback function def ble_irq(event,data): if event == _IRQ_CENTRAL_CONNECT: # A central has connected to this peripheral. conn_handle, addr_type, addr = data print("BLE device connected successfully") elif event == _IRQ_CENTRAL_DISCONNECT: # A central has disconnected from this peripheral. conn_handle, addr_type, addr = data print("BLE device disconnected") adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name BLE.gap_advertise(100, adv_data) elif event == _IRQ_GATTS_WRITE: # A client has written to this characteristic or descriptor. conn_handle, attr_handle = data print("write event: ",BLE.gatts_read(data[1]).decode('UTF-8').strip()) elif event == _IRQ_GATTS_READ_REQUEST: # A client has issued a read. Note: this is only supported on STM32. # Return a non-zero integer to deny the read (see below), or zero (or None) # to accept the read. conn_handle, attr_handle = data print("read event: ",data)
Results
With this simple code, you can connect to the device and then read and write to the chosen service in the application
MicroPython v1.22.1 on 2024-01-05; Generic ESP32 module with ESP32 Type "help()" for more information. >>> %Run -c $EDITOR_CONTENT device MAC address is: 3c6105315f12 >>> BLE device connected successfully BLE device disconnected BLE device connected successfully write event: hello read event: (0, 31)
Creation of a MicroPython class to manage ESP32 BLE communication
It’s a good idea to create a class to manage BLE communication that you can reuse in different projects. In the class code, you’ll find all the elements described above
- BLE initialization and activation
- definition of callback functions self.ble_irq
- service registration self.register
- announcement self.advertise
- We’ve also added a notification function to update the value of a sensor self.set_sensor
class ESP32BLE(): def __init__(self, name): # Create BLE device management self.name = name self.ble = bluetooth.BLE() self.ble.active(True) #get MAC address mac = self.ble.config('mac')[1] print("device MAC address is: "+mac.hex()) self.ble.irq(self.ble_irq) self.connections = set() self.register() self.advertise() def ble_irq(self, event, data): #define event callback functions if event == _IRQ_CENTRAL_CONNECT: # A central has connected to this peripheral. conn_handle, addr_type, addr = data self.connections.add(conn_handle) print("BLE device connected successfully") elif event == _IRQ_CENTRAL_DISCONNECT: # A central has disconnected from this peripheral. conn_handle, addr_type, addr = data self.connections.remove(conn_handle) print("BLE device disconnected") self.advertise() elif event == _IRQ_GATTS_WRITE: # A client has written to this characteristic or descriptor. conn_handle, attr_handle = data print("write event: ",self.ble.gatts_read(data[1]).decode('UTF-8').strip()) elif event == _IRQ_GATTS_READ_REQUEST: # A client has issued a read. Note: this is only supported on STM32. # Return a non-zero integer to deny the read (see below), or zero (or None) # to accept the read. conn_handle, attr_handle = data print("read event: ",data) def register(self): #define services and characteristics HR_UUID = bluetooth.UUID(0x180D) HR_CHAR = (bluetooth.UUID(0x2A37), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) HR_SERVICE = (HR_UUID, (HR_CHAR,),) UART_UUID = bluetooth.UUID('6E400001-B5A3-F393-E0A9-E50E24DCCA9E') UART_TX = (bluetooth.UUID('6E400003-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) UART_RX = (bluetooth.UUID('6E400002-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_WRITE,) UART_SERVICE = (UART_UUID, (UART_TX, UART_RX,),) MY_UUID = bluetooth.UUID("0bd62591-0b10-431a-982e-bd136821f35b") SEN_CHAR = (bluetooth.UUID("0bd62592-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) CMD_CHAR = (bluetooth.UUID("0bd62593-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_WRITE,) MY_SERVICE = (MY_UUID, (SEN_CHAR, CMD_CHAR,),) SERVICES = (HR_SERVICE, UART_SERVICE, MY_SERVICE) ( (self.hr,), (self.tx, self.rx,), (self.sen,self.cmd,), ) = self.ble.gatts_register_services(SERVICES) self.ble.gatts_write(self.sen, str(43.256), False) def advertise(self): #advertise BLE module with name name = bytes(self.name, 'UTF-8') adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name self.ble.gap_advertise(100, adv_data) def set_sensor(self, data, notify=False): # Data is sint16 with a resolution of 0.01. self.ble.gatts_write(self.sen, str(data)) if notify: for conn_handle in self.connections: # Notify connected centrals to issue a read. self.ble.gatts_notify(conn_handle, self.sen)
BLE device disconnected BLE device connected successfully write event: hello read event: (65535, 31) read event: (0, 31) read event: (65535, 31) read event: (0, 31) read event: (65535, 31) read event: (0, 31) write event: hello World read event: (65535, 31) read event: (0, 31) read event: (65535, 31) read event: (0, 31)
Complete BLE ESP32 management code with micropython
import bluetooth #https://docs.micropython.org/en/latest/library/bluetooth.html import random import time from micropython import const _IRQ_CENTRAL_CONNECT = const(1) _IRQ_CENTRAL_DISCONNECT = const(2) _IRQ_GATTS_WRITE = const(3) _IRQ_GATTS_READ_REQUEST = const(4) _IRQ_SCAN_RESULT = const(5) _IRQ_SCAN_DONE = const(6) _IRQ_PERIPHERAL_CONNECT = const(7) _IRQ_PERIPHERAL_DISCONNECT = const(8) _IRQ_GATTC_SERVICE_RESULT = const(9) _IRQ_GATTC_SERVICE_DONE = const(10) _IRQ_GATTC_CHARACTERISTIC_RESULT = const(11) _IRQ_GATTC_CHARACTERISTIC_DONE = const(12) _IRQ_GATTC_DESCRIPTOR_RESULT = const(13) _IRQ_GATTC_DESCRIPTOR_DONE = const(14) _IRQ_GATTC_READ_RESULT = const(15) _IRQ_GATTC_READ_DONE = const(16) _IRQ_GATTC_WRITE_DONE = const(17) _IRQ_GATTC_NOTIFY = const(18) _IRQ_GATTC_INDICATE = const(19) _IRQ_GATTS_INDICATE_DONE = const(20) _IRQ_MTU_EXCHANGED = const(21) _IRQ_L2CAP_ACCEPT = const(22) _IRQ_L2CAP_CONNECT = const(23) _IRQ_L2CAP_DISCONNECT = const(24) _IRQ_L2CAP_RECV = const(25) _IRQ_L2CAP_SEND_READY = const(26) _IRQ_CONNECTION_UPDATE = const(27) _IRQ_ENCRYPTION_UPDATE = const(28) _IRQ_GET_SECRET = const(29) _IRQ_SET_SECRET = const(30) class ESP32BLE(): def __init__(self, name): # Create BLE device management self.name = name self.ble = bluetooth.BLE() self.ble.active(True) #get MAC address mac = self.ble.config('mac')[1] print("device MAC address is: "+mac.hex()) self.ble.irq(self.ble_irq) self.connections = set() self.register() self.advertise() def ble_irq(self, event, data): #define event callback functions if event == _IRQ_CENTRAL_CONNECT: # A central has connected to this peripheral. conn_handle, addr_type, addr = data self.connections.add(conn_handle) print("BLE device connected successfully") elif event == _IRQ_CENTRAL_DISCONNECT: # A central has disconnected from this peripheral. conn_handle, addr_type, addr = data self.connections.remove(conn_handle) print("BLE device disconnected") self.advertise() elif event == _IRQ_GATTS_WRITE: # A client has written to this characteristic or descriptor. conn_handle, attr_handle = data print("write event: ",self.ble.gatts_read(data[1]).decode('UTF-8').strip()) elif event == _IRQ_GATTS_READ_REQUEST: # A client has issued a read. Note: this is only supported on STM32. # Return a non-zero integer to deny the read (see below), or zero (or None) # to accept the read. conn_handle, attr_handle = data print("read event: ",data) def register(self): #define services and characteristics HR_UUID = bluetooth.UUID(0x180D) HR_CHAR = (bluetooth.UUID(0x2A37), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) HR_SERVICE = (HR_UUID, (HR_CHAR,),) UART_UUID = bluetooth.UUID('6E400001-B5A3-F393-E0A9-E50E24DCCA9E') UART_TX = (bluetooth.UUID('6E400003-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) UART_RX = (bluetooth.UUID('6E400002-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_WRITE,) UART_SERVICE = (UART_UUID, (UART_TX, UART_RX,),) MY_UUID = bluetooth.UUID("0bd62591-0b10-431a-982e-bd136821f35b") SEN_CHAR = (bluetooth.UUID("0bd62592-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,) CMD_CHAR = (bluetooth.UUID("0bd62593-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_WRITE,) MY_SERVICE = (MY_UUID, (SEN_CHAR, CMD_CHAR,),) SERVICES = (HR_SERVICE, UART_SERVICE, MY_SERVICE) ( (self.hr,), (self.tx, self.rx,), (self.sen,self.cmd,), ) = self.ble.gatts_register_services(SERVICES) self.ble.gatts_write(self.sen, str(43.256), False) def advertise(self): #advertise BLE module with name name = bytes(self.name, 'UTF-8') adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name self.ble.gap_advertise(100, adv_data) def set_sensor(self, data, notify=False): # Data is sint16 with a resolution of 0.01. self.ble.gatts_write(self.sen, str(data)) if notify: for conn_handle in self.connections: # Notify connected centrals to issue a read. self.ble.gatts_notify(conn_handle, self.sen) def main(): BLE = ESP32BLE("ESP32BLEmPy") #simulate sensor sensorVal=43.256 i=0 while True: # Write every second, notify every 10 seconds. i = (i + 1) % 10 BLE.set_sensor(sensorVal, notify=i == 0) # Random walk the temperature. sensorVal += random.uniform(-1.5, 1.5) time.sleep_ms(1000) if __name__ == "__main__": main()