Golioth is all about sharing ideas and knowledge. This now includes our hardware, which today has been released as open source under the CERN OHL-P license. The Aludel Elixir and Ostentus are available on GitHub as KiCad projects and you can use them to start building your own designs.

The Aludel Platform

As an IoT platform that doesn’t build or sell commercial hardware, we still need to find ways to highlight the capabilities our platform offers. This often includes building on top of development boards from our various partners. Engineers are used to starting from well known vendor platforms, like some of our favorites which are in the list of Continuously Verified Boards (CVBs) on the Golioth platform:

When you’re ready to start building devices that look more like real-world products, however, it becomes a bit bulky to use the easy-to-access development platforms. We knew we wanted to capture this “magic in a box”, which was the genesis of the name — an “aludel” was a sublimating pot in alchemy.

We started using alternative boards like the nRF9160 Feather from Circuit Dojo and encapsulating the Feather form factor in a box. The early Aludel boxes were much larger and included headers to plug in different boards. Over time, we knew we wanted to more closely approximate designs that will go out into the field, so we downsized to the current ‘Mini’ version that utilizes the Bud Industries CU-1937-MB Utilibox and then we started modifying it and adding 3D printing elements. That formed the basis of the new Aludel platform, which includes two boards we’re releasing today.

Elixir


The Elixir Hardware Repository is now open source on GitHub

The Elixir board is a natural offshoot of the Aludel Mini form factor, including the new case we adopted. We had first created a board called the aludel-mini which you’ll see mentioned in some of our reference design repositories. This was an interposer board between a soldered down nRF9160 Feather and some mikroBUS Click headers. However, we started to experience some limitations with how the board operated and wanting to add other features. This is when we planned to build the Elixir.

The Elixir has many of the same elements from the OSHW Feather board, and adds on new capabilities that are common to many of our designs:

  • nRF9160 – Cat M1 / NB-IOT / GPS chip with dual core Cortex M33 processing
  • ESP32-C3 – Running ESP-AT firmware, interfaced to the nRF9160 over UART
  • BME280 – Weather sensor from Bosch, common to many Golioth designs
  • Power
    • Low quiescent buck circuit, inherited from Feather Board
    • 5V boost to service the 5V output on the mikroBUS
    • Battery charging
    • Fuel gauge
    • High voltage input (5.5V to 48V) for powering from things like OBD-II and industrial supplies
  • LIS2DH – Accelerometer, inherited from Feather board
  • PWM Buzzer – Allow audible indication similar to the Thingy91
  • Real Time Clock
  • Secure Element (ATECC608B)
  • Power Switching – Power switch of 3V3 for sensor nodes to shut down in low power situations
  • QWIIC / STEMMA Headers – External sensors and peripherals including the Aludel Ostentus
  • mikroBUS headers
  • USB C interface and power
  • External SIM connector
  • Multiple programming interfaces
    • 10 pin SWD (02×05 .127mm pitch), accessible from outside the case
    • 10 pin SWD Tag-Connect (accessible from the top side of the board)
    • USB using the serial bootloader / MCUBoot

Ostentus


The Ostentus Hardware Repository is now open source on GitHub

Early Reference Designs were simply aludel-mini boards in the Utilicase. We utilized vinyl stickers to differentiate between designs, but this didn’t have the impact we were looking for when we went to tradeshows or shared photos on our projects site. What’s more, it didn’t represent a product that might be on-site at an industrial facility (more on the ‘field readiness’ in a bit).

If you walk up to a sensor monitor at a plant, you likely don’t want to pull out a phone and require a network in order to interact with the device in front of you. Having a simple, power efficient, persistent display that is visible in high brightness environments felt really useful. In fact, our early design hypothesis is that you’d still want to have readings on the screen, which is why the Ostentus continues displaying data even when the QWIIC header has been powered down from the main board (in low power modes). That trick is handles using ePaper displays.

Over different iterations we added other capabilities to allow user interactivity. Capacitive touch buttons around the bezel of the screen and an accelerometer for sensing double tap actions through the case deliver user input in the field.

  • Raspberry Pi Pico – Originally chosen during the part shortages of 2022-23, the RP2040 on the Pico provides a low cost solution including a programming interface over the USB port + MicroPython ease-of-use
  • ePaper interface – This design was derived from the Pimoroni Badger 2040 and that had a reference circuit for interfacing to a Good Display 1.54in square ePaper display (not cheap!)
  • Accelerometer (LIS2DH12) — Currently unimplemented but targeting “double tap” behaviors in the future
  • 3 button capacitive touch controller (CAP-1203-1)
  • QWIIC header for incoming power and i2c
  • Downward firing LEDs

Two critical pieces of firmware power this part of the platform. We utilized i2c in order to cut down on the number of I/O required on the main board (in this case the Elixir) and also to abstract it for future boards that might differ from the Elixir:

License and certification

These designs are being released under the CERN Open Hardware License – Permissive, version 2. This is one of the most permissive hardware licenses available. This board has not yet been certified by OSHWA, but we believe the hardware and files comply with the OHSWA definition. No FCC, CE, or cellular carrier certification is guaranteed with these boards and users should expect to take any resulting product through the proper regulatory channels.

Field Readiness

