Mise en place d'un réseau IoT avec RIOT

Spécialité(s)


Résumé

Dans le précédent numéro, nous avions présenté RIOT, un système d'exploitation destiné à l'Internet des objets (IoT), et donné les éléments nécessaires pour commencer à l'utiliser dans le développement d'applications embarquées. Cependant, la partie communication entre objets et Internet (on parle d'IoT, n'est-ce pas ?) y était traitée de manière partielle, se contentant d'échanger des messages sur le lien-local IPv6. Cet article a donc pour objectif de combler ce manque en donnant quelques explications complémentaires pour mettre en œuvre un réseau de capteurs accessibles depuis Internet et utilisant des protocoles standards de l'IoT.


Body

1. Quelques généralités sur l'IoT

Le déploiement de la version 6 du protocole IP ouvre des perspectives quasi illimitées de communication à travers Internet entre tout système disposant d'une pile réseau compatible. En effet, par construction, cette version apporte un espace d'adressage beaucoup plus vaste que celui de son ancêtre pratiquement saturé IPv4, passant de 232 (un peu plus de 4 milliards) à 2128 adresses*. Cela doit permettre à terme d'adresser et donc de connecter, à peu près tout ce qui se trouve à la surface de la Terre. C'est donc en partie grâce à IPv6 qu'un si bel avenir, à l'horizon 2020, est promis à l'IoT.

* C'est-à-dire 3,4×1038, soit 340282366920938463463374607431768211456 adresses.

Actuellement, des systèmes tels que [RIOT], [Contiki] ou [Zephyr] arrivent à faire communiquer par ondes radio des objets fonctionnant sur des configurations très contraintes, leur donnant ainsi une autonomie maximale sur batterie. On parle ici d'objets à base de microcontrôleurs ayant des caractéristiques largement inférieures à ceux équipant des cartes Raspberry Pi ou équivalents : quelques Ko de SRAM et de flash et un CPU fonctionnant à quelques dizaines de MHz. Donc plutôt des objets à base d'AVR, de MSP430 ou d'ARM Cortex-M0 jusqu'au Cortex-M4. Pour vous donner un ordre d'idée, ce type de systèmes peut rester en fonctionnement jusqu'à plusieurs années sans avoir besoin d'être rechargé et donc être installé dans des zones peu accessibles, voire hostiles [Low Power].

Pour réussir à faire tenir une pile réseau qui respecte les problématiques de l'IoT (peu de mémoire, autonomie maximale), plusieurs groupes de travail de l'IETF ont proposé de nouveaux protocoles et en ont adapté d'autres dans le modèle de couches IP (Figure 1).

couches_iot

Figure 1 : Le modèle TCP/IP adapté pour l'IoT. Les protocoles sont ceux utilisés dans cet article.

Dans le cadre qui nous intéresse pour cet article, les objets connectés, qu'on appellera aussi « noeuds » par la suite, utilisent des interfaces de communication sans fil de type [802.15.4]. La consommation de ce protocole radio est très faible et on parle alors de réseau LowPAN pour LoW Power wireless Area Networks ou LLN pour Low-Power and Lossy Networks. En contrepartie, leur portée est courte : en fonction de la fréquence/modulation, jusqu'à la centaine de mètres pour la variante populaire dans la bande 2.4 GHz à 250 Kbps.

[6LowPAN] est une couche d'adaptation de la couche réseau IPv6 destinée à faire passer des trames IPv6 dont le MTU est au minimum de 1280 octets dans des trames 802.15.4 (127 octets). Cette couche d'adaptation permet donc de faire interopérer différents types de réseau en IPv6 (Ethernet, 802.15.4, Wifi, 4G... ). Le routage interne IPv6 entre les nœuds du réseau utilise le protocole à vecteur de distance [RPL], adapté à un réseau moins fiable (pertes de paquets, faible bande passante...). Sur un LLN, puisqu'on s'autorise de la perte de paquets, le transport des données s'effectue généralement par UDP, ce qui limite aussi la taille des entêtes.

Enfin, tout en haut de la pile, [CoAP] est un protocole de transport applicatif de type web (HTTP) proposant un modèle requête/réponse de type REST.

Tous ces protocoles sont disponibles dans RIOT et l'objectif de cet article est de vous montrer comment les utiliser pour faire de l'IoT.

2. Rappels préalables sur IPv6

Avant de rentrer dans les explications de configuration réseau sur les nœuds, il est essentiel d'avoir quelques notions sur IPv6. Ici, seuls les éléments nécessaires à la compréhension de la suite de l'article sont donnés, mais le lecteur désirant aller plus loin pourra se pencher sur lecture des RFC décrivant la norme en détail :  [RFC4291], [RFC2460], [RFC3306], [RFC3307], [RFC3849].

Contrairement à une adresse IPv4 qui est codée sur 32 bits (i.e 4 octets) et utilise une notation décimale (par exemple : 192.168.6.19), une adresse IPv6 est représentée par une série de 128 bits, soit 16 octets, et se représente avec une notation hexadécimale (Figure 2). Par exemple, une adresse IPv6 peut avoir pour représentation complète :

2001:0db8:3c4d:00ab:0000:0000:0000:0019

qui, suivant certaines règles (entre autres, une série de 0 contigus est remplacée une seule fois par ::, [RFC 5952]), peut s'abréger en :

2001:db8:3c4d:ab::19

Cette série de 128 bits est souvent divisée en 2 parties :

  • les 64 bits de poids faible correspondent à l'adresse de l'hôte, ::19 dans l'exemple précédent. Généralement, ils sont construits à partir de l'adresse MAC de l'hôte pour garantir l'unicité d'une adresse IPv6 dans un sous-réseau (puisque l'adresse physique est elle-même unique, normalement) ;
  • les 64 bits de poids fort correspondent eux à l'adresse réseau, 2001:db8:3c4d::ab:: dans l'exemple précédent. Ils contiennent en particulier le « préfixe » utilisé pour le routage de paquets IPv6 et utilisent, comme en IPv4, la notation CIDR : <préfixe>/<longueur en bit>.  Ce bloc de 64 bits se divise en une première partie contenant jusqu'à 48 bits et désignant le « préfixe de site », le reste des bits identifiant le sous-réseau. Un préfixe contient toujours 64 bits.
format-adresse-ipv6

Figure 2 : Format d'une adresse IPv6.

Certains préfixes sont réservés pour des usages bien précis :

  • 2001:db8::/32 est utilisé pour la documentation (comme dans notre exemple). Il définit également une adresse unicast dite « globale », c'est-à-dire routable,
  • fe80::/10 est utilisé pour les adresses unicast dites « lien-local ». Ce type d'adresse ne permet à 2 nœuds du réseau de communiquer que s'ils partagent un lien physique direct. C'est ce qui avait été utilisé dans le précédent article sur RIOT puisque les nœuds se voyaient directement via leur interface sans fil (802.15.4),
  • ff00::/8 est utilisé pour les adresses multicast,
  • fd00::/8 est destiné aux adresses « Unique Local Address ». Ces adresses correspondent aux adresses privées en IPv4 et ne sont pas routables.

Lorsqu'un nœud initialise une interface réseau et cherche à configurer son IP, il effectue une procédure dite de découverte de voisins définie par le Neighbor Discovery Protocol, NDP ([RFC4861]). Tout d'abord, il envoie un message ICMPv6 de type « Router Solicitation » et si un routeur est présent sur le même lien, celui-ci renvoie un message ICMPv6 de type « Router Advertisement » contenant le préfixe (ces derniers sont aussi envoyés périodiquement par le routeur). De cette manière, le nœud configure automatiquement son adresse IP globale avec le préfixe en utilisant le format illustré par la figure 2.

3. Communication directe entre 2 nœuds

Maintenant que le cadre et les éléments théoriques sont posés, il est temps de passer à la pratique. Le premier objectif consistera à faire communiquer 2 nœuds par radio 802.15.4 sur un lien local IPv6 à l'aide d'une des applications d'exemple disponibles dans le code source de RIOT, gnrc_networking. Cet exemple est utile pour plusieurs raisons :

  • il utilise le shell de RIOT permettant d'avoir une console interactive sur un nœud,
  • il charge les modules réseau et donne accès à des commandes supplémentaires liées à la configuration réseau et l'envoi de messages UDP entre nœuds,
  • nous pouvons l'utiliser tel quel pour faire communiquer 2 nœuds.

Le principe est illustré par la figure 3.

link-local

Figure 3 : Communication entre 2 nœuds via le lien local en IPv6.

Tout d'abord, il faut télécharger depuis GitHub le code source de la dernière version stable de RIOT (2016.04 au moment où ces lignes sont écrites) :

host$ cd

host$ git clone --depth 1 --branch 2016.04 https://github.com/RIOT-OS/RIOT.git

Toutes les manipulations qui suivent ont été effectuées sur des cartes Atmel SAMR21 Xplained Pro (Figure 4) et un PC sous Ubuntu 15.10 utilisé pour la compilation et pour flasher les firmwares sur les cartes. La version 16.04 d'Ubuntu devrait aussi fonctionner.

