Cellular-enabled devices are often deployed into far-flung locations. They are quite likely to be out of reach from physical access once deployed. Having a way to verify the network status for a device is really important to maintaining a fleet.

Nordic Semiconductors (makers of the nRF9160) built tools for returning cellular connection info into the nRF Connect SDK, their customized flavor of Zephyr. This week, we needed to retrieve cellular network info for a project. We want to share the joy of how convenient and useful this is for fleet operations.

What’s there to know about the nRF9160 Modem?

The primary info most people want from a cellular modem is the Reference Signal Received Power (RSRP). This a measure of how strong the signal from the cell tower is, and for battery-powered operations this is crucial.

For instance, let’s say you want to perform a firmware update that will download a (relatively) large amount of data. The stronger the RSRP, the more likely that packets will be received quickly and without the need to resend. Better throughput means less radio-on time for a lower power draw.

Modem Info pulled using a Golioth Remote Procedure Call

Modem Info returned using a Golioth Remote Procedure Call (RPC)

However, this is only one info item and one use example. You may want to know what type of network you’re on, which band you’re using, which bands are available, or gather device specific information like IMEI. This is all possible! Let’s walk through the Nordic Modem Info library together!

Using the Nordic Modem Info Library with Zephyr

First off, you should be using nRF Connect SDK (NCS), the Nordic flavor of Zephyr. We have a guide for setting up an NCS workspace if you need it. Using the Modem Info library is pretty straightforward. All of the steps below were found on the Nordic Modem information documentation.

1. Enable Modem Info in Kconfig

Make sure the library is built into the project by adding its Kconfig symbol in prj.conf:

CONFIG_MODEM_INFO=y

2. Initialize the Modem Info Library

You need to initialize the library before you can use it. We recommend initializing this when the app starts running, so place this call near the top of main:

int err = modem_info_init();
if (err) {
    LOG_ERR("Failed to initialize modem info: %d", err);
}

3. Use the modem_info enum to access desired data

Now we’re ready to grab data from the Modem Info library. There is a modem_info enum that contains all possible keys. The code below pulls (almost) all of those keys/values as strings and prints them out as logs.

int network_info_log(void)
{
    LOG_DBG("====== Cell Network Info ======");
    char sbuf[128];
    modem_info_string_get(MODEM_INFO_RSRP, sbuf, sizeof(sbuf));
    LOG_DBG("Signal strength: %s", sbuf);

    modem_info_string_get(MODEM_INFO_CUR_BAND, sbuf, sizeof(sbuf));
    LOG_DBG("Current LTE band: %s", sbuf);

    modem_info_string_get(MODEM_INFO_SUP_BAND, sbuf, sizeof(sbuf));
    LOG_DBG("Supported LTE bands: %s", sbuf);

    modem_info_string_get(MODEM_INFO_AREA_CODE, sbuf, sizeof(sbuf));
    LOG_DBG("Tracking area code: %s", sbuf);

    modem_info_string_get(MODEM_INFO_UE_MODE, sbuf, sizeof(sbuf));
    LOG_DBG("Current mode: %s", sbuf);

    modem_info_string_get(MODEM_INFO_OPERATOR, sbuf, sizeof(sbuf));
    LOG_DBG("Current operator name: %s", sbuf);

    modem_info_string_get(MODEM_INFO_CELLID, sbuf, sizeof(sbuf));
    LOG_DBG("Cell ID of the device: %s", sbuf);

    modem_info_string_get(MODEM_INFO_IP_ADDRESS, sbuf, sizeof(sbuf));
    LOG_DBG("IP address of the device: %s", sbuf);

    modem_info_string_get(MODEM_INFO_FW_VERSION, sbuf, sizeof(sbuf));
    LOG_DBG("Modem firmware version: %s", sbuf);

    modem_info_string_get(MODEM_INFO_LTE_MODE, sbuf, sizeof(sbuf));
    LOG_DBG("LTE-M support mode: %s", sbuf);

    modem_info_string_get(MODEM_INFO_NBIOT_MODE, sbuf, sizeof(sbuf));
    LOG_DBG("NB-IoT support mode: %s", sbuf);

    modem_info_string_get(MODEM_INFO_GPS_MODE, sbuf, sizeof(sbuf));
    LOG_DBG("GPS support mode: %s", sbuf);

    modem_info_string_get(MODEM_INFO_DATE_TIME, sbuf, sizeof(sbuf));
    LOG_DBG("Mobile network time and date: %s", sbuf);

    LOG_DBG("===============================");

    return 0;
}

Here’s what the log messages look like after this code runs:

[00:00:07.457,733] <dbg> net_info: network_info_log: ====== Cell Network Info ======
[00:00:07.458,648] <dbg> net_info: network_info_log: Signal strength: 54
[00:00:07.459,411] <dbg> net_info: network_info_log: Current LTE band: 12
[00:00:07.459,960] <dbg> net_info: network_info_log: Supported LTE bands: (1,2,3,4,5,8,12,13,18,19,20,25,26,28,66)
[00:00:07.460,906] <dbg> net_info: network_info_log: Tracking area code: 4311
[00:00:07.461,395] <dbg> net_info: network_info_log: Current mode: 2
[00:00:07.461,944] <dbg> net_info: network_info_log: Current operator name: 310410
[00:00:07.462,890] <dbg> net_info: network_info_log: Cell ID of the device: 0494980F
[00:00:07.464,050] <dbg> net_info: network_info_log: IP address of the device: 100.71.101.177
[00:00:07.464,965] <dbg> net_info: network_info_log: Modem firmware version: mfw_nrf9160_1.3.2
[00:00:07.465,850] <dbg> net_info: network_info_log: LTE-M support mode: 1
[00:00:07.466,735] <dbg> net_info: network_info_log: NB-IoT support mode: 0
[00:00:07.467,407] <dbg> net_info: network_info_log: GPS support mode: 0
[00:00:07.468,170] <dbg> net_info: network_info_log: Mobile network time and date: 23/05/19,18:48:15-20
[00:00:07.468,170] <dbg> net_info: network_info_log: ===============================

