Ruuvi Firmware – Part 16: BLE GATT Connection

Ruuvi Firmware series part 16 intro image

In this part of the tutorial we’ll add Generic Attributes (GATT) profile to our project. Final code of this blog post can be downloaded at Ruuvi GitHub in the ruuviblog-branch, tag 3.16.0-alpha.

Please follow part 1 of the series for details on how to clone the repository and compile the code. Final hex of this tutorial can be downloaded from the Ruuvi Jenkins.

Generic Attributes for Ruuvi

“The Generic Attributes (GATT) define a hierarchical data structure that is exposed to connected Bluetooth Low Energy (LE) devices.” — Bluetooth.com

The GATT profiles have been well explained by Adafruit, please refer to their tutorial for more detailed explanation on what the GATT is.

On RuuviTag we’ll implement a simple profile with 3 services:

  • Device Firmware Update service (DFU)
  • Device Information Service (DIS)
  • Nordic UART Service (NUS)

Device Firmware Update service allows the tag to enter bootloader without physical user interaction.

Device Information Service exposes some core information about the device, such as manufacturer, hardware version and firmware version.

Nordic UART Service will be used to create 2-way communication between RuuviTag and an another device.

GATT Interface

Or interface is simple enough: DFU service only needs to be initialized without parameters, DIS service requires strings for data to display and NUS uses the previously defined communication channel interface. Additionally we’ll have a common initialization function which setups the GATT parameters

There is a lot of complexity hidden under the hood, such as connection intervals and negotiating them, supervisory timeout for the connection and so on. But as before, we want to keep things as simple as humanly possible and leave those settings as “reasonable defaults”.

#define HCI_ERROR_CODE_CONN_TERM_BY_LOCAL_HOST 0x16

typedef struct
{
 char fw_version[32];
 char model[32];
 char hw_version[32];
 char manufacturer[32];
 char deviceid[32];
}ruuvi_interface_communication_ble4_gatt_dis_init_t;

/**
 * Initializes GATT stack. Uses default values from sdk_config.h, 
 * these can be overridden in nrf5_sdk15_application_config.h
 *
 * Returns RUUVI_DRIVER_ERROR_INVALID_STATE 
 *         if radio module is not initialized 
 *         with handle RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT
 *
 *
 */
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_init(void);

/**
 * Initialize Nordic UART Service as a communication channel.
 * ruuvi_interface_communication_radio_init(RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT) 
 * must be called before initializing service
 *
 * Parameter channel: Pointer to communication interface which will be populated. 
 *                    Pointer will be copied, the structure
 *                    must be retained. Adding any event handler to structure after 
 *                    initialization will take effect immediately
 * Returns RUUVI_DRIVER_SUCCESS on success
 * Returns RUUVI_DRIVER_ERROR_INVALID_STATE 
 *         if radio module is not initialized with handle 
 *         RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT
 *         or if ruuvi_interface_communication_ble4_gatt_init
 *         has not been called.
 * Returns error code from stack in case there is other error.
 */
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_init
                      (ruuvi_interface_communication_t* const channel);

/**
 * Initialize BLE4 Device firmware update service.
 *
 * Returns RUUVI_DRIVER_SUCCESS on success
 * Returns error code from stack in case there is  error.
 */
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_dfu_init(void);

/**
 * Initialize BLE4 Device Information service
 *
 * parameter dis: pointer to data which should be presented over DIS.
 *                Memory will be deep-copied
 * Returns RUUVI_DRIVER_SUCCESS on success
 * Returns error code from stack in case there is  error.
 */
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_dis_init
             (const ruuvi_interface_communication_ble4_gatt_dis_init_t* const dis);

/**
 * Start or stop advertising GATT connection.
 *
 * parameter connectable: True to start advertising connectablity 
 *                        False to stop advertising connectablity
 * parameter name: Name of the device to be advertised.
 * parameter advertise_nus: True to enable advertising UUID of NUS in the scan response.
 * Returns RUUVI_DRIVER_SUCCESS on success
 * Returns error code from stack in case there is  error.
 *
 */
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_advertise_connectablity
                      (const bool connectable, 
                       const char* const name, 
                       const bool advertise_nus);

