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.

We are returning to Embedded World in 2024 (EW24) and will be showing off the things we’ve been working on at Golioth since last year. We’re also living a little closer to the edge and bringing some experiments and unreleased items to showcase on the floor. Demo gods, be kind!

We’ll be at the Zephyr booth, helping to showcase one of our favorite Real Time Operating Systems and Ecosystems (though not the only one we support!). Golioth generally only targets one part in our standard hardware setup, but we continuously verify a bunch of hardware on our Firmware SDK. This means that you can be certain that every new feature added to the Golioth Cloud and SDK is fully tested throughout our supported hardware. The great thing about Zephyr is that the underlying hardware works very similarly to all of our CVBs, because of the work from the silicon vendors that participate in the open source project. We’re excited because 2024 saw even more silicon vendors joining the fray!

Golioth Demos

Being part of a large project like Zephyr, we share time at the booth Kiosks. We’ll be at the Zephyr Booth (4-170) at the following times:

  • Tuesday, April 9th from 1500 to 1800
  • Wednesday, April 10th from 1200 to 1500
  • Thursday, April 11th from 0900 to 1130

All times CEST, which is GMT + 1

Hardware we’ll have on hand

  • Modbus Vibration monitor – This is our newest Reference Design that captures vibration and temperature data from a sensor normally used in industrial environments for large motors. The hardware in the Reference Design talks over RS-485 to the remote sensor and queries all of the available registers and publishes them to the Golioth cloud for processing and viewing.
  • Air Quality Monitor – It’s always interesting to see the trend lines of air quality metrics throughout the day. There are a lot of people that filter in and through the Nuremberg convention center, so we can track things like CO2 concentration just from the elevated number of bodies. The particulate counter is unlikely to go up a lot, but if there’s something like a cotton candy booth nearby, it could potentially have particulates flying through the air. I’ll be sure to go and investigate and clean up that particular air quality problem if the need arises.
  • Aludel Elixir – Powering our newer reference designs is the Aludel Elixir. This is an evolution of our previous designs (the Aludel Mini) that stitched together outside development boards that we loved like the Circuit Dojo nRF9160 Feather and the Sparkfun ThingPlus nRF9160. We pulled many of those components onto a bespoke PCB, and also added other components that can enhance any reference design. It all comes together in a custom milled case that enables maximum flexibility.

Aludel Elixir Rev B

Are you interested in your very own Aludel? Shoot me a note to discuss at Embedded World or fill out this form

Partner Demos:

AL2LOG at the AL2TECH booth (3A-335)

A render of the AL2LOG Unit

Our design partner AL2TECH based out of Italy is bringing an exciting new logging project based on the nRF9160 and an STM32 coprocessor. This is meant to target industrial applications that require a flexible way to capture data in a harsh environment.

The AL2LOG is a compact industrial logger with different sensor and actuator interfaces. This is based upon AL2TECH’s broad experience serving their clients in the energy meter/remote sensor monitoring industries.

The AL2LOG PCB (rev 1.0)

Some high level features include Cellular and GPS connectivity (nRF9160), as well as external module comms using USB-C, RS-485, and CAN. There is an Ultra Low Power acquisition and logging sub system (STM32) with a true 14 bit ADC. Industrial inputs including Digital Input, Dry Contact, Pulse Counter, 0-10V Voltage Analog Input, and 4-20mA Current Loop Input. There are outputs like Open Drain Output, and programmable supplies for devices downrange of the logger. The device can also switch external 220V via a relay. For powering the device you can use a 12-24V rail, or depend on the large D-Size High Power LTC Cells.

The device communicates back to Golioth using Zephyr/NCS and can be remotely updated using Golioth’s OTA service. Data routed from the field back to the cloud is accessible via the Golioth REST API.

BT Mesh demo from partner Ambient Sensors at the Zephyr booth (4-170)

BLE Mesh Demo from Ambient Sensors communicating through Golioth, on display at Embedded World

This demo shows Golioth controlling an LTE/BLE bridge (Thingy91) that communicates to a BLE Mesh composed of a series of lights that are running the Light Lightness Controller (the LC Server) model. Eight instances of the BBC micro:bit represent lights in this demo. The boards run BLE Mesh firmware on Nordic Semiconductor’s nRF52833, which could easily be controlling an LED driver instead of the demo LEDs.

The BLE SIG standard LC server code is running on each microbit, and is communicating with the Thingy91’s nRF52840, which communicates on the mesh as an LC client, Generic OnOff Client, and Light Lightness client. Using Golioth, the  demo remotely sends a command to turn the LC server on, and lights will then slowly dim over time (as is the functionality of the LC server). Normally lights dim after several hours, but for the purpose of the demo, we set that value to several seconds. Delay values may also be set via Golioth, showing how these settings can be tuned remotely if desired. Additionally, we have created commands to allow the Golioth user to turn on the lights, while turning off the LC Server (to prevent it from dimming the lights), and to set the lightness value to any desired value.

Golioth connectivity demo at the NXP FRDM Lab

Find Golioth at the FRDM Lab at EW24

NXP is another vendor that has great support in Zephyr. We’ll be showcasing Golioth’s solution on NXP boards at their FRDM Lab in the Messepark classroom (the main open area when you walk into the conference). You can also see all of NXPs new solutions at their main booth in the presentation hall, booth 4A-222.

Even more Golioth!

What’s that? You haven’t seen enough of us yet? You’d love to hear more on-site at Embedded World? Well let’s telegraph every place we’ll be! In addition to our time showcasing Golioth at the Zephyr booth, we’ll also be giving a couple of talks at the Zephyr stage (next to the Zephyr booth):

  • April 9th at 1400: “Building end-to-end Zephyr demos on IoT hardware”
  • April 10th at 1530: “Multi MCU OTA updates with Golioth and Simple Management Protocol”
  • April 11th at 1200: “Local toolchains, no more! Using GitHub Codespaces for training people on Zephyr throughout the world”

In the evenings (if we’re still standing), we plan to attend these open events and socialize with others in the industry:

  • IoT Stars – This is a short speaker event, as well as a networking session. We’ll have some of our portable demos with us.
  • PCBarts meetup – This is a more social event hosted by a local consulting and manufacturing company. Last year this was a wonderful group of engineers showing off their projects–personal and professional–after a long day at the tradeshow.

We really hope to see you at the conference! If you’d like to talk about Golioth and how we can work together, please fill out this form!

Timon Skerutsch is a software and electronics engineer who enjoys the systems design aspect of the work the most, from the cloud all the way down to silicon. He is the founder of the product development consultancy Diodes Delight.

Last year, I made the decision to do a personal prototype project with Zephyr instead of my go-to prototyping choices of Arduino, MicroPython, and CircuitPython. The latter two I still use a lot for rapid prototyping, as scripting languages like Python are hard to beat.

For the forthcoming project, I already knew where I was heading. I needed a sensor for my garden irrigation system that tracks the water level in my water reservoir. I had two requirements:

  • See the water level live on-site
  • Have the data transmitted to the internet so that I can see how things are doing while I’m away

The system regularly had leaks and failures in the irrigation pipes, which caused the automated watering pump to not only waste water but also fail to water my plants. This is detrimental during a heat wave! That added an additional requirement of an alarm to alert me of abnormal situations. When I started the project, I reached for Zephyr first. Let’s talk about why.

Why Arduino, anyway?

Change is a constant; no more so, than the early design phases of a product, when requirements rapidly shift. Prototyping is a crucial aspect of any development process and we often employ different tools for this than we do for our production firmware.

In the firmware world, the Arduino framework has been an immense success. Not only for teaching firmware development, but also as a quick and easy way to try out concepts and develop prototype solutions.

Not without controversy though. To this day, many embedded engineers will utter a silent curse on Arduino whenever it is encountered on the way to production. The supposed prototype firmware has morphed into the production firmware and now that needs to be extended with very complex functionality.

When deadlines are near and the stress levels are high, it is very appealing to companies to just keep using the code base that already seems to do 90% of the job. A lot of products end up shipping firmware based on frameworks like Arduino and then try to deal with the “last 10% of the work”. If you have developed software for a while you probably know those famous last 10% can be 90% of the work, completely derailing budgets and timelines.

But what is the issue? At the end of the day, Arduino is just a very light HAL based on C and C++! You can do whatever you could do in a bare metal C project. The emphasis is on “light”, which is very beneficial when you have only very basic requirements. Maybe you can get by just fine with printf() debugging and hitting that compile button in your IDE.

The Arduino HAL’s success was in part due to its simplicity and fully integrated development flow but it offers very few solutions to the modern challenges firmware developers are facing. When you consider the complexities you are faced with in modern devices, Over-the-Air firmware updates, firmware update encryption, Continuous Integration, connection to a cloud service, tracking of device status and metrics, then things start to look different.

Zephyr is changing things up in the industry

The Zephyr Project Real Time Operating System (RTOS) has seen a lot of adoption over the past years, even by Arduino themselves. They became a member of the Zephyr Project in 2023 and now contribute to the code base.

Zephyr is a very “IoT aware” RTOS and offers a lot of robust solutions to many of the very complex topics I mentioned. It is also one of the main targets of the Golioth Firmware SDK for that reason.

When developers first start interacting with Zephyr they often tend to be a bit intimidated. Device Tree and KConfig may be familiar tools for Embedded Linux developers, but not for someone coming from bare metal C or FreeRTOS. (Editor’s note: this is why Golioth offers free training around Zephyr).

Zephyr-specific tooling like the west meta tool means there’s a lot to learn when you start diving into Zephyr. You might start to question if that work is worth it. Especially early on in a development process where you want to move quickly and prove your concepts. You might feel a huge system like Zephyr could slow you down.

Due to the steep learning curve, Zephyr does not really have a reputation for being a tool for prototyping. But I think Zephyr has very much a place in that phase of a project and it comes with a lot of benefits once you move beyond the prototype: You are already in an environment that won’t hold you back when it comes to solving the tough problems of modern production ready firmware.

Now that I am up the (arguably steep) learning curve associated with Zephyr, I think in many cases I can produce a working solution a lot quicker than with Arduino or even MicroPython.

Not just for production grade firmware

Since I was starting a prototype, I chose an ESP32 dev board I had laying around which came with a nice little OLED screen. For the sensor, I opted for an industrial liquid level sensor. They are essentially pressure sensors in a sturdy form factor that measure the pressure differential of the outside air and the pressure seen in the liquid container.

I needed something rugged and precise to track abnormal water usage so that was a perfect solution. I ended up getting a stable 0.2mm resolution for my water column, much more than I needed. The sensor is a simple 4-20mA current loop that you often see in industrial automation and I connected that to an external precision ADC.

My firmware needs included:

  • WiFi provisioning
  • Network and application protocol to get the data to a server
  • OTA to update the device remotely
  • A GUI to show water levels on the OLED
  • ADC reads to ingest the sensor data

I opted to use Golioth for networking and OTA. While not a typical service for a hobby project, they have a (recently updated) free tier for individuals and it made the whole thing really easy. It only takes a couple lines of code to integrate into any Zephyr project and makes transmitting data to a database as easy as Serial.print(). Having OTA available is a matter of a KConfig option. Most importantly I don’t need to manage an internet facing server application!

net_connect();

golioth_client_t client;
const golioth_client_config_t* client_config = golioth_sample_credentials_get();
client = golioth_client_create(client_config);
golioth_client_register_event_callback(client, on_client_event, NULL);

err = golioth_lightdb_set_int_sync(client, "water-level", water_level/1000, 2);
if (err) {
    LOG_WRN("Failed to transmit water-level: %d", err);
}

I could have directly implemented CoAP or MQTT and host my own server for the receiving side. Both protocols are natively supported by Zephyr, which means I have flexibility if I change my mind on the server side in the future.

OTA firmware updates is also a concept native to Zephyr and very important: no matter what platform I choose! The Arduino ESP32 core has an option for OTA but if you are looking at any other MCU you would have to implement that from scratch which is a whole project in itself.
In Zephyr this is all enabled by the fantastic MCUBoot bootloader.

Abstractions are your friend in a complex world

The platform agnostic nature of Zephyr is powerful. Say you have already written a lot of code and then notice that your chosen MCU does not actually fulfill your needs. You don’t need to start from scratch because all of these advanced APIs are fully abstracted in Zephyr. You can retarget your code to a different platform with minimal code changes. The primary work will be in recreating your devicetree for the new hardware.

Arduino also is known for abstraction, but when it comes to more complex features the Arduino HAL is not defining an interface. Generally, you tend to need to stick to a particular platform if you want to take advantage of the underlying hardware’s fancy features, that is if they are available at all (in Arduino).

Lock-in with a specific IC is a painful lesson we all learned during the chip shortages of 2021 and 2022. Device Tree overlays are a great tool to stay on-top of changing hardware and describe those changes in a clean way. That flexibility is not only important from a risk perspective. Staying flexible during the prototype stage (where requirements change rapidly) allows you to try out different sensors and peripherals.

Changing a sensor in Zephyr is a matter of changing a dozen lines of devicetree definitions without needing to touch a single line of C code. This is made easier when the sensor is “in tree”, but it not the only way to use a new sensor. Devicetree also becomes a powerful tool in the early days of hardware development where your product might go through many revisions and changes. People on your team might be working with different hardware revisions but require the latest bug fixes.

This can quickly become tough to manage in the firmware if you had pin or even IC changes. No need for a ton of #ifdef‘s; all you need is a set of devicetree overlays that describe your various board revisions, the C code can most often stay the same. This not only makes your life easier but also helps reduce mistakes and stale code.

If you are still trying out options you can also interactively work with sensors through the Zephyr Shell, which makes for a great workflow to quickly try out several sensor candidates without writing firmware.

During my project I was unsure whether to choose a different MCU, because the built-in ADC of the ESP32 is quite noisy. In the end, I kept the ESP32 and chose to use an external ADC. My code did not have to change because the ADC API abstracts that away. It was just a matter of defining what ADC my project should use in the devicetree. My code does not need to care if that is an external I2C device or a peripheral internal to my MCU.

/{
    zephyr,user {
        io-channels =
            <&adc_ext 0>;
    };
};

&i2c0 {
    status = "okay";
    adc_ext: mcp3421@68 {
        compatible = "microchip,mcp3421";
        reg = <0x68>;
        #io-channel-cells = <1>;
    };
};

&adc_ext {
    status = "okay";
    channel@0 {
        reg = <0>;
        zephyr,gain = "ADC_GAIN_1";
        zephyr,reference = "ADC_REF_INTERNAL";
        zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
        zephyr,resolution = <18>;
        zephyr,differential;
    };
};

That is the benefit of fully abstracted subsystems, your application’s assumptions can stay the same most of the time. Last minute system changes are less painful during firmware development.

Complex UI’s don’t have to be complex to build

For the GUI, I went with LVGL, a popular UI framework that has been integrated into Zephyr.
That was probably the most eye opening experience to me. Normally you would have to mess with display drivers that all work very differently depending on the plugged in display. Then I would need to write code to manually transfer the rendered framebuffer to that display, which  again, tends to work differently with each display.

In Zephyr all I have to do is to modify the devicetree for which display driver my OLED needs, the resolution, and the bus it is connected to.

&spi3 {
    status = "okay";
    st7789v_st7789v_ttgo_128x64: st7789v@0 {
        compatible = "sitronix,st7789v";
        spi-max-frequency = <20000000>;
        reg = <0>;
        cmd-data-gpios = <&gpio0 16 GPIO_ACTIVE_LOW>;
        reset-gpios = <&gpio0 23 GPIO_ACTIVE_LOW>;
        width = <135>;
        height = <240>;
        x-offset = <53>;
        y-offset = <40>;
        vcom = <0x19>;
        rgb-param = [CD 08 14];
    };
};

With that done you can write powerful UIs with just a couple lines of code, all the hard stuff is handled behind the scenes. I figured the UI part would be the majority of work for this project, but ended up being done in under an hour.

Often Arduino prototypes tend to have character displays or use the same old school bitmap font because fonts are hard and font systems even harder. In LVGL, you have an array of modern fonts available and it’s fairly easy to include your own font.

Arranging elements is also trivial in LVGL. No need to manually calculate a bunch of stuff like your text length. It has a lot of functions available for laying out complex arrangements.
You can build some really pretty smartphone level UIs with it. These run on very constrained hardware and it doesn’t cost you your sanity in the process!

const struct device *display_dev;
display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));

lv_obj_t *level_label;
lv_obj_t *status_label;
static lv_style_t level_style;
lv_style_init(&level_style);
lv_style_set_text_font(&level_style, &lv_font_montserrat_48);

level_label = lv_label_create(lv_scr_act());
lv_obj_align(level_label, LV_ALIGN_CENTER, 0, -20);

status_label = lv_label_create(lv_scr_act());
lv_obj_align(status_label, LV_ALIGN_BOTTOM_MID, 0, -50);
lv_obj_add_style(level_label, &level_style, 0);
// display the current water level
lv_label_set_text_fmt(level_label, "%llu", water_level/1000U);
lv_task_handler();

Quick, once you know how to get around

Within a day I had firmware for my hardware and it even looked…pretty!
Since its creation, my device has dutifully reported the water level and withstood the winter season.

The application is ~180 lines of C code, including a lot of error handling. That is really not a lot of code, for so much complex functionality. This is only possible thanks to all of the available abstractions that make writing the actual application logic a breeze. In the background there are multiple threads running but my code doesn’t even need to be aware of that. It is all handled by the kernel and the well-written libraries.

While simple things like an ADC read can be very verbose in Zephyr and a bit more complicated than in Arduino land, the hard parts–the things we often perceive as the last 10%–are a whole lot easier! You don’t want to start empty handed when you are tasked to implement encrypted firmware updates, OTA, telemetry or integrating the cloud team’s new backend system.

The grass is not always greener, of course. Zephyr is still comparatively young in embedded terms and while there are a lot of devices already supported, it is hard to beat the vast amount of drivers available for Arduino. In some situations you might want to do a first board bring up with an existing Arduino driver before implementing a Zephyr driver. License permitting, the driver code could be the basis for a port to Zephyr.

One reason why I think it is so common for firmware to get “stuck” in Arduino-land is because there has not been a good (free) alternative if you wanted to write C (or C++) and not pigeon hole yourself early on into one specific obscure vendor toolchain. If there is nothing obvious to move to, it can make the decision even harder and it will be put off until it’s usually too late.

Concepts like devicetree and a complex build system like CMake can be daunting at first, but there are ripe benefits at the end of the learning curve. If you want to learn Zephyr, Golioth regularly offers free Zephyr training sessions or you can read some of the great blog posts that cover the more gnarly bits of Zephyr in digestible bite sized portions.

The Hackaday Superconference returned for the second post-lockdown year. This was actually the seventh “Supercon” and Dan and Mike were on hand to represent Golioth. As badge-hacking is a large part of the social scene at the conference, we spent a fair amount of time getting data from the non-connected badge up to the cloud. What started out as a test-equipment related project ended up as a community art project.

Conference Admission with a Decades-Old Bench Tool

If you read the Golioth blog, chances are you already know about Supercon. But if not, it’s a conference aimed at people building and working with electronic hardware. Fittingly, instead of a printed plastic rectangle on a lanyard, the Supercon badge is itself an electronic device. This year it was modelled after a Vectorscope; a type of oscilloscope that plots two voltage signals along X and Y axes, instead of plotting a signal in the time domain.

Hackaday Superconference Badge

Hackaday Supercon badge displaying the Golioth logo

We had high ambitions for our badge hack. The original plan was to cache the captured voltages being measured by the badge’s ADCs and send them up to the cloud so they could be replayed later. This was a pretty interesting idea, since every badge has a set of inputs and outputs. We could run custom code on the inputs of our badge and capture data from the unaltered badges of people we ran into. We could potentially catch the vector source by using jumper wires to their output pins.

Animated GIF of the badge showing a vector trace

Vectorscope demonstration. Image credit: Hackaday

Alas, the badge creators did a great job of squeezing impressive performance out of the RP2040-based hardware. A tight chain of DMA and PIO (programmable input-output) kept the pipeline of samples out of the processor. We were able to capture some lossy data, and we think we could have tapped into a data stream on the order of 50 ksps, but then RAM becomes an issue to cache that kind of throughput. This meant our original plan was not to be.

Luckily, Dan noticed that a “sketch” app was included on the badge. A static image is simpler to offload to the cloud and render on a gallery page. We had some of the Golioth Aludel-mini hardware with us that uses an nRF9160 cellular modem. So we set off to make it happen.

Badgecase: the Badge Showcase

Golioth Badgecase website

The impatient reader can head over to Badgecase.io and see the results of our badge hacking. All of the art on that page was entered on a conference badge and uploaded over cellular.

The Gist of the Hack

The stock badges run Micropython. We compiled a custom version so that we could implement i2c peripheral mode on the badge using the RP2040 hardware peripheral. This makes it look like a sensor; Golioth is great at harvesting sensor data and sending it to the cloud. We grab the image data (ignoring all white background pixels), sliced it up into i2c packets, then combined those into CBOR packets and upload them to LightDB Stream.

This is where the cloud side of things takes over. A frontend written in Rust uses a Websocket listener to react to incoming packets. It queries the Golioth cloud for the unique ID of each image, collects all of the blocks that shared the same UID, and reassembles them into PNG files. To add a maker’s mark, a Golioth remote procedure call (RPC) was used just prior to upload to add a title/name to each piece of artwork. All is hosted on a dynamic page which we will convert to a static entry for posterity. However, we had enough fun with this that we may do more of these shenanigans using different subdomains in the future, so watch this space.

The Firmware

Alterations to the actual Micropython apps running on the badge were minimal, the majority of the work came in the i2c peripheral functions as they chopped up data into packets and handled incoming requests from the i2c controller.

def encode_point_for_upload(x, y, color):
    x_high = (x << 10) & 0b1111110000000000
    y_mid = (y << 4) & 0b0000001111110000
    coord = x_high | y_mid
    if color == gc9a01.RED:
        coord |= 1
    elif color == gc9a01.GREEN:
        coord |= 2
    elif color == gc9a01.BLUE:
        coord |= 3
    elif color == gc9a01.BLACK:
        coord |= 4
    else:
        coord |= 0

    print(coord >> 8, coord & 0x00FF)
    return (coord & 0x00FF, coord >> 8)

def menu(key):       # exit and return to menu
    ostentus_i2c.fifo_init_samples()
    for i,m in enumerate(model):
        for j,n in enumerate(m):
            if n != gc9a01.WHITE:
                print(i,j,n)
                coord, col = encode_point_for_upload(i, j, n)
                ostentus_i2c.fifo_put_point(coord, col)
    ostentus_i2c.fifo_finalize_samples()

    while(ostentus_i2c.outgoing_data_available()):
        time.sleep(1)



    global stopflag
    print("menu")
    if vos_state.active:
        stopflag=True
        joy.detach()
        btn.detach()
        csel.detach()

The highlighted lines above are what was added to the stock Vectorscope sketch app. It works by capturing program flow when the user presses the “menu” button, which signals an exit from the app. The fifo_put_point(coord, col) and related functions are calling C code which was marshalled up to the Micropython layer (more on this in a sec).

The sketch app uses a 40×40 grid with five colors (white, black, red, green, blue). We assume all pixels are white and only upload pixels of a different color. Some run-length encoding could have made things a bit leaner, but a simple approach to bit-packing worked for us.

Most of the firmware work went into the C layer and custom Micropython build. The build itself is… shall we say “bespoke”? (ie. “it’s a hack!”) It was not really in a state to publish to a repo. The custom i2c stuff is an extension of the work we already did on the Golioth Ostentus faceplate. We’ll be publishing that project publicly in a few months, so for now, here’s a Gist of the pertinent code for those who are curious.

For Supercon, the i2c files implement a FIFO, into which data from the Micropython layer may be placed. Two i2c register addresses were added, one indicating data is available, the other will send the data. It forms 36-byte packets, with the first packet containing metadata.

The Software

The Rust backend leveraged Tokio, an asynchronous runtime, and its sibling web server framework, Axum. At startup, a thread was spawned to connect to Golioth over Websockets and listen for new messages. Upon receiving a message, this thread checks whether the data was fragmented by comparing the provided total point count with the number of points in the message. If so, it waits for subsequent messages until all points had been acquired for a given image.

Given the time constraints, we wanted to make the application as lean as possible to simplify connecting to external services when deploying. The information we needed to persist after processing an image included the image itself and the name that had been provided via the previously mentioned RPC. For the image data, a blob storage service was a natural solution.

As a hack around introducing another data store to maintain a list of the images, their links, and their names, we encoded the name and UID into the name of the image file in blob storage bucket. When a request arrives for badgecase.io, the server fetches a list of objects in the buckets, parses the file names to extract the user-chosen image name, and renders an HTML page from a handlebars template using the names and links. Some lightweight server-side caching reduced the overhead of accessing the bucket on every request, and the cache can easily be invalidated upon processing a new image, meaning that new images are displayed in the showcase just moments after submission.

The popular image crate made building the PNG from the encoded 16-bit coordinate points fairly straightforward.

fn build_png(data: Vec<i16>) -> image::ImageBuffer<image::Rgb<u8>, Vec<u8>> {
    let mut buffer = image::DynamicImage::new_rgb8(40, 40).to_rgb8();

    let mut state = [[PixelColor::White; 40]; 40];
    for p in data {
        let (x, y, color) = parse_point(p);
        state[x as usize][y as usize] = color;
    }

    for x in 0..40 {
        for y in 0..40 {
            let pixel = buffer.get_pixel_mut(x, y);
            *pixel = image::Rgb(state[x as usize][y as usize].rgb().into());
        }
    }

    imageops::resize(&buffer, 400, 400, imageops::FilterType::Nearest)
}

Parsing the points was essentially the inverse of the operation in the firmware, but Rust’s enum support made working with our custom color scheme much more enjoyable.

#[derive(Clone, Copy, Debug)]
pub enum PixelColor {
    Black,
    White,
    Red,
    Blue,
    Green,
}

impl PixelColor {
    fn new(i: u8) -> PixelColor {
        match i {
            1 => PixelColor::Red,
            2 => PixelColor::Green,
            3 => PixelColor::Blue,
            4 => PixelColor::Black,
            _ => PixelColor::White,
        }
    }

    fn rgb(&self) -> (u8, u8, u8) {
        match *self {
            PixelColor::Black => (0, 0, 0),
            PixelColor::White => (255, 255, 255),
            PixelColor::Red => (255, 0, 0),
            PixelColor::Blue => (0, 0, 255),
            PixelColor::Green => (0, 255, 0),
        }
    }
}

fn parse_point(point: i16) -> (u8, u8, PixelColor) {
    (
        (point >> 10) as u8 & 0b00111111,
        ((point >> 4) as u8 & 0b00111111),
        PixelColor::new(point as u8 & 0b00001111),
    )
}

Deploying the application involved building an OCI image and pushing it up to Google Artifact Registry, then with a few clicks we had it running on Google Cloud Run. Because we had chosen Google Cloud Storage for storing the images, setting up access from the Cloud Run service was seamless. The last bit was mapping the Cloudflare-managed domain to the service because what is a hack project without a proper catchy domain?

Closing Thoughts

We spent way too much time on this hack… it was so much fun! Although our initial dream of capturing the vector traces didn’t materialize, it was still a blast seeing people make a simple drawing and having it appear almost instantly on the website. It also highlights how much stuff there can be in the process from a cellular device up to Cloud, and how Golioth makes things even easier. We only just got back home and already we can’t wait to see what happens with next year’s badge!

Every IoT device should operate over an encrypted channel. But how exactly does that security work as your fleet rapidly grows? Our recommendation is to use certificate authentication to deliver strong encryption while solving common fleet management problems. Today we’re showing how to provision and store IoT device certificates.

Certificate Authentication based on ECDSA

Golioth uses ECDSA encryption. You generate a root certificate and private key, use that key to sign all of your device certificates, then upload your public root certificate to Golioth. The last piece of the puzzled is placing a unique device certificate onto each device.

One method of doing so is to store the cert on a filesystem in non-volatile storage (NVS) that is separate from the firmware itself. Golioth has sample code to show you how, and that’s the subject of today’s video.

Root and Device Certificates

Generating the certificates is beyond the scope of this post, but the process is well-documented in the certificate authentication section of the Golioth docs. As a result of this process you will upload the generated public root certificate to Golioth. This lets Golioth securely connect to any device that uses certificates signed by your private root key.

Every device you put into the field needs to have a unique public/private key pair signed by your private root key. That device key and device certificate is what we store on the device file system.

Storing Device Certificates in a File System

Zephyr RTOS includes a filesystem which we have turned on in our certificate-provisioning sample code. Once the firmware is flashed to the device, use the Zephyr shell to create a directory in which the credentials are stored. This directory creation process can be further automated (through an mcumgr command or perhaps with some clever initialization in the firmware) but we chose to leave this as a manual step so that it’s obvious what is happening under the hood.

Credentials are transferred to the device over a USB connection using mcumgr because it facilitates writing to the filesystem over the serial connection. This is a separate process from updating the firmware, which means you can change the firmware without altering the device certificates. Likewise, you can update the certificates without overwriting the firmware.

Golioth Automatically Provisions Your Devices

We often discuss the security benefits of using certificates instead of pre-shared key authentication. But from a fleet-management standpoint there’s another huge benefit to doing so: automatic cloud provisioning for your devices.

Golioth Console showing IoT device summary

This device was automatically added using certificate authentication

In a nutshell, you don’t need to tell Golioth that you’ve created new devices. As long as they have unique certificate credentials that were signed by your root key, Golioth will authenticate your device and automatically add it to your project.

We begin with an empty project in the video, and after adding the certificates to the device it automatically appears in the Golioth device list. The name of the device is specified in the certificate itself. You choose how you want to organize your device names when you create the certificates.

This may not feel profound with a test fleet of just one device. But as you scale, the ability to add hundreds or thousands of devices to your network becomes unmanageable. This is especially true if you are responsible for adding encryption keys and device names manually. Instead, upload your public root certificate to Golioth and we take care of the rest.

Test Out Provisioning and Storing IoT Credentials Now!

Your first 50 devices are free with Golioth’s Dev Tier. Take your test drive today and see how smooth and powerful the provisioning process can be!

Golioth is a platform that helps firmware and hardware engineers integrate useful cloud functions into their products. We make it really easy to get things connected and immediately start peering into the behavior of devices in a useful way.

That said, not everyone wants to write code at any given moment. Zephyr, for all its wonderfulness, also has a pretty steep learning curve. This is why we are focused on hosting training and helping people move up that curve. So if you’re not ready to pull down an example from GitHub and compile, what is a person to do (engineer, or otherwise)?

Try out Golioth with our pre-compiled binary! We are targeting the Nordic Thingy:91, an all-in-one sensor prototyping platform (with battery) built on top of the Nordic Semiconductor nRF9160.

Great idea, but why now?

It’s a bit embarrassing to say that we didn’t think of this sooner. We have lots of platforms and lots of people wanting to try out Golioth…why didn’t we have binaries ready to go?

One reason is we expect that engineers want to build for their own platforms. A key value proposition of Golioth is that we work on custom hardware. Other platforms require that you buy their hardware in order to get access to get connectivity and hooked into their cloud platform. Wouldn’t engineers want to try things out on their own hardware? The answer is, “Yes, but not if it takes too long”. So now we’re also giving the option to try things out on Golioth without needing to set up the programming toolchain.

Getting Started with the Thingy91 Binary

There are a couple of simple steps to get started with the Thingy91 binary and trying out Golioth

  1. Get out your Thingy91 device (buy one here, if you don’t have one) and insert an activated SIM card.
  2. Download the binaries and PDF Instructions from the latest release on GitHub
  3. Follow the PDF instructions for installing tools to program your Thingy91 and get your device onto the Golioth Cloud

It really is that easy. But for a deep dive, read on for info about the Golioth Services you can test drive now that you have a functional IoT device on your workbench.

What’s in the binary?

Now that we have something you can simply program onto a device and enter credentials for, lets look at what you get to try out:

Databases

A key feature of Golioth and something that makes it into nearly every Reference Design we do is capturing time-series sensor data using LightDB Stream. When there is one-way data going from device to cloud, this is a great fit. On a sensor platform like the Thingy91, there are plenty of sensors to capture: two accelerometers, a light sensor, and a weather sensor. With Zephyr, it’s easy to take these readings and then forward them along to Golioth at customizable interval. It’s also possible to manually trigger a reading using the button on the Thingy91 (the button is under the center of the overmolded orange rubber).

We highlight the LightDB State service using 2 counters, one is incrementing, the other is decrementing. These fire on the same interval as above, but the user is able to interact with the counters from the cloud. This two-way communication is more complex, but can also provide an interesting control mechanism from the Cloud. Users can change where the counters are dynamically, by resetting the count to a particular value.

Settings

The settings service focuses on cloud to device communication. We often see deployments that want to push configuration data out to their devices in the field. The settings service allows users to select if a setting is applicable to the entire project, a subset of devices (using Blueprints as the filter), or on an individual device basis. The final option is great if you are troubleshooting a device and want to dynamically change something on the device.

With the Thingy91 binary, you can configure the red, green, and blue LED color intensities (mix and match to make new colors), the fade speed of the LEDs (it pulses on and off during operation), and the overall reporting interval mentioned above in the databases section above. By default we set the reporting interval to 60 seconds, but you might want to have your device reporting every 5 seconds for higher fidelity data on the cloud. This also allows you to scale how much data you are using from your MVNO/MNO/SIM provider.

Remote procedure calls (RPCs)

RPCs are a cloud to device communication, but the device is doing all the hard work. These enable users to trigger a function on their device from afar and the device may optionally send back data as a result. The key point is that the function being called is written on the device-side by the device programmer.

Another example of being able to throttle device data and battery up and down is the set_log_level RPC in the example below. If you call the set_log_level method along with a parameter (in this case 1, 2, 3, or 4), you can scale the verbosity of logging messages being sent back to the cloud. This is super useful for field devices, as it allows you to have a low amount of logging by default (ie: only send errors) and then scale up if there is an error reported.

Notice how the “Recent Calls” section on the Golioth Console tells you whether they have completed successfully and the round-trip time. This is also where any return messages from the RPC will show up (click the three dots to see return values).

Another example (and of course one of my favorites) is that we managed to program the piezoelectric transducer on board to play a range of different songs. Using the play_song RPC, we can trigger sounds like beep, golioth(the startup tune), mario, and funkytown.

The video above is showcasing sounds coming from the Thingy91, so…make sure you have sound turned on if you want the video to make sense.

Logging

The Golioth Logging service is a device to cloud communication service that automatically compresses and transports log messages that are part of RTOSes like Zephyr and FreeRTOS.  For the Thingy91, we enable Golioth logging in the Golioth Zephyr SDK and all of the messages being printed out on the serial terminal are also sent to the cloud.

In the RPC section above, we mentioned it’s possible to throttle log messages up and down. It’s also possible to filter messages coming back from the cloud to pick out important bits of communication coming from a wide range of devices.

Over-The-Air (OTA) updates

Golioth’s OTA service enables users to field-upgrade devices without a programming cable. This is baked into all of our SDKs and is a truly hands-off cloud to device communication. Each device that is eligible to receive a particular firmware update is notified via a listening service on a specific endpoint. When the device is eligible, it can start downloading the blocks of data over the network, validate the image, and then initiate a reset using the bootloader APIs. On the Thingy91, there is no required device interaction, the device starts the download in the background (while still transmitting things like LightDB Stream data) and then reboots when the image has been validated. You can watch all of the log messages as the download happens, if your logging is set up to see all information during the update process.

The GitHub release with the binaries includes a couple of files that makes it possible for people to not only try out the features above, but also to initiate a firmware update.

  • The initial .hex file for initial programming of the Thingy91 using nRF Connect for Desktop tools (v.1.0.0 as of this writing)
  • A .pdf showing how to download and execute the programming.
  • A .bin file that matches the hex file mentioned above for uploading to the Golioth Cloud (v.1.0.0 as of this writing)
  • An incremental .bin file that will act as your firmware upgrade (v.1.0.1 as of this writing). When you enable this release on the Golioth Cloud, you’ll see the block download start on your device.

Interacting with devices over the REST API

The Thingy91 binary is a great way to inject real data onto the Golioth platform so you can try out the Golioth REST API. All of the functions you have read about here are accessible on the REST API, including pulling data that is going from device to cloud and pushing things down from cloud to device. Hopefully having a real device you can control helps you to understand just how powerful a middleware solution like Golioth can be.

While this isn’t part of the Golioth platform, we think it’s important to point out how easy it is to map the data once it’s in Golioth. We set up a Grafana dashboard talking to our REST API endpoint and were able to extract and visualize the data described above. Notice the various readings coming back from LightDB Stream. In the lower right, we are also querying the settings for a particular device so we can view what color the LEDs should be for the device we’re viewing and how often it is sending back data. If you’d like to get access to this dashboard or need help setting up your own, email [email protected] and reference this post.

Try it, you’ll like it!

With the pre-compiled binary you’ll have a bunch of interesting data being sent to Golioth and things you can modify on the device. What’s more, you can take the code in the thingy91_golioth repository and modify it for your own projects. This demo serves as a great framework for building out your next IoT project or product, including on different hardware.

If you need help translating the code for your next device or have trouble with your Thingy91, please join us on the Golioth Forums to ask questions and brainstorm what else you can build!

A few weeks ago we held a live webinar with Infineon about how to collect sensor data using the Golioth ModusToolbox™ SDK and push that data up to the Golioth Cloud. I (Chris) was an audience member for the webinar and thought I would use my experience watching from the sidelines to comment on some things that I noticed during the video. You can watch the webinar in its entirety on YouTube now.

Starting from a low-cost dev kit

As a hardware engineer, the first thing I notice in any demonstration is the hardware the demo is running on. In this case, Mike was showcasing the SDK running on the PSOC™ 6  CY8CPROTO-062-4343W. This is a fun, low cost board (only $28 at Digikey) with the 4343W Wi-Fi module external to the main processor. It has Infineon’s signature capacitive touch controller capability on it.

The CAPSENSE™ feature is what Mike pulled into the demonstration. He utilized the touch controller to modify the brightness of an LED on a scale of 0-255. There are also cap-touch “push buttons” that can control the on/off state of that same LED. These are common use cases of cap-touch, but Mike was able to take these values and publish them to the cloud using Golioth. From there, it’s possible to view and/or chart the values in a visualization program like Grafana.

Mike demonstrating the capacitive touch controller values going through Golioth to Grafana

It’s not a large mental leap to consider this same function being used for larger LED lighting arrays or industrial control panels. With Golioth’s LightDB State, it’s also possible to modify the device side on/off behavior, but using the Cloud. Within the span of a few minutes, Mike was able to showcase what entire companies build their product functionality around. Golioth gives you these capabilities, just about “out of the box”.

The ModusToolbox™ Ecosystem

Clark from Infineon walked everyone through how ModusToolbox™ pulls together a bunch of tools. This feels like the evolution–and improvement–of the ways that IDEs used to work. Many of the same paradigms of pulling in sample code are there, but the tools are now external to an IDE (hardware and firmware engineers will recall the days of every IDE remaining captive within a customized Eclipse environment).

Now that the tools are external, you have the option to choose a wider variety of coding platforms to work from. This is a welcome change, especially as a VScode convert. Mike showed how it’s possible to boot up a new project using the examples pulled in through ModusToolbox™, while also configuring a VScode workspace for your project.

Under the hood, this is a FreeRTOS implementation. However, I am regularly calling out the difference between the RTOS (in this case, FreeRTOS) and the ecosystem (in this case, ModusToolbox™). The ecosystem is important to engineers, because this is the layer of integration that they really want: piecing together 3rd party plugins and a range of examples to get them started quickly. The engineers at Infineon have spent their time layering drivers on top of the FreeRTOS core and enabling a range of new features specific to their parts.

Since many engineers are starting from the context of a specific chipset, it’s important to understand the difference between RTOS and ecosystem, in my opinion. As a hardware engineer, I want the convenience of trying out a range of examples specific to my hardware, instead of building my own tooling from the ground up, which would be required if I was starting just from the FreeRTOS core. Granted, some firmware and software engineers might prefer a different flow.

Get started with Golioth and Infineon

Golioth takes you even further down the path to a functional demo that you can show to your team when trying out a PSOC™ 6 part in an IoT context to your team. The Golioth Firmware SDK enables you to jumpstart development and immediately open up a range of features on your project, such as:

After you watch the video above, sign up for a free Dev Tier Golioth Account, which gives you access to your first 50 devices on the platform for free. If you have any questions, check out our Forums or jump over to our Discord channel.

This is a guest post from Chris Wilson discussing how the Golioth training inspired him to create a custom Zephyr board definition for the Adafruit MagTag board used in the training.

Back in November of 2022, I ran across a post from Chris Gammell announcing a free developer training that Golioth would be offering the following month. At the time, I had no previous experience working with Zephyr or the Golioth IoT platform, but this seemed like a good introduction to both–so I signed up!

New to Golioth? Sign up for our newsletter to keep learning more about IoT development or create your free Golioth account to start building now.

The training offered by the Golioth team was really approachable, even for people like me without an extensive background in firmware development or real-time operating systems. The training starts with a basic introduction to building firmware in the underlying Zephyr RTOS and progresses through a series of examples that showcase the features of the Golioth SDK.

However, there was one aspect of the training that initially confused me: the training docs instruct you to build firmware for the ESP32-S2-Saola-1 board, but then run that firmware image on the Adafruit MagTag board.

For example, to build the firmware for the Golioth Demo application, the -b esp32s2_saola board argument is passed to the west build command:

west build -b esp32s2_saola app/golioth-demo

Why are we building firmware for a completely different board? 🤔

It turns out this works because:

  1. The ESP32-S2-Saola-1 board uses the exact same ESP32-S2 system-on-chip (SoC) as the Adafruit MagTag board, so firmware compiled for one board can run on the other.
  2. The Golioth training repo includes some additional Zephyr “overlay” files that modify the base board definition for the ESP32-S2-Saola-1 in Zephyr to work with the additional hardware features on the MagTag board.

This highlights one of the strengths of the underlying Zephyr RTOS: the ability to quickly extend or modify existing board definitions through the use of devicetree overlay files. Overlays make it possible to extend or modify an existing board definition to support new hardware variants, without having to go through the major effort of defining and upstreaming a brand new board definition to the Zephyr project.

This is great for getting something running quickly, but since these are totally different boards, I thought it felt a bit awkward (and potentially confusing) to keep using the esp32s2_saola board name in the training demos. I thought:

Wouldn’t it be nice if we could use the adafruit_magtag board name in the Golioth demo apps without having to add it to the upstream Zephyr repo?

Fortunately, Zephyr’s flexibility provides us with an option: we can bundle a custom MagTag board definition alongside the training demo apps, without having to touch the upstream Zephyr repository!

In this article, I’ll walk through step-by-step how I added a new board definition for the Adafruit MagTag board in the Golioth magtag-demo repository. By the end of the article, we’ll be able to pass the adafruit_magtag board argument to west commands like this:

west build -b adafruit_magtag app/golioth-demo

Understanding “Boards” in Zephyr

Since we want to add support for a new physical board, we need to understand what a “Board” is in the Zephyr ecosystem.

Zephyr has a layered architecture that explicitly defines a “Board” entity that is distinct from other layers in the architecture like a “CPU” or a “SoC”.

Configuration Hierarchy image from https://docs.zephyrproject.org/latest/hardware/porting/board_porting.html

The Zephyr glossary defines a board this way:

A target system with a defined set of devices and capabilities, which can load and execute an application image. It may be an actual hardware system or a simulated system running under QEMU. The Zephyr kernel supports a variety of boards.

Zephyr already has support for the Xtensa CPU (zephyr/arch/xtensa/core/) and the ESP32-S2 SoC (zephyr/soc/xtensa/esp32s2/), so we don’t need to add anything new for these layers. The only thing we need to add is a new definition for the MagTag board itself.

Let’s dig into the Zephyr documentation to see how to add support for a new board.

Adding a new Board in Zephyr

Zephyr has extensive documentation on how to add support for new hardware (see Porting). For this article specifically, I referred to the Board Porting Guide that covers how to add support for a new board in Zephyr.

The board porting guide provides a generic overview of the porting process for a fake board named “plank”, while this article tries to “fill in the gaps” for some of the more specific questions I had while working on the definition for the Adafruit MagTag board. I find it’s helpful to walk through the end-to-end process for a real board, but because this article is tailored specifically for the MagTag board, it may not exhaustively cover every possible aspect of porting Zephyr to a new board.

Zephyr is flexible and it supports pulling in board definitions from multiple possible locations. Before we can dive in and start adding a new MagTag board definition, we need to understand where to locate the files so the Zephyr build system can find them. To do that, we need to take a quick step back to understand how west workspaces and manifest repositories work.

Understanding west workspaces and manifest repositories

Building a Zephyr-based firmware image requires pulling in source code for the bootloader, kernel, libraries, and application logic from multiple Git repositories (the Zephyr term for these individual Git repositories is projects). Managing these individual repos manually would be a nightmare! Thankfully, Zephyr provides a command line tool named west that automatically manages these Git repositories for us.

West manages all these dependencies inside a top-level directory called a workspace. Every west workspace contains exactly one manifest repository, which is a Git repository containing a manifest file. The manifest file (named west.yml by default) defines the Git repositories (projects) to be managed by west in the workspace.

West is flexible and supports multiple topologies for application development within a workspace (you can read about all the supported topologies here). The magtag-demo repo is structured as a variation of the T2: Star topology. This means the magtag-demo repo is the manifest repository inside the magtag-demo-workspace west workspace, and the zephyr repository is included as a dependency in the west manifest file (in our example we keep this in deps/zephyr).

The workspace looks something like this (some folders redacted for clarity):

magtag-demo-workspace/                 # west workspace ("topdir")
├── .west/                             # marks the location of the west topdir
│   └── config                         # per-workspace local west configuration file
│
│   # The manifest repository, never modified by west after creation:
├── app/                               # magtag-demo.git repo cloned here as "app" by west
│   ├── golioth-demo/                  # Zephyr app for Golioth demo
│   │   └── boards/
│   │       ├── esp32s2_saola.conf     # app-specific software configuration
│   │       └── esp32s2_saola.overlay  # app-specific hardware configuration
│   └── west.yml                       # west manifest file
│
│   # Directories containing dependencies (git repos) managed by west:
└── deps/
    ├── bootloader/
    ├── modules/
    ├── tools/
    └── zephyr/
        └── boards/
            └── xtensa/
                └── esp32s2_saola/     # board definition for ESP32-S2-Saola-1

When we run the west build -b esp32s2_saola command, the Zephyr build system will look for a board named esp32s2_saola in a subdirectory of the zephyr/boards directory AND in a subdirectory of app/boards (if it exists). As you can see in the hierarchy above, the zephyr repo already includes the board definition for the ESP32-S2-Saola-1 board in the zephyr/boards/xtensa/esp32s2_saola/ directory, so this is the board definition that is pulled in when building the golioth-demo application.

However, if you look in the magtag-demo-workspace/app/golioth-demo/boards/ directory, you’ll notice files like esp32s2_saola.conf and esp32s2_saola.overlay that extend the esp32s2_saola board definition to enable additional software/hardware features on the MagTag board (LEDs, buttons, etc). I’ll cover the details of these files later on in this article, but for now, you just need to know that they allow application-specific modifications to the base esp32s2_saola board definition. The key takeaway here is that your Zephyr application can use and extend any existing board definition from the upstream zephyr repo.

So, to recap, if we want to add a new adafruit_magtag board definition for our app, there are two places where we could add it:

  1. In the upstream zephyr repository as boards/xtensa/adafruit_magtag
  2. In the magtag-demo repository as boards/xtensa/adafruit_magtag

If we add the board definition into the upstream zephyr repository, it would make the board definition available to anybody who uses Zephyr. That’s great! However, it can take a while for the Zephyr developers to review and approve a PR to add a new board definition. It is also required to add documentation for the board as part of the PR, which adds some additional overhead to the submission process.

In this article, we’re just going to add the custom board definition in the magtag-demo repo (as described here) so that we can bundle it alongside the training apps without waiting for it to go through the upstream submission process.

By the end of this article, we’ll end up creating the following new files:

magtag-demo-workspace/
└── app/
    ├── boards/
    │   └── xtensa/
    │       ├── Kconfig.board
    │       ├── Kconfig.defconfig
    │       ├── adafruit_magtag-pinctrl.dtsi
    │       ├── adafruit_magtag.dts
    │       ├── adafruit_magtag_defconfig
    │       └── board.cmake
    ├── dts/
    │   └── bindings/
    │       └── gpios.yaml
    ├── golioth-demo/
    │   └── boards/
    │       ├── adafruit_magtag.conf
    │       └── adafruit_magtag.overlay
    └── zephyr/
        └── module.yml

Let’s take a look at each of these files in detail.

Create the new board directory

The first step is to create a new directory where we can add the files for the adafruit_magtag board definition:

magtag-demo-workspace/app/boards/xtensa/adafruit-magtag/
This directory doesn’t need to match the board name. However, the board name must be unique. You can run west boards to get a list of the existing Zephyr board names.

Define the board hardware using Devicetree

In order to generate customized firmware for each supported board, Zephyr needs to have an understanding of each board’s hardware configuration. Rather than hard coding all the hardware details of each board into the operating system, Zephyr uses the Devicetree Specification to describe the hardware available on supported boards. Using devicetree, many aspects of the hardware can be described in a data structure that is passed to the operating system at boot time. Using this data structure, the firmware can get information about the underlying hardware through the standard devicetree.h API at runtime.

It’s easy to get overwhelmed when you first start trying to understand devicetree. Hang in there! You’ll soon see that the benefits of devicetree are worth the initial learning curve. If you’ve never worked with devicetree before, I would encourage you to spend some time reading the Introduction to devicetree in the Zephyr docs. If you prefer a video introduction, check out Marti Bolivar’s talk A deep dive into the Zephyr 2.5 device model from the 2021 Zephyr Developer’s Summit.

The devicetree data structure is essentially a hierarchy of nodes and properties. In practice, the hierarchy of nodes reflects the real-world hierarchy of the hardware, and the properties describe or configure the hardware each node represents.

There are four Devicetree files we need to provide as part of the board definition:

magtag-demo-workspace/
└── app/
    ├── boards/
    │   └── xtensa/
    │       ├── adafruit_magtag-pinctrl.dtsi
    │       └── adafruit_magtag.dts
    ├── dts/
    │   └── bindings/
    │       └── gpios.yaml
    └── golioth-demo/
        └── boards/
            └── adafruit_magtag.overlay

adafruit_magtag-pinctrl.dtsi

Zephyr uses a system called Pin Control to map peripheral functions (UART, I2C, etc) to a specific set of pins. It’s common to put these pin definitions in a <board_name>-pinctrl.dtsi file and include that file in the main <board_name>.dts device tree source file for the board.

The Golioth magtag-demo uses UART0 for the serial console, I2C1 for the onboard LIS3DH accelerometer, SPIM2 for the WS2812 “neopixel” LEDs, and LEDC0 as the PWM controller for the red LED.

Here’s the pin mapping for these peripherals on the MagTag board:

UART0:

  • TX: GPIO43
  • RX: GPIO44

I2C1:

  • SDA: GPIO33
  • SCL: GPIO34

SPIM2

  • MOSI: GPIO1
  • MISO: (not used)
  • SCLK: (not used)

To describe the hardware pin mapping, we need to create a devicetree include file:

magtag-demo-workspace/app/boards/xtensa/adafruit-magtag/adafruit_magtag-pinctrl.dtsi

First, we need to include a couple pin control header files for the ESP32-S2. These files contain macros that we’ll use in the pin control definitions:

#include <zephyr/dt-bindings/pinctrl/esp-pinctrl-common.h>
#include <dt-bindings/pinctrl/esp32s2-pinctrl.h>
#include <zephyr/dt-bindings/pinctrl/esp32s2-gpio-sigmap.h>
Although DTS has a /include/ "<filename>" syntax for including other files, the C preprocessor is run on all devicetree files, so includes are generally done with C-style #include <filename> instead.

Espressif also provides an ESP32-S2 devicetree include file (zephyr/dts/xtensa/espressif/esp32s2.dtsi) that contains a devicetree node for the pin controller called pin-controller with a node label named pinctrl:

pinctrl: pin-controller {
    compatible = "espressif,esp32-pinctrl";
    status = "okay";
};

We need to extend this node to include the missing pin configuration for the MagTag board. Zephyr provides a convenient shortcut to refer to existing devicetree nodes via the &node syntax (where node is the node label). In the adafruit_magtag-pinctrl.dtsi file, we’ll refer to this node as &pinctrl and extend it by providing additional properties:

&pinctrl {
    ...
};

Pin control has the concept of states, which can be used to set different pin configurations based on runtime operating conditions. Currently, two standard states are defined in Zephyr: default and sleep. For the Golioth magtag-demo we’re only going to define pin mappings for the default state.

Let’s define the default state mapping for the UART0 pins. We’ll define a node named uart0_default with matching node label uart0_default. Since the RX pin requires an internal pull-up to be enabled on our board, we’ll define two groups: group1 and group2. Groups allow properties to be applied to multiple pins at once, and we’ll use it here to apply the bias-pull-up property to the RX pin. In each group, pins are declared by assigning one of the macro definitions from esp32s2-pinctrl.h to the pinmux property. For example, the UART0_TX_GPIO43 macro assigns GPIO43 to the UART0 peripheral as TX, and UART0_RX_GPIO44 assigns GPIO44 to the UART0 peripheral as RX:

&pinctrl {

    uart0_default: uart0_default {
        group1 {
            pinmux = <UART0_TX_GPIO43>;
        };
        group2 {
            pinmux = <UART0_RX_GPIO44>;
            bias-pull-up;
        };
    };
    
};

We can follow the same procedure to define additional pin mappings for the I2C1, SPIM2, and LEDC0 peripherals (you can see the complete pin control mapping file here).

Now that we’ve got the pin control mappings defined, we can use them in the main adafruit_magtag.dts devicetree source file.

adafruit_magtag.dts

To describe the hardware available on the board, we need to create a devicetree source (DTS) file:

magtag-demo-workspace/app/boards/xtensa/adafruit-magtag/adafruit_magtag.dts

First, we add a line specifying the devicetree syntax version we’re going to be using in the file:

/dts-v1/;

Next, we include the ESP32-S2 SoC devicetree definitions provided by Espressif in zephyr/dts/xtensa/espressif/esp32s2.dtsi:

#include <espressif/esp32s2.dtsi>

This file defines the hardware available on the ESP32-S2 SoC such as the available CPUs, flash memory, WiFi, GPIOs, etc.

Note that many of the peripherals defined in this file are disabled by default (status = "disabled";). We’ll enable all the peripherals used on the MagTag board later.

Since the MagTag board has a PWM-capable LED, we also need to include the PWM device tree bindings header file so that we can use the PWM_HZ(x) macro:

#include <zephyr/dt-bindings/pwm/pwm.h

Finally we include the Pin Control file we created earlier which defines the pin control mappings for the board:

#include "adafruit_magtag-pinctrl.dtsi"

Now we can define the actual device tree data structure for the MagTag board.

/ defines the root node for the board. The model property defines a human readable name for the board, and the compatible property can be used to match this node to a compatible devicetree binding file (you can think of bindings as a sort of schema for the nodes):

/ {
    model = "adafruit_magtag";
    compatible = "adafruit,magtag";
    
    ...

First, we’ll create a node for the GPIO-controlled LEDs on the MagTag board.

The LEDs on the MagTag board are connected to GPIO pins on the ESP32-S2, so we’ll look in the devicetree bindings index to see if there is already a binding that describes this hardware feature. There’s one called gpio-leds and the description says:

This allows you to define a group of LEDs. Each LED in the group is controlled by a GPIO. Each LED is defined in a child node of the gpio-leds node.

Perfect! That sounds exactly like what we want.

We’ll create a leds node for the MagTag based on the example provided in the binding file. The compatible property says that this node is compatible with the gpio-leds binding. Each individual LED is defined as a child node under leds. For example, led_0 is defined as pin 13 on gpio0, and is assigned the node label red_led. The GPIO_ACTIVE_HIGH flag means the LED is on when the pin is high,  and off when the pin is low.

leds {
    compatible = "gpio-leds";
    red_led: led_0 {
        gpios =  <&gpio0 13 GPIO_ACTIVE_HIGH>;
    };
};

Right about now, you might be scratching your head wondering how the heck we knew what to put in the value for the gpios property (i.e. <&gpio0 13 GPIO_ACTIVE_HIGH>;).

Here’s how you figure it out:

The gpio-leds.yaml file defines the gpios property as type: phandle-array, so we know that the value for this property must be of the form <&phandle specifier1 specifier2 etc...>. We also know that the MagTag board has a RED LED connected to pin 13 of the GPIO0 controller, so we need to use the &gpio0 phandle to refer to the controller node. Let’s look up the gpio0 controller in zephyr/dts/xtensa/espressif/esp32s2.dtsi:

gpio0: gpio@3f404000 {
    compatible = "espressif,esp32-gpio";
    gpio-controller;
    #gpio-cells = <2>;
    reg = <0x3f404000 0x800>;
    interrupts = <GPIO_INTR_SOURCE>;
    interrupt-parent = <&intc>;
    ngpios = <32>;   /* 0..31 */
};

The #gpio-cells = <2>; property tells us that there are two specifiers required for the &gpio0 phandle. The compatible = "espressif,esp32-gpio"; property tells us the name of the binding that defines what those specifiers should be. Looking in zephyr/dts/bindings/pwm/espressif,esp32-ledc.yaml, it defines the specifiers required for gpio-cells:

gpio-cells:
  - pin
  - flags

Putting it all together, we can see that the property must be specified like this:

gpios = <&gpioX pin flags>;

which in this specific example is:

gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>

We can follow the same procedure to define additional nodes for the PWM LEDs and the buttons on the MagTag (using the pwm-leds and gpio-keys bindings respectively). You can see these nodes in the complete device tree source file here.

The MagTag board has a couple other GPIOs that are used to gate the neopixel power, control the ePaper display, and drive the speaker. Unfortunately, there aren’t any existing Zephyr bindings we can use to expose this hardware to the custom drivers in the magtag-demo repo, so we’ll create a simple gpios.yaml binding file that allows us to define groups of GPIOs:

magtag-demo-workspace/app/dts/bindings/gpios.yaml

The binding defines a single gpios property (similar to gpio-leds and gpio-keys):

description: |
  This allows you to define a group of GPIOs.
  
  Each GPIO is defined in a child node of the gpios node.

  Here is an example which defines three GPIOs in the node /brd-ctrl:

  / {
      brd-ctrl {
          compatible = "gpios";
          ctrl_0 {
              gpios = <&gpio0 1 GPIO_ACTIVE_LOW>;
          };
          ctrl_1 {
              gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
          };
          ctrl_2 {
              gpios = <&gpio1 15 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
          };
      };
  };

compatible: "gpios"

child-binding:
    description: GPIO child node
    properties:
        gpios:
            type: phandle-array
            required: true

Now that we have a generic gpios binding, we can add the missing nodes for the remaining GPIOs.

Let’s create a speaker node that contains the GPIOs needed for the speaker on the MagTag board. In the same way we defined the LEDs above, we define two GPIOs, active to enable the speaker and sound to drive the speaker:

speaker {
    compatible = "gpios";
    active: active_pin {
        gpios = <&gpio0 16 GPIO_ACTIVE_HIGH>;
    };
    sound: sound_pin {
        gpios = <&gpio0 17 GPIO_ACTIVE_HIGH>;
    };
};

We can follow the same procedure to define additional nodes for the neopixel power and the e-paper display GPIOs (you can see these nodes in the complete device tree source file here).

Finally, we’ll create the special /alias and /chosen nodes.

The /chosen node is used to define a set of commonly used Zephyr properties for system-wide settings like the UART device used by console driver, or the default display controller. These properties refer to other nodes using their phandles (&node, where node is the node label):

chosen {
    zephyr,sram = &sram0;
    zephyr,console = &uart0;
    zephyr,shell-uart = &uart0;
    zephyr,flash = &flash0;
};

The /aliases node is used to override generic hardware devices defined by an application. For example, the Blinky sample application requires an alias led0 to be defined. We can build and run the Blinky app on any board that defines this alias, including the MagTag board which defines the alias led0 = &red_led; to map led0 to the red LED:

aliases {
    watchdog0 = &wdt0;
    led0 = &red_led;
    pwm-led0 = &red_pwm_led;
    led-strip = &led_strip;
    sw0 = &button0;
    sw1 = &button1;
    sw2 = &button2;
    sw3 = &button3;
    neopower = &neopower;
    mosi = &mosi;
    sclk = &sclk;
    csel = &csel;
    busy = &busy;
    dc = &dc;
    rst = &rst;
    activate = &active;
    sound = &sound;
};

Now that we’ve finished creating new child nodes under the root node, we can start to customize the existing SoC nodes we included from espressif/esp32s2.dtsi. This is required to provide board-specific customizations, such configuring the pins used for a SPI peripheral or specifying the devices present on an I2C bus. As I mentioned earlier, Zephyr provides a convenient shortcut to refer to existing nodes via the &node syntax (where node is the node label) so we don’t need to write out the full device tree path.

Let’s start by taking a look at the I2C1 controller node that is defined in zephyr/dts/xtensa/espressif/esp32s2.dtsi:

i2c1: i2c@3f427000 {
    compatible = "espressif,esp32-i2c";
    #address-cells = <1>;
    #size-cells = <0>;
    reg = <0x3f427000 0x1000>;
    interrupts = <I2C_EXT1_INTR_SOURCE>;
    interrupt-parent = <&intc>;
    clocks = <&rtc ESP32_I2C1_MODULE>;
    status = "disabled";
};

We can see that the I2C1 controller is disabled by default (status = "disabled";) and it’s missing some of the properties required by the espressif,esp32-i2c binding (for example, the pinctrl properties). In our adafruit_magtag.dts file, we can refer to the &i2c1 node and define the missing required properties:

&i2c1 {
    ...
};

The pinctrl-* properties assign the i2c1_default pin control state to the controller and give it the name "default". To enable the the I2C1 controller, we override the status property by assigning status = "okay";. We also set the I2C clock frequency to I2C_BITRATE_STANDARD (100 Kbit/s).

&i2c1 {
    pinctrl-0 = <&i2c1_default>;
    pinctrl-names = "default";
    status = "okay";
    clock-frequency = <I2C_BITRATE_STANDARD>;
};

The MagTag board has an onboard LIS3DH accelerometer on the I2C1 bus, so we also add a subnode lis3dh@19. In devicetree jargon, the @19 is called the unit address and it defines the “subnode’s address in the address space of its parent node” (which in this case is the accelerometer’s I2C address in the address space of possible I2C addresses). The compatible = "st,lis2dh"; property assigns the correct binding for the accelerometer so that the Zephyr sensor drivers can use it, and the reg = <0x19>; property sets the device’s I2C address on the bus.

&i2c1 {
    pinctrl-0 = <&i2c1_default>;
    pinctrl-names = "default";
    status = "okay";
    clock-frequency = <I2C_BITRATE_STANDARD>;

    lis3dh@19 {
        compatible = "st,lis2dh";
        reg = <0x19>;
    };
};

Some nodes, like &gpio0, don’t require any additional configuration, but are disabled by default. These nodes can be enabled simply by overriding the status property:

&gpio0 {
    status = "okay";
};

We can follow the same procedure to configure the remaining nodes for the ESP32-S2 SoC (you can see these nodes in the complete device tree source file here).

adafruit_magtag.overlay

In some cases, an application may need to extend or modify nodes in the board’s devicetree structure. Zephyr provides this flexibility through the use of a devicetree overlay file. The build system will automatically pick up the overlay file if it’s placed in the <app>/boards/ subdirectory and named <board_name>.overlay.

For example, let’s create an overlay for the golioth-demo app in the magtag-demo repo:

magtag-demo-workspace/app/golioth-demo/boards/adafruit_magtag.overlay

The &wifi node for the ESP32-S2 is disabled by default. The golioth-demo app needs Wi-Fi to be enabled so it can connect to the Golioth cloud, so we’ll enable it in the app overlay:

&wifi {
    status = "okay";
};

You can see the complete overlay file here.

Define the board software features using Kconfig

Before we can compile a firmware image for the board, we need to provide some configuration options that will allow us to control which software features are enabled when building for this board. Similar to the Linux kernel, Zephyr uses the Kconfig language to specify these configuration options.

For more details on how to use Kconfig to configure the Zephyr kernel and subsystems, see Configuration System (Kconfig) in the Zephyr docs.

There are four Kconfig files we need to provide as part of the board definition:

magtag-demo-workspace/
└── app/
    ├── boards/
    │   └── xtensa/
    │       └── adafruit-magtag/
    │           ├── Kconfig.board
    │           ├── Kconfig.defconfig
    │           └── adafruit_magtag_defconfig
    └── golioth-demo/
        └── boards/
            └── adafruit_magtag.conf

Kconfig.board

This file is included by boards/Kconfig to include your board in the list of available boards. We need to add a definition for the top-level BOARD_ADAFRUIT_MAGTAG Kconfig option. Note that this option should depend on the SOC_ESP32S2 Kconfig option which is defined in soc/xtensa/esp32s2/Kconfig.soc:

config BOARD_ADAFRUIT_MAGTAG
    bool "Adafruit MagTag board"
    depends on SOC_ESP32S2

Kconfig.defconfig

This file sets board-specific default values.

# Always set CONFIG_BOARD here. This isn't meant to be customized,
# but is set as a "default" due to Kconfig language restrictions.
config BOARD
    default "adafruit_magtag"
    depends on BOARD_ADAFRUIT_MAGTAG

The ENTROPY_GENERATOR Kconfig option enables the entropy drivers for the networking stack:

config ENTROPY_GENERATOR
    default y

adafruit_magtag_defconfig

This file is a Kconfig fragment that is merged as-is into the final .config in the build directory whenever an application is compiled for this board.

The CONFIG_XTENSA_RESET_VECTOR Kconfig option controls whether the initial reset vector code is built. On the ESP32-S2, the reset vector code is located in the mask ROM of the chip and cannot be modified, so this option is disabled:

CONFIG_XTENSA_RESET_VECTOR=n

Whenever we’re building an application for this board specifically, we want to ensure that the top-level Kconfig options for the SoC and the board itself are enabled:

CONFIG_BOARD_ADAFRUIT_MAGTAG=y
CONFIG_SOC_ESP32S2=y

Change the main stack size for the various system threads to 2048 (the default is 1024):

CONFIG_MAIN_STACK_SIZE=2048

Set the system clock frequency to 240 MHz:

CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC=240000000

Zephyr is flexible and it supports emitting console messages to a wide variety of console “devices” beyond just a serial port. For example, it is possible to emit console messages to a RAM buffer, the semihosting console, the Segger RTT console, etc. As a result, we need to configure Zephyr to:

  1. Enable the console drivers (CONFIG_CONSOLE)
  2. Enable the serial drivers (CONFIG_SERIAL)
  3. Use UART for console (CONFIG_UART_CONSOLE)
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y

The ESP32-S2 defines its own __start so we need to disable CONFIG_XTENSA_USE_CORE_CRT1:

CONFIG_XTENSA_USE_CORE_CRT1=n

Enable the GPIO drivers:

CONFIG_GPIO=y

The ESP32 platform uses the gen_isr_tables script to generate its interrupt service request tables. Reset vector code is located in the mask ROM of the ESP32 chip and cannot be modified, so it does not need an interrupt vector table to be created:

CONFIG_GEN_ISR_TABLES=y
CONFIG_GEN_IRQ_VECTOR_TABLE=n

Enable support for the hardware clock controller driver:

CONFIG_CLOCK_CONTROL=y

Configure the ESP-IDF bootloader to be built and flashed with our Zephyr application:

CONFIG_BOOTLOADER_ESP_IDF=y

Enable the SPI drivers for the WS2812 “neopixel” LEDs:

CONFIG_SPI=y
CONFIG_WS2812_STRIP_SPI=y

adafruit_magtag.conf

This file defines the application-specific configuration options.

For example, magtag-demo-workspace/app/golioth-demo/boards/adafruit_magtag.overlay enables & configures the WiFi networking stack, including the Golioth utilities for easy WiFi setup:

CONFIG_WIFI=y
CONFIG_HEAP_MEM_POOL_SIZE=37760

CONFIG_NET_L2_ETHERNET=y

CONFIG_NET_DHCPV4=y

CONFIG_NET_CONFIG_LOG_LEVEL_DBG=y
CONFIG_NET_CONFIG_NEED_IPV4=y

CONFIG_MBEDTLS_ENTROPY_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y
CONFIG_MBEDTLS_ECP_ALL_ENABLED=y

CONFIG_ESP32_WIFI_STA_AUTO_DHCPV4=y

CONFIG_GOLIOTH_SAMPLE_WIFI=y

# when enabling NET_SHELL, the following
# helps to optimize memory footprint
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=8
CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=8
CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=8
CONFIG_ESP32_WIFI_IRAM_OPT=n
CONFIG_ESP32_WIFI_RX_IRAM_OPT=n

Configure the build system

Before we can actually build and flash the firmware, we need to add a couple additional files for the Zephyr build system.

zephyr/module.yml

First, we need to create a module description file:

magtag-demo-workspace/app/zephyr/module.yml

This tells the build system where to find the new board and device tree source files we added above:

build:
    settings:
        board_root: .
        dts_root: .

board.cmake

In order to flash the firmware image onto the MagTag board, we need to add CMake board file:

magtag-demo-workspace/app/boards/xtensa/adafruit-magtag/board.cmake

We can just copy the file that Espressif provided for the esp32s2_saola board (since the MagTag uses the same ESP32-S2 module). This file includes the generic CMake files for the ESP32 family and OpenOCD (making sure the correct OpenOCD from the Espressif toolchain is used):

if(NOT "${OPENOCD}" MATCHES "^${ESPRESSIF_TOOLCHAIN_PATH}/.*")
    set(OPENOCD OPENOCD-NOTFOUND)
endif()
find_program(OPENOCD openocd PATHS ${ESPRESSIF_TOOLCHAIN_PATH}/openocd-esp32/bin NO_DEFAULT_PATH)

include(${ZEPHYR_BASE}/boards/common/esp32.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)

Build the firmware

At this point, we should have everything we need for the new MagTag board definition. For example, we should be able to build the firmware for the golioth-demo app using the following command:

west build -b adafruit_magtag app/golioth-demo

Next Steps

Hooray! We’ve successfully added a new board definition! 🎉

If you’d like to try out the Golioth demo apps yourself, you can take the self-paced training online for free at https://training.golioth.io/docs/intro

We can also provide private training for your company or group. Please contact us directly if interested.

Most of the time we’re writing about how to build out the firmware that will go onto your IoT Devices. Our device Software Development Kits (SDKs) are a key offering that Golioth provides to hardware and firmware engineers; we want it to be easier for you to build the code that will go onto the device out in the field. Once the code is ready to go, you need to think about how you are going to enclose the thing you just made, whether it’s a product or a prototype.

We have been building prototypes in the form of Golioth Reference Designs. These are end-to-end demos that include hardware, firmware, software, and visualization, and we’ve been building a bunch of them. That has meant we are developing a “high mix, low volume” set of products (really prototypes) and have needed to figure out how to utilize enclosures for the different things we’re trying to do.

What’s in the box

Most of our current Reference Designs contain a few key things:

  • A development board, such as the Sparkfun Thing Plus nRF9160 (our current most-used board)
  • Sensor boards or interfaces to external items
  • A battery
  • A centralized board that makes it easy to mount each of the above items

Off-the-shelf hardware like this is less a “product” than it is a first round prototype or even Proof-of-Concept. It can be used to test a business idea or to build requirements for a future high volume product.

Requirements influence design

We have tried a few different ways of enclosing our projects so far. As you’ll read below, each time we built something, we took the lessons we learned and modified our next iteration.

Color demos (Aludel)

Our first round of assumptions was that we needed to make everything waterproof and have maximum flexibility. As such, we decided on the Bud model PN-1324-CMB. It’s a normally gray opaque base with a clear polycarbonate lid where we could have a display element on a top PCB. The main function was that we could implement lots of cabling internally but still have access to Click boards (based on the Mikroelectronica MikroBus standard). The cabling would all go through cable glands to keep out moisture. I also liked that the box had flanges so we could screw this unit to a wall or a display stand.

This method worked out OK but had the downside of requiring each sensor to be either outside the box and cabled in or we needed to “break” the waterproof nature to monitor things like air humidity. Most things we were planning to do involved direct monitoring of the environment. This, in combination with the large overall size of the demos, meant that we thought we could improve on future revisions.

You can see these cases in action (including cover plates that hid the base PCB) in the green and blue demos that we took to Embedded World in 2022.

Trashcan Reference Design – First iteration

Since we were monitoring using a time-of-flight sensor on the Trashcan Reference Design, I knew we would need the sensor to be external to the case. Or at least it would need to be pointing outside the envelope of the case, as shown below. I used a Sparkfun breakout board for the VL53L0X sensor from ST Micro and drilled out a hole to allow the signal to point down into a trashcan and reflect off the top of the trash piled up inside that can. Then I hot glued it in place.

Open case

This is unrealistic in a deployment as a trashcan is a harsh environment, especially deployed somewhere like a national park. The moisture and heat inside a trashcan would require the device be more watertight. After building this iteration and realizing we were not designing something deployable, my mindset around how we needed to make our cases began to change. It would be imperative on anyone who utilizes our reference designs to make revisions, optimizing it for manufacture and hardening it for their specific environmental needs. What is the impact if our Reference Designs are no longer planned to be field ready or watertight?

Trashcan Reference Design – Second Iteration

In the second iteration of the Trashcan Reference Design, I started to optimize for some different elements, most noticeably the repeatability of the design. There is nothing special about the PCB shown below, only that it can be manufactured more easily than the hotglue-laden prototype in the first iteration. Again, we see that the design is not water tight. I did focus on maintaining a smaller form factor than the Aludel, with a low cost case.

Side view of case open

The IoT Trashcan Reference Design with the top of the case open

This one is the Bud CU-1937-MB, part of their “Utilibox” line. Not designed to show much of anything, it’s meant to really be screwed to the wall and forgotten (as many IoT things are). I once again chose a design that had flanges as part of the case for easy mounting on a wall; in this case, it’d be mounted on the underside of a trashcan lid.

The difficulty was in a non-standard cutout. Also the fact that I was using a Dremel hand tool instead of something more precise like a milling machine. This goes back to the “high mix” element, it was tough to justify buying a milling machine and then programming it or getting better at manual machining. I took my best shot with a Dremel (spending quite a bit of time in the process) and then 3D printed a thin case over top of the quite ragged holes that I cut.

Side view of case closed

A side view of the IoT Trashcan Reference Design including 3D printed cover

Most people wouldn’t/won’t see this element of the design, but it’s still important not to have holes cut out that are all jagged and rough.

Aludel Mini

In our most current iteration on display at Embedded World coming up in 2 weeks, I decided to take a different tack, based on some brainstorming with my coworker Mike. We had already re-designed the landing board to once again utilize Click boards from Mikroelectronica, and to fit in the same case (CU-1937-MB); this was called the “Aludel mini”, playing off the name of the original Aludel case design.

It was unclear at first if we could fit the size of the headers (in width and height) into that case, but it turns out we can. We really liked that there is a wide variety of sensors available in that form factor and we can also buy things like the Arduino to Click converter boards to make the same sensors available on platforms like the nRF9160-DK.

So now that we know we are using that same case and that we are somewhat OK with a 3D printed element on that case, I decided to take a leap and have Bud modify the boxes to plan for this flexibility. Instead of having them mill out just the button and USB C port cutouts, I decided to have them basically wipe out two of the walls of the case. That might be a bit extreme, but it allows for ultimate flexibility.

This was a custom order (with an upcharge for doing so, understandably), but it was a pretty seamless process to go back and forth with the manufacturer rep (Pinnacle Marketing, based on my location in the US Southeast) and the manufacturer (Bud Industries) and get these made. They did warn that the structural integrity of the case would be impacted, but I moved forward knowing we would be able to reinforce with the inserts we design.

Now the trick was to design something that could fit this shape. They have a high draft angle, ostensibly to make it an easier ejection from injection molding equipment, but that makes for some odd trapezoidal walls. I brought the supplied 3D model into FreeCAD and set to work printing and iterating on the design. Here’s the shape I ended up with:

I utilized the mounting posts for the top case as a reinforcing element. For most of the designs I put this into, I’ll glue the plate in place, but it is also designed to slide in and out for prototypes.

On the other side of the board, we expect there to be much more variation. Often this is where wires or extension cables are interfacing with the case. I took the base plate and was able to draw simple shapes onto the face plate to create a variation for each reference design that we make. This will depend on the type of Click board we have plugged into the Aludel Mini. For the Soil Moisture Reference Design, you can see that I made a cutout that allows the Click Shuttle Expander to go out to individual boards:

Soil Moisture Monitor closed with sensors closed

The obvious question

With all of these modifications, the obvious question is: Why not 3D print the entire thing?

A fair question, and definitely something that I might consider in a future iteration. I would still want to maintain many of the same features that we have put into the existing designs (relatively small envelope, mounting flanges, plain exterior). The cost would be roughly the same if sent to China for 3D printing (depending on material used), when compared to a low volume run of modified cases + printed inserts. I also like the implied constraints of choosing an enclosure and then needing to work around those constraints, although the exercise changes when designing a product. It prevents adding “just one more thing”, when in our case we’re primarily trying to showcase the platform and not the hardware itself.

High mix is not the norm

Most product companies work differently. If you are spending enough money to hire engineers to design a product, you are likely making a higher volume of devices to support the salaries of engineers. In that case, your constraints change quite a bit. When moving to a higher volume manufacturing run, it makes sense to take everything you learn from a Golioth Reference Design and spin it into a higher volume product (custom PCBs, integrated sensors, more complex battery management, bespoke watertight enclosures, etc). If you want to take Reference Designs for a spin and use them as the basis of your next product, we’d love to talk. If you’re more interested in chatting about how to optimize cases for a variety of different designs, post about it on our forum!

Eli Hughes is the principal of Wavenumber LLC which delivers positive outcomes in the areas of embedded systems, software, IOT, audio, acoustics, industrial design, and content creation.

My experience with Nordic microcontrollers and modern cloud connectivity began in December of 2017. However, it is only this month that Nordic Semiconductor finally released a Wi-Fi capable component. The nRF7002 is a low power Wi-Fi 6 helper chip (no microcontroller internally) that is deeply integrated with the Nordic Connect SDK (NCS). This article details how I got it booted and why I decided to make my first foray into using it with the Golioth Cloud.

Using Golioth for my 1st nRF7002 project

The nRF7002-DK arrives on my bench

Most of my experiences straddle the worlds of hardware and software. Network connectivity, security, and cloud “backend” can get complex quickly. For hardware-oriented minds, navigating the complexities of modern data transport are daunting. Gone are the days when adding embedded “internet connectivity” meant all one had to do was send a raw UDP packet!

Likewise, folks experienced in modern web backend technologies infrequently approach embedded development. Golioth appeared to be a bridge between these two worlds. Providing engineers on the embedded side tools for easily and securely connecting to the cloud is a big deal. Likewise, an experienced backend engineer will love hearing about access via REST APIs.
The arrival of the nRF7002-DK was a great opportunity for me to “kick the tires”. I also had personal interest in the secure approach to CoAP with DTLS.

Setup to “Live on the Edge” with nRF7002-DK

At the time of this writing, Zephyr support for the nRF7002 is still in active development. One of my soapbox pitches for Zephyr is that it is much more than an RTOS. It comes with a testing framework, open community development model, and packaging system. I could see the current nRF7002 activity on the main branch of the sdk-nrf repository. The last official release was v2.2. To experiment with the nRF7002 I needed to work from main or with one of the v2.2.99 development tags.

Observing nRF7002 development activity

Zephyr’s west tool is used to manage packages and dependencies through a west manifest. Golioth takes full advantage of this system by building their Golioth Zephyr SDK with the NCS in mind. I knew in advance that I would be hacking on samples in the Golioth SDK tree and that I would also need to use a bleeding edge version of the nRF SDK. In this case, I chose to use the fork approach for development. Namely, I forked the Golioth SDK to give myself a personal sandbox.

Forking the Golioth Zephyr SDK

This approach has the advantage that I can experiment with the Golioth samples and submit a pull request to the Golioth team in case I come up with something useful. With my fork in place, the west manifest can be modified so I can be working with the latest nRF7002 examples.
Inside the Golioth SDK is west-ncs.yml, the manifest file for Nordic specific development.

A “one liner” to be on the bleeding edge

It literally is a “one-liner” to be on the bleeding edge, From here, I pull everything down to my local machine to get started.

Note: The assumption here is that your machine has already been setup using the Zephyr getting started guide

Initializing my “sandbox” fork of the Golioth SDK

The west update operation can take a while the first time. It is pulling down all the repositories specified in the manifest.

Basic Verification of the nRF7002

Before I dove into the Golioth samples, I loaded a nRF7002 sample to verify Wi-Fi connectivity. Inside of nrf/samples/wifi/shell is a shell sample which exposes Wi-Fi and network behaviors.

Kicking off a “pristine” build using the nRF7002dk_nrf5340_cpuapp board

The nRF7002-DK has a J-Link debugger onboard and I prefer the Ozone programming/debugging experience. Note that the nRF7002 is a Wi-Fi companion the nRF7002-DK uses a nRF5340 as the host processor. The Zephyr Wi-Fi shell is a helpful tool. I could quickly connect to my guest network and perform network operations such as a ping and DNS query.

Using the Zephyr Wi-Fi Shell for connectivity verification

Getting to Golioth

I choose to start with the Golioth logging sample located in modules/lib/golioth/samples/logging inside the forked repository. Logging is a fundamental Zephyr feature and Golioth implements a logging backend to get data up to the cloud with minimal hassle. Since the nRF7002 board is on the bleeding edge, an nRF7002dk_nrf5340_cpuapp.conf board configuration had to be added to the logging sample. The Golioth examples have board Kconfig overlays located in boards directory inside of the example application folder. These overlays are used to select settings tailored for specific board or hardware platforms.

Adding a nRF7002dk_nrf5340_cpuapp board Kconfig overlay for the Golioth Logging Sample

I started by using the esp32.conf as a template, renamed it to nRF7002dk_nrf5340_cpuapp.conf and then started hacking. Since I had just run the Wi-Fi shell example, I copied all the settings in the prj.conf from the shell application example. This approach is a bit overkill, but it is easy to remove unneeded settings once everything is functional.

I also had a hunch that TLS configuration might be an issue. Nordic has their own fork of mbedTLS, so I copied in the TLS settings from nrf9160dk_nrf9160ns.conf in the logging/boards folder

Working in a Nordic Specific TLS Configuration

Next I configured the application prj.conf with a device PSK_ID and PSK that I created via the Golioth control panel. FluffyBunny1 is now provisioned!

Adding in a Test PSK and ID

To get the Golioth Wi-Fi sample to auto-magically connect to my network at boot, the Wi-Fi SSID and password were added to the prj.conf as well. This is required because we haven’t pulled in the settings subsystem, which would allow me to configure things over UART.

Configuring the sample to auto connect to my Wi-Fi Network

One of the most frustrating “living on the edge” was trying to remember the password for my Wi-Fi network. Is it possible this task consumed the majority of my time on getting things booted? My frantic, scattered brain almost resorted to the “emergency” button on the route. Lucky for me, my spouse came to the rescue. Starting with the stock logging example, I personalized the main loop to be FluffyBunny1 friendly.

The FluffyBunny1 Logging Experiment

FluffyBunny1 is reporting!

Great success! Secure communication to the cloud in minutes. Mind you, I had to do some hacking to get support for bleeding edge hardware, but getting this result was impressive.
Me being me, I couldn’t stop after seeing log messages flow to the Golioth backend.

Enabling Golioth RPC functionality

I had read about the ability to trigger a function on the device by using Remote Procedure Calls (RPCs) on Golioth and wanted to try extending this demo. I sprinkled in a bit of code to define an RPC callback and register it. It was surprisingly simple.

Code modifications to add a bit of RPC functionality

Adding in this function allowed us to “pet the fluffy bunny” directly from the control panel. Let’s look at how it happens on the console + the uart output.

Fluffy Bunny demo

Wrapping Up

My experience with Golioth and the nRF7002 was very smooth! I was quite impressed how quickly I achieve communication with the cloud backend. Even with the “bleeding edge” nature of the nRF7002, the Zephyr ecosystem simplified bring-up. Having Golioth essentially work “out of the box” on new hardware is a big plus.

I already have an idea for a new project using Golioth with custom sensors and the nRF7002. In a previous life I was involved with “raw” postgres and TimeScaleDB. After seeing Golioth LightDB Streams, I believe I have an approach to my newfound hobby: Bonsai monitoring.

I hope you can check out Golioth for your next connected project. The overall experience was Zen-like.