Reading Thingy:53 sensor data using Bluetooth LE

Reading Thingy:53 sensor data using Bluetooth LE

The Thingy:53 is a bleeding edge IoT prototyping platform featuring the flagship nRF5340 dual-core Wireless SoC. It also contains several sensors and a rechargeable Li-Po battery, making it an excellent tool to rapidly prototype an application that can be deployed in the field.  To learn more about Thingy:53, please visit the product webpage and watch the webinar, which is available on demand.

In this blog post, we will set up the Thingy:53 as a sensor hub that takes measurements from the onboard BME688 environmental sensor, BH1749 color and light sensor, and the on-chip SAADC to read out the Li-Po battery voltage. The data will also be available to read over GATT by a connected Bluetooth LE central device.

We will go through all the necessary steps to get this prototype up and running in the section below, but if you wish to skip directly to the finished firmware to test it for yourself, it is linked at the end of this blog post. If you at any point are unsure about the Zephyr devicetree, application configuration, or macro-usage, then we would highly recommend going through the nRF Connect SDK Fundamentals course in the Nordic Developer Academy before proceeding. It is an excellent course for gaining an understanding of how the nRF Connect SDK works behind the scenes.

Setting up the project

Having installed the nRF Connect SDK v2.1.0 using the Toolchain Manager, we start by using the 'Create a new application' option in Visual Studio Code (VS Code) to create a freestanding application based on the peripheral LBS sample. Once the project has been created we need to add a build configuration for the thingy53_nrf5340_cpuapp_ns target, which means we are building the application to run on the application CPU in the non-secure environment. This might sound scary, but it is in fact making our firmware more secure as it will incorporate TF-M into the build and create a secured domain that is separate from the non-secure one. Steps for adding a build configuration can be found in the nRF Connect for VS Code documentation.

The main reason for choosing the peripheral LBS sample as our place to start is that it is already compatible with Thingy:53. So it already has the necessary DFU parts implemented and some of the Bluetooth LE functionality, which lets us focus on the data collection and transmission.

We need our application to feature DFU right from the beginning because Thingy:53 does not have an onboard debugger. Therefore we need to implement a means of updating the application on the Thingy:53. For convenience, we could, of course, have used a standalone debug-probe, or another DK's debugger, to perform the programming of the Thingy:53, but that would require additional hardware. It would have robbed us of the opportunity to see how easy it is to add DFU to our application.

After building the project, the DFU-ready application can be found in <project_path>/build/zephyr/dfu_application.zip. If you already have an existing application that you would like to add DFU to, you could follow this guide.

Trimming down to a minimal project

To highlight the necessary steps for the sensor hub implementation, we will start by trimming away the peripheral LBS-specific functionality from our application. In this case, we do not need any of the LBS specific code or the button functionality - we can keep the generic Bluetooth parts and the LEDs. Removing all the LBS service specific code leaves us with the following, minimal main.c which will be used as baseline for our project:

/*
* Copyright (c) 2018 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
 
#include <zephyr/types.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <soc.h>
 
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
 
#include <zephyr/settings/settings.h>
 
#include <dk_buttons_and_leds.h>
 
#define DEVICE_NAME             CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN         (sizeof(DEVICE_NAME) - 1)
 
 
#define RUN_STATUS_LED          DK_LED1
#define CON_STATUS_LED          DK_LED2
#define RUN_LED_BLINK_INTERVAL  1000
 
#define USER_LED                DK_LED3
 
#define USER_BUTTON             DK_BTN1_MSK
 
static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};
 
static const struct bt_data sd[] =
{
 
};
 
static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        printk("Connection failed (err %u)\n", err);
        return;
    }
 
    printk("Connected\n");
 
    dk_set_led_on(CON_STATUS_LED);
}
 
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    printk("Disconnected (reason %u)\n", reason);
 
    dk_set_led_off(CON_STATUS_LED);
}
 
BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected        = connected,
    .disconnected     = disconnected,
};
 
void main(void)
{
    int blink_status = 0;
    int err;
 
    printk("Starting Sensor Hub application\n");
 
    err = dk_leds_init();
    if (err)
    {
        printk("LEDs init failed (err %d)\n", err);
        return;
    }
 
    err = bt_enable(NULL);
    if (err) {
        printk("Bluetooth init failed (err %d)\n", err);
        return;
    }
 
    printk("Bluetooth initialized\n");
 
    if (IS_ENABLED(CONFIG_SETTINGS))
    {
        settings_load();
    }
 
    err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad),
                sd, ARRAY_SIZE(sd));
    if (err)
    {
        printk("Advertising failed to start (err %d)\n", err);
        return;
    }
 
    printk("Advertising successfully started\n");
 
    for (;;)
    {
        dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);
        k_sleep(K_MSEC(RUN_LED_BLINK_INTERVAL));
    }
}

We will also need to make some changes to the prj.conf - we'll keep the general Bluetooth parts, and remove the LBS specific parts. In addition, we have renamed the device to 'T53_sensor_hub' for convenience and easy discoverability by a BLE central device during scanning. The prj.conf which should look like this afterwards:

#
# Copyright (c) 2018 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
 
# Enable Bluetooth
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="T53_sensor_hub"
 
# Enable buttons and LEDs
CONFIG_DK_LIBRARY=y
 
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048

Lastly, you can delete the Kconfig and prj_minimal.conf files from the sample directory as well.

Setting up the BH1749 color and light sensor

We may now start to add the data collection functionality to our project. We will use the BH1749 to collect sensor readings of the ambient light in the environment, specifically the red/green/blue values. To make things easy for ourselves, we will here make use of the SENSOR module, which works with the variety of sensors already implemented in Zephyr. We start by adding the following configurations to our prj.conf:

# Configure Thingy:53 sensors
CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_BH1749=y

The specific function of each of these configurations can be checked by either going to the kconfig reference search, or by hovering over the specific configuration in the prj.conf opened in your VS Code editor, as shown below.

Now we need to add the source code that brings the sensor functionality into our project. In main() we will retrieve the sensor from the devicetree and check for its readiness. In addition we will create a separate function sample_and_update_all_sensor_values for sampling all the sensors, which gets called from the main loop. In that function the sensor data is sent to sensor_hub_update_* functions which handle the BLE data transfer over GATT. We will go over that functionality in the sections below.

It is also required to make a slight modification to the connected/disconnected bluetooth callbacks and in the main loop, so that the sensor sampling is only done if there is a BLE central device connected to the Thingy:53 (allowing for energy savings), and when the LEDs are off. This is because the BH1749 is right next to the RGB LED on the Thingy:53 board, and that would influence the readings if we were to sample the sensor when the LED is on.

...
#include <zephyr/drivers/sensor.h>
...
 
//BT globals and callbacks
struct bt_conn *m_connection_handle = NULL;
static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        printk("Connection failed (err %u)\n", err);
        return;
    }
 
    m_connection_handle = conn;
 
    printk("Connected\n"); 
}
 
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    printk("Disconnected (reason %u)\n", reason);
 
    m_connection_handle = NULL;
}
 
BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected        = connected,
    .disconnected     = disconnected,
};
 
//Local function prototypes
static int sample_and_update_all_sensor_values(const struct device *bme688Dev, const struct device *bh1749Dev, const struct device *adc_dev);
 
// Main loop
void main(void)
{
    uint8_t blink_status = 0;
    int err;
 
    printk("Starting Sensor Hub application\n");
 
    //Setting up BH1749 light and color sensor
    const struct device *bh1749rgbDev = DEVICE_DT_GET_ONE(rohm_bh1749);
 
    if (!device_is_ready(bh1749rgbDev))
    {
        printk("Sensor device not ready\n");
        return;
    }     
 
    ...
 
    for (;;)
    {
         
        if(!(blink_status % 2) && m_connection_handle)
        {
            /*When blink is even number it means the LED has been OFF for 500ms, so we can sample
            the sensors if there is a BLE central connected */
            sample_and_update_all_sensor_values(bme688SensorDev, bh1749rgbDev, adc_dev);
        }
 
        //Change LED status
        dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);
         
        //Put thread to sleep for UPDATE_INTERVAL
        k_sleep(K_MSEC(UPDATE_INTERVAL));
    }
}
 
//This function samples all the needed data from the sensors and sends it over to the service.c/.h module that handles the GATT data transfer
static int sample_and_update_all_sensor_values(const struct device *bme688Dev, const struct device *bh1749Dev, const struct device *adc_dev)
{
    int err;
    struct sensor_value red_value;
    struct sensor_value green_value;
    struct sensor_value blue_value;
     
    //Trigger sampling of BH1749 - The sensor does only support fetching SENSOR_CHAN_ALL
    err = sensor_sample_fetch_chan(bh1749Dev, SENSOR_CHAN_ALL);
     
    if (err)
    {
        printk("sensor_sample_fetch failed err %d\n", err);
        return err;
    }
     
    //Collect red light sample and update characteristic
    err = sensor_channel_get(bh1749Dev, SENSOR_CHAN_RED, &red_value);
    if (err)
    {
        printk("sensor_channel_get failed err %d\n", err);
        return err;
    }
    sensor_hub_update_red_color(m_connection_handle, (uint8_t*)(&red_value.val1), sizeof(red_value.val1));
 
    //Collect green light sample and update characteristic
    err = sensor_channel_get(bh1749Dev, SENSOR_CHAN_GREEN, &green_value);
    if (err)
    {
        printk("sensor_channel_get failed err %d\n", err);
        return err;
    }
    sensor_hub_update_green_color(m_connection_handle, (uint8_t*)(&green_value.val1), sizeof(green_value.val1));
 
    //Collect red light sample and update characteristic
    err = sensor_channel_get(bh1749Dev, SENSOR_CHAN_BLUE, &blue_value);
    if (err)
    {
        printk("sensor_channel_get failed err %d\n", err);
        return err;
    }
    sensor_hub_update_blue_color(m_connection_handle, (uint8_t*)(&blue_value.val1), sizeof(blue_value.val1));
 
    printk("All sensors sampled and characteristics updated!\n");
 
    return 0;
}

Setting up the BME688 environmental sensor

Now we can also add the BME688 sensor, to gather the pressure, humidity and temperature data. We will also here make use of the SENSOR module, as shown in the previous step. Since we already have some of the sensor modules kconfigs added, we will only need to add the following to our prj.conf:

CONFIG_BME680=y

At the time of writing, the BME688 sensor that we are going to be using is not declared in the thingy53_nrf5340_common.dts file, which can be found in <sdk_path>\<sdk_version>\zephyr\boards\arm\thingy53_nrf5340. This is the devicetree file that contains the hardware definition for the board (e.g. buttons, LEDs, sensors). It would also be possible to add the BME688 sensor declaration into the overlay files in <project_path>/boards, but doing it on the SDK file will make the change available for any project targeting Thingy:53.

We will have to make a small amendment to include the missing sensor. As we see on the file, the i2c1 instance is already used for both BH1749 and the BMM150, but there is no mention of the BME688 that we are interested in using. Looking at the Thingy:53 hardware files, downloadable from here, tells us that these three sensors are on the same I2C bus, and so we can just add it to the declaration as so:

&i2c1 {
    compatible = "nordic,nrf-twim";
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;
 
    pinctrl-0 = <&i2c1_default>;
    pinctrl-1 = <&i2c1_sleep>;
    pinctrl-names = "default", "sleep";
    bmm150@10 {
        compatible = "bosch,bmm150";
        label = "BMM150";
        reg = <0x10>;
    };
 
    bh1749@38 {
        compatible = "rohm,bh1749";
        label = "BH1749";
        reg = <0x38>;
        int-gpios = <&gpio1 5 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
    };
 
    bme688@76 {
        compatible = "bosch,bme680";
        label = "BME688";
        reg = <0x76>;
    };
};

Similarly to the previous section, we need to add the source code that brings the sensor functionality into our project. In main() we will retrieve the sensor from the devicetree and check for its readiness. In addition we will add the BME688 sampling to a the function sample_and_update_all_sensor_values.

// Main loop
void main(void)
{
    ...
 
    //Setting up BME688 environmental sensor */
    const struct device *bme688SensorDev = DEVICE_DT_GET_ONE(bosch_bme680);
 
    if (!device_is_ready(bme688SensorDev))
    {
        printk("Sensor device not ready\n");
        return;
    }
 
    ...
 
    for (;;)
    {
         
        if(!(blink_status % 2) && m_connection_handle)
        {
            /*When blink is even number it means the LED has been OFF for 500ms, so we can sample
            the sensors if there is a BLE central connected */
            sample_and_update_all_sensor_values(bme688SensorDev, bh1749rgbDev, adc_dev);
        }
 
        //Change LED status
        dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);
         
        //Put thread to sleep for UPDATE_INTERVAL
        k_sleep(K_MSEC(UPDATE_INTERVAL));
    }
}
 
