Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Idiomaschevron-right
C++chevron-right

Adote BLE: implementando sensores BLE com MCU Devkits

Jorge D. Ortiz-Fuentes13 min read • Published Nov 28, 2023 • Updated Apr 02, 2024
PythonC++
APLICATIVO COMPLETO
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
No primeiro capítulo desta série, compartilhei com você o projeto que planejo implementar. Apliquei o planejamento inicial e apresentei uma seleção de Placas de devkit MCU que seriam adequadas para nossos propósitos.
Neste episódio, tentarei implementar a comunicação BLE em uma das placas. Como a ideia é implementar esse projeto como se fosse uma prova de conceito (PoC), quando eu for moderadamente bem-sucedido com uma implementação, pararei por aqui e avançará para a próxima etapa, que é implementar a função central do BLE no Raspberry Pi.
Já cuidou dos preparativos? Em seguida, prepare-se para ver o Bluetooth que está por vir!

Sumário

Conceitos

BLE ao invés de BLE

Bluetooth é uma tecnologia para comunicações sem fio. Embora falemos sobre Bluetooth como se fosse uma única coisa, Bluetooth Classic e Bluetooth Low Energy são em sua maioria feras diferentes e também incompatíveis. O Bluetooth Classic tem uma taxa de transferência mais alta (até 3Mb/s) do que o Bluetooth Low Energy (até 2Mb/s), mas com ótima taxa de transferência vem um grande consumo de energia (como o tio do Aranha costumava dizer). Connection Existem vários usos predefinidos do Bluetooth Classic, abstraídos como perfis. A maioria deles é orientada para streaming contínuo de dados, como os usados por fones de ouvido de áudio, dispositivos de chamada viva-voz ou até mesmo aplicativos de transferência de arquivos. O Bluetooth Low Energy tem uma latência menor do que sua contraparte mais antiga e permite topologias de rede adicionais (transmissão e malha). Esses recursos o tornam uma ótima opção para aplicativos de IoT e saúde e até mesmo beacons ou tags onde o volume de dados geralmente é menor, mas os usuários são mais sensíveis ao tempo de resposta.
Neste artigo, usaremos o Windows de baixa energia para implementar nossos sensores.

dados BLE

Ao contrário do WiFi e do TCP/IP, o Bluetooth é muito mais específico em seus usos esperados e em como os dados devem ser codificados e decodificados. Essa quantidade de definição facilita a interoperação de dispositivos de diferentes fornecedores para o aplicativo esperado. No entanto, isso também significa que o desenvolvedor deve Go as especificações, entender os requisitos para o uso pretendido e ter um ótimo conjunto de testes.
Os dados expostos por um dispositivo BLE são organizados em atributos que são o átomo da informação, o bloco de construção desse padrão. Cada atributo tem um UUID 16-bit predefinido (Atributos Adotados pelo SIG) ou um 128-bit UUID, se for definido de forma personalizada. Na verdade, os de 16bits são apenas uma forma simplificada de se referir aos UUIDs de 128bits, onde o restante dos bits é constante e reservado para o Bluetooth Special Interest Group (SIG).
Dentro de um servidor, os atributos também têm um identificador de atributo. É um número que identifica esse atributo para esse tipo de servidor. Os atributos também podem ter permissões que definem se podem ser lidos, gravados, notificados (pushed) e/ou indicados (push com confirmação) e o nível de segurança necessário para cada operação. O Attribute Protocol (ATT) define como os elementos de dados são representados e transferidos no BLE. Nossas medições de nível de ruído serão um atributo padrão, com um identificador que dependerá de onde esse atributo foi definido em relação a outros no mesmo servidor Bluetooth. As medições do nível de ruído podem ser lidas e notificadas, mas não importa o quanto tentemos, elas não podem ser gravadas.
Os atributos são organizados hierarquicamente e sua estrutura é exposta utilizando a representação definida no Protocolo de Atributo. Uma vez que a conexão é estabelecida, o Perfil de Atributo Genérico (GATT) é usado exclusivamente e define os tipos de atributos e como eles são usados para permitir que dois dispositivos BLE interajam entre si. O GATT utiliza o ATT para descrever cada um dos componentes dessa hierarquia: perfis, serviços, características e descritores. Os perfis definem todas as funções de comunicação de um dispositivo BLE como um conjunto de um ou mais serviços; eles representam possíveis casos de uso do dispositivo. Em seguida, os serviços são um tipo de atributo que define um grupo de uma ou mais características que estão de alguma forma relacionadas. No nível mais baixo da hierarquia de dados, características são elementos de dados individuais expostos pelo servidor que contêm outros atributos, como as propriedades (por exemplo, permissões) ou descritores (por exemplo, condições para quando uma características devem ser notificadas).

