Interfacing and sending BME280 sensor data over a BLE with nRF52840-dongle

I was a newbie some days ago to the Riot-os. I started with some of the getting started guides Riot has. I found that Riot is something where I could make progress compared to my past experience with other Rtos famous in the open-source community.

The Riot-os is indeed a lightweight operating system with all standard tools like gdb, gcc, and Posix api. I have had only one supported board the time when I started and that was the Nordic nRF52840 microcontroller-based dongle.

It is a tiny USB-like development board with connectivity like BLE 5 support. It also has major peripheral interface protocol support like the I2C, SPI, UART etc. There are two onboard leds and one button.

I thought of interfacing a BLE280 sensor to the board and sending the data over BLE protocol so that other BLE devices can read the GATT server Characteristic values.(BLE devices have services and characteristics values that can be read through other BLE devices. Both services and Characteristics have unique UUIDs.) Fortunately, there was already an implementation of the I2C library for interfacing the BME280 sensor and some examples for the Nimble BLE library. Nimble is a fully open-source BLE stack for tiny microcontroller devices with BLE support.

The first thing to do was to install the Riot correctly as explained in the following guide.

https://doc.riot-os.org/

At this point, a good idea is to check whether the board can be flashed from the command. Navigate to RIOT’s root directory and type the following command. The command will build the /saul application in /examples and flash the binary to the board.

make BOARD=nrf52840dongle -C examples/saul flash PORT=/dev/ttyACM1

Once the step is done one can create a separate application in the folder with the source file with a makefile in it. The Makefile contains the information which tells the build system how to build the application.

The following is the Makefile settings required for the BLE application with sends I2C sensor data over a BLE.

The Makefile contains the information which tells the build system how to build the application.

# Set the name of your application:
APPLICATION = foobar
BOARD ?= nrf52840dongle
USEMODULE += fmt
USEMODULE +=periph_i2c
USEMODULE += bme280_i2c
FEATURES_PROVIDED += periph_i2c
FEATURES_REQUIRED += periph_i2c
FEATURES_REQUIRED += periph_uart
USEMODULE += dht

CFLAGS += -DBMX280_PARAM_I2C_DEV=I2C_DEV\(0\)
CFLAGS += -DBMX280_PARAM_I2C_ADDR=0x76
# This has to be the absolute path to the RIOT base directory:
RIOTBASE ?= $(CURDIR)/../../
# Include NimBLE
USEPKG += nimble
USEMODULE += nimble_svc_gap
USEMODULE += nimble_svc_gatt
# Use automated advertising
USEMODULE += nimble_autoadv
CFLAGS += -DNIMBLE_AUTOADV_DEVICE_NAME='"Antica247"'
CFLAGS += -DNIMBLE_AUTOADV_START_MANUALLY=1
DEVELHELP ?= 1
# Change this to 0 to show compiler invocation lines by default:
QUIET ?= 1

include $(RIOTBASE)/Makefile.include

Then create a file with main.c and paste the following code in it.

3 Likes
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "nimble_riot.h"
#include "nimble_autoadv.h"

#include "host/ble_hs.h"
#include "host/util/util.h"
#include "host/ble_gatt.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"

#include "xtimer.h"
#include "timex.h"
#include "board.h"
#include "periph_conf.h"

#include "bmx280_params.h"
#include <bmx280.h>
#include "fmt.h"

These are the includes that are needed in order to develop a BLE application that sends sensor data. The following code will be needed to define a pin. The GPIO_PIN(1,9) means P1.9(Port 1 Pin 9) as the naming of ports and pins for hardware is defined that way in Riot. The next line defines the timer interval needed for the application to have delays, interrupts, etc. Then default parameters are instantiated from bmx280_params.h file. Finally a instance of class for bme280 sensor is defined.

#define REDLED GPIO_PIN(1,9)
#define INTERVAL (2U * US_PER_SEC)
bmx280_params_t params;
bmx280_t dev;
static char str_answer[1000];

For BLE there are services and characteristics which need to be defined with 128-bit long UUIDs. Each service can have one or multiple characteristics. Here there is one service defined with gatt_server_service_uuid and the rest three are characteristics.

static const ble_uuid128_t gatt_server_service_uuid =
          BLE_UUID128_INIT(0x99, 0xa3, 0xc7, 0x14, 0x3e, 0x03, 0x3e, 0xa1, 0xff,
                  0x48, 0x37, 0xd1, 0xb3, 0x38, 0xce, 0xff);

static const ble_uuid128_t gatt_server_char_uuid =
          BLE_UUID128_INIT(0x45, 0xa3, 0xc7, 0x14, 0x3e, 0x03, 0x3e, 0xa1, 0xff,
                  0x48, 0x37, 0xd1, 0xb3, 0x38, 0xce, 0xf1);

static const ble_uuid128_t gatt_server_char_humy_uuid =
          BLE_UUID128_INIT(0x45, 0xa3, 0xc7, 0x14, 0x3e, 0x03, 0x3e, 0xa1, 0xff,
                  0x48, 0x37, 0xd1, 0xb3, 0x38, 0xce, 0xf2);

static const ble_uuid128_t gatt_server_char_pres_uuid =
          BLE_UUID128_INIT(0x45, 0xa3, 0xc7, 0x14, 0x3e, 0x03, 0x3e, 0xa1, 0xff,
                  0x48, 0x37, 0xd1, 0xb3, 0x38, 0xce, 0xf3);

