Come learn Zephyr with Golioth on July 12th, 2023! You will need to order your own hardware, but there is no cost to attend the live training. Sign up now so that you know you have a seat and can order a dev board with plenty of time.

Golioth’s Zephyr Training in a Nutshell

Golioth is an instant IoT Cloud for microcontroller-level devices. We are hardware agnostic, and we use Zephyr, the fastest growing RTOS, because it supports a wide range of hardware from different vendors. We have a number of customers who ask where to find Zephyr training, so we developed our own boot camp to get you started.

Nordic nRF7002-DK board

Nordic’s new nRF7002 Development Kit

This three-hour training uses your choice of the Nordic nRF7002 DK (WiFi) or nRF9160 DK (Cellular). We begin by installing the Nordic tools on your local machine for loading new firmware on the boards. Everything else happens in a browser-based container. You’ll use VS Code and the Zephyr build environment, but it’s already set up for you to start working quickly.

Two sections are presented. We first load a pre-compiled binary on the board and test out the Golioth platform features. This ensures you are able to successfully program the board, and exposes you to the Zephyr networking stack, serial shell (used to assign credentials which are loaded into non-volatile storage), and logging system. The second portion of the training provides an overview of how the Zephyr development environment works before getting hands-on with Devicetree, Kconfig, pin mapping, timers, threads, and general RTOS knowledge.

You will come away with an understanding of how a Zephyr application is formatted, how the build system works, and what to expect when your application is running on your board.

Take a Peek, then Join Us Live!

Our training is no secret, the self-guiding documentation and the sample code are both available to peruse right now. However, you’ll find the interaction with other attendees and with the Golioth staff running the training fills in a lot of knowledge that’s not so easy to print on a webpage.

Sign up

Join us Live on July 12th! Note that we also changed our signup policy: if you meet the criteria, you will be automatically enrolled into training (see form for more details). We are limited to 60 trainees, but we plan to also hold a training in August. We hope to see you there!

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

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 is a platform that helps firmware and hardware engineers integrate useful cloud functions into their products. We make it really easy to get things connected and immediately start peering into the behavior of devices in a useful way.

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

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

Great idea, but why now?

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

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

Getting Started with the Thingy91 Binary

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

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

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

What’s in the binary?

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

Databases

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

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

Settings

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

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

Remote procedure calls (RPCs)

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

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

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

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

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

Logging

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

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

Over-The-Air (OTA) updates

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

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

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

Interacting with devices over the REST API

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

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

Try it, you’ll like it!

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

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

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.

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.

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.

Visual Studio Code, colloquially known as VScode, is among the most popular integrated development environments (IDEs). Today we’re going to walk through the process of setting up ESP-IDF in VScode and using it to run Golioth device management example code on an ESP32.

Not everyone likes to live their lives hammering away at a command prompt. What we’ll cover today is another option which uses Espressif’s VScode extension (plugin) to largely automate how you work with the Espressif IoT Development Framework (ESP-IDF). That means nice buttons and interfaces to build, flash, and monitor applications for the ESP32 family of chips.

Installing VScode and the ESP-IDF extension

As a prerequisite you will need to have VScode installed. If you don’t, head over to the download page and do so now.

ESP-IDF VScode extension

Open VScode and click on the extensions icon (looks like four boxes) on the left sidebar. Type esp-idf into the search bar that appears and the top result will be “Espressif IDF”. Click the install button and you’re off to the races.

Configure ESP-IDF VScode Extension

You will be greeted with options for installing the various ESP-IDF tools. If you don’t have an opinion on how things are installed you can choose the automatic route. I wanted to specify what directories were used during the install so I chose the manual route and used the settings above.

It will take a few minutes for everything to download. You will want to click on the “Download ESP-IDF Tools” to ensure that the compilers and other tools are downloaded (in addition to the Espressif SDK). If you need more help, check out Espressif’s installation guide.

Installing the Golioth Firmware SDK

Now that VScode and the ESP-IDF are installed, let’s take a moment to install the Golioth Firmware SDK. This provides the tools and sample code for connecting your ESP32 to Golioth.

Cloning the Golioth Firmware SDK

VScode has a handy tool for cloning git repositories. Bring up the command palette (ctrl-shift-p), type in gitcl, and press enter. A prompt will open in the same window for you to enter the following URL:

https://github.com/golioth/golioth-firmware-sdk.git