funções BLE

O perfil de acesso genérico (GAP) anuncia o dispositivo e controla as conexões. Ela define as funções dos dispositivos. The Gap envia dados de publicidade por meio do Carga útil de dados de publicidade ou por meio do Carga útil de resposta de digitalização. O primeiro é obrigatório e é transmitido em intervalos regulares, enquanto o segundo deve ser solicitado pelo dispositivo de escaneamento.
Há quatro funções em que um dispositivo BLE pode operar:
  • A transmissão anuncia a si mesma e não se destina a estar conectada.
  • O Observer procura pacotes de publicidade, mas não se conecta.
  • O periférico se anuncia e escuta as conexões de uma central, agindo como um servidor.
  • A Central procura por periféricos e inicia uma conexão atuando como um cliente.
Deixe-me detalhar as responsabilidades do servidor versus o cliente. Servidor é o dispositivo que expõe os dados que controla ou contém e talvez algumas configurações. Ele aceita comandos recebidos e envia respostas, notificações e indicações. Nossas painéis MCU atuarão como sensores, fornecendo níveis de ruído. O nível de ruído pode ser consultado por um comando, mas também pode ser enviado como uma notificação periodicamente ou quando for alterado. Cliente é o dispositivo que se conecta ao servidor e envia comandos para ler ou alterar, se possível, os dados expostos, e solicitar e aceitar notificações quando os dados são alterados. A estação de coleta atuará como cliente de nossas placas MCU, a fim de obter medições de ruído delas.

Configurar

Começarei com o Raspberry Pi Pico (RP2) e usarei o MicroPython para a primeira implementação de um Periférico BLE. O objetivo dessa implementação é ler as informações do micro e disponibilizar as medições por meio de uma conexão BLE, mas vamos construir esse microcódigo iterativamente.

Ambiente de desenvolvimento

A instalação de um novo firmware no RP2 é bastante fácil porque ele suporta o USB Flashing Format (UF2), um mecanismo criado pela Microsoft e implementado por algumas placas que emula um dispositivo de armazenamento quando conectado à porta USB. Você pode então inserir um arquivo nesse dispositivo de armazenamento em um formato especial. O arquivo contém o firmware que você deseja instalar com alguns metadados e redundância e, depois de algumas verificações básicas, ele é automaticamente atualizado para o microcontrolador.
Nesse caso, vamos atualizar a versão mais recente do MicroPython para o RP2. Pressionamos e seguramos o botão BootSEL enquanto conectamos a placa ao USB e soltamos o arquivo de armazenamento em massa GB mais recente do2 arquivo no dispositivo de armazenamento em massa USB que aparece e que é chamado RPI-RP2. O microcódigo será atualizado e a placa será reiniciada. RP2 Bootsel Vamos usar o VSCode para trabalhar com o RP2. A extensão MicroPico nos ajudará a executar o código no RP2. Se você planeja instalá-lo, recomendamos que crie um perfil para ter extensões diferentes para diferentes diretorias, se necessário. Neste perfil, você também pode instalar as extensões Python recomendadas para ajudá-lo com o código python.
Vamos começar criando um novo diretório para nosso projeto e abrir o VSCode lá:
1mkdir BLE-periph-RP2
2cd BLE-periph-RP2
3code .
Em seguida, vamos inicializar o projeto para que o recurso autocompletar código funcione. No menu principal, selecione View- > Command Palette (ou Command + Shift + P) e localize MicroPico: Configure Project. Esse comando adicionará um arquivo ao projeto e vários botões na parte inferior esquerda do editor que lhe permitirão carregar os arquivos no quadro, executá-los e reiniciá-lo, entre outras coisas.
Você pode encontrar todo o código explicado no repositório. Sinta-se à vontade para fazer pull requests onde achar que elas se encaixam ou fazer perguntas.

Ambiente de teste

Como vamos desenvolver apenas o Periférico BLE, precisaremos de alguma ferramenta existente para atuar como Central BLE. Existem vários aplicativos móveis gratuitos disponíveis que farão isso. Estou usando o "nRF Connect for Mobile" (Android ou iOS), mas há outros que também podem ajudar, como o Lightblue (macOS/iOS ou Android).

Implementação do sensor BLE

Primeiros passos

  1. O MicroPython carrega e executa o código armazenado em dois arquivos, chamados boot.py e main.py, nessa ordem. O primeiro é usado para configurar alguns recursos da placa, como rede ou periféricos, apenas uma vez e somente após (re) iniciar a placa. Ele só deve conter isso para evitar problemas de inicialização. O arquivomain.pyé carregado e executado pelo MicroPython logo após boot.py, se existir, e que contém o código do aplicativo. A menos que seja explicitamente configurado, omain.py é executado em um loop, mas pode ser interrompido com mais facilidade. No nosso caso, não precisamos de nenhuma configuração prévia, então vamos começar com um arquivomain.py.
  2. Vamos começar piscando o LED embutido. Portanto, a primeira coisa de que precisaremos é um módulo que nos permita trabalhar com os diferentes recursos da placa. Esse módulo se chama machine e nós o importamos, apenas para ter acesso aos pinos:
    1from machine import Pin
  3. Em seguida, obtemos uma instância do pino que está conectado ao LED que usaremos para emitir tensão, ligando ou desligando o LED:
    1led = Pin('LED', Pin.OUT)
  4. Criamos um loop infinito e ligamos e desligamos o Led com os métodos desse nome, ou melhor ainda, com o métodotoggle().
    1while True:
    2 led.toggle()
  5. Isso vai ligar e desligar o Led tão rápido que não conseguiremos vê-lo, então vamos introduzir um atraso, importando o módulotime :
    1import time
    2
    3while True:
    4 time.sleep_ms(500)
  6. Execute o código usando o botãoRun na parte inferior esquerda do VSCode e veja o LED piscando. Yay!

Ler de um sensor

Nossos dispositivos medirão o nível de ruído de um microfone e o enviarão para a estação de coleta. No entanto, nosso Raspberry Pi Pico não tem microfone embutido, então vamos começar usando o sensor de temperatura que o RP2 tem para fazer algumas medições.
  1. Primeiro, importamos os recursos de conversão de análogo para digital:
    1from machine import ADC
  2. O sensor integrado está no quinto canal ADC (índice 4), então obtemos uma variável apontando para ele:
    1adc = ADC(4)
  3. No loop principal, leia a Tensão. É um número inteiro sem sinal de 16bits, na faixa 0V a 3.3V, que se converte em graus Celsius de acordo com as especificações do sensor. Imprima o valor:
    1temperature = 27.0 - ((adc.read_u16() * 3.3 / 65535) - 0.706) / 0.001721
    2print("T: {}ºC".format(temperature))
  4. Executamos esta nova versão do código e as medições devem ser atualizadas a cada meio segundo.

GAP periférico BLE