Of course, it’s not just for printing out string, there are many functions for using this information programmatically.

How Golioth is Using the Modem Info Library

Our initial use for this is purely informational. As we test devices in the field, we want to have access to cell tower information that will be helpful in troubleshooting. We could just set up a timer to periodically call our log function; since Golioth has a remote logging feature, all logs will be sent and retained on the servers. What about when we want to know this modem info on-demand?

This is perfect use-case for a Remote Procedure Call (RPC). The one tripping point I had during implementation is that the initialization function must run outside of any interrupts to avoid hard faults. With that ironed out, it was a simple matter of adding the information to the RPC response package.

static enum golioth_rpc_status on_get_network_info(QCBORDecodeContext *request_params_array,
                        QCBOREncodeContext *response_detail_map,
                        void *callback_arg)
{
    QCBORError qerr;

    qerr = QCBORDecode_GetError(request_params_array);
    if (qerr != QCBOR_SUCCESS) {
        LOG_ERR("Failed to decode array items: %d (%s)", qerr, qcbor_err_to_str(qerr));
        return GOLIOTH_RPC_INVALID_ARGUMENT;
    }

    char sbuf[128];
    modem_info_string_get(MODEM_INFO_RSRP, sbuf, sizeof(sbuf));
    QCBOREncode_AddSZStringToMap(response_detail_map,
                     "Signal strength",
                     sbuf);

    return GOLIOTH_RPC_OK;
}

The syntax is not all that different from logging the information. In this case I’m using a QCBOR helper function to add the RSRP reading to the data that will be returned to Golioth. This ensures the serialization of the packets is as efficient as possible.

Once all of the values I’m interested in are added this way, they are present in the data object returned from the RPC:

What will you use the Modem Info library for?

We’d love to hear what you are using the modem info for in your projects. Start a thread in the Golioth Forum to show off your work!

Golioth continues to run training for hardware and firmware developers looking to learn more about Zephyr and connecting their devices to the cloud; our last public training had 30 engineers trying out Zephyr and Golioth on accessible IoT hardware.

Our next training will take place on June 7th, 2023 at noon EST / 9 am PST / 6 pm CEST. There are limited slots to take part in this training and we will give priority to a select group described below.

What’s new in our training

We will continue to run training using Kasm, a virtual environment for building Golioth and Zephyr projects. Using Kasm in conjunction with Gather.town was a successful combination for our past virtual trainings. We want to ensure we can easily answer questions from different groups and accommodate as many individuals as possible.

One big change for this training is we are switching to Nordic Semiconductor hardware for the training. We previously chose to use the Adafruit Magtag, which features the Espressif ESP32-S2 onboard. However, we are excited to try out new partner hardware. It will also be interesting to have an included debugger on this hardware, which we hope to target in future versions of the training. Users who decide to continue following Golioth examples will be well prepared by using the boards listed below:

  • nRF9160-DK
    • This is the primary board that we recommend for this upcoming training. It is also one of our Continuously Verified Boards, which means you will be able to use this board to try out any feature of the Golioth platform. Golioth works great on cellular devices, and is well supported on the nRF9160. Being cellular, it means you could take part in the training from just about anywhere.
  • nRF7002-DK
    • We had written about our excitement for this platform, but with sourcing issues occurring right now, it doesn’t line up well with this upcoming training. We think this will be our main board going forward. The cost for this board is much less than the nRF9160-DK and it doesn’t require a cellular SIM in order to operate, just a Wi-Fi connection.

The key thing for this training is you can use either of these platforms and achieve the same output. We may add additional hardware in the future to become even more resilient against sourcing issues.

Want a guaranteed slot?

Golioth offers free training, but we love learning more about users and customers up front. We are offering preferential placement for users who have a business use case and are willing to talk through it with Golioth team members. Fill out the form below and indicate you’d like to be considered for this option. This isn’t a requirement, just a way to get a guaranteed spot on training.

Golioth provides a lot of different services for your IoT fleet. Under the hood they all boil down to one thing: transferring some type of data to or from a constrained device. Whether it’s your microcontroller-based sensors sending back readings, or the new settings from the cloud being pushed to a device via Remote Procedure Call (RPC), it’s all data transfer. That data should be as efficient as possible, which is why Golioth uses CBOR for serialization. This saves bandwidth and radio-on time (ie: battery life). So you’d be wise to use CBOR in your application code. Let’s dive into an example!

What is CBOR?

CBOR is the Concise Binary Object Representation. It uses the JSON data model, but packs the data more tightly. The result is not human readable, but that’s the point. IoT networks are basically robots talking to other robots, we want to tailor our data packets with that in mind. Golioth makes it easier by doing the work under the hood (in our SDK) so you don’t have to think about it…you just reap the data savings.

Sending Data with CBOR

Let’s stream some data to Golioth using the LightDB Stream sample. Our data for this will include the following:

int32_t neg_reading = −2147483647
double temperature = 23.45
double pressure = 100.133019
double humidity = 32.204101
double accX = 0.480525
double accY = 0.156906
double accZ = -9.100571
char text[] = "Golioth"

If we were sending this as JSON, it would look pretty nice:

{
    "neg_reading": -2147483647,
    "weather": {
        "temperature": 23.45,
        "pressure": 100.133019,
        "humidity": 32.204101
    },
    "accelerometer": {
        "accX": 0.480525,
        "accY": 0.156906,
        "accZ": -9.100571
    },
    "text": "Golioth"
}

But we can reduce the data footprint with CBOR encoding by using the QCBOR library. This library is already installed in your workspace because the Golioth SDK uses it for all of the behind-the-scenes communications. For instance, when sending Remote Procedure Calls (RPCs), Device Settings information, and the firmware manifest for our Over-the-Air (OTA) firmware update service. Our resident Zephyr expert Marcin gave a talk at ZDS 2022 where he described in depth how we use CBOR as part of our logging service as well. Anywhere it makes sense for us to save you data, we do that for you.

CBOR Encoding Example Code

#include <qcbor/qcbor.h>

UsefulBuf_MAKE_STACK_UB(Buffer, 300);
QCBOREncodeContext EncodeCtx;
QCBOREncode_Init(&EncodeCtx, Buffer);

QCBOREncode_OpenMap(&EncodeCtx);
QCBOREncode_AddInt64ToMap(&EncodeCtx, "neg_reading", -2147483647);
QCBOREncode_OpenMapInMap(&EncodeCtx, "weather");
QCBOREncode_AddDoubleToMap(&EncodeCtx, "temperature", 23.45);
QCBOREncode_AddDoubleToMap(&EncodeCtx, "pressure", 100.133019);
QCBOREncode_AddDoubleToMap(&EncodeCtx, "humidity", 32.204101);
QCBOREncode_CloseMap(&EncodeCtx);
QCBOREncode_OpenMapInMap(&EncodeCtx, "accelerometer");
QCBOREncode_AddDoubleToMap(&EncodeCtx, "accX", 0.480525);
QCBOREncode_AddDoubleToMap(&EncodeCtx, "accY", 0.156906);
QCBOREncode_AddDoubleToMap(&EncodeCtx, "accZ", -9.100571);
QCBOREncode_CloseMap(&EncodeCtx);
QCBOREncode_AddSZStringToMap(&EncodeCtx, "text", "Golioth");
QCBOREncode_CloseMap(&EncodeCtx);

UsefulBufC EncodedCBOR;
QCBORError uErr;
uErr = QCBOREncode_Finish(&EncodeCtx, &EncodedCBOR);

if (uErr != QCBOR_SUCCESS) {
    LOG_ERR("Failed to encode CBOR: %d", uErr);
} else {
    LOG_DBG("CBOR data size is: %d", EncodedCBOR.len);
    LOG_HEXDUMP_DBG(EncodedCBOR.ptr, EncodedCBOR.len, "CBOR Object");

    err = golioth_stream_push_cb(client, "cbor_test",
                     GOLIOTH_CONTENT_FORMAT_APP_CBOR,
                     EncodedCBOR.ptr, EncodedCBOR.len,
                     async_error_handler, NULL);
    if (err) {
        LOG_WRN("Failed to push to LightDB Stream: %d", err);
        return;
    }
}

This code was put together by following the example.c file in the QCBOR library. It begins by allocating a 300 byte stack the library will use during encoding.

A map is then opened on line 5 using QCBOREncode_OpenMap(). This is similar to an opening-curly-bracket in JSON (think of maps like JSON objects). We can then add various data types to the map.

When it comes time to encode the "weather" data, we need another map nested inside the original. We call QCBOREncode_OpenMapInMap() on line 7, assigning a key to the object ("weather"), before adding temperature/pressure/humidity data with the  QCBOREncode_AddDoubleToMap() function.

Note that we close nested maps when done adding data using the QCBOREncode_CloseMap() function. It is also called at the very end to close the outermost map.

All of our data is held as a map until the encode step that actually creates the CBOR package. On line 20 we set up a UsefulBuf—a struct from the library that simply holds a pointer and a length. On line 22, QCBOREncode_Finish() is called to finish the encoding process. Our UsefulBuf is then used to log the packet and send it to Golioth.

When it comes to the Golioth API, the only difference between using JSON and using CBOR is the last four characters on line 31: GOLIOTH_CONTENT_FORMAT_APP_CBOR

When Golioth receives this data, the CBOR is unpacked and displayed in JSON format so that it is human-readable:

Comparing CBOR serialization to JSON

If you remove all of the whitespace from our initial JSON string, you end up with 186 bytes, but when encoded using CBOR that is reduced to 154 bytes.

That savings of 32 bytes is a reduction of 17.2%. Extrapolate that to every reading, from every device, over the life of your fleet. This makes a significant impact on cellular data charges and the power budget used when sending data.

Trying it yourself, and further Reading

Give CBOR a try in your own projects. With Golioth’s Dev Tier, your first 50 devices are free just for registering an account.

You can drop the C code shared above into the Golioth LightDB Stream sample to replicate what we’ve shared in this post. If you are looking for examples of CBOR decoding, it’s used in the Golioth RPC sample as well as the LightDB LED sample.

To learn more about the QCBOR library, start with the README in the repo. From there dig into the header files in the inc folder for the Doxygen comments. As always, we’d love to know what you’re building and we’re happy to answer questions. Post your thoughts on the Golioth Forum.

Golioth hosts a lot of training on Zephyr RTOS. It’s the fastest growing RTOS and Zephyr is great for Golioth because it delivers a ton of cross-platform support, includes a built in networking stack, and it’s an open source project guided by the Linux Foundation. The only problem we’ve had is finding target hardware to use during training. Until now! The nRF7002 Development Kit from Nordic is what we are targeting for upcoming training. Let’s look at why that is.

Golioth’s hardware needs for Zephyr Training

Nordic nRF7002-DK board

Nordic’s new nRF7002 Development Kit

We have a few base requirements when choosing hardware for training:

  • In-Tree Zephyr support
  • WiFi – although we support Cellular, WiFi, Ethernet, and Thread, we find WiFi is ubiquitous and usually the most straight-forward to get connected during training.
  • USB-based firmware update we don’t want to require a separate programmer
  • User Interface – there should be some way for the user to interact with the hardware
  • Integrated sensor – Golioth makes it easy to stream sensor data to the cloud, it’s nice to show this happening live during training
  • All-in-one – we don’t want the added complexity of the user having to wire up modules.
  • Inexpensive – there’s a chance people will never use this device again after the training. So it should be inexpensive, and generate as little waste as possible.
  • Available – see also: chip shortage