The services and characteristics are defined but they are still unused. We need to have a callback function which defines the structure of the services and characteristics. The following callback will define the struct to arrange GATT services and characteristics. All the characteristics are kept read only to read sensor data from mobile application or a BLE central device. A callback function is set and the uuid’s are also assigned.

static int gatt_bme280_clbk(
  uint16_t conn_handle, uint16_t attr_handle,
  struct ble_gatt_access_ctxt *ctxt, void *arg);
static const struct ble_gatt_svc_def gatt_server_svcs[] = {
  {
    .type = BLE_GATT_SVC_TYPE_PRIMARY,
    .uuid = (ble_uuid_t*) &gatt_server_service_uuid.u,
    .characteristics =(struct ble_gatt_chr_def[]){
      {
      .uuid = (ble_uuid_t*) &gatt_server_char_uuid.u,
      .access_cb = gatt_bme280_clbk ,
      .flags = BLE_GATT_CHR_F_READ,
    },
    {
      .uuid = (ble_uuid_t*) &gatt_server_char_humy_uuid.u,
      .access_cb = gatt_bme280_clbk ,
      .flags = BLE_GATT_CHR_F_READ,
    },
    {
      .uuid = (ble_uuid_t*) &gatt_server_char_pres_uuid.u,
      .access_cb = gatt_bme280_clbk ,
      .flags = BLE_GATT_CHR_F_READ,
    },
    {
        0,
    }
  }
},
{
  0,
}
};

Next the callback functions definition which is the same function which we already defined. But, this part of function definition will read the sensor values and update the GATT profile so that the client can read the correct sensor data.

static int gatt_bme280_clbk( uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { puts(“Callback for the device”);

(void) conn_handle;
(void) attr_handle;
(void) arg;

int rc = 0;
int16_t Temprature, Humidity, Pressure;

Temprature = bmx280_read_temperature(&dev);
Humidity = bme280_read_humidity(&dev);
Pressure = bmx280_read_pressure(&dev);

ble_uuid_t* uuid_read = (ble_uuid_t*) &gatt_server_char_uuid.u;
ble_uuid_t* uuid_read_humy = (ble_uuid_t*) &gatt_server_char_humy_uuid.u;
ble_uuid_t* uuid_read_pres = (ble_uuid_t*) &gatt_server_char_pres_uuid.u;

if (ble_uuid_cmp(ctxt->chr->uuid, uuid_read) == 0){
  fmt_s16_dfp(str_answer, Temprature, -2);
  puts("read-only characteristic");
  if(ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR){
    puts(str_answer);
    rc = os_mbuf_append(ctxt->om, str_answer, strlen(str_answer));
    puts("");
    return rc;
  }
  return 0;
}
else if(ble_uuid_cmp(ctxt->chr->uuid, uuid_read_humy) == 0){
  fmt_s16_dfp(str_answer, Humidity, -2);
  if(ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR){
    puts(str_answer);
    rc = os_mbuf_append(ctxt->om, str_answer, strlen(str_answer));
    puts("");
    return rc;
  }
  return 0;
}
else if(ble_uuid_cmp(ctxt->chr->uuid, uuid_read_pres) == 0){
  fmt_s16_dfp(str_answer, Pressure, -2);
  if(ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR){
    puts(str_answer);
    rc = os_mbuf_append(ctxt->om, str_answer, strlen(str_answer));
    puts("");
    return rc;
  }
  return 0;
}
puts("Unhandled uuid ");
return 1;

}

Finally, a main function which is required by every application which will do the gpio, timer and bluetooth stack initialization. Also the callback will be registered so that whenever a client wants to update a sensor data a data will be read through the I2C interface and updated.

int main(void)
{
  const char *name = "Myapplication";
  puts("Hello From RIOT!");
  gpio_init(REDLED,GPIO_OUT);
  bmx280_init(&dev, &bmx280_params[0]);

  xtimer_ticks32_t last_wakeup = xtimer_now();

  puts("Nimble server for ");

  int rc;
  (void) rc;

  const char *ble_svc_gap_device_name(void);

    /* set the device name */
  ble_svc_gap_device_name_set(name);
  /* varify and add our custom service */

  rc = ble_gatts_count_cfg(gatt_server_svcs);
  assert(rc==0);
  rc = ble_gatts_add_svcs(gatt_server_svcs);
  assert(rc==0);

  /* reload the GATT server to link our added services */
  ble_gatts_start();
  /* start to advertise this node */
  nimble_autoadv_start();
  gpio_toggle(REDLED);
  xtimer_periodic_wakeup(&last_wakeup, INTERVAL);
  return 0;
}

Here are the images of the nRF52840-dongle and the screenshots of the nRFConnect application.

1 Like

Very interesting post :slight_smile:

Something similar I need to do for a temporary test setup…send temperature values from an 10K NTC every 5 seconds…

As you are using the “xtimer_periodic_wakeup” function…does it mean it sends data every 2 seconds and sleeps otherwise?

Thank you…

The periodic timer is something just there, it has nothing to do with sending values. As far as the node is advertising you can get those values from the remote ble device.

I am not sure how nimble deals with connections setups for ble devices, like it auto reconnects when the number of maximum packet transmission has reached to the highest values. I need to verify that. Also, how streaming works with it.