#endif

View raw

GATT Implementation

Luckily for us, Nordic Semiconductor has done the heavy lifting. Initializing GATT itself requires some thought, for now we hardcode in default values for things such as connection interval inside ruuvi_platform_communication_ble4_gatt.c

Adding DFU and DIS services is trivial matter of calling the Software Development Kit (SDK) functions. NUS is a bit trickier, since we need to interface the NUS with our application. We use the communication interface defined earlier which requires

  • Init — line 1
  • Uninit — line 25
  • Send — line 43
  • Read — line 64
  • Event handler — line 70

Previously a design decision was made to allow only one mode of radio communication at a time. Therefore we add a function to advertise the connection to nearby devices rather than using the previously made advertising interface — line 113.

ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_init
                     (ruuvi_interface_communication_t* const _channel)
{
  if(NULL == _channel) { return RUUVI_DRIVER_ERROR_NULL; }
  uint32_t           err_code;
  ble_nus_init_t     nus_init;

  // Initialize NUS.
  memset(&nus_init, 0, sizeof(nus_init));
  nus_init.data_handler = nus_data_handler;
  err_code = ble_nus_init(&m_nus, &nus_init);
  channel = _channel;
  channel->init   = ruuvi_interface_communication_ble4_gatt_nus_init;
  channel->uninit = ruuvi_interface_communication_ble4_gatt_nus_uninit;
  channel->send   = ruuvi_interface_communication_ble4_gatt_nus_send;
  channel->read   = ruuvi_interface_communication_ble4_gatt_nus_read;

  m_gatt_is_init = true;
  return ruuvi_platform_to_ruuvi_error(&err_code);
}

/**
 *
 */
static ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_uninit
                            (ruuvi_interface_communication_t* const _channel)
{
  if(NULL == _channel) { return RUUVI_DRIVER_ERROR_NULL; }
  memset(_channel, 0, sizeof(ruuvi_interface_communication_t));
  m_gatt_is_init = false;
  // disconnect
  if(BLE_CONN_HANDLE_INVALID != m_conn_handle)
  {
    sd_ble_gap_disconnect(m_conn_handle, HCI_ERROR_CODE_CONN_TERM_BY_LOCAL_HOST);
  }
  // Services cannot be uninitialized
  return RUUVI_DRIVER_SUCCESS;
}

/**
 *
 */
static ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_send
                            (ruuvi_interface_communication_message_t* const message)
{
  if(NULL == message) { return RUUVI_DRIVER_ERROR_NULL; }
  if(BLE_NUS_MAX_DATA_LEN < message->data_length) { return RUUVI_DRIVER_ERROR_DATA_SIZE; }
  if(BLE_CONN_HANDLE_INVALID == m_conn_handle) { return RUUVI_DRIVER_ERROR_INVALID_STATE; }

  if(message->repeat)
  {
    return RUUVI_DRIVER_ERROR_NOT_IMPLEMENTED;
  }

  ret_code_t err_code = NRF_SUCCESS;
  uint16_t data_len = message->data_length;
  err_code |= ble_nus_data_send(&m_nus, message->data, &data_len, m_conn_handle);
  return ruuvi_platform_to_ruuvi_error(&err_code);
}

/**
 *
 */
static ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_read
                            (ruuvi_interface_communication_message_t* const message)
{
  return RUUVI_DRIVER_ERROR_NOT_SUPPORTED;
}

static void nus_data_handler(ble_nus_evt_t * p_evt)
{
  if(NULL == channel ||
     NULL == channel->on_evt)
  {
    return;
  }

  switch(p_evt->type)
  {
    case BLE_NUS_EVT_RX_DATA:
      channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_RECEIVED, 
                      (void*)p_evt->params.rx_data.p_data, 
                      p_evt->params.rx_data.length);
      break;

    case BLE_NUS_EVT_COMM_STARTED:
      channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_CONNECTED, NULL, 0);
      break;

    case BLE_NUS_EVT_COMM_STOPPED:
      channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_DISCONNECTED, NULL, 0);
      break;

    case BLE_NUS_EVT_TX_RDY:
      channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_SENT, NULL, 0);
      break;

    default:
      break;
  }
}