One thing to note is that the Ostentus plus the cut-out version of the Bud Industries case results in an unrealistic deployment option for the platform. I would never feel comfortable sending these boards out into a harsh environment, let alone a slightly damp one, due to the fact that there are gaps in the case and there is no IP rating. However, we didn’t design it for high reliability or even production: the Aludel platform is truly to showcase the widest variety of use cases and maximum flexibility.

This is why you’ll often hear us discussing how this is an “80% done design”. We expect our users to take these designs and shrink them, harden them for environments, and extend them for specific use cases. This could also include doing rounds of Design for Manufacturing (DfM), doing cost optimization, and improving testing for high volume production. Also important are taking the designs through FCC Part 15B certification and any required cellular carrier certification (which depends on which carrier you use and their requirements).

All of those things are left to you, dear reader. We followed the best design practices that we know, though we are always open to learning more and receiving feedback on our forum and issues filed on our GitHub repositories. You can start to evaluate whether this custom hardware is the right path by starting from our range of Follow-Along Hardware and open source firmware on the Golioth Reference Design Project site.

IoT is all about data. How you choose to handle sending that data over the network can have a large impact on your bandwidth and power budgets. Golioth includes the ability to batch upload streaming data, which is great for cached readings that allows your device to stay in low power mode for more of the time. Today I’ll detail how to send IoT data in batches.

What is Batch Data?

Batch data simply means one payload that encompasses multiple sensors readings.

[
    {
        "ts": 1719592181,
        "counter": 330
    },
    {
        "ts": 1719592186,
        "counter": 331
    },
    {
        "ts": 1719592191,
        "counter": 332
    }
]

The example above shows three readings, each passing a counter value the represents a sensor reading, along with a timestamp for when that reading was taken.

Sending Batch Data from an IoT Device

The sample firmware can be found at the end of the post, but generally speaking, the device doesn’t need to do anything different to send batch data. The key is to format the data as a list of readings, whether you’re sending JSON or CBOR.

int err = golioth_stream_set_async(client,
                                   "",
                                   GOLIOTH_CONTENT_TYPE_JSON,
                                   buf,
                                   strlen(buf),
                                   async_push_handler,
                                   NULL);

We call the Stream data API above. The client and data type are passed as the first two arguments, then the buffer holding the data and the buffer length are supplied. The last two parameters are a callback function and an optional user data pointer.

Routing Batch Data using a Golioth Pipeline

Batch data will be automatically sorted out by the Golioth servers based on the pipeline you use.

filter:
  path: "*"
  content_type: application/json
steps:
  - name: step0
    destination:
      type: batch
      version: v1
  - name: step1
    destination:
      type: lightdb-stream
      version: v1

This example pipeline listens for JSON data coming in on any stream path. In step0 it “unpacks” the batch data into individual readings. In step1 the individual readings are routed to Golioth’s LightDB stream. Here’s what that looks like:

Note that all three readings are coming in with the same server-side timestamp. The device timestamp is preserved in the data, but you can also use Pipelines to tell Golioth to use the embedded timestamps.

Batch Data with Timestamp Extract

For this example we’re using a very similar pipeline, with one additional transformer to extract the timestamp from the readings and use it as the LightDB Stream timestamp.

filter:
  path: "*"
  content_type: application/json
steps:
  - name: step0
    destination:
      type: batch
      version: v1
  - name: step1
    transformer:
      type: extract-timestamp
      version: v1
    destination:
      type: lightdb-stream
      version: v1

Note that we didn’t even need an additional step, but simply added the transformer to the step that already set lightdb-stream as the destination.

You can see that the Linux epoch formatted timestamp has been popped out of the data and assigned to the LightDB timestamp. Extracting the timestamp is not unique to Golioth’s LightDB Stream service.

Streaming data may be routed anywhere you want it. For instance, if you wanted to send your data to a webhook, just use the webhook destination. If you included the extract-timestamp transformer, you data will arrive at the webhook with the timestamps from your device as part of the metadata instead of nested in the JSON.object.

Using a Special Path for Batch Data

What happens if your app wants to send other types of streaming data beyond batch data? The batch destination will automatically drop data that isn’t a list of data objects. But you might like to be more explicit about where you send data and for that you can easily create a path to receive batch data.

filter:
  path: "/batch/"
  content_type: application/json
steps:
  - name: step0
    destination:
      type: batch
      version: v1
  - name: step1
    transformer:
      type: extract-timestamp
      version: v1
    destination:
      type: lightdb-stream
      version: v1

This pipeline is nearly the same as before with the only change on line 2 where the * wildcard was removed from path and replaced with "/batch/". Now we can update the API call in the device firmware to target that path:

int err = golioth_stream_set_async(client,
                                   "batch",
                                   GOLIOTH_CONTENT_TYPE_JSON,
                                   buf,
                                   strlen(buf),
                                   async_push_handler,
                                   NULL);

Although the result hasn’t changed, this does make the intent of the firmware more clear, and it differentiates the intent of this pipeline from others.

Sample Firmware

This is a quick sample firmware I made to use while writing this post. It targets the nrf9160dk. One major caveat is that the function that pulls time from the cellular network is quite rudimentary and should be replaced on anything that you plan to use in production.

To try it out, start from the Golioth Hello sample and replace the main.c file. This post was written using v0.14.0 of the Golioth Firmware SDK.

Wrapping Up

Batch data upload is a common request in the IoT realm. Golioth has not only the ability to sort out your batch data uploads, but to route them where you want and even to transform that data as needed. If you want to know more about what Pipelines brings to the party, check out the Pipelines announcement post.

Embedded systems, like any software system, benefits from modularizing software components, especially as they approach production. In this talk at the Embedded Open Source Summit 2024, Golioth Firmware Lead Sam Friedman talks about how to create “microservices” for microcontrollers. He maps a popular web concept onto existing software characteristics in Zephyr and shows how a real-world example can benefit from truly modular software.

Mapping a web concept to the microcontroller realm

As Sam points out early in this talk, it’s not really about microservices, because that’s a web concept. A microservice on the web is a piece of software, normally deployed onto cloud infrastructure, that can stand alone. It has defined inputs and outputs (APIs) and can operate independent of any other microservice. This helps for scalability and testing, but is a general trend in web software and deploying applications.

Microcontrollers are smaller and traditionally operate more like a “monolith” (another web term) because everything is interconnected. But there are concepts like Inter-Process Communication (IPC), which allows constrained devices to have similar ideas. IPC is a computer science idea that helps to optimize communication inside of operating systems. As it so happens, Zephyr is a (real time) operating system. Let’s look at what these are in practice.

How firmware developers can benefit

Sam describes how the concepts of Tasks, IPC, and Event Tasks are defined and might be used. But it is the Zephyr analogs that highlights familiar features, like the relatively new ZBus methodology. If a user adds a listener on the ZBus, they can listen (subscribe) for a particular value (topic) on the bus and take action based off of it. This helps to make the overall system more modular, because the addition or removal of a feature is not deeply integrated between elements of the system. Instead, the new piece of code is reacting to data put on the bus, which reduces interdependency and improves test areas.

Real-World Example

Sam drives home his point by talking about a Golioth Reference Design like the Cold Chain Asset Tracker and how we can add capabilities like an onboard alarm when we hit a temperature threshold. Previously, this would have required refactoring to also send data from the sensor process to a new module that containes the alarm code. But with something like ZBus, the alarm can simply listen for a topic on ZBus and when the temperature module publishes to that topic, all relevant parties are updated.

This works in the opposite direction as well. Code written with this in mind would not break any future builds if a hardware cost-down removed an element like a front panel display. Instead, the user chooses not to build in that portion of the code (memory savings, yay!) and other parts of the code are not negatively impacted.

Bringing together the Cloud and Embedded Developers

Sam’s talk showcases what Golioth does well: match up the capabilities of the Cloud with the capabilities of an embedded system. Often many of the key ideas from computer science are more onerous to implement on a constrained system like a microcontroller, but Zephyr’s growing software toolbox makes it easier than ever to build a modular, testable system. Check out Sam’s talk above and his slides below for more context into how to build such a system.

 

We got an early look at Nordic’s new cellular modem, the nRF9151, and it already works with Golioth!

With any new board, we ask ourselves “can we connect it to Golioth?”. You may remember a similar post when the nRF7002-DK first came out. Of course the answer for these two boards, and pretty much all other network-enabled embedded systems, is: yes, you can use them with Golioth. So today we’ll walk though the experience of connecting the nRF9151 to Golioth for the first time.

What’s new with the nRF9151?

We love the nRF9160 cellular modem and have support for it in all of the Golioth Firmware SDK samples, as well as using it in the Hardware-in-the-Loop (HIL) testing that is connected to our continuous integration infrastructure. So what’s the deal with the new part?

Finger pointing at a small rectangular chip (SOC)Most obviously, it’s really really small. The 9151 is about a 20% size reduction from the 9160 (new dimensions are approximately 11×12 mm). Here you can see it’s smaller than the fingernail on my pointer finger. The smaller sized also delivers lower peak current consumption. As with the recently announced nRF9161, the nRF9151 supports DECT NR+. And Nordic indicates the new design is fully compatible with the existing nRF91 family of chips.

This is also the first Nordic dev board I’ve seen that uses a USB-C connector. While you can’t get your hands on one of these just yet, since Golioth is partners with Nordic they were kind enough to send us one of these nRF9151 Development Kits to take for a test drive.

Building Golioth examples with the nRF9151

This board is not yet available to order, but support has already been added to Zephyr. To get it working with Golioth, we needed a fix that Nordic merged after their v2.6.1 release of the nRF Connect SDK (NCS). So today I’ll be checking out a commit in between releases. When Nordic releases v2.7.0 everything will work without this extra step.

0. Install the Golioth Firmware SDK

You will need an NCS build environment along with the Golioth SDK. You can follow the Golioth Docs to install an NCS workspace, or add Golioth to your existing NCS workspace.

1. Update NCS version (if needed)

If you are using NCS v2.7.0 (not yet released at the time of writing) or newer, you can skip this step. Otherwise, edit your west manifest and update the NCS version. Below is the west-nrf.yml file from the Golioth SDK with the changed line highlighted.

manifest:
  projects:
    - name: nrf
      revision: 85097eb933d93374fe270ce4c004bea10ee80e97
      url: http://github.com/nrfconnect/sdk-nrf
      import: true

  self:
    path: modules/lib/golioth-firmware-sdk

This happened to be the commit at the tip of main when writing this post. We usually recommend against targeting commits in between releases, so consider this experimental.

2. Add a board Kconfig file for the nRF9151

Add the board-specific configuration to the boards’ directory. For today’s post, I’m building the Golioth stream sample so I’ve added this nrf9151dk_nrf9151_ns.conf board file to that sample directory.

