ÍNDICE DE CONTEÚDO
Olá caro leitor, como vai? Ganhei há alguns meses um combo contendo uma Qualcomm DragonBoard, fornecido pela Arrow, contendo a poderosa (mas mal explorada) SoC SnapDragon 400. Uma das características chave (porém nem tão inovadora) dessa pequena placa de desenvolvimento é a presença de uma SoC Bluetooth low energy perfeita para fazer dela um cliente BLE e por consequência um gateway para… IoT! Nesse artigo pretendemos ir além da linha de comando e vamos realmente fazer uma aplicação em C para acessar os serviços e características de um periférico qualquer.
Revisitando a pilha Bluetooth Low Energy
Antes de qualquer coisa, temos que dar aquela revisada nos componentes principais da pilha bluetooth low energy, que chamarei de BLE daqui pra frente. De modo geral ela é bem parecida com o já conhecido bluetooth classic, a sua distinção ocorre nas camadas superiores, principalmente em uma que não existe no bluetooth classic, o GATT. A camada do GATT é responsável por descrever os diversos serviços e as características existentes dentro destes funcionando como endpoint para troca de dados. Abaixo vamos ver a arquitetura simplificada da pilha BLE:
Além do GATT, a pilha BLE também possui distinções de como os dispositivos devem operar dentro de uma conexão. Esse “modus operandi” do dispositivo fica na camada GAP (Generic Access Profile), e basicamente define o dispositivo como sendo servidor, ou cliente do ponto de visa de conexão. Para simplificar as coisas, um dispositivo servidor é tipicamente um periférico, podendo ser um vestível ou um beacon. Na nossa aplicação, vamos utilizar um Bosch XDK para fazer esse trabalho.
Já o cliente, costuma ser o smarphone, tablet, ou notebook. Do ponto de vista de conexão o cliente procura por periféricos que estejam indicando presença (através do Advertisement), encontrando o desejado, ele requisita uma conexão, que uma vez estabelecida, permite que o cliente faça a descoberta dos serviços existentes e realize a troca de dados, e é onde colocaremos a DragonBoard, visto que ela possui características típicas de um cliente, gerencia e roteia os dados para um serviço em nuvem. Vejam abaixo uma linha do tempo simplificada de uma conexão BLE:
Agora que estamos com o modelo de comunicação e troca de dados do BLE, vamos revisitar o dispositivo que fará o papel do cliente, a DragonBoard.
DragonBoard: Revisitando suas capacidades para BLE
Falar de todas as características da DragonBoard foge ao escopo desse artigo, a placa, bem como o SoC possui seus predicados e seus pontos fracos (mais ligados a falta de suporte ou documentação obscura, o mesmo que uma Raspberry ou derivados sofrem), por isso vamos falar do que interessa para esse artigo, o Bluetooth.
Uma das coisas legais dessa placa reside no fato da solução inteira ser Qualcomm, ou seja se por um lado você leitor não vai ter nenhum suporte por parte deles, por outro o time de engenharia fez bem o dever de casa deixando o HCI, um dos layers mais baixos da pilha BLE, totalmente portado e funcional, seja rodando Android ou Linux embarcado. Só para refrescar a memória, vejam uma foto dessa placa:
A PHY já vem com uma antena on-board, e aqui vale um elogio, nos meus testes obtive alcances que foram além dos 50 metros em ambiente com paredes, claro que parte desse desempenho se deve também ao conjunto de recepção do periférico. Do lado do software, tanto do ponto de vista do Android, como pro Linux embarcado, a principal forma de interagir com a PHY é utilizando o a boa e velha pilha bluetooth também desenvolvida pela Qualcomm, o BlueZ.
Mas você pode questionar: “Poxa, li até aqui pra ver mais outro tutorial de BlueZ, é sempre a mesma coisa, abre socket RFCOMM, e manda comando HCI”. Foi o que eu pensei ao ganhar a placa,e depois de experiências horríveis com o Bluetooth nada documentado da Intel Edison, porém a pilha bluetooth que compõem a imagem da Linaro, sofreu grandes avanços e conseguiu fornecer as primitivas para acesso ao GATT através de um mecanismo de comunicação bem conhecido do Linux, o DBUS.
Mas é fato que usar DBUS requer paciência, esse IPC não é dos mais amigáveis, ainda mais partindo do zero e será no próximo tópico onde vamos explicar como lidar com ele.
BLE: Preparando o ambiente na DragonBoard
Agora vamos preparar a DragonBoard para deixar o BLE acessível, falamos do DBUS alí em cima, porém podemos ficar aliviados, não precisaremos lidar com ele, ao menos não diretamente, para fazer as coisas funcionarem vamos precisar do seguinte:
- DragonBoard 410C carregada com Linux embarcado;
- BlueZ versão 4.1 ou superior;
- Gattlib;
- libbluetooth-dev;
- libreadline-dev;
- CMake;
A receita de bolo para ter tudo isso funcionando e falando harmoniosamente parece complicada, mas não assusta ninguém, a começar pela imagem Linux a ser carregada na DragonBoard 410C. Gerar essa imagem foge ao escopo desse artigo, clicando aqui você vai ter o melhor guia para te ajudar nessa tarefa, utilizamos para teste a última versão da imagem fornecida pela Linaro. Depois que a placa estiver com a imagem devidamente instalada, é hora de checar o BlueZ, de um modo geral ele vem instalado por default se não for o caso, o passo de instalação deverá ser feito. Para isso, abra um terminal, ou uma sessão por ssh e conecte-se na sua DragonBoard. Não sabe ainda como acessar ela pelo console? Então faça uma visitinha nesse link externo aqui.
Ao estabelecer a conexão digite:
1 |
dpkg --status bluez |
Você deve receber algo parecido com isso aqui:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Package: bluez Status: install ok installed Priority: optional Section: admin Installed-Size: 4119 Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> Architecture: amd64 Multi-Arch: foreign Version: 5.37-0ubuntu5.1 Replaces: bluez-alsa, bluez-audio (<= 3.36-3), bluez-input, bluez-network, bluez-serial, bluez-utils (<= 3.36-3), udev (<< 170-1) Depends: libc6 (>= 2.15), libdbus-1-3 (>= 1.9.14), libglib2.0-0 (>= 2.31.8), libreadline6 (>= 6.0), libudev1 (>= 196), init-system-helpers (>= 1.18~), lsb-base (>= 4.1+Debian11ubuntu7), kmod, udev (>= 170-1), dbus Breaks: udev (<< 170-1) Conflicts: bluez-alsa, bluez-audio (<= 3.36-3), bluez-utils (<= 3.36-3) Conffiles: /etc/bluetooth/input.conf 9f85017f861ac34d983fa76fa715f9c3 /etc/bluetooth/main.conf 6123cc0548b077b77c99379f9b93b080 /etc/bluetooth/network.conf 0c7497c405b963382ff71789d0730abd /etc/bluetooth/proximity.conf b75823a140e00905d41465c380bf89fe /etc/dbus-1/system.d/bluetooth.conf 2fd2de572a1221533e707d058e64e33a /etc/init.d/bluetooth 33ed7811d65a775cf10f04c2e6ee3cbf /etc/init/bluetooth.conf c7e11afe4581c8829a79a5ac8aa558b5 Description: Bluetooth tools and daemons This package contains tools and system daemons for using Bluetooth devices. . BlueZ is the official Linux Bluetooth protocol stack. It is an Open Source project distributed under GNU General Public License (GPL). Homepage: http://www.bluez.org Original-Maintainer: Debian Bluetooth Maintainers <pkg-bluetooth-maintainers@lists.alioth.debian.org> |
Se o BlueZ não estiver instalado esse passo vai precisar ser realizado, como instalar o BlueZ foge ao escopo desse artigo não vamos entrar em detalhes de como instalar, se você precisar fazer esse passo, vá logo nesse guia aqui.
Checado os dois componentes que deveriam vir por padrão na DragonBoard, vamos resolver os pacotes necessários para instalação da gattlib, para isso instale os pacotes abaixo utilizando o gerenciador de pacotes do Linux, na DragonBoard:
1 |
sudo apt-install libbluetooth-dev libreadline-dev |
Agora vamos copilar a gattlib dentro da DragonBoard, certique-se que o CMAKE esteja instalado, caso contrário, instale-o:
1 |
sudo apt-get install cmake |
Para enviar os fontes da gattlib para sua máquina existem dois caminhos. Para não ser preciso instalar o git-core dentro da DB, eu clonei o repositorio na minha máquina host:
1 |
git clone https://github.com/labapart/gattlib.git |
Descompactei em um local conhecido e enviei para DragonBoard utilizando a dobradinha SSH com SCP, se o leitor tiver dúvidas, acho que visitar esse local aqui, vai ajudar a entender o que esses comandos significam.
Com todos os arquivos na DragonBoard, podemos agora compilar a gattlib, para isso, volte ao console serial, e acesse o local onde você copiou os arquivos da gattlib, e rode o cmake para compilar a lib:
1 2 3 4 |
cd <local/onde/voce/salvou/a/gattlib/naDragonBoard> mkdir build && cd build cmake .. make |
O build é rápido e vai gerar os binários (shared libraries) necessários para que você utilize a gattlib nas suas aplicações. Ao compilar as suas aplicações com a gattlib, você precisa adcionar a flag -L e passar o local onde está o binário da lib, juntamente com a flag -lgattlib para indicar a lib que deseja linkar ao código final. Adicionalmente você pode utilizar o utilário cpack e gerar um pacote RPM, para isso digite o comando abaixo de dentro do diretório build:
1 |
cpack .. |
Em seguida instale o pacote, utilizando o gerenciador de pacotes do Linux:
1 |
sudo dpkg -i <local/onde/foi/parar/o/pacote/gattlib> |
De um modo geral, o pacote acaba ficando no mesmo diretório de build de onde o comando cpack foi invocado, fazendo esse passo, você não vai precisar das flags -L e -l, bastando apenas fazer isso aqui pra usar a gattlib (e portanto o BLE) do seu código em C:
1 |
#include <gattlib.h> |
E pronto! Sua DragonBoard está pronta para ter aplicações capazes de invocar o uso do layer GATT do BLE e trocar dados com seus periféricos, agora vamos usar nosso novo módulo!
BLE: Desenvolvendo com a gattlib na DragonBoard
Agora temos um método de acessar o BLE, ou seja, poderemos agora desenvolver nossa aplicação. Para aumentar a produtividade desse texto resolvi compartilhar com vocês o código fonte de um projeto que estava envolvido e que roda em uma DragonBoard, a primeira versão do gateway do BeeInformed um projeto envolvendo monitoramento de colmeias possuía um módulo chamado de gerenciador de sensores, esse módulo utilizava o próprio BLE e fazia algumas coisas interessantes:
- Escanear os sensores próximos periodicamente;
- Estabeler a conexão;
- Fazer descoberta de serviços e características;
- Trocar dados entre sensor e cliente (DragonBoard).
Esse pedaço da aplicação foi desenvolvida em C, o código pode ser obtido aqui, o procedimento para copiar o repositório e compilar na DragonBoard é exatamente igual o para compilar a gattlib, basta copiar os arquivos para a DragonBoard, e dentro do diretório digitar o comando make.
Embora seja um projeto que possa servir de ponto de partida, o objetivo do uso desse código é mostrar o passo-a-passo para escanear, conectar, descobrir e trocar dados por BLE, para isso vejam o arquivo principal do gerenciador de sensores abaixo, é um pouco extenso mas nos limitaremos apenas a pontos relevantes ao BLE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 |
/** * THE BeeInformed Team * @file app_ble.c * @brief beeinformed edge sensor connection application */ #include "beeinformed_gateway.h" /** default adapter name */ #define BEEINFO_BLE_DEF_ADAPTER "hci0" /** scan timeout value */ #define BEEINFO_BLE_DEF_TIMEOUT 2 /** defines the messaging max slot size */ #define BLE_MESSAGE_SLOT_SIZE sizeof(ble_data_t) /** define the sleep period in seconds */ #define BEEINFO_BLE_SCAN_SLEEP_TIME (1000 * 500) #define BEEINFO_BLE_ACQ_PERIOD (1000 * 1) /** characteristics handle */ #define BLE_TX_HANDLE 0x0010 #define BLE_RX_HANDLE 0x0012 #define BLE_NOTI_HANDLE 0x0013 /** connected device manager structure */ /** static variables */ static pthread_t ble_conn_thread; static pthread_attr_t ble_conn_att; static pthread_mutex_t cfg_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t scan_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t gatt_mutex = PTHREAD_MUTEX_INITIALIZER; static bool ble_conn_should_run = true; static void* hci_adapter = NULL; char *cfg; k_list_t ble_devices; /** static funcions */ /** * @fn ble_comm_timeout() * @brief handles communication timeout * * @param * @return */ static void ble_comm_timeout(union sigval s) { ble_device_handle_t *dev = (ble_device_handle_t *)s.sival_ptr; if(dev != NULL) { printf("%s : --------------- COMMUNICATION TIMEOUT ---------------\n\r\n\r", __func__); ble_data_t packet; packet.type = k_command_packet; packet.id = 0xFF; mqd_t mq; struct mq_attr attr; attr.mq_flags = O_NONBLOCK; char mq_str[32] = {0}; strcpy(mq_str, "/mq_"); strcat(mq_str, dev->bd_addr); mq = mq_open(mq_str,O_WRONLY, 0644, &attr); mq_send(mq, (uint8_t *)&packet, sizeof(packet) , 0); mq_close(mq); printf("%s : --------------- COMMUNICATION HANDLERED ---------------\n\r\n\r", __func__); } } /** * @fn ble_add_device_to_list() * @brief handles device discovering * @param * @return */ static bool ble_add_device_to_list(char * path , ble_device_handle_t *h) { bool ret = true; ble_device_handle_t dev; bool found = false; int bread= 0; FILE *fp = fopen(path, "rb"); printf("%s:Config file: %s \n\r", __func__, path); pthread_mutex_lock(&cfg_mutex); /* this should never happen */ assert(h != NULL); assert(fp != NULL); for(;;){ /* reads one entry of the config file */ bread = fread( &dev, 1, sizeof(ble_device_handle_t), fp ); if(!bread) { printf("%s: reached on end of file exiting \n\r", __func__); break; } else { printf("%s: Current device found: %s \n\r", __func__, dev.bd_addr); /* compare if the entry already exist */ if(!strcmp(h->bd_addr, dev.bd_addr)) { printf("%s:----------------DEVICE FOUND IT ACQUISITION WILL BE RESTORED ------------\n\r", __func__); found = true; break; } } } fclose(fp); /* if no such device, add it as a new one */ if(!found) { printf("%s:----------------NEW BEEHIVE SENSOR ADDING IT ON KNOWNS LIST ------------\n\r", __func__); fp = fopen(path, "ab"); fwrite (h, 1, sizeof(ble_device_handle_t), fp ); fclose(fp); ret = true; } else { ret = false; } pthread_mutex_unlock(&cfg_mutex); return(ret); } /** * @fn ble_rx_handler() * @brief handles the incoming data from BLE device * @param * @return */ static void ble_rx_handler(const uuid_t* uuid, const uint8_t* data, size_t data_length, void* user_data) { ble_device_handle_t *dev = (ble_device_handle_t *)user_data; ble_data_t dump; assert(dev != NULL); memcpy(&dump, data, data_length); //printf("%s: device: %s \n\r", __func__, dev->bd_addr ); //printf("%s: type: %d!! \n\r", __func__, dump.type); //printf("%s: id: %d!! \n\r", __func__, dump.id); //printf("%s: pack_amount: %d!! \n\r", __func__, dump.pack_amount); //printf("%s: payload_size: %d!! \n\r", __func__, dump.payload_size); /* data received, store on queue for furthre processing */ mqd_t mq; struct mq_attr attr; attr.mq_flags = O_NONBLOCK; char mq_str[32] = {0}; strcpy(mq_str, "/mq_"); strcat(mq_str, dev->bd_addr); mq = mq_open(mq_str,O_WRONLY, 0644, &attr); if(mq_send(mq, (uint8_t *)&dump, sizeof(dump) , 0) < 0) { printf("%s: queue seems to be full! \n\r", __func__); } mq_close(mq); } /** * @fn ble_device_handle_acquisition() * @brief handles device acquisition * @param * @return */ static inline void ble_device_handle_acquisition(ble_device_handle_t *h) { /* this should never happen */ assert(h != NULL); ble_data_t packet = {0}; uint8_t mq_data[sizeof(ble_data_t) * 2]; ble_data_t *rx_packet = (ble_data_t *)&mq_data; int ret; packet.type = k_command_packet; packet.id = k_get_sensors; /* send the command to the current sensor node */ printf("%s: sending command to sensor node\n\r", __func__); ret = gattlib_write_char_by_handle(h->conn_handle, BLE_TX_HANDLE, &packet, sizeof(packet)); if(ret) { fprintf(stderr, "failed to send command to device .\n"); h->should_run = false; goto cleanup; } else { h->timer.trigger.it_value.tv_sec = BLE_COMM_TIMEOUT; timer_settime(h->timer.timerid, 0, &h->timer.trigger, NULL); printf("%s: packet sent to device, waiting response\n\r", __func__); } if(mq_receive(h->mq, mq_data, sizeof(mq_data), NULL) < sizeof(rx_packet)) { printf("%s: corrupt packet arrived, discarding!! \n\r", __func__); printf("%s: type: %d!! \n\r", __func__, rx_packet->type); printf("%s: id: %d!! \n\r", __func__, rx_packet->id); printf("%s: pack_amount: %d!! \n\r", __func__, rx_packet->pack_amount); /* some error occurred, so destroy the thread and waits a reconnection */ h->should_run = false; } else { if(rx_packet->type == k_command_packet) { /* a command packet here, indicate fault with communication, exit */ h->should_run = false; goto cleanup; } /* stops timer until packet processing */ h->timer.trigger.it_value.tv_sec = 0; timer_settime(h->timer.timerid, 0, &h->timer.trigger, NULL); uint32_t packet_cnt = rx_packet->pack_amount - 1; uint8_t *ptr = (uint8_t *)&h->data_env; bool error = false; memcpy(ptr, &rx_packet->pack_data, rx_packet->payload_size); ptr += rx_packet->payload_size; while (packet_cnt) { /* rearm timer to avoid deadlock */ h->timer.trigger.it_value.tv_sec = BLE_COMM_TIMEOUT; timer_settime(h->timer.timerid, 0, &h->timer.trigger, NULL); if(mq_receive(h->mq, mq_data, sizeof(mq_data), NULL) < sizeof(rx_packet)) { /* stops timer until packet processing */ h->timer.trigger.it_value.tv_sec = 0; timer_settime(h->timer.timerid, 0, &h->timer.trigger, NULL); if(rx_packet->type == k_command_packet) { /* a command packet here, indicate fault with communication, exit */ h->should_run = false; goto cleanup; } printf("%s: corrupt packet arrived, discarding!! \n\r", __func__); h->should_run = false; goto cleanup; } /* stops timer until packet processing */ h->timer.trigger.it_value.tv_sec = 0; timer_settime(h->timer.timerid, 0, &h->timer.trigger, NULL); if(!error) { memcpy(ptr, &rx_packet->pack_data, rx_packet->payload_size); ptr += rx_packet->payload_size; } packet_cnt--; } } /* prints the data */ printf("%s: data sent by sensor_id: %s are: \n\r", __func__, h->bd_addr); printf("Temperature: %u [mdeg] \n\r", h->data_env.temperature); printf("Humidity: %u [percent]\n\r", h->data_env.humidity); printf("Pressure: %u [Pa]\n\r", h->data_env.pressure); printf("Luminance: %u [mLux] \n\r", h->data_env.luminosity); cleanup: return; } /** * @fn ble_discover_service_and_enable_listening() * @brief discover device characteristics and enable notification * @param * @return */ static void ble_discover_service_and_enable_listening(ble_device_handle_t *h) { int ret; /* this should never happen */ assert(h != NULL); /* discover device characteristic and services */ ret = gattlib_discover_primary(h->conn_handle, &h->services, &h->services_count); if (ret != 0) { fprintf(stderr, "Fail to discover primary services.\n"); goto cleanup; } for (int i = 0; i < h->services_count; i++) { gattlib_uuid_to_string(&h->services[i].uuid, h->uuid_str, sizeof(h->uuid_str)); printf("%s: service[%d] start_handle:%02x end_handle:%02x uuid:%s\n",__func__, i, h->services[i].attr_handle_start, h->services[i].attr_handle_end, h->uuid_str); } ret = gattlib_discover_char(h->conn_handle, &h->characteristics, &h->characteristics_count); if (ret != 0) { fprintf(stderr, "Fail to discover characteristics.\n"); goto cleanup; } for (int i = 0; i < h->characteristics_count; i++) { gattlib_uuid_to_string(&h->characteristics[i].uuid, h->uuid_str, sizeof(h->uuid_str)); printf("%s: characteristic[%d] properties:%02x value_handle:%04x uuid:%s\n", __func__, i, h->characteristics[i].properties, h->characteristics[i].value_handle, h->uuid_str); } /* enable listening by setting nofitication and read characteristic * bitmask */ uint16_t char_prop = 0x000C; ret = gattlib_write_char_by_handle(h->conn_handle, BLE_TX_HANDLE+1, &char_prop, sizeof(char_prop)); if(ret) { fprintf(stderr, "failed set tx characteristic properties.\n"); } char_prop = 0x0003; ret = gattlib_write_char_by_handle(h->conn_handle, BLE_NOTI_HANDLE, &char_prop, sizeof(char_prop)); if(ret) { fprintf(stderr, "failed set noti characteristic properties.\n"); } gattlib_register_notification(h->conn_handle, ble_rx_handler, h); cleanup: return; } /** * @fn ble_device_manager_thread() * @brief handles device discovering * @param * @return */ static void *ble_device_manager_thread(void *args) { ble_device_handle_t *handle = args; FILE *fp_acq; FILE *fp_audio; char root_path[MAX_NAME_SIZE]={0}; char aud_path[MAX_NAME_SIZE]={0}; char acq_path[MAX_NAME_SIZE]={0}; printf("%s:-------------- NEW DEVICE PROCESS STARTED! ----------------\n\r", __func__); strcat(root_path, "beeinformed/"); strcat(root_path, handle->bd_addr); strcat(aud_path, root_path); strcat(aud_path,"/beeaudio.dat"); strcat(acq_path, root_path); strcat(acq_path,"/beedata.dat"); printf("%s:-------------- SETTING BEE DEVICE ENVIRONMENT ----------------\n\r", __func__); printf("%s: Audio File: %s \n\r", __func__, aud_path); printf("%s: Hive environment File: %s \n\r", __func__, acq_path); printf("%s:---------------------------------------------------------------\n\r", __func__); if(handle->new_device) { mkdir(root_path,0644); } /* obtains the acquisition file of the device */ fp_acq =fopen(acq_path, "ab"); fp_audio =fopen(aud_path, "ab"); assert(fp_audio != NULL); assert(fp_acq != NULL); /* obtains device connection handle */ handle->conn_handle = gattlib_connect(NULL, handle->bd_addr, BDADDR_LE_PUBLIC, BT_SEC_LOW, 0, 200); if (handle->conn_handle == NULL) { handle->conn_handle = gattlib_connect(NULL, handle->bd_addr, BDADDR_LE_RANDOM, BT_SEC_LOW, 0, 200); if (handle->conn_handle == NULL) { fprintf(stderr, "Fail to connect to the bluetooth device.\n"); goto cleanup; } else { printf("%s: Succeeded to connect to the bluetooth device with random address.\n\r", __func__); } } else { printf("%s: Succeeded to connect to the bluetooth device.\n\r", __func__); } /* enable the notifications and gets the service database */ printf("%s:---------- DISCOVERING BEEINFORMED EDGE BLE DATABASE -----------\n\r", __func__); ble_discover_service_and_enable_listening(handle); printf("%s:---------- DISCOVERED BEEINFORMED EDGE BLE DATABASE -----------\n\r", __func__); /* creates the timeout channel */ memset(&handle->timer.sev, 0, sizeof(struct sigevent)); memset(&handle->timer.trigger, 0, sizeof(struct itimerspec)); handle->timer.sev.sigev_notify = SIGEV_THREAD; handle->timer.sev.sigev_notify_function = &ble_comm_timeout; handle->timer.sev.sigev_value.sival_ptr = handle; handle->timer.trigger.it_value.tv_sec = BLE_COMM_TIMEOUT; if(timer_create(CLOCK_REALTIME, &handle->timer.sev, &handle->timer.timerid) < 0) { /* failed to create timer, exit */ fprintf(stderr, "ERROR: Failed to create ble device timer.\n"); goto cleanup; } /* creates a messaging system to store messages */ handle->attr.mq_flags = 0; handle->attr.mq_maxmsg = 128; handle->attr.mq_msgsize = BLE_MESSAGE_SLOT_SIZE; handle->attr.mq_curmsgs = 0; char mq_str[32] = {0}; strcpy(mq_str, "/mq_"); strcat(mq_str, handle->bd_addr); printf("%s: mqueue name: %s \n\r", __func__, mq_str); /* close the mqueue before to use it, this will flushes the queue */ mq_unlink(mq_str); handle->mq = mq_open(mq_str, O_CREAT | O_RDWR, 0644, &handle->attr); if(handle->mq < 0) { fprintf(stderr, "ERROR: Failed to create ble device managerqueue.\n"); goto cleanup; } /* connection estabilished, now just manages the device * until connection closes */ while(handle->should_run && (handle->conn_handle != NULL)) { ble_device_handle_acquisition(handle); usleep(BEEINFO_BLE_ACQ_PERIOD); } cleanup: printf("%s:-------------- EDGE DEVICE THREAD TERMINATING! ----------------\n\r", __func__); fclose(fp_audio); fclose(fp_acq); timer_delete(handle->timer.timerid); mq_close(handle->mq); mq_unlink(mq_str); gattlib_disconnect(handle->conn_handle); free(handle); return(NULL); } /** * @fn ble_discovered_device() * @brief handles device discovering * @param * @return */ static void ble_discovered_device(const char* addr, const char* name) { int ret; printf("%s:------------------ DEVICE DISCOVERED! ------------------\n\r", __func__); printf("%s: NAME: %s \n\r", __func__, name); printf("%s: BD_ADDRESS: %s \n\r", __func__, addr); printf("%s:--------------------------------------------------------\n\r", __func__); if(!strcmp(name, "beeinformed_edge")) { ble_device_handle_t *handle = malloc(sizeof(ble_device_handle_t)); memset(handle, 0, sizeof(ble_device_handle_t)); assert(handle != NULL); /* gets the device information */ strcpy(&handle->bd_addr[0], addr); strcpy(&handle->device_name[0], name); handle->new_device = ble_add_device_to_list(cfg, handle); handle->should_run = true; /* add device on connected devices list */ sys_dlist_init(&handle->link); sys_dlist_append(&ble_devices, &handle->link); /* creates and starts the device thread */ ret = pthread_create(&handle->ble_device_thread, &handle->ble_dev_att,ble_device_manager_thread, handle); if(ret) { fprintf(stderr, "ERROR: Failed to start ble device manager.\n"); } } cleanup: return; } /** * @fn ble_connection_manager_thread() * @brief connection manager background task * @param * @return */ static void *ble_connection_manager_thread(void *args) { int ret; (void)args; printf("%s: starting beeinformed connection manager! \n\r", __func__); while(ble_conn_should_run) { /* now we start the discovery and create connections thread * to each new device */ printf("%s:-----------------SCANNING BLE DEVICES! -----------------------\n\r", __func__); pthread_mutex_lock(&scan_mutex); /* perform some basic initialization and open the bt adapter */ ret = gattlib_adapter_open(BEEINFO_BLE_DEF_ADAPTER, &hci_adapter); if (ret) { fprintf(stderr, "ERROR: Failed to open adapter.\n"); pthread_mutex_unlock(&scan_mutex); continue; } ret = gattlib_adapter_scan_enable(hci_adapter, ble_discovered_device,BEEINFO_BLE_DEF_TIMEOUT); if(ret) fprintf(stderr, "ERROR: Failed to scan.\n"); gattlib_adapter_scan_disable(hci_adapter); gattlib_adapter_close(hci_adapter); pthread_mutex_unlock(&scan_mutex); printf("%s:-----------------END OF SCANNING BLE DEVICES! -----------------------\n\r", __func__); usleep(BEEINFO_BLE_SCAN_SLEEP_TIME); } /* if application was terminated, disconnects all the devices */ ble_device_handle_t *dev; SYS_DLIST_FOR_EACH_CONTAINER(&ble_devices,dev , link) { /* disconnects and free the memory */ dev->should_run = false; pthread_join(dev->ble_device_thread, NULL); } return(NULL); } /** public functions */ void beeinformed_app_ble_start(char *path) { int ret = 0; cfg = path; sys_dlist_init(&ble_devices); /* creates and starts the connman thread */ ret = pthread_create(&ble_conn_thread, &ble_conn_att,ble_connection_manager_thread, NULL); if(ret) { fprintf(stderr, "ERROR: Failed to start ble conn manager.\n"); goto cleanup; } cleanup: return; } void beeinformed_app_ble_finish(void) { /* request conn man to terminate */ ble_conn_should_run = false; pthread_join(ble_conn_thread, NULL); } int beeinformed_app_ble_send_data(void *data, size_t size, app_ble_data_tag_t tag) { int ret; /** TODO */ return(ret); } |
Agora, qual é o processo para efetuar a conexão? Antes de tudo, temos que iniciar o adaptador bluetooth presente da DragonBoard, ele está enumerado como sendo o device hci0. Na linha 533, o usuário vai notar a chamada da função gattlib_adapter_open(), essa função toma dois argumentos, o nome do device ou seja hci0, e um ponteiro onde será depositado uma estrutura de informação dos devices, se a execução dessa função ocorrer com sucesso, a aplicação deterá o controle do bluetooth da DragonBoard, assim podemos passar para o proximo passo.
Como dissemos no começo desse artigo, o servidor do ponto de vista do BLE faz o que chamamos de advertisement, ou seja, periodicamente ele envia um pacote indicando presença. O papel do cliente é monitorar a chegada periodica desses pacotes, ou seja, devemos fazer um ciclo de scan, para isso na linha note a chamada gattlib_adapter_scan_enable(), essa função fala para o BLE iniciar o ciclo de escaneamento. O curioso é que diferente do RFCOMM, essa função somente irá considerar dispositivos BLE, essa chamada recebe três parametros, um deles é ponteiro com as informações do adaptador que você passou na chamada anterior, o segundo parametro é uma callback que será chamada toda vez que um dispositivo for encontrado, o terceiro parametro indica quanto tempo o ciclo de scan deve ocorrer.
Quando o uso do escaneamento não for mais necessário a aplicação poderá invocar o gattlib_adapter_scan_disable() e gattlib_adapter_close() nessa ordem liberando o uso do scanner para outras aplicações. Agora vamos para a linha 477, que basicamente contém a implementação da callback de dispositivo BLE encontrado, essa função recebe dois parâmetros, sendo o nome do dispositivo e seu MAC, o bom desses parâmetros é que o MAC recebido pode diferenciar um dispositivo do outro, fazendo com que o cliente consiga gerenciar múltiplos sensores. A implementação particular dessa callback, basicamente cria uma pthread (em caso de dúvidas sobre POSIX threads, sugiro a leitura desse excelente artigo aqui) passando como argumento uma estrutura de contexto, contendo nome e MAC.
Na linha 362 temos a função que implementa a pthread criada, o primeiro ponto é receber o contexto do device para qual essa thread foi criada, em seguida de posse dessas informações vamos para a linha 397, onde é invocada a função gattlib_connect(), que recebe dentre os parâmetros o MAC do dispositivo que desejamos conectar, o retorno dessa função é um handle, que é guardado em um local seguro, pois usaremos ele para trocar os dados com o dispositivo.
Nesse momento temos o dispositivo conectado, mas ainda não sabemos que tipos de serviços ele suporta ou se sabemos como escrever ou ler nas suas caracteristicas, o sensor em questão é um Bosch XDK, que possui um serviço BLE chamado de Exchange Data, com duas características, sendo uma para TX e outra para RX, em cada uma delas é possível escrever ou receber até 20bytes por transação, apesar desse código ter sido direcionado para esse sensor node (que caso o leitor se interesse, temos também mais um post sobre ele aqui), qualquer dispositivo BLE pode ser utilizado como periférico.
Se a requisição de conexão for um sucesso, vamos agora para o próximo passo de uma conexão BLE a descoberta dos serviços, para isso vamos para a linha 305, a chamada da função gattlib_discover_primary(), essa função irá devolver os serviços existentes e mais que isso, a quantidade de características disponíveis no dispositivo encontrado que são retornados nos dois argumentos que são passados para essa função.
Na linha 319, finalmente faremos a descoberta das características, ou seja, os endpoints onde são efetivamente enviados e recebidos dados por BLE. A função gattlib_discover_char() é responsável por isso, no retorno é importante o usuário verificar quais são os handles das características de TX e RX do serviço Data Exchange, essas constantes estão definidas pro XDK, mas para seu dispositivo e serviço podem ser diferentes, no final do artigo tem um link para o BLE SIG que lista os serviços e características padrão. De posse desses dois handles, resta apenas um passo a fazer.
Acertar a forma como o dispositivo responde a solicitação de dados por BLE, esse passo é opcional, pois a transferência de dados é assíncrona, ou seja, precisamos deixar a característica de RX do cliente aguardando por evento de transmissão do servidor, para isso habilitamos a opção de notificação utilizando a função gattlib_write_char_by_handle() da linha 344, embora exista mais uma chamada ela não entra no escopo desse artigo, essa função faz a primeira troca de dados efetivas e envia ao descritor de característica do serviço de Data Exchange uma requisição avisando que o cliente, vai atender os chamados de notificação da característica de RX.
Com esse último passo, encerramos o ciclo de conexão em um periférico, agora podemos registrar uma callback para receber dados do BLE pela característica de RX, na linha 348 que chama a função gattlib_register_notification(), na implementação da callback na linha 144, nenhum trabalho precisa ser feito, pois na sua chamada ela vem com o id da característica que notificou, o ponteiro para os dados e o número de bytes recebidos, na minha implementação como fraciono os pacotes de dados, utilizo uma POSIX queue para o processo de remontagem dos pacotes, mas em muitos casos é comum receber menos de 20 bytes em um pacote não sendo necessário qualquer tipo de estratégia de fatiamento de pacotes de dados.
E finalmente para enviar dados ao sensor, utilizamos a mesma função gattlib_write_char_by_handle(), vejam a linha 196, nesse caso utilizamos o handle da característica de TX, que obtivemos durante a descoberta de serviços do periférico, os parâmetros seguintes são triviais, basicamente o os dados e o número de bytes enviados nessa transação, no máximo 20, é possível trabalhar com payloads maiores, mas isso exige negociar o MTU para um valor maior durante o processo de conexão.
Conclusão
Caro leitor, nesse artigo nosso objetivo foi dar um subsídio além das ferramentas de linha de comando para que você consiga desenvolver sua aplicação com suporte BLE na Qualcomm DragonBoard, aproveitando a integração com o módulo Bluetooth existente nela. Optamos pelo caminho da aplicação em C pela facilidade de controlar seu cliente BLE, além da possibilidade de explorar recursos POSIX para efetuar sincronização e “bufferização” de dados, o que não impede o uso de outras linguagens, mas sim possibilita que o processamento dos dados dos sensores seja feito em mais baixo nível, e um Python possa fazer processamento dos dados numa camada superior da aplicação. Os repositórios aqui mencionados estão abertos a contribuições e são livres para uso, adicionalmente o repositório da própria gattlib possuem exemplos que podem servir de ponto de referência para as próximas aplicações envolvendo BLE. Se tiver dúvida, deixe seu comentário, que vamos te ajudar, até a próxima!
Referências