/**
 * Start or stop advertising GATT connection.
 *
 * parameter connectable:   True to start advertising connectablity 
 *                          False to stop advertising connectablity
 * parameter name:          Name of the device to be advertised.
 * parameter advertise_nus: True to enable advertising 
 *                          UUID of NUS in the scan response.
 *
 */
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_advertise_connectablity
  (const bool connectable, 
   const char* const name, 
   const bool advertise_nus)
{
  ret_code_t err_code = NRF_SUCCESS;
  if(!connectable)
  {
    err_code |= sd_ble_gap_adv_stop(m_adv_handle);
    return ruuvi_platform_to_ruuvi_error(&err_code);
  }

// Initialize advertising parameters (used when starting advertising).
  memset(&m_adv_params, 0, sizeof(m_adv_params));
  m_adv_params.filter_policy   = BLE_GAP_ADV_FP_ANY;
  m_adv_params.duration        = 0;       // Never time out.
  m_adv_params.properties.type = BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED;
  m_adv_params.p_peer_addr     = NULL;    // Undirected advertisement.
  m_adv_params.interval        = 
    MSEC_TO_UNITS(APPLICATION_CONNECTION_ADVERTISEMENT_INTERVAL, UNIT_0_625_MS);

  // OPEN security for name
  ble_gap_conn_sec_mode_t security;
  uint8_t len = strlen(name);
  security.sm = 1;
  security.lv = 1;
  err_code |= sd_ble_gap_device_name_set(&security, (uint8_t*)name, len);

  memset(&m_adv_data, 0, sizeof(m_adv_data));
  memset(&m_advertisement, 0, sizeof(m_advertisement));
  memset(&m_scanresp, 0, sizeof(m_scanresp));
  ble_advdata_t advdata = {0};
  // Only valid flag
  advdata.flags     = BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED;
  // Name will be read from the above GAP data
  advdata.name_type = BLE_ADVDATA_FULL_NAME;

  // Encode data
  m_adv_data.adv_data.len = sizeof(m_advertisement);
  err_code |= ble_advdata_encode(&advdata, 
                                 m_advertisement, 
                                 &m_adv_data.adv_data.len);
  m_adv_data.adv_data.p_data = m_advertisement;

  // Add scan response if requested
  if(advertise_nus)
  {
    ble_advdata_t scanrsp = {0};
    scanrsp.uuids_complete.uuid_cnt = 1;
    scanrsp.uuids_complete.p_uuids = &(m_adv_uuids[0]);
    // Encode data
    m_adv_data.scan_rsp_data.len = sizeof(m_scanresp);
    err_code |= ble_advdata_encode(&scanrsp, 
                                   m_scanresp, 
                                   &m_adv_data.scan_rsp_data.len);
    m_adv_data.scan_rsp_data.p_data = m_scanresp;
  }

  // Configure advertisement and start
  err_code |= sd_ble_gap_adv_set_configure(&m_adv_handle, 
                                           &m_adv_data, 
                                           &m_adv_params);
  err_code |= sd_ble_gap_adv_start(m_adv_handle, NRF5_SDK15_BLE4_STACK_CONN_TAG);

  return ruuvi_platform_to_ruuvi_error(&err_code);
}

View raw

It should be noted that the read function will only return error for now. This is because we cannot poll external device for data, we can only handle data received events. The channel specification would have the data buffered and application could handle it at a later time, however we’ll implement it later. We also haven’t implemented repeated sends.

Integrating GATT to Application

Previously we hinted at using GATT to stream data from accelerometer at a faster rate. Let’s implement the accelerometer FIFO streaming. We comment out the FIFOand activity interrupt initialization at accelerometer boot and add acclerometer tasks to enable and disable the FIFO buffer and interrupt.