//This function samples all the needed data from the sensors and sends it over to the service.c/.h module that handles the GATT data transfer
static int sample_and_update_all_sensor_values(const struct device *bme688Dev, const struct device *bh1749Dev, const struct device *adc_dev)
{
    ...
 
    struct sensor_value temperature_value;
    struct sensor_value pressure_value;
    struct sensor_value humidity_value;
 
    ...
 
    //Trigger sampling of BME688 sensor
    err = sensor_sample_fetch(bme688Dev);
    if(err)
    {
        printk("Failed to collect samples from bme688\n");
        return err;
    }
 
    //Collect temperature sample and update characteristic
    err = sensor_channel_get(bme688Dev, SENSOR_CHAN_AMBIENT_TEMP, &temperature_value);
    if(err)
    {
        printk("Failed to fetch temperature sample");
        return err;
    }
    sensor_hub_update_temperature(m_connection_handle, (uint8_t*)(&temperature_value.val1), sizeof(temperature_value.val1));
 
    //Collect pressure sample and update characteristic
    err = sensor_channel_get(bme688Dev, SENSOR_CHAN_PRESS, &pressure_value);
    if(err)
    {
        printk("Failed to fetch pressure sample");
        return err;
    }
    sensor_hub_update_pressure(m_connection_handle, (uint8_t*)(&pressure_value.val1), sizeof(pressure_value.val1));
 
    //Collect humidity sample and update characteristic
    err = sensor_channel_get(bme688Dev, SENSOR_CHAN_HUMIDITY, &humidity_value);
    if(err)
    {
        printk("Failed to fetch humidity sample");
        return err;
    }
    sensor_hub_update_humidity(m_connection_handle, (uint8_t*)(&humidity_value.val1), sizeof(humidity_value.val1));
 
    ...
 
    printk("All sensors sampled and characteristics updated!\n");
 
    return 0;
}

Setting up ADC to read Li-Po battery voltage

The final peripheral functionality to be added to our project is the ADC, which allows us to read the on-kit Li-Po battery voltage battery. Since the battery voltage exceeds the nRF5340 supply voltage, it will be read through an on-board voltage divider with two resistors that is enabled via a GPIO. This can be found on page 4 of the boards schematics.

This voltage divider is also defined in the device tree, on the earlier mentioned thingy53_nrf5340_common.dts file. The properties in the vbatt node tell us that ADC channel 2 is being used to sample the voltage through a 1M5+180k ohm resistor ladder, and also that P0.16 is used to enable the ladder.

vbatt {
    compatible = "voltage-divider";
    io-channels = <&adc 2>;
    output-ohms = <180000>;
    full-ohms = <(1500000 + 180000)>;
    power-gpios = <&gpio0 16 0>;
};
To bring the ADC into our project first we need to add the following kconfig to your prj.conf file:
CONFIG_ADC=y
Then we need to add the ADC settings and and voltage divider, initialize, and finally sample the ADC and calculate the battery voltage based on the resistor ladder configuration.
...
#include <drivers/adc.h>
#include <hal/nrf_saadc.h>
 
// ---- ADC settings START ---- //
#define ADC_RESOLUTION 14
 
static const struct adc_channel_cfg adc_channel_cfg = {
    .gain = ADC_GAIN_1_6,
    .reference = ADC_REF_INTERNAL,
    .acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40),
    .input_positive = NRF_SAADC_INPUT_AIN2,
};
 
static struct adc_sequence adc_seq = {
    .channels = BIT(0),
    .oversampling = 4,
    .calibrate = true,
    .resolution = ADC_RESOLUTION,
};
// ---- ADC settings END ---- //
 
 
// ---- Voltage divider settings START ---- //
/* The vbatt node in the board device tree contains the HW definition for the voltage divider, which can be applied
to the battery voltage via an enable pin. That information will be retrieved to populate an instance of the
structure defined below.
The Voltage divider can be found on page 4 of the Thingy:53 schematics.
Board device tree can be found in <sdk_location>\<sdk_version>\zephyr\boards\arm\thingy53_nrf5340\thingy53_nrf5340_common.dts -> vbatt */
struct voltage_divider_config {
    struct gpio_dt_spec enable_pin;
    uint32_t output_ohm;
    uint32_t full_ohm;
};
 
 
static const struct voltage_divider_config volt_div_conf = {
    .enable_pin = GPIO_DT_SPEC_GET(DT_PATH(vbatt), power_gpios),
    .output_ohm = DT_PROP(DT_PATH(vbatt), output_ohms),
    .full_ohm = DT_PROP(DT_PATH(vbatt), full_ohms),
};
// ---- Voltage divider settings END ---- //  
 