# General config
CONFIG_HEAP_MEM_POOL_SIZE=4096
CONFIG_NEWLIB_LIBC=y

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

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

# Modem library
CONFIG_NRF_MODEM_LIB=y
CONFIG_NRF_MODEM_LIB_ON_FAULT_APPLICATION_SPECIFIC=y

# LTE connectivity with network connection manager
CONFIG_NRF_MODEM_LIB_NET_IF=y
CONFIG_NRF_MODEM_LIB_NET_IF_AUTO_START=y
CONFIG_NRF_MODEM_LIB_NET_IF_AUTO_CONNECT=y
CONFIG_NRF_MODEM_LIB_NET_IF_AUTO_DOWN=y

CONFIG_NET_CONNECTION_MANAGER=y
CONFIG_NET_CONNECTION_MANAGER_MONITOR_STACK_SIZE=1024

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

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

# Generate MCUboot compatible images
CONFIG_BOOTLOADER_MCUBOOT=y

3. Build the Golioth stream sample

Building and running this sample is now quite simple. I have included the option to use runtime credentials in this build so that we can provision the device from the Zephyr shell.

$ cd examples/zephyr/stream
$ west build -b nrf9151dk/nrf9151/ns -- -DEXTRA_CONF_FILE=../common/runtime_settings.conf
$ west flash

4. Provision and run the sample

Golioth is free for individual use so sign up for an account if you have not already done so. After creating a project and device we can provision the PSK-ID/PSK by opening a serial connection to the device.

uart:~$ settings set golioth/psk-id <your-psk-id>
uart:~$ settings set golioth/psk <your-psk>

Here’s the terminal output during my tests:

*** Booting nRF Connect SDK v2.6.99-85097eb933d9 ***
*** Using Zephyr OS v3.6.99-18285a0ea4b9 ***
[00:00:00.538,452] <inf> fs_nvs: 2 Sectors of 4096 bytes
[00:00:00.538,482] <inf> fs_nvs: alloc wra: 0, fb8
[00:00:00.538,482] <inf> fs_nvs: data wra: 0, 68
[00:00:00.538,879] <dbg> golioth_stream: main: Start Golioth stream sample
[00:00:00.539,001] <inf> golioth_samples: Bringing up network interface
[00:00:00.539,001] <inf> golioth_samples: Waiting to obtain IP address
[00:00:01.691,894] <inf> lte_monitor: Network: Searching
uart:~$ settings set golioth/psk-id 20240603190757-nrf9151dk@nrf9151-demo
Setting golioth/psk-id to 20240603190757-nrf9151dk@nrf9151-demo
Setting golioth/psk-id saved as 20240603190757-nrf9151dk@nrf9151-demo
uart:~$ settings set golioth/psk e487ea809e5fa705c2af4050150f822c
Setting golioth/psk to e487ea809e5fa705c2af4050150f822c
Setting golioth/psk saved as e487ea809e5fa705c2af4050150f822c
[00:01:10.748,168] <inf> lte_monitor: Network: Registered (roaming)
[00:01:10.748,901] <inf> golioth_mbox: Mbox created, bufsize: 1232, num_items: 10, item_size: 112
[00:01:12.994,964] <inf> golioth_coap_client_zephyr: Golioth CoAP client connected
[00:01:12.995,025] <inf> golioth_stream: Sending temperature 20.000000 (sync)
[00:01:12.995,269] <inf> golioth_stream: Golioth client connected
[00:01:12.995,269] <inf> golioth_coap_client_zephyr: Entering CoAP I/O loop
[00:01:13.543,975] <dbg> golioth_stream: temperature_push_cbor: Temperature successfully pushed
[00:01:18.544,067] <inf> golioth_stream: Sending temperature 20.500000 (async)
[00:01:20.953,582] <wrn> golioth_coap_client: Resending request 0x2001e2c0 (reply 0x2001e308) (retries 2)
[00:01:23.544,311] <inf> golioth_stream: Sending temperature 21.000000 (sync)
[00:01:25.544,677] <wrn> golioth_stream: Failed to push temperature: 9
[00:01:25.772,186] <wrn> golioth_coap_client: Resending request 0x2001e2c0 (reply 0x2001e308) (retries 1)
[00:01:25.947,631] <wrn> golioth_coap_client: Resending request 0x2001e440 (reply 0x2001e488) (retries 2)
[00:01:30.544,738] <inf> golioth_stream: Sending temperature 21.500000 (async)
[00:01:30.581,359] <dbg> golioth_stream: temperature_async_push_handler: Temperature successfully pushed
[00:01:30.949,401] <dbg> golioth_stream: temperature_async_push_handler: Temperature successfully pushed
[00:01:35.544,952] <inf> golioth_stream: Sending temperature 22.000000 (sync)
[00:01:36.326,812] <dbg> golioth_stream: temperature_push_cbor: Temperature successfully pushed
[00:01:41.326,873] <inf> golioth_stream: Sending temperature 22.500000 (async)
[00:01:42.582,946] <dbg> golioth_stream: temperature_async_push_handler: Temperature successfully pushed
[00:01:46.327,117] <inf> golioth_stream: Sending temperature 23.000000 (sync)
[00:01:46.947,204] <dbg> golioth_stream: temperature_push_cbor: Temperature successfully pushed
[00:01:51.947,296] <inf> golioth_stream: Sending temperature 23.500000 (async)
[00:01:52.718,261] <dbg> golioth_stream: temperature_async_push_handler: Temperature successfully pushed
[00:01:56.947,540] <inf> golioth_stream: Sending temperature 24.000000 (sync)
[00:01:57.663,665] <dbg> golioth_stream: temperature_push_cbor: Temperature successfully pushed
[00:02:02.663,726] <inf> golioth_stream: Sending temperature 24.500000 (async)
[00:02:03.725,708] <dbg> golioth_stream: temperature_async_push_handler: Temperature successfully pushed
[00:02:07.663,970] <inf> golioth_stream: Sending temperature 25.000000 (sync)
[00:02:08.589,111] <dbg> golioth_stream: temperature_push_cbor: Temperature successfully pushed
[00:02:13.589,172] <inf> golioth_stream: Sending temperature 25.500000 (async)

