Implementing a Joiner in nRF Connect SDK
Developing custom Thread Joiner, and provisioning it with a commissioner. Exploring the nRF Connect APIs.
⬅️ 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:
- Enable Thread — Ensure that the Thread network and IPv6 are enabled.
- Discovery — Scan the network, request credentials, and select the best available network.
- 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.
- 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.