Playing with Thread and MQTT-SN on Nordic nRF52840
Introduction
IoT is in the air. Impossible to surf the web in peace without tripping over some gushy article on billions of devices connecting to the Internet. The Teapot yelling at the Washing Machine has already become a stale joke. There are myriad complex technologies coming together to make this grand vision true. But ultimately, IoT is a simple concept – a bunch of devices containing sensors or actuators, talking to each other over a wireless network.
This article is about one such technology – Thread – an IPv6 based mesh networking technology aimed at home automation and similar IoT applications. Specifically, I will be talking about OpenThread, which is an Open Source implementation of the Thread protocol. More specifically, I will be talking about running Thread on the Nordic nRF52840 SoC which supports both BLE and 802.15.4.
I won’t go into to explaining Thread here – the OpenThread website has a wonderful Thread primer that you should read to get started. The Thread group also has a list of whitepapers which you will find useful. Ultimately you should probably read the Thread specification itself, a 348 page password-protected, watermarked PDF which the Thread Group will send you – after you fill up their form and agree to their EULA. It ain’t exactly light reading.
Objective
Here’s what we’re trying to do:
Build a Thread network using Nordic nRF52840, and establish bidirectional communication with a Thread node via the Internet from any computer.
To achieve the above, I will make use of a Nordic nRF52840-DK, a Nordic nRF8240 dongle, and a Raspberry Pi. The Pi serves as the Border Router for the Thread network with the dongle acting as the Thread NCP (Network Co-Processor).
The Border Router (among other things) will run an MQTT-SN gateway. (MQTT-SN is a simpler version of MQTT designed for wireless sensor networks) The gateway acts as a protocol converter to from MQTT-SN to MQTT. This lets you publish/subscribe to this gateway, and hence your node, using a client like MQTTfx, from any computer.
Setting up the Border Router and NCP
Let’s first talk about setting up the Border Router. This is the connection between your Thread network and the Internet. It does a whole bunch of other things as well, as you will see below.
There are two ways to do this:
- Manual setup on a Pi using instructions from OpenThread.
- Download pre-configured Pi image from Nordic. (Click on Downloads tab and look for RaspPi-Thread-Border-Router-Demo)
We’re going with option #2 here. You may wish to read up on raspberry pi installation procedure in case you are unfamiliar with it. The Nordic OpenThread Border image ships with the following components:
- OpenThread Border Router
- wpantund
- Eclipse PAHO MQTT-SN Gateway
- NFC Deamon
With this image, the Pi will boot up in command line mode – no windows. Now, if you issue the sudo systemctl status command, under the list of services, you will see the following:
- wpantund.service
- tayga.service
- paho-mqttsn-gateway.service
- otbr-web.service
- otbr-agent.service
wpantund provides IPv6 connectivity to the NCP. tayga is for NAT64 so you can connect to IPv4 addresses from your IPv6 Thread network. paho-mqttsn-gateway is for MQTT-SN. The otbr-web service provides a web UI for Thread configuration, and otbr-agent helps with Thread external commisioning.
The relationship between these components is illustrated in the graphic below from OpenThread.
[Source: https://openthread.io/guides/border-router]
Next, let’s set up the NCP. We’ll be using the Nordic nRF52840 dongle for this purpose, but you can use any nRf52840 board for this purpose.
First, you need to set up the toolchain to build OpenThread. Then, build the NCP firmware as follows:
make -f examples/Makefile-nrf52840 clean make -f examples/Makefile-nrf52840 BORDER_AGENT=1 BORDER_ROUTER=1 COMMISSIONER=1 UDP_PROXY=1 USB=1 cd output/nrf52840/bin/ arm-none-eabi-objcopy -O ihex ot-ncp-ftd ot-ncp-ftd.hex
Now you need to upload this firmware on to the dongle. You can do that using SWD with nRF52840-DK. Here’s how you hook it up.
Here’s the command to upload the firmware.
nrfjprog -f nrf52 --chiperase --program ot-ncp-ftd.hex --reset
Now, plug in the dongle to a USB port of the pi and check the wpantund status. You should see something like:
$ wpanctl status wpan0 => [ "NCP:State" => "associated" "Daemon:Enabled" => true "NCP:Version" => "OPENTHREAD/20170716-00745-g0f2e87c; NRF52840; Aug 7 2018 19:35:08" "Daemon:Version" => "0.08.00d (; Feb 23 2018 13:17:33)" "Config:NCP:DriverName" => "spinel" "NCP:HardwareAddress" => [E42AA89D474105E4] "NCP:Channel" => 13 "Network:NodeType" => "leader" "Network:Name" => "nRF52840thread" "Network:XPANID" => 0xABCD1111ABCD1111 "Network:PANID" => 0x1122 "IPv6:LinkLocalAddress" => "fe80::60de:d138:a38f:f56d" "IPv6:MeshLocalAddress" => "fdab:cd11:11ab:0:4796:7c9d:a533:1199" "IPv6:MeshLocalPrefix" => "fdab:cd11:11ab::/64" "com.nestlabs.internal:Network:AllowingJoin" => false ]
The above means that your NCP is talking to wpantund and the Thread network has been formed. The Border Router also sets up a WiFi access point (SoftAP), and by connecting to the Border Router’s local IP address, you can see a web page which lets you configure the Thread network.
For this project, we need to connect the Pi to the Internet. For this, assuming you have a WiFi connection, you can run:
$wifi_connect your_SSID your_password
At this point, check wlan0 interface as follows:
$ ifconfig wlan0 wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.0.180 netmask 255.255.255.0 broadcast 192.168.0.255 inet6 fe80::ba27:ebff:fe6f:4ad8 prefixlen 64 scopeid 0x20 ether b8:27:eb:6f:4a:d8 txqueuelen 1000 (Ethernet) RX packets 898 bytes 228455 (223.1 KiB) RX errors 0 dropped 3 overruns 0 frame 0 TX packets 12190 bytes 2975623 (2.8 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
The exact address may vary for you, but it my case, if I open my browser to 192.168.0.180 I get the following:
The web interface can be used for various tasks such as forming a Thread network, joining other networks, and showing status for the existing network.
Thread Commisioning
Commissioning is the process by which a device (called a Joiner) joins a Thread network. For this, the network credentials need to be passed to it. The thread specification has a whole Mesh Commisioning Protocol (MeshCop) designated for this purpose. Practically speaking, to get your Thread device on to your network, usually you use some kind of external method – like scanning a QR Code or using an NFC tag – to pass on the required information to the device. We won’t go into Thread Commisioning in this article. To keep things simple, we’re going to pass in the network credentials directly to the joiner via the serial port using the Thread CLI.
Setting up a Thread network
The NCP firmware comes preconfigured with a default Thread network configuration. We’ll change all the default values so that we get a sense of what’s needed to set up the network. Run these commands on the Pi.
sudo wpanctl leave sudo wpanctl setprop Network:PANID 0x1122 sudo wpanctl setprop Network:Key baad0000deed0000baad0000deed0000 sudo wpanctl config-gateway -d "fd11:22::" sudo wpanctl form "nRF52840thread" $ sudo wpanctl status wpan0 => [ "NCP:State" => "associated" "Daemon:Enabled" => true "NCP:Version" => "OPENTHREAD/20170716-00745-g0f2e87c; NRF52840; Aug 7 2018 19:35:08" "Daemon:Version" => "0.08.00d (; Feb 23 2018 13:17:33)" "Config:NCP:DriverName" => "spinel" "NCP:HardwareAddress" => [E42AA89D474105E4] "NCP:Channel" => 13 "Network:NodeType" => "leader" "Network:Name" => "nRF52840thread" "Network:XPANID" => 0xABCD1111ABCD1111 "Network:PANID" => 0x1122 "IPv6:MeshLocalAddress" => "fdab:cd11:11ab:0:4796:7c9d:a533:1199" "IPv6:MeshLocalPrefix" => "fdab:cd11:11ab::/64" "com.nestlabs.internal:Network:AllowingJoin" => false
We’ll use the above information to manually connect our Joiner node to this Thread network.
Setting up MQTT-SN on the Pi Border Router
The Nordic Pi image sets up the MQTT-SN gateway, but there’s an error in their config file. The UPDv6 broadcast address does not match with that used in the code in their SDK so make the following change in /etc/paho-mqtt-sn-gateway.conf.
GatewayUDP6Broadcast = ff03::1
Make sure you restart the service for your settings to take effect.
sudo service paho-mqttsn-gateway restart
Also, you’ll see the following in /etc/paho-mqtt-sn-gateway.conf:
BrokerName=198.41.30.241
That’s the IP address of the Eclipse MQTT broker.
Setting up the FTD
Install nRF5_SDK_for_Thread_and_Zigbee_v1.0.0, and build the code from the download link at the end of this article, and upload it to the nRF52840-DK. If you are just getting started with Nordic development, please a take a look at the official documentation for setting up the toolchain. We use Visual Studio Code for development, so you may find our blog article useful as well.
Once you upload the code, connect the FTD (nRF52840-DK) to you computer and open up a serial connection with 115200/8/N/1 setting. You will be using the OpenThread CLI below to connect to set it up.
Above, you can see that we’re connecting to the Thread network we started on the Pi Border Router. You need to match the network key, channel and PANID to be on the same Thread network. This is what the Thread Commissioning process does. We’re just doing it manually by directly passing this information to the node.
If you’ve done everything correctly, you’ll be able to ping to an external IP address – Google’s DNS server, in the above case.
Code on the Node
Now let’s take a look at some of the code on the FTD – the nRF52840-DK, that is. Our code is adapted from the MQTT-SN examples provided by Nordic.
Here’s the main loop:
int main(int argc, char *argv[]) { log_init(); scheduler_init(); timer_init(); leds_init(); thread_instance_init(); thread_bsp_init(); mqttsn_init(); while (true) { thread_process(); app_sched_execute(); if (NRF_LOG_PROCESS() == false) { thread_sleep(); } } }
Nordic Thread examples use the OpenThread libraries, as well as the Nordic app_scheduler, and if you’ve worked on their BLE API you will see that they have very similar calls here.
Here’s thread_instance_init:
static void thread_instance_init(void) { thread_configuration_t thread_configuration = { .role = RX_ON_WHEN_IDLE, .autocommissioning = true, }; thread_init(&thread_configuration); thread_cli_init(); thread_state_changed_callback_set(state_changed_callback); }
You can see above that Thread CLI is initialised, which is why you can talk to the device over serial and set it up.
The Nordic MQTT-SN code can be found under nRF5_SDK_for_Thread_and_Zigbee_v1.0.0/external/paho/mqtt-sn. The MQTT-SN calls (like much of web programming) work asynchronously. You make a call, pass in a callback handler, and only when you get the callback do you know whether you call succeeded.
Our device is both a subscriber and a publisher. It subscibes to the topic nRF52840/cmd and it publishes to the topic nRF52840/data.
So here’s the code flow:
- Call mqttsn_client_init and pass in the mqttsn_evt_handler
- On button press, start searching for an MQTT-SN gateway
- On button press, connect to gateway.
- On valid connection call mqttsn_client_topic_register to register topics.
- Subscribe to topic.
- Now you are ready to publish as well as receive subscribed topic events.
Item #4 above is a bit complicated.
static void regack_callback(mqttsn_event_t * p_event) { NRF_LOG_INFO("MQTT-SN event: Topic has been registered with ID: %d.\r\n", p_event->event_data.registered.packet.topic.topic_id); // register subscriber if not already registered if (!g_sub_registered) { m_topic_pub.topic_id = p_event->event_data.registered.packet.topic.topic_id; g_sub_registered = true; uint32_t err_code = mqttsn_client_topic_register(&m_client, m_topic_sub.p_topic_name, strlen(m_topic_name_sub), &m_msg_id); if (err_code != NRF_SUCCESS) { NRF_LOG_ERROR("REGISTER message could not be sent. Error code: 0x%x\r\n", err_code); } } else { // store id m_topic_sub.topic_id = p_event->event_data.registered.packet.topic.topic_id; // subscribe subscribe(); } }
As you can see above, due to the asynchronous nature of these calls, you need to wait for the first topic registration to succeed before issuing the next one.
Here’s the publish method.
static void publish(void) { char* pub_data = g_led_2_on ? "1" : "0"; uint32_t err_code = mqttsn_client_publish(&m_client, m_topic_pub.topic_id, (uint8_t*)pub_data, strlen(pub_data), &m_msg_id); if (err_code != NRF_SUCCESS) { NRF_LOG_ERROR("PUBLISH message could not be sent. Error code: 0x%x\r\n", err_code) } }
The above code sends the state of the LED 0/1 on the topic.
Here’s the callback for subscribe.
static void received_callback(mqttsn_event_t * p_event) { if (p_event->event_data.published.packet.topic.topic_id == m_topic_sub.topic_id) { uint8_t* p_data = p_event->event_data.published.p_payload; NRF_LOG_INFO("MQTT-SN event: Content to subscribed topic received.\r\n"); NRF_LOG_INFO("Topic id: %d, data: %5s", p_event->event_data.published.packet.topic.topic_id, p_data); // turn LEDs on/off if (p_data[0] == '1') { LEDS_ON(BSP_LED_2_MASK); g_led_2_on = true; } else { LEDS_OFF(BSP_LED_2_MASK); g_led_2_on = false; } } else { NRF_LOG_INFO("MQTT-SN event: Content to unsubscribed topic received. Dropping packet.\r\n"); } }
The data received on the subscribed topic is used to turn the LED on/off.
Testing
To test our Thread device, turn the DK on and wait for LED1 to stabilise. Then, press Button 2, which will start looking for an MQTT-SN gateway. When a valid gateway is found, LED 3 will light up. Now press Button 3, and our device will connect to the gateway. Upon successful connection, LED 4 will light up.
Note that the LED and Button numbering is off by one in the code. For example, LED_2 in the code is LED 3 on the DK.
Next, we need to set up an MQTT client on our computer. I used MQTTfx.
Configure client connection as follows:
Once connected, set up the Publish screen to publish to the nRF52840/cmd topic.
Next, set up the Subscribe screen to subscribe to the nRF52840/data topic.
Now, if you press Button 4 on the device, the Subscribe screen will print a message with the state of LED 3 (0/1). Similarly, you can set the state of the LED on the Publish screen to 0/1 and watch the LED on the device turn off and on.
Using NRF_LOG
Nordic nRF52840 with the built-in Segger chip can be used for logging information from you code – very useful for testing and debugging. To be able to do this, enable Segger RTT in you sdk_config.h file:
#define NRF_LOG_BACKEND_RTT_ENABLED 1
Then, in a terminal #1 (in OS X and Linux), type:
JLinkExe -autoconnect 1 -if SWD -speed 4000
And press Enter.
Now in terminal #2, type:
JLinkRTTClient
Now you’ll see that all outputs from NRF_LOG calls from your application will end up in terminal #2. Try this with our project and watch all the Thread and MQTT-SN logs appear on the screen.
Conclusion
So there you have it – an IoT application using Thread. Devices talking to each other is fine, but when you can get notifications from them and issue commands to them over the Internet, then things get interesting. This project is a good example of a low power wireless network with an “edge router” that connects devices to the external world via the Internet.
Downloads
You can download code for this project here:
https://gitlab.com/electronut/nRF52840-thread-intro/
Acknowledgements
I thank Tavish Naruka for our discussions and continuing experiments on nRF52840 and Thread.
About this Article
Title: Playing with Thread and MQTT-SN on Nordic nRF52840
Author: Mahesh Venkitachalam
Website: electronut.in
First Published: 11 Aug 2018
Revisions