The accelerometer FIFO task is modified to send the data in ASCII-format with explicit sign, one digit and two fractions and semicolon, i.e.

+0.08;+0.04;+1.04

This fits within the 20 bytes of a single GATT transmission.

static void task_acceleration_fifo_full_task(void *p_event_data, uint16_t event_size)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  ruuvi_interface_acceleration_data_t data[32];
  size_t data_len = sizeof(data);
  err_code |= ruuvi_interface_lis2dh12_fifo_read(&data_len, data);
  char msg[APPLICATION_LOG_BUFFER_SIZE] = { 0 };
  for(int ii = 0; ii < data_len; ii++)
  {
    memset(msg, 0, sizeof(msg));
    snprintf(msg, 21,"%+4.2f;%+4.2f;%+4.2f", data[ii].x_g, data[ii].y_g, data[ii].z_g );
    ruuvi_interface_communication_message_t gatt_msg = { 0 };
    memcpy(gatt_msg.data, msg, 20);
    gatt_msg.data_length = 20;
    // Loop here until data is sent
    while(RUUVI_DRIVER_SUCCESS != task_gatt_send(&gatt_msg));
  }
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
}

ruuvi_driver_status_t task_acceleration_fifo_use(const bool enable)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  if(true == enable)
  {
    err_code |= ruuvi_interface_lis2dh12_fifo_use(true);
    err_code |= ruuvi_interface_lis2dh12_fifo_interrupt_use(true);
  }
  if(false == enable)
  {
    err_code |= ruuvi_interface_lis2dh12_fifo_use(false);
    err_code |= ruuvi_interface_lis2dh12_fifo_interrupt_use(false);
  }
}

View raw

We do not initialize the advertisement task in our main initialization, but rather we initialize the GATT task and start advertising the connection in GATT module. The initialization requires data for the DIS as well as a event handler for the NUS events.

One “gotcha” to pay attention to is the disconnection event. The advertisement does not continue by default, we need to manually restart advertising the connection at line 80 if we wish to connect to tag again.

While we’re at it we make the watchdog do something useful: We feed the watchdog on “data sent” event at line 85. If we lose connection to the tag or data cannot be sent anymore for some reason the tag will restart in 120 seconds.



static ruuvi_interface_communication_t channel;

ruuvi_driver_status_t task_gatt_init(void)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  ruuvi_interface_communication_ble4_gatt_dis_init_t dis = {0};
  uint64_t id;
  err_code |= ruuvi_interface_communication_id_get(&id);
  size_t index = 0;
  for (size_t ii = 0; ii < 8; ii ++)
  {
    index += snprintf(dis.deviceid + index, 
                      sizeof(dis.deviceid)-index, 
                      "%02X", 
                      (uint8_t)(id >> ((7 - ii) * 8)) & 0xFF);
    if(ii < 7) 
    { 
      index += snprintf(dis.deviceid + index, 
                        sizeof(dis.deviceid)-index, 
                        ":"); 
    }
  }
  memcpy(dis.fw_version, 
         APPLICATION_FW_VERSION, 
         sizeof(APPLICATION_FW_VERSION));
  memcpy(dis.model, 
         RUUVI_BOARD_MODEL_STRING, 
         sizeof(RUUVI_BOARD_MODEL_STRING));
  memcpy(dis.manufacturer, 
         RUUVI_BOARD_MANUFACTURER_STRING, 
         sizeof(RUUVI_BOARD_MANUFACTURER_STRING));

  err_code |= ruuvi_interface_communication_radio_init
    (RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT);
  // Init fails if something else - such as advertising - as reserved radio.
  if(RUUVI_DRIVER_SUCCESS != err_code) { return err_code; }
  err_code |= ruuvi_interface_communication_ble4_gatt_init();
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);

  err_code |= ruuvi_interface_communication_ble4_gatt_nus_init(&channel);
  channel.on_evt = task_gatt_on_gatt;
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);

  err_code |= ruuvi_interface_communication_ble4_gatt_dfu_init();
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);

  err_code |= ruuvi_interface_communication_ble4_gatt_dis_init(&dis);
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);

  err_code |= 
    ruuvi_interface_communication_ble4_gatt_advertise_connectablity(true, "Ruuvi", true);
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);

  return err_code;
}

