This is a guest post by Sandra Capri, CTO at Ambient Sensors, a Golioth Design Partner who regularly designs and deploys Bluetooth Solutions.

My previous article described a demo that used the Golioth console to send RPCs (Remote Procedure Calls) and device settings to a Bluetooth Mesh. The purpose was to show how easy it is to control a Bluetooth Mesh from an internet connected device. This article is to give some details as to how I created the demo.

But first, let me give you a quick tutorial on the Bluetooth Mesh LC server (the following diagram represents the information from the Bluetooth® SIG specification MshMDL_v1.1):

The Y-axis is the brightness of a luminaire (“the device that is lighting up”). LuxLevel/Lightness is defined for three levels: On, Prolong, and Standby. The general rule is that On is the brightest setting, Prolong is dimmer than On, and Standby is the dimmest (or completely off). The X-axis is the time axis during which the luminaire maintains each of those lightness levels.

Each bbc:microbit in the demo implements an LC Server which while following the above diagram:

  1. Transitions the LEDs to the “On” lightness level (LuxLevel/Lightness On)
  2. Stays at that lightness level for the “Run” time (Light LC Time Run)
  3. Dims the LEDs to the “Prolong” lightness level (LuxLevel/Lightness Prolong)
  4. Stays at that lightness level for the “Prolong” time (Light LC Time Prolong)
  5. Dims the LEDs to the “Standby” level (LuxLevel/Lightness Standby)

The Device Settings

The demo needed a way of setting these five parameters (the three lightness levels and two times). The Device Settings page of the Golioth console was ideal for this. I mapped the settings as follows:

  • Device setting name
  • TIME_RUN
  • TIME_PROLONG
  • LVL_RUN
  • LVL_PROLONG
  • LVL_STANDBY
  • LC server value
  • Light LC Time Run
  • Light LC Time Prolong
  • LuxLevel/Lightness On
  • LuxLevel/Lightness Prolong
  • LuxLevel/Lightness Standby

And of course, the Mesh firmware specifies default values for each of these.  But when doing demos (or development), there are many times you want to change these values.  If you don’t have a handy LC client around, you often resort to changing the defaults in the firmware and pushing over the air updates.  And if you have 8 (or more) LC servers to update in a mesh, that gets painful quickly.

In the demo, you can set TIME_RUN to 5 seconds, so when the LEDs first turn on, they stay “On” for 5 seconds.  If that time is too short for you, set it to 20 seconds, and the next time the LEDs turn “On”, they stay there for 20 seconds.  Similarly with TIME_PROLONG – you easily define how long the lights stay at “Prolong.”

And then comes the individual light levels – do you want the Run level to be 100%, Prolong at 50%, and Standby at 0% (off)?  Then all you need to do is change a setting.  Voilà, it’s done.  This is much easier than modifying every LC server firmware on each device, every time you want to view a new set of levels and times.

The astute reader will notice that there are other values in the diagram above (and there are several other values in the LC Server model as well), but only a subset is implemented for simplicity’s sake.

The Remote Procedure Calls

Now what about those RPCs?  Several were written specifically for this demo, turning the LEDs on and off in different ways.   Some turn off the control of the LC server, and some turn it back on.  The following functions were implemented:

  • set_light
    • This is often the first RPC sent – it demonstrates the basic LC model functionality.  A set_light on RPC tells the microbits to follow the LC server diagram (shown above).  Each microbit ramps its LEDs to the “On” lightness level, then starts dimming, eventually ramping back to “Standby”.  A set_light off RPC tells the microbits to immediately set the LEDs to “Standby”, bypassing the ramping and the “Prolong” level.  Note that pushing the button on the Thingy91 sends the same command as the set_light RPC.
  • lightness
    • This RPC overrides the lightness level, turning off the LC server.  The lightness will stay at the specified percentage level until another command is received.
  • lc_mode
    • This RPC turns the LC server on or off.  This sends the LC model command to turn on/off the server.  Anytime the LC server is turned off (e.g., with a lightness command), this is the only command that can turn it back on.
  • gen_on_off
    • This RPC is specific to the structure of an LC server.  Every LC server node has at least two elements – one with a Lightness Server, and one with an LC server.  The gen_on_off RPC sends a generic on/off command to the specified element.  The first parameter is the on/off command (1 == on, 0 == off), and the second parameter specifies the element (0 == LC server, 1 == Lightness server).

How Does the Code Know about Changes to the Device Settings?

Golioth provides a registration function for device settings.  The firmware passes a callback function to the register function.  Anytime a device setting changes, the callback function is invoked, and it receives:

  1. The string name of the setting (e.g., “TIME_RUN”)
  2. A structure containing the value.

The firmware then does a simple name string comparison, and then we send that value on to the Mesh (more about that later).

int err = golioth_settings_register_callback(settings_client, on_setting);
if (err) {
    LOG_ERR("Failed to register settings callback: %d", err);
}

And the definition of the on_setting() callback:

enum golioth_settings_status on_setting(const char *key, const struct golioth_settings_value *value)
{
…
    if (strcmp(key, "TIME_RUN") == 0) {
        /* time in seconds - numeric */
        if (value->type != GOLIOTH_SETTINGS_VALUE_TYPE_INT64) {
            LOG_DBG("Received TIME_RUN is not an integer type.");
            return GOLIOTH_SETTINGS_VALUE_FORMAT_NOT_VALID;
        }
        /* Only update if value has changed */
        if (_time_run_sec == (int32_t)value->i64) {
            LOG_DBG("Received TIME_RUN already matching local value.");
        } else {
            _time_run_sec = (int32_t)value->i64;
            _time_run_changed = true;
            // tell system thread to send the property data across the UART to
            // the BLE chip
            wake_system_thread();
        }
        return GOLIOTH_SETTINGS_SUCCESS;
    }
…
}

How Does the Code Know About RPCs?

It’s a slightly different mechanism, but straightforward: Golioth provides an RPC registration function. For every RPC you create, provide it to the registration function along with a string (linking your RPC function to the string name in the console). When the string name is invoked in the console, the local RPC function is called, passing any provided parameters. In the demo, we send that information to the Mesh (more on that later). Code snippet:

err = golioth_rpc_register(rpc_client, "lightness", on_lightness, NULL);
rpc_log_if_register_failure(err);

And earlier in the file we defined RPC function on_lightness() as:

static enum golioth_rpc_status on_lightness(zcbor_state_t *request_params_array,
                                            zcbor_state_t *response_detail_map,
                                            void *callback_arg)
{
    bool ok;
    double valuef1;
    uint8_t value1;

    ok = zcbor_float_decode(request_params_array, &valuef1);
    if (!ok)
    {
        LOG_ERR("Failed to decode RPC int1 argument");
        return GOLIOTH_RPC_INVALID_ARGUMENT;
    }

    value1 = (uint8_t) valuef1;
    LOG_DBG("Received argument '%d' from 'lightness' RPC", value1);
    send_lightness(value1);

    return GOLIOTH_RPC_OK;
}

Passing commands between nRF9160 and nRF52840

Up to now, everything has been communication between the nRF9160 and Golioth (via LTE-M or NB-IoT). So now the nRF9160 is going to send this information to the nRF52840 (the BLE chip configured as an LC client). The UART is a convenient communication path provided by the Thingy91. When any device setting changes or an RPC is called, the nRF9160 maps that to a series of bytes to send to the BLE chip.

For example, when the nRF9160 calls send_lightness(value1), it executes:

bytebuf[0] = '3'; // lightness cmd – character
bytebuf[1] = v1;  // integer val, not character - percentage
err = uart_tx(Uart, bytebuf, sizeof(bytebuf), SYS_FOREVER_US);

which sends those two bytes to the BLE chip.  Upon receiving the UART data, the BLE chip parses the lightness command, grabs the lightness value, and then sends a Bluetooth Mesh lighting command to tell the micro:bits to set the lightness value.

How does the BLE chip (the nRF52840) send Mesh commands?

The Bluetooth Mesh has a different communication method from the traditional BLE central/peripheral characteristic write/read method. The Mesh doesn’t really have the idea of a connection, nor of a central or peripheral. Every device on the Mesh is called a node, and Mesh nodes send messages that every node on the Mesh can hear (more details in a future blog).

For the demo, the nRF52840 has been programmed as a full Bluetooth Mesh node – specifically as a client (it sends commands to servers). Each LC server (micro:bit) listens for commands from the nRF52840 clients. Yes, that is plural – clients…The nRF52840 node has

  • One LC client
  • Two Generic On/Off clients
  • One Lightness client.

When the node wants to send the LC mode command, it uses the LC client function:

int bt_mesh_light_ctrl_cli_mode_set(…,…,bool enabled,…);

where the “enabled” parameter will set the LC Mode on or off.

When it wants to send a Lightness command, it uses the Lightness client function:

int bt_mesh_lightness_cli_light_set(…,…,… *set,…);

where the “set” parameter points to a structure containing the lightness level.

In general, when a node needs to send a Mesh command, it calls the appropriate client model function to send it. So, when an LC model command needs to be sent, we call an LC client model function. When a Generic On/Off model command needs to be sent, we call a Generic On/Off client model function (and similarly with Lightness commands, or any of the other commands that have been defined in the Bluetooth Mesh Model Specification).

And then the micro:bits?

The final piece of the puzzle is the micro:bits – they are programmed as mesh nodes, implementing the LC server. Each one “hears” the commands from the client node, and independently acts upon those commands. Note that each server node can return status(es) as a result of these commands, and the client node can act upon these statuses (this was not implemented in the demo, for simplicity).

The full signal chain, described

So now you have an overview of the process – specifically how:

  • The nRF9160 receives the device settings and RPCs from the Golioth console,
  • The nRF9160 sends that info over the UART to the nRF52840,
  • The nRF52840 sends that info, translated into Bluetooth Mesh commands, to the rest of the nodes in the Mesh (the micro:bits).

Bluetooth Mesh and the LC server nodes are simple in concept, but often difficult in execution. With the Thingy91 running the Golioth Firmware SDK, it has gotten a lot easier to control and extend Bluetooth Mesh demos to show to clients.

This is a guest post by Sandra Capri, CTO at Ambient Sensors, a Golioth Design Partner who regularly designs and deploys Bluetooth Solutions.

We have been building Bluetooth® Mesh (hereafter referred to as “BT Mesh”) products for a long time. In fact, we helped write some of the BT Mesh models for Nordic, so we are very familiar with it. However, we’ve always been dependent upon applications written for phones to control the BT Mesh, which can be limiting while doing Mesh development or putting together quick prototypes for potential customers. This particular demo allowed us to add network connectivity by building on top of Golioth’s Thingy91 demo, which unlocks a faster design iteration cycle when it comes to BT Mesh. We asked Chris to help record the demo video above.

Controlling from afar

This demo shows that Golioth can control a BT Mesh-based lighting system where each of the individual components (nodes in the mesh) of the lighting system, (e.g., switches, luminaires, sensors), contain a BLE SoC. Normally a user must be within a few tens of meters of a Bluetooth device to interact with it using (for example) a mobile app on a smartphone. But instead, we show Golioth connecting via LTE-M cellular service to a Nordic Semiconductor Thingy91 (combined nRF9160 cellular modem and nRF52840 BLE SoC) which then communicates to the rest of the Bluetooth mesh, thus allowing an authorized user from anywhere on Earth to control the lights (represented in the demo by a series of bbc:microbits). This gives the user control over all aspects of the Mesh servers – sending commands and updating settings without needing to be physically present.

How it works

The button on the Thingy91 is programmed to function as a simple on/off light switch. A much greater set of control functions for the BT Mesh comes from the Golioth web interface (Console). The demo shows a small sample of the kinds of control that can be accomplished using Golioth:

  • Ramping up the LED level on the lights
  • Defining the LED levels
  • Setting the time to keep the LEDs at those levels before ramping down
  • Overriding the LED level – preventing ramping

The Thingy91 contains an nRF9160 LTE-M chip and an nRF52840 BLE chip – this represents the bridge between LTE-M and the BT Mesh. The bbc:microbits contain an nRF52833 BLE chip, and represent the luminaires (Mesh lighting nodes). The Golioth SDK has been integrated into the code for the nRF9160, connecting the Thingy91 to Golioth. When a user executes a Remote Procedure Call (RPC) or sets a device setting, this information is sent via LTE-M to the nRF9160. That chip converts that information into opcode values sending it via UART to the nRF52840 chip. The nRF52840 then builds an appropriate Bluetooth mesh command and sends it to the Mesh network (wirelessly). Each lighting node on the network receives and executes the Bluetooth Mesh command.

In this demo, each lighting node (bbc:microbit) implements the Bluetooth Mesh LC server: an intelligent Lighting Controller. If the LC element on the lighting node receives a Generic On command, it ramps up its LED level to the configured lightness on value and maintains that level for the period of the configured run on time. After that time has expired, it will fade the light level to the configured prolong lightness level and maintain that for the configured prolong time. Then it fades the LEDs to the configured standby lightness level.

Each of these configurable lightness levels and times can be changed in the Device Settings.

Once these are set, an RPC can initiate the LC server control, or it can override the LC server and set an exact light level preventing the LC server from controlling the lights. Additionally, there is an RPC that will re-enable the LC server.

Other applications

This demo gives just a taste of the control that Golioth can provide with the Bluetooth Mesh. In addition to lights and switches, many different classes of devices can be on a Mesh:

  • Burglar and fire alarms
  • Smart locks
  • Thermostats
  • All kinds of sensors (e.g., temperature/humidity, photometry, motion, power, environmental, …)
  • More!

Commands can flow from the Cloud to each device, and status can be returned to the Cloud (e.g., alerts, alarms, etc.) Are the warehouse doors locked and the burglar alarm set? Well let’s check Golioth – oops, still unlocked! Let’s fix that. OK, Golioth just locked the front door, verified the locked status of the other doors, and turned on the burglar alarm. And just for good measure, we checked that the current temperature and humidity are within acceptable parameters. We didn’t even have to leave home to do that.

Now that we have the ability to control all this from the Cloud, we’ve unlocked new designs for our customers, and we’re able to spin up test deployments faster than ever.

We always seem to meet people at conferences who are looking for a Bluetooth gateway solution. Golioth is the universal connector for IoT, and today we’re going to take a look at one way to extend that to your Bluetooth devices.

Luckily, Bluetooth is well supported by Zephyr. We’re going to run some sample code, then customize it to use Golioth as the cloud connection for a Bluetooth gateway device. Our target hardware is an nRF9160-DK as the gateway (we’ll refer to this as the central), and an nRF52840-DK as the sensor (which we’ll call the peripheral).

Two blue Nordic development boards on a wooden desk

Upper: nRF52840-DK as a Bluetooth health temperature sensor
Lower: nRF9160-DK as a Bluetooth-to-Cellular gateway

We’re using the nRF Connect SDK version of Zephyr. You can follow our NCS with Golioth documentation to install it locally if you have not already done so.

This guide uses Golioth Firmware SDK v0.12.0 with NCS v2.5.2

HCI low power UART

We chose the nRF9160-DK because it has both an nRF9160 to provide a cellular connection, and an nRF52840 as a Bluetooth radio. The nRF9160 will run the show, communicating with the nRF52840 over UART. Nordic has an HCI low power UART sample that we will compile and run on the nRF52840 (the one that’s on the nRF9160-DK board) before we work on the central code for the nRF9160.

1. Build the HCI low power UART

Note that this build command compiles code for the nRF52840 chip that is on the nRF9160-DK development board.

cd ~/golioth-ncs-workspace/nrf/samples/bluetooth/hci_lpuart
west build -b nrf9160dk_nrf52840 .

2. Move the Prog/Debug switch and flash

Locate the PROG/DEBUG switch on the nRF9160-DK and move it to the nRF52 position. This controls a mux that points at the Bluetooth chip. Use west to flash the firmware:

➜ west flash
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner nrfjprog
Using board 960088581
-- runners.nrfjprog: Flashing file: /home/mike/golioth-compile/golioth-firmware-sdk/nrf/samples/bluetooth/hci_lpuart/build/zephyr/zephyr.hex
[ #################### ]   4.820s | Erase file - Done erasing
[ #################### ]   1.207s | Program file - Done programming
[ #################### ]   1.227s | Verify file - Done verifying
Enabling pin reset.
Applying pin reset.

When done, move the programming switch back to nRF9160 so it’s ready for the next step.

Load the central_ht sample on the nRF9160-DK

Zephyr includes a Health Temperature Service sample application which we’ll use as our hello world. Let’s compile and flash the “central” part of that sample.

1. Add board files to the central_ht sample code

Navigate to the central_ht sample code in the Zephry tree:

cd ~/golioth-ncs-workspace/zephyr/samples/bluetooth/central_ht/

We need to add a boards directory and create a conf file and and two overlay files for the nRF9160 in that directory. These configure the board to use the HCI low power UART firmware we flashed in the previous section as the Bluetooth radio (think of it as a secondary modem controlled by the application processor on the nRF9160).

In total we add 3 files:

  • nrf9160dk_nrf9160_ns.conf
  • nrf9160dk_nrf9160_ns.overlay
  • nrf9160dk_nrf9160_ns_0_14_0.overlay
# HCI low power UART
CONFIG_NRF_SW_LPUART=y
CONFIG_NRF_SW_LPUART_INT_DRIVEN=y

CONFIG_UART_2_ASYNC=y
CONFIG_UART_2_INTERRUPT_DRIVEN=n
CONFIG_UART_2_NRF_HW_ASYNC=y
CONFIG_UART_2_NRF_HW_ASYNC_TIMER=2
#include <nrf9160dk_nrf52840_reset_on_if5.dtsi>

/ {
    chosen {
        zephyr,bt-uart=&lpuart;
    };
};

&gpiote {
    interrupts = <49 NRF_DEFAULT_IRQ_PRIORITY>;
};

&uart2 {
    current-speed = <1000000>;
    status = "okay";
    /delete-property/ hw-flow-control;

    pinctrl-0 = <&uart2_default_alt>;
    pinctrl-1 = <&uart2_sleep_alt>;
    pinctrl-names = "default", "sleep";
    lpuart: nrf-sw-lpuart {
        compatible = "nordic,nrf-sw-lpuart";
        status = "okay";
        req-pin = <21>; /* <&interface_to_nrf52840 3 0>; */
        rdy-pin = <19>; /* <&interface_to_nrf52840 2 0>; */
    };
};

&pinctrl {
    uart2_default_alt: uart2_default_alt {
        group1 {
            psels = <NRF_PSEL(UART_TX, 0, 18)>,
                <NRF_PSEL(UART_RX, 0, 17)>;
        };
    };

    uart2_sleep_alt: uart2_sleep_alt {
        group1 {
            psels = <NRF_PSEL(UART_TX, 0, 18)>,
                <NRF_PSEL(UART_RX, 0, 17)>;
            low-power-enable;
        };
    };

};

It’s important to add this second overlay file that properly maps the reset line for newer nRF9160-DK boards:

/* Use the reset line that is available starting from v0.14.0 of the DK. */
#include <nrf9160dk_nrf52840_reset_on_if9.dtsi>

2. Build and flash the central_ht sample code

west build -b nrf9160dk_nrf9160_ns .
west flash

If you get an error when trying to flash the board, ensure you have the PROG/DEBUG switch in the nRF91 position.

The nRF9160-DK will immediately begin scanning for compatible Bluetooth human temperature services. Let’s set up one of those next.

Load the peripheral_ht sample on the nRF52840-DK

The previous steps complete the “central” part of the Bluetooth equation which will scan for available sensors. Now we need to create a “peripheral”, which is the sensor that will advertise itself and serve temperature readings from a sensor.

Building and flashing this sample code is very straight-forward:

cd ~/golioth-ncs-workspace/zephyr/samples/bluetooth/peripheral_ht/
west build -b nrf52840dk_nrf52840 .
west flash

Monitor the output

Connect to the nRF9160-DK over serial and you should see the device scan for a compatible peripheral, connect to it, and begin taking temperature readings:

*** Booting nRF Connect SDK v2.5.2 ***
Temperature 23.75C.
Connected: E9:E3:F1:6D:A9:87 (random)
[ATTRIBUTE] handle 25
[ATTRIBUTE] handle 26
[ATTRIBUTE] handle 28
[SUBSCRIBED]
Temperature 23.75C.
Temperature 23.75C.
Temperature 23.75C.
Temperature 23.75C.
Temperature 24C.

Next up, let’s connect Golioth and send these readings to the cloud!

Add Golioth to the project

To Golioth to this project we need three things:

  1. Ensure Golioth is installed as a Zephyr module
  2. Add Kconfig symbols to enable nRF9160 cellular and to add Golioth
  3. Add a few API calls to the central_ht code to connect to Golioth and push the temperature reading to the cloud

In this section we’ll be working with the Zephyr central_ht sample so let’s switch to that directory:

cd ~/golioth-ncs-workspace/zephyr/samples/bluetooth/central_ht/

1. Add Golioth as a Zephyr Module

If you followed our getting started guide for NCS, this is already done. If not, you can follow the Adding the Golioth Firmware SDK to an Existing Zephyr West Project section of our SDK readme.

2. Add Kconfig symbols for cellular and Golioth

For these changes, we’ll crib a lot of code from Golioth’s LightDB Stream sample since we’re using that service to stream the temperature to the cloud.

Add the contents of the nRF9160-DK board file from the Golioth lightdb_stream sample to the board file you previously created in the central_ht Zephyr sample. Here’s what that file should look like now:

# General config
CONFIG_HEAP_MEM_POOL_SIZE=4096
CONFIG_NEWLIB_LIBC=y

# Networking
CONFIG_NET_SOCKETS_OFFLOAD=y
CONFIG_NET_IPV6=y
CONFIG_NET_IPV6_NBR_CACHE=n
CONFIG_NET_IPV6_MLD=n

# Increase native TLS socket implementation, so that it is chosen instead of
# offloaded nRF91 sockets
CONFIG_NET_SOCKETS_TLS_PRIORITY=35

# Modem library
CONFIG_NRF_MODEM_LIB=y
CONFIG_NRF_MODEM_LIB_ON_FAULT_APPLICATION_SPECIFIC=y

# LTE connectivity with network connection manager
CONFIG_LTE_CONNECTIVITY=y
CONFIG_NET_CONNECTION_MANAGER=y
CONFIG_NET_CONNECTION_MANAGER_MONITOR_STACK_SIZE=1024

# Increased sysworkq size, due to LTE connectivity
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048

# Disable options y-selected by NCS for no good reason
CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_PSK_ENABLED=n
CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED=n

# Generate MCUboot compatible images
CONFIG_BOOTLOADER_MCUBOOT=y

# HCI low power UART
CONFIG_NRF_SW_LPUART=y
CONFIG_NRF_SW_LPUART_INT_DRIVEN=y

CONFIG_UART_2_ASYNC=y
CONFIG_UART_2_INTERRUPT_DRIVEN=n
CONFIG_UART_2_NRF_HW_ASYNC=y
CONFIG_UART_2_NRF_HW_ASYNC_TIMER=2

Now in the prj.conf file for the central_ht code sample, add the following Kconfig symbols. These have the effect of enabling the Golioth SDK, enabling the Stream service, and using runtime settings to store your device credentials in the storage partition using the Zephyr shell. The main stack size is also increased to account for some additional memory usage.

CONFIG_BT=y
CONFIG_LOG=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_SMP=y
CONFIG_BT_GATT_CLIENT=y
CONFIG_CBPRINTF_FP_SUPPORT=y
# Golioth Firmware SDK
CONFIG_GOLIOTH_FIRMWARE_SDK=y

# Application
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_GOLIOTH_SAMPLE_COMMON=y
CONFIG_LOG_BACKEND_GOLIOTH=y
CONFIG_GOLIOTH_SETTINGS=y
CONFIG_GOLIOTH_STREAM=y

CONFIG_GOLIOTH_SAMPLE_HARDCODED_CREDENTIALS=n

CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y

CONFIG_SHELL=y
CONFIG_SETTINGS=y
CONFIG_SETTINGS_RUNTIME=y
CONFIG_GOLIOTH_SAMPLE_PSK_SETTINGS=y
CONFIG_GOLIOTH_SAMPLE_SETTINGS_AUTOLOAD=y
CONFIG_GOLIOTH_SAMPLE_SETTINGS_SHELL=y

CONFIG_LOG=y
CONFIG_EVENTFD_MAX=14
CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=1536
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=10240
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=2048
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=2048
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_POSIX_MAX_FDS=23

3. Add Golioth API calls to main.c

With all the configuration in place, we’re now ready to update main.c to connect to Golioth and push temperature readings to the cloud.

First, add some includes, create a semaphore, and add a callback near the top of main.c:

#include <golioth/client.h>
#include <golioth/stream.h>
#include <samples/common/net_connect.h>
#include <samples/common/sample_credentials.h>

static struct golioth_client *client;
static K_SEM_DEFINE(golioth_connected, 0, 1);

static void on_client_event(struct golioth_client *client, enum golioth_client_event event,
                void *arg)
{
    bool is_connected = (event == GOLIOTH_CLIENT_EVENT_CONNECTED);
    if (is_connected) {
        k_sem_give(&golioth_connected);
    }
    printk("Golioth client %s\n", is_connected ? "connected" : "disconnected");
}

Next, in the notify_func() function, add an API call to send data to Golioth:

char sbuf[32];
snprintk(sbuf, sizeof(sbuf), "{\"temperature\":%g}", temperature);
printf("Sending to Golioth: %s\n", sbuf);


int err = golioth_stream_set_async(client,
                   "sensor",
                   GOLIOTH_CONTENT_TYPE_JSON,
                   sbuf,
                   strlen(sbuf),
                   NULL,
                   NULL);
if (err) {
    printf("Failed to push temperature: %d\n", err);
}

Finally, in main() add code to start the Golioth client connection:

char sbuf[32];
snprintk(sbuf, sizeof(sbuf), "{\"temperature\":%g}", temperature);
printf("Sending to Golioth: %s\n", sbuf);


int err = golioth_stream_set_async(client,
                   "sensor",
                   GOLIOTH_CONTENT_TYPE_JSON,
                   sbuf,
                   strlen(sbuf),
                   NULL,
                   NULL);
if (err) {
    printf("Failed to push temperature: %d\n", err);
}

Running the demo and viewing data on the cloud

The first time you run the nRF9160-DK you need to add device credentials. Open a serial terminal to the device and use the shell to issue the following commands:

uart:~$ settings set golioth/psk-id my-psk-id@my-project
uart:~$ settings set golioth/psk my-psk
uart:~$ kernel reboot warm

With both of our boards programmed and powered on, we can view the terminal output of the nRF9160-DK to see the connection and scanning process:

*** Booting nRF Connect SDK v2.5.2 ***                                                                                                                                                         
[00:00:00.465,972] <inf> fs_nvs: 2 Sectors of 4096 bytes                                                                                                                                       
[00:00:00.466,003] <inf> fs_nvs: alloc wra: 0, fb8                                                                                                                                             
[00:00:00.466,003] <inf> fs_nvs: data wra: 0, 68                                                                                                                                               
[00:00:00.466,278] <inf> golioth_samples: Bringing up network interface                                                                                                                        
[00:00:00.466,308] <inf> golioth_samples: Waiting to obtain IP address                                                                                                                         
[00:00:02.545,684] <inf> lte_monitor: Network: Searching                                                                                                                                       
[00:00:05.688,049] <inf> lte_monitor: Network: Registered (roaming)                                                                                                                            
[00:00:05.689,117] <inf> golioth_mbox: Mbox created, bufsize: 1232, num_items: 10, item_size: 112                                                                                              
[00:00:07.861,846] <inf> golioth_coap_client_zephyr: Golioth CoAP client connected                                                                                                             
Golioth client connected                                                                                                                                                                       
[00:00:07.862,365] <inf> golioth_coap_client_zephyr: Entering CoAP I/O loop                                                                                                                    
[00:00:08.559,051] <wrn> bt_hci_core: opcode 0x0000 pool id 5 pool 0x2000d430 != &hci_cmd_pool 0x2000d488                                                                                      
[00:00:08.600,585] <inf> bt_hci_core: HW Platform: Nordic Semiconductor (0x0002)                                                                                                               
[00:00:08.600,616] <inf> bt_hci_core: HW Variant: nRF52x (0x0002)                                                                                                                              
[00:00:08.600,646] <inf> bt_hci_core: Firmware: Standard Bluetooth controller (0x00) Version 141.732 Build 3324398027                                                                          
[00:00:08.610,504] <inf> bt_hci_core: Identity: E1:99:6F:78:C4:C4 (random)                                                                                                                     
[00:00:08.610,534] <inf> bt_hci_core: HCI: version 5.4 (0x0d) revision 0x1168, manufacturer 0x0059                                                                                             
[00:00:08.610,565] <inf> bt_hci_core: LMP: version 5.4 (0x0d) subver 0x1168                                                                                                                    
Bluetooth initialized                                                                                                                                                                          
Scanning successfully started                                                                                                                                                                  
[DEVICE]: F4:BC:DA:35:5E:68 (public), AD evt type 0, AD data len 27, RSSI -85                                                                                                                  
[AD]: 1 data_len 1                                                                                                                                                                             
[AD]: 9 data_len 12                                                                                                                                                                            
[AD]: 255 data_len 8                                                                                                                                                                           
[DEVICE]: F4:BC:DA:35:5E:68 (public), AD evt type 4, AD data len 31, RSSI -85                                                                                                                  
[DEVICE]: F0:5F:8B:2D:1C:A2 (random), AD evt type 0, AD data len 20, RSSI -82                                                                                                                  
[AD]: 9 data_len 5                                                                                                                                                                             
[AD]: 25 data_len 2                                                                                                                                                                            
[AD]: 1 data_len 1                                                                                                                                                                             
[AD]: 2 data_len 4                                                                                                                                                                             
[DEVICE]: F0:5F:8B:2D:1C:A2 (random), AD evt type 4, AD data len 0, RSSI -81                                                                                                                   
[DEVICE]: E9:E3:F1:6D:A9:87 (random), AD evt type 0, AD data len 11, RSSI -29                                                                                                                  
[AD]: 1 data_len 1                                                                                                                                                                             
[AD]: 3 data_len 6                                                                                                                                                                             
uart:~$ Temperature 21.5C.                                                                                                                                                                     
Sending to Golioth: {"temperature":21.5}                                                                                                                                                       
Connected: E9:E3:F1:6D:A9:87 (random)                                                                                                                                                          
[ATTRIBUTE] handle 25                                                                                                                                                                          
[ATTRIBUTE] handle 26                                                                                                                                                                          
[ATTRIBUTE] handle 28                                                                                                                                                                          
[SUBSCRIBED]                                                                                                                                                                                   
uart:~$ Temperature 21.5C.                                                                                                                                                                     
Sending to Golioth: {"temperature":21.5}                                                                                                                                                       
Temperature 21.25C.                                                                                                                                                                            
Sending to Golioth: {"temperature":21.25}                                                                                                                                                      
Temperature 21.5C.                                                                                                                                                                             
Sending to Golioth: {"temperature":21.5}                                                                                                                                                       
Temperature 21.25C.                                                                                                                                                                            
Sending to Golioth: {"temperature":21.25}

We see the chip boot, connect to the cell network, then connect to Golioth. After that, the Bluetooth scan begins, connecting to devices it finds to query for the desired health temperature service (HTS). Once a service is found, we see temperature readings that are then pushed to the cloud.

Checking on the LightDB Stream tab in the device view of the Golioth web console shows the data arriving on the cloud!

Bluetooth sensor data shown on the LightDB Stream tab of the Golioth web console

Connecting Bluetooth to the Cloud

This lays the groundwork for connecting your Bluetooth devices to the cloud. One gateway (or a relatively small number of them) can service multiple BLE peripheral devices for both read and write activities. It’s even possible to update firmware over a Bluetooth connection. But that’s a post for a different day.

Give Golioth a try, it’s free for individuals! If you have a need for a more involved Bluetooth to Cellular gateway, we’d love to hear about it. Reach out to the Golioth DevRel team.

If you want to dive into the code from this post, start with the Zephyr central_ht sample and apply the file changes found in this gist.