Low power wireless: 6LoWPAN, IEEE802.15.4 and the Raspberry Pi

Resources

Introduction

The Internet of Things (IoT) is one of the new kids on the block. It promises connection of sensors and actuators to the internet, for data to flow both ways, and once in the internet to become part of new and exciting business systems, reaching up into the realms of big data and artifical intelligence.

IoT applications will rely on a large and complex system. One of the components in this will be the connections between sensors and actuators and the internet. This will most likely be wireless and will have to be low power: if you have a thousand sensors, they will most likely be running off batteries and you will want those batteries to last years, not days.

There are two directions that low power wireless is heading right now: personal area networks (LOWPAN) spanning upto 20 or 30 metres and wide area networking (LPWAN) of upto 20 or more kilometres. The technologies at the physical layer are completely different and lead to different Linux solutions. This article only deals with LOWPAN.

The physical layer for LOWPAN is specified by IEEE802.15.4. This defines communication using various wireless bands such as 2.4Ghz, with a range of about 10 metres and data transfer rates of 250kb/s - good enough for most sensors, but not good enough to stream MP3s!

On top of IEEE802.15.4 is a variety of protocols: Zigbee, Z-Wave, Thread, ... . Of these, only the IETF 6LoWPAN is an open standard, and this is where the Linux development community has settled. This article will only look at 6LoWPAN. We will also ignore other wireless systems such as BlueTooth LE.

6LoWPAN and Linux

6LoWPAN is IPv6 over IEEE802.15.4 wireless. That isn't easy: IPv6 is designed for the current internet while IEEE802.15.4 is designed for a diferent environment. We don't need to worry about how this mismatch has been overcome but it does mean you need to be aware that two different levels are dealt with here: getting two wireless devices to talk to each other and getting a networking layer talking over these devices.

The device layer is where physical hardware choices come into play. Linux supports several devices such as the AT86RF230 series, the MRF24J40 and several others. The kernel needs to have these device drivers compiled in or available as dynamically loadable modules.

The networking layer requires 6LoWPAN support. Again, the kernel needs to have this compiled in or available as modules. These modules are the ieee802154_6LoWPAN, ieee802154 and mac802154 modules.

6LoWPAN devices and the Raspberry Pi

The Raspberry Pi is a wonderful toy or a full blown Linux computer, depending on your viewpoint. With its GPIO pins it can act as a connection into the realm of sensors and actuators, while with ethernet (and on the RPi3, WiFi) can be a part of LANs and WANs. For the IoT it (and the Arduino) form an excellent bridge between the physical and ICT worlds. But there are now IEEE802.15.4 modules available and these can be used to turn an RPi into a "full function 6LoWPAN device".

I used the RPi with the OpenLabs "Raspberry Pi 802.15.4 radio". This is an Atmel AT86RF233 radio on a small board with a header which allows it to be plugged straight onto pins 15-26 of the RPi. It can be plugged in facing out or facing in: facing in the right way to do it.

I started off using the standard Raspbian distro (dated 27 May, 2016). This can be set up to recognise the radio, but - oh dear! - the 4.4 Linux kernel it uses has 6LoWPAN modules, but they don't work properly in that kernel. The IPv6 packets get corrupted even for ping'ing itself and so this Raspbian distro won't support 6LoWPAN.

The hunt is then on for a setup which allow the RPi to support 6LoWPAN with an AT86RF233 radio. This is painful: there are many helpful sites which are outdated or which I just couldn't get to work. I was finally pointed by Sebastian Meiling to his page Create a generic Raspbian image with 6LoWPAN support . In summary, what is needed is an upstream Linux kernel, 4.7 or 4.8, recent firmware and suitable configuration of the file /boot/config.txt. At the time of writing, these instructions only work for the RPi 1 and 2 - the RPi 3 isn't working yet, but may be by the time this appears.

Installing a 6LoWPAN kernel

I'm only going to talk about the OpenLabs module on the RPi 2B. For other modules and RPi's see Sebastian's page. I'm going to assume a reasonable amount of Linux savvy, in installing software and building from source.

Start by installing the latest Raspbian image. If that runs a 4.7 (or later) kernel you may be okay already, otherwise you have to build and install an upstream 4.7 kernel. You will probably need extra tools for this, such as rpi-update, git, libncurses5-dev, bc and maybe development tools which you can install using apt-get.

Before you do anything else, make sure your system is up-to-date by running

	rpi-update
      
This will install the latest firmware bootloader.

Download a 4.7 kernel into the directory linux-rpi2 by

	git clone --depth 1 https://github.com/raspberrypi/linux.git \
	          --branch rpi-4.7.y --single-branch linux-rpi2
      

Building a kernel means compiling a lot of files and is very slow on the RPi. Most people recommend cross-compiling. That's more complex and I like things simple. So I prefer to build on the RPi itself. It only takes about 5 hours, so start it up and either go to bed or go out, listen to some jazz and stay out late.

In the linux-rpi2 directory set up a configuration file for the RPi 2B by

	make bcm2709_defconfig
      
Then run menuconfig to do two things:

Build the kernel and associated files by

	make zImage modules dtbs -j4
      
Five hours later, install the modules and dtbs files:
	sudo make modules_install dtbs_install
      

The safest way to install the kernel is to copy it to an appropriate location. When I run make kernelversion in the source tree, it tells me I have built 4.7.2. So I use that number in copying the kernel

	sudo cp arch/arm/boot/zImage /boot/kernel.4.7.2.img
      
That way I don't destroy any existing images so I have a safe fallback to the previous system.

Finally, we need to tell the RPi to boot into the new kernel. As root, edit /boot/config.txt and add these lines at the end:

	kernel=kernel.4.7.2.img
	device_tree=bcm2709-rpi-2-b.dtb
	dtoverlay=at86rf233
      
What does that do? Firstly it tells the RPi to use the new boot image kernel.4.7.2.img. Secondly - and this is currently ARM specific - it tells the RPi to pick up hardware default values using the device tree system from bcm2709-rpi-2-b.dtb. And thirdly - and this is RPi specific - it says to add in the at86rf233 device in an additional file to the device tree file.

Finally...reboot. If all went well, you should have the new kernel running. Check this by

	uname -a
      
It should show something like
	Linux raspberrypi 4.7.2-v7+ #1 SMP Fri Aug 26 15:45:29 UTC 2016 armv7l GNU/Linux
      
If it didn't boot or showed the wrong kernel, take your SD card back to somewhere else so you can comment out the lines you added to /boot/config.txt. Back on the RPi reboot back into the default kernel and try to figure out which step went wrong. I skipped some steps from Sebastian's guide because I didn't need them, but if your system isn't working then pay very close attention to his guide: he seems to be pretty diligent about updating it.

Setting up 6LoWPAN

Are we there yet? Sorry, no. we've built and installed an upstream kernel with 6LoWPAN support. We're over half-way there, though. To configure the 6LoWPAN stack, we need another tool, wpan-tools. Get this off GitHub:

	git clone --depth 1 https://github.com/linux-wpan/wpan-tools.git wpan-tools
      
Before we can build this though, we need autoreconf:
	sudo apt-get install dh-autoreconf
      
Then in the wpan-tools directory you can run
	./autogen.sh
	./configure CFLAGS='-g -O0' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib
	make
	sudo make install
      
What's going on here? Linux is part of the Unix family of operating systems (including BSD, among many others). They all have quirks, and source code authors have to deal with those. There have been many tools to make this management easier, and wpan-tools uses autoreconf to build a configuration file, then configure to work out the specifics of your RPi system so that when you make your application all of the correct pieces are in place.

The result of this is the application iwpan is now in the /usr/bin directory for use.

We are nearly there! Remember in the kernel configuration we set the 6LoWPAN and device drivers to be dynamic modules. They won't have been installed by default like you would expect modules to be. That's what all this device tree stuff is about, bringing devices into the system when it can't detect them normally. So the next step is to load the modules:

	sudo modprobe at86rf230
      
