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

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

Jorge D. Ortiz-Fuentes16 min read • Published Sep 17, 2024 • Updated Sep 17, 2024
RaspberryPiC++
APLICATIVO COMPLETO
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Em nosso último artigo, compartilhei como interagir com dispositivos habilitados para dispositivos móveis e de baixa energia a partir de um Raspberry Pi com Linux, usando o DBus e o AzureZ. Faça um passo a passo de como se comunicar com um dispositivo BLE usando uma ferramenta de linha de comando, para termos uma imagem clara da sequência de operações que precisavam ser executadas para interagir com o dispositivo. Em seguida, repeti o processo, mas concentrei-me nas mensagens do DBus que devem ser trocadas para obter essa interação.
Agora, é hora de colocar esse conhecimento em prática e implementar um aplicativo que se conecta ao sensor RP2 BLE que criamos em nosso segundo artigo e lê o valor da... temperatura. (Sim, mudaremos para o ruído em breve. Por favor, tenha cuidado.)
Pronto para começar? Vamos começar!

Configurar

O aplicativo que desenvolveremos neste artigo será executado em um Raspberry Pi 4B, nossa estação de coleta. Você pode usar a maioria dos outros modelos, mas eu recomendo fortemente que você o conecte à sua rede usando um cabo Ethernet e desative o WiFi. Caso contrário, isso pode interferir nas comunicações Bluetooth.
Farei todo o meu desenvolvimento usando o Visual Studio Code no meu Macbook Pro e me conectar via SSH ao Raspberry Pi (RPi). Todo o projeto será mantido no RPi, e eu o compilarei e executarei lá. Você precisará da extensãoRemoto - SSH instalada no Visual Studio Code para que isso funcione e, na primeira vez que você se conectar ao RPI, levará algum tempo para configurá-la. Se você usa Emacs, oTRAMP está disponível imediatamente.
Também precisamos de algum software instalado no RPi. No mínimo, precisaremos git e CMake, porque esse é o sistema de construção que usarei para o projeto. O compilador C++ (g++) é instalado por padrão no Raspberry Pi OS, mas você pode instalar o Clang se preferir utilizar o LLVM.
1sudo apt-get install git git-flow cmake
De qualquer forma, precisaremos instalar o sdbus-c++. Essa é a biblioteca que nos permite interagir com o DBus usando vinculações C++. Existem várias alternativas, mas o sdbus-c++ é mantido adequadamente e tem boa documentação.
1sudo apt-get install libsdbus-c++-{bin,dev,doc}

Projeto inicial

Vou escrever este projeto do zero, portanto, quero ter certeza de que você e eu começaremos com o mesmo conjunto de arquivos. Vou começar com um arquivomain.cpptrivial e, em seguida, criarei a semente para as instruções de compilação que usaremos para produzir o executável ao longo deste episódio.

Inicial main.cpp

Nosso arquivomain.cppinicial imprimirá apenas uma mensagem:
1#include <iostream>
2
3int main(int argc, char *argv[])
4{
5 std::cout << "Noise Collector BLE" << std::endl;
6
7 return 0;
8}

Projeto básico

E agora devemos criar um arquivoCMakeLists.txtcom as instruções mínimas de compilação para esse projeto:
1cmake_minimum_required(VERSION 3.5)
2project(NoiseCollectorBLE CXX)
3add_executable(${PROJECT_NAME} main.cpp)
Antes de prosseguirmos, vamos verificar se tudo funciona bem:
1mkdir build
2cmake -S . -B build
3cmake --build build
4./build/NoiseCollectorBLE

Fale com o DBus de C++

Enviar a primeira mensagem

