Golioth just wrapped another Zephyr training session that was open to the public. This was a group of 30 trainees, all remote. There continue to be challenges with remote training, but we are always refining how we train engineers and will detail some of the learnings below. We appreciate everyone who took part in the training and are energized to do another one. In fact, you can sign up for our July session at the end of this post!

Why does Golioth focus on training?

Golioth’s main business is creating the web infrastructure that IoT devices connect to and enhance IoT offerings. The devices have a seamless connection experience by using our various device SDKs. Supporting customers that use Zephyr means we have a large amount of hardware that we can support “out of the box”. Our Zephyr SDK helps us advance our goal to support the maximum number of hardware platforms. We also have a Golioth Firmware SDK that extends that goal by supporting additional ecosystems like ESP-IDF and ModusToolbox™. We have plans for other ecosystems in the future.

Golioth focuses on Zephyr training because we think it gives customers a great opportunity to build hardware that is tightly coupled to Cloud services that Golioth provides.

Zephyr is unique because it is both an ecosystem and a Real Time Operating System (RTOS). This is in contrast to something like FreeRTOS, which is an RTOS (obviously), but then vendors like Espressif or Infineon (and a range of others) maintain a vendor specific ecosystem on top of the core RTOS. The net result is that Zephyr has many vendors contributing, but ultimately the core members of the open source Zephyr project drive the direction of the ecosystem and the RTOS. I believe this is what is driving a lot of the popularity of Zephyr. As the popularity continues to increase, we see more people looking to learn about how to use Zephyr in their projects.

There is a steep learning curve when getting started with Zephyr. This, of course, depends on your experience. To hardware and firmware engineers who are used to bare metal programming, it might be difficult to learn about an RTOS for the first time. Understanding how KConfig, Devicetree, and the west build systems can work for your project is another layer to unfold. Device vendor specific implementations of Zephyr APIs and interacting with real hardware takes the problem off of the code editor and into the real world. And throughout the entire process, understanding error messages and building troubleshooting capabilities is a necessary skill-building exercise.

This is why we want to help train people around Zephyr. We know the power of the platform and we want to unblock developers from getting their device talking to the internet using the Golioth Zephyr SDK.

Things we learned

In preparation for our June training, we revamped multiple aspects of our Zephyr program. We also learned more about what works, and what doesn’t

New training site

Training.golioth.io is where we maintain all of our training documentation. Our training programs are focused around written material, instead of being dependent on presentations from the trainers. This unlocks users to move ahead during the training or go back and review a section that is difficult for them to understand.

We also broke up the training into some new sections. We have a new REST API training section, that is separate from our Zephyr training. We will hopefully have other types of training on there in the future. We broke out the “Intro to Golioth” into its own section, because this is the same regardless of which training you’ll be going through.

Having well written documentation continues to be a positive aspect to our training, though we found additional areas we can clarify.

A focus on binaries

In the Intro to Golioth section, we encourage users to download and flash a binary onto their hardware. This matches a recent experiment we did with pre-compiled binaries on the Thingy91, since we think this is the fastest way to try out the capabilities of Golioth. We want trainees to also be able to explore the Golioth Console, which is made easier by a binary that can be put onto the supported hardware during the training. An added benefit is that this tests that users are capable of programming their hardware with a known-working application, which means they’re ready for subsequent training sections.

Previously we had users compiling a program immediately after starting the training. The binary seems to be a better solution to accelerate users’ progress.

Switching to Nordic hardware

Another change for this training is focusing on development hardware from our partner Nordic Semiconductor. We wrote about our excitement around the nRF7002-DK for training, since it provides an affordable board with a built-in programmer. We also enabled a second piece of hardware, the nRF9160-DK. That board is cellular based, which gave users more flexibility to take the training in places where they didn’t have access to the Wi-Fi credentials; it also happens to be one of our best supported boards. Finally, this showcases how Golioth, Zephyr, and Nordic Semiconductor hardware enable cross platform solutions (even different connectivity types), with almost the same underlying code.

Another reason we were excited to switch to Nordic-based hardware is the cross platform support of the nRF Connect for Desktop tool suite. This gives a graphical programmer and reliable serial terminal, which we integrated into our training. Supporting engineers across the range of computing hardware (laptops, desktops) is a surprising challenge. Even though Windows, Mac, and Linux based options are the norm, there is a huge variability between different types of machines. This is also why we continue to be excited about Kasm.

Kasm Environments

Kasm is a streaming container platform that we have standardized on in our training. This allows us to create an entire desktop computing environment that gets streamed to a user’s browser. The best part is that as soon as the user logs in, they have access to all of the toolchains, IDEs, downloaded firmware projects, and any helper tools needed on the virtual remote machine. As the user goes through training and compiles binaries for themselves, they can download the binary to their host machine. That binary can then be programmed using the nRF Connect for Desktop programmer tool. The net result is that our users are ready to compile Zephyr programs within minutes of starting the training. See below for a look “inside” our training.

Where our training still falls short is a direct connection from the Kasm remote environment to the users’ development boards. This is a tough nut to crack.

You might be thinking, “Why not have the user install tools locally? Or have them install tools before they show up to training?”. Past training experiments where we asked users to install the Zephyr tooling onto their host machine took a significant amount of time, and sometimes didn’t work at all because users show up with a wide range of laptops to a training. Don’t believe us? At our in-person training at Supercon 2022, we had a user show up with a Valve Steamdeck handheld Linux gaming computer (his laptop had broken the night before). He was able to successfully use our Kasm container to compile and program the hardware!

For now, we will continue to experiment with connections to pre-configured containers, as we think this unified development experience is the best case scenario for training users.

Sign up for the next one

We continue to refine our capabilities on these training sessions and decided to hold another one on July 12th! We also changed our signup policy: if you meet the criteria, you will be automatically enrolled into training (see form for more details). We hope to see you there!

Fill out the signup form at https://forms.gle/3yk5WrWJ3Dunds9CA

Many Thanks

Though I had the opportunity to help administer this training, the majority of credit goes to my co-worker Mike Szczys. He wrote, tested, and built all of the firmware images and training materials and the training would not be possible without him.

Thanks also go to Golioth team members like Chris W, Dan, Dylan, Marko, and Sam for being on the training and walking users through Golioth.

It’s easier than ever to create a system that plays audio. Greeting cards do it, watches do it, just about any consumer system is capable of playing sound. Yes, the quality is variable, but that abides by the old maxim of, “you get what you pay for”.

So what happens if we have a very low cost piezoelectric element (shortened to piezo for the rest of this article), an NPN transistor, and a microcontroller with PWM capabilities…can we make that do anything fun from Zephyr? It turns out, yes, we can. Let’s take a look.

Simplicity

The hardware components on the schematic are pretty bare bones. The output pin from the microcontroller drives an NPN transistor, which then draws current down through a piezo. If we vary the on-off nature through the piezo, we’ll get a tone that matches the frequency of the AC waveform we are using. Using a frequency of 440 Hz should result in an A4 tone. In fact, it does!

Click to download Thingy91 schematic files from Nordic Semiconductor

The on-off nature of a PWM signal creates a square wave. This creates harmonics that produce a recognizable, if not harsh, tone. Songs with tonality like this are sometimes referred to as “chiptune“, because of its association with early video games and their limited capabilities to generate more complex waveforms. Personally, I have a lot of positive associations with this style of sound and music, since I grew up with early games that employed this type of music; it has also developed as a musical subgenre. But for today’s article, all that’s necessary to know is that we will be generating simple square wave tones, which produces a particular sound (examples in a video below).

Another important element is that there is only one generator of sound being used. This means the sound will be monophonic (“one voice”), as opposed to more complex polyphonic sounds, even within the chiptune genre. So like we said, this will be a simplistic output.

Zephyr challenges

Now that we know the hardware, how do we actually get the PWM generator inside a microcontroller to output the waveform we want?

For this exercise, we will be using the Nordic Semiconductor nRF9160, based on a dual core Cortex-M33 part with some standard PWM peripherals built in. However, because we are using Zephyr, the actual register map of the Nordic-specific PWM are not important. Instead, we will be interacting with the standard Zephyr PWM peripheral and APIs. This level of abstraction is implemented in the Zephyr ecosystem by Nordic Semiconductor and the community, and is also implemented by other chip vendors who participate. The net result is that code interacting with the Zephyr PWM element should work with any device that is supported in the Zephyr ecosystem.

We had trouble finding code that was already written for driving the piezo, so we decided to dig into the PWM peripherals. This is where things got confusing.

PWMs only for LEDs?

The first confusing fact is that all of the references to PWM in existing code seemed to apply only to LEDs. PWM is a great use case for LEDs and we implemented this in a thread on our Thingy91 code and pre-compiled binary to help show device status. But what about an alternative PWM device that’s not an LED?

The issue exists in the “compatible” keyword in the devicetree overlay. We only found pwm-leds as a compatible type. We spent a bunch of time chasing other types and seeing if we should use it, but at the end of the day we shrugged and decided our buzzer wouldn’t be bothered being called an ‘led’ in this way. Below is what our thingy91 overlay file looks like.

From our file thingy91_nrf9160.overlay

The pwms field is all we’re really going to be messing with in the program, so we set it to something that made sense for a getting started value and moved on.

From the included thingy91_nrf9160_common.dts files in the nRF Connect SDK

You’ll also note that it refers to &pwm1 in that line. It is calling out a PWM element in the hardware that is also called out in the thingy91_nrf9160_common.dts file as a placeholder to be used with the buzzer. This could be used for other features as well, but the Thingy91 has a limited set of peripherals and not many ways to break out signals, so it makes sense that it was “reserved” for the buzzer, despite not being plumbed in otherwise.

We call out the devicetree entry in the overlay file using the following line in app_work.c (view the entire thingy91_golioth project here)

const struct pwm_dt_spec sBuzzer = PWM_DT_SPEC_GET(DT_ALIAS(buzzer_pwm));

This gives us a struct to work with. When we want to turn the buzzer on, the call looks like

pwm_set_dt(&sBuzzer, PWM_HZ(1000), PWM_HZ(1000) / 2);

We’re passing in the handle sBuzzer and then telling it to play a tone at 1 kHz with a cycle time of 500 Hz–a 50% duty cycle. Since we’re creating a square wave, we can keep the duty cycle at 50% and then vary the frequency to change tones. For things like LED fading, we’d actually change the duty cycle but not the frequency (see other parts of the thingy91_golioth repo for example of LED fades).

Framework

So now we can play a 1 kHz tone, which works great for a buzzer. But what about when we want to easily add a bunch of different notes? To get recognizable notes into the code, we set up a struct that ties together a frequency and a duration:

struct note_duration
{
    int note; // hz
    int duration; // msec
};

We also put a bunch of lookup values into the app_work.h header file:

The note durations were hard-coded in here, but could also be set to be a multiplier of a song tempo, if you wanted a faster or slower song; each note is just a fraction of the overall tempo. The notes are simple frequency lookups. If you don’t have a music background, check out how each increasing octave of a particular note is double the frequency of the note an octave below (ie. A5 is 880 while A4 is 440). Music is math!

Songs

Now that we have a framework for creating songs, it’s all about translating sheet music into an array of notes/durations. This is what our version of “funkytown” looks like:

The REST note sets the frequency to 1 Hz. Since the small piezo is not capable of playing low frequency notes (think about how large most speaker woofers need to be for playing bass loudly), we treat anything less than 10 Hz as a “skip” or “rest”.

All of this is done in an RTOS thread dedicated to playing the song. We pass in which song we want to play, cycle through a for loop playing all the notes in the array and then the thread goes back to sleep. The nice thing is that the buzzer is set as an extremely low priority, so if anything else important is happening in the system, the RTOS scheduler will switch to that task. Even when it’s interrupted to go handle something like an incoming message on the cellular modem, it’s not a noticeable change in how the song sounds. See the app_work.c code to see how this thread operates.

See it in action

This standalone video shows the songs being triggered using the Golioth Remote Procedure Call (RPC) service.

Since we’re running the piezo music in a separate thread, we can receive the RPC, process which song we want to play, and then wake up the thread with the requested song that was passed over the internet.

What will you build?

It takes a lot of tech to play a song over a cellular network, but it helps showcase all the things you can do with Zephyr, Golioth, and Nordic Semiconductor hardware. Of course you can always modify our thingy91_golioth repo and play any other song you’d like…or you can expand on these capabilities and build a wide range of other IoT applications. Our Reference Designs give a good idea of real-world applications you can achieve with the Golioth platform. We’d love to hear about what you’re building on our Forum!

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!