IOTPractices

This publication covers the practical knowledge and experience of software development practices such as TDD, CICD, Automated Testing, Agile for IoT development. It is an open community initiative for and by the IoT enthusiasts

Implementing a Joiner in nRF Connect SDK

3 min readFeb 9, 2025

--

⬅️ Thread Network Provisioning | Current | Implementing TCP Client ➡️ ️

In a previous article, we explored how a Commissioner node provisions a Joiner in a Thread network using OpenThread’s default firmware. However, in real-world applications, custom firmware logic is often required, along with programmatic control over the commissioning process. In this article, we will implement a Joiner using the nRF Connect SDK, following these four key steps.

Joiner in Action

The Joiner process involves the following steps:

  1. Enable Thread — Ensure that the Thread network and IPv6 are enabled.
  2. Discovery — Scan the network, request credentials, and select the best available network.
  3. Request to Join — Configure the dataset for the selected network and send a join request using a pre-shared key (PSKd). The Commissioner approves the request if the PSKd is valid.
  4. Start Thread — Once accepted, the Joiner starts the Thread network.

We will implement these steps in an nRF Connect application.

Developing the Joiner Application

1. Create a New Application from the Thread CLI Sample

Using the nRF Connect extension in VS Code:

  • Select Create a New Application > Copy a Sample
  • Choose the SDK version
  • Select OpenThread CLI Sample
  • Set the project path

This will generate a Thread CLI-based project.

2. Configure the Build

Add a build configuration for nRF52840 Dongle or Development Kit (DK). This exposes the OpenThread command-line interface for debugging and testing.

Modify the configuration file to include:

CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=n
CONFIG_LOG=y
CONFIG_OPENTHREAD_THREAD_VERSION_1_2=y
CONFIG_OPENTHREAD_POLL_PERIOD=1000
CONFIG_OPENTHREAD_DEBUG=y
CONFIG_OPENTHREAD_JOINER=y
CONFIG_OPENTHREAD_NORDIC_LIBRARY_MTD=y
CONFIG_OPENTHREAD_MTD=y

3. Implement the Joiner Logic

Create a new file, e.g., thread_joiner.c, and include the necessary dependencies:

#include <stdio.h>
#include <zephyr/kernel.h>
#include <version.h>
#include <zephyr/net/openthread.h>
#include <zephyr/logging/log.h>
#include <openthread/thread.h>
#include <openthread/ip6.h>
#include <string.h>
#include <openthread/netdata.h>

Step 1: Enable Thread

Register a state change callback and ensure IPv6 is enabled:


void otStateChangeCallback(otChangedFlags flags, struct openthread_context *ot_context, void *user_data)
{
LOG_WRN("thread_state_changed(0x%08" PRIx32 ")", flags );
}

static struct openthread_state_changed_cb otStateChangeCallbackInst = {
.state_changed_cb = otStateChangeCallback
};

openthread_state_changed_cb_register(otInstance, state_changed_callback, NULL);
if (!otIp6IsEnabled(otInstance)) {
otIp6SetEnabled(otInstance, true);
}

Step 2: Network Discovery

Call otThreadDiscover to scan for available networks:

errorCode = otThreadDiscover(
otInstance,
0 | OT_CHANNEL_25_MASK | OT_CHANNEL_26_MASK, // Scan channels
OT_PANID_BROADCAST,
false,
false,
&otDiscoverCallback,
openthread_get_default_context()
);

In otDiscoverCallback, choose the network node with the highest result->mLqi.

Step 3: Request to Join

Send a join request using a predefined PSKd:

const char *PSKD = "KULD55P";
errorCode = otJoinerStart(
otInstance,
PSKD, NULL, NULL, NULL,
KERNEL_VERSION_STRING, NULL,
&otJoinerStartCallback,
openthread_get_default_context()
);

On success, otJoinerStartCallback will be triggered.

Step 4: Start the Thread Network

Once the Joiner request is approved, enable the Thread network:

errorCode = otThreadSetEnabled(otInstance, true);

Step 5: Listen to Thread State changes

Implement otStateChangeCallback

void otStateChangeCallback(otChangedFlags flags, struct openthread_context *ot_context, void *user_data)
{
LOG_INF("Thread state changed(0x%08" PRIx32 ")", flags );

if (flags & OT_CHANGED_THREAD_ROLE)
{
otDeviceRole currentRole = otThreadGetDeviceRole(openthread_get_default_instance());
if(currentRole == OT_DEVICE_ROLE_CHILD)
{
LOG_INF("Device joined as clild successfully!");
}
}

4. Build and Run the Joiner

  • Add thread_joiner.c to the CMake configuration.
  • Build the project and flash it onto the Dongle or DK.
  • On a successful flash, the device will attempt to join the network and print logs on state changes.
<inf> thread_joiner: thread state changed(0x01009009)
<inf> thread_joiner: thread state changed(0x00008000)
<inf> thread_joiner: thread state changed(0x0800800b)
<inf> thread_joiner: thread state changed(0x00008000)
<inf> thread_joiner: thread state changed(0x0800c00b)
<inf> thread_joiner: thread state changed(0x08000000)
<inf> thread_joiner: thread state changed(0x08000000)
<inf> thread_joiner: thread state changed(0x18040100)
<inf> thread_joiner: thread state changed(0x1800100f)
<inf> thread_joiner: thread state changed(0x301332b7)
<inf> thread_joiner: Device joined as clild successfully!

Commissioner Logs

A Commissioner node must be running and configured to allow Joiner access. If successful, the Commissioner will log events similar to:

Commissioner: Joiner start 0ed58b6b7917a948
Commissioner: Joiner connect 0ed58b6b7917a948
Commissioner: Joiner finalize 0ed58b6b7917a948
Commissioner: Joiner end 0ed58b6b7917a948

This confirms that the Joiner successfully connected to the Thread network.

Conclusion

By following these steps, we have implemented a Joiner application using the nRF Connect SDK. This approach allows for customized firmware logic, enabling a more flexible and automated provisioning process in Thread networks.

--

--

IOTPractices
IOTPractices

Published in IOTPractices

This publication covers the practical knowledge and experience of software development practices such as TDD, CICD, Automated Testing, Agile for IoT development. It is an open community initiative for and by the IoT enthusiasts

Kuldeep Singh
Kuldeep Singh

Written by Kuldeep Singh

Engineering Director and Head of XR Practice @ ThoughtWorks India.

No responses yet