Vamos começar anunciando o nome do dispositivo e suas características. Isso é feito com o GAP (Generic Access Profile) para a função periférica. Poderíamos usar a interface de baixo nível para Bluetooth fornecida pelo módulobluetoothou a interface de nível superior fornecida pelo aioble. Este último é mais simples e recomendado no manual do MicroPython, mas a documentação é um pouco deficiente. Vamos começar com este e ler seu código-fonte em caso de dúvida.
  1. Começaremos importando aioble e bluetooth, ou seja, o bluetooth de baixo nível (usado aqui apenas para os UUIDs):
    1import aioble
    2import bluetooth
  2. Todos os dispositivos devem ser capazes de se identificar por meio do Serviço de Informações do Dispositivo, identificado com o UUID 0x180A. Começamos criando este serviço:
    1# Constants for the device information service
    2_SVC_DEVICE_INFO = bluetooth.UUID(0x180A)
    3svc_dev_info = aioble.Service(_SVC_DEVICE_INFO)
  3. Em seguida, vamos adicionar algumas características somente leitura a esse serviço, com valores iniciais que não serão alterados:
    1_CHAR_MANUFACTURER_NAME_STR = bluetooth.UUID(0x2A29)
    2_CHAR_MODEL_NUMBER_STR = bluetooth.UUID(0x2A24)
    3_CHAR_SERIAL_NUMBER_STR = bluetooth.UUID(0x2A25)
    4_CHAR_FIRMWARE_REV_STR = bluetooth.UUID(0x2A26)
    5_CHAR_HARDWARE_REV_STR = bluetooth.UUID(0x2A27)
    6aioble.Characteristic(svc_dev_info, _CHAR_MANUFACTURER_NAME_STR, read=True, initial='Jorge')
    7aioble.Characteristic(svc_dev_info, _CHAR_MODEL_NUMBER_STR, read=True, initial='J-0001')
    8aioble.Characteristic(svc_dev_info, _CHAR_SERIAL_NUMBER_STR, read=True, initial='J-0001-0000')
    9aioble.Characteristic(svc_dev_info, _CHAR_FIRMWARE_REV_STR, read=True, initial='0.0.1')
    10aioble.Characteristic(svc_dev_info, _CHAR_HARDWARE_REV_STR, read=True, initial='0.0.1')
  4. Agora que o serviço foi criado com as características relevantes, nós o registramos:
    1aioble.register_services(svc_dev_info)
  5. Agora podemos criar uma tarefa assíncrona que se encarregará de lidar com as conexões. Por definição, nosso periférico só pode ser conectado a um dispositivo central. Habilitamos o protocolo de acesso genérico (GAP), também conhecido como serviço de acesso geral, começando a anunciar os serviços registrados e, portanto, aceitamos conexões. Poderíamos não permitir conexões (connect=False) para dispositivos sem conexão, como beacons. O nome e a aparência do dispositivo são características obrigatórias do GAP, portanto, são parâmetros do métodoadvertise().
    1from micropython import const
    2
    3_ADVERTISING_INTERVAL_US = const(200_000)
    4_APPEARANCE = const(0x0552) # Multi-sensor
    5
    6async def task_peripheral():
    7 """ Task to handle advertising and connections """
    8 while True:
    9 async with await aioble.advertise(
    10 _ADVERTISING_INTERVAL_US,
    11 name='RP2-SENSOR',
    12 appearance=_APPEARANCE,
    13 services=[_DEVICE_INFO_SVC]
    14 ) as connection:
    15 print("Connected from ", connection.device)
    16 await connection.disconnected() # NOT connection.disconnect()
    17 print("Disconnect")
  6. Seria útil saber quando esse periférico está conectado para que possamos fazer o que for necessário. Criamos uma variável booleana global e a expomos para ser alterada na tarefa para o periférico:
    1connected=False
    2
    3async def task_peripheral():
    4 """ Task to handle advertising and connections """
    5 global connected
    6 while True:
    7 connected = False
    8 async with await aioble.advertise(
    9 _ADVERTISING_INTERVAL_MS,
    10 appearance=_APPEARANCE,
    11 name='RP2-SENSOR',
    12 services=[_SVC_DEVICE_INFO]
    13 ) as connection:
    14 print("Connected from ", connection.device)
    15 connected = True
  7. Podemos fornecer um feedback visual sobre o status da conexão em outra tarefa:
    1async def task_flash_led():
    2 """ Blink the on-board LED, faster if disconnected and slower if connected """
    3 BLINK_DELAY_MS_FAST = const(100)
    4 BLINK_DELAY_MS_SLOW = const(500)
    5 while True:
    6 led.toggle()
    7 if connected:
    8 await asyncio.sleep_ms(BLINK_DELAY_MS_SLOW)
    9 else:
    10 await asyncio.sleep_ms(BLINK_DELAY_MS_FAST)
  8. Em seguida, importamos asyncio para usá-lo com o mecanismo async/await:
    1import uasyncio as asyncio
  9. E mova a leitura do sensor para outra tarefa:
    1async def task_sensor():
    2 """ Task to handle sensor measures """
    3 while True:
    4 temperature = 27.0 - ((adc.read_u16() * 3.3 / 65535) - 0.706) / 0.001721
    5 print("T: {}°C".format(temperature))
    6 time.sleep_ms(_TEMP_MEASUREMENT_INTERVAL_MS)
  10. Definimos uma constante para o intervalo entre as medições de temperatura:
    1_TEMP_MEASUREMENT_INTERVAL_MS = const(15_000)
  11. E substitua o atraso por uma implementação compatível assíncrona:
    1await asyncio.sleep_ms(_TEMP_MEASUREMENT_FREQUENCY)
  12. Excluímos a importação do módulotimede que não precisaremos mais.
  13. Finalmente, criamos uma função principal na qual todas as tarefas são instanciadas:
    1async def main():
    2 """ Create all the tasks """
    3 tasks = [
    4 asyncio.create_task(task_peripheral()),
    5 asyncio.create_task(task_flash_led()),
    6 asyncio.create_task(task_sensor()),
    7 ]
    8 asyncio.gather(*tasks)
  14. E inicie principal quando o programa iniciar:
    1asyncio.run(main())
  15. Lave, enxágue e repita. Quer dizer, execute-o e tente se conectar ao dispositivo usando um dos aplicativos mencionados acima. Você deve ser capaz de encontrar e ler as características codificadas.