ruuvi_driver_status_t 
task_gatt_on_gatt(ruuvi_interface_communication_evt_t evt, 
                  void* p_data, 
                  size_t data_len)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  char str[21] = { 0 };
  switch(evt)
  {
    case RUUVI_INTERFACE_COMMUNICATION_CONNECTED:
      ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, "Connected \r\n");
      err_code |= task_acceleration_fifo_use(true);
      ruuvi_interface_communication_message_t msg = {0};
      memcpy(msg.data, "Hello! Here's data!", 20);
      msg.data_length = 20;
      channel.send(&msg);
      break;

    case RUUVI_INTERFACE_COMMUNICATION_DISCONNECTED:
      ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, "Disonnected \r\n");
      err_code |= task_acceleration_fifo_use(false);
      ruuvi_interface_communication_ble4_gatt_advertise_connectablity
        (true, "Ruuvi", true);
      break;

    case RUUVI_INTERFACE_COMMUNICATION_SENT:
      ruuvi_interface_watchdog_feed();
      break;

    case RUUVI_INTERFACE_COMMUNICATION_RECEIVED:
      snprintf(str, data_len, "%s", p_data);
      ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, str);
      break;

    default:
      break;

  }
  return err_code;
}

ruuvi_driver_status_t 
task_gatt_send(ruuvi_interface_communication_message_t* const msg)
{
  if(NULL == msg)          { return RUUVI_DRIVER_ERROR_NULL; }
  if(NULL == channel.send) { return RUUVI_DRIVER_ERROR_INVALID_STATE; }

  return channel.send(msg);
}

View raw

Let’s try it out. The tag is advertised as “Ruuvi”, we can connect to it via nRF Connect.

nRF Connect DIS service
DIS service

The DFU service fails to upload new package automatically, but it’s possible to enter the bootloader by registering to notifications of Secure DFU Service and sending “enter bootloader” command.

nRF Connect DFU service
Update without physical access does have security risks though

And finally, we can check the acceleration stream with nRF UART. Just before disconnection we send “Thanks for data!” to the RuuviTag to verify the 2-way communication.

10 Hz data in 32-sample sets
10 Hz data in 32-sample sets
RuuviTag logs
RuuviTag logs

On our RuuviTag log we notice that the DFU service initialization warns us about the missing bootloader on the development tag. The connection, FIFO streaming and data receiving works nicely, and a while after the disconnection our watchdog kicks in and resets the tag. So far, so good.

Power profiling

Let’s see what our power profile looks like. First, let’s check the advertising.

Power profile back down to 24.4 μA
We’re back down to 24.4 μA

As we have gotten rid of the extra FIFO reads, we’re back to 24.4 μA from previous 29.7 μA. While we were running the accelerometer at 1 Hz the consumption was 23 μA, the extra is probably coming from current samplerate of 10 Hz. How about while connected?

Power profile while sending data
Sending data is not so good

One issue is immediately apparent from the power profile: Our tag cannot send data fast enough and the program loops in the line 16 of the task_acceleration.c gist above, waiting for the buffer to clear. We’ll have to look into ways of boosting the GATT throughput and let the tag sleep between the connection intervals in the future.

Conclusion

We now have implemented the GATT profile and can stream accelerometer data over it. However more work remains to optimize the GATT throughput and consumption.

In the next part of the series we’ll implement NFC writes to the tag. Soon we’ll have all the features we’re going to use implemented at least on a proof-of-concept level, then we can do a feature freeze and consider our firmware to be in a beta stage. Then we can start ironing out the bugs, improve documentation and tests, and optimize things like the GATT throughput.

Stay tuned and follow @ojousima and @ruuvicom on Twitter for #FirmwareFriday posts!