Agora que definimos as bases do projeto, podemos enviar nossa primeira mensagem para o DBus. Um bom para começar é aquele que usamos para consultar se o rádio Bluetooth está ligado ou desligado.
  1. Vamos começar adicionando a biblioteca ao projeto usando o comandofind_packagedo CMake :
    1find_package(sdbus-c++ REQUIRED)
  2. A biblioteca deve estar vinculada ao nosso binário:
    1target_link_libraries(${PROJECT_NAME} PRIVATE SDBusCpp::sdbus-c++)
  3. E reforçamos o uso do padrão C++17 porque ele é exigido pela biblioteca:
    1set(CMAKE_CXX_STANDARD 17)
    2set(CMAKE_CXX_STANDARD_REQUIRED ON)
  4. Com a biblioteca instalada, vamos criar o esqueleto para implementar nosso sensor BLE. Primeiro criamos o arquivoBleSensor.h:
    1#ifndef BLE_SENSOR_H
    2#define BLE_SENSOR_H
    3
    4class BleSensor
    5{
    6};
    7
    8#endif // BLE_SENSOR_H
  5. Adicionamos um construtor e um método que cuidará de todas as etapas necessárias para verificar e se conectar ao sensor:
    1public:
    2 BleSensor();
    3 void scanAndConnect();
  6. Para nos comunicar com o AzureZ, devemos criar um objeto proxy. Um proxy é um objeto local que nos permite interagir com o objeto DBus remoto. Criar a instância do proxy sem passar uma conexão para ela significa que o proxy criará sua própria conexão automaticamente, e será uma conexão de barramento do sistema.
    1private:
    2 std::unique_ptr<sdbus::IProxy> bluezProxy;
  7. E precisamos incluir a biblioteca:
    1#include <sdbus-c++/sdbus-c++.h>
  8. Vamos criar um arquivoBleSensor.cpp para a implementação e incluir o arquivo de cabeçalho que acabamos de criar:
    1#include "BleSensor.h"
  9. Esse proxy requer o nome do serviço e um caminho para a instância com a qual queremos conversar, portanto, vamos definir ambos como constantes dentro do construtor:
    1BleSensor::BleSensor()
    2{
    3 const std::string SERVICE_BLUEZ { "org.bluez" };
    4 const std::string OBJECT_PATH { "/org/bluez/hci0" };
    5
    6 bluezProxy = sdbus::createProxy(SERVICE_BLUEZ, OBJECT_PATH);
    7}
  10. Vamos adicionar a primeira etapa ao nosso método scanAndConnect usando uma função privada que declaramos no cabeçalho:
    1bool getBluetoothStatus();
  11. Em seguida, escrevemos a implementação, onde usamos o proxy que criamos antes para enviar uma mensagem. Definimos uma mensagem para um método em uma interface usando os parâmetros necessários, que aprenderam usando a interface introspectiva e os rastreamentos do DBus. O resultado é uma variante que pode ser convertida para o tipo adequado usando o operator()sobrecarregado:
    1bool BleSensor::getBluetoothStatus()
    2{
    3 const std::string METHOD_GET { "Get" };
    4 const std::string INTERFACE_PROPERTIES { "org.freedesktop.DBus.Properties" };
    5 const std::string INTERFACE_ADAPTER { "org.bluez.Adapter1" };
    6 const std::string PROPERTY_POWERED { "Powered" };
    7 sdbus::Variant variant;
    8
    9 // Invoke a method that gets a property as a variant
    10 bluezProxy->callMethod(METHOD_GET)
    11 .onInterface(INTERFACE_PROPERTIES)
    12 .withArguments(INTERFACE_ADAPTER, PROPERTY_POWERED)
    13 .storeResultsTo(variant);
    14
    15 return (bool)variant;
    16}
  12. Usamos este método privado do nosso método público:
    1void BleSensor::scanAndConnect()
    2{
    3 try
    4 {
    5 // Enable Bluetooth if not yet enabled
    6 if (getBluetoothStatus())
    7 {
    8 std::cout << "Bluetooth powered ON\n";
    9 } else
    10 {
    11 std::cout << "Powering bluetooth ON\n";
    12 }
    13 }
    14 catch(sdbus::Error& error)
    15 {
    16 std::cerr << "ERR: on scanAndConnect(): " << error.getName() << " with message " << error.getMessage() << std::endl;
    17 }
    18}
  13. E inclua o cabeçalho iostream:
    1#include <iostream>
  14. Precisamos adicionar os arquivos de origem ao projeto:
    1file(GLOB SOURCES "*.cpp")
    2add_executable(${PROJECT_NAME} ${SOURCES})
  15. Finalmente, importamos o cabeçalho que definimos no main.cpp, criamos uma instância do objeto e invocamos o método:
    1#include "BleSensor.h"
    2
    3int main(int argc, char *argv[])
    4{
    5 std::cout << "Noise Collector BLE" << std::endl;
    6 BleSensor bleSensor;
    7 bleSensor.scanAndConnect();
  16. Nós compilamos com CMake e executamos.

Enviar uma segunda mensagem

Nossa primeira mensagem consultou o status de uma propriedade. Também podemos mudar as coisas usando mensagens, como o status do rádio Bluetooth:
  1. Declaramos um segundo método privado no cabeçalho:
    1void setBluetoothStatus(bool enable);
  2. E também o adicionamos ao arquivo de implementação — nesse caso, somente a mensagem sem as constantes:
    1void BleSensor::setBluetoothStatus(bool enable)
    2{
    3 // Invoke a method that sets a property as a variant
    4 bluezProxy->callMethod(METHOD_SET)
    5 .onInterface(INTERFACE_PROPERTIES)
    6 .withArguments(INTERFACE_ADAPTER, PROPERTY_POWERED, sdbus::Variant(enable))
    7 // .dontExpectReply();
    8 .storeResultsTo();
    9}
  3. Como você pode ver, as chamadas para criar e enviar a mensagem usam a maioria das mesmas constantes. O único novo é o METHOD_SET, usado em vez de METHOD_GET. Definimos esse dentro do método:
    1const std::string METHOD_SET { "Set" };
  4. E produzimos as outras três constantes estáticas da classe. Antes do C++17, poderíamos declará-los no cabeçalho e inicializá-los na implementação, mas, desde então, podemos usar inline para inicializá-los no local. Isso ajuda na legibilidade:
    1static const std::string INTERFACE_ADAPTER { "org.bluez.Adapter1" };
    2static const std::string PROPERTY_POWERED { "Powered" };
    3static const std::string INTERFACE_PROPERTIES { "org.freedesktop.DBus.Properties" };
  5. Com o método privado completo, o usamos a partir do público:
    1if (getBluetoothStatus())
    2{
    3 std::cout << "Bluetooth powered ON\n";
    4} else
    5{
    6 std::cout << "Powering bluetooth ON\n";
    7 setBluetoothStatus(true);
    8}
  6. A segunda mensagem está pronta e podemos construir e executar o programa. Você pode verificar seus efeitos usando bluetoothctl.

Lidar com sinais

A próxima coisa que gostaríamos de fazer é habilitar a varredura de dispositivos BLE, encontrar o sensor de nosso interesse, conectar-se a ele e desabilitar a varredura. Obviamente, quando começamos a digitalizar, não conhecemos imediatamente os dispositivos BLE disponíveis. Alguns respondem quase instantaneamente e alguns responderão um pouco mais tarde. O DBus enviará sinais, mensagens assíncronas que são enviadas para um determinado objeto, que ouviremos.

Use mensagens que tenham uma resposta atrasada

  1. Vamos usar um método privado para habilitar e desabilitar a digitalização. A primeira coisa a fazer é declarar em nosso cabeçalho:
    1void enableScanning(bool enable);
  2. No arquivo de implementação, o método será semelhante aos que definimos anteriormente. Aqui, não precisamos nos preocupar com a resposta porque temos que esperar que nosso sensor apareça:
    1void BleSensor::enableScanning(bool enable)
    2{
    3 const std::string METHOD_START_DISCOVERY { "StartDiscovery" };
    4 const std::string METHOD_STOP_DISCOVERY { "StopDiscovery" };
    5
    6 std::cout << (enable?"Start":"Stop") << " scanning\n";
    7 bluezProxy->callMethod(enable?METHOD_START_DISCOVERY:METHOD_STOP_DISCOVERY)
    8 .onInterface(INTERFACE_ADAPTER)
    9 .dontExpectReply();
    10}
  3. Podemos então usar esse método em nosso método público para ativar e desativar a varredura:
    1enableScanning(true);
    2// Wait to be connected to the sensor
    3enableScanning(false);
  4. Precisamos esperar que os dispositivos respondam, então vamos adicionar algum atraso entre as duas chamadas:
    1// Wait to be connected to the sensor
    2std::this_thread::sleep_for(std::chrono::seconds(10))
  5. E adicionamos os cabeçalhos para este novo código:
    1#include <chrono>
    2#include <thread>
  6. Se compilarmos e executarmos, não veremos erros, mas também não veremos resultados de nossa varredura. Ainda assim.

Inscrever-se em sinais

Para obter os dados dos dispositivos gerados pela varredura de dispositivos, precisamos ouvir os sinais enviados que são transmitidos pelo barramento.
  1. Precisamos interagir com um objeto DBus diferente, portanto precisamos de outro proxy. Vamos declará-lo no cabeçalho:
    1std::unique_ptr<sdbus::IProxy> rootProxy;
  2. E instanciá-lo no construtor:
    1rootProxy = sdbus::createProxy(SERVICE_BLUEZ, "/");
  3. Em seguida, definimos o método privado que cuidará da assinatura:
    1void subscribeToInterfacesAdded();
  4. A implementação é simples: fornecemos um fechamento para ser chamado em uma thread diferente toda vez que recebermos um sinal que corresponda aos nossos parâmetros:
    1void BleSensor::subscribeToInterfacesAdded()
    2{
    3 const std::string INTERFACE_OBJ_MGR { "org.freedesktop.DBus.ObjectManager" };
    4 const std::string MEMBER_IFACE_ADDED { "InterfacesAdded" };
    5
    6 // Let's subscribe for the interfaces added signals (AddMatch)
    7 rootProxy->uponSignal(MEMBER_IFACE_ADDED).onInterface(INTERFACE_OBJ_MGR).call(interfaceAddedCallback);
    8 rootProxy->finishRegistration();
    9}
  5. O fechamento deve ter como argumentos os dados que vêm com um sinal: uma string para o caminho que aponta para um objeto no DBus e um dicionário de chaves/valores, onde as chaves são strings e os valores são dicionários de strings e valores:
    1auto interfaceAddedCallback = [this](sdbus::ObjectPath path,
    2 std::map<std::string,
    3 std::map<std::string, sdbus::Variant>> dictionary)
    4{
    5};
  6. Faremos mais com os dados posteriormente, mas agora, exibir o ID do thread, o caminho do objeto e o nome do dispositivo, se existir, será suficiente. Usamos uma expressão regular para restringir nossa atenção aos dispositivos Bluetooth:
    1const std::regex DEVICE_INSTANCE_RE{"^/org/bluez/hci[0-9]/dev(_[0-9A-F]{2}){6}$"};
    2std::smatch match;
    3std::cout << "(TID: " << std::this_thread::get_id() << ") ";
    4if (std::regex_match(path, match, DEVICE_INSTANCE_RE)) {
    5 std::cout << "Device iface ";
    6
    7 if (dictionary["org.bluez.Device1"].count("Name") == 1)
    8 {
    9 auto name = (std::string)(dictionary["org.bluez.Device1"].at("Name"));
    10 std::cout << name << " @ " << path << std::endl;
    11 } else
    12 {
    13 std::cout << "<NAMELESS> @ " << path << std::endl;
    14 }
    15} else {
    16 std::cout << "*** UNEXPECTED SIGNAL ***";
    17}
  7. E adicionamos o cabeçalho para expressões regulares:
    1#include <regex>
  8. Usamos o método privado antes de iniciar a varredura:
    1subscribeToInterfacesAdded();
  9. E imprimimos a ID do tópico no mesmo método:
    1std::cout << "(TID: " << std::this_thread::get_id() << ") ";
  10. Se você construir e executar este código, ele deverá exibir informações sobre os dispositivos BLE que você tem ao seu redor. Você pode mostrá-lo aos seus amigos e dizer que está procurando por microfones espiões.

Comunique-se com o sensor

Bem, isso parece um progresso para mim, mas ainda faltam os recursos mais importantes: conectar-se ao dispositivo BLE e ler valores dele.
Devemos nos conectar ao dispositivo, se o encontrarmos, a partir do fechamento que usamos em subscribeToInterfacesAdded()e, em seguida, devemos parar a digitalização. No entanto, esse fechamento e o método scanAndConnect() estão sendo executados em threads diferentes simultaneamente. Quando o fechamento se conecta ao dispositivo, ele deve informar à thread principal, para que ela interrompa a digitalização. Vamos usar um mutex para proteger o acesso simultâneo aos dados compartilhados entre esses dois threads e uma variável condicional para informar ao outro thread quando ele foi alterado.

Conecte-se ao dispositivo BLE

  1. Primeiro, vamos declarar um método privado para nos conectar a um dispositivo por nome:
    1void connectToDevice(sdbus::ObjectPath path);
  2. Obteremos esse caminho do objeto a partir dos sinais que nos informam sobre os dispositivos descobertos durante a digitalização. Compararemos o nome no dicionário de propriedades do sinal com o nome do sensor que estamos procurando. Receberemos esse nome por meio do construtor, então precisamos alterar sua declaração:
    1BleSensor(const std::string &sensor_name);
  3. E declare um campo que será usado para manter o valor:
    1const std::string deviceName;
  4. Se encontrarmos o dispositivo, criaremos um proxy para o objeto que o representa:
    1std::unique_ptr<sdbus::IProxy> deviceProxy;
  5. Passamos para a implementação e começamos adaptando o construtor para inicializar os novos valores usando o preâmbulo:
    1BleSensor::BleSensor(const std::string &sensor_name)
    2 : deviceProxy{nullptr}, deviceName{sensor_name}
  6. Em seguida, criamos o método:
    1void BleSensor::connectToDevice(sdbus::ObjectPath path)
    2{
    3}
  7. Criamos um proxy para o dispositivo que selecionamos usando o nome:
    1deviceProxy = sdbus::createProxy(SERVICE_BLUEZ, path);
  8. E mova a declaração da constante de serviço, que agora é usada em dois lugares, para o cabeçalho:
    1inline static const std::string SERVICE_BLUEZ{"org.bluez"};
  9. E envie uma mensagem para se conectar a ele:
    1deviceProxy->callMethodAsync(METHOD_CONNECT).onInterface(INTERFACE_DEVICE).uponReplyInvoke(connectionCallback);
    2std::cout << "Connection method started" << std::endl;
  10. Definimos as constantes que estamos usando:
    1const std::string INTERFACE_DEVICE{"org.bluez.Device1"};
    2const std::string METHOD_CONNECT{"Connect"};
  11. E o fechamento que será invocado. O uso de this na especificação de captura permite o acesso à instância do objeto. O código na closure será adicionado abaixo.
    1auto connectionCallback = [this](const sdbus::Error *error)
    2{
    3};
  12. O método privado agora pode ser usado para se conectar a partir do método BleSensor::subscribeToInterfacesAdded(). Já extraímos o nome do dispositivo, agora o usamos para nos conectar a ele:
    1if (name == deviceName)
    2{
    3 std::cout << "Connecting to " << name << std::endl;
    4 connectToDevice(path);
    5}
  13. Gostaríamos de interromper a digitalização quando estivermos conectados ao dispositivo. Isso acontece em dois threads diferentes, então vamos usar o padrão de design de simultaneidade do produtor-consumidor para obter o comportamento esperado. Definimos alguns campos novos - um para o mutex, um para a variável condicional e um para um sinalizador booleano:
    1std::mutex mtx;
    2std::condition_variable cv;
    3bool connected;
  14. E incluímos os cabeçalhos necessários:
    1#include <condition_variable>
  15. Eles são inicializados no preâmbulo do construtor:
    1BleSensor::BleSensor(const std::string &sensor_name)
    2 : deviceProxy{nullptr}, deviceName{sensor_name},
    3 cv{}, mtx{}, connected{false}
  16. Podemos então utilizar estes novos campos no métodoBleSensor::scanAndConnect(). Primeiro, obtemos um bloqueio exclusivo no mutex antes de assinar as notificações:
    1std::unique_lock<std::mutex> lock(mtx);
  17. Então, entre o início e o fim do processo de varredura, aguardamos que a variável condicional seja sinalizada. Essa é uma implementação mais robusta e confiável do que usar o atraso:
    1enableScanning(true);
    2// Wait to be connected to the sensor
    3cv.wait(lock, [this]()
    4 { return connected; });
    5enableScanning(false);
  18. No connectionCallback, lidamos primeiro com os erros, caso eles ocorram:
    1if (error != nullptr)
    2{
    3 std::cerr << "Got connection error "
    4 << error->getName() << " with message "
    5 << error->getMessage() << std::endl;
    6 return;
    7}
  19. Em seguida, obtemos um bloqueio no mesmo mutex, alteramos o sinalizador, liberamos o bloqueio e sinalizamos o outro thread por meio da variável de conexão:
    1std::unique_lock<std::mutex> lock(mtx);
    2std::cout << "Connected!!!" << std::endl;
    3connected = true;
    4lock.unlock();
    5cv.notify_one();
    6std::cout << "Finished connection method call" << std::endl;
  20. Finalmente, alteramos a inicialização do BleSensor no arquivo principal para passar o nome do sensor:
    1BleSensor bleSensor { "RP2-SENSOR" };
  21. Se compilarmos e executarmos o que temos até agora, deveremos conseguir nos conectar ao sensor. Mas se o sensor não estiver lá, ele ficará esperando indefinidamente. Se tiver problemas para se conectar ao seu dispositivo e receber "le-connection-abort-by-local," use um cabo ethernet em vez de WiFi e desative-o com sudo ip link set wlan0 down.

Ler a partir do sensor

Agora que temos uma conexão com o dispositivo BLE, receberemos sinais sobre outras interfaces adicionadas. Esses serão os serviços, características e descritores. Se quisermos ler dados de uma característica, temos que encontrá-la –usando seu UUID, por exemplo– e usar o método "Read" do DBus para obter seu valor. Já temos um fechamento que é invocado toda vez que um sinal é recebido porque uma interface é adicionada, mas nesse fechamento, verificamos se o caminho do objeto corresponde a um dispositivo, em vez de a um atributo Bluetooth.
  1. Queremos combinar o caminho do objeto com a estrutura de um atributo BLE, mas queremos fazer isso somente quando o dispositivo já estiver conectado. Então, cercamos a correspondência de expressão regular existente:
    1if (!connected)
    2{
    3 // Current code with regex goes here.
    4}
    5else
    6{
    7}
  2. Na parteelse, adicionamos uma correspondência diferente:
    1if (std::regex_match(path, match, DEVICE_ATTRS_RE))
    2{
    3}
    4else
    5{
    6 std::cout << "Not a characteristic" << std::endl;
    7}
  3. Esse código requer a expressão regular declarada no método:
    1const std::regex DEVICE_ATTRS_RE{"^/org/bluez/hci\\d/dev(_[0-9A-F]{2}){6}/service\\d{4}/char\\d{4}"};
  4. Se o caminho corresponder à expressão, verificamos se ele tem o UUID da características que queremos ler:
    1std::cout << "Characteristic " << path << std::endl;
    2if ((dictionary.count("org.bluez.GattCharacteristic1") == 1) &&
    3 (dictionary["org.bluez.GattCharacteristic1"].count("UUID") == 1))
    4{
    5 auto name = (std::string)(dictionary["org.bluez.GattCharacteristic1"].at("UUID"));
    6 if (name == "00002a1c-0000-1000-8000-00805f9b34fb")
    7 {
    8 }
    9}
  5. Quando encontramos a característica desejada, precisamos criar (sim, você adivinhou) um proxy para enviar mensagens para ele.
    1tempAttrProxy = sdbus::createProxy(SERVICE_BLUEZ, path);
    2std::cout << "<<<FOUND>>> " << path << std::endl;
  6. Esse proxy está armazenado em um campo que ainda não declaramos. Vamos fazer isso no arquivo de cabeçalho:
    1std::unique_ptr<sdbus::IProxy> tempAttrProxy;
  7. E façamos uma inicialização explícita no preâmbulo do construtor:
    1BleSensor::BleSensor(const std::string &sensor_name)
    2 : deviceProxy{nullptr}, tempAttrProxy{nullptr},
    3 cv{}, mtx{}, connected{false}, deviceName{sensor_name}
  8. Tudo está pronto para ser lido, portanto, vamos declarar um método público para fazer a leitura:
    1void getValue();
  9. E um método privado para enviar as mensagens do DBus:
    1void readTemperature();
  10. Implementamos o método público, apenas usando o método privado:
    1void BleSensor::getValue()
    2{
    3 readTemperature();
    4}
  11. E fazem a implementação no método privado:
    1void BleSensor::readTemperature()
    2{
    3 tempAttrProxy->callMethod(METHOD_READ)
    4 .onInterface(INTERFACE_CHAR)
    5 .withArguments(args)
    6 .storeResultsTo(result);
    7}
  12. Definimos as constantes que usamos:
    1const std::string INTERFACE_CHAR{"org.bluez.GattCharacteristic1"};
    2const std::string METHOD_READ{"ReadValue"};
  13. E a variável que será usada para qualificar a consulta deve ter um deslocamento zero, assim como a que armazenará a resposta do método:
    1std::map<std::string, sdbus::Variant> args{{{"offset", sdbus::Variant{std::uint16_t{0}}}}};
    2std::vector<std::uint8_t> result;
  14. A temperatura começa no segundo byte do resultado (offset 1) e termina no quinto, que nesse caso é o último da matriz de bytes. Podemos extraí-lo:
    1std::cout << "READ: ";
    2for (auto value : result)
    3{
    4 std::cout << +value << " ";
    5}
    6std::vector number(result.begin() + 1, result.end());
  15. Esses bytes no formato ieee11073 devem ser transformados em uma flutuação regular, e usamos um método privado para isso:
    1float valueFromIeee11073(std::vector<std::uint8_t> binary);
  16. Esse método é implementado invertendo a transformação que fizemos no segundo artigo desta série:
    1float BleSensor::valueFromIeee11073(std::vector<std::uint8_t> binary)
    2{
    3 float value = static_cast<float>(binary[0]) + static_cast<float>(binary[1]) * 256.f + static_cast<float>(binary[2]) * 256.f * 256.f;
    4 float exponent;
    5 if (binary[3] > 127)
    6 {
    7 exponent = static_cast<float>(binary[3]) - 256.f;
    8 }
    9 else
    10 {
    11 exponent = static_cast<float>(binary[3]);
    12 }
    13 return value * pow(10, exponent);
    14}
  17. Essa implementação requer a inclusão da declaração matemática:
    1#include <cmath>
  18. Usamos a transformação depois de ler o valor:
    1std::cout << "\nTemp: " << valueFromIeee11073(number);
    2std::cout << std::endl;
  19. E usamos o método público na função principal. Devemos usar o padrão produtor-consumidor aqui novamente para saber quando o proxy da característica de temperatura está pronto, mas reduzi os custos novamente para essa implementação inicial usando alguns atrasos para garantir que tudo funcione bem.
    1std::this_thread::sleep_for(std::chrono::seconds(5));
    2bleSensor.getValue();
    3std::this_thread::sleep_for(std::chrono::seconds(5));
  20. Para que isso funcione, o cabeçalho do tópico deve ser incluído:
    1#include <thread>
  21. Construímos e executamos para verificar se um valor pode ser lido.

Desconecte-se do sensor BLE

Finalmente, devemos nos desconectar deste dispositivo para deixar as coisas como as encontraram. Caso contrário, a reexecução do programa não funcionará porque o sensor ainda estará conectado e ocupado.
  1. Declaramos um método público no cabeçalho para lidar com desconexões:
    1void disconnect();
  2. E um privado para enviar a mensagem DBus correspondente:
    1void disconnectFromDevice();
  3. Na implementação, o método privado envia a mensagem necessária e cria um fechamento que é invocado quando o dispositivo é desconectado:
    1void BleSensor::disconnectFromDevice()
    2{
    3 const std::string INTERFACE_DEVICE{"org.bluez.Device1"};
    4 const std::string METHOD_DISCONNECT{"Disconnect"};
    5
    6 auto disconnectionCallback = [this](const sdbus::Error *error)
    7 {
    8 };
    9
    10 {
    11 deviceProxy->callMethodAsync(METHOD_DISCONNECT).onInterface(INTERFACE_DEVICE).uponReplyInvoke(disconnectionCallback);
    12 std::cout << "Disconnection method started" << std::endl;
    13 }
    14}
  4. E esse fechamento precisa alterar a bandeira conectada usando acesso exclusivo:
    1if (error != nullptr)
    2{
    3 std::cerr << "Got disconnection error " << error->getName() << " with message " << error->getMessage() << std::endl;
    4 return;
    5}
    6std::unique_lock<std::mutex> lock(mtx);
    7std::cout << "Disconnected!!!" << std::endl;
    8connected = false;
    9deviceProxy = nullptr;
    10lock.unlock();
    11std::cout << "Finished connection method call" << std::endl;
  5. O método privado é usado a partir do método público:
    1void BleSensor::disconnect()
    2{
    3 std::cout << "Disconnecting from device" << std::endl;
    4 disconnectFromDevice();
    5}
  6. E o método público é usado a partir da função principal:
    1bleSensor.disconnect();
  7. Construa e execute para ver o resultado final.

Recapitulação e trabalho futuro

Neste artigo, usei C++ para escrever um aplicativo que lê dados de um sensor de baixa energia do Azure. Percebi que escrever C++ nãoé como andando de bicicleta. Muitas coisas mudou desde que Escrevi meu último código C++ que entrou em produção, mas espero ter feito um trabalho de valor ao usá-lo para essa tarefa. Frustração O maior desafio não era o idioma, no entanto. Eu bateva minha cabeça contra uma parede de blocos toda vez que tentou descobrir por que obtive "org.bluez.Error.Failed", causado por um "Falha na conexão ao ser estabelecida (0x3e)", quando tentando se conectar ao sensor azul. Isso aconteceu com frequência, mas nem sempre. No início, eu não saberia se era o meu código que definia, a biblioteca ou o que. Depois de capturar exceções em todos os lugares, imprimir todas as mensagens, capturar rastreamentos de Azure com btmone não encontrar muito (embora tenha aprender algumas coisas novas nos fórunsUnix e Linux StackExchange, Stack Overflow e Raspberry Pi), derrepentei que o o responsável foi o processador Raspberry Pi WiFi/bluetooth. O problema era uma conexão semelhante à de umaconexãosem fio confiável, mas meu sensor e o RPi estavam muito próximos um do outro e sem nenhuma intervenção relevante do ambiente. A causa raiz era o compartilhamento da frequência derádio (RF) no mesmo processador43438) com uma Antena relativamente pequena. Mudei do RPi3A+ para um RPi4B com um cabo ethernet e WiFi desativado e, derrepente, as coisas começaram a funcionar.
Embora a implementação não fosse muito complexa e a prova de conceito tenha sido aprovada, o problema do hardware suscitou algumas preocupações. Só pioraria se eu falasse com vários sensores em vez de apenas um. E é exatamente isso que faremos em episódios futuros para coletar os dados do sensor e enviá-los para um cluster MongoDB com séries temporais. Eu ainda poderia usar um dongle USB Bluetooth e ignorar o hardware interno. Mas antes de seguir esse caminho, gostaria de trabalhar na alternativa MQTT e tomar uma decisão mais bem informada. E esse será o nosso próximo capítulo.
Fique curioso, hackeie seu código e até a próxima!
Principais comentários nos fóruns
Avatar do Comentarista do Fórum
Koevin_KempKoevin Kempno mês passado

O código fonte completo deste projeto está disponível? Estou tentando construir isso a partir do artigo, mas estou me perdem com o arquivo CMakeLists.txt. Conheço bem C++ , mas não estou tão bom com cmake.

Btw, o projeto que estou criando não é exatamente como o seu, mas é próximo, estou usando um modelo raspberrypi 4B para a central e arduinno nano 33 IoT para os Periféricos. Pretendo ter cerca 12 Periféricos. Tenho 4 deles agora e eles estão funcionando. Só preciso criar uma central que possa se conectar a eles. Btw, este é o meu primeiro projeto BLE.


Avatar do Comentarista do Fórum
Jorge_Ortiz_FuentesJorge Ortiz Fuenteslast month

Olá Kavin,
O código está aqui. Esperemos que isso resolva a sua pergunta, mas fique à vontade para perguntar se não resolver.
Boa sorte com seu projeto.
Atenciosamente,

jorge

Veja mais nos fóruns

Í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
Mais nesta série
Relacionado
Tutorial

Primeiros passos no MongoDB e C++


Aug 14, 2024 | 7 min read
Tutorial

Adote BLE: implementando sensores BLE com MCU Devkits


Apr 02, 2024 | 13 min read
exemplo de código

EnSat


Feb 08, 2023 | 3 min read
Tutorial

Seleção de planos e hardware para uma implementação prática de IoT com MCUs e MongoDB


Aug 28, 2024 | 11 min read
Sumário