After pressing enter, a window will open where you can select a folder to store the repository. A folder called golioth-firmware-sdk will be placed in that location.

VScode will ask if you want to open the cloned repository. Please click Cancel on this window. The Golioth Firmware SDK supports multiple platforms and we will open the ESP-IDF specific directory in the next step.

Open the project and update the git submodules

Now that the repository has been cloned, let’s open the sample code in VScode. Click File→Open Folder and navigate to the golioth-firmware-sdk/examples/esp_idf/golioth_basicsfolder, then click Open.

The Golioth SDK includes a few packages as submodules and these must be updated before continuing. We’re going to use the terminal for this step. In VScode click Terminal→New Terminal. A terminal window will open in the golioth-basics folder. Type this command to update the submodules:

git submodule update --init --recursive

Type exit to close the terminal window.

Build, flash, and monitor the golioth-basics application

Now that everything is installed we get to see the ease of using an IDE.

ESP-IDF Build, Flash and Monitor

The bar along the bottom of the VScode window includes icons for working with the ESP-IDF tools. Make sure your ESP32 is plugged into USB. Click the flame-shaped icon which will build the project, flash it to the ESP32, and open a serial connection to the chip.

The build will take place and then VScode will open a window in the top center prompting you to select JTAG/UART/DFU. We will be using UART. Also note that there is a selection in the bottom menu bar where you can set the port that will be used when flashing (the image above shows /dev/ttyUSB1 in my case).

Assign device credentials in the monitor window

ESP32 running the golioth-basics app

The golioth-basics app will begin running immediately and you should see an output from the chip in a window inside VScode. We need to give the chip WiFi and Golioth credentials so that it can connect to the cloud.

If you have not yet signed into Golioth, our Dev Tier is free for your first 50 devices. (Tip: there is a Console Overview on our docs that will walk you through creating a set of device credentials.) Get your Golioth credentials, and the login info for your WiFi access point, and pass them to the chip using this command format:

settings set wifi/ssid YourWiFiAccessPointName
settings set wifi/psk YourWiFiPassword
settings set golioth/psk-id YourGoliothDevicePSK-ID
settings set golioth/psk YourGoliothDevicePSK

assign credentials to the device

Once you’ve set the credentials, type reset and the ESP32 will reboot, connect to WiFi and then to Golioth.

Successful connection to Golioth

Wrapping up

You’ve successfully compiled, flashed, and run a demo Golioth application for ESP-IDF using VScode. The same principles can be applied to your own projects.

If you’d like to dig deeper into how the golioth-basics code works, I encourage you to study the golioth_basics.c file in the golioth-firmware-sdk/examples/common folder. It demonstrates all of the Golioth device management features like OTA firmware updates, remote procedure calls (RPC), IoT fleet settings service, LightDB State and LightDB Stream data services, and remote logging.

We’d love to hear about the projects your working on. Share your successes and post your question on the Golioth Forums. If you’re interested in learning how to add Golioth to your IoT fleet, get in touch with our Developer Relations crew.

References:

 

How do you ensure you achieve the lowest possible power with your Cellular IoT Devices? We held a webinar on January 18th, 2023 where we tackled this question, alongside our friend Jared Wolff of CircuitDojo. In this video, we review how to measure and optimize your hardware, firmware, and software. The main focus of this video is the nRF9160 from Nordic Semiconductor, which runs NCS / Zephyr. Jared shared many of the tips and tricks he has learned while building the CircuitDojo Feather nRF9160.

A three pronged approach

There are three key areas to focus on when trying to optimize your design for low power.

  • Hardware – Jared talked about the importance of part choice, especially on the switching regulator. The nRF9160 has a low quiescient current if everything is configured properly.
  • Firmware – In order to take advantage of the low current draw of the part in question, we need to understand and enable features within the nRF9160. This would be true of any part we chose to use, the configuration is crucial. Understanding how the cellular modem is communicating with the tower is another important step, since each time you ping the tower it takes energy to check in.
  • Software – In this case, we mean Cloud software, such as the capabilities that Golioth provides to its users. The Golioth platform enables efficiencies like CoAP communication and CBOR encoding, lowering the size (and power draw) of each packet you decide to send to the Cloud. Things like the Golioth Setting service allows you to enable and disable different modes on your device to achieve fine-grained control of devices in your fleet.

Take your design (and battery life) further

Golioth is focused on building better cellular IoT deployments. We enable engineers to build customizable and reliable device fleets that monitor and impact the world around them. If you need any help optimizing your fleet, let us know on our forum or drop us a note.

I built the Internet of Memes, making it possible for anyone at my company to push funny pictures to the ePaper display on the desk of all our coworkers.

I work at Golioth, but I don’t really work at Golioth. We’re headquartered in San Francisco but we’re also a completely remote company spread across the US, Brazil, and Poland. I love working with insanely talented and interesting people, but we don’t get to see each other face to face in the office each day. So at risk of “all work and no play”, I took on an after-hours project to make sharing memes with each other a bit more IRL.

ePaper is Standard-Issue at this Company

MePaper powered by Golioth

As part of onboarding, each new employee is shipped is a small development board that includes an ePaper display, RGB LEDs, buttons, a speaker, accelerometer, and WiFi. It’s Adafruit’s MagTag and we use it for developer training so it makes sense to hand one out to everyone when they join up. But the boards sit unused the vast majority of the time. Why not use that messaging real estate for something fun?

U Got IoT! meme

The project comes in two parts: firmware for the ESP32-S2 that drives the MagTag and a Slack Bot that handles the image publishing. Let’s jump into the device-side first.

Espressif’s ESP-IDF, Golioth’s Device Management, and Google Drive

The device firmware got a bit of a head start because we have a MagTag-based demo that’s part of the Golioth Firmware SDK. The demo can already drive the hardware and use all Golioth features.

RAM considerations

The images for the display are 296×128 black and white so you need 4.7 kB of RAM to download an image. This was a challenge for the on-chip SRAM, but the ESP32-S2 WROVER module on these boards include a 2MB SPI PSRAM chip built into the module. I enabled this in Kconfig and used malloc() to place a framebuffer for the display in external ram.

#define FRAME_BUFFER_SIZE   5000

/* Get memory space for frame buffer */
fb = (uint8_t *) malloc(FRAME_BUFFER_SIZE);

The buffer is slightly oversized because it’s used to store an HTTP response that includes a bit of header data.

Leverage the ESP-IDF http_client

The next challenge was how to get the images onto the device. It is possible to repurpose Golioth’s OTA system to supply these images, but for reasons I’ll get to in the next section that’s is an inefficient approach. What I really want is a way to upload images to one single location and have many devices access them. That’s how webpages work, right?

I adapted the https_with_url() example from the ESP-IDF esp_http_client_example code. Just supply a URL to the image and the http(s) client will grab the page and dump it into the frame buffer.

void fetch_page(void) {
    ESP_LOGI(TAG, "MEME FILE");
    const char* url_string_p = mepaper_provisional_url;
    esp_http_client_config_t config = {
        .url = url_string_p,
        .event_handler = _http_event_handler,
        .crt_bundle_attach = esp_crt_bundle_attach,
        .user_data = (char*)fb,
        .timeout_ms = 5000,
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_err_t err = esp_http_client_perform(client);

    if (err == ESP_OK) {
        ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d",
                esp_http_client_get_status_code(client),
                esp_http_client_get_content_length(client));
                validate_and_display();
    } else {
        ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
        xEventGroupSetBits(_event_group, EVENT_DOWNLOAD_COMPLETE);
    }
    esp_http_client_cleanup(client);
}

A question of image formats

What kind of image should this be, anyway? My friend Larry Bank has written a bunch of image decoders for embedded systems which I considered using. But I ended up going with an easier solution. A format I had never heard of before but that’s perfectly suited for a 1-bit-per-pixel display like this one is called PBM or “Portable Bit Map”.

The PBM specification is for a 1-bit color depth image. The byte-ordering and endianness is exactly what is needed for this ePaper display, and the header is super simple to parse. When the file is downloaded to the PSRAM, the firmware searches for a header. Once found, the data after that header is piped directly to the display. Voila!

static const uint8_t HEADER_PATTERN[] = { 0x31, 0x32, 0x38, 0x20, 0x32, 0x39, 0x36, 0x0A };
static const uint8_t WHITESPACE[] = { 0x0A, 0x0B, 0x0D, 0x20 };

static bool is_whitespace(uint8_t test_char) {
    for (uint8_t i=0; i<WHITESPACE_SIZE; i++) {
        if (test_char == WHITESPACE[i]) {
            return true;
        }
    }
    return false;
}