Released earlier this year, the Nordic nRF7002-DK ticks almost all of these boxes. It includes the nRF7002 which is a WiFi connectivity IC, along with the nRF5340 that runs the applications. It uses the Segger on-board programmer via the USB port. It includes two user buttons and two LEDs for user interaction. There is no integrated sensor but we’re working around that.

It is inexpensive at an MSRP of $59, however production is still ramping up since the product launch. As a result, there are some stock shortages right now which we hope will improve soon. This board is a great way to try out Golioth, Nordic, and Zephyr all at the same time. All around, we think it’s likely that anyone learning to use Zephyr will find this board useful far beyond the Golioth session, helping to reduce the chance of training hardware becoming e-waste.

An early glimpse at the code

If you happen to already have an nRF7002-DK on your bench, you can give our training repository an early look. We’d love to have your feedback!

nRF7002dk simulating temperature sensor readings

Temperature values streaming to Golioth

At the time of writing, only one module has been published. It’s a “kitchen sink” demo that shows off all of the Golioth services.

I mentioned we are working around the lack of a built-in sensor, this is done with an algorithm that simulates temperature sensor data. That’s being recorded on the cloud using the LightDB Stream service which adds timestamps to each data set received.

The user-interface includes a blinking LED for output. Pressing the buttons on the board selects which of the two LEDs is currently blinking, and reports that LED number to the Golioth LightDB State service. The blinking speed can be updated remotely using our Settings service. And of course our Remote Logging service is enable on this demo so you can access the logging output as the application runs.

If you want to try this out, just follow the README and keep a few things in mind:

  1. Use `west init` to set up your workspace, and do not directly clone. That way, when you call `west update` it will pull in all dependencies (including the nRF Connect SDK and Golioth Zephyr SDK)
  2. These boards will require WiFi credentials to connect to your network, and device credentials to connect to Golioth. Both are entered via serial terminal after flashing firmware, and will be persistent (in the Zephyr settings partition) over power cycle and future firmware upgrades

Wait, what about the Zephyr training?

This post is a sneak peek. We are still actively developing this new training module and will add more subdirectories to this repository over time. I recommend starring the repo so you are notified.

The most immediate plans are to implement modules that show how to blink an LED using timers and threads, and how to react to button input. We’ll show how to define your own LEDs and buttons using Devicetree overlays and how to add sensors and enable the in-tree drivers. Of course we’d love your feedback on what else to include, please let us know!

You’re invited to Golioth’s free Zephyr training

Get up to speed with Zephyr and Golioth, sign up for our free training which is held virtually. You’ll be the first notified when our next session is scheduled.

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.

Golioth returned to Embedded World in 2023 to showcase at the Zephyr booth. We brought a range of designs built with Zephyr and connected to the Golioth Cloud. Each of our Reference Designs show how Golioth technology can target verticals throughout the industry. We are regularly creating new designs and posting about them, both on this blog and on the Golioth Projects site.

Moving To Common Elements With Our 2023 Designs

We had a more standardized form factor and design elements with our 2023 designs than our demos at Embedded World 2022. Last year, we wanted to differentiate the functions and features of the Golioth Cloud when showcasing the “color demos”. Each of these demonstrated the different parts of our platform.

Golioth Embedded World 2022 Color Demos

This year was all about showcasing how similar many IoT designs can be. By extension, we wanted to show how we can swap some hardware and firmware to target entirely different market segments.

We built a new form factor that contains off-the-shelf hardware but still presents it in a somewhat compact manner. This took the form of the Aludel Mini case and PCB design, as well as our Ostentus front panel, both of which we have written about before. The result is a black box (har har) that allows us to target verticals. Our goals in the near future is to create additional firmware resources to make it easier for our users to replicate these designs using 100% off-the-shelf components.

Asset Tracker Port Side of Aludel Mini with Ostentus

Asset Tracker Connector Side of Aludel Mini with Ostentus

The 2022 designs had explanatory information / diagrams on the top PCB. This year we migrated to putting that information on a laser cut backing plate used as a mounting surface for the actual Reference Designs. These allowed visitors to read more on their own, if they desired, and kept our Reference Designs smaller and more like what might be deployed to the field. See images below for examples of backing plates.

Reference Design Demos

We brought 5 Reference Designs with us to Embedded World. In fact, we had more demos than we had space in the Zephyr booth to showcase them! Alas, we tried our best to highlight each element to the people walking by our booth.

DC Power monitor

This design was based off of our AC Power Monitor Reference Design that we have published about before. However, when thinking about logistics at a conference, we didn’t feel comfortable monitoring AC power in the booth. Instead, Mike took the design and swapped out the Click headers and reworked some of the firmware to instead monitor USB power flowing through the design. In the video above, you can see that we monitored the current of a fan and a USB lightbulb and then were able to dynamically chart the power usage on our bespoke Grafana dashboard.

Air Quality Monitor

Our Air Quality Monitor Reference Design was so new for Embedded World that we had only just published about it on our projects site. We will be doing a blog post and video about it soon. The main focus was capturing and displaying this information both on the Ostentus display (front panel) and then on the associated dashboard.

There are two interesting things that differentiate this design from the others. The first is a Remote Procedure Call (RPC) that directly activates the fan onboard to start a cleaning cycle. This is a great example of how RPCs can be used for one-off activities triggered programmatically on an “as needed” basis.

The second is the use of LightDB State to visualize and trigger warnings from the device. Note the red dotted line in the chart on the CO2 concentration. This is a configurable level in LightDB State on a per-device basis. It could be used to trigger a local alarm (light, sound) or can be used to trigger other notification/alarm activities on the cloud.

Cold Storage Asset Tracker

Last year we brought an asset tracker in the form of the Orange Demo, based upon the Nordic Thingy91. This year we upgraded with a more accurate tracking GPS module that can run simultaneously with the cellular modem.

The unit tracks temperature for “cold storage” applications. This is a common use case for refrigerated trucks and shipping containers, as wells as tracking of vaccines in transit between medical facilities. Demonstrate GPS inside a conference hall is a challenge because of being locked to one position and under a bunch of metal girders, but we were able to showcase the underlying hardware and example paths that recalled historic trip data stored on Golioth.

IoT Trashcan Monitor

Our waste management solution is made to help municipalities and parks departments more efficiently route their diesel trash trucks. As in the rest of our demos, this becomes and exercise in scaling things down to fit on a tabletop in a conference facility. We achieved this by creating a portable (foldable) trashcan that we can setup on conference booth tables.

IoT Trashcan Monitor Demo on the right side

The miniature version of a trashcan helps to illustrate the usefulness of the Golioth Settings Service. The original demo had a trashcan that was roughly 1 meter tall and all of the “percentage-full” levels were based off of that height. Golioth makes it simple to select the individual device we brought to the show and adjust the height for a 300 mm tall trashcan. This “calibration” was instantly sent down to the device and it reported levels in exactly the same way it had for a taller trashcan.

Soil Moisture Monitor

The Soil Moisture Monitor Reference Design measures soil moisture levels and the amount of light reaching the unit. During this conference it barely saw any light, since we ran out of room on the desk! You can find full details in the soil moisture monitor demo video, and project page. We will have this and many new designs on display at the Embedded Open Source Summit in Prague in late June. Please be sure to stop by there to see what we have been working on!

We’ll Train You To Build Your Own Zephyr Design

One of the things we were sure to point out in each of the example videos above is that we are running new training sessions showing people how to design with Zephyr. If you’d like to learn how to build your next design with the popular Open Source RTOS and Ecosystem, sign up at golioth.io/ew23.

The Nordic nRF9160 is a fantastic solution for cellular connectivity. All of the Golioth sample code runs on the nRD9160-DK without any changes to configuration so you can test out all of our features. But eventually you’re going to start scratching your head about how the LTE connection works. This is especially true because the device will do nothing while first establishing the connection, which can take over one minute depending on your network. The answer is to use the LTE link controller library.

Using Automatic LTE Control

The Nordic nRF Connect SDK (based on Zephyr) makes it really easy to automatically connect to LTE. Simply put, there’s a Kconfig symbol that causes the modem to connect to the cellular network when the device powers up:

CONFIG_LTE_AUTO_INIT_AND_CONNECT=y

While this takes care of the connection, it does so before main() begins running, and it blocks program execution until the network is connected. That can take more than a minute and depends on things like your distance to your closest tower and the strength of the signal in your office. Since you can’t write log messages, toggle LEDs, or write to a screen, it can look to a user like the device is stuck.

For battery-controlled devices you want to carefully control when the radio is on and when it is off. In this case, automatic control is usually not an option.

Using Manual LTE Control

Nordic’s docs for the LTE link controller are fantastic, you should spend the time to read through them. We’ll discuss the most basic form of link control: manually establishing a connection.

To use the link controller, first select the library using Kconfig:

CONFIG_LTE_LINK_CONTROL=y

Then include the library in your c file:

#include <modem/lte_lc.h>

We can now start using the link controller functions. For me, the most interesting ones are the async functions. For instance:

int err;

//Initalize the modem and connect to the network (register a callback)
err = lte_lc_init_and_connect_async(lte_handler);

/* Do some things on the network */

//Place the modem in offline mode
err = lte_lc_offline();

/* Do some offline things */

//Modem already initialized, just reconnect again
err = lte_lc_connect_async(lte_handler);

Notice that lines 4 and 14 register a callback. Nordic has a nice example of what that callback should look like:

/* Semaphore used to block the main thread until the link controller has
 * established an LTE connection.
 */
K_SEM_DEFINE(lte_connected, 0, 1);

static void lte_handler(const struct lte_lc_evt *const evt)
{
     switch (evt->type) {
     case LTE_LC_EVT_NW_REG_STATUS:
             if ((evt->nw_reg_status != LTE_LC_NW_REG_REGISTERED_HOME) &&
             (evt->nw_reg_status != LTE_LC_NW_REG_REGISTERED_ROAMING)) {
                     break;
             }

             printk("Connected to: %s network\n",
             evt->nw_reg_status == LTE_LC_NW_REG_REGISTERED_HOME ? "home" : "roaming");

             k_sem_give(&lte_connected);
             break;
     case LTE_LC_EVT_PSM_UPDATE:
     case LTE_LC_EVT_EDRX_UPDATE:
     case LTE_LC_EVT_RRC_UPDATE:
     case LTE_LC_EVT_CELL_UPDATE:
     case LTE_LC_EVT_LTE_MODE_UPDATE:
     case LTE_LC_EVT_TAU_PRE_WARNING:
     case LTE_LC_EVT_NEIGHBOR_CELL_MEAS:
     case LTE_LC_EVT_MODEM_SLEEP_EXIT_PRE_WARNING:
     case LTE_LC_EVT_MODEM_SLEEP_EXIT:
     case LTE_LC_EVT_MODEM_SLEEP_ENTER:
             /* Callback events carrying LTE link data */
             break;
     default:
             break;
     }
}

Also notice that they recommend using a semaphore. Because this runs asynchronously, checking this semaphore is a good way for the rest of your code to know if an LTE connection has been established.

Considerations when Using Golioth with Manual Control

Golioth depends on a network connection. When you manually control the network, you should also take Golioth into consideration. You need to wait until LTE has been connected to start the Golioth System Client. A good place to do this is in the callback:

case LTE_LC_EVT_NW_REG_STATUS:
    if ((evt->nw_reg_status != LTE_LC_NW_REG_REGISTERED_HOME) &&
     (evt->nw_reg_status != LTE_LC_NW_REG_REGISTERED_ROAMING)) {
        break;
    }

    LOG_INF("Connected to LTE network. Starting Golioth System Client...");

    golioth_system_client_start();

    break;

Before going offline or putting the modem into a sleep mode it is recommended that you stop the Golioth client:

golioth_system_client_stop();

Calls to Golioth services (LightDB State, LightDB Stream, etc.) will cause errors if the client is not connected. You may choose simply to ignore the errors in the serial terminal, however, gating those function calls with a semaphore is another option.

For applications that utilize an intermittent network connection, we like using message queues to cache data. Timestamps may be added to each reading so that the data is properly recorded the next time a connection is available. We have previously discussed using Zephyr message queues for this purpose.

Conclusion

Automatic LTE control is great for trying out demo code. However, we think in most applications you’ll want to decide when and how to use the modem. Luckily, for this particular SIP, Nordic has made the control library really easy to use.

Do you have questions about cellular modem control with your IoT fleets? We’d love to hear from you! Open a new thread on the Golioth Forum, or set up a video call with our Developer Relations crew to discuss your use case.

In a best case scenario, once an IoT fleet is deployed, you never need to (physically) touch them again. Golioth helps give you tools to work with your devices remotely and make this a reality. Today, we’ll look at dynamically modifying the number of logs being sent back to the Cloud. This allows fleet managers to peek into individual devices without needing to waste data and battery power by always sending every log message back to Golioth.

Logging to the cloud is already built into Golioth, so it’s really just a matter of tuning how many logs are being sent by your devices. Golioth hooks into the Zephyr RTOS Logging service, which we’ll be showcasing here today.

Background on Remote Logging with Zephyr

The Golioth Zephyr SDK has remote logging built it, and in our sample applications (like the hello sample) it is enabled by default in the prj.conf files:

CONFIG_LOG_BACKEND_GOLIOTH=y
CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048

At the top of each C file you need to register for logging. This is also a good place to set the default logging level, which I’ll refer to as the “compiled-in” logging level.

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(remote_logging_example, LOG_LEVEL_DBG);

This compiled-in level is an important decision because the preprocessor will not include logging calls if they have a higher value than this parameter. For instance, if you set the default to LOG_LEVEL_ERR, you cannot remotely turn on debugging messages because LOG_LEVEL_DBG is a higher logging level. Here is the hierarchy of logging levels in Zephyr:

#define LOG_LEVEL_NONE 0U
#define LOG_LEVEL_ERR  1U
#define LOG_LEVEL_WRN  2U
#define LOG_LEVEL_INF  3U
#define LOG_LEVEL_DBG  4U

In our case, the solution is to use LOG_LEVEL_DBG when compiling, and programmatically set the value to a lower level at run time. This will make your binary a bit larger, since the strings for every logging message will be included, but it delivers the option to turn on debugging messages after deployment which in most cases is worth the extra bytes of flash.

Write a Function to Set Log Levels at Run Time

Now we need a function that, when called, will automatically adjust the logging level for every logging source on the device.

#include <zephyr/logging/log_ctrl.h>

void change_logging_level(int log_level) {
    int source_id = 0;
    char *source_name;
    while(1) {
        source_name = (char *)log_source_name_get(0, source_id);
        if (source_name == NULL) {
            break;
        } else {
            LOG_WRN("Settings %s log level to: %d", source_name, log_level);
            log_filter_set(NULL, 0, source_id, log_level);
            ++source_id;
        }
    }
}

This function uses Zephyr’s logging control to query the name of each logging source that is available on the system. It then uses that source name to set the new logging level.

If you are putting cellular devices into the field, you probably don’t want to have logging turned on very high (or at all) by default since you’ll be paying for bandwidth. The first thing you can do at run time is call this function:

change_logging_level(LOG_LEVEL_ERR);

Now, only error messages will be logged to the Golioth cloud.

Setting Log Level Remotely

There are two obvious ways you can go about setting log levels remotely: the Golioth Remote Procedure Call (RPC) service, and the Golioth Settings service. Since the most likely use for this is turning on logs for a single device (and not fleetwide all at the same time), using a remote procedure call makes the most sense to me.

// Remote procedure call (RPC) callback for setting the logging levels
static enum golioth_rpc_status on_set_log_level(QCBORDecodeContext *request_params_array,
                       QCBOREncodeContext *response_detail_map,
                       void *callback_arg)
{
    double a;
    uint32_t log_level;
    QCBORError qerr;

    QCBORDecode_GetDouble(request_params_array, &a);
    qerr = QCBORDecode_GetError(request_params_array);
    if (qerr != QCBOR_SUCCESS) {
        LOG_ERR("Failed to decode array item: %d (%s)", qerr, qcbor_err_to_str(qerr));
        return GOLIOTH_RPC_INVALID_ARGUMENT;
    }

    log_level = (uint32_t)a;

    if ((log_level < 0) || (log_level > LOG_LEVEL_DBG)) {

        LOG_ERR("Requested log level is out of bounds: %d", log_level);
        return GOLIOTH_RPC_INVALID_ARGUMENT;
    }

    change_logging_level(log_level);
    return GOLIOTH_RPC_OK;
}

// Register RPC to listen for "set_log_levels"
// This should be called from your "on_connect()" callback
golioth_rpc_register(rpc_client, "set_log_level", on_set_log_level, NULL);

There are two parts to the function above. The first is a callback that will run when a remote procedure call (RPC) instruction is received from the Golioth servers. It will get incoming log level as a parameter, validate it, then run the function we discussed in the previous section to change the log levels.

The second part of the code is the act of registering the RPC. This tells the Golioth servers that this device wants to be notified whenever a callback with the name "set_log_level" is issued from the Golioth web console, or via the Golioth REST API.

Golioth RPC for setting logging levels

The Golioth web console can be used to send an RPC, or you may do so using the Golioth REST API

Here’s an example of using the web console interface to submit this RPC. I sent a request to change to log level 3 (LOG_LEVEL_INF), and upon success/failure we get a notification message back. This RPC was successful, and took 913 milliseconds for the device to receive the message, execute it, and report the results.

[00:00:37.736,663] <inf> app_work: Sending hello! 2
[00:01:07.738,800] <inf> app_work: Sending hello! 3
[00:01:36.947,418] <wrn> app_rpc: Settings golioth_dfu log level to: 3
--- 12 messages dropped ---
[00:01:36.947,448] <wrn> app_rpc: Settings golioth_rd_template log level to: 3
[00:01:36.947,479] <wrn> app_rpc: Settings golioth_samples log level to: 3
[00:01:36.947,479] <wrn> app_rpc: Settings golioth_system log level to: 3
[00:01:36.947,509] <wrn> app_rpc: Settings lightdb log level to: 3
[00:01:36.947,540] <wrn> app_rpc: Settings log log level to: 3
[00:01:36.947,570] <wrn> app_rpc: Settings lte_lc log level to: 3
[00:01:36.947,601] <wrn> app_rpc: Settings lte_lc_helpers log level to: 3
[00:01:36.947,631] <wrn> app_rpc: Settings mcuboot_util log level to: 3
[00:01:36.947,662] <wrn> app_rpc: Settings modem_antenna log level to: 3
[00:01:36.947,692] <wrn> app_rpc: Settings mpu log level to: 3
[00:01:36.947,723] <wrn> app_rpc: Settings net_buf log level to: 3
[00:01:36.947,753] <wrn> app_rpc: Settings net_coap log level to: 3
[00:01:36.947,753] <wrn> app_rpc: Settings net_core log level to: 3
[00:01:36.947,784] <wrn> app_rpc: Settings net_if log level to: 3
[00:01:36.947,814] <wrn> app_rpc: Settings net_shell log level to: 3
[00:01:36.947,845] <wrn> app_rpc: Settings net_sock log level to: 3
[00:01:36.947,875] <wrn> app_rpc: Settings net_sock_addr log level to: 3
[00:01:36.947,906] <wrn> app_rpc: Settings net_sock_tls log level to: 3
[00:01:36.947,937] <wrn> app_rpc: Settings net_sock_wrapper log level to: 3
[00:01:36.947,967] <wrn> app_rpc: Settings net_socket_offload log level to: 3
[00:01:36.947,998] <wrn> app_rpc: Settings net_utils log level to: 3
[00:01:36.948,028] <wrn> app_rpc: Settings nrf_modem log level to: 3
[00:01:36.948,059] <wrn> app_rpc: Settings os log level to: 3
[00:01:36.948,089] <wrn> app_rpc: Settings pm log level to: 3
[00:01:36.948,120] <wrn> app_rpc: Settings settings log level to: 3
[00:01:36.948,150] <wrn> app_rpc: Settings shell.shell_uart log level to: 3
[00:01:36.948,181] <wrn> app_rpc: Settings shell_uart log level to: 3
[00:01:36.948,211] <wrn> app_rpc: Settings soc log level to: 3
[00:01:36.948,242] <wrn> app_rpc: Settings stream log level to: 3
[00:01:36.948,272] <wrn> app_rpc: Settings uart_nrfx_uarte log level to: 3
[00:01:37.741,027] <inf> app_work: Sending hello! 4
[00:02:07.743,041] <inf> app_work: Sending hello! 5
uart:~$ 

On the device side, we can see the output of our RPC on a serial terminal (above). There are a lot of logging sources running on this device and they have all been individually set to level 3.

Remember, if the compiled-in level for any given source has been set lower (to only show errors, or to show no logging), setting a higher number at run time will not return additional messages because higher-level messages were not included in the build.

There and Back Again

What does it take to troubleshoot an IoT device in the field? If designed correctly, it will not take a physical visit to the device, but merely a few remote communications. Ideally, you will turn on debugging, analyze the issues you’re having, and then send a command to adjust accordingly.

But even if you haven’t planned very far ahead, with Golioth you can still enable these features. We recommend that every device you put in the field have Golioth Over-the-Air (OTA) firmware updates enabled. That way, you can send these remote-logging features to your devices, even if they are already in the field.

Do you have questions or suggestions on adjusting logging levels remotely? We’d love to hear from you on the Golioth Forum!

One of my first engineering jobs was in the Test and Measurement space. Tracing everything back to standards and calibration is a key part of the process. It takes a long time and is taken very seriously. I had many learning experiences that reinforced the importance of calibration (despite my tongue-in-cheek article title).

IoT devices often don’t have the luxury of being as accurate: the cost of sensors, the power usage of analog measurements, the “awake” windows for battery power devices… all of these things contribute to different priorities while measuring the physical world. However, if you have a precise (repeatable) sensor, you can utilize the trend of the data in a useful way.

If you ARE going to calibrate, it’s normally done in a sensor library. For certain sensors, Zephyr excels at this. There are built in sensor libraries that return standard values. You can even pull calibrated and normalized readings from the sensor in real time from Zephyr’s sensor shell. I consistently use sensors like the BME280 and the LIS2DH in my reference designs since it is really easy to add the readings to the pile of data I send back to Golioth.

What happens when a sensor driver is not in tree and I don’t want to go about writing my own sensor driver? I pull the raw readings to the cloud and work with them there!

Prototyping, not production

When you are trying to prove your IoT system can work or test out a business idea, you don’t always start by making production ready designs. But you can still make useful designs by pulling raw data readings.

I did this recently with an i2c sensor that was pulling in ADC readings. The “counts” that I gathered from the ADC are tied to physical phenomenon (in this case, soil moisture), but they are not absolute values. Instead I am able to gather the readings using direct ADC readings and then publish them to Golioth’s LightDB Stream service.

Below, I will discuss three ways you can interact with this data using other Golioth services and interactively produce even more useful data. These include tweaking threshold settings, calibrating via remote procedure call (RPC), and using OTA to upload new machine learning sets.

Set “thresholds” with Settings

One of my favorite things to use the Golioth Settings Service for is interacting with data and creating an extra layer of intelligence. For the IoT Trashcan Monitor reference design, I do this to set “threshold” on each device in the field. There is a time-of-flight sensor that gathers the distance from the top of the trashcan to the top of the garbage in that trashcan. What happens if you want to utilize the same sensor but on different sized trashcans? What if you want to set the “100%” level of the trashcan differently, say if one part of the national park needs to have a cleaner look than another?

You could keep this intelligence on the Cloud, but then you don’t get the benefit of the device reporting it’s various levels, so it’s harder to read from a terminal or in the logs. I push a level I created on the Settings Service down to each device that defines different thresholds:

I also utilize these levels coming back from each trashcan to trigger icons on our Grafana dashboards, which makes it even easier to tell which devices require intervention.

The above is just for the Trashcan example, there are loads of other examples where you might want to have field-settable values, from the installer or technician. In the case of the soil moisture sensor, I want to be able to calibrate “wet” and “dry” (per the simplistic calibration instructions) and then do some interpolation in between.

On the fly modifications with Remote Procedure Calls

Most of the time when using raw data from a sensor, it is done in a “device to cloud” context. You take the reading from the sensor and ship it off to be dealt with by larger computers (AKA “the cloud”). However, some applications will include the need for some kind of feedback from the cloud computing element. You could argue this is what we were doing above, since the Settings Service is pushing data down to the device.

Sometimes you want to be able to inject data into your data measurement and management process on the device, which is a perfect use case for Remote Procedure Calls (RPCs). One way to think of RPCs is if you were accessing a function between two different parts of your code. You put the function prototype in your code.h file… except now you can access that function from the cloud. So maybe I don’t want to do a full calibration on the device, but it is beneficial to set an offset for something like an i2c-based thermocouple measurement chip like the MCP96L01T. I could easily pipe raw data from the chip up to the cloud, but I might want to change a setting for the resolution of that data or the cold junction compensation temperature.

Registers on the MCP960X family of parts. You could write a function to change these values with raw i2c writes to the chip and have the function be accessible via a Golioth RPC.

I could have a function on the device that I use during start that sets these values like set_thermocouple_resolution() and set_cold_junction_temp_c(), which I use during startup. Under the covers they would be simple i2c writes to the device to set registers with bit-masked values. However, I could also expose these to the cloud using an RPC. When I call that RPC from the REST API or the Golioth Console, I also pass a value (in this case a new resolution setting or cold junction temp), it gets validated on the device as an acceptable value, and then a success message is sent back to the cloud once its executed. Add in logging messages into the device-side code, and you should be able to easily see that the device has successfully switched modes and the raw data being piped back is now different. (The change means a higher resolution or a different cold junction compensation.)

Machine Learning + data capture + OTA

The ultimate (and most trendy) way to deal with sensor data is to not care at all about the sensor output. Instead, capture and correlate data with desired behaviors; collect data with a “known good” and a “known bad” state of your machine, for instance, to allow the model to discern between the two.

Golioth has the tools to enhance this method of working with data, on a local machine or from afar. First, you can capture general data using LightDB stream, sending back your raw data readings from your sensor. If desired, you could also link a button, switch, or other input on the device to correlate when you are performing “Action A” that is different from “Action B”. Next, you capture and ingest that data into a machine learning algorithm like PyTorch or TinyML. Finally, when you create or revise your model and build it into your design, you can upload that new firmware using the Golioth Over-The-Air (OTA) service. Over time, you can continue to refine the model and even combine it with the other methods mentioned above.

Start prototyping today!

One thing I hope you get out of reading this article is the prototyping capabilities available to you when you use Golioth. While I benefit from the Zephyr driver ecosystem (as well as driver ecosystems from other SDKs that we support), I don’t want to feel limited when I want to try out a new chip that isn’t in-tree yet. I feel comfortable doing things like raw i2c and SPI read/write functions, and now can make that data available to the Cloud for even faster prototyping. If you need help prototyping your next IoT device, stop by our Forum to discuss or send an email to [email protected].

Cellular IoT products struggle with battery life. Getting and staying connected to a cell tower takes a good amount of energy. Though we’re past the days of GSM drawing amps of current (!), it is still costly to open sessions to the tower. Understanding how your device is communicating with the Cloud is crucial to building robust devices that will last years in the field.

This webinar will include Golioth team members, alongside Jared Wolff of CircuitDojo. Jared is an early adopter of Golioth and a Golioth Ambassador, as well as an advocate for Zephyr devices. Golioth utilizes Jared’s nRF9160 Feather board designs in all of our current cellular-based Reference Designs.

What you can expect from this Webinar

First and foremost, we hope to make this a more dynamic and interactive session than many technical webinars. (No one will be reading Powerpoint slides in a monotone voice here!) The session will cover:

  • Understanding your connection to cellular towers
  • Understanding your power draw when in a passive or sleep mode
  • Measurement challenges for embedded cellular projects
  • Methods for saving data (and power) when connecting to the cloud
  • Building robust device health metrics for your fleet
  • System architecture decisions for lower power circuit boards

Sign up now!

This webinar is at 1 pm EST / 10 am PST on January 18th, 2023. If you can’t make it the day of, you can still still sign up to access the on-demand content. Those who attend will have an opportunity to ask questions towards the end of the presentation.