Tag Archive for: shell

Zephyr has a lot of tricks up its sleeve and most recently I used it to enable power regulators on a custom Golioth board. Perhaps the most interesting part of this is that it can be done entirely with the configuration code, without needing to dive in to any of the C files. And as the icing on the cake, Zephyr even includes interactive shell for working with regulators!

If you’ve been following our blog for a while, or you’ve checked out our growing library of reference designs, you’ll know that we’ve been using an internal hardware platform codenamed “Aludel” for rapid prototyping. Chris Gammell has been busy working on a new version of the Aludel main board with the ability to shut off power to the mikroBUS Click board sockets for low-power operation.

For example, a GPIO from the onboard nRF9160 SIP is connected to the EN pin on the +5V boost regulator. This grants Zephyr firmware the power (ha!) to enable or disable the regulator:

When the first prototype boards arrived from the manufacturer, we needed to enable these power regulators as part of the hardware bring-up process. I was pleasantly surprised to discover that this is actually possible to do entirely with Devicetree & Kconfig without writing any C code!

Zephyr’s regulator bindings

Out of the box, Zephyr provides regulator-gpio and regulator-fixed devicetree bindings for variable and fixed-voltage regulators respectively. These bindings are used to define GPIO-controlled power regulators that can be enabled automatically when the OS boots.

Here’s the devicetree node we’re using for the +5V boost regulator on the Aludel board:

reg_mikrobus_5v: reg-mikrobus-5v {
    compatible = "regulator-fixed";
    regulator-name = "reg-mikrobus-5v";
    enable-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
    regulator-boot-on;
};

The compatible property matches this node to Zephyr’s regulator-fixed device tree binding, and the regulator-name property is just a descriptive name for the regulator output.

Next, the enable-gpios property is a phandle-array devicetree property which defines the GPIO connected to the regulator’s EN pin:

  • &gpio0 is the phandle for the GPIO0 controller node that controls the pin
  • 4 is the pin number on the controller
  • the pin is configured with the GPIO_ACTIVE_HIGH flag because the regulator EN pin is an active-high input

The regulator-boot-on property tells the regulator driver that the OS should turn on the regulator at boot (but shouldn’t prevent it from being turned off later). We just needed to set the following Kconfig symbol to enable the regulator drivers:

CONFIG_REGULATOR=y

Regulator shell

When you’re bringing up a new board, it’s really helpful to have full control over the power regulators from a diagnostic shell. Zephyr provides a regulator shell that allows you to interact with regulators on your board via the shell interface!

Set the following Kconfig symbols to enable the regulator shell:

CONFIG_SHELL=y
CONFIG_REGULATOR=y
CONFIG_REGULATOR_SHELL=y

With the regulator shell enabled, you can enable/disable the regulator. You can also get/set parameters like voltage, current limit, and operating modes.

Regulator shell example

First, we need to get the device node for the regulator. You can list the available devices and their states using the device list shell command:

uart:~$ device list
devices:
- nrf91_socket (READY)
- clock@5000 (READY)
- gpio@842500 (READY)
- psa-rng (READY)
- uart@9000 (READY)
- uart@8000 (READY)
- flash-controller@39000 (READY)
- i2c@a000 (READY)
- reg-mikrobus-5v (READY)
- reg-mikrobus-3v3 (READY)

To disable the reg-mikrobus-5v regulator, we can run the following shell command:

uart:~$ regulator disable reg-mikrobus-5v

To enable the reg-mikrobus-5v regulator again, we can run a similar shell command:

uart:~$ regulator enable reg-mikrobus-5v

Time to hit that play button on some Warren G!

🎵 “Regulators, mount up!” 🎵

Learn more about the Zephyr shell

Controlling power regulators is just one of the neat things you can do with the Zephyr shell. At Golioth we think the Zephyr shell is fantastic and we’ve written extensively about it on our blog. We also use it in the Golioth SDK for things like setting device credentials.

If you’d like to learn more about Zephyr in general, join us for a free Zephyr training. Our next session is coming up in February. Sign up now!

The Zephyr shell is a powerful tool that we use all the time here at Golioth for prototyping and troubleshooting. But it’s also a fantastic way to provide user control of your devices. It’s actually quite easy to add your own commands, and that’s what I’m writing about today!

Back in May we showed how to provision devices by adding Golioth encryption keys using the Zephyr shell. One use case is sending unprovisioned devices to customers, enabling them to connect via USB to add their encryption keys when they first receive the hardware. That system used a custom shell command build into the Golioth SDK as the interface. The same technique can be used to provide run-time interactivity. I recently implemented a serial command interface that let me send commands from Node-RED to a Zephyr device for automated control.

The good news is that all you have to do is turn on the shell (if it’s not already enabled), register your commands with Zephyr, and provide a callback function to react to the user input.

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.

Starting a basic project and turning on the Zephyr shell

The first step is to enable the Zephyr shell using KConfig, and include a header file in your main.c. I’m using a virtual device based on QEMU for today’s example (but this will work with any Zephyr-compatible device that has a serial connection). Golioth has a quickstart guide for using QEMU to simulate devices, which is really handy for testing out new development in isolation from existing projects.

To begin this demo I copied the basic/minimal sample from the Zephyr tree. From there I added a prj.conf file with just one KConfig symbol in it to turn on the shell:

CONFIG_SHELL=y

I also included shell.h in the main.c file:

#include <shell/shell.h>

Creating the custom shell command

I will walk through a very basic custom shell command. The topic goes much deeper and it’s worth reading through the official Zephyr documentation on shell commands.

Inside of main we want to set up the command structure, then register the command. Our demo will set a numeric value for how awesome Golioth is. We also want to be able to read back that value. So I’ll set up two different commands, one that takes a value as an argument.

SHELL_STATIC_SUBCMD_SET_CREATE(
    g_awesome_cmds,
    SHELL_CMD_ARG(set, NULL,
        "set the Golioth awesome level\n"
        "usage:\n"
        "$ golioth_awesome set <awesome_value>\n"
        "example:\n"
        "$ golioth_awesome set 1337\n",
        cmd_g_awesome_set, 2, 0),
    SHELL_CMD_ARG(get, NULL,
        "get the Golioth awesome level\n"
        "usage:\n"
        "$ golioth_awesome get",
        cmd_g_awesome_get, 1, 0),
    SHELL_SUBCMD_SET_END
    );
 
SHELL_CMD_REGISTER(golioth_awesome, &g_awesome_cmds, "Set Golioth Awesomeness", NULL);

The SHELL_STATIC_SUBCMD_SET_CREATEdefines our command structure and I’ve given it an arbitrary symbol name (g_awesome_cmds)as an identifier.

The next two commands are established using the SHELL_CMD_ARG. Let’s walk through the arguments for that macro:

  • The first argument is the subcommand set orget (note these are not strings)
  • The second argument is for subcommands of these subcommands, which are needed for this example
  • The third argument is the help string to show in the shell
  • The fourth argument is the callback function the shell will execute
  • The fifth and six arguments are the number of required and optional arguments. Note that we need 2 required arguments for set in order to capture the submitted value, but only one for get (the subcommand itself).

Finally, I need to register the shell command. The SHELL_CMD_REGISTERtakes a series of arguments. First is the command itself (note that this is not a string), the address of the subcommand structure, the help text for this command, and a function handler which we don’t need since we’re using callback functions defined in the subcommand macro.

A custom shell command in action

We’re not quite done coding yet as we need to make the callback functions. See the bottom of this post for the complete code. But let’s skip right to the demo since the meat of the work has already been done.

uart:~$ help
Please press the <Tab> button to see all available commands.
You can also use the <Tab> button to prompt or auto-complete all commands or its subcommands.
You can try to call commands with <-h> or <--help> parameter for more information.
 
Shell supports following meta-keys:
  Ctrl + (a key from: abcdefklnpuw)
  Alt  + (a key from: bf)
Please refer to shell documentation for more details.
 
Available commands:
  clear            :Clear screen.
  device           :Device commands
  devmem           :Read/write physical memory"devmem address [width [value]]
  golioth_awesome  :Set Golioth Awesomeness
  help             :Prints the help message.
  history          :Command history.
  kernel           :Kernel commands
  resize           :Console gets terminal screen size or assumes default in case
                    the readout fails. It must be executed after each terminal
                    width change to ensure correct text display.
  shell            :Useful, not Unix-like shell commands.
uart:~$

The readout above shows that typing help lists our new command in the menu.

uart:~$ golioth_awesome --help
golioth_awesome - Set Golioth Awesomeness
Subcommands:
  set  :set the Golioth awesome level
        usage:
        $ golioth_awesome set <awesome_value>
        example:
        $ golioth_awesome set 1337
 
  get  :get the Golioth awesome level
        usage:
$ golioth_awesome get
uart:~$

If we call our custom golioth_awesome --help command, the help strings we specified are displayed.

Golioth custom shell command in Zephyr

And finally, using the commands shows the expected output. I’ve embedded an image of my terminal so that you can see the different styles of output. This printout is the result of calling `shell_fprintf()` in my callback functions. We haven’t covered that yet, so let’s look at the whole main.c file next.

Putting it all together

The final piece of the puzzle is to define the callback functions for custom shell commands. These functions interact with the Zephyr app on your device, and are responsible for printing messages in the Zephyr shell as feedback for the user.

#include <zephyr/zephyr.h>
#include <stdlib.h>
 
#include <shell/shell.h>
 
uint16_t golioth_awesome = 10000;
 
static int cmd_g_awesome_set(const struct shell *shell, size_t argc,
                char *argv[])
{
    /* Received value is a string so do some test to convert and validate it */
    int32_t desired_awesomeness = -1;
    if ((strlen(argv[1]) == 1) && (argv[1][0] == '0')) {
        desired_awesomeness = 0;
    }
    else {
        desired_awesomeness = strtol(argv[1], NULL, 10);
        if (desired_awesomeness == 0) {
            //There was no number at the beginning of the string
            desired_awesomeness = -1;
        }
    }
 
    /* Reject invalid values */
    if ((desired_awesomeness < 0) || (desired_awesomeness > 65535)) {
        shell_fprintf(shell, SHELL_ERROR, "Invalid value: %s; expected [0..65535]\n", argv[1]);
        return -1;
    }
    /* Otherwise set and report to the user with a shell message */
    else {
        golioth_awesome = (uint16_t)desired_awesomeness;
        shell_fprintf(shell, SHELL_NORMAL, "Golioth awesomeness set to: %d\n", desired_awesomeness);
    }
 
    return 0;
}
 
static int cmd_g_awesome_get(const struct shell *shell, size_t argc,
                char *argv[])
{
    shell_fprintf(shell, SHELL_NORMAL, "Current Golioth awesomeness level: %d\n", golioth_awesome);
    return 0;
}
 
void main(void)
{
    SHELL_STATIC_SUBCMD_SET_CREATE(
        g_awesome_cmds,
        SHELL_CMD_ARG(set, NULL,
            "set the Golioth awesome level\n"
            "usage:\n"
            "$ golioth_awesome set <awesome_value>\n"
            "example:\n"
            "$ golioth_awesome set 1337\n",
            cmd_g_awesome_set, 2, 0),
        SHELL_CMD_ARG(get, NULL,
            "get the Golioth awesome level\n"
            "usage:\n"
            "$ golioth_awesome get",
            cmd_g_awesome_get, 1, 0),
        SHELL_SUBCMD_SET_END
        );
 
    SHELL_CMD_REGISTER(golioth_awesome, &g_awesome_cmds, "Set Golioth Awesomeness", NULL);
}

The callback functions begin on line 8 and 38 of the example above. The cmd_g_awesome_set function looks more complicated than it is. That’s because the value we receive from the shell is a string and needs to be converted to an integer and then validated (confirm it is actually a number, and inside the acceptable bounds).

The thing to focus on are the shell_fprintf() functions which use some constants to select the text decoration. SHELL_NORMAL is used when everything is working correctly, and SHELL_ERROR for out-of-bounds settings. You can see all constants that work for this, as well as the shorthand functions for them, in the official docs.

The utility of custom shell commands

So fare I’ve focused on shell interaction with a human user. But once this is in place, you can leverage it programmatically as well. In Linux, I can set our Golioth value from the command line: echo "golioth_awesome set 10000" > /dev/ttyUSB0.

You can also extrapolate this feature from set/get to a much more robust tool. For instance, I covered the built-in Zephyr i2c and sensor shells in a previous post. These facilitate runtime changes to the menu (what sensors and devices are available changes without needing to compile for that information). This should get your mind running on how deep you can go with a custom shell tool to fit your needs. But that’s a topic for next time!

Zephyr has a powerful interactive shell that you need to have in your bag of tricks. Two weeks ago I showed how to use the Zephyr shell to set device keys for authenticating with the Golioth platform. Today I’ll dive into using the same interface for live-debugging of i2c sensors and devices.

i2c shell basics

The ability to type out i2c commands, rather than writing/compiling/flashing code to test your changes will speed up the prototyping process with new i2c parts. My favorite feature is the scan command which lets me verify the part is connected correctly and at the address that I expected. Let’s begin by testing that out.

1. Starting from minimal sample

For this demo I’ll be using an ESP32 and the Zephyr basic/minimal sample. As the name suggests, this starts out with almost nothing running. We need to add a KConfig file that enables GPIO, I2C, and the related Zephyr shells:

CONFIG_GPIO=y
CONFIG_I2C=y

CONFIG_SHELL=y
CONFIG_I2C_SHELL=y

I’m using an ESP32 board with the i2c0 pins. I also have a sensor connected which is shown as a node and will be used in the next section of this demo.

&i2c0 {
	status = "okay";

	apds9960@39 {
		compatible = "avago,apds9960";
		reg = <0x39>;
		label = "APDS9960";
		int-gpios = <&gpio0 26 (GPIO_ACTIVE_LOW)>;
	};
};
west build -b esp32 samples/basic/minimal/
west flash

This can be built and flashed as normal:

west build -b esp32 samples/basic/minimal/
west flash

2. Opening a terminal connection

From there I drop into the shell by opening the device with a serial terminal program. I like to use minicom -D /dev/ttyUSB0 --color=on. As a side note, I have noticed with the ESP32 I need to turn off hardware flow control or else keystrokes don’t make it to the device.

3. Basic i2c in the shell

Shell commands often include help menus that can be activated by adding -h to your command. I use this to remind me of the syntax for the shell functions I’m using.

uart:~$ i2c -h
i2c - I2C commands
Subcommands:
  scan        :Scan I2C devices
  recover     :Recover I2C bus
  read        :Read bytes from an I2C device
  read_byte   :Read a byte from an I2C device
  write       :Write bytes to an I2C device
  write_byte  :Write a byte to an I2C device
uart:~$ device list
devices:
- clock@5000 (READY)
- gpio@842500 (READY)
- CRYPTOCELL_SW (READY)
- uart@8000 (READY)
- nrf91_socket (READY)
- i2c@9000 (READY)
- flash-controller@39000 (READY)
- bme280@76 (READY)
  requires: i2c@9000
- lis2dh@18 (READY)
  requires: gpio@842500
  requires: i2c@9000
uart:~$

Here you can see that the help menu for the i2c keyword lists the basic syntax. I also called device list which prints out the available devices, including the i2c bus. This means the actual command I want is:

uart:~$ i2c scan i2c@9000
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:             -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- 18 -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- 39 -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- 51 -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- 76 --
4 devices found on i2c@9000

Voila! The grid that prints out shows that I have five devices that responded on the i2c bus. Note that I also get a number of error logs, which is the naturally result of trying to talk to i2c devices that are not present.

4. Direct communication with an i2c device

I know that the device at address 0x58 is an AW9523 port expander. There is no Zephyr driver for this part, so I need to control it with my own code. Before writing the functions in Zephyr, I can try out each command to verify the behavior.

uart:~$ i2c read_byte i2c@9000 0x58 0x12
Output: 0xff
uart:~$ i2c write_byte i2c@9000 0x58 0x12 0x00
uart:~$ i2c read_byte i2c@9000 0x58 0x12
Output: 0x0
uart:~$

Here I’ve read the LED mode switch register on the device, set it to LED mode, and then verified that the new setting was received. The syntax places the device address (0x58) after the read/write command, then the register address (0x12), and for write commands you then add the value you want stored on that register (0x00).

Sensor shell

But wait, what about the i2c sensors with built in Zephyr support? There’s a shell for that too! Using it is an easy way to verify your sensors are working, and to confirm what sensor channels (the uniformed types of data used by the sensor subsystem) are available.

1. Turn on the sensor shell and sensor subsystem

You may remember that I already have an APDS9960 sensor declared as a subnode in my overlay file above. But to use it, we need to configure the sensor subsystem and the sensor shell. Here is my prj.conf file with three new entries:

CONFIG_GPIO=y
CONFIG_I2C=y

CONFIG_SHELL=y
CONFIG_I2C_SHELL=y

CONFIG_SENSOR_SHELL=y
CONFIG_SENSOR=y
CONFIG_APDS9960=y

With those changes in place, just rebuild and flash:

west build -b esp32 samples/basic/minimal/
west flash

2. Open the terminal connection

As above, I’m using minicom -D /dev/ttyUSB0 --color=on to open a serial connection to the Zephyr shell on the device.

3. Read sensor values in the shell

I will again use the built-in help to find the right syntax:

uart:~$ sensor -h
sensor - Sensor commands
Subcommands:
  get  :Get sensor data. Channel names are optional. All channels are read when
        no channels are provided. Syntax:
        <device_name> <channel name 0> .. <channel name N>
uart:~$ sensor get -h
get - Get sensor data. Channel names are optional. All channels are read when no
      channels are provided. Syntax:
      <device_name> <channel name 0> .. <channel name N>
Subcommands:
  RTC     
  GPIO_1  
  GPIO_0  
  UART_0  
  I2C_0   
  APDS9960
uart:~$ sensor get APDS9960
channel idx=15 prox =  16.000000
channel idx=17 light = 143.000000
channel idx=19 red =  77.000000
channel idx=20 green =  56.000000
channel idx=21 blue =  39.000000
uart:~$ 

Can you hear my evil laugh building to a crescendo?

The help message tells us that get is the only available sensor command, and calling help on that lists our devices. The new entry on the list is the sensor we declared in our devicetree overlay file. Calling it without specifying a channel lists out all that are available. Now compare that to the sensor_channel_get() commands in code sample for this sensor:

sensor_channel_get(dev, SENSOR_CHAN_LIGHT, &intensity);
sensor_channel_get(dev, SENSOR_CHAN_PROX, &pdata);

It’s worth mentioning that we’ve done all of this using KConfig values in Zephyr. The source code for this example is literally empty:

#include <zephyr/zephyr.h>;

void main(void)
{
}

Zephyr has so many shells!

How many shells does Zephyr have? How many stars are there in the sky? There are surely ways to answer these questions but I don’t have them in front of me right now.

Popular among our team is the Network Shell for networking diagnostics, and the OpenThread Shell was used extensively in getting our Open Thread demo up and running. I have used the Kconfig Search page on the Zephyr docs to search for other shells and that yields a lot of really interesting info.

But mostly I just ask Marcin on the Golioth firmware team. He seems to already know about all of the cool ones. Like the Kernel Shell that lets you check on threads and stacks. Here’s a shell output that I’m using to tune up how I’m using RAM. Note that I’ve over-allocated stacks for my animation threads, and the system work queue probably needs a bit more stack space, just to be safe.

uart:~$ kernel stacks
0x3ffd3d60            (real size 2048): unused 1740     usage 308 / 2048 (15 %)
0x3ffd3cc0 weather_tid (real size 2560):        unused 272      usage 2288 / 2560 (89 %)
0x3ffd3c20 hello_tid  (real size 2048): unused 272      usage 1776 / 2048 (86 %)
0x3ffd4490 golioth_system (real size 3072):     unused 768      usage 2304 / 3072 (75 %)
0x3ffd3b80 connection_tid (real size 2048):     unused 304      usage 1744 / 2048 (85 %)
0x3ffd3a40 animate_sense_tid (real size 1024):  unused 524      usage 500 / 1024 (48 %)
0x3ffd3ae0 animate_ping_tid (real size 1024):   unused 524      usage 500 / 1024 (48 %)
0x3ffd47d0 rx_q[0]    (real size 1504): unused 256      usage 1248 / 1504 (82 %)
0x3ffd4718 net_mgmt   (real size 768):  unused 288      usage 480 / 768 (62 %)
0x3ffd45e0 wifi       (real size 3584): unused 1660     usage 1924 / 3584 (53 %)
0x3ffd4938 esp_event  (real size 4096): unused 3388     usage 708 / 4096 (17 %)
0x3ffd4530 esp_timer  (real size 4096): unused 3584     usage 512 / 4096 (12 %)
0x3ffd4b20 sysworkq   (real size 1024): unused 32       usage 992 / 1024 (96 %)
0x3ffd3ef8 shell_uart (real size 2048): unused 588      usage 1460 / 2048 (71 %)
0x3ffd3e20 logging    (real size 2048): unused 336      usage 1712 / 2048 (83 %)
0x3ffd49d8 idle       (real size 1024): unused 828      usage 196 / 1024 (19 %)
0x3ffd4a78 main       (real size 4096): unused 3040     usage 1056 / 4096 (25 %)
0x3ffed510 IRQ 00     (real size 2048): unused 1776     usage 272 / 2048 (13 %)
uart:~$ kernel threads
Scheduler: 2745 since last call
Threads:
 0x3ffd3d60           
        options: 0x0, priority: -1 timeout: 0
        state: pending, entry: 0x4008d72c
        stack size 2048, unused 1740, usage 308 / 2048 (15 %)

 0x3ffd3cc0 weather_tid
        options: 0x0, priority: 14 timeout: 10093
        state: suspended, entry: 0x400d16b0
        stack size 2560, unused 272, usage 2288 / 2560 (89 %)

 0x3ffd3c20 hello_tid 
        options: 0x0, priority: 14 timeout: 5843
        state: suspended, entry: 0x400d1b60
        stack size 2048, unused 272, usage 1776 / 2048 (86 %)

 0x3ffd4490 golioth_system
        options: 0x0, priority: 14 timeout: 3785
        state: pending, entry: 0x400dba6c
        stack size 3072, unused 768, usage 2304 / 3072 (75 %)

 0x3ffd3b80 connection_tid
        options: 0x0, priority: 14 timeout: 1635823
        state: suspended, entry: 0x400d1c28
        stack size 2048, unused 304, usage 1744 / 2048 (85 %)

 0x3ffd3a40 animate_sense_tid
        options: 0x0, priority: 14 timeout: 592
        state: suspended, entry: 0x400d1bc8
        stack size 1024, unused 524, usage 500 / 1024 (48 %)

 0x3ffd3ae0 animate_ping_tid
        options: 0x0, priority: 14 timeout: 287
        state: suspended, entry: 0x400d1b20
        stack size 1024, unused 524, usage 500 / 1024 (48 %)

 0x3ffd47d0 rx_q[0]   
        options: 0x0, priority: -1 timeout: 0
        state: pending, entry: 0x4008872c
        stack size 1504, unused 256, usage 1248 / 1504 (82 %)

 0x3ffd4718 net_mgmt  
        options: 0x0, priority: -1 timeout: 0
        state: pending, entry: 0x40086ff0
        stack size 768, unused 288, usage 480 / 768 (62 %)

        0x3ffd45e0 wifi      
        options: 0x8, priority: 2 timeout: 0
        state: pending, entry: 0x400949c4
        stack size 3584, unused 1660, usage 1924 / 3584 (53 %)

 0x3ffd4938 esp_event 
        options: 0x8, priority: 4 timeout: 0
        state: pending, entry: 0x400ea860
        stack size 4096, unused 3388, usage 708 / 4096 (17 %)

 0x3ffd4530 esp_timer 
        options: 0x8, priority: 3 timeout: 0
        state: pending, entry: 0x400dcf88
        stack size 4096, unused 3584, usage 512 / 4096 (12 %)

 0x3ffd4b20 sysworkq  
        options: 0x0, priority: -1 timeout: 0
        state: pending, entry: 0x4008d72c
        stack size 1024, unused 32, usage 992 / 1024 (96 %)

*0x3ffd3ef8 shell_uart
        options: 0x0, priority: 14 timeout: 0
        state: queued, entry: 0x400d5570
        stack size 2048, unused 588, usage 1460 / 2048 (71 %)

 0x3ffd3e20 logging   
        options: 0x0, priority: 14 timeout: 36
        state: pending, entry: 0x4008f538
        stack size 2048, unused 336, usage 1712 / 2048 (83 %)

 0x3ffd49d8 idle      
        options: 0x1, priority: 15 timeout: 0
        state: , entry: 0x4008d1b0
        stack size 1024, unused 828, usage 196 / 1024 (19 %)

 0x3ffd4a78 main      
        options: 0x1, priority: 0 timeout: 184000604
        state: suspended, entry: 0x4008cbd4
        stack size 4096, unused 3040, usage 1056 / 4096 (25 %)
uart:~$

Until next time, go out and explore Zephyr shells. Just make sure to pop into our Discord channel and let us know which shells you find the most useful!