Adicionar um serviço de sensor

  1. Definimos um novo serviço, como o que fizemos com o de informações do dispositivo. Neste caso, é um Serviço de detecção ambiental (ESS) que expõe uma ou mais características para diferentes tipos de medições ambientais.
    1# Constants for the Environmental Sensing Service
    2_SVC_ENVIRONM_SENSING = bluetooth.UUID(0x181A)
    3svc_env_sensing = aioble.Service(_SVC_ENVIRONM_SENSING)
  2. Também definimos uma característica para... Sim, você adivinhou, uma medição de temperatura:
    1_CHAR_TEMP_MEASUREMENT = bluetooth.UUID(0x2A1C)
    2temperature_char = aioble.Characteristic(svc_env_sensing, _CHAR_TEMP_MEASUREMENT, read=True)
  3. Em seguida, adicionamos o serviço ao que registramos:
    1aioble.register_services(svc_dev_info, svc_env_sensing)
  4. E também para os serviços que são anunciados:
    1services=[_SVC_DEVICE_INFO, _SVC_ENVIRONM_SENSING]
  5. O formato em que os dados devem ser escritos é especificado nodocumento " Suplemento de Especificação do GATT". Meu Conselho é que, antes de selecionar a funcionalidade que vai usar, verifique os dados que serão contidos lá. Para essa características, precisamos codificar a temperatura codificada como IEEE 11073-20601 memfloat32 :Cool: :
    1def _encode_ieee11073(value, precision=2):
    2 """ Binary representation of float value as IEEE-11073:20601 32-bit FLOAT """
    3 return int(value * (10 ** precision)).to_bytes(3, 'little', True) + struct.pack('<b', -precision)
  6. Precedemos os dados com um byte contendo sinalizadores, definido como zero (o que significa que a temperatura é fornecida em Celsius e não há carimbo de data/hora ou informações sobre onde a temperatura é medida) e escrevemos os bytes 5 na propriedade:
    1temperature_char.write(struct.pack("<B4s", 0, _encode_ieee11073(temperature)))
  7. Por fim, desmarcamos o tempo limite para aguardar a desconexão da conexão, a fim de evitar desconexões indesejadas:
    1await connection.disconnected(timeout_ms=None)
  8. Podemos executar esse código no RP2 e consultar esse novo serviço e o anterior usando um dos aplicativos mencionados anteriormente.

Adicionar notificações

