In the previous section we covered some of the core features of Contiki, basics of sensors and a general overview of how the applications are built, programmed and simulated in Contiki. This section introduces the wireless communication, details about radios, and in general how to configure our platforms.
The very first step is understanding how our platform is configured.
Each platform implements its own set of default values and configurations, to be used by underlying modules like the radio, the serial port, etc.
The places to visit for the Zolertia platform are the following:
-
Specific hardware settings: parameters such as the default I2C pins, ADC channels, module-specific pin assignment and platform information can be found at
platform/zoul/remote/dev/board.h
andplatform/z1/platform-conf.h
. -
Specific Contiki settings: UART settings, MAC driver, radio channel, IPv6, RIME and network buffer configuration, among others, can be found at
platform/zoul/contiki-conf.h
andplatform/z1/contiki-conf.h
.
As a general good practice, user configurable parameters are normally allowed to be overridden by the applications, this also serves as a guideline to discern which values can be changed by the casual user, from those meant to be changed only if you really know what you are doing. Below is an example:
#ifndef UART0_CONF_BAUD_RATE
#define UART0_CONF_BAUD_RATE 115200
#endif
By defining UART0_CONF_BAUD_RATE
in our application’s project-conf.h
, we can change the default 115200 baud rate. Notice that generally it is a good practice to add CONF
to the user configurable parameters.
Tip
|
One of the most used tools is probably For windows Astrogrep is a good option. |
In the next section we will review the most notable parameters to configure, but as usual depending on your application and setup, the best way to ensure everything is properly set is by reviewing the specific platform configuration files, and modify or redefine accordingly.
To start working you must first define the Node ID of each node, this will be used to generate the mote’s MAC address and the IPv6 addresses (link-local and global).
The RE-Mote
platform comes with two pre-saved MAC addresses stored in its internal flash memory, but the user can instead choose a hardcoded one. The following switches at platform/zoul/contiki-conf.h
selects the chosen one.
/**
* \name IEEE address configuration
*
* Used to generate our RIME & IPv6 address
* @{
*/
/**
* \brief Location of the IEEE address
* 0 => Read from InfoPage,
* 1 => Use a hardcoded address, configured by IEEE_ADDR_CONF_ADDRESS
*/
#ifndef IEEE_ADDR_CONF_HARDCODED
#define IEEE_ADDR_CONF_HARDCODED 0
#endif
/**
* \brief Location of the IEEE address in the InfoPage when
* IEEE_ADDR_CONF_HARDCODED is defined as 0
* 0 => Use the primary address location
* 1 => Use the secondary address location
*/
#ifndef IEEE_ADDR_CONF_USE_SECONDARY_LOCATION
#define IEEE_ADDR_CONF_USE_SECONDARY_LOCATION 0
#endif
If using your own hardcoded address, the following define can be overridden by the application:
#ifndef IEEE_ADDR_CONF_ADDRESS
#define IEEE_ADDR_CONF_ADDRESS { 0x00, 0x12, 0x4B, 0x00, 0x89, 0xAB, 0xCD, 0xEF }
#endif
Let’s use the ID from the mote list:
Reference Device Description
--------------------------------------------------
Z1RC3301 /dev/ttyUSB0 Silicon Labs Zolertia Z1
The node ID should be 3301
(decimal) if no previously saved node ID is found in the flash memory.
Let’s see how Contiki uses this to derive a full IPv6 and MAC address. At platforms/z1/contiki-z1-main.c
#ifdef SERIALNUM
if(!node_id) {
PRINTF("Node id is not set, using Z1 product ID\n");
node_id = SERIALNUM;
}
#endif
node_mac[0] = 0xc1; /* Hardcoded for Z1 */
node_mac[1] = 0x0c; /* Hardcoded for Revision C */
node_mac[2] = 0x00; /* Hardcoded to arbitrary even number so that the 802.15.4 MAC address is compatible with an Ethernet MAC address - byte 0 (byte 2 in the DS ID) */
node_mac[3] = 0x00; /* Hardcoded */
node_mac[4] = 0x00; /* Hardcoded */
node_mac[5] = 0x00; /* Hardcoded */
node_mac[6] = node_id >> 8;
node_mac[7] = node_id & 0xff;
}
So the mote should have the following addresses:
MAC c1:0c:00:00:00:00:0c:e5
Node id is set to 3301.
Tentative link-local IPv6 address fe80:0000:0000:0000:c30c:0000:0000:0ce5
Where 0xce5
is the hex value corresponding to 3301
. The global address is only set when an IPv6 prefix is assigned (by now you should know this from earlier sections).
If instead you wish to have your own addressing scheme, you can edit the node_mac values at contiki-z1-main.c
file. If you wish to replace the node id value obtained from the product id, you need to store a new one in the flash memory, luckily there is already an application to do so:
Go to examples/zolertia/z1
location and replace the 158
for your own required value:
make clean && make burn-nodeid.upload nodeid=158 nodemac=158 && make z1-reset && make login
You should see the following:
MAC c1:0c:00:00:00:00:0c:e5 Ref ID: 3301
Contiki-2.6-1803-g03f57ae started. Node id is set to 3301.
CSMA ContikiMAC, channel check rate 8 Hz, radio channel 26
Tentative link-local IPv6 address fe80:0000:0000:0000:c30c:0000:0000:0ce5
Starting 'Burn node id'
Burning node id 158
Restored node id 158
As you can see, now the node ID has been changed to 158, when you restart the mote you should see that the changes have been applied:
MAC c1:0c:00:00:00:00:00:9e Ref ID: 3301
Contiki-2.6-1803-g03f57ae started. Node id is set to 158.
CSMA ContikiMAC, channel check rate 8 Hz, radio channel 26
Tentative link-local IPv6 address fe80:0000:0000:0000:c30c:0000:0000:009e
The bandwidth and allowed channels depend on the operating frequency band. They will be determined by the spectrum regulation agency of the country, along with maximum transmitted power allowable. .The IEEE 802.15.4 standard
The IEEE 802.15.4 is a standard for wireless communication, it specifies the physical and media access control layers for low-rate wireless personal area networks (LR-WPANs).
The standard specifies the use of the 868-868.8 MHz (in Europe and many other countries), the 902-928 MHz (in United States, Canada, and some Latin America countries), or the world-wide 2.400-2.4835 GHz band part of the Industrial Scientific and Medical applications (ISM).
In practice the 2.4 GHz band is being heavily used due to its world-wide availability. The ZigBee proprietary protocol by the ZigBee alliance was one of the early adopters of IEEE 802.15.4. It did so leveraging the physical and MAC layer of IEEE 802.15.4, specifying on top additional routing and networking functionality to build mesh networks.
Quite recently the Thread Group has proposed its own simplified IPv6-based mesh networking protocol for connecting products around the home to each other, to the Internet and to the cloud.
As the 2.4 GHz band is also used by other technologies like WiFi and Bluetooth, this spectrum is shared and overlaps might occur. The Figure below shows the channel allocation of the 2.4 GHz IEEE 802.15.4, and the recommended channels to avoid interferences with other co-located devices. With the rise of the Bluetooth Low Energy, and the ubiquitous WiFi present in our lives, the selection of a proper operating channel is crucial in any deployment.
The default channel of the RE-Mote is defined as follows:
#ifndef CC2538_RF_CONF_CHANNEL
#define CC2538_RF_CONF_CHANNEL 26
#endif /* CC2538_RF_CONF_CHANNEL */
The Z1 mote defines its default channel as:
#ifdef RF_CHANNEL
#define CC2420_CONF_CHANNEL RF_CHANNEL
#endif
#ifndef CC2420_CONF_CHANNEL
#define CC2420_CONF_CHANNEL 26
#endif /* CC2420_CONF_CHANNEL */
The radio channel can be defined from the application’s project-conf.h
or the Makefile
.
The radio drivers in Contiki are implemented to comply with the struct radio_driver
in core/dev/radio.h
. This abstraction allows to interact with the radio using a standardized API, independently of the radio hardware. The functions to set and read radio parameters are explained below.
/** Get a radio parameter value. */
radio_result_t (* get_value)(radio_param_t param, radio_value_t *value);
/** Set a radio parameter value. */
radio_result_t (* set_value)(radio_param_t param, radio_value_t value);
To change the channel from the application use RADIO_PARAM_CHANNEL
as follows:
rd = NETSTACK_RADIO.set_value(RADIO_PARAM_CHANNEL, value);
Where value
can be any value from 11 to 26, and rd will be either RADIO_RESULT_INVALID_VALUE
or RADIO_RESULT_OK
.
The RE-Mote has a dual 2.4 GHz and 863-950 MHz RF interface, which can be selected alternatively, or used simultaneously (the latter currently not supported in Contiki at the moment).
As default the RE-Mote uses the IEEE 802.15.4g mandatory mode for the 868 MHz band, configured for 2-GFSK modulation, 50 kbps data rate and with 33 channels available.
The RE-Mote uses the Texas Instruments CC1200 RF transceiver, referred to in Contiki as dev/cc1200
. The default configuration file is located in dev/cc1200/cc1200-802154g-863-870-fsk-50kbps.c
.
To change channels from the application we use the RF API:
rd = NETSTACK_RADIO.set_value(RADIO_PARAM_CHANNEL, value);
Where value
can be any value from 11 to 26, and rd it will be either RADIO_RESULT_INVALID_VALUE
or RADIO_RESULT_OK
.
The radio frequency power transmission is that at the output of the transmitter before reaching the antenna. The higher the transmission power the higher the wireless range, but the power consumption usually increases as well. The range is heavily dependent on the frequency, the antenna used and its height above the ground.
The RE-Mote platform uses the CC2538 built-in 2.4 GHz radio. As default the transmission power is set to 3 dBm (2 mW) in the cpu/cc2538/cc2538-rf.h
header as shown below.
CC2538_RF_TX_POWER_RECOMMENDED 0xD5
This recommended value is taken from the SmartRF Studio.
Other values and its corresponding output power levels are shown in the next table.
TX Power (dBm) | Value |
---|---|
+7 |
0xFF |
+5 |
0xED |
+3 |
0xD5 |
+1 |
0xC5 |
0 |
0xB6 |
-1 |
0xB0 |
-3 |
0xA1 |
-5 |
0x91 |
-7 |
0x88 |
-9 |
0x72 |
-11 |
0x62 |
-13 |
0x58 |
-15 |
0x42 |
-24 |
0x00 |
Tip
|
As illogical as it may sound, there might be some reasons to reduce transmission power:
|
The current consumption can go from 24 mA to 34 mA when changing the transmission power from 0 dBm to 7 dBm (AN125).
The Z1 mote uses the Texas Instrument CC2420 RF transceiver. As default the transmission power is set to 0 dBm (1 mW), which is the maximum allowed by the radio.
The available output power and its corresponding configuration values are listed in the table below, as well as the current consumption at each level.
TX Power (dBm) | Value | mA |
---|---|---|
0 |
31 |
17.4 |
-1 |
27 |
16.5 |
-3 |
23 |
15.2 |
-5 |
19 |
13.9 |
-7 |
15 |
12.5 |
-10 |
11 |
11.2 |
-15 |
7 |
9.9 |
-25 |
3 |
8.5 |
For both platforms the transmission power can be changed with:
rd = NETSTACK_RADIO.set_value(RADIO_PARAM_TXPOWER, value);
Where value
can be any value from the above table, and rd it will be either RADIO_RESULT_INVALID_VALUE
or RADIO_RESULT_OK
.
As mentioned earlier, the RE-Mote has an on-board sub-1 GHz interface based on the CC1120 radio transceiver, configured to operate in the 863-950 MHz bands. The maximum transmission power allowed depends on the specific band and regulations in place, which can also impose limits on the maximum antenna gain, be sure to check the local regulations before changing the output power.
The regulations are country specific and out of the scope of this section.
The following values are taken from the SmartRF Studio, using default IEEE 802.15.4g ETSI compliant configuration.
TX Power (dBm) | Value |
---|---|
+14 |
0x7F |
+13 |
0x7C |
+12 |
0x7A |
+11 |
0x78 |
+8 |
0x71 |
+6 |
0x6C |
+4 |
0x68 |
+3 |
0x66 |
+2 |
0x63 |
+1 |
0x61 |
0 |
0x5F |
-3 |
0x58 |
-6 |
0x51 |
-11 |
0x46 |
-24 |
0x42 |
-40 |
0x41 |
These values correspond to the CC1200_PA_CFG1
register.
As default the CC1200 driver in Contiki starts with the maximum transmission power, defined as follows:
/* The maximum output power in dBm */
#define RF_CFG_MAX_TXPOWER CC1200_CONST_TX_POWER_MAX
The minimum and maximum allowed values are set in dev/cc1200/cc1200-const.h
as shown below.
/* Output power in dBm */
/* Up to now we don't handle the special power levels PA_POWER_RAMP < 3, hence
* the minimum tx power is -16. See update_txpower().
*/
#define CC1200_CONST_TX_POWER_MIN (-16)
/*
* Maximum output power will probably depend on the band we use due to
* regulation issues
*/
#define CC1200_CONST_TX_POWER_MAX 14
The CC1200 driver calculates the proper CC1200_PA_CFG1
register value, so we need to pass as value
argument the required transmission power.
rd = NETSTACK_RADIO.set_value(RADIO_PARAM_TXPOWER, value);
Where value
can be any value from -14 to 16, and rd will be either RADIO_RESULT_INVALID_VALUE
or RADIO_RESULT_OK
.
Due to the changing environment conditions that normally affect the wireless systems, such as rain, interferences, obstacles, reflections, etc., measuring the wireless medium and links quality is important.
Checking the wireless medium should be done in three stages: before deploying your network, at deployment phase and later at network runtime, to ensure that the nodes create and select the best available routes.
Link Quality Estimation is an integral part of assuring reliability in wireless networks. Various link estimation metrics have been proposed to effectively measure the quality of wireless links.
The ETX metric, or expected transmission count, is a measure of the quality of a path between two nodes in a wireless packet data network. ETX is the number of expected transmissions of a packet necessary for it to be received without error at its destination. This number varies from one to infinity. An ETX of one indicates a perfect transmission medium, where an ETX of infinity represents a completely non-functional link. Note that ETX is an expected transmission count for a future event, as opposed to an actual count of a past events. It is hence a real number, generally not an integer.
ETX can be used as the routing metric. Routes with a lower metric are preferred. In a route that includes multiple hops, the metric is the sum of the ETX of the individual hops.
Below we describe how to read the LQI and RSSI to have a first approximation of the link conditions.
RSSI (Received Signal Strenght Indicator) is a generic radio receiver technology metric used internally in a wireless networking device to determine the amount of radio energy received in a given channel. The end-user will likely observe an RSSI value when measuring the signal strength of a wireless network through the use of a wireless network monitoring tool like Wireshark, Kismet or Inssider.
The image below shows how the Packet Reception Rate (PRR) dramatically decreases as the CC2420 RSSI values worsen.
There is no standardized relationship of any particular physical parameter to the RSSI reading, Vendors and chipset makers provide their own accuracy, granularity, and range for the actual power (measured in mW or dBm) and the corresponding RSSI values.
There are 2 different types of RSSI readings available:
-
The first one is an indication of the amount of power present in the wireless medium at the given frequency and at given time. In the absence of any packet in the air, this will be the noise floor. This measurement is also used to decide if the medium is free, and available to send a packet. A high value could be due to interference or to the presence of a packet in the air.
-
The second measurement is performed only after a packet has been correctly decoded, and gives the strength of the packet received from a specific node.
The first measurement can be read using the radio API as follows:
rd = NETSTACK_RADIO.get_value(RADIO_PARAM_RSSI, value);
Where value
is a variable passed as a pointer to store the RSSI value, and rd it will be either RADIO_RESULT_INVALID_VALUE
or RADIO_RESULT_OK
.
To read the RSSI value of a correctly decoded received packet, at the receive
callback:
packetbuf_attr(PACKETBUF_ATTR_RSSI);
More information about the packetbuf
attributes is available in core/net/packetbuf.h
.
For the CC2420 radio frequency transceiver on the Z1 mote, the RSSI can range from 0 to -100, values close to 0 mean good links while values close to -100 are indicators of a bad link, which could be due to multiple factors such as distance, environment, obstacles, interferences, etc.
LQI (Link Quality Indicator) is a digital value often provide by Chipset vendors as an indicator of how well a signal is demodulated, in terms of the strength and quality of the received packet, thus indicating a good or bad wireless medium.
The example below shows how the Packet Reception Rate decreases as the LQI decreases.
To read the LQI value we use the Radio API:
rd = NETSTACK_RADIO.get_value(PACKETBUF_ATTR_LINK_QUALITY, value);
Where value
is a variable passed as a pointer to store the LQI value, and rd it will be either RADIO_RESULT_INVALID_VALUE
or RADIO_RESULT_OK
.
The CC2420 radio used by the Z1 mote typically ranges from 110 (indicates a maximum quality frame) to 50 (typically the lowest quality frames detectable by the transceiver).
Detailed information about the CC2538 LQI calculation is found in the CC2538 user guide.
Medium Access Control (MAC) protocols describe the medium access adopted in a network, by establishing the rules that specify when a given node is allowed to transmit packets.
Protocols can be classified as contention-based or reservation-based protocols.
The first are based on Carrier Sensing for detecting medium activity and are prone to collisions and lower efficiency at heavy loads, but are easy to implement. The second group is efficient in terms of throughput and energy, but require precise synchronization and is less adaptable to dynamic traffic.
The medium access implementation in Contiki has 3 different layers: Framer, Radio Duty-Cycle (RDC) and Medium Access Control (MAC).
The network layer can be accessed through the global variables NETSTACK_FRAMER
, NETSTACK_RDC
and NETSTACK_MAC
, which are defined at compilation time.
The variables are located in core/net/netstack.h
, and can be defined by each platform as default and overridden by applications.
Contiki provides two MAC drivers: CSMA and NullMAC
CSMA (Carrier-Sense Multiple Access) receives incoming packets from the RDC layer and uses the RDC layer to transmit packets. If the RDC layer or the radio layer detects that the medium is busy, the MAC layer may retransmit the packet at a later point in time. CSMA protocol keeps a list of packets sent to each of the neighbors and calculate statistics such as number of retransmissions, collisions, deferrals, etc. The medium access check is performed by the RDC driver.
NullMAC is a simple pass-through protocol. It calls the appropriate RDC functions.
As default both Z1 mote and RE-Mote uses the CSMA driver.
#ifndef NETSTACK_CONF_MAC
#define NETSTACK_CONF_MAC csma_driver
#endif
Alternatively, a user can choose NullMAC as follow:
#define NETSTACK_CONF_MAC nullmac_driver
Radio Duty-Cycle (RDC) layer handles the sleep period of nodes. This layer decides when packets will be transmitted and ensures that nodes are awake when packets are to be received.
The implementation of Contiki’s RDC protocols are available in core/net/mac
. The following RDC drivers are implemented: contikimac
, xmac
, lpp
, nullrdc
and sicslowmac
. The implementation and details of the aforementioned RDC drivers are out of the scope of this chapter. The most commonly used is ContikiMAC. NullRDC is a pass-through layer that never switches the radio off.
#ifndef NETSTACK_CONF_RDC
#define NETSTACK_CONF_RDC contikimac_driver
#endif
RDC drivers try to keep the radio off as much as possible, periodically checking the wireless medium for radio activity. When activity is detected, the radio is kept on to check if it has to receive the packet, or it can go back to sleep.
The channel check rate is given in Hz, specifying the number of times the channel is checked per second, and the default channel check rate is 8 Hz. Channel check rates are given in powers of two and typical settings are 2, 4, 8, and 16 Hz.
#ifndef NETSTACK_CONF_RDC_CHANNEL_CHECK_RATE
#define NETSTACK_CONF_RDC_CHANNEL_CHECK_RATE 8
#endif
A packet must generally be retransmitted or "strobed" until the receiver is on and receives it. This increments the power consumption of the transmitter and increases the radio traffic, but the power savings at the receiver compensates for this and there is a net overall power saving.
One alternative to optimize the RDC is to enable "phase optimization", which delays strobing until just before the receiver is expected to be awake. This however requires a good time synchronization between the transmitter and the receiver (more details in RDC Phase Optimization). To enable phase optimization change the 0 below to one.
#define CONTIKIMAC_CONF_WITH_PHASE_OPTIMIZATION 0
#define WITH_FAST_SLEEP 1
The Framer driver is actually a set of functions to frame the data to be transmitted, and to parse the received data. The Framer implementations are located in core/net/mac
, of which the most noticeable ones are framer-802154
and framer-nullmac
.
In the RE-Mote platform the following configuration is the default:
#ifndef NETSTACK_CONF_FRAMER
#if NETSTACK_CONF_WITH_IPV6
#define NETSTACK_CONF_FRAMER framer_802154
#else /* NETSTACK_CONF_WITH_IPV6 */
#define NETSTACK_CONF_FRAMER contikimac_framer
#endif /* NETSTACK_CONF_WITH_IPV6 */
#endif /* NETSTACK_CONF_FRAMER */
Meaning that when IPv6 is used, the framer-802154
is selected, else the contikimac_framer
is used (default one for the contikimac_driver
).
The framer-nullmac
framer should be used together with nullmac_driver
(MAC layer). This simply fills in the 2 fields of nullmac_hdr
, which are: receiver address and sender address.
The framer-802154
is implemented in core/net/mac/framer-802154.c
. The driver frames the data in compliance to the IEEE 802.15.4 (2003) standard. The framer insert and extracts the data to the packetbuf
structure.
One of Contiki’s most prominent feature is the support of IP protocols, being one of the first embedded operating systems to provide IPv6 support.
Alternatively Contiki also supports IPv4 and non-IP communication (Rime), however the remainder of this book will focus in IPv6. There is a good set of rime examples available at examples/rime
. The RE-Mote zoul-demo.c
most basic example at examples/zolertia/zoul
uses rime as well.
The uIP is an Open Source TCP/IP stack designed to be used even with tiny 8 and 16 bit microcontrollers. It was initially developed by Adam Dunkels while at the Swedish Institute of Computer Science (SICS), licensed under a BSD style license, and further developed by a wide group of developers.
The implementation details of the uIP/uIPv6 is out of the scope of this section. The remainder of this section explains the basic configurations at the platform and application level.
To enable IPv6 the following has to be defined, either in the application’s Makefile
or in its project-conf.h
file:
#define UIP_CONF_IPV6 1
#ifndef NBR_TABLE_CONF_MAX_NEIGHBORS
#define NBR_TABLE_CONF_MAX_NEIGHBORS 20
#endif
#ifndef UIP_CONF_MAX_ROUTES
#define UIP_CONF_MAX_ROUTES 20
#endif
/* uIP */
#ifndef UIP_CONF_BUFFER_SIZE
#define UIP_CONF_BUFFER_SIZE 1300
#endif
#define UIP_CONF_IPV6_QUEUE_PKT 0
#define UIP_CONF_IPV6_CHECKS 1
#define UIP_CONF_IPV6_REASSEMBLY 0
#define UIP_CONF_MAX_LISTENPORTS 8
Tip
|
Depending on your application and your platform you might eventually ran out of RAM memory, specially true for platforms with 8-10KB RAM. A good way to save RAM is to reduce the |
There are several routing flavors to chose, but ultimately all do the same thing: ensure that packets arrive at the right destination. This is done in different ways depending on factors such as the routing metric (how a route is qualified as better than others), whether the routing is done dynamically or statically, etc.
In Contiki the default routing protocol is RPL. Other protocols such as Ad hoc On-Demand Distance Vector (AODV) are out of the scope of this section.
The specifics of the RPL implementation are out of the scope of this section, we merely describe the common configurations and provide a brief introduction to RPL. For more details, check the RPL implementation at core/net/rpl
.
RPL is an IPv6 routing protocol for low power and lossy networks designed by the IETF Routing Over Low power and Lossy network (ROLL) group, used as the de facto routing protocol in Contiki. RPL is a proactive distance vector protocol, it starts finding the routes as soon as the RPL network is initialized.
It supports three traffic patterns:
-
Multipoint-to-point (MP2P)
-
Point-to-multipoint (P2MP)
-
Point-to-point (P2P)
RPL builds Destination Oriented DAGs (DODAGs) rooted towards one sink (DAG ROOT) identified by a unique identifier DODAGID. The DODAGs are optimized using an Objective Function (OF) metric identified by an Objective Code Point (OCP), which indicates the dynamic constraints and the metrics such as hop count, latency, expected transmission count, parents selection, energy consumption, etc. A rank number is assigned to each node which can be used to determine its relative position and distance to the root in the DODAG.
Within a given network, there may be multiple, logically independent RPL instances. An RPL node may belong to multiple RPL instances, and may act as a router in some and as a leaf in others. A set of multiple DODAGs can be in an RPL INSTANCE and a node can be a member of multiple RPL INSTANCEs, but can belong to at most one DODAG per DAG INSTANCE.
A trickle timer mechanism regulates DODAG Information Object (DIO) message transmissions, which are used to build and maintain upwards routes of the DODAG, advertising its RPL instance, DODAG ID, RANK and DODAG version number.
A node can request DODAG information by sending DODAG Information Solicitation messages (DIS), soliciting DIO messages from its neighborhoods to update its routing information and join an instance.
Nodes have to monitor DIO messages before joining a DODAG, and then join a DODAG by selecting a parent Node from its neighbors using its advertised latency, OF and RANK. Destination Advertisement Object (DAO) messages are used to maintain downward routes by selecting the preferred parent with lower rank and sending a packet to the DAG ROOT through each of the intermediate Nodes.
RPL has two mechanisms to repair the topology of the DODAG, one to avoid looping and allow nodes to join/rejoin, and other called global repair. Global repair is initiated at the DODAG ROOT by incrementing the DODAG Version Number to create a new DODAG Version.
More information about RPL can be found in RFC6550.
Routing support is enabled as default in the Z1 mote and RE-Mote platform. To enable routing the following has to be enabled:
#ifndef UIP_CONF_ROUTER
#define UIP_CONF_ROUTER 1
#endif
To enable RPL add the following to your application’s Makefile
or its project-conf.h
file.
#define UIP_CONF_IPV6_RPL 1
The following is the default configuration done in the RE-Mote:
/* ND and Routing */
#define UIP_CONF_ND6_SEND_RA 0 (1)
#define UIP_CONF_IP_FORWARD 0 (2)
#define RPL_CONF_STATS 0 (3)
-
Disable sending routing advertisements
-
Disable IP forwarding
-
RPL Configuration statistics are disabled
The RPL_CONF_OF
parameter configures the RPL objective function. The Minimum Rank with Hysteresis Objective Function (MRHOF) uses ETX as routing metric and it also has stubs for energy metric.
#ifndef RPL_CONF_OF
#define RPL_CONF_OF rpl_mrhof
#endif
The Expected Transmissions metric (ETX) measure how many tries it takes to receive an acknowledgment (ACK) of a sent packet, keeping a moving average for each neighbor, computing the sum of all ETXs to build the routes.
As default Contiki uses storing mode
for RPL downward routes. Basically all nodes store in a routing table the addresses of their child nodes.
A packet sniffer is a must-have tool for any wireless network application, it allows to see what you are transmitting over the air, verifying both that the transmissions are taking place, the frames/packets are properly formatted, and that the communication is happening on a given channel.
There are commercial options available, such as the Texas Instruments SmartRF packet Sniffer, which can be used with a CC2531 USB dongle to capture packets like the one below.
We will use for this exercise the SenSniff application, paired with a RE-Mote and Wireshark (already installed in instant Contiki). This setup will allow us to understand how the wireless communication is done in Contiki.
To program the RE-Mote or a Z1 as a packet Sniffer:
cd examples/zolertia/tutorial/02-ipv6/06-sniffer
Compile and program:
make TARGET=zoul sniffer.upload
Or
make TARGET=z1 sniffer.upload
Note this application has its own project-conf.h
, not shared with the other applications in the same 02-ipv6
folder.
The project-conf.h
has specific platform settings to make the sniffer work (don’t change those unless you know what you are doing), only change the following:
#undef IEEE802154_CONF_PANID
#define IEEE802154_CONF_PANID 0xABCD
#if CONTIKI_TARGET_ZOUL
/* The following are Zoul (RE-Mote, etc) specific */
#undef CC2538_RF_CONF_CHANNEL
#define CC2538_RF_CONF_CHANNEL 26
#else /* Default is Z1 */
/* The following are Z1 specific */
#undef RF_CHANNEL
#define RF_CHANNEL 26
#undef CC2420_CONF_CHANNEL
#define CC2420_CONF_CHANNEL 26
For the Z1 mote launch the sensniff application with the following command:
python sensniff.py --non-interactive -d /dev/ttyUSB0 -b 115200
For the RE-Mote:
python sensniff.py --non-interactive -d /dev/ttyUSB0 -b 460800
Sensniff will read data from the mote over the serial port, dissect the frames and pipe to /tmp/sensniff
by default, now we need to connect the other extreme of the pipe to wireshark, else you will get the following warning:
"Remote end not reading"
Which is not worrisome, it only means that the other pipe endpoint is not connected. You can also save the sniffed frames for later opening with wireshark, adding the following argument to the above command -p name.pcap
, which will save the session output in a name.pcap
file. Change the naming and location for storing the file accordingly.
Note
|
At the moment of writing this tutorial changing channels from the Sensniff application was not implemented but proposed as a feature. |
Open another terminal and launch wireshark with the following command, which will add the pipe as a capture interface:
sudo wireshark -i /tmp/sensniff
Select the /tmp/sensniff
interface from the droplist and click Start
just above.
Make sure that the pipe is configured to capture packets in promiscuous mode, if needed you can increase the buffer size, but 1 MB is normally enough.
Now the captured frames should start to appear on screen.
You can add specific filters to limit the frames being shown on screen, for this example click at the Expression
button and a list of available attributes per protocol are listed, scroll down to IEEE 802.15.4 and check the available filters. You can also chain different filter arguments using the Filter
box, in this case we only wanted to check the frames belonging to the PAN 0xABCD
and coming from node c1:0c::0309
, so we used the wpan.dst_pan
and wpan.src64
attributes.
When closing the Sensniff python application, a session information is provided reporting the following statistics:
Frame Stats:
Non-Frame: 6
Not Piped: 377
Dumped to PCAP: 8086
Piped: 7709
Captured: 8086
Now that we have a sniffer configured and ready to use, let’s create a first wireless application and start sniffing packets!
Now that we have covered the mote configurations and the MAC and routing layers, let us set up a UDP network.
UDP (User Datagram Protocol) is a communications protocol that offers a limited amount of services for messages exchanged among devices in a network that uses the Internet Protocol (IP).
UDP is an alternative to the Transmission Control Protocol (TCP) and, together with IP, is sometimes referred to as UDP/IP. Like the Transmission Control Protocol, UDP uses the Internet Protocol to actually get a data unit (called a datagram) from one computer to another.
Unlike TCP, UDP does not provide message fragmentation and reassembling at the other end, this means that the application must be able to make sure that the entire message has arrived and is in the right order.
Network applications that want to save processing time because they have very small data units to exchange (and therefore very little message reassembling to do) may prefer UDP to TCP
The UDP implementation is Contiki resides in core/net/ip
. The remainder of the section will focus on describing the UDP available functions.
We need to create a socket for the connection, this is done using the udp_socket
structure, which has the following elements:
struct udp_socket {
udp_socket_input_callback_t input_callback;
void *ptr;
struct process *p;
struct uip_udp_conn *udp_conn;
};
After creating the UDP socket structure, we need to register the UDP socket. This is done with the udp_socket_register
.
/**
* \brief Register a UDP socket
* \param c A pointer to the struct udp_socket that should be registered
* \param ptr An opaque pointer that will be passed to callbacks
* \param receive_callback A function pointer to the callback function that will be called when data arrives
* \retval -1 The registration failed
* \retval 1 The registration succeeded
*/
int udp_socket_register(struct udp_socket *c,
void *ptr,
udp_socket_input_callback_t receive_callback);
As the UDP socket has been created and registered, let us listen on a given port. The udp_socket_bind
function binds the UDP socket to a local port so it will begin to receive data that arrives on the specified port. A UDP socket will receive data addressed to the specified port number on any IP address of the host. A UDP socket bound to a local port will use this port number as source port for outgoing UDP messages.
/*
* \brief Bind a UDP socket to a local port
* \param c A pointer to the struct udp_socket that should be bound to a local port
* \param local_port The UDP port number, in host byte order, to bind the UDP socket to
* \retval -1 Binding the UDP socket to the local port failed
* \retval 1 Binding the UDP socket to the local port succeeded
*/
int udp_socket_bind(struct udp_socket *c,
uint16_t local_port);
The udp_socket_connect
function connects the UDP socket to a specific remote port and optional remote IP address. When a UDP socket is connected to a remote port and address, it will only receive packets that are sent from that remote port and address. When sending data over a connected UDP socket, the data will be sent to the connected remote address.
A UDP socket can be connected to a remote port, but not to a remote IP address, by providing a NULL
parameter as the remote_addr parameter. This lets the UDP socket receive data from any IP address on the specified port.
/**
* \brief Bind a UDP socket to a remote address and port
* \param c A pointer to the struct udp_socket that should be connected
* \param remote_addr The IP address of the remote host, or NULL if the UDP socket should only be connected to a specific port
* \param remote_port The UDP port number, in host byte order, to which the UDP socket should be connected
* \retval -1 Connecting the UDP socket failed
* \retval 1 Connecting the UDP socket succeeded
*/
int udp_socket_connect(struct udp_socket *c,
uip_ipaddr_t *remote_addr,
uint16_t remote_port);
To send data over a connected UDP socket it must have been connected to a remote address and port with udp_socket_connect
.
/**
* \brief Send data on a UDP socket
* \param c A pointer to the struct udp_socket on which the data should be sent
* \param data A pointer to the data that should be sent
* \param datalen The length of the data to be sent
* \return The number of bytes sent, or -1 if an error occurred
*/
int udp_socket_send(struct udp_socket *c,
const void *data, uint16_t datalen);
To send data over a UDP socket without being connected we use the function udp_socket_sendto
instead.
/**
* \brief Send data on a UDP socket to a specific address and port
* \param c A pointer to the struct udp_socket on which the data should be sent
* \param data A pointer to the data that should be sent
* \param datalen The length of the data to be sent
* \param addr The IP address to which the data should be sent
* \param port The UDP port number, in host byte order, to which the data should be sent
* \return The number of bytes sent, or -1 if an error occurred
*/
int udp_socket_sendto(struct udp_socket *c,
const void *data, uint16_t datalen,
const uip_ipaddr_t *addr, uint16_t port);
To close a UDP socket previously registered with udp_socket_register
the function below is used. All registered UDP sockets must be closed before exiting the process that registered them, or undefined behavior may occur.
/**
* \brief Close a UDP socket
* \param c A pointer to the struct udp_socket to be closed
* \retval -1 If closing the UDP socket failed
* \retval 1 If closing the UDP socket succeeded
*/
int udp_socket_close(struct udp_socket *c);
Each UDP socket has a callback function that is registered as part of the call to udp_socket_register
. The callback function gets called every time a UDP packet is received.
/**
* \brief A UDP socket callback function
* \param c A pointer to the struct udp_socket that received the data
* \param ptr An opaque pointer that was specified when the UDP socket was registered with udp_socket_register()
* \param source_addr The IP address from which the datagram was sent
* \param source_port The UDP port number, in host byte order, from which the datagram was sent
* \param dest_addr The IP address that this datagram was sent to
* \param dest_port The UDP port number, in host byte order, that the datagram was sent to
* \param data A pointer to the data contents of the UDP datagram
* \param datalen The length of the data being pointed to by the data pointer
*/
typedef void (* udp_socket_input_callback_t)(struct udp_socket *c,
void *ptr,
const uip_ipaddr_t *source_addr,
uint16_t source_port,
const uip_ipaddr_t *dest_addr,
uint16_t dest_port,
const uint8_t *data,
uint16_t datalen);
Alternatively there is another UDP library called simple-udp
, which simplifies the UDP API to fewer functions. The library is located in core/net/ip/simple-udp.c
. For the next example we are going to use the simple-udp
library, to show how to create a very first wireless example. In a later example we will come back to the full-fledged UDP API.
This is the first example of the 02-ipv6
folder.
The 01-udp-local-multicast
summarizes how to read and set radio parameters, such as:
-
RSSI (Received signal strength indication) and LQI (Link quality indicator)
-
Radio channel
-
PAN ID (network identifier)
We will also learn how to use the Simple-UDP
library, which allows to create wireless applications on top of IPv6/UDP, and transmit data such as sensor readings and system information.
You will need at least two Zolertia motes, the RE-Mote
and Z1
can be used together in the same network as the network implementation is platform-independent.
Flash the two Zolertia devices as:
make 01-udp-local-multicast.upload
Tip
|
Remember to use the make TARGET=zoul 01-udp-local-multicast.upload PORT=/dev/ttyUSB1 Likewise to open a serial console over USB to see the debug output: make TARGET=zoul login PORT=/dev/ttyUSB0 |
The RE-Mote
platform will show something similar to:
Contiki-3.x-2576-g4499d80
Zolertia RE-Mote platform
Rime configured with address 00:12:4b:00:06:15:ab:25 (1)
Net: sicslowpan (2)
MAC: CSMA (3)
RDC: nullrdc (4)
* Radio parameters:
Channel 15 (Min: 11, Max: 26) (5)
Tx Power 3 dBm (Min: -24 dBm, Max: 7 dBm) (6)
PAN ID: 0xBEEF
***
Sending packet to multicast adddress ff02::1 (7)
ID: 171, core temp: 37.619, ADC1: 2332, ADC2: 0, ADC3: 1496, batt: 3300, counter: 1
-
MAC address of the
RE-Mote
-
6LoWPAN implementation in Contiki
-
Carrier Sense Multiple Access Medium Access Control CSMA
-
Radio Duty cycling disabled (NullRDC)
-
2.4GHz RF channel (11-26 available)
-
Transmission power (-24dBm to 7dBm)
As probably noticed, the default values for the 2.4GHz band channel is 26, not 15… why is now different? let’s take a closer look at the code:
PROCESS_THREAD(mcast_example_process, ev, data)
{
static struct etimer periodic_timer;
/* Data container used to store the IPv6 addresses */
uip_ipaddr_t addr;
PROCESS_BEGIN();
set_radio_default_parameters();
print_radio_values();
(...)
}
The set_radio_default_parameters()
configures the radio values to be used by the application:
static void
set_radio_default_parameters(void)
{
NETSTACK_RADIO.set_value(RADIO_PARAM_TXPOWER, EXAMPLE_TX_POWER);
NETSTACK_RADIO.set_value(RADIO_PARAM_PAN_ID, EXAMPLE_PANID);
NETSTACK_RADIO.set_value(RADIO_PARAM_CHANNEL, EXAMPLE_CHANNEL);
}
But where are the EXAMPLE_CHANNEL
, EXAMPLE_TX_POWER
and EXAMPLE_PANID
values defined?
Tip
|
Try to do the following before reading further:
|
The Makefile
shows the location of the project-conf.h
configuration file:
# Includes the project-conf configuration file
CFLAGS += -DPROJECT_CONF_H=\"../project-conf.h\"
# This flag includes the IPv6 libraries
CONTIKI_WITH_IPV6 = 1
Notice the CONTIKI_WITH_IPV6
flag, this effectively enabled IPv6 support in our application.
The project-conf-h
file is in 02-ipv6
, and configures the following relevant values:
/* Comment this out to use Radio Duty Cycle (RDC) and save energy */
#undef NETSTACK_CONF_RDC
#define NETSTACK_CONF_RDC nullrdc_driver
#undef IEEE802154_CONF_PANID
#define IEEE802154_CONF_PANID 0xABCD
/* The following are Z1 specific */
#undef RF_CHANNEL
#define RF_CHANNEL 26
#undef CC2420_CONF_CHANNEL
#define CC2420_CONF_CHANNEL 26
/* The following are Zoul (RE-Mote, etc) specific */
#undef CC2538_RF_CONF_CHANNEL
#define CC2538_RF_CONF_CHANNEL 26
Now we know we are disabling Radio-Duty cycling and using NullRDC
and keeping the radio on, but the channel value default value is 26, not 15.
In the same 02-ipv6
folder there is a header file named example.h
which defines:
/* This is the UDP port used to send and receive data */
#define UDP_CLIENT_PORT 8765 (1)
#define UDP_SERVER_PORT 5678 (2)
/* Radio values to be configured for the 01-udp-local-multicast example */
#define EXAMPLE_TX_POWER 31 (3)
#define EXAMPLE_CHANNEL 15 (4)
#define EXAMPLE_PANID 0xBEEF (5)
/* This data structure is used to store the packet content (payload) */
struct my_msg_t { (6)
uint8_t id;
uint16_t counter;
uint16_t value1;
uint16_t value2;
uint16_t value3;
uint16_t value4;
uint16_t battery;
};
-
The UDP port opened by the device itself
-
The UDP port to send data to (opened by the server)
-
The new transmission power set in the example
-
The new channel set in the example
-
The new PAN ID configured in the example
-
The structure in which we are to store the data to be sent wirelessly
Let’s go back to the 01-udp-local-multicast.c
example:
/* The structure used in the Simple UDP library to create an UDP connection */
static struct simple_udp_connection mcast_connection;
/* Create the UDP connection. This function registers a UDP connection and
* attaches a callback function to it. The callback function will be
* called for incoming packets. The local UDP port can be set to 0 to indicate * that an ephemeral UDP port should be allocated. The remote IP address can
* be NULL, to indicate that packets from any IP address should be accepted.
*/
simple_udp_register(&mcast_connection, UDP_CLIENT_PORT, NULL, UDP_CLIENT_PORT, receiver);
First we have configured an UDP connection, passing as an argument the mcast_connection
structure to be used by the simple-udp
library. As the remote IP address is set to NULL
any incoming packet will be accepted, invoking the receiver
callback handler whenever that happens:
static void
receiver(struct simple_udp_connection *c,
const uip_ipaddr_t *sender_addr,
uint16_t sender_port,
const uip_ipaddr_t *receiver_addr,
uint16_t receiver_port,
const uint8_t *data,
uint16_t datalen)
{
radio_value_t aux;
struct my_msg_t *msgPtr = (struct my_msg_t *) data; (1)
leds_toggle(LEDS_GREEN);
uip_debug_ipaddr_print(sender_addr); (2)
printf("\nData received on port %d from port %d with (3)
length %d\n", receiver_port, sender_port, datalen);
NETSTACK_RADIO.get_value(RADIO_PARAM_CHANNEL, &aux); (4)
printf("CH: %u ", (unsigned int) aux);
aux = packetbuf_attr(PACKETBUF_ATTR_RSSI); (5)
printf("RSSI: %ddBm ", aux);
aux = packetbuf_attr(PACKETBUF_ATTR_LINK_QUALITY); (6)
printf("LQI: %u\n", aux);
/* Print the received data */ (7)
printf("ID: %u, core temp: %u, ADC1: %d, ADC2: %d, ADC3: %d",
msgPtr->id, msgPtr->value1, msgPtr->value2, msgPtr->value3,
msgPtr->value4);
printf("batt: %u, counter: %u\n", msgPtr->battery,
msgPtr->counter);
}
-
Creates a pointer to dereference the message payload into the
my_msg_t
structure defined inexample.h
-
A helper function used to print an IPv6 address in a readable form
-
Prints the UDP origin and destination port, and the message length
-
Retrieve the current RF channel number
-
Retrieve the RSSI level of the received message (on a 1-hop basis)
-
Retrieve the LQI value of the received message
-
Prints the payload content
As probably you have already noticed, we are sending the on-board sensor readings via wireless to the all-nodes multicast address:
etimer_set(&periodic_timer, SEND_INTERVAL);
while(1) {
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&periodic_timer));
/* Create a link-local multicast address to all nodes */
uip_create_linklocal_allnodes_mcast(&addr);
uip_debug_ipaddr_print(&addr);
printf("\n");
/* Take sensor readings and store into the message structure */
take_readings();
/* Send the multicast packet to all devices */
simple_udp_sendto(&mcast_connection, msgPtr, sizeof(msg), &addr);
etimer_reset(&periodic_timer);
}
The take_readings()
function reads the sensors values and then stores it into msg
, a my_msg_t
structure.
/* Create a structure and pointer to store the data to be sent as payload */
static struct my_msg_t msg;
static struct my_msg_t *msgPtr = &msg;
Notice the simple_udp_sendto
accepts any type of data as payload, we use the msgPtr
pointer which points to msg
and the sizeof(msg)
function to inform simple-udp
how large is our payload.
Program at least two Z1
or RE-Mote
with the 01-udp-local-multicast.c
application, and open a console connection using make login
for each connected device.
You should be receiving messages from the other devices! Write down the node ID of other motes. This will be useful later.
Tip
|
Experiment with the channel, transmission power, RSSI and LQI levels:
|
To change the sending interval you can also modify the values at:
#define SEND_INTERVAL (15 * CLOCK_SECOND)
Tip
|
Program at least one Remember you are using the |
The border router or edge router is typically a device sitting at the edge of our network, which allow us to talk to outside networks using its built-in network interfaces, such as WiFi, Ethernet, Serial, etc.
In Contiki the current and most used border router application implements a serial-based interface called SLIP, it allows to connect a given mote to a host using scripts like tunslip6
in tools/tunslip6
over the serial port, creating a tunnelled network interface, which can be given an IPv6 prefix to set the network global IPv6 addresses.
A simplification of tunslip6
and the Border Router is: IPv6 packets received by the host are forwarded over the USB connection to the Border Router via tunslip6
, and subsequently all 6LoWPAN wireless packets are forwarded by the Border Router to the host as IPv6 packets via the USB connection through tunslip6
.
The border router application is located at examples/ipv6/rpl-border-router
, the following code snippets are the most relevant:
/* Request prefix until it has been received */
while(!prefix_set) {
etimer_set(&et, CLOCK_SECOND);
request_prefix();
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));
}
dag = rpl_set_root(RPL_DEFAULT_INSTANCE,(uip_ip6addr_t *)dag_id);
if(dag != NULL) {
rpl_set_prefix(dag, &prefix, 64);
PRINTF("created a new RPL dag\n");
}
The snippet above bootstraps until a valid prefix has been given. Once the prefix has been assigned, the node will set the prefix and convert itself in the root node (DODAG).
Normally it is preferable to configure the border router as a non-sleeping device, so that the radio receiver is always on (no radio duty cycling).
By default the border-router applications includes a built-in web server, displaying information about the network, such as the immediate neighbors (1-hop away) and the known routes to nodes in their networks. To enable the web server, the WITH_WEBSERVER
flag should be enabled, and by default it will add the httpd-simple.c
application.
The following assumes to use a RE-Mote platform, but the Z1 mote can be used as well.
make TARGET=zoul savetarget
To compile, flash the mote and connect the border router to your host; run:
cd /home/user/contiki/examples/zolertia/tutorial/02-ipv6/02-border-router
make border-router.upload && make connect-router
By default it will try to connect to a mote at port /dev/ttyUSB0
using the following serial settings: 115200 baud rate, 8 bits, No parity and 1 bit stop. If you do not state an IPv6 prefix it will use the default aaaa::1/64
, to specify a different one run the tunslip tool instead using the following:
make connect-router PREFIX="2001:abcd:dead:beef::1/64"
Or if you want to specify a different USB port:
make connect-router PREFIX="-s /dev/ttyUSB1 fd00::1/64"
You can also compile and run the tunslip6
tool directly from the tools location, to compile just type:
cd tools
cc tunslip6.c -o tunslip6
And to run with specific arguments, i.e. connect to a specific serial port, name your tunnel connection with a specific name, or proxify to a given address and port, use the following:
./tunslip -s /dev/ttyUSB0 -t tun0 2001:abcd:dead:beef::1/64
Run tunslip -H
for more information.
Important
|
To enable IPv6 forwarding you need to run:
To make this change permanent, edit the
|
The output of the tunslip6
script is:
using saved target 'z1'
sudo ../../../../../tools/tunslip6 fd00::1/64
[sudo] password for zolertia:
********SLIP started on ``/dev/ttyUSB0''
opened tun device ``/dev/tun0''
ifconfig tun0 inet `hostname` mtu 1500 up
ifconfig tun0 add fd00::1/64
ifconfig tun0 add fe80::0:0:0:1/64
ifconfig tun0
tun0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
inet addr:127.0.1.1 P-t-P:127.0.1.1 Mask:255.255.255.255
inet6 addr: fe80::1/64 Scope:Link
inet6 addr: fd00::1/64 Scope:Global
UP POINTOPOINT RUNNING NOARP MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:500
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
*** Address:fd00::1 => fd00:0000:0000:0000
Got configuration message of type P
Setting prefix fd00::
Server IPv6 addresses:
fd00::c30c:0:0:13c2
fe80::c30c:0:0:13c2
This will create a tun0
interface in our host, try running the ifconfig
command in a terminal.
Note the Border Router uses Stateless Auto Configuration SLAC to create its IPv6 address, by using the IPv6 prefix given to the tunslip6
script and its own MAC address.
Note
|
Contiki has changed the default prefix from #define UIP_CONF_DS6_DEFAULT_PREFIX 0xaaaa |
We can make a ping6
request to both the tun0
interface and to the Border Router
:
$ ping6 fd00::c30c:0:0:13c2
PING fd00::c30c:0:0:13c2(fd00::c30c:0:0:13c2) 56 data bytes
64 bytes from fd00::c30c:0:0:13c2: icmp_seq=1 ttl=64 time=19.8 ms
64 bytes from fd00::c30c:0:0:13c2: icmp_seq=2 ttl=64 time=20.3 ms
64 bytes from fd00::c30c:0:0:13c2: icmp_seq=3 ttl=64 time=20.5 ms
64 bytes from fd00::c30c:0:0:13c2: icmp_seq=4 ttl=64 time=20.8 ms
64 bytes from fd00::c30c:0:0:13c2: icmp_seq=5 ttl=64 time=20.6 ms
64 bytes from fd00::c30c:0:0:13c2: icmp_seq=6 ttl=64 time=20.6 ms
Furthermore, we can open a web browser and enter the Border Router’s IPv6 address, in this example:
http://[fd00::c30c:0:0:13c2]
The built-in web service will show the Border Router’s neighbors list and its routes. This is quite handy to check if the devices in our deployment have joined our Border Router’s network, retrieve their IPv6 addresses and even make a ping6
request.
Note we have been used a local IPv6 prefix, routable only in our network for testing purposes. It is also possible to assign the tunslip6
a globally routable IPv6 prefix, thus connecting our Border Router to Internet.
Using a public and globally reachable IPv6 prefix will make our Border Router, and subsequently all devices inside our IPv6/6LoWPAN network accessible over Internet:
This means our 6LoWPAN network of constrained devices is reachable from anywhere in the world, and viceversa.
Tip
|
6lbr is a deployment-ready 6LoWPAN border router solution based on Contiki, it has support for the Z1 mote and the RE-Mote platform. To take your border router to the next level, this is the tool you have been looking for. |
If you launch the sniffer you will be able to see how the Border Router starts sending the RPL control messages, such as DIO
(DAG Information Object) messages. If you flash another device with the previously used 01-udp-multicast.c
example, you would be able to also see how a device joins the RPL network by exchanging DIO
, DAO
(Destination Advertisement Object) and DIS
(DAG Information Solicitation) messages.
Tip
|
Remember the |
In the 02-ipv6
folder open the 03-udp-client-and-server
example.
The example shows how to send sensor information to an UDP server running on the host network, even on a different network over Internet.
The UDP server is implemented in a python script, it allows to forward data also to other services.
The UDP server will send data to the MQTT broker of the Eclipse IoT foundation (at iot.eclipse.org
), or to IFTTT to easily configure different types of events using a HTTP message as trigger.
Depending on the device the UDP client runs (Z1
or RE-Mote
), you need to adjust in the UDP-MQTT-server.py
or UDP-IFTTT-server.py
the following:
EXAMPLE_WITH_Z1 = 1
This example is the easiest way if you want to send data to a service or server outside your network, and you don’t have an IPv6 network on your own. The Border Router as seen before can use a local IPv6 prefix
and then the UDP server running on the host can forward to anywhere and anyone.
This example requires at least two Zolertia devices: A Border Router and an UDP client. You can use alternatively Z1
and RE-Motes
as both can interoperate with each other.
You will also need to install the following python library if using the MQTT forwarder:
pip install paho-mqtt
You will need at least two Zolertia motes, the RE-Mote
and Z1
can be used together in the same network.
The application (on the networking perspective) will be similar to:
And from the application service perspective the MQTT forwarder application will look like:
And for the IFTTT example, we can implement a simple use case related to preventive maintenance: if the battery is close to deplete, or the device has been tampered (sensed by the accelerometer), or the radio link is close to fade (due to an unexpected obstacle), then schedule a calendar task to the maintenance crew.
Let’s start!
In the 03-udp-client.c
example first note the server address:
/* Set the server address here */
uip_ip6addr(&server_ipaddr, 0xfd00, 0, 0, 0, 0, 0, 0, 1);
As we are assuming we are going to use the fd00::1/64
IPv6 address for the tunslip6
virtual interface, this will be the address of the host running the UDP-MQTT-server.py
python script, if not change accordingly.
To verify that we have set the address correctly:
printf("Server address: ");
PRINT6ADDR(&server_ipaddr);
printf("\n");
The print_local_addresses(…)
function will print the device configured addresses:
static void
print_local_addresses(void)
{
int i;
uint8_t state;
PRINTF("Client IPv6 addresses:\n");
for(i = 0; i < UIP_DS6_ADDR_NB; i++) {
state = uip_ds6_if.addr_list[i].state;
if(uip_ds6_if.addr_list[i].isused &&
(state == ADDR_TENTATIVE || state == ADDR_PREFERRED)) {
PRINT6ADDR(&uip_ds6_if.addr_list[i].ipaddr);
PRINTF("\n");
/* hack to make address "final" */
if (state == ADDR_TENTATIVE) {
uip_ds6_if.addr_list[i].state = ADDR_PREFERRED;
}
}
}
}
Printing at boot something similar to:
Server address: fd00::1
Client IPv6 addresses:
fe80::c30c:0:0:13c8
Remember addresses starting with fe80
are link-local, this means the device has not joined the RPL network, as it has not received an IPv6 prefix from the Border Router (the DODAG) to create its own IPv6 global address using SLAC.
As we know the prefix given to the Border Router is fd00::/64
, we know the device in this example will have the fd00::c30c:0:0:13c8
IPv6 global address.
You can verify this later by checking the Border Router’s web service as done in the previous section.
The UDP connection is created in the following block:
/* Create a new connection with remote host. When a connection is created
* with udp_new(), it gets a local port number assigned automatically.
* The "UIP_HTONS()" macro converts to network byte order.
* The IP address of the remote host and the pointer to the data are not used
* so those are set to NULL
*/
client_conn = udp_new(NULL, UIP_HTONS(UDP_SERVER_PORT), NULL);
if(client_conn == NULL) {
PRINTF("No UDP connection available, exiting the process!\n");
PROCESS_EXIT();
}
/* This function binds a UDP connection to a specified local por */
udp_bind(client_conn, UIP_HTONS(UDP_CLIENT_PORT));
PRINTF("Created a connection with the server ");
PRINT6ADDR(&client_conn->ripaddr);
PRINTF(" local/remote port %u/%u\n", UIP_HTONS(client_conn->lport),
UIP_HTONS(client_conn->rport));
As you have probably noticed, we are using the same project-conf.h
and the example.h
headers from the previous 01-udp-multicast.c
example. In this case we are using both UDP client and server port, opening the first to receive data from the UDP server running in the python script, and the second value to send data to the UDP server.
Upon receiving a message (from the UDP server for example) the tcpip_handler
is called to process the incoming data:
static void
tcpip_handler(void)
{
char *str;
if(uip_newdata()) {
str = uip_appdata;
str[uip_datalen()] = '\0';
printf("Received from the server: '%s'\n", str);
}
}
And as done in the previous example the devices will send data from its on-board sensors using the my_msg_t
structure to store the data as payload.
while(1) {
PROCESS_YIELD();
/* Incoming events from the TCP/IP module */
if(ev == tcpip_event) {
tcpip_handler();
}
/* Send data to the server */
if((ev == sensors_event && data == &button_sensor) ||
(ev == PROCESS_EVENT_TIMER)) {
send_packet();
if(etimer_expired(&periodic)) {
etimer_reset(&periodic);
}
}
}
The sensors information is sent periodically as configured by the SEND_INTERVAL
value (default is every minute, change as you wish), or by pressing the user button
to request an immediate packet to be sent to the UDP server.
The code snippet related to the payload sent to the UDP server address is shown next:
PRINTF("Send readings to %u'\n",
server_ipaddr.u8[sizeof(server_ipaddr.u8) - 1]);
uip_udp_packet_sendto(client_conn, msgPtr, sizeof(msg),
&server_ipaddr, UIP_HTONS(UDP_SERVER_PORT));
Compile and program the mote:
make TARGET=zoul savetarget
make 03-udp-client.upload && make login
The output should be something similar to:
Contiki-3.x-2611-g61576c3
Zolertia RE-Mote platform
CC2538: ID: 0xb964, rev.: PG2.0, Flash: 512 KiB, SRAM: 32 KiB, AES/SHA: 1, ECC/RSA: 1
System clock: 16000000 Hz
I/O clock: 16000000 Hz
Reset cause: External reset
Rime configured with address 00:12:4b:00:06:15:ab:25
Net: sicslowpan
MAC: CSMA
RDC: nullrdc
UDP client process started
Server address: fd00::1
Client IPv6 addresses:
fe80::212:4b00:615:ab25
Created a connection with the server :: local/remote port 8765/5678
ID: 171, core temp: 34.47, ADC1: 2332, ADC2: 0, ADC3: 1500, batt: 3300, counter: 1
Send readings to 1'
Remember that you can also compile for the Z1 platform.
Remember to check the network and connection by using ping6
, like you normally would test a wireless device. Open the Border Router’s web service and see if you can find the node in your neighbor list!
Once you found the node’s global IPv6 assigned address, make a ping6
request and check the ICMPv6
response. Don’t forget to use the wireless Sniffer and see how these ICMP packets looks like:
Tip
|
With the Sniffer you are able to see the wireless messages within the 6LoWPAN network, but you can also capture in Wireshark the network traffic through the |
There is a Wireshark capture saved as an example for you to take a look in wireshark-ipv6-6lowpan.pcap
.
To execute the UDP-MQTT-server.py
script just run:
python UDP-MQTT-server.py
This is the expected output when running and receiving a UDP packet from a Z1
node:
UDP6 server side application V0.1
Started 2016-02-26 09:23:49.673940
UDP server ready: 5678
msg structure size: 13
MQTT: Connected (0)
2016-02-26 09:23:58 -> fd00::c30c:0:0:13c8:8765 14
{
"values": [
{
"value": 171,
"key": "id"
},
{
"value": 0,
"key": "counter"
},
{
"value": 2320,
"key": "temperature"
},
{
"value": -135,
"key": "x_axis"
},
{
"value": 40,
"key": "y_axis"
},
{
"value": -120,
"key": "z_axis"
},
{
"value": 2981,
"key": "battery"
}
]
}
MQTT: Publishing to {0}... 0 (171)
Sending reply to fd00::c30c:0:0:13c8
MQTT: Published 2
If we run the mqtt-client.py
python script in a separate terminal we will receive a notification each time the UDP server publishes new data to the v2/zolertia/tutorialthings/#
topic:
python mqtt-client.py
connecting to iot.eclipse.org
Connected with result code 0
Subscribed to v2/zolertia/tutorialthings/#
v2/zolertia/tutorialthings/171 {"values":[{"key": "id", "value": 171},{"key": "counter", "value": 0},{"key": "temperature", "value": 2326},{"key": "x_axis", "value": -129},{"key": "y_axis", "value": 38},{"key": "z_axis", "value": -122},{"key": "battery", "value": 2987}]}
Note
|
The v2/zolertia/tutorialthings/{{ID}} Where msg.id = 0xAB; Thus the default topic will be The pound sign in |
If you want to change the default MQTT topic, change the following in UDP-MQTT-server.py
:
MQTT_URL_PUB = "v2/zolertia/tutorialthings/"
If you have an android mobile phone you can download the MyMQTT
application and receive the notifications in your phone:
Server : "iot.eclipse.org"
Port : 1883
Username/password not required
Topic: v2/zolertia/tutorialthings/#
Create an IFTTT account and subscribe to the Maker Channel:
And copy your Key
as shown below:
Then create the Recipe you want, for example to automatically create a calendar entry to the maintenance crew if the battery level is critical or the temperature of the room is too high, send an email whenever someones pushes the button, etc… you have plenty of channels to choose!
In the UDP-IFTTT-server.py
just add the event
and key
values:
IFTTT_EVENT = "test"
IFTTT_KEY = ""
The Transmission Control Protocol (TCP) is a core protocol of the Internet Protocol (IP).
TCP is a reliable stream delivery service that ensures that all bytes received will be in the correct order. It uses a technique known as positive acknowledgment with retransmission to guarantee reliability of packet transfer. TCP handles the received fragments and reorders the data.
Applications that do not require reliable data stream service may use the User Datagram Protocol (UDP), which provides a connectionless datagram service that emphasizes reduced latency over reliability.
TCP is commonly used by HTTP, FTP, email and any connection-oriented service.
The TCP implementation is Contiki resides in core/net/ip
. The remainder of the section will focus on describing the TCP available functions.
We need to create a socket for the connection, this is done using the tcp_socket
structure, which has the following elements:
struct tcp_socket {
struct tcp_socket *next;
tcp_socket_data_callback_t input_callback;
tcp_socket_event_callback_t event_callback;
void *ptr;
struct process *p;
uint8_t *input_data_ptr;
uint8_t *output_data_ptr;
uint16_t input_data_maxlen;
uint16_t input_data_len;
uint16_t output_data_maxlen;
uint16_t output_data_len;
uint16_t output_data_send_nxt;
uint16_t output_senddata_len;
uint16_t output_data_max_seg;
uint8_t flags;
uint16_t listen_port;
struct uip_conn *c;
};
Socket status:
enum {
TCP_SOCKET_FLAGS_NONE = 0x00,
TCP_SOCKET_FLAGS_LISTENING = 0x01,
TCP_SOCKET_FLAGS_CLOSING = 0x02,
};
After creating the TCP socket structure, we need to register the TCP socket. This is done with the tcp_socket_register
, which takes as arguments the TCP socket, and input/output buffers to use for sending and receiving data. Be sure to dimension these buffers according to the expected amount of data to be sent and received.
/**
* \brief Register a TCP socket
* \param s A pointer to a TCP socket
* \param ptr A user-defined pointer that will be sent to callbacks for this socket
* \param input_databuf A pointer to a memory area this socket will use for input data
* \param input_databuf_len The size of the input data buffer
* \param output_databuf A pointer to a memory area this socket will use for outgoing data
* \param output_databuf_len The size of the output data buffer
* \param data_callback A pointer to the data callback function for this socket
* \param event_callback A pointer to the event callback function for this socket
* \retval -1 If an error occurs
* \retval 1 If the operation succeeds.
*/
int tcp_socket_register(struct tcp_socket *s, void *ptr,
uint8_t *input_databuf, int input_databuf_len,
uint8_t *output_databuf, int output_databuf_len,
tcp_socket_data_callback_t data_callback,
tcp_socket_event_callback_t event_callback);
As the TCP socket has been created and registered, let us listen on a given port. When a remote host connects to the port, the event callback will be called with the TCP_SOCKET_CONNECTED
event. When the connection closes, the socket will go back to listening.
/**
* \brief Start listening on a specific port
* \param s A pointer to a TCP socket that must have been previously registered with tcp_socket_register()
* \param port The TCP port number, in host byte order, of the remote host
* \retval -1 If an error occurs
* \retval 1 If the operation succeeds.
*/
int tcp_socket_listen(struct tcp_socket *s,
uint16_t port);
To stop listening on a given TCP port, just call the following function:
/**
* \brief Stop listening for new connections
* \param s A pointer to a TCP socket that must have been previously registered with tcp_socket_register()
* \retval -1 If an error occurs
* \retval 1 If the operation succeeds.
*/
int tcp_socket_unlisten(struct tcp_socket *s);
We can connect the TCP socket to a remote host. When the socket has connected, the event callback will get called with the TCP_SOCKET_CONNECTED
event. If the remote host does not accept the connection, the TCP_SOCKET_ABORTED
will be sent to the callback. If the connection times out before conecting to the remote host, the TCP_SOCKET_TIMEDOUT
event is sent to the callback.
/**
* \brief Connect a TCP socket to a remote host
* \param s A pointer to a TCP socket that must have been previously registered with tcp_socket_register()
* \param ipaddr The IP address of the remote host
* \param port The TCP port number, in host byte order, of the remote host
* \retval -1 If an error occurs
* \retval 1 If the operation succeeds.
*/
int tcp_socket_connect(struct tcp_socket *s,
const uip_ipaddr_t *ipaddr,
uint16_t port);
As we are using an output buffer to send data over the TCP socket, a good practice is to query the TCP socket and check the number of bytes available.
/**
* \brief The maximum amount of data that could currently be sent
* \param s A pointer to a TCP socket
* \return The number of bytes available in the output buffer
*/
int tcp_socket_max_sendlen(struct tcp_socket *s);
To send data over a connected TCP socket the data is placed in the output buffer. When the data has been acknowledged by the remote host, the event callback is sent with the TCP_SOCKET_DATA_SENT
event.
/**
* \brief Send data on a connected TCP socket
* \param s A pointer to a TCP socket that must have been previously registered with tcp_socket_register()
* \param dataptr A pointer to the data to be sent
* \param datalen The length of the data to be sent
* \retval -1 If an error occurs
* \return The number of bytes that were successfully sent
*/
int tcp_socket_send(struct tcp_socket *s,
const uint8_t *dataptr,
int datalen);
Alternatively we can send a string over a TCP socket as follows:
/**
* \brief Send a string on a connected TCP socket
* \param s A pointer to a TCP socket that must have been previously registered with tcp_socket_register()
* \param strptr A pointer to the string to be sent
* \retval -1 If an error occurs
* \return The number of bytes that were successfully sent
*/
int tcp_socket_send_str(struct tcp_socket *s,
const char *strptr);
To close a connected TCP socket the function below is used. The event callback is called with the TCP_SOCKET_CLOSED
event.
/**
* \brief Close a connected TCP socket
* \param s A pointer to a TCP socket that must have been previously registered with tcp_socket_register()
* \retval -1 If an error occurs
* \retval 1 If the operation succeeds.
*/
int tcp_socket_close(struct tcp_socket *s);
And to unregister a TCP socket the tpc_socket_unregister
function is used. This function can also be used to reset a connected TCP socket.
/**
* \brief Unregister a registered socket
* \param s A pointer to a TCP socket that must have been previously registered with tcp_socket_register()
* \retval -1 If an error occurs
* \retval 1 If the operation succeeds.
*
* This function unregisters a previously registered
* socket. This must be done if the process will be
* unloaded from memory. If the TCP socket is connected,
* the connection will be reset.
*
*/
int tcp_socket_unregister(struct tcp_socket *s);
The TCP socket event callback function gets called whenever there is an event on a socket, such as the socket getting connected or closed.
/**
* \brief TCP event callback function
* \param s A pointer to a TCP socket
* \param ptr A user-defined pointer
* \param event The event number
*/
typedef void (* tcp_socket_event_callback_t)(struct tcp_socket *s,
void *ptr,
tcp_socket_event_t event);
The TCP data callback function has to be added to the application, it will get called whenever there is new data on the socket:
/**
* \brief TCP data callback function
* \param s A pointer to a TCP socket
* \param ptr A user-defined pointer
* \param input_data_ptr A pointer to the incoming data
* \param input_data_len The length of the incoming data
* \return The function should return the number of bytes to leave in the input buffer
*/
typedef int (* tcp_socket_data_callback_t)(struct tcp_socket *s,
void *ptr,
const uint8_t *input_data_ptr,
int input_data_len);
Now let us put to practice the TCP API described before and browse a TCP application. The 05-tpc-socket
example simply echoes back the request done on port 80.
Then let us open the tcp-server.c
example and browse the implementation.
The port 80 will be used for the TCP server to receive remote connections. As shown earlier we need to create a tcp_socket
structure, and use two separate input/output buffers to send and receive data.
#define SERVER_PORT 80
static struct tcp_socket socket;
#define INPUTBUFSIZE 400
static uint8_t inputbuf[INPUTBUFSIZE];
#define OUTPUTBUFSIZE 400
static uint8_t outputbuf[OUTPUTBUFSIZE];
These two variables will be used to count the number of bytes received, and the bytes to be sent.
static uint8_t get_received;
static int bytes_to_send;
As commented earlier, we need to include a tcp_socket_event_callback_t
to handle events.
static void
event(struct tcp_socket *s, void *ptr,
tcp_socket_event_t ev)
{
printf("event %d\n", ev);
}
We register the TCP socket and pass as a pointer the tcp_socket
structure, the data buffers and our callback handlers. Next we start listening for connections on port 80.
tcp_socket_register(&socket, NULL,
inputbuf, sizeof(inputbuf),
outputbuf, sizeof(outputbuf),
input, event);
tcp_socket_listen(&socket, SERVER_PORT);
The input
callback handler receives the data, prints the string and its length, then if the received string is a complete request we save the number of bytes received into bytes_to_send
(the atoi
function converts string numbers into integers). If the received string is not complete, we return the number of bytes received to the driver to keep the data in the input buffer.
static int
input(struct tcp_socket *s, void *ptr,
const uint8_t *inputptr, int inputdatalen)
{
printf("input %d bytes '%s'\n", inputdatalen, inputptr);
if(!get_received) {
/* See if we have a full GET request in the buffer. */
if(strncmp((char *)inputptr, "GET /", 5) == 0 &&
atoi((char *)&inputptr[5]) != 0) {
bytes_to_send = atoi((char *)&inputptr[5]);
printf("bytes_to_send %d\n", bytes_to_send);
return 0;
}
printf("inputptr '%.*s'\n", inputdatalen, inputptr);
/* Return the number of data bytes we received, to keep them all
in the buffer. */
return inputdatalen;
} else {
/* Discard everything */
return 0; /* all data consumed */
}
}
The application will wait for an event to happen, in this case the incoming connection from above. After the event is handled, the code inside the while()
loop and after the PROCESS_PAUSE()
will be executed.
while(1) {
PROCESS_PAUSE();
If we have previously received a complete request, we echo it back over the TCP socket. We use the tcp_socket_send_str
function to send the header of the response as a string. The remainder of the data is sent until the bytes_to_send
counter is empty.
if(bytes_to_send > 0) {
/* Send header */
printf("sending header\n");
tcp_socket_send_str(&socket, "HTTP/1.0 200 ok\r\nServer: Contiki tcp-socket example\r\n\r\n");
/* Send data */
printf("sending data\n");
while(bytes_to_send > 0) {
PROCESS_PAUSE();
int len, tosend;
tosend = MIN(bytes_to_send, sizeof(outputbuf));
len = tcp_socket_send(&socket, (uint8_t *)"", tosend);
bytes_to_send -= len;
}
tcp_socket_close(&socket);
}
}
PROCESS_END();
}
When all the data is echoed back, the TCP socket is closed.