Then lsmod should include something like
	Module                  Size  Used by
	ieee802154_6LoWPAN     19335  0 
	6LoWPAN                13191  8 nhc_fragment,ieee802154_6LoWPAN
	at86rf230              22211  0 
	mac802154              49035  1 at86rf230
	ieee802154             55698  2 ieee802154_6LoWPAN,mac802154
	crc_ccitt               1278  1 mac802154
      

And now - TaDah! - iwpan list shows something like

	wpan_phy phy0
	supported channels:
	page 0: 11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26 
	current_page: 0
	current_channel: 13,  2415 MHz
	cca_mode: (1) Energy above threshold
	cca_ed_level: -77
	tx_power: 4
	capabilities:
	iftypes: node,monitor 
	channels:
	page 0: 
	[11]  2405 MHz, [12]  2410 MHz, [13]  2415 MHz, 
	[14]  2420 MHz, [15]  2425 MHz, [16]  2430 MHz, 
	[17]  2435 MHz, [18]  2440 MHz, [19]  2445 MHz, 
	[20]  2450 MHz, [21]  2455 MHz, [22]  2460 MHz, 
	[23]  2465 MHz, [24]  2470 MHz, [25]  2475 MHz, 
	[26]  2480 MHz  
	tx_powers: 4,3.7,3.4,3,2.5,2,1,0,-1,-2,-3,-4,-6,-8,-12,-17 
	cca_ed_levels: -91,-89,-87,-85,-83,-81,-79,-77,-75,-73,-71,-69,-67,-65,-63,-61 
	cca_modes: 
	(1) Energy above threshold
	(2) Carrier sense only
	(3, cca_opt: 0) Carrier sense with energy above threshold (logical operator is 'and')
	(3, cca_opt: 1) Carrier sense with energy above threshold (logical operator is 'or')
	min_be: 0,1,2,3,4,5,6,7,8 
	max_be: 3,4,5,6,7,8 
	csma_backoffs: 0,1,2,3,4,5 
	frame_retries: 0,1,2,3,4,5,6,7 
	lbt: false
	Supported commands:
        ...
      
Your 6LoWPAN device is now known to the Linux system.

Note taht you won't have to go through those steps each time. Once you have got it set up and tested as shown, on reboots the modules will be isntalled automaticaly.

Configuring 6LoWPAN

So now we have a new kernel, we have the at86rf230 device recognised and the 6LoWPAN networking stack is in place. The final steps are to configure networking and bring the device up. You will be used to WiFi networks having an SSID. IEEE802.15.4 networks have a similar concept, a PAN ID. Two devices will only be on the same network if they have the same PAN ID. We use iwpan to set this:

	iwpan dev wpan0 set pan_id 0xbeef
      
The id of 0xbeef isn't fixed, just every example seems to use it!

Then we bring up the interface using normal networking tools:

	ip link add link wpan0 name lowpan0 type lowpan
	ifconfig wpan0 up
	ifconfig lowpan0 up
      

What have we got now? ifconfig tells us something like

	lowpan0   Link encap:UNSPEC  HWaddr EE-0B-FB-0F-76-B9-F3-93-00-00-00-00-00-00-00-00  
        inet6 addr: fe80::ec0b:fb0f:76b9:f393/64 Scope:Link
        UP BROADCAST RUNNING MULTICAST  MTU:1280  Metric:1
        RX packets:38 errors:0 dropped:0 overruns:0 frame:0
        TX packets:39 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1 
        RX bytes:5205 (5.0 KiB)  TX bytes:5205 (5.0 KiB)

	wpan0     Link encap:UNSPEC  HWaddr EE-0B-FB-0F-76-B9-F3-93-00-00-00-00-00-00-00-00  
        UP BROADCAST RUNNING NOARP  MTU:123  Metric:1
        RX packets:58 errors:0 dropped:0 overruns:0 frame:0
        TX packets:55 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:300 
        RX bytes:4111 (4.0 KiB)  TX bytes:4904 (4.7 KiB)
      
The interface wpan0 is the wireless device, The interface lowpan0 is the 6LoWPAN network device, just like eth0, the loopback device, etc. Note how it has an IPv6 address, but no IPv4 address - this is next generation IP only!