samr21-xpro

Figure 4 : La carte Atmel SAMR21 Xplained Pro utilisée pour les exemples de l'article. Cette carte fonctionne avec un microcontrôleur Atmel Samd21 de type ARM Cortex-M0 et dispose d'une antenne 802.15.4.

Sur le PC hôte, si ce n'est pas déjà fait, il faut également installer la chaîne de compilation pour l'architecture ARM du MCU de la SAMR21 :

host$ sudo apt install build-essential g++-multilib openocd gcc-arm-none-eabi gdb-arm-none-eabi binutils-arm-linux-gnueabi python-serial

Et ajouter l'utilisateur <user> aux groupes plugdev et dialout :

host$ sudo adduser <user> plugdev

host$ sudo adduser <user> dialout

Si vous rencontrez des problèmes avec les outils de compilation, une solution est d'utiliser l'image vagrant fournie par le projet (qui utilise Ubuntu 15.10) et est déjà préconfigurée. La documentation d'installation et d'utilisation est disponible à l'adresse https://github.com/RIOT-OS/RIOT/tree/master/dist/tools/vagrant. Il faudra aussi installer le logiciel VirtualBox. Sous Ubuntu, la commande d'installation est la suivante :

host$ sudo apt-get install vagrant virtualbox virtualbox-ext-pack virtualbox-guest-additions-iso virtualbox-guest-dkms virtualbox-guest-utils

Sur le PC, pour simplifier les manipulations, notamment lors de la compilation des firmwares, on peut définir les variables d'environnement BOARD et RIOTBASE et les ajouter à ~/.bashrc :

[…]

export BOARD=samr21-xpro

export RIOTBASE=~/RIOT

L'environnement utilisateur doit alors être rechargé :

host$ . ~/.bashrc

Si vous avez bien suivi les étapes précédentes, le code source de l'exemple gnrc_networking doit se trouver dans ~/RIOT/examples/gnrc_networking et vous pouvez brancher et récupérer l'identifiant des 2 cartes à l'aide de l'outil fourni par RIOT, ~/RIOT/dist/tools/usb-serial/list-ttys.sh. Ces informations permettent d'identifier chaque carte pendant les étapes de flashage (serial) et de se connecter à leur port série (tty).

host$ ~/RIOT/dist/tools/usb-serial/list-ttys.sh

/sys/bus/usb/devices/2-2: Atmel Corp. EDBG CMSIS-DAP serial: '<SERIAL1>', tty(s): ttyACM0

/sys/bus/usb/devices/2-3.4: Atmel Corp. EDBG CMSIS-DAP serial: '<SERIAL2>', tty(s): ttyACM1

Dans un premier terminal (qui sera nommé par la suite term1), on commence par flasher la carte identifiée par SERIAL=<SERIAL1> et se connecter sur PORT=/dev/ttyACM0:

host$ cd ~/RIOT/examples/gnrc_networking

host$ make SERIAL=<SERIAL1> PORT=/dev/ttyACM0 flash term

Building application "gnrc_networking" for "samr21-xpro" with MCU "samd21".

[…]

Done flashing

/home/user/RIOT/dist/tools/pyterm/pyterm -p "/dev/ttyACM0" -b "115200"

Twisted not available, please install it if you want to use pyterm's JSON capabilities

2016-05-23 10:38:46,570 - INFO # Connect to serial port /dev/ttyACM0

Welcome to pyterm!

Type '/exit' to exit.

L'opération est répétée dans un second terminal (term2), cette fois-ci sur la carte SERIAL=<SERIAL2> et PORT=/dev/ttyACM1 :

host$ cd ~/RIOT/examples/gnrc_networking

host$ make SERIAL=<SERIAL2> PORT=/dev/ttyACM1 flash term