...
 
// Main loop
void main(void)
{
    ...
 
    //Setting up ADC
    const struct device *adc_dev = DEVICE_DT_GET_ONE(nordic_nrf_saadc);
 
    if (!device_is_ready(adc_dev))
    {
        printk("ADC is not ready\n");
        return;
    }
 
    err = adc_channel_setup(adc_dev, &adc_channel_cfg);
    if (err)
    {
        printk("Error in ADC setup: %d\n", err);
        return;
    }
 
    //Setting up voltage divider
    if (!device_is_ready(volt_div_conf.enable_pin.port)) {     
        return;
    }
 
    gpio_pin_configure_dt(&volt_div_conf.enable_pin, GPIO_OUTPUT);
    gpio_pin_set_dt(&volt_div_conf.enable_pin, 1);
 
    ...
}
 
//This function samples all the needed data from the sensors and sends it over to the service.c/.h module that handles the GATT data transfer
static int sample_and_update_all_sensor_values(const struct device *bme688Dev, const struct device *bh1749Dev, const struct device *adc_dev)
{
    ...
    int16_t batt_volt;
 
    ...
 
    //Collect ADC battery measurement  
    adc_seq.buffer = &batt_volt;
    adc_seq.buffer_size = sizeof(batt_volt);
     
    err = adc_read(adc_dev, &adc_seq);
    if (err)
    {
        printk("adc_read() failed with code %d\n", err);
        return err;
    }  
 
    //Convert raw ADC measurements to mV with Zephyr ADC API
    adc_raw_to_millivolts(adc_ref_internal(adc_dev), ADC_GAIN_1_6, ADC_RESOLUTION, (int32_t*)(&batt_volt));
     
    //Calculate actual battery voltage using voltage divider
    batt_volt = batt_volt * volt_div_conf.full_ohm / volt_div_conf.output_ohm; 
     
    sensor_hub_update_batt_volt(m_connection_handle, (uint8_t*)(&batt_volt), sizeof(batt_volt));
 
    printk("All sensors sampled and characteristics updated!\n");
 
    return 0;
}

Implementing a custom sensor hub GATT service

Now that we have got all of our sensor readings up and running we can shift our focus over to transferring the data to our connected BLE central device over GATT. To do this, we will create our own BLE service to transfer the different measurements to our central device. We will be following the steps as thoroughly detailed in this blogpost series. In our case, we will need to diverge slightly from the tutorial in the blogpost series, since we will need a couple more characteristics, and no characteristic for receiving data from the central device. We begin by creating our sensor_hub_service.c and sensor_hub_service.h files under a new services project folder.

Using a UUID generator we created the following custom UUIDs which will be used as detailed on the table below.

UUID Description Units
a5b46352-9d13-479-9fcb-3dcdf0a13f4d Thingy:53 Demo Service N/A
06a55c4-b5e7-46fa-8326-8acaeb1189eb Temperature Reading Characteristic Celsius (C)
51838aff-2d9a-b32a-b32a-8187e41664ba Pressure Reading Characteristic Kilopascal (kPa)
753e3050-df06-4b53-b090-5e1d810c4383 Humidity Reading Characteristic Percentage (%)
82754bbb-6ed3-4d69-a0e1-f19f6b654ec2 Red Color Reading Characteristic N/A
db7f9f36-92ce-4509-a2ef-af72ba38fb48 Green Color Reading Characteristic N/A
f5d2eab5-41e8-4f7c-aef7-c9fff4c544c0 Blue Color Reading Characteristic N/A
fa3cf070-d0c7-4668-96c4-86125c8ac5df Battery Reading Characteristic mV

This yields the following sensor_hub_service.h:

// sensor_hub_service.h
 
#include <zephyr/types.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr.h>
#include <soc.h>
 
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>
 
//Declaration of custom GATT service and characteristics UUIDs
#define SENSOR_HUB_SERVICE_UUID \
    BT_UUID_128_ENCODE(0xa5b46352, 0x9d13, 0x479f, 0x9fcb, 0x3dcdf0a13f4d)
 
#define TEMP_CHARACTERISTIC_UUID \
    BT_UUID_128_ENCODE(0x506a55c4, 0xb5e7, 0x46fa, 0x8326, 0x8acaeb1189eb)
 
#define PRESSURE_CHARACTERISTIC_UUID \
    BT_UUID_128_ENCODE(0x51838aff, 0x2d9a, 0xb32a, 0xb32a, 0x8187e41664ba)
 
#define HUMIDITY_CHARACTERISTIC_UUID \
    BT_UUID_128_ENCODE(0x753e3050, 0xdf06, 0x4b53, 0xb090, 0x5e1d810c4383)
 
#define RED_COLOR_CHARACTERISTIC_UUID \
    BT_UUID_128_ENCODE(0x82754bbb, 0x6ed3, 0x4d69, 0xa0e1, 0xf19f6b654ec2)
 
#define GREEN_COLOR_CHARACTERISTIC_UUID \
    BT_UUID_128_ENCODE(0xdb7f9f36, 0x92ce, 0x4509, 0xa2ef, 0xaf72ba38fb48)
 
#define BLUE_COLOR_CHARACTERISTIC_UUID \
    BT_UUID_128_ENCODE(0xf5d2eab5, 0x41e8, 0x4f7c, 0xaef7, 0xc9fff4c544c0)
 
#define BATT_VOLT_CHARACTERISTIC_UUID \
    BT_UUID_128_ENCODE(0xfa3cf070, 0xd0c7, 0x4668, 0x96c4, 0x86125c8ac5df)
 
void sensor_hub_update_temperature(struct bt_conn *conn, const uint8_t *data, uint16_t len);
void sensor_hub_update_humidity(struct bt_conn *conn, const uint8_t *data, uint16_t len);
void sensor_hub_update_pressure(struct bt_conn *conn, const uint8_t *data, uint16_t len);
void sensor_hub_update_red_color(struct bt_conn *conn, const uint8_t *data, uint16_t len);
void sensor_hub_update_green_color(struct bt_conn *conn, const uint8_t *data, uint16_t len);
void sensor_hub_update_blue_color(struct bt_conn *conn, const uint8_t *data, uint16_t len);
void sensor_hub_update_batt_volt(struct bt_conn *conn, const uint8_t *data, uint16_t len);

We also need to add the 'notify' property to each of these characteristic, so that we may alert our central device (if it has enabled notifications for that specific measurement) whenever there is a new measurement available. We also need to implement that functions that we will use to update the characteristics and to send the notifications when new measurements are available. These are the functions called from sample_and_update_all_sensor_values, each time the sensors get samples.

// sensor_hub_service.c
 
#include <zephyr/types.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <sys/printk.h>
#include <sys/byteorder.h>
#include <zephyr.h>
#include <soc.h>
 
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/addr.h>
#include <bluetooth/gatt.h>
 
#include "sensor_hub_service.h"
 
#define BT_UUID_SENSOR_HUB              BT_UUID_DECLARE_128(SENSOR_HUB_SERVICE_UUID)
#define BT_UUID_SENSOR_HUB_TEMP         BT_UUID_DECLARE_128(TEMP_CHARACTERISTIC_UUID)
#define BT_UUID_SENSOR_HUB_PRESSURE     BT_UUID_DECLARE_128(PRESSURE_CHARACTERISTIC_UUID)
#define BT_UUID_SENSOR_HUB_HUMIDITY     BT_UUID_DECLARE_128(HUMIDITY_CHARACTERISTIC_UUID)
#define BT_UUID_SENSOR_HUB_RED_COLOR    BT_UUID_DECLARE_128(RED_COLOR_CHARACTERISTIC_UUID)
#define BT_UUID_SENSOR_HUB_GREEN_COLOR  BT_UUID_DECLARE_128(GREEN_COLOR_CHARACTERISTIC_UUID)
#define BT_UUID_SENSOR_HUB_BLUE_COLOR   BT_UUID_DECLARE_128(BLUE_COLOR_CHARACTERISTIC_UUID)
#define BT_UUID_SENSOR_HUB_BATT_VOLT    BT_UUID_DECLARE_128(BATT_VOLT_CHARACTERISTIC_UUID)
 
/*This function is called whenever the Client Characteristic Control Descriptor (CCCD) has been
changed by the GATT client, for each of the characteristics*/
void on_cccd_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
    ARG_UNUSED(attr);
    switch(value)
    {
        case BT_GATT_CCC_NOTIFY:
            // Start sending stuff!
            break;
         
        case 0:
            // Stop sending stuff
            break;
 
        default:
            printk("Error, CCCD has been set to an invalid value");    
    }
}
 
//Sensor hub Service Declaration and Registration
BT_GATT_SERVICE_DEFINE(sensor_hub,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_SENSOR_HUB),
     
    BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_HUB_TEMP,
                    BT_GATT_CHRC_NOTIFY,
                    BT_GATT_PERM_READ,
                    NULL, NULL, NULL),
    BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
     
    BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_HUB_PRESSURE,
                    BT_GATT_CHRC_NOTIFY,
                    BT_GATT_PERM_READ,
                    NULL, NULL, NULL),
    BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
 
    BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_HUB_HUMIDITY,
                    BT_GATT_CHRC_NOTIFY,
                    BT_GATT_PERM_READ,
                    NULL, NULL, NULL),
    BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
 
    BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_HUB_RED_COLOR,
                    BT_GATT_CHRC_NOTIFY,
                    BT_GATT_PERM_READ,
                    NULL, NULL, NULL),
    BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
 
    BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_HUB_GREEN_COLOR,
                    BT_GATT_CHRC_NOTIFY,
                    BT_GATT_PERM_READ,
                    NULL, NULL, NULL),
    BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
 
    BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_HUB_BLUE_COLOR,
                    BT_GATT_CHRC_NOTIFY,
                    BT_GATT_PERM_READ,
                    NULL, NULL, NULL),
    BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
 
    BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_HUB_BATT_VOLT,
                    BT_GATT_CHRC_NOTIFY,
                    BT_GATT_PERM_READ,
                    NULL, NULL, NULL),
    BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);
 
/* The below functions send a notification to a GATT client with the provided data,
given that the CCCD has been set to Notify (0x1) */
void sensor_hub_update_temperature(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
    const struct bt_gatt_attr *attr = &sensor_hub.attrs[2];
 
    struct bt_gatt_notify_params params =
    {
        //.uuid   = BT_UUID_SENSOR_HUB_TEMP,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = NULL
    };
 
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY))
    {
        // Send the notification
        if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled for temperature characteristic\n");
    }
}
 
void sensor_hub_update_pressure(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
    const struct bt_gatt_attr *attr = &sensor_hub.attrs[5];
 
    struct bt_gatt_notify_params params =
    {
        //.uuid   = BT_UUID_SENSOR_HUB_PRESSURE,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = NULL
    };
 
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY))
    {
        // Send the notification
        if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled for pressure characteristic\n");
    }
}
 
void sensor_hub_update_humidity(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
    const struct bt_gatt_attr *attr = &sensor_hub.attrs[8];
 
    struct bt_gatt_notify_params params =
    {
        //.uuid   = BT_UUID_SENSOR_HUB_HUMIDITY,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = NULL
    };
 
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY))
    {
        // Send the notification
        if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled for humidity characteristic\n");
    }
}
 
void sensor_hub_update_red_color(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
    const struct bt_gatt_attr *attr = &sensor_hub.attrs[11];
 
    struct bt_gatt_notify_params params =
    {
        //.uuid   = BT_UUID_SENSOR_HUB_RED_COLOR,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = NULL
    };
 
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY))
    {
        // Send the notification
        if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled for red color characteristic\n");
    }
}
 
void sensor_hub_update_green_color(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
    const struct bt_gatt_attr *attr = &sensor_hub.attrs[14];
 
    struct bt_gatt_notify_params params =
    {
        //.uuid   = BT_UUID_SENSOR_HUB_GREEN_COLOR,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = NULL
    };
 
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY))
    {
        // Send the notification
        if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled for green color characteristic\n");
    }
}
 
void sensor_hub_update_blue_color(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
    const struct bt_gatt_attr *attr = &sensor_hub.attrs[17];
 
    struct bt_gatt_notify_params params =
    {
        //.uuid   = BT_UUID_SENSOR_HUB_BLUE_COLOR,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = NULL
    };
 
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY))
    {
        // Send the notification
        if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled for blue color characteristic\n");
    }
}
 