static bool headers_match(uint8_t *slice) {
    // Spec is complicated by "whitespace" instead of a specific character
    for (uint8_t i=0; i<HEADER_LEN; i++) {
        if (i == WHITESPACE_IDX) {
            if (is_whitespace(slice[i]) == false) {
                return false; //this char must be whitespace
            }
        }
        else if (HEADER_PATTERN[i] != slice[i]) {
            return false; //members don't match
        }
    }
    return true;
}

Images are created in your favorite editing software (I use GIMP). Set the mode to 1-bit depth size it to 296×128 (then rotate counterclockwise by 90 degrees) and export it to as PBM.

A Slack Bot Made of Python

With the device side proved out, I turned my mind to the “distribution” problem. How do I let different users control the fleet? Here is a list of consideration on my mind:

  • How can users register their own devices for this service?
  • How can users push their own images to the entire fleet?
  • How will firmware upgrades be managed?

As a fully-remote company, we’re on Slack all day long. A Slackbot (a daemon that sits in the chat listening for commands) makes the most sense for granting some fleet control to everyone in the company.

How should the devices be provisioned so that they are all accessible by the bot? This topic could itself be a whole post. I decided the best way was to let each person add their MagTag to a Golioth project they control, assigning it the mepaper tag (“meme” + “epaper” = mepaper). They then share an API key to that project to receive notifications when there is a new image available for the screen. OTA firmware updates can also be staged for registered devices.

Starting from a stock MagTag, you download the latest firmware release and flash it to the board using esptool.py (the programming tool from Espressif). When the board boots, a serial shell is available via USB where WiFi credentials and Golioth device credentials can be assigned by the user. That’s it for setup, the bot takes care of the rest.

Mepaper Slack bot publish command

Each device is observing a mepaper endpoint on the Golioth LightDB State. It’s a URL to the image to display, and whenever it changes the boards get notified, then uses the https client to download them. The bot listens for a publish <url_of_image> message on the Slack channel, validates the file as existing and being in the proper format. Then the bot iterates through each API key it has stored, updating the LightDB State endpoint for each board tagged as mepaper.

This was chosen instead of pushing OTA artifacts for each image. That’s because the image would need to be uploaded multiple times (one for each project API-key registered) and a copy of all past memes would remain as artifacts. By uploading the images to our Google Drive, they are only stored one time, and all devices can access them as long as they have a copy of the share link.

Mepaper Slack bot commands

Of course, as a quick and dirty hack, the original version of the firmware wasn’t the greatest. I made sure to use the OTA firmware upgrade built into our sample code. As with the image updates, users who the bot recognizes as admins can issue /register ota and /register rollout commands. These accept parameters like version, package name, and the filename of the new firmware. For security, the bot will only upload firmware that is located in the same directory as the bot itself.

Oh the Fun of a Side-Channel for Memes!

Having a dedicated, non-intrusive screen for Memes turns out to be an excellent boost to the remote workplace vibe. Your computer doesn’t need to be unlocked, and you don’t need to switch context to see the newest message. The reason for pushing a meme doesn’t have to be meaningful, it is purely a mechanism for spreading joy. And it worked!

We’ve previously written about the Demo Culture at Golioth. The “Demo early, demo often” mantra is an effort to break out of a rut where you only communicate with your coworkers for Official Business™. Sending memes is one more way to be silly, and enjoy each others’ company, even if we can’t be in the same room. It’s also a neat way to eat our own dogfood.

Future Improvements

There’s lots of room to improve this project, but the first area I’ve been actively working on is the fleet organization itself. When starting out I wanted each user to be able to provision their own device so I chose to have users add the device to their project and share an API key with the bot using a private command on Slack. This an interesting application for controlling devices across multiple projects, but it’s overly complex for this particular application.

Bot-created, credentials, all on the same project

Currently, users need to generate credentials, assign them to the device via serial, and share an API key using the Slack bot. A better arrangement would have been to use the Slackbot to acquire the credentials in the first place. A future improvement will change the /register command to generate new PSK-ID/PSK credentials and send them to the user privately in Slack. This way all devices can be registered on one project, even though the user doesn’t have access to that project’s Golioth Console.

OTA is crucial to fleet management

With the fleet already deployed, how do you make these changes? Via OTA firmware update, of course. I have already implemented a key rotation feature using Golioth’s Remote Procedure Call. The bot can send each device a new PSK-ID/PSK, which will be tested and store, move each member of the fleet over to one singular project.