À ce stade, un terminal série est ouvert sur chacune des 2 cartes (via l'outil pyterm), ce qui permet d'interagir avec elles en utilisant les commandes du shell RIOT.  La commande help, renvoie la liste des commandes disponibles. La commande ifconfig affiche la configuration réseau de la carte sur term1 :

> ifconfig

Iface 7   HWaddr: 3f:16  Channel: 26  Page: 0  NID: 0x23

           Long HWaddr: 5a:41:2d:52:e4:33:3f:16

           TX-Power: 0dBm  State: IDLE  max. Retrans.: 3  CSMA Retries: 4

           AUTOACK  CSMA  MTU:1280  HL:64  6LO  RTR  RTR_ADV  IPHC  

           Source address length: 8

           Link type: wireless

           inet6 addr: ff02::1/128  scope: local [multicast]

           inet6 addr: fe80::5841:2d52:e433:3f16/64  scope: local

           inet6 addr: ff02::1:ff33:3f16/128  scope: local [multicast]

On voit que cette carte n'a qu'une interface réseau sans fil Iface 7 qui correspond à l'antenne 802.15.4. 3 adresses IP sont définies sur cette interface et en particulier 1 adresse « unicast » de type lien local : fe80::5841:2d52:e433:3f16/64.

Cette interface ne dispose pas d'adresse globale, car aucun routeur ne se trouve à proximité pour propager un préfixe. La carte term1 ne peut par conséquent pas communiquer avec d'autres hôtes à travers Internet. Par contre, puisque les 2 cartes sont proches et utilisent le même lien physique (canal 26 en 802.15.4), il est tout de même possible de les faire communiquer entre elles directement.

Pour cela, le shell dispose de la commande udp qui peut démarrer un serveur UDP ou envoyer des données par UDP. Sur term1, le serveur UDP est mis en écoute sur le port 8888 :

> udp server start 8888

Success: started UDP server on port 8888

Et depuis term2, les données sont envoyées par UDP à term1 :

> udp send fe80::5841:2d52:e433:3f16 8888 "Coucou Open Silicium"

Success: send 20 byte to [fe80::5841:2d52:e433:3f16]:8888

Normalement, les données reçues par le serveur UDP sont affichées sur term1 :

> PKTDUMP: data received:

~~ SNIP  0 - size:  20 byte, type: NETTYPE_UNDEF (0)

000000 43 6f 75 63 6f 75 20 4f 70 65 6e 20 53 69 6c 69

000010 63 69 75 6d

~~ SNIP  1 - size:   8 byte, type: NETTYPE_UDP (4)

   src-port:  8888  dst-port:  8888

   length: 28  cksum: 0x4157d

~~ SNIP  2 - size:  40 byte, type: NETTYPE_IPV6 (2)

traffic class: 0x00 (ECN: 0x0, DSCP: 0x00)

flow label: 0x00000

length: 28  next header: 17  hop limit: 64

source address: fe80::5846:1e6d:b1b9:16b6

destination address: fe80::5841:2d52:e433:3f16

~~ SNIP  3 - size:  24 byte, type: NETTYPE_NETIF (-1)

if_pid: 7  rssi: 42  lqi: 252

src_l2addr: 5a:46:1e:6d:b1:b9:16:b6

dst_l2addr: 5a:41:2d:52:e4:33:3f:16

~~ PKT    -  4 snips, total size:  92 byte

Remarquez le découpage (SNIP) du datagramme : SNIP 0 contient les données, SNIP 1 contient le header UDP, SNIP 2 contient le header IPv6 et SNIP 3 contient le header 802.15.4 MAC.

Le serveur UDP a bien reçu le message de 20 octets dont le contenu est affiché en hexadécimal (en rouge). Les deux cartes arrivent donc à s'échanger des messages UDP à travers leurs interfaces sans fil.

4. Configuration du routeur de bordure

Pour qu'un nœud du réseau puisse communiquer en IPv6 avec un hôte sur Internet, il lui faut une adresse unicast « globale ». Pour cela, il faut ajouter au réseau un routeur de bordure (i.e « Border Router » ou « BR ») qui sera chargé de propager le préfixe aux autres nœuds. On parle ici de routeur de bordure, car il se trouve à la frontière entre un réseau 802.15.4 et un réseau Ethernet. Pour cela, la carte branchée sur term1 est réutilisée pour être transformée en BR en la « flashant » avec le firmware de l'exemple ~/RIOT/examples/gnrc_border_router. Le terminal série sur term1 doit être fermé avec la commande /exit.

Le BR aura une interface reliée à l'ordinateur et une autre interface 802.15.4 pour propager le préfixe et communiquer avec les nœuds du réseau. Le schéma de principe est représenté figure 5 :

border_router

Figure 5 : Principe de la communication entre un nœud et un PC à travers un routeur de bordure.

L'interface reliée à l'ordinateur n'est autre que la liaison série UART qui est multiplexée avec le flux des messages IP. C'est là une nouveauté de la version 2016.04 de RIOT, appelée Ethos pour Ethernet Over Serial. Le PC et le BR communiquent sur ce lien qui peut être vu comme un pont en utilisant leurs adresses lien local respectives. UHCP (Micro host configuration protocol) est utilisé pour configurer les interfaces du BR à travers Ethos. Attention, ces deux outils/protocoles ne sont pas standards.

L'exemple gnrc_border_router est d'abord flashé depuis term1 sur la carte 1 :

host$ cd ~/RIOT/examples/gnrc_border_router

host$ make SERIAL=<SERIAL1> flash

Ensuite, il faut compiler deux programmes nécessaires pour configurer le lien série et le BR :

  • ethos sert à multiplexer à la fois les données réseau (paquet) et séries (shell) sur une seule UART ;
  • uhcpd, un micro « DHCP » propre à Ethos (et RIOT) permet de configurer les adresses du BR.

Les codes sources de ces deux outils se trouvent respectivement dans ~/RIOT/dist/tools/uhcpd et ~/RIOT/dist/tools/ethos :

host$ cd ~/RIOT/dist/tools/uhcpd

host$ make

host$ cd ~/RIOT/dist/tools/ethos

host$ make

Enfin, pour mettre en œuvre toute la configuration depuis l'hôte Linux, RIOT utilise un script d'automatisation ~/RIOT/dist/tools/ethos/start_network.sh effectuant les opérations suivantes :

  • la création d'une interface tap sur le PC, tap0, pour communiquer avec la carte par le lien série ;
  • l'activation du forwarding et de l'autoconfiguration IPv6 dans le kernel de l'hôte Linux (vers tap0) ;
  • l'ajout une adresse « ULA », fd00:dead:beef::1, sur l'interface loopback lo côté hôte Linux. Cette adresse sera utilisable par les nœuds du réseau LLN pour accéder au PC ;
  • l'ajout d'une route pour le préfixe 2001:db8:3c4d:ab::/64 via l'interface tap0 ;
  • le démarrage du démon uhcpd pour que le BR configure ses adresses et préfixes ;
  • le lancement d'ethos.

Dans notre exemple, la carte est visible sur le terminal /dev/ttyACM0, la commande à lancer est donc :

host$ sudo ~/RIOT/dist/tools/ethos/start_network.sh /dev/ttyACM0 tap0 2001:db8:3c4d:ab::/64

net.ipv6.conf.tap0.forwarding = 1

net.ipv6.conf.tap0.accept_ra = 0

----> ethos: sending hello.

----> ethos: activating serial pass through.

----> ethos: hello reply received

got packet from fe80::9098:17ff:fe54:204e port 37102

uhcp: push from fe80::9098:17ff:fe54:204e:37102 prefix=2001:db8:3c4d:ab::/64

gnrc_uhcpc: uhcp_handle_prefix(): configured new prefix 2001:db8:3c4d:ab:5841:2d52:e433:3f16/64

En plus de faire passer le flux des données réseau, ethos donne accès au port série du routeur qui dispose aussi d'un shell. La configuration de ces interfaces peut donc être obtenue par la commande ifconfig :

> ifconfig

Iface 6   HWaddr: 3f:16  Channel: 26  Page: 0  NID: 0x23

           Long HWaddr: 5a:41:2d:52:e4:33:3f:16

           TX-Power: 0dBm  State: IDLE  max. Retrans.: 3  CSMA Retries: 4

           AUTOACK  CSMA  MTU:1280  HL:64  6LO  RTR  RTR_ADV  IPHC  

           Source address length: 8

           Link type: wireless

           inet6 addr: ff02::1/128  scope: local [multicast]

           inet6 addr: fe80::5841:2d52:e433:3f16/64  scope: local

           inet6 addr: ff02::1:ff33:3f16/128  scope: local [multicast]

           inet6 addr: 2001:db8:3c4d:ab:5841:2d52:e433:3f16/64  scope: global

           inet6 addr: ff02::2/128  scope: local [multicast]

           

Iface 7   HWaddr: 00:21:d3:60:5a:8d

           

           MTU:1500  HL:64  RTR  RTR_ADV  

           Source address length: 6

           Link type: wired

           inet6 addr: ff02::1/128  scope: local [multicast]

           inet6 addr: fe80::221:d3ff:fe60:5a8d/64  scope: local

           inet6 addr: ff02::1:ff60:5a8d/128  scope: local [multicast]

           inet6 addr: ff02::2/128  scope: local [multicast]

           inet6 addr: fe80::2/64  scope: local

           inet6 addr: ff02::1:ff00:2/128  scope: local [multicast]

Le préfixe 2001:db8:3c4d:ab s'est bien propagé à l'interface sans fil Iface 6. On remarque aussi l'adresse lien local utilisée pour l'interface filaire (« wired ») Iface 7. C'est sur ce lien que la carte communique avec l'interface tap0 du PC via Ethos.

Dans term2, le préfixe a bien été propagé par le BR vers la deuxième carte :

> ifconfig

Iface  7   HWaddr: 16:b6  Channel: 26  Page: 0  NID: 0x23

           Long HWaddr: 5a:46:1e:6d:b1:b9:16:b6

           TX-Power: 0dBm  State: IDLE  max. Retrans.: 3  CSMA Retries: 4

           AUTOACK  CSMA  MTU:1280  HL:64  6LO  RTR  RTR_ADV  IPHC  

           Source address length: 8

           Link type: wireless

           inet6 addr: ff02::1/128  scope: local [multicast]

           inet6 addr: fe80::5846:1e6d:b1b9:16b6/64  scope: local

           inet6 addr: ff02::1:ffb9:16b6/128  scope: local [multicast]

           inet6 addr: 2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6/64  scope: global

           inet6 addr: ff02::2/128  scope: local [multicast]

Son adresse IPv6 globale est 2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6 et elle est donc en mesure de recevoir un message ICMPv6 envoyé depuis le PC avec ping6 :

host$ ping6 -c 1 2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6

PING 2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6(2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6) 56 data bytes

64 bytes from 2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6: icmp_seq=1 ttl=63 time=41.2 ms

--- 2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6 ping statistics ---

1 packets transmitted, 1 received, 0% packet loss, time 0ms

Inversement, un message ICMPv6 peut être envoyé depuis le shell de la carte term2 vers l'ULA fd00:dead:beef::1 configurée par start_network.sh sur le PC :

> ping6 fd00:dead:beef::1

12 bytes from fd00:dead:beef::1: id=83 seq=1 hop limit=63 time = 46.507 ms

[…]

--- fd00:dead:beef::1 ping statistics ---

3 packets transmitted, 3 received, 0% packet loss, time 2.06117395 s

Pour faire communiquer un nœud avec Internet (et inversement), le PC sous Linux doit disposer, d'une part, sur une autre de ses interfaces, d'une adresse IPv6 globale (routable) et, d'autre part, d'un préfixe supplémentaire IPv6, lui-même routé vers le PC. Certains fournisseurs d'accès Internet offrent les deux ; et si vous n'êtes pas dans ce cas, une autre solution potentielle consiste à passer par un « tunnel broker » comme https://www.tunnelbroker.net/.

Sur le PC, il faut ensuite activer le routage (https://wiki.ubuntu.com/IPv6) entre l'interface tap0 et l'interface disposant d'une adresse IPv6 globale, et utiliser le préfixe supplémentaire IPv6 dans la commande start_network.sh.

Vous pourrez alors envoyer un message ICMPv6 vers un hôte IPv6 sur le Web depuis la carte branchée sur term2 (et inversement). Exemple pour un serveur DNS Google :

> ping6 2001:4860:4860::8888

12 bytes from 2001:4860:4860:8888: id=83 seq=1 hop limit=52 time = 46.507 ms

[…]

--- 2001:4860:4860:8888 ping statistics ---

3 packets transmitted, 3 received, 0% packet loss, time 2.06117395 s

5. Accès aux nœuds par CoAP

Le routeur de bordure est désormais fonctionnel et assure la connectivité IP entre les cartes et le PC. Il reste alors à s'intéresser au niveau applicatif pour accéder aux ressources des nœuds du réseau (capteurs, LED, bouton, etc.). Pour cela, RIOT permet d'utiliser le protocole [CoAP].

Les spécifications de CoAP ont été proposées par le groupe de travail CoRE (Constrained RESTful Environments) de l'IETF. Le protocole repose sur l'utilisation de requêtes de type REST pour accéder aux ressources des nœuds du réseau tout en restant compatible avec les contraintes des LLN. Le serveur CoAP (généralement en écoute sur le port 5683) s'exécute sur un nœud du réseau et répond, comme en REST, aux requêtes GET, POST, PUT et DELETE.

Le code source de RIOT contient l'exemple microcoap_server permettant d'exposer un nœud de notre réseau comme une ressource CoAP. Pour envoyer des requêtes CoAP, cet exemple se sert de [microcoap], une bibliothèque externe importée dans RIOT sous la forme d'un paquetage externe (voir dossier ~/RIOT/pkg). Lors de la première compilation du firmware, les sources de microcoap sont téléchargées avec Git, patchées pour être rendues compatibles avec l'architecture de RIOT puis compilées.

Cet exemple est donc flashé sur la deuxième carte (term2) :

host$ cd ~/RIOT/examples/microcoap_server

host$ make SERIAL=<SERIAL2> PORT=/dev/ttyACM1 flash term

[…]

Waiting for incoming UDP packet...

Par défaut, l'exemple microcoap_server retourne seulement le type de la carte à une requête GET sur l'URL coap://[adresse]/riot/board. Ici l'adresse en question est celle de la carte term2, comme on l'a vu dans la précédente partie : 2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6.

Depuis un nouveau terminal sur le PC, il suffit alors d'utiliser le client CoAP de [libcoap] pour interroger le nœud. Comme libcoap n'est pas disponible dans les dépôts officiels Ubuntu, il faut commencer par en récupérer les sources depuis GitHub puis les compiler :

host$ cd

host$ sudo apt-get install autoconf libtool

host$ git clone https://github.com/obgm/libcoap.git

host$ cd libcoap

host$ ./autogen.sh

host$ ./configure --disable-documentation && make

La requête GET envoyée avec le programme examples/coap-client sur l'URL mentionnée plus haut retourne alors le type de la carte sur laquelle s'exécute le serveur CoAP :

host$ ~/libcoap/examples/coap-client -m get coap://[2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6]/riot/board

v:1 t:CON c:GET i:28e4 {} [ ]

samr21-xpro

Il est aussi très facile de modifier le code source de l'exemple pour étendre l'API exposée par le serveur CoAP du nœud. Cette API est définie et implémentée dans le fichier ~/RIOT/examples/microcoap_server/coap.c. À partir de la ligne 25, y sont d'abord initialisées des structures pour définir les chemins disponibles (coap_endpoint_path_t) puis la liste des endpoints vers les fonctions callback associées (coap_endpoint_t) :

static const coap_endpoint_path_t path_well_known_core =

        { 2, { ".well-known", "core" } };

static const coap_endpoint_path_t path_riot_board =

        { 2, { "riot", "board" } };

const coap_endpoint_t endpoints[] =

{

    { COAP_METHOD_GET,  handle_get_well_known_core,

        &path_well_known_core, "ct=40" },

    { COAP_METHOD_GET,  handle_get_riot_board,

        &path_riot_board,          "ct=0"  },

    /* marks the end of the endpoints array: */

    { (coap_method_t)0, NULL, NULL, NULL }

};

Une requête GET (COAP_METHOD_GET) sur le chemin /.well-known/core renvoie la liste des chemins disponibles et définit la fonction handle_get_well_known_core comme endpoint (voir son implémentation ligne 41). D'après les spécifications CoAP, ce chemin doit être défini par défaut et retourner la liste des ressources disponibles sur le nœud. De même, est défini pour /riot/board le endpoint d'une requête GET vers la fonction handle_get_riot_board. Il est très simple d'adapter cet exemple pour récupérer par CoAP les valeurs d'un capteur branché sur la carte (voir l'article sur RIOT dans le précédent numéro). La fin du fichier contient les implémentations de ces fonctions callback (voir l'implémentation de handle_get_riot_board, ligne 89).

Microcoap supporte aussi les requêtes PUT (COAP_METHOD_PUT) : un endpoint peut donc être rajouté pour piloter la led qui se trouve sur la carte depuis le client CoAP.

Pour cela, comme il s'agit de piloter un périphérique GPIO de la carte, il faut inclure les définitions nécessaires dans coap.c :

#include "board.h"

#include "periph/gpio.h"

Puis, définir le chemin /led dans notre API :

static const coap_endpoint_path_t path_led =

        { 1, { "led" } };

Ensuite, ajouter un endpoint vers une fonction handle_put_led dans le tableau endpoints déjà défini :

const coap_endpoint_t endpoints[] =

{

[…]

    { COAP_METHOD_PUT,  handle_put_led,

      &path_led,             "ct=0" },

    /* marks the end of the endpoints array: */

    { (coap_method_t)0, NULL, NULL, NULL }

};

Définir et implémenter la fonction handle_put_led :

/* définition de la fonction à mettre en début de fichier */

static int handle_put_led(coap_rw_buffer_t *scratch,

                          const coap_packet_t *inpkt,

                          coap_packet_t *outpkt,

                          uint8_t id_hi, uint8_t id_lo);

static int handle_put_led(coap_rw_buffer_t *scratch,

                          const coap_packet_t *inpkt,

                          coap_packet_t *outpkt,

                          uint8_t id_hi, uint8_t id_lo)

{

  coap_responsecode_t resp = COAP_RSPCODE_CHANGED;

  

  /* On vérifie que la valeur donnée est correcte (0 ou 1)*/

  uint8_t val = inpkt->payload.p[0];

  if ((inpkt->payload.len == 1) &&

    ((val == '1') || (val == '0'))) {

    /* écriture de la nouvelle valeur de la led */

    gpio_write(LED0_PIN, (val - '1'));

  }

  else {

    puts("wrong payload");

    resp = COAP_RSPCODE_BAD_REQUEST;

  }

  /* Réponse faite au client */

  return coap_make_response(scratch, outpkt, NULL, 0,

                            id_hi, id_lo,

                            &inpkt->tok, resp,

                            COAP_CONTENTTYPE_TEXT_PLAIN);

}

Enfin, la feature periph_gpio est requise dans le fichier Makefile :

FEATURES_REQUIRED += periph_gpio

Après flashage de la carte sur term2, la led de la carte est alors pilotable depuis le PC avec coap-client :

- LED allumée :

host$ ~/libcoap/examples/coap-client -m put -e 1 coap://[2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6]/led

- LED éteinte :

host$ ~/libcoap/examples/coap-client -m put -e 0 coap://[2001:db8:3c4d:ab:5846:1e6d:b1b9:16b6]/led

À noter qu'il existe d'autres outils pour interagir avec un serveur CoAP :

Grâce à des bibliothèques de ce type, il est alors tout à fait envisageable de développer des applications web pour piloter et interroger des objets à travers Internet.

Vous voilà donc prêts pour développer avec RIOT des objets connectés utilisant les protocoles standards de l'IoT.

Conclusion

Cet article vous a présenté le principe et la mise en place avec RIOT d'un réseau basé sur la pile 802.15.4/6LowPAN/IPv6/CoAP pour le faire communiquer avec Internet. Cependant, il reste encore quelques étapes avant d'avoir un réseau d'objets connectés parfaitement fonctionnel et facile à déployer. Par exemple, avec les instructions données ici, le client doit connaître l'adresse IP des nœuds pour savoir à qui envoyer une requête CoAP. Le groupe de travail CoRE a fait une proposition dans ce sens avec [PubSub], mais cela nécessite un peu de travail supplémentaire pour la mettre en œuvre.

Nous n'avons également pas parlé des aspects liés à la sécurité lors des échanges de données entre nœuds et clients. En effet, tous les messages transitent « en clair » par radio... Pour limiter le risque d'écoute, le projet Eclipse développe une implémentation du protocole DTLS adaptée aux systèmes fortement contraints [TinyDTLS]. Un portage de cette bibliothèque est actuellement en cours [PR5395] et sera disponible dans la prochaine version de RIOT qui sortira en juillet (2016.07).

Enfin, pour échanger des informations avec un objet connecté, d'autres protocoles applicatifs existent et commencent à être considérés par la communauté en vue d'être intégrés prochainement dans RIOT. Citons par exemple [MQTT] utilisé depuis de nombreuses années et assez répandu et [IoTivity], un projet plus récent qui semble prometteur, et beaucoup d'autres architectures liant les objets IoT et le Cloud (comme Node-Red...).

Remerciements

Je tiens à remercier Cédric Adjih, chercheur chez Inria, ainsi que Francisco Acosta et José Alamos, ingénieurs chez Inria pour leurs conseils éclairés et pour le temps qu'ils ont pu me consacrer pour relire (et corriger) cet article.

Bibliographie

[RIOT] http://riot-os.org/

[Contiki] http://www.contiki-os.org/

[Zephyr] https://www.zephyrproject.org/

[Low power] http://www.silabs.com/Support%20Documents/TechnicalDocs/low-power-32-bit-microcontroller-dtm.pdf

[802.15.4] https://tools.ietf.org/html/rfc6282

[6LowPAN] https://tools.ietf.org/html/rfc6282

[RPL] https://tools.ietf.org/html/rfc6550

[CoAP] http://tools.ietf.org/html/rfc7252#section-8

[RFC4291] https://tools.ietf.org/html/rfc4291

[RFC2460] https://tools.ietf.org/html/rfc2460

[RFC3306] https://tools.ietf.org/html/rfc3306

[RFC3307] https://tools.ietf.org/html/rfc3307

[RFC3849] https://www.ietf.org/rfc/rfc3849.txt

[RFC 5952] https://tools.ietf.org/html/rfc5952

[RFC4861] https://tools.ietf.org/html/rfc4861

[PR5291] https://github.com/RIOT-OS/RIOT/pull/5291

[PR4562] https://github.com/RIOT-OS/RIOT/pull/4562

[microcoap] https://github.com/1248/microcoap

[libcoap] https://libcoap.net/doc/install.html

[PubSub] https://tools.ietf.org/html/draft-koster-core-coap-pubsub-01

[TinyDTLS] https://projects.eclipse.org/projects/iot.tinydtls

[PR5395] https://github.com/RIOT-OS/RIOT/pull/5395

[MQTT] http://mqtt.org/

[IoTivity] https://www.iotivity.org/



Article rédigé par

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous