Qualcomm DragonBoard: Utilize o Bluetooth Low Energy nas suas aplicações!

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:

DragonBoard: bluetooth low energy
Figura 1 – 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:

DragonBoard: bluetooth low energy
Figura 2 – Exemplo de 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:

DragonBoard: bluetooth low energy
Figura 3 – DragonBoard 410C

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:

dpkg --status bluez

Você deve receber algo parecido com isso aqui:

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: https://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:

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:

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:

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:

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:

cpack ..

Em seguida instale o pacote, utilizando o gerenciador de pacotes do Linux:

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:

#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:

/**
 *          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

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Comunicação » Qualcomm DragonBoard: Utilize o Bluetooth Low Energy nas suas aplicações!

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: