Eu e o Diabo AzureZ: Lendo sensores BLE de C++
Jorge D. Ortiz-Fuentes16 min read • Published Sep 17, 2024 • Updated Sep 17, 2024
APLICATIVO COMPLETO
Avalie esse Tutorial
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!
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.1 sudo 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.1 sudo apt-get install libsdbus-c++-{bin,dev,doc}
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 arquivo
main.cpp
trivial 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.Nosso arquivo
main.cpp
inicial imprimirá apenas uma mensagem:1 #include <iostream> 2 3 int main(int argc, char *argv[]) 4 { 5 std::cout << "Noise Collector BLE" << std::endl; 6 7 return 0; 8 }
E agora devemos criar um arquivo
CMakeLists.txt
com as instruções mínimas de compilação para esse projeto:1 cmake_minimum_required(VERSION 3.5) 2 project(NoiseCollectorBLE CXX) 3 add_executable(${PROJECT_NAME} main.cpp)
Antes de prosseguirmos, vamos verificar se tudo funciona bem:
1 mkdir build 2 cmake -S . -B build 3 cmake --build build 4 ./build/NoiseCollectorBLE
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.
- Vamos começar adicionando a biblioteca ao projeto usando o comando
find_package
do CMake :1 find_package(sdbus-c++ REQUIRED) - A biblioteca deve estar vinculada ao nosso binário:
1 target_link_libraries(${PROJECT_NAME} PRIVATE SDBusCpp::sdbus-c++) - E reforçamos o uso do padrão C++17 porque ele é exigido pela biblioteca:
1 set(CMAKE_CXX_STANDARD 17) 2 set(CMAKE_CXX_STANDARD_REQUIRED ON) - Com a biblioteca instalada, vamos criar o esqueleto para implementar nosso sensor BLE. Primeiro criamos o arquivo
BleSensor.h
:1 #ifndef BLE_SENSOR_H 2 #define BLE_SENSOR_H 3 4 class BleSensor 5 { 6 }; 7 8 #endif // BLE_SENSOR_H - Adicionamos um construtor e um método que cuidará de todas as etapas necessárias para verificar e se conectar ao sensor:
1 public: 2 BleSensor(); 3 void scanAndConnect(); - 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.
1 private: 2 std::unique_ptr<sdbus::IProxy> bluezProxy; - E precisamos incluir a biblioteca:
1 #include <sdbus-c++/sdbus-c++.h> - Vamos criar um arquivo
BleSensor.cpp
para a implementação e incluir o arquivo de cabeçalho que acabamos de criar:1 #include "BleSensor.h" - 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:
1 BleSensor::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 } - Vamos adicionar a primeira etapa ao nosso método scanAndConnect usando uma função privada que declaramos no cabeçalho:
1 bool getBluetoothStatus(); - 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:1 bool 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 } - Usamos este método privado do nosso método público:
1 void 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 } - E inclua o cabeçalho iostream:
1 #include <iostream> - Precisamos adicionar os arquivos de origem ao projeto:
1 file(GLOB SOURCES "*.cpp") 2 add_executable(${PROJECT_NAME} ${SOURCES}) - 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 3 int main(int argc, char *argv[]) 4 { 5 std::cout << "Noise Collector BLE" << std::endl; 6 BleSensor bleSensor; 7 bleSensor.scanAndConnect(); - Nós compilamos com CMake e executamos.
Nossa primeira mensagem consultou o status de uma propriedade. Também podemos mudar as coisas usando mensagens, como o status do rádio Bluetooth:
- Declaramos um segundo método privado no cabeçalho:
1 void setBluetoothStatus(bool enable); - E também o adicionamos ao arquivo de implementação — nesse caso, somente a mensagem sem as constantes:
1 void 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 } - 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 deMETHOD_GET
. Definimos esse dentro do método:1 const std::string METHOD_SET { "Set" }; - 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:1 static const std::string INTERFACE_ADAPTER { "org.bluez.Adapter1" }; 2 static const std::string PROPERTY_POWERED { "Powered" }; 3 static const std::string INTERFACE_PROPERTIES { "org.freedesktop.DBus.Properties" }; - Com o método privado completo, o usamos a partir do público:
1 if (getBluetoothStatus()) 2 { 3 std::cout << "Bluetooth powered ON\n"; 4 } else 5 { 6 std::cout << "Powering bluetooth ON\n"; 7 setBluetoothStatus(true); 8 } - A segunda mensagem está pronta e podemos construir e executar o programa. Você pode verificar seus efeitos usando
bluetoothctl
.
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.
- Vamos usar um método privado para habilitar e desabilitar a digitalização. A primeira coisa a fazer é declarar em nosso cabeçalho:
1 void enableScanning(bool enable); - 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:
1 void 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 } - Podemos então usar esse método em nosso método público para ativar e desativar a varredura:
1 enableScanning(true); 2 // Wait to be connected to the sensor 3 enableScanning(false); - Precisamos esperar que os dispositivos respondam, então vamos adicionar algum atraso entre as duas chamadas:
1 // Wait to be connected to the sensor 2 std::this_thread::sleep_for(std::chrono::seconds(10)) - E adicionamos os cabeçalhos para este novo código:
1 #include <chrono> 2 #include <thread> - Se compilarmos e executarmos, não veremos erros, mas também não veremos resultados de nossa varredura. Ainda assim.
Para obter os dados dos dispositivos gerados pela varredura de dispositivos, precisamos ouvir os sinais enviados que são transmitidos pelo barramento.
- Precisamos interagir com um objeto DBus diferente, portanto precisamos de outro proxy. Vamos declará-lo no cabeçalho:
1 std::unique_ptr<sdbus::IProxy> rootProxy; - E instanciá-lo no construtor:
1 rootProxy = sdbus::createProxy(SERVICE_BLUEZ, "/"); - Em seguida, definimos o método privado que cuidará da assinatura:
1 void subscribeToInterfacesAdded(); - 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:
1 void 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 } - 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:
1 auto interfaceAddedCallback = [this](sdbus::ObjectPath path, 2 std::map<std::string, 3 std::map<std::string, sdbus::Variant>> dictionary) 4 { 5 }; - 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:
1 const std::regex DEVICE_INSTANCE_RE{"^/org/bluez/hci[0-9]/dev(_[0-9A-F]{2}){6}$"}; 2 std::smatch match; 3 std::cout << "(TID: " << std::this_thread::get_id() << ") "; 4 if (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 } - E adicionamos o cabeçalho para expressões regulares:
1 #include <regex> - Usamos o método privado antes de iniciar a varredura:
1 subscribeToInterfacesAdded(); - E imprimimos a ID do tópico no mesmo método:
1 std::cout << "(TID: " << std::this_thread::get_id() << ") "; - 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.
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.- Primeiro, vamos declarar um método privado para nos conectar a um dispositivo por nome:
1 void connectToDevice(sdbus::ObjectPath path); - 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:
1 BleSensor(const std::string &sensor_name); - E declare um campo que será usado para manter o valor:
1 const std::string deviceName; - Se encontrarmos o dispositivo, criaremos um proxy para o objeto que o representa:
1 std::unique_ptr<sdbus::IProxy> deviceProxy; - Passamos para a implementação e começamos adaptando o construtor para inicializar os novos valores usando o preâmbulo:
1 BleSensor::BleSensor(const std::string &sensor_name) 2 : deviceProxy{nullptr}, deviceName{sensor_name} - Em seguida, criamos o método:
1 void BleSensor::connectToDevice(sdbus::ObjectPath path) 2 { 3 } - Criamos um proxy para o dispositivo que selecionamos usando o nome:
1 deviceProxy = sdbus::createProxy(SERVICE_BLUEZ, path); - E mova a declaração da constante de serviço, que agora é usada em dois lugares, para o cabeçalho:
1 inline static const std::string SERVICE_BLUEZ{"org.bluez"}; - E envie uma mensagem para se conectar a ele:
1 deviceProxy->callMethodAsync(METHOD_CONNECT).onInterface(INTERFACE_DEVICE).uponReplyInvoke(connectionCallback); 2 std::cout << "Connection method started" << std::endl; - Definimos as constantes que estamos usando:
1 const std::string INTERFACE_DEVICE{"org.bluez.Device1"}; 2 const std::string METHOD_CONNECT{"Connect"}; - 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.1 auto connectionCallback = [this](const sdbus::Error *error) 2 { 3 }; - 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:1 if (name == deviceName) 2 { 3 std::cout << "Connecting to " << name << std::endl; 4 connectToDevice(path); 5 } - 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:
1 std::mutex mtx; 2 std::condition_variable cv; 3 bool connected; - E incluímos os cabeçalhos necessários:
1 #include <condition_variable> - Eles são inicializados no preâmbulo do construtor:
1 BleSensor::BleSensor(const std::string &sensor_name) 2 : deviceProxy{nullptr}, deviceName{sensor_name}, 3 cv{}, mtx{}, connected{false} - Podemos então utilizar estes novos campos no método
BleSensor::scanAndConnect()
. Primeiro, obtemos um bloqueio exclusivo no mutex antes de assinar as notificações:1 std::unique_lock<std::mutex> lock(mtx); - 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:
1 enableScanning(true); 2 // Wait to be connected to the sensor 3 cv.wait(lock, [this]() 4 { return connected; }); 5 enableScanning(false); - No
connectionCallback
, lidamos primeiro com os erros, caso eles ocorram:1 if (error != nullptr) 2 { 3 std::cerr << "Got connection error " 4 << error->getName() << " with message " 5 << error->getMessage() << std::endl; 6 return; 7 } - 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:
1 std::unique_lock<std::mutex> lock(mtx); 2 std::cout << "Connected!!!" << std::endl; 3 connected = true; 4 lock.unlock(); 5 cv.notify_one(); 6 std::cout << "Finished connection method call" << std::endl; - Finalmente, alteramos a inicialização do BleSensor no arquivo principal para passar o nome do sensor:
1 BleSensor bleSensor { "RP2-SENSOR" }; - 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
.
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.
- 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:
1 if (!connected) 2 { 3 // Current code with regex goes here. 4 } 5 else 6 { 7 } - Na parteelse, adicionamos uma correspondência diferente:
1 if (std::regex_match(path, match, DEVICE_ATTRS_RE)) 2 { 3 } 4 else 5 { 6 std::cout << "Not a characteristic" << std::endl; 7 } - Esse código requer a expressão regular declarada no método:
1 const std::regex DEVICE_ATTRS_RE{"^/org/bluez/hci\\d/dev(_[0-9A-F]{2}){6}/service\\d{4}/char\\d{4}"}; - Se o caminho corresponder à expressão, verificamos se ele tem o UUID da características que queremos ler:
1 std::cout << "Characteristic " << path << std::endl; 2 if ((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 } - Quando encontramos a característica desejada, precisamos criar (sim, você adivinhou) um proxy para enviar mensagens para ele.
1 tempAttrProxy = sdbus::createProxy(SERVICE_BLUEZ, path); 2 std::cout << "<<<FOUND>>> " << path << std::endl; - Esse proxy está armazenado em um campo que ainda não declaramos. Vamos fazer isso no arquivo de cabeçalho:
1 std::unique_ptr<sdbus::IProxy> tempAttrProxy; - E façamos uma inicialização explícita no preâmbulo do construtor:
1 BleSensor::BleSensor(const std::string &sensor_name) 2 : deviceProxy{nullptr}, tempAttrProxy{nullptr}, 3 cv{}, mtx{}, connected{false}, deviceName{sensor_name} - Tudo está pronto para ser lido, portanto, vamos declarar um método público para fazer a leitura:
1 void getValue(); - E um método privado para enviar as mensagens do DBus:
1 void readTemperature(); - Implementamos o método público, apenas usando o método privado:
1 void BleSensor::getValue() 2 { 3 readTemperature(); 4 } - E fazem a implementação no método privado:
1 void BleSensor::readTemperature() 2 { 3 tempAttrProxy->callMethod(METHOD_READ) 4 .onInterface(INTERFACE_CHAR) 5 .withArguments(args) 6 .storeResultsTo(result); 7 } - Definimos as constantes que usamos:
1 const std::string INTERFACE_CHAR{"org.bluez.GattCharacteristic1"}; 2 const std::string METHOD_READ{"ReadValue"}; - 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:
1 std::map<std::string, sdbus::Variant> args{{{"offset", sdbus::Variant{std::uint16_t{0}}}}}; 2 std::vector<std::uint8_t> result; - 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:
1 std::cout << "READ: "; 2 for (auto value : result) 3 { 4 std::cout << +value << " "; 5 } 6 std::vector number(result.begin() + 1, result.end()); - Esses bytes no formato ieee11073 devem ser transformados em uma flutuação regular, e usamos um método privado para isso:
1 float valueFromIeee11073(std::vector<std::uint8_t> binary); 1 float 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 } - Essa implementação requer a inclusão da declaração matemática:
1 #include <cmath> - Usamos a transformação depois de ler o valor:
1 std::cout << "\nTemp: " << valueFromIeee11073(number); 2 std::cout << std::endl; - 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.
1 std::this_thread::sleep_for(std::chrono::seconds(5)); 2 bleSensor.getValue(); 3 std::this_thread::sleep_for(std::chrono::seconds(5)); - Para que isso funcione, o cabeçalho do tópico deve ser incluído:
1 #include <thread> - Construímos e executamos para verificar se um valor pode ser lido.
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.
- Declaramos um método público no cabeçalho para lidar com desconexões:
1 void disconnect(); - E um privado para enviar a mensagem DBus correspondente:
1 void disconnectFromDevice(); - Na implementação, o método privado envia a mensagem necessária e cria um fechamento que é invocado quando o dispositivo é desconectado:
1 void 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 } - E esse fechamento precisa alterar a bandeira conectada usando acesso exclusivo:
1 if (error != nullptr) 2 { 3 std::cerr << "Got disconnection error " << error->getName() << " with message " << error->getMessage() << std::endl; 4 return; 5 } 6 std::unique_lock<std::mutex> lock(mtx); 7 std::cout << "Disconnected!!!" << std::endl; 8 connected = false; 9 deviceProxy = nullptr; 10 lock.unlock(); 11 std::cout << "Finished connection method call" << std::endl; - O método privado é usado a partir do método público:
1 void BleSensor::disconnect() 2 { 3 std::cout << "Disconnecting from device" << std::endl; 4 disconnectFromDevice(); 5 } - E o método público é usado a partir da função principal:
1 bleSensor.disconnect(); - Construa e execute para ver o resultado final.
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. 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
btmon
e 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
Koevin_KempKoevin Kempno mês passado
{Parte de uma série
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.