Federico Fuga

Engineering, Tech, Informatics & science

When Zephyr meets OpenThread

27 Apr 2023 10:53 CEST

OpenThread is the implementation of the Thread protocol on Zephyr.

Introduction

If you need to connect a sensor or board to the Thread network, you need first to provision (“commission” in Thread terms) your device, that is, share the network security credentials and keys with it.

This is not different from the Wifi procedure – in wifi you usually have a Network name (“Access Point” name) and a shared key.

But in Thread this is even easier, because you don’t need to connect to your IoT device using bluetooth LE, push the PSK and ap name, or copy it by hand. In Thread, if you are using the “External Commissioning” method, you just need to take two string – EUID64 and Joiner Credential – that can be combined in a QR code, and scan it with the Commissioning App on your smartphone.

So first step is Commissioning the device.

On our Hobbyst device, we’ll use a serial shell to retrieve the EUID64 and generate the Commissioning Credentials, we’ll copy it on a QR Code Generator and scan with the Commissioning app.

Configuring and compiling the firmware

We’re going to use a nrf52840 board (Adafruit Feather NRF52840), because esp32 don’t support Thread as far as I know.

Since we need to save the network data somewhere, we’re going to use the settings service I explained in an earlier post, and a shell, because we’ll need to type some command to instruct the device to enter the joiner state. This is not mandatory, you can always use an automated task to retrieve the euid64, generate a random Commissioning Credential and output them in the serial console; or even, send them to some screen in form of a QR code; or via bluetooth LE; or whatever.

The shell is the easier and faster way to accomplish this.

So: First we enable the flash and settings services:

CONFIG_FLASH=y
CONFIG_FLASH_MAP=y

CONFIG_SETTINGS=y
CONFIG_SETTINGS_RUNTIME=y

CONFIG_NVS=y
CONFIG_SETTINGS_NVS=y

We also enable the shell:

CONFIG_SHELL=y
CONFIG_SETTINGS_SHELL=y

We can also disable some shell option to optimize the space (more later, eventually).

Then we enable OpenThread:

CONFIG_NETWORKING=y
CONFIG_NET_L2_OPENTHREAD=y
CONFIG_OPENTHREAD=y
CONFIG_OPENTHREAD_THREAD_VERSION_1_3=y
CONFIG_OPENTHREAD_JOINER=y
CONFIG_OPENTHREAD_JOINER_AUTOSTART=y
CONFIG_OPENTHREAD_FTD=y

If we want to use the USB port as a serial emulation, we need to enable the cdc-usb service, as explained in another post. I suggest to put this in a board overlay, because some other board just provide a converter for the serial port.

In the code, we need to start the thread stack, it’s a one liner in the main() function:

	openthread_start(openthread_get_default_context());

A problem with the mainline Zephyr

The mainline zephyr has some problem with the openthread support for the Nordic board. Indeed, everything compiles fine but the generated binary doesn’t work, it seems to work as soon as it has been flashed but then the bootloader is screwed and the board doesn’t boot anymore after the first reset.

The solution is to use the Nordic Connect SDK. I suggest to install it manually, using a venv for west instead of the system wide installation in the Nordic instructions.

When in section 2, titled Install West, do not run pip3 install --user west as stated but first create a virtual env in the installation folder (I installed it system-wide in /opt/ncs).

$ python3 -m venv /opt/ncs/venv
$ . /opt/ncs/venv/bin/activate

Then install west and all the required packages by running any python command from the venv just installed. Remember to omit the --user parameter because there’s no such a user role in the virtual env. Anyway pip3 will kindly refuse to work with that.

Then, to compile the firmware, just use the Nordic Connect SDK instead. You can run the environment setup commands from a shell script, like this:

export ZEPHYR_BASE="/opt/ncs/v2.3.0/zephyr/"
source /opt/ncs/venv/bin/activate
source /opt/ncs/zephyr/zephyr-env.sh

west build -b adafruit_feather_nrf52840 will generate a perfectly working hex file that you can flash in the board using the segger board. I haven’t tested yet the uf2 and uf2 bootloader method, that might work as well if you put the proper config lines:

CONFIG_FLASH_LOAD_OFFSET=0x26000
CONFIG_BUILD_OUTPUT_UF2=y
CONFIG_BUILD_OUTPUT_UF2_FAMILY_ID="0xada52840"

Joining the network

How to join the Thread network by using the external commissioning method is pretty simple, but the examples provided by websites use the cli app that implements some different command and don’t use the default shell commands that are enabled in our firmware.

When powered on for the first time, the firmware will bring the interface up but will not try to join the network or connect, despite the name of the CONFIG_OPENTHREAD_JOINER_AUTOSTART configuration entry. Not sure what this means. Anyway.

When in the shell, first we’ll bring the interface up, and retrieve the parameters:

uart:~$ ot ifconfig up
Done
uart:~$ ot eui64
f4ce36b3070c0852
Done

Generate a random string of at least 6 characters in the set 0-9 and A-Y, excluding I, O, Q, and Z for readability, strictly uppercase. So “J01NME” is ok, “JoinMe” is not.

Generate a QRCode in this form: v=1&&eui=(your_euid64)&&cc=(your_pass), for example v=1&&eui=f4ce36b3070c0852&&cc=J01NME.

The generated QRCode

Open the Commissioning App, if the smartphone is connected to the same AP as the thread Border Router, you should see your thread network listed with the Board Router Address. Select it and click on Add Device. Then scan the QR Code you just generated.

After a few second, the app will go in “petitioner mode” and instruct the border router of the credentials of the Joiner app. When the petition is accepted, back on the console and put the device in Joiner mode:

uart:~$ ot joiner start J01NME
Done                                                                            
Join success 

After a few second the shell will instruct you that the device has joined the network!

But it’s not finished, you must now connect the device, because it has returned to idle state.

uart:~$ ot thread start
Done

The device will become a Router if possible or an End Device eventually.

Note: Do not start the join procedure before the petitioner. If ot joiner start is run before the app is in petitioner mode, it will fail:

uart:~$ ot joiner start J01NME                                              
Done                                                                            
Join failed [NotFound]                                                          

Adding some visual feedback

It would be nice to have a led (we have it) to show if the device is connected or not to the thread network. The Adafruit Feather board has just one button and one led. We’ll use the led to show when we’re connected.

To receive the feedback when some event occurs in the Open Thread network we need to register a callback. Put the following line just before the openthread_start call:

	openthread_state_changed_cb_register(openthread_get_default_context(), &ot_state_chaged_cb);

Of course we need to define the callback function and the callback data:

static bool is_connected = false;

/* Open Thread callback */

static void on_thread_state_changed(otChangedFlags flags, struct openthread_context *ot_context,
				    void *user_data)
{
	if (flags & OT_CHANGED_THREAD_ROLE) {
		switch (otThreadGetDeviceRole(ot_context->instance)) {
		case OT_DEVICE_ROLE_CHILD:
		case OT_DEVICE_ROLE_ROUTER:
		case OT_DEVICE_ROLE_LEADER:
			k_work_submit(&on_connect_work);
			is_connected = true;
			break;

		case OT_DEVICE_ROLE_DISABLED:
		case OT_DEVICE_ROLE_DETACHED:
		default:
			k_work_submit(&on_disconnect_work);
			is_connected = false;
			break;
		}
	}
}

static struct openthread_state_changed_cb ot_state_chaged_cb = {
	.state_changed_cb = on_thread_state_changed
};

We use two worker to change the state of the leds, as recommended for any interrupt or callback use:

typedef void (*ot_connection_cb_t)(struct k_work *item);
typedef void (*ot_disconnection_cb_t)(struct k_work *item);

static struct k_work on_connect_work;
static struct k_work on_disconnect_work;

static void on_ot_connect(struct k_work *item)
{
	ARG_UNUSED(item);

	gpio_pin_set_dt(&led, 1);
}

static void on_ot_disconnect(struct k_work *item)
{
	ARG_UNUSED(item);

	gpio_pin_set_dt(&led, 0);
}

/* .... */

void main(void)
{
	int ret;

	/* ... */

	k_work_init(&on_connect_work, on_ot_connect);
	k_work_init(&on_disconnect_work, on_ot_disconnect);

	openthread_state_changed_cb_register(openthread_get_default_context(), &ot_state_chaged_cb);
	openthread_start(openthread_get_default_context());
}

It’s important to note that if the board is reflashed, the settings are lost so you need to redo the commissioning procedure again.

But once connected, the device will join the network after a reset or power down.