O documento "Suplemento de Especificação do GATT" afirma que as notificações devem ser implementadas adicionando um descritor de "Configuração de características do cliente", onde são habilitadas e iniciadas. Depois que as notificações forem habilitadas, elas devem obedece as condições de trigger definidas no descritor "Configuração de trigger ES". Se dois ou três (máximo permitido) descritores de trigger forem definidos para a mesma propriedade, o descritor "ES Configuration" também deverá estar presente para definir se os Atlas Triggers devem ser combinados com OR ou AND. Além disso, para alterar os valores desses descritores, vinculação do cliente --ie emparelhamento persistente-- é necessário.
Isso é muito trabalhoso para uma prova de conceito, portanto, vamos simplificá-lo notificando sempre que o sensor for lido. Quero deixar claro que nãoé assim que deve ser feito. Estamos cortando caminho aqui, mas meu entendimento neste ponto do projeto é que podemos adiar essa parte da implementação porque ela não afeta a viabilidade do nosso dispositivo. Adicionamos uma tarefa para nos lembrar mais tarde de que precisaremos fazer isso, se decidirmos usar sensores Bluetooth sobre MQTT.
  1. Alteramos a declaração de característica para habilitar notificações:
    1temperature_char = aioble.Characteristic(svc_env_sensing, _CHAR_TEMP_MEASUREMENT, read=True, notify=True)
  2. Adicionamos um descritor, embora vamos ignorá-lo por enquanto:
    1_DESC_ES_TRIGGER_SETTING = bluetooth.UUID(0x290D)
    2aioble.Descriptor(temperature_char, _DESC_ES_TRIGGER_SETTING, write=True, initial=struct.pack("<B", 0))
  3. Criamos uma variável global para a conexão:
    1connection = None
  4. Damos acesso a essa variável na tarefa secundária:
    1global connected, connection
  5. Definimos de volta para None quando a conexão é fechada:
    1connection = None
  6. E simplesmente invocamos o método notify com a mesma carga:
    1payload = struct.pack("<B4s", 0, _encode_ieee11073(temperature))
    2temperature_char.write(payload)
    3if connection is not None:
    4 temperature_char.notify(connection, payload)
  7. Todas as boas notificações chegam ao fim. Mas antes de prosseguir, execute-o e verifique se você pode receber as notificações no aplicativo cliente.

Recapitulação

Neste artigo, abordei alguns conceitos relevantes do Bluetooth Low Energy e os coloquei em prática, usando-os para escrever o firmware de uma placa Raspberry Pi Pico. Nesse firmware, usei o LED integrado, fiz a leitura do sensor de temperatura integrado e implementei um periférico BLE que oferecia dois serviços e uma característica que dependia dos dados medidos e podia enviar notificações.
Ainda não conectamos um gancho ao painel nem lemos os níveis de ruído com ele. Decidi adiar isso até que decidamos qual mecanismo será usado para enviar os dados dos sensores para as estações de coleta: BLE ou MQTT. Se, por qualquer motivo, eu tiver que trocar de painel durante a implementação das próximas etapas, esse capital será perdido. Portanto, parece razoável mover essa parte para mais tarde em nosso esforço de desenvolvimento.
Em meu próximo artigo, mostrarei como precisamos interagir com o Bluetooth a partir da linha de comando e como o Bluetooth pode ser usado em nosso software usando o DBus. O objetivo é entender o que precisamos fazer para passar da teoria para a prática usando C++ mais tarde.
Se você tiver dúvidas ou feedback, junte-se a mim no MongoDB Developer Community!

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
{Parte de uma série
Explorações de IoT com MongoDB
Próximo
Continuar

Mais nesta série
Relacionado
Tutorial

CMake + Conan + VS Code


Sep 04, 2024 | 6 min read
Tutorial

Eu e o Diabo AzureZ: Lendo sensores BLE de C++


Sep 17, 2024 | 16 min read
Tutorial

Primeiros passos no MongoDB e C++


Aug 14, 2024 | 7 min read
Tutorial

Red Mosquitto: Implemente um sensor de ruído com um cliente MQTT em um ESP32


Sep 17, 2024 | 25 min read
Sumário
  • Sumário