Neste tutorial, aprenderemos a gerenciar e testar a comunicação BLE (Bluetooth Low Energy) em um ESP32 usando MicroPython.
Equipamento
- Um módulo ESP32
- Um computador com Python instalado
- Cabo USB para ligação ao computador ESP32
- Um dispositivo Android
Configuração do ambiente e do IDE
Para comunicar e programar o seu ESP32 em Python, pode seguir este tutorial anterior para utilizar o MicroPython.
Também pode instalar a aplicação bluetoothbleterminal.free&hl=fr&gl=US’>Terminal BLE no seu telemóvel Android para testar a comunicação BLE.
Ativação do Bluetooth LE
Para ativar o BLE no seu EPS32, copie o seguinte código micropython para o editor do Thonny IDE e exporte-o para o seu módulo. Neste exemplo, utilizamos a biblioteca bluetooth (outra opção 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.: é possível obter o endereço MAC em formato hexadecimal utilizando a função hex() ou a biblioteca ubinascii.
Uma vez carregado o código e ativado o anúncio, pode encontrar o dispositivo na aplicação Terminal BLE.
IMPORTANTE: o aviso deve ser ativado após cada desconexão para voltar a ligar o ESP32.
Registo de serviços
A comunicação BLE baseia-se no conceito de serviços e características com determinados direitos de acesso (leitura, escrita, notificação). Existem serviços predefinidos (serviço Nordic UART (NUS), ritmo cardíaco (HR)) ou pode criar os seus próprios serviços com identificadores UUID únicos.
Um serviço tem um UUID único e pode conter diferentes características. Cada caraterística é definida por um UUID único e diferentes direitos de acesso.
- A função bluetooth.UUID é utilizada para definir os endereços dos serviços e funcionalidades.
- bluetooth.FLAG_READ dá acesso de leitura ao cliente
- bluetooth.FLAG_NOTIFY notifica o cliente sem qualquer ação da sua parte
- bluetooth.FLAG_WRITE dá acesso de escrita ao 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.: a publicidade deve ser interrompida antes do registo dos serviços.
Definição de funções de eventos
Para gerir corretamente o módulo BLE, vamos criar funções de retorno de chamada para detetar e atuar em vários eventos
Para tal, é necessário conhecer os diferentes 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)
Podemos então utilizar isto para definir as acções para 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
Com este código simples, pode ligar-se ao dispositivo e, em seguida, ler e escrever no serviço escolhido na aplicação
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)
Criação de uma classe MicroPython para gerir a comunicação BLE do ESP32
Vale a pena criar uma classe para gerir a comunicação BLE que possa ser reutilizada em diferentes projectos. No código da classe, encontrará todos os elementos descritos acima
- Inicialização e ativação BLE
- definição das funções de retorno de chamada self.ble_irq
- registo de serviços self.register
- anúncio self.advertise
- Também adicionámos uma função de notificação para atualizar o valor de um 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 gestão do ESP32 BLE com 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()