,En este tutorial, aprenderemos a gestionar y probar la comunicación BLE (Bluetooth Low Energy) en un ESP32 usando MicroPython.
Equipamiento
- Un módulo ESP32
- Un ordenador con Python instalado
- Cable USB para conexión ESP32-ordenador
- Un dispositivo Android
Configuración del entorno y del IDE
Para comunicar y programar tu ESP32 en Python, puedes seguir este tutorial anterior para usar MicroPython.
También puedes instalar la aplicación BLE Terminal en tu teléfono Android para probar la comunicación BLE.
Activación de Bluetooth LE
Para habilitar BLE en tu EPS32, copia el siguiente código micropython en el editor Thonny IDE y expórtalo a tu módulo. En este ejemplo, usamos la librería bluetooth (otra opción 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.: es posible recuperar la dirección MAC en formato hexadecimal utilizando la función hex() o la biblioteca ubinascii.
Una vez cargado el código y activado el anuncio, puedes encontrar el dispositivo en la aplicación Terminal BLE.
IMPORTANTE: después de cada desconexión debe activarse la anunciación para volver a conectar el ESP32.
Registro de servicios
La comunicación BLE se basa en el concepto de servicios y características con determinados derechos de acceso (lectura, escritura, notificación). Existen servicios por defecto (Nordic UART service (NUS), Heart rate (HR) o puedes crear tus propios servicios con identificadores UUID únicos.
Un servicio tiene un UUID único y puede contener diferentes características. Cada característica se define mediante un UUID único y diferentes derechos de acceso.
- La función bluetooth.UUID se utiliza para definir las direcciones de los servicios y funciones.
- bluetooth.FLAG_READ da acceso de lectura al cliente
- bluetooth.FLAG_NOTIFY notifica al cliente sin ninguna acción por su parte
- bluetooth.FLAG_WRITE da acceso de escritura al cliente
#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: la publicidad debe detenerse antes de registrar los servicios.
Definición de funciones de eventos
Para gestionar correctamente el módulo BLE, vamos a crear funciones callback para detectar y actuar sobre varios eventos
Para ello, necesitamos conocer los distintos identificadores de eventos
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)
A continuación, podemos utilizarlo para definir las acciones de cada evento
#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)
Resultados
Con este sencillo código, puede conectarse al dispositivo y, a continuación, leer y escribir en el servicio elegido en la aplicación
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)
Creación de una clase MicroPython para gestionar la comunicación ESP32 BLE
Merece la pena crear una clase para gestionar la comunicación BLE que puedas reutilizar en diferentes proyectos. En el código de la clase, encontrarás todos los elementos descritos anteriormente
- Inicialización y activación de BLE
- definición de funciones callback self.ble_irq
- registro de servicios self.register
- anuncio auto.anunciar
- También hemos añadido una función de notificación para actualizar el valor de un 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)
Código completo de gestión de ESP32 BLE con 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()