5. View the data sent from the device

In the Golioth web console I can navigate to the LightDB Stream tab for the device and see the data as it arrives on the cloud. Try out Pipelines to transform and send that data to a destination.

A table of temperature data displayed on the Golioth web console

What will you do with the nRF9151?

We see a lot of IoT deployments using the nRF9160 to provide a cellular connection. They’re versatile parts with plenty of peripherals. The new nRF9151 part number is nice for your board footprint, and your power budget. And of course, every fleet needs management and data handling. Golioth already works with this SoC and so many more!

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

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

Controlling from afar

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

How it works

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

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

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

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

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

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

Other applications

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

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

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

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

Jerónimo Agulló is an open-source projects enthusiast and a Zephyr RTOS contributor. He has worked on different IoT projects across different industries, from research centers such as the University of Sevilla, to large companies like Ferrovial Construction. Check out his work on his GitHub profile.

Location is a basic part of the most interesting IoT systems. It’s not just about loss prevention; geolocation opens the door to several applications and enhances the management of devices. In many cases, this involves Global Navigation Satellite System (GNSS).

Position is so important in IoT that the latest Zephyr release (v3.6.0) has added a new API for GNSS based on the NMEA0183 standard. Today I’m going to explain the basics of GNSS, what NMEA means, and dive into this awesome new Zephyr feature.

The basics of the NMEA standard

NMEA is the acronym for the National Marine Electronics Association, an organization created before GPS was invented. Its aim is creating better communications with manufacturers. I can assure you that they achieved their goal with GNSS.

Their most extended and well-known norm is the NMEA0183 protocol, which has become the GNSS standard for almost all manufacturers. It facilitates module integration and new applications development.

NMEA data is transmitted from a source such as a GPS module (known as a “Talker”) to equipment, such as our running Zephyr device (known as a “Listener”). One important aspect is that a single talker can communicate to many listeners.

From the electronic perspective, NMEA0183 originally used RS-422, but now uses several interfaces such as UART, USB, RS-232, WIFI, Bluetooth, and more. We can consider NMEA protocol as a common message structure standard. NMEA0183 protocol makes software developer life easier due to the standardization between GNSS devices.

NMEA message structure

Each NMEA sentence contains only ASCII characters, starting with the dollar symbol “$” and ending with the <CR> (Carriage return) and <LF> (Line feed) characters. The content of the message is a tuple, separated by commas. This starts with an NMEA identifier, followed by data, and ending by a checksum preceded by an asterisk “*”.

For a better understanding, let’s examine a popular NMEA sentence from a Quectel module. “GGA” sentence contains the Global Positioning System fix data, time, position, and fix related data for a GNSS receiver.

$GPGGA,102744.00,6155.393269,N,00848.433734,E,1,03,1.6,821.5,M,52.0,M,,*70
  • $GPGGA is the sentence identifier which can be split into “GP” which means the GNNS type, in this case GPS, and “GGA” which is the sentence identifier.
  • 102744.00 is the time of fix (hhmmss).
  • 6155.393269,N is the latitude (ddmm.mmm format, N for North).
  • 00848.433734,E is the longitude (dddmm.mmm format, E for East).
  • 1 is the fix quality indicator.
  • 03 is the number of satellites being tracked.
  • 1.6 is the horizontal dilution of precision.
  • 821.5,M is the altitude in meters above mean sea level.
  • 52.0,M is the height in meters of geoid separation.
  • *70 is the checksum (Note: this is not the correct checksum for this payload, we moved the location manually)

 

All talker devices don’t rely on GPS or on different constellations simultaneously. The most common constellations are GLONASS (GL), BEIDOU(DB or GB) and GALILEO (GA).

Besides GGA, other popular NMEA sentences are:

  • RMC: Recommended Minimum Navigation Information
    • This contains information similar to GGA such as the latitude and longitude and also the speed over ground (in knots), the date and the magnetic variation (in degrees).
  • GSV: Satellites in View
    • This NMEA message prints information such as the total number of satellites in view for each constellation and for each satellite its number (PRN), elevation in degrees, azimuth in degrees and Signal to Noise Ratio (SNR) in dB. Each GSV sentence contains the information of more than one satellite.

The previous NMEA sentences are named as “talker sentences”. In addition to that, there are “Proprietary sentences” and “query sentences”. On the one hand, the proprietary sentences start with “$P” and allow manufacturers to define custom NMEA sentences format for custom functions such as power management. On the other hand, the query sentences are the means for listener to request a particular sentence from a talker

Zephyr GNSS API

If you’ve reached this point, it means that you already know and like Zephyr RTOS. If you’re not familiar with Zephyr, I encourage you to familiarize yourself with it by using this getting started guide.

Zephyr RTOS is not just a simple scheduler for simple applications. Zephyr is a whole operating system with drivers, services and common APIs which facilitates the new developments and sensors and boards exchange. I would go beyond and say that Zephyr is even a complete ecosystem with integration in awesome clouds such as Golioth.

A great example of this affirmation is the new Zephyr GNSS support in version v3.6.0. The GNSS API is built upon the modem subsystem, which provides the necessary modules to communicate with modems. The GNSS subsystem covers everything from sending and receiving commands to and from the modem, to parsing, creating and processing NMEA0183 messages.

The source code can be found under the path zephyr/drivers/gnss, which is divided into specific GNSS module drivers with custom features such as power management, some files with utils and a generic NMEA0183 driver. Covering in detail each of those utils would require a whole new post. However, I will provide a brief overview of each of them:

  • gnss_dump
    • A set of utilities to get, convert and print GNSS information into readable and useful data.
  • gnss_nmea0183 and gnss_parse
    • NMEA0183 utilities such as checksum calculation, parsing a ddmm.mmmm formatted coordinates to nano degrees or parsing the content of GGA, RMC and GSV NMEA messages.
  • gnss_nmea0183_match
    • This code is based on “modem_chat” match handlers, a Zephyr utility to process messages from modems. The callbacks to parse GGA, RMC and GSV messages are defined here.

Zephyr has created a generic NMEA driver which can be used for any NMEA talker. As any other driver, it is instantiated by the DEVICE_DT_INST_DEFINE macro. It includes the following code to call the callbacks defined in the gnss_nmea0183_match.c for corresponding NMEA identifier:

MODEM_CHAT_MATCHES_DEFINE(unsol_matches,
MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
#if CONFIG_GNSS_SATELLITES
MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback),
#endif
);

GNSS example in Zephyr

Now you know about NMEA0183 and the Zephyr GNSS API, so get your hands dirty and write some code! The full code can be found in my github repository:

https://github.com/jeronimoagullo/Zephyr-GNSS-Sample

KConfig Changes

In this example, I will show you how to configure a microcontroller in Zephyr to use a GNSS module which supports NMEA0183. For this aim, I will use an ESP32S3-Mini-1 development board along with an Air530z GNSS module. We’re going to modify our prj.conf file

  1. Add the CONFIG_GNSS=y kconfig variable to add the GNSS support.
  2. Add the variables CONFIG_GNSS_SATELLITES=y to display satellites’ information
  3. Add CONFIG_GNSS_DUMP_TO_LOG=y to print GNSS information like the fixation status, coordinates and time.

Devicetree Changes

Next, we’ll configure the device tree correctly for our GNSS module. In this case, the Air530z GNSS module uses an UART interface. By default, ESP32S3 has the UART0 configured in pins 43 and 44. However, this UART is configured at 115200 bps and used for both programming the board and by Zephyr to print messages into the serial terminal. This is not a good idea.

What is the solution? The use of another UART, for example, the UART1. However, it is not defined in esp32s3_devkitm.dts. So let ‘s do it!

According to GPIO, the UART1 uses the pins 17 and 18 (you can use others too). I have chosen these default pins and defined them in the device tree overlay using the pin control subsystem. The macro of the pins can be found in the file zephyr/include/zephyr/dt-bidings/pinctrl/esp32s3-pinctrl.h you can find your board pins in the corresponding header file of your board.

 

&pinctrl {

  uart1_pins: uart1_pins {
    group1 {
      pinmux = <UART1_TX_GPIO17>;
      output-high;
    };

    group2 {
      pinmux = <UART1_RX_GPIO18>;
        bias-pull-up;
      };
    };

};

Then, in the UART node, I have added our above pin definition and a “gnss-nmea-generic” compatible device. I have created an alias to this GNSS device to facilitate the access from the main code.

&uart1 {

  status = "okay";
  current-speed = <9600>;
  pinctrl-0 = <&uart1_pins>;
  pinctrl-names = "default";
  gnssdev: gnss-nmea-generic {
    compatible = "gnss-nmea-generic";
  };

};

Finally, the main code uses the GNSS_DATA_CALLBACK_DEFINE macro to define a callback for getting the information about satellites in view and another for GNSS information such as time, coordinates and tracking satellites. The following screenshot depicts the application output:

 

We managed to get the GNSS data!

Final thoughts

We have covered in this post what NMEA means and its importance in GNSS as well as the main NMEA messages, how the new Zephyr GNSS API relies on NMEA, and a sample of how to localize our device using Zephyr. Now, it is your turn to send the GNSS data to Golioth cloud!

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

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

Two blue Nordic development boards on a wooden desk

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

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

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

HCI low power UART

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

1. Build the HCI low power UART

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

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

2. Move the Prog/Debug switch and flash

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

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

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

Load the central_ht sample on the nRF9160-DK

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

1. Add board files to the central_ht sample code

Navigate to the central_ht sample code in the Zephry tree:

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

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

In total we add 3 files:

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

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

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

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

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

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

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

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

};

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

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

2. Build and flash the central_ht sample code

west build -b nrf9160dk_nrf9160_ns .
west flash

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

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

Load the peripheral_ht sample on the nRF52840-DK

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

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

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

Monitor the output

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

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

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

Add Golioth to the project

To Golioth to this project we need three things:

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

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

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

1. Add Golioth as a Zephyr Module

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

2. Add Kconfig symbols for cellular and Golioth

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

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

# General config
CONFIG_HEAP_MEM_POOL_SIZE=4096
CONFIG_NEWLIB_LIBC=y

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

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

# Modem library
CONFIG_NRF_MODEM_LIB=y
CONFIG_NRF_MODEM_LIB_ON_FAULT_APPLICATION_SPECIFIC=y

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

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

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

# Generate MCUboot compatible images
CONFIG_BOOTLOADER_MCUBOOT=y

# HCI low power UART
CONFIG_NRF_SW_LPUART=y
CONFIG_NRF_SW_LPUART_INT_DRIVEN=y

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

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

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

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

CONFIG_GOLIOTH_SAMPLE_HARDCODED_CREDENTIALS=n

CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y

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

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

3. Add Golioth API calls to main.c

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

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

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

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

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

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

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


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

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

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


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

Running the demo and viewing data on the cloud

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

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

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

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

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

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

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

Connecting Bluetooth to the Cloud

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

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

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

Golioth works with Qualcomm

We’re excited to announce the latest update to the Golioth Firmware SDK, release 0.12.0, which now includes support for Zephyr’s newly introduced Modem Subsystem. This enhancement significantly increases the flexibility of our platform, enabling support for a broader array of cellular modem technologies, starting with Qualcomm. 0.12.0 adds support for the Quectel BG95, joining the Nordic Semiconductor nRF9160 (our go-to modem around here!) as a first class cellular modem. We also introduced additional ways to securely store credentials.

Zephyr’s New Modem Subsystem

Introduced in Zephyr 3.5.0, the Modem Subsystem is a unified interface for modem drivers. This addition simplifies the integration of cellular modems (and others) into Zephyr-based projects, greatly expanding the range of devices and technologies that developers can utilize effectively. For a detailed overview of the modem subsystem, check out this summary from Zephyr’s Developer Advocate, Benjamin Cabé.

Integration in Golioth Firmware SDK

With the integration of this modem subsystem in the Golioth Firmware SDK, Golioth users can now more flexibly incorporate a wider array of modem technologies into their IoT projects. There are a lot of great modems and module vendors in the market and providing choice is at the heart of what we do at Golioth.

First Supported Modem and Board

The first modem we are supporting with this updated SDK is the BG95 from Quectel, based on Qualcomm technology. The BG95 is paired with the nRF52840 on the RAK5010 development board from RAKwireless. This combination highlights the flexibility of Qualcomm’s technology integrated into Quectel’s hardware, offering developers robust tools for deploying cellular IoT solutions efficiently.

Why Qualcomm?

We chose to support Qualcomm modems because our community asked for it! Developers have different design needs and want maximum flexibility. They need more options that accommodate diverse business needs. Qualcomm chipsets offer the latest in connectivity protocols and radio technology at competitive prices. Qualcomm provides silicon and support for a wide ecosystem of module vendors, such as Quectel, U-Blox, Telit, and more. Golioth customers have used Qualcomm modems in their products in the past, but needed to do much of the integration engineering themselves. Zephyr’s Modem Subsystem makes it easier to develop applications that integrate Qualcomm modems. Connecting this wider range of modems to Golioth is more hands-off for the user, reducing complexity. Developers can focus more on innovation and less on technical hurdles.

Also in This Release

In addition to new modem support, this release introduces a another feature: DTLS socket offloading for Zephyr. This includes an example for the long-supported Nordic Semiconductor nRF9160.

DTLS socket offloading leverages a modem’s secure credential store, which allows for the use of secure, offloaded sockets. This means there is not any private key material in RAM. This can be a significant advantage as it helps reduce RAM usage, code size, CPU load, and power consumption. Actual benefits will vary depending on the application and how the code is utilized.

This new feature enhances device security and efficiency, contributing further to the versatility and robustness of the Golioth Firmware SDK. Mike previously wrote how to store credentials on the nRF9160 using TLS tags.

Getting Started

To get started with the latest SDK:

  1. Update to the newest release, 0.12.0, from the Golioth Firmware SDK repository.
  2. Explore the documentation and examples provided for integrating the RAK5010 board or try DTLS socket offloading with the nRF9160.
  3. Visit our community forums or support channels if you need help or want to discuss your projects.

Focused on Your Success

At Golioth, we’re committed to providing you with the tools and flexibility needed to succeed in the fast-evolving world of IoT. By adding support for new modems and enhancing the ways you can manage credentials, we aim to streamline your development process and empower your innovative projects. Whether you’re integrating the latest modem technology or implementing secure credential management, Golioth is here to support every step of your journey towards building smarter, more connected solutions.

In a previous post on Debugging nRF9160 Zephyr apps with Ozone, I showed how to use the New Project Wizard to create a new Ozone project for debugging a Zephyr app.

However, the project files generated by the New Project Wizard contain hard-coded paths. This makes them unsuitable for sharing with colleagues or checking them into a git repository.

In this post, I’ll demonstrate how to create a portable Ozone project file that you can check into your Zephyr application’s git repository.

Build the example app

To make this example concrete, we’ll be debugging the zephyr/samples/basic/blinky/ app from the nRF Connect SDK Zephyr repo. The firmware image will be built for the nrf9160dk_nrf9160_ns target running on the Nordic nRF9160 DK board. However, the concepts in the examples below should apply to any Zephyr app or board.

I’ve manually added an additional debug KConfig fragment file that we’ll be merging in to this app during the build process. This debug.conf file enables thread-aware debugging in Ozone:

CONFIG_DEBUG_THREAD_INFO=y

# CONFIG_DEBUG_THREAD_INFO needs the heap memory pool to
# be defined for this app
CONFIG_HEAP_MEM_POOL_SIZE=256

To build the firmware for the example blinky app:

cd <your ncs workspace>/
west build -p -b nrf9160dk_nrf9160_ns zephyr/samples/basic/blinky/ -- -DEXTRA_CONF_FILE="debug.conf"

If the build completed successfully, you’ll see the build/zephyr/zephyr.elf & build/zephyr/merged.hex files we need to start a debugging session in Ozone.

Create a portable Ozone project file

Next, we’ll create a portable Ozone project file.

I’ll show the entire file first so you can see what it looks like all put together, and then I’ll walk through what each line in the file does in more detail.

I named my project file nRF9160.jdebug and located it in the root of my NCS workspace, but you can name this file anything you want (the file extension should be .jdebug):

void OnProjectLoad(void)
{
	Project.SetDevice("nRF9160_xxAA");
	Project.SetTargetIF("SWD");
	Project.SetTIFSpeed("4 MHz");
	Project.AddSvdFile("$(InstallDir)/Config/CPU/Cortex-M33F.svd");
	File.Open("$(ProjectDir)/build/zephyr/zephyr.elf");
	Project.SetOSPlugin("ZephyrPlugin.js");
}

void TargetDownload(void)
{
	Exec.Download("$(ProjectDir)/build/zephyr/merged.hex");
}

Ozone scripts

Project files in Ozone are scripts written in a simplified C-style language. Ozone defines a set of event handler functions that are called when specific debug events happen in the debugger.

The OnProjectLoad() event handler is executed when the project file is opened, so this is where we’ll do most of the setup for our project.

Project action functions

The nRF9160 DK board has an onboard SEGGER J-Link OB debugger which is connected to the nRF9160 SIP via SWD.

Ozone defines a set of project actions that can be used to configure the debugger settings:

  • Project.SetDevice("nRF9160_xxAA"); configures the debugger for the nRF9160 SIP
  • Project.SetTargetIF("SWD"); configures the debugger to use the Serial Wire Debug (SWD) protocol
  • Project.SetTIFSpeed("4 MHz"); sets the SWD signaling speed to 4 MHz

Next, we need to provide the debugger with a link to the System View Description (SVD) files for the ARM Cortex-M33F applications MCU in the nRF9160 SIP. Ozone comes bundled with a bunch of SVD files for different ARM cores, so we can provide a portable path to the file’s location by using the $(InstallDir) directory macro:

  • Project.AddSvdFile("$(InstallDir)/Config/CPU/Cortex-M33F.svd");

Since we’re debugging a Zephyr app, we can load the Zephyr RTOS awareness plugin automatically:

  • Project.SetOSPlugin("ZephyrPlugin.js");

File action functions

In addition, we need to provide Ozone with the path to the ELF file for the Zephyr app we’re trying to debug. We can use the $(ProjectDir) directory macro to provide a portable path to the ELF file relative to the directory containing the project file:

    • File.Open("$(ProjectDir)/build/zephyr/zephyr.elf");

Process replacement functions

In this example, we have a multi-image build for the nRF9160. We need to instruct the debugger to download the full merged image (not just the ELF). Ozone provides a set of process replacement functions that replace the default implementations of built-in debug operations. We can use the TargetDownload(); function to replace the default program download routine with one that loads the merged.hex file instead. Note that this also uses the $(ProjectDir) directory macro to provide a portable portable path to the merged image:

  • Exec.Download("$(ProjectDir)/build/zephyr/merged.hex");

Start the debug session

Now, click “File” → “Open…” and open the nRF9160.jdebug project file in Ozone.

Click on “Debug” → “Start Debug Session” → “Download & Reset Program” to start the debug session:

If you have multiple SEGGER devices plugged into your USB port, you’ll get a pop-up window asking which device should be used to flash the target:

If you don’t see the “Zephyr” window in the Ozone project, click on “View” → “Zephyr” to show the window:

That’s it!

You can relocate this file into any directory, including the git repository for your Zephyr app. Just make sure to update the relative paths in the file. Anyone who checks out the repo can quickly start a debug session without having to configure their own Ozone project.

Conclusion

Golioth can help you connect and secure your IoT devices, send sensor data to the web, update firmware over-the-air, and scale your fleet with an instant IoT cloud.

Try it today! You can make a small connected fleet and start sending data to and from the cloud immediately. Device management is free for individual users!

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

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

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

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

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

Why Arduino, anyway?

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

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

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

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

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

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

Zephyr is changing things up in the industry

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

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

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

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

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

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

Not just for production grade firmware

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

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

My firmware needs included:

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

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

net_connect();

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

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

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

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

Abstractions are your friend in a complex world

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Quick, once you know how to get around

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

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

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

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

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

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