Ping!

We are done! Well, almost. There is an old B.C. cartoon where one character invents the telephone. "Who do we ring?" asks his friend. "I only invented one" is the reply. We need someone to talk to. So do all this over again with another RPi. You did buy two RPi's and two wireless modules, didn't you?

The ifconfig command tells us the IPv6 address of the 6LoWPAN device. From the other device, once you have it set up

	ping6 -I lowpan0 fe80::ec0b:fb0f:76b9:f393 # IPv6 address of the first
	device
      
or
	ping6  fe80::ec0b:fb0f:76b9:f393%lowpan0
      
The command ping6 is the IPv6 version of ping. The IPv6 address of each network interface is automatically asssigned, and is a link local address. If you have multiple interfaces each of them can be on a network segment with non-routable link local addresses. Hosts on these different network segments can have the same address. These are like IPv4 link local addresses 169.254.0.0/16, and can't be routed across different network segments. So in Linux you need to specify the interface to use (lowpan0), to avoid possible confusion. There are two ways: either use the -I lowpan0 option or append %lowpan0 to the IPv6 address.

On my system this produces

	$ping6 -I lowpan0 fe80::ec0b:fb0f:76b9:f393
	PING fe80::ec0b:fb0f:76b9:f393(fe80::ec0b:fb0f:76b9:f393) from fe80::f0f9:a4ed:3cad:d1de lowpan0: 56 data bytes
	64 bytes from fe80::ec0b:fb0f:76b9:f393: icmp_seq=1 ttl=64 time=11.6 ms
	64 bytes from fe80::ec0b:fb0f:76b9:f393: icmp_seq=2 ttl=64 time=11.1 ms
	64 bytes from fe80::ec0b:fb0f:76b9:f393: icmp_seq=3 ttl=64 time=10.5 ms
      
Success! The two devices can ping each other across 6LoWPAN.

What if it doesn't work? Well, it didn't work for me for a long time, and working out where the failure occurrred was painful. It turned out to be a wrong kernel for 6LoWPAN. First, keep running ifconfig. This tells you which interfaces are getting and sending packets. It told me that the wireless layer (wpan0) was getting and receiving packets, but the networking layer wasn't. Then I ran wireshark using selector ip6 on packets and it showed me errors at the network layer. The command dmesg gave gold, telling me the IPv6 packets were corrupted, even when pinging myself.

In desparation I turned to Sebastian, giving him as much information as I could (uname, firmare version using /opt/vc/bin/vcgencmd, contents of /boot/config.txt, decompiling the device tree using dtc -I fs /proc/device-tree, and then wireshark and dmesg reports). He only needed the first line: wrong kernel. But spending time working out a detailed report at least shows you are serious: "Duh, it doesn't work" isn't helpful to a maintainer!

A sensor and a receiver (using TCP)

We don't really need 6LoWPAN to communicate between Raspberry Pi's. WiFi and ethernet are better. But now suppose one of them is a sensor running off a battery or solar panel. WiFi is estimated to drain a battery within a fortnight whereas 6LoWPAN on batteries can be expected to run for several years. We are simulating this here by using one of the RPi's as sensor for convenience.

We will need to set up a client-server system. Usually, we think of servers as big grunty machines somewhere, but in the IoT world, the sensors will be the servers, handling requests for values from clients elsewhere in the network.

The server is just like a normal IPv6 server as described in the Python documentation 18.1. socket — Low-level networking interface But note that just as in the ping6 command above, we need to specify the network interface to be used. This means we have to use Python 3 rather than Python 2 as this has the socket function socket.if_nametoindex() which allows us to specify the IPv6 "scope id" which is the interface we use.

We don't want to complicate this article with how to add sensors to an RPi. Instead, we will just measure the temperature of the RPi's CPU, as this can be found really easily by running this command from a shell:

	vcgencmd measure_temp	
      
This will return a string like
	temp=36.9'C
      
Within Python we create a process to run this command using Popen and read from the stdout pipeline.

An IPv6 TCP server that waits for connections, sends the temperature and then closes the connection is temp_server.py:

#!/usr/bin/python3

import socket
from subprocess import PIPE, Popen

HOST = ''       # Symbolic name meaning all available interfaces
PORT = 2016     # Arbitrary non-privileged port

def get_cpu_temperature():
    process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE)
    output, _error = process.communicate()
    return output

def main():
    s6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
    scope_id = socket.if_nametoindex('lowpan0')
    s6.bind((HOST, PORT, 0, scope_id))
    s6.listen(1)
    
    while True:
        conn, addr = s6.accept()
        conn.send(get_cpu_temperature())
        conn.close()

if __name__ == '__main__':
    main()
	
       

A client that opens a connection and reads the temperature every 10 seconds is temp_client.py:

#!/usr/bin/python3

import socket
import time

ADDR = 'fd28:e5e1:86:0:e40c:932d:df85:4be9' # the other RPi
PORT = 2016

def main():
    # scope_id = socket.if_nametoindex('lowpan0')
    while True:
        s6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
        s6.connect((ADDR, PORT, 0, 0))
        data = s6.recv(1024)
        print(data.decode('utf-8'), end='')

        # get it again after 10 seconds
        time.sleep(10)

if __name__ == '__main__':
    main()
	
      

The output looks like this

temp=37.4'C
temp=37.4'C
temp=37.9'C
      

A sensor and a receiver (using UDP)

The point of 6LoWPAN is that it is designed for low-power systems. I gave an example of a TCP client and server above because TCP is probably more familiar than UDP prgoramming. But TCP doesn't meet the criteria of low-power: it has a handshake to setup; messages received are confirmed, and if not confirmed are re-sent. That is all in the name of getting reliable, in-order transmission.

UDP on the other hand is send-and-foget. One packet instead of many. It is far more suited to a low power environment.

An IPv6 TCP server that waits for connections, sends the temperature and then closes the connection is temp_serverUDP.py:

#!/usr/bin/python

import socket
from subprocess import PIPE, Popen
import time

HOST = ''       # Symbolic name meaning all available interfaces
PORT = 2016     # Arbitrary non-privileged port

def get_cpu_temperature():
        process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE)
        output, _error = process.communicate()
        return output

            
def main():
        s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 0)
        scope_id = socket.if_nametoindex('lowpan0')
        s6.bind((HOST, PORT, 0, scope_id))
        
        while True:
                data, addr = s6.recvfrom(1024)
                s6.sendto(get_cpu_temperature(), addr)
        
        
if __name__ == '__main__':
    main()
	
       

A client that opens a connection and reads the temperature every 10 seconds is temp_clientUDP.py:

#!/usr/bin/python

import socket
from subprocess import PIPE, Popen
import time

ADDR = '::1' # localhost
PORT = 2016

def get_cpu_temperature():
    process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE)
    output, _error = process.communicate()
    return output

def main():
    scope_id = socket.if_nametoindex('lowpan0')
    while True:
        s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 0)
        s6.sendto(b" ", (ADDR, PORT, 0, scope_id))
        data = s6.recv(1024)
        print(data.decode())
        time.sleep(10)

if __name__ == '__main__':
    main()
	
      

What's my server's address?

So imagine we've now got 1,000 of these sensors scattered out in the wild somewhere and they are all running IPv6 servers. What are their addresses? How do we talk to them? Unfortunately, the OpenLabs module generates a new MAC address each time it is booted so it generates a new IPv6 address each time. Running multi-cast discovery is not recommended for these low power networks as it is a power drain. We will cheat a bit in the next article, but show better ways in the third article.

Conclusion

The scenario presented in the last section is still a bit unrealistic: if you have enough power to drive an RPi as a sensor then you probably have enough power for it to use WiFi or ethernet. But soon there will be genuine low power sensors using 6LoWPAN, and this article has shown you how to bring these into one particular Linux system. It's been pretty heavy going, but right now this is cutting edge stuff, so expect to bleed a bit!

In the next article we will look at how to bring a 6LoWPAN network into the standard IPv6 world and in the third article look at CoAP, the equivalent of HTTP for low power networks.