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 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.
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.
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-updateThis 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_defconfigThen run
menuconfig
to do two things:
Device Drivers --> Network device support --> IEEE 802.15.4 drivers
Networking support --> Networking options --> IEEE Std 802.15.4 Low-Rate Wireless Personal Area Networks support
Build the kernel and associated files by
make zImage modules dtbs -j4Five 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.imgThat 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=at86rf233What 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 -aIt should show something like
Linux raspberrypi 4.7.2-v7+ #1 SMP Fri Aug 26 15:45:29 UTC 2016 armv7l GNU/LinuxIf 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.
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-toolsBefore we can build this though, we need
autoreconf
:
sudo apt-get install dh-autoreconfThen in the
wpan-tools
directory you can run
./autogen.sh ./configure CFLAGS='-g -O0' --prefix=/usr --sysconfdir=/etc --libdir=/usr/lib make sudo make installWhat'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 at86rf230Then
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.
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 0xbeefThe 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!
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 deviceor
ping6 fe80::ec0b:fb0f:76b9:f393%lowpan0The 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 msSuccess! 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!
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_tempThis will return a string like
temp=36.9'CWithin 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
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()
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.
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.