void sensor_hub_update_batt_volt(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
    const struct bt_gatt_attr *attr = &sensor_hub.attrs[20];
 
    struct bt_gatt_notify_params params =
    {
        //.uuid   = BT_UUID_SENSOR_HUB_ADC_MEAS,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = NULL
    };
 
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY))
    {
        // Send the notification
        if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled for battery voltage characteristic\n");
    }
}

Now that we have a new source file in our project services\sensor_hub_service.c,  we need to remember to add it to our project by modifying our CMakeLists.txt to contain the following:

# NORDIC SDK APP START
target_sources(app PRIVATE
  src/main.c services/sensor_hub_service.c
)

Testing

Now, if we head over to nRF Connect Bluetooth Low Energy desktop app, we can check out of newfound service and characteristics, to see all the sensor data roll in once we enable each of the characteristics notification property. I will use an nRF52840 DK as the connectivity device - plugging it into the USB, choosing it in the Device Select menu, and clicking 'yes' to the re-programming prompt that appears. If you don't have a spare kit then nRF Connect for Mobile can be used in a similar way as described here.

When the programming finishes I click Start scan, and Connect when I see the T53_sensor_hub name appear in the list of devices. After the connection is made we see a list of available services and characteristics appear in the T53_sensor_hub list!

As you can see, all of our customer UUIDs show up as unknown, which makes them hard to read out directly. As an added bonus, we will therefore make this a little easier for ourselves by quickly adding them to the nRF Connect SDK BLE application's list of recognized UUIDs. To achieve this, navigate to: C:\Users\USERNAME\AppData\Roaming\nrfconnect\uuid_definitions.json, and adding in the following:

"uuid16bitDescriptorDefinitions": {},
    "uuid128bitServiceDefinitions": {
        "6E400001B5A3F393E0A9E50E24DCCA9E": {
            "name": "UART over BLE"
        },
        "A5B463529D13479F9FCB3DCDF0A13F4D": {
            "name": "Thingy:53 Sensor Hub"
        }
    },
    "uuid128bitCharacteristicDefinitions": {
        "6E400002B5A3F393E0A9E50E24DCCA9E": {
            "name": "UART RX"
        },
        "6E400003B5A3F393E0A9E50E24DCCA9E": {
            "name": "UART TX"
        },
        "506A55C4B5E746FA83268ACAEB1189EB": {
            "name": "Temperature"
        },
        "51838AFF2D9AB32AB32A8187E41664BA": {
            "name": "Pressure"
        },
        "753E3050DF064B53B0905E1D810C4383": {
            "name": "Humidity"
        },
        "82754BBB6ED34D69A0E1F19F6B654EC2": {
            "name": "Red Value"
        },
        "DB7F9F3692CE4509A2EFAF72BA38FB48": {
            "name": "Green Value"
        },
        "F5D2EAB541E84F7CAEF7C9FFF4C544C0": {
            "name": "Blue Value"
        },
        "FA3CF070D0C7466896C486125C8AC5DF": {
            "name": "Battery mV"
        }
    },
    "uuid128bitDescriptorDefinitions": {}
}
We can now see all of our characteristics proper name and value displayed in the nRF Connect SDK BLE application, and we see that the characteristics that we have enabled notify for, updates every second. It should look like this:

Versions used

The following software and tools versions were used to create this blog post. If you are using newer versions then the information shown here might be slightly different.

Software/Tool Version
nRF Connect SDK 2.1.0
nRF Connect for Desktop Bluetooth Low Energy 4.0.0
nRF Connect for mobile (iOS) 2.5.3
nRF Connect for VS Code 2022.10.30

Closing

The source code for this project is hosted on GitHub: https://github.com/nordic-tiago/thingy53_sensor_hub.

We hope you have found this post useful to get you up and running with your Thingy:53 development. If you should have any questions about the topics covered in this tutorial, or if you require technical support with your Thingy:53 development, please do not hesitate to open a ticket in DevZone.

thingy53_sensor_hub.zip
  • @

    I'm able to use build configuration  thingy53_nrf5340_cpuapp and  thingy53_nrf5340_cpuapp_ns for Toolchain 2.5.2 / SDK 2.5.2.

    Yesterday, I was successfully able to run thingy53_nrf5340_cpuapp build. However, even the same build I'm not able to run. The thingy53_nrf5340_cpuapp_ns build target doesn't run at all.

    The last version of Toolchain & SDK that thingy53_sensor_hub runs is 2.3.0.

    Coupd you please review and see what may be wron
    g? Thanks.