Dans ce tutoriel, nous allons apprendre comment gérer et tester la communication BLE (Bluetooth Low Energy) sur un ESP32 avec MicroPython.
Matériels
- Un module ESP32
- Un ordinateur avec Python installé
- Câble USB pour la connexion ESP32-ordinateur
- Un appareil Android
Environnement et Configuration de l’IDE
Pour communiquer et programmer en Python votre ESP32, vous pouvez suivre ce tutoriel précédent pour utiliser MicroPython.
Vous pouvez également installer l’application BLE Terminal sur votre téléphone Android pour tester la communication BLE
Activation du Bluetooth LE
Pour activer le BLE de votre EPS32, copier le code micropython suivant dans l’éditeur Thonny IDE et exportez le sur votre module. Dans cet exemple, nous utilisons la librairie bluetooth (autre 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.: il est possible de récupérer l’adresse MAC au format hexadécimal en utilisant la fonction hex() ou la librairie ubinascii
Une fois le code chargé et l’annonce activé, vous pouvez retrouver l’appareil dans l’application BLE Terminal.
IMPORTANT: l’annonciation doit être activée après chaque déconnexion pour pouvoir reconnecter l’ESP32
Enregistrer des services
La communication BLE passe par le concept de services et caractéristiques avec certain droit d’accès (read, write, notify). Il existe des services par défaut (Nordic UART service (NUS), Heart rate (HR) ou vous pouvez créer vos propres services avec des identifiants uniques UUID.
Un service a un UUID unique et peut contenir différentes caractéristiques. Chaque caractéristique est définie par un UUID unique et différents droits d’accès.
- La fonction bluetooth.UUID permet de définir les adresses des services et caractéristiques
- bluetooth.FLAG_READ donne un accès en lecture au client
- bluetooth.FLAG_NOTIFY permet de notifier le client sans action de sa part
- bluetooth.FLAG_WRITE donne un accès en écriture au 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: l’annonciation (advertising) doit être stopper avant d’enregistrer des services
Définir les fonctions évènement
Pour gérer correctement le module BLE, nous allons créer des fonctions callback pour détecter et agir en fonction des différents évènements
Pour cela, nous avons besoin de connaitre les différents identifiant des évènements
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)
Nous pouvons ensuite utiliser définir les actions pour chaque évènement
#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)
Résultat
Avec ce code simple, vous pouvez vous connecter à l’appareil puis lire et écrire sur le service choisi dans l’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)
Création d’une classe MicroPython pour gérer la communication BLE ESP32
Il est intéressant de créer une classe pour gérer la communication BLE que vous pourrez réutiliser dans différents projets. Dans le code de la classe, vous retrouvez tous les éléments décrit plus haut
- initialisation et activation du BLE
- définition des fonctions callback self.ble_irq
- enregistrement des services self.register
- annonciation self.advertise
- Nous avons également rajouté une fonction de notification pour mettre à jour la valeur d’un capteur 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)
Code complet de gestion du BLE ESP32 avec 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()