open1024.fr

Libérez vos octets !

Outils pour utilisateurs

Outils du site


Panneau latéral

iot:cours_fun_mooc_iot

2021-11-11: Début du cours

Mémo IOT

Le cours est organisé par FUN-MOOC:
- URL: https://www.fun-mooc.fr/fr/cours/programmer-linternet-des-objets/
- 5 semaines avec 5h par semaine en moyenne
- Cours: Du 5 octobre 2021 au 18 décembre 2021
- Ce que vous allez apprendre:
→ Fabriquer et programmer un objet connecté
→ Intégrer un objet dans un système “Internet des Objets” (IOT)
→ Structurer les données transmises par un objet en vue de leur interopérabilité
→ Traiter les données transmises par un objet au sein d‘une application de monitorage

Brain map

REST

REST: REpresentational State Transfer.

Voici les 6 propriétés qui définissent REST

  • architecture client-serveur
  • sans état (ou stateless) : cela signifie que l’état de l’interaction entre le client et le serveur n’est pas stocké.
  • cacheability : les périphériques intermédiaires ainsi que les clients peuvent mettre en cache les réponses pour une utilisation ultérieure. Cela conduit à améliorer les performances.
  • système en couches : cela signifie que les services peuvent être réalisés en utilisant des couches telles que différents serveurs responsables de différentes parties de services (stockage, recherche, etc.), mais le client ne sait pas s’il est connecté au serveur final ou intermédiaire.
  • code à la demande (facultatif) : les serveurs peuvent éventuellement envoyer du code exécutable aux clients.
  • interface uniforme : elle fait référence aux principes selon lesquelles une ressource doit avoir une seule représentation telle que les URI (Uniform Resource Identifier), et suivre certaines directives pour la dénomination

REST - Serveur sans état

Le principe REST permet de concevoir des serveurs évolutifs. Un serveur doit être sans état, ce qui signifie qu’il ne conserve pas d’information après avoir répondu à une demande d’un client. Cela permet de simplifier le traitement dans le serveur qui doit traiter les requêtes d'un grand nombre de clients.

HTTP

HTTP est un protocole qui peut être utilisé pour mettre en œuvre un serveur Web les principes de REST (qualifié en anglais de RESTfull). HTTP définit différentes méthodes permettant au client d’interagir avec les ressources sur le serveur :

  • GET est utilisée pour récupérer la représentation d’une ressource (par exemple page Web, valeur de température d’un capteur, etc.). Par exemple, la figure ci-dessous donne le format d’en-tête HTTP GET pour récupérer ma page Web :


contenu d’un GET HTTP (capture d’écran de l’outil Wireshark)

  • HEAD est utilisée pour récupérer uniquement les métadonnées présentes dans les en-têtes de réponse sans le corps de réponse ;
  • POST est utilisée pour indiquer au serveur une nouvelle ressource ;
  • PUT est utilisée pour stocker une ressource à l’endroit identifié par l’URI dans la requête. Si la ressource existe déjà, elle sera modifiée ;
  • PATCH permet au client de ne modifier qu’une partie de la ressource ;
  • DELETE est utilisée pour supprimer la ressource définie.

HTTP réponse

  • 1xx: Information
  • 2xx: Réponse positive. Exemple: 200 = OK
  • 3xx: Redirection.
  • 4xx: Erreur coté client. Exemple: 404 = Not Found
  • 5xx: Erreur coté serveur.

Le site web de l'IANA (http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml\#http-status-codes-1) donne les erreurs que l’on retrouve dans le protocole HTTP. Comme indiqué précédemment, le chiffre de gauche varie entre 1 et 5 tandis que les deux chiffres de droite, précisant la raison de la notification, varient généralement entre 0 et 31.

Publish/Subscribe

publish/subscribe vise des applications intégrées tandis que REST vise l’interopérabilité globale.

Le modèle publish/subscribe fait le découplage entre l’expéditeur d’un message et son destinataire.
Dans ce paradigme (cf. figure ci-dessous), il existe des ”Publishers” qui produisent des données ou des messages et envoient le message à une entité généralement appelée ”Broker”. En outre, les messages peuvent être classés en ”Topics”, contenus ou types, etc.
Ensuite, il existe des abonnés qui souscrivent au broker, par exemple à un topic donné, afin de recevoir les messages qui les intéressent, comme montré dans le schéma.
Le broker peut alors utiliser des filtres pour envoyer uniquement ces messages aux abonnés du topic concerné.
Il existe plusieurs protocoles Publish-Subscribe:

  • MQTT (Message Queuing Telemetry Transport)
  • AMQP (Advanced Message Queuing Protocol)
  • JMS (Java Messaging Service)
  • XMPP (Extensible Messaging Protocol et Présence)

Exemple de structuration de données

Imaginons par exemple que plusieurs capteurs soient installés dans deux bâtiments A et B.
Certains capteurs collectent des informations sur la température et d’autres collectent des informations sur l’humidité.
Ces capteurs peuvent envoyer les données régulièrement à un broker central.

Les données peuvent être classées en différentes rubriques qui peuvent également être organisées de manière hiérarchique.
Par exemple, le topic /sensor signifie “toutes les données de capteurs”, /sensor/buildingA/ signifie “des données de capteurs uniquement installées dans le bâtiment A”.
En plus, /sensor/buildingA/temperature pourrait signifier “des données de capteurs de température installés uniquement dans le bâtiment A”.

Certains abonnés peuvent s’abonner aux messages en fonction de leur intérêt.
Ainsi, un abonné intéressé uniquement par les données d’humidité du bâtiment B peut s’abonner au sujet /sensor/buildingB/humidity et le broker n’enverra que ces données à cet abonné.

Client/Serveur versus Publish/Subscribe

Les principaux avantages du paradigme publish-subscribe par rapport au paradigme client-serveur, tels qu'inclus en REST, sont les suivants :

  • faible couplage entre émetteur et récepteur, le broker sert d'intermédiaire et stocke les informations ;
  • passage à l’échelle.
    • Les données provenant d'une source ne sont émises qu'une fois par la source.
    • Le broker les recopie vers tous les abonnés.
    • Dans un mode client/serveur, les données doivent être émises par le serveur autant que fois que les clients le demandent.

L’absence de couplage entre l’expéditeur et le destinataire se fait en termes d’espace, de temps et de synchronisation.
Celui qui publie les données a une tâche simplifiée.
Il n'a pas à gérer ou connaître ceux qui les consomment, il n'a qu’à les envoyer au broker.

MQTT

MQTT est très léger et conçu pour les périphériques de faibles puissances.
Il a une très petite empreinte logicielle et est optimisé pour fonctionner dans les environnements à faible bande passante.
Cela rend MQTT idéal pour les applications IoT.
Malgré tout, l’usage de TCP et des très nombreux acquittements peut s’avérer lourds pour les équipements ou les réseaux très contraints.
Une version plus légère basée sur UDP existe pour ces cas d’usage, mais elle est peu utilisée.

S'ils se ressemblent, les principes de nommage des topics MQTT et des URI REST sont complètement différents.
Par rapport à MQTT, le chemin dans l'URI n'a pas de sémantique. Il a juste vocation à être unique.
Il ne peut pas être utilisé pour agréger plusieurs sources d'information.
Si deux capteurs publient respectivement sur les topics /sensor/buildingA/temperature et /sensor/buildingB/temperature, un subscriber peut s'abonner au topic /sensor/*/temperature pour recevoir toutes les mesures ;
Ce qui est impossible avec REST : il faudra autant de requêtes que de capteurs pour récupérer l'ensemble des mesures.

Les URI sont simplement uniques au monde par leur construction alors que les topics du MQTT sont spécifiques à une application. Un topic MQTT peut être interprété différemment par deux applications différentes. Cela ne permet pas une interopérabilité sémantique. Les abonnés doivent être construits avec une connaissance des topics utilisés par les publieurs.

Architecture de l'IoT

Objectif: Réduire la consommation d'énergie, donc réduction des couches réseau
Comparaison des Architecture de le l'internet et architecture de l'IoT:

Couche 1 et 2

Les topologies de réseaux

Les réseaux pour l’internet des objets peuvent être divisés en deux catégories :

  • les topologies maillées (mesh in english).
  • étoilées (star).


Réseaux maillés

Les réseaux maillés, tels que la famille IEEE 802.15.4, sont une adaptation d’un protocole d’accès Wi-Fi pour préserver l’énergie. La portée de transmission est limitée à 50 mètres pour limiter la consommation d'énergie ; et par conséquent les messages doivent être relayés par d’autres nœuds pour atteindre leur destination.

Le débit est de quelques centaines de kilobits/s et la taille de la trame est de quelques centaines d’octets.

Ces réseaux sont performants pour transporter des données IoT, mais le protocole de routage, ainsi que le relayage des trames, consomment l’énergie des objets.

Topologies en étoile

Les topologies en étoile ne nécessitent pas de tels mécanismes de routage. Toutes les communications se font avec un point central qui relaie les informations vers la destination.

Les progrès réalisés dans le traitement des signaux permettent d’étendre la portée de transmission à faible puissance. Cette famille de réseaux est appelée réseaux étendus à faible puissance (Low-Power Wide-Area Networks, LPWAN) comme Sigfox, LoRaWAN, ou même du côté de la téléphonie cellulaire avec des évolutions de la norme 4G et une intégration plus complète dans la 5G. Le [RFC 8376] donne, en anglais, un aperçu de ces techniques.

Avec une puissance de transmission de 25 mW, il est possible de communiquer sur une distance de 3 km en milieu urbain et de 20 km dans un environnement dégagé. Les LPWAN sont compatibles avec les appareils de classe 0 car ils ne nécessitent pas la mise en place d’une pile IP. La figure ci-dessous décrit une architecture typique pour les LPWAN.



L’appareil envoie des données brutes sur le réseau radio.
Le signal radio est capté par une ou plusieurs passerelles radio, et la trame est envoyée à une passerelle réseau.
Exemple

  • LNS - LoRa Network Server- pour les réseaux LoRaWAN
  • SCEF - Service Capability Exposure Function Gateway - pour les réseaux 3GPP

Le propriétaire de l’appareil a associé l’appareil à un connecteur dans le LPWAN Gateway (NGW) qui peut être un URI (Universal Ressource Identifier), une adresse de broker MQTT (Message Queuing Telemetry Transport) ou une Web socket.
Lorsque l’appareil envoie des données, il est relié à l’application par ce tunnel.

Certaines technologies telles que LoRaWAN ou Sigfox utilisent des bandes sans licence, imposant un cycle d’utilisation (duty cycle) de 0,1 à 10 % selon les canaux pour assurer l’équité entre les nœuds, empêchant ainsi qu'un équipement ne monopolise le canal de transmission.
Comme cette restriction s’applique également à l’antenne du fournisseur, la communication entre le réseau et l’appareil est considérablement limitée.

L’utilisation principale de ces réseaux LPWAN est la télémétrie où un appareil envoie régulièrement des informations ou une alarme de temps en temps (par exemple des capteurs de température).
Le débit et la taille des messages est beaucoup plus réduit que dans le cas de réseaux maillés.

IPv6 et couches d'adaptation


Une couche d'adaptation entre la couche IP et le niveau 2 est nécessaire puisque les niveaux 2 conçus pour l'internet des objets ne peuvent pas transporter naturellement de grands paquets.
Deux actions sont mises en œuvre :

  • compression de la taille des en-têtes pour réduire leur impact
  • fragmentation pour découper le paquet en petites trames si la première mesure ne suffit pas.

Il existe deux grandes familles de couche d'adaptation :

  • 6LoWPAN [RFC4944, RFC 6282], qui va intégrer un mécanisme de compression de l'en-tête IPv6 et de fragmentation pour envoyer un gros paquet divisé en petites trames.

En effet, dans un réseau maillé, il n'est pas possible de se priver d'informations fournies par la couche IP car les nœuds intermédiaires en ont besoin pour acheminer le message vers le destinataire.
6LoWPAN est sans état et compresse toutes les en-têtes IPv6 sans configuration.

  • SCHC (prononcer chic) pour Static Context Header Compression [RFC 8724] va imposer des règles décrivant l'en-tête du message et va envoyer le numéro de la règle en remplacement de l'en-tête. La compression est beaucoup plus importante et peut porter sur plusieurs couches protocolaires. Cependant, pour la mettre en œuvre, il faut avoir une idée des flux qui vont circuler sur le réseau. SCHC est spécifié pour les réseaux en étoile et plus particulièrement les LPWAN.

UDP et CoAP

Pour poursuivre dans l’intégration des objets dans l’internet, le protocole CoAP (Constraint Application Protocol) [RFC 7252] se substitue à HTTP. Il en reprend le mécanisme de nommage, d’utilisation des ressources, et les primitives de manipulation entre un client et un serveur.

La capacité de traitement du capteur et son alimentation en énergie sont souvent très limitées. La grande force de CoAP est d’être :

  • facile à mettre en œuvre. Les mises en œuvre de CoAP nécessitent peu de mémoire ;
  • entièrement compatible avec HTTP et il est possible d’aller d’un protocole à l’autre au travers de passerelles génériques, c’est-à-dire non liées à un usage particulier.

De ce fait, CoAP va manipuler des ressources, identifiées par des URI. Il est donc possible d'ancrer les données fournies par les objets dans l'écosystème actuel des communications entre ordinateurs, fortement structuré autour des principes REST.

La sécurité, en particulier le chiffrement des données, suit aussi les mêmes chemins que l’internet traditionnel. Il existe un chiffrement au-dessus d’UDP qui, à l’instar de HTTPS, chiffre les échanges.

Les réseaux LPWAN

Caractéristiques

Pour simplifier les infrastructures, une nouvelle catégorie de réseaux est apparue, dédiée à l’internet des objets :
les LPWAN (Low Power Wide Area Network).
Comme leur nom l’indique, ces réseaux couvrent une large étendue (de l’ordre de 2 km en ville et 15 km sans obstacle) pour une faible consommation d'énergie.
En contrepartie, le trafic est très limité et s’en tient à une centaine de messages par jour.

Sigfox et LoRaWAN

Pionnier de cette technologie, Sigfox définit son propre protocole de transmission et opère son réseau dans un grand nombre de pays.

D'autres technologies radio présentent des performances similaires à Sigfox, en particulier la modulation LoRa.
La LoRa Alliance regroupant des industriels à défini le standard LoRaWAN. L'approche commerciale est différente de Sigfox.
Sigfox fédère le marché en ayant défini le standard permettant aux objets de communiquer avec des applications généralement situées dans le cloud.

La LoRa Alliance repose sur une modulation propriétaire intégrée aux composant produit par la société Semtech, mais il n'y a pas un unique réseau LoraWAN.
Des opérateurs, comme en France Bouygues Télécom et Orange offrent une couverture nationale, mais d'autres initiatives existent, comme The Things Network qui propose une approche collaborative ; des propriétaires d'antenne LoRaWAN les mettent à disposition de la communauté pour construire un réseau mondial.
Il est également possible de déployer son propre réseau LoRaWAN pour couvrir un domicile, une entreprise, une ville, …

Contrainte des LPWAN sur fréquences non-licenciées

Sigfox et LoRaWAN opèrent dans la bande non licenciée des 868 MHz qui offre une faible atténuation et une bonne pénétration dans les bâtiments.
Pour permettre un accès équitable, le régulateur impose qu’un émetteur n’émette pas plus qu'un pourcentage du temps.
Ce pourcentage dépend de la fréquence et varie entre 0.1% et 10%.
Si, pour un objet, ce n’est pas une contrainte, cela le devient pour les stations de l’infrastructure qui doivent limiter les transmissions vers les objets.

duty-cycling Le mécanisme le plus populaire est le duty-cycling où le nœud désactive la communication pendant un certain temps, proportionnel au temps de transmission.
Ainsi pour un duty-cycle de 1%, l'émission d’une trame de 1 seconde impose un silence de 99 secondes, soit 1 minute et 39 secondes (voir figure ci-dessous).

Architecture LPWAN

Les LPWAN ont une architecture en étoile autour d’une Radio Gateway.

Caractéristiques des réseaux LPWAN

  • Portée de 15 à 20 km dans des environnements avec peu de distorsion et environ 2 à 5 km dans les environnements urbains.
  • Débits de données de 0,3 kbit/s à 50 kbit/s pour LoRaWAN et 100 bit/s pour Sigfox.
  • Durée de vie d'un objet jusqu'à 10 ans sur batterie.

Sigfox
Un objet Sigfox peut envoyer jusqu’à 140 messages par jour (la taille de la charge utile pour chaque message est de 12 octets).
Dans le sens contraire, Sigfox n'autorise que 4 messages de 8 octets chacun pour configurer l'équipement.

LoRaWAN
LoRaWAN est moins contraint, les opérateurs n'imposent pas de limitation sur le nombre de messages émis par un objet, s'il reste dans la contrainte du duty cycle, en revanche les messages vers les objets sont limités ou facturés.

Réseau LPWAN sur fréquence spécifique

Arrivé plus tardivement sur le marché, le 3GPP, avec l'évolution des protocoles de 4G et en ligne de mire la 5G, propose également un mode économe en énergie qui s’apparente aux technologies LPWAN.
En bénéficiant de l’infrastructure et des fréquences des opérateurs, les limitations imposées sur les fréquences non licenciées tombent, la bande de fréquence peut être utilisée à 100% en émission et réception pour les objets rattachés à un opérateur.

Les communications vers les objets sont facilitées et le réseau peut coordonner leur instant d'émission, autorisant de futurs usages comme les communications avec les véhicules ou les robots.

Wireshark



L'écran de Wireshark se divise en 3 parties :

  • en haut, principalement en vert, les trames qui ont circulé sur le réseau sont affichées synthétiquement sur une ligne. Chaque ligne contient :
    • le numéro de trame capturée, il s'agit d'une information ajoutée par Wireshark,
    • l'heure de capture de la trame. Cette information est aussi ajoutée par Wireshark,
    • l'adresse IP de la machine à l'origine du paquet,
    • l'adresse IP de la machine destinataire du paquet,
    • le protocole de plus haut niveau contenu dans la trame.
      Dans notre cas, cela peut être TCP si le message TCP ne contient pas de données, comme lors de l'ouverture de connexion, ou de certains acquittements.
      On voit également les messages HTTP qui sont bien entendu encapsulés dans TCP,
    • la taille en octets de la trame capturée par Wireshark,
    • finalement Wireshark fourni un résumé du contenu de la trame, pour comprendre ce qui se passe sur le réseau. Dans la capture, on retrouve pour les messages HTTP, les requêtes GET ou les notifications ;
  • si une trame est sélectionnée dans la liste, elle apparait dans la zone du milieu avec l'empilement protocolaire.
    Le contenu de chacun de ces protocoles peut être détaillé en cliquant sur le petit triangle à gauche ;
  • la fenêtre du bas donne l'équivalent en hexadécimal.
    Les parties surlignées correspondent aux champs sélectionnés dans la fenêtre du milieu. \\À noter que l'on retrouve l'information à la fois en hexadécimal et en caractère ASCII, ce qui aide à la lecture quand on cherche une valeur spécifique.



Visualisation du protocole TCP


Pour ouvrir une connexion (c'est-à-dire établir un contexte aux deux extrémités pour superviser l'échange de données), TCP utilise 3 messages :

  • SYN pour indiquer à l'autre extrémité que la machine veut ouvrir la connexion ;
  • l'autre extrémité répond avec un SYN ACK ;
  • puis l'initiateur de la connexion répond à ce dernier message par un message ACK.
  • → Cet échange est appelé poignée de main à trois voies.


Une fois la connexion ouverte, le message TCP peut contenir des données.

Pour fermer une connexion (c'est-à-dire supprimer le contexte des deux extrémités utilisé pour superviser l'échange de données), TCP utilise 4 messages :

  • une extrémité envoie un message FIN
  • qui est reconnu par un message ACK.
  • Ensuite, l'autre extrémité envoie un message FIN
  • qui est également acquitté.

Modbus TCP/IP

Quelques info sur l'origine du protocole Modbus:

  • Apparu en 1979
  • Construit sur un bus série RS-485
  • Équipements secondaires ou slaves et un primaire ou master qui gère les communications
  • Adresse esclave de 1 à 247
  • Le maître n'a pas d'adresse, toutes les communications sont avec lui
  • Information sous forme des registres
  • Registre avec valeur binaire:
    • En écriture: Coil
    • En lecture seule: discrete input
  • Registre 16 bits:
    • En écriture: holding register
    • En lecture seule: input register



Un équipement Modbus peut avoir jusqu’à 10000 registres de ces 4 catégories:


Modbus est un protocole requête/réponse. Le primaire envoie une requête à l’adresse d’un équipement pour lire ou écrire un de ses registres.



Une trame Modbus est une séquence de caractères

  • un octet avec l’adresse du secondaire,
  • suivi d’une commande (ou code de fonction) spécifique à chaque catégorie de registre :
    • 1 pour lire un coil
    • 2 pour lire un discrete input
    • 3 pour lire un input register
    • 4 pour lire un holding register
    • 5 pour écrire un holding register et
    • 6 pour écrire un input register
  • La suite de la trame contient les données, puis un CRC pour valider qu’il n’y a pas d’erreur de transmission dans la trame.

Exemple:

Pour lire un holding register, la requête contient l’adresse du premier registre à lire, et le nombre de registres à lire.
La réponse contient le nombre d’octets transmis suivi de leur valeur.

Pour écrire sur un registre, les données de la trame seront l’adresse du registre et les données à écrire.
La réponse est le même message.

La représentation des données

La sérialisation

La sérialisation consiste à transformer une structure de données en une séquence qui pourra être transmise sur le réseau, stockée dans un fichier ou une base de données. L'opération inverse, consistant à reconstruire localement une structure de données, s'appelle désérialisation.

Python Hexlify

En Python, il existe le module binascii très pratique qui permet de convertir une séquence binaire en une chaîne de caractères ou inversement :

  • hexlify prend un tableau d'octets et le convertit en une chaîne de caractères hexadécimaux plus lisible pour les spécialistes. Cela permet de visualiser n'importe quelle séquence de données.
  • unhexify fait l'inverse. Il prend une chaîne de caractères et la convertit en un tableau d'octets. Cela peut vous faciliter la programmation car, dans votre code, il est plus facile de manipuler des chaînes de caractères.
mac = lora.mac()
print ('devEUI: ',  binascii.hexlify(mac))
 
# create an OTAA authentication parameters
app_eui = binascii.unhexlify('70 B3 D5 7E D0 03 3A E3'.replace(' ',''))

La fonction lora.mac() retourne un tableau d'octets.
La fonction hexlify ligne suivante le convertit en chaîne de caractères pour un affichage plus propre.

Inversement, nous devons affecter une séquence binaire à la variable app_eui. Nous mettons cette séquence hexadécimale en chaîne de caractères. Les espaces offrent plus de lisibilité. Ils sont retirés par la méthode replace et le résultat est converti en binaire.

Conversion en ligne: https://www.asciitohex.com/

Autre exemple de sérialisation

  • HTML (Hyper Text Markup Language),
  • XML (eXtensible Markup Language),

JSON

JSON (JavaScript Object Notation). À l’origine, JSON était utilisé par Javascript pour échanger des informations.
JSON rfc8259] est un format d’échange simple.
Il définit 4 types de données :

  • nombre, composés de chiffres et peuvent être positifs, négatifs, entiers ou flottants.
  • texte, délimité par des guillemets simples ou doubles.
  • tableau, sont des listes d’éléments séparés par des virgules et entourés de crochets.
  • objet, est une liste de paires composées d’une clé et d’une valeur.
    • La clé est une chaîne de caractères et la valeur peut être de n’importe quel type.
    • La clé doit être unique à l’intérieur d’un objet, et référence entièrement la valeur qui la suit.
    • Le couple clé - valeur est séparé par le caractère 2 points (:).
    • Les éléments de l’objet sont séparés par des virgules.
    • L'objet est délimité par des accolades.
  • L’ordre dans lequel sont placés les éléments est indifférent.


Exemple de structure JSON tirée de la RFC 8259:

{
"Image": {
      "Width": 800,
      "Height": 600,
      "Title": "View from 15th Floor",
      "Thumbnail": {   
           "Url": "http://www.example.com/image/481989943",
           "Height": 125,
           "Width": 100
       },
       "Animated" : false,
       "IDs": [11, 943, 234, 38793]
    }
}

Exemple en Python:

import json
import pprint
 
struct_python = {
"Image": {
      "Width": 800,
      "Height": 600,
      "Title": "View from 15th Floor",
      "Thumbnail": {   
           "Url": "http://www.example.com/image/481989943",
           "Height": 125,
           "Width": 100
       },
       "Animated" : False,
       "Copyright" : None,
       "IDs": [0x11, 0x943, 234, 38793],
       "Title": "Empty picture"
    }
}
 
print (struct_python)
pprint.pprint(struct_python)
 
struct_json = json.dumps(struct_python)
 
print(struct_json)
 
struct_python2 = json.loads(struct_json)
 
pprint.pprint (struct_python2)

Sortie écran:

>>> print (struct_python)
{'Image': {'Width': 800, 'Height': 600, 'Title': 'Empty picture', 'Thumbnail': {'Url': 'http://www.example.com/image/481989943', 'Height': 125, 'Width': 100}, 'Animated': False, 'Copyright': None, 'IDs': [17, 2371, 234, 38793]}}
>>> pprint.pprint(struct_python)
{'Image': {'Animated': False,
           'Copyright': None,
           'Height': 600,
           'IDs': [17, 2371, 234, 38793],
           'Thumbnail': {'Height': 125,
                         'Url': 'http://www.example.com/image/481989943',
                         'Width': 100},
           'Title': 'Empty picture',
           'Width': 800}}

CBOR

JSON et CBOR sont tous les deux des modes de codage de la donnée.

JSON introduit une notation très flexible permettant de représenter toutes les structures de données. Le choix de l'ASCII rend ce format universel et n'importe quel ordinateur pourra le comprendre. Mais l'utilisation de l'ASCII ne permet pas de transmettre de manière optimale l'information sur un réseau. Quand les réseaux ont un débit raisonnable, cela ne pose pas de problème. Quand on en vient à l'internet des objets, il faut prendre en compte la capacité de traitement limité des équipements et la faible taille des messages échangés.

Ainsi, en ASCII, la valeur 123 est codée sur 3 octets (un octet par caractère),
tandis qu'en binaire elle n'occuperait qu'un seul octet : 0111 1011.

CBOR (Concise Binaire Object Representation), défini dans le RFC 7049, permet de représenter les structures de JSON mais suivant une représentation binaire. Comme nous le verrons par la suite, si CBOR est complètement compatible avec JSON, il est possible de représenter d'autres types d'information très utiles dans l'internet des objets.

CBOR définit 8 types majeurs qui sont représentés par les 3 premiers bits d'une structure CBOR.
Ces types majeurs ont donc des valeurs comprises entre 0 et 7 (000 à 111 en binaire).

Les cinq bits suivants contiennent soit une valeur soit une longueur indiquant combien d'octets sont nécessaires pour coder la valeur.
CBOR offre ainsi des optimisations qui permettent de réduire la longueur totale de la structure des données comme nous le verrons par la suite en étudiant les différents types majeurs.

Major 000: Type Entier Positif

JSON ne fait pas de différence entre les nombres, entiers, décimaux, positifs ou négatifs.
CBOR réintroduit une distinction pour optimiser la représentation.

Le premier type majeur correspond aux entiers positifs.

  • Il est codé par 3 bits à 0 ;
  • les 5 bits suivants finissent l'octet et, suivant leur valeur, vont avoir une signification différente :
    • de 0 à 23, il s'agit de la valeur de l'entier à coder ;
    • 24 indique que l'entier est codé sur 1 octet qui sera codé dans l'octet suivant ;
    • 25 indique que l'entier est codé sur 2 octets qui seront codés dans les deux octets suivants ;
    • 26 indique que l'entier est codé sur 4 octets qui seront codés dans les quatre octets suivants ;
    • 27 indique que l'entier est codé sur 8 octets qui seront codés dans les huit octets suivants.


On peut noter qu'il n'y a pas de surcoût pour coder un entier de 0 à 23.
Ainsi, la valeur 15 sera codée 0x0F (000-0 1111) tandis que, pour toutes les autres valeurs supérieures, le surcoût ne sera que d'un octet.
100 sera codé 000-1 1000 (11000 correspond à 24) suivi de la valeur 100 (0110 0100).
Installation:

$ pip3 install cbor2

Le programme cbor-integer-ex1.py va afficher les puissances de 10 entre 10exp0 et 10exp18.

import cbor2 as cbor
 
v = 1
 
for i in range (0, 19):
    c = cbor.dumps(v)
    print ("{0:3} {1:30} {2}".format(i, v, c.hex()))
 
    v *= 10

Affichage:

  0                              1 01
  1                             10 0a
  2                            100 1864
  3                           1000 1903e8
  4                          10000 192710
  5                         100000 1a000186a0
  6                        1000000 1a000f4240
  7                       10000000 1a00989680
  8                      100000000 1a05f5e100
  9                     1000000000 1a3b9aca00
 10                    10000000000 1b00000002540be400
 11                   100000000000 1b000000174876e800
 12                  1000000000000 1b000000e8d4a51000
 13                 10000000000000 1b000009184e72a000
 14                100000000000000 1b00005af3107a4000
 15               1000000000000000 1b00038d7ea4c68000
 16              10000000000000000 1b002386f26fc10000
 17             100000000000000000 1b016345785d8a0000
 18            1000000000000000000 1b0de0b6b3a7640000

On voit facilement que les valeurs 1 et 10 sont codées sur 1 octet ; que 100 est codé sur 2 octets tandis que les valeurs 1 000 et 10 000 sont codées sur 3 octets. Les valeurs entre 100 000 et 1 000 000 000 nécessitent 5 octets et les suivantes 9 octets.

La taille de la représentation s'adapte à la valeur. Ainsi, il n'est pas nécessaire de définir une taille fixe pour coder une donnée.

On peut aussi noter que comme le type majeur est sur 3 bits, ce type peut être reconnu car il commence par la valeur “0” ou “1”.

Major 001: Type Entier Négatif

Pour coder -15, on va coder la valeur 14, ce qui donne en binaire 001-1 1110.
-24 peut également être codé sur 1 octet tandis que +24 sera codé sur 2 octets.

import cbor2 as cbor
 
v = -1
 
for i in range (0, 19):
    c = cbor.dumps(v)
    print ("{0:3} {1:30} {2}".format(i, v, c.hex()))
 
    v *= 10

Affichage:

  0                             -1 20
  1                            -10 29
  2                           -100 3863
  3                          -1000 3903e7
  4                         -10000 39270f
  5                        -100000 3a0001869f
  6                       -1000000 3a000f423f
  7                      -10000000 3a0098967f
  8                     -100000000 3a05f5e0ff
  9                    -1000000000 3a3b9ac9ff
 10                   -10000000000 3b00000002540be3ff
 11                  -100000000000 3b000000174876e7ff
 12                 -1000000000000 3b000000e8d4a50fff
 13                -10000000000000 3b000009184e729fff
 14               -100000000000000 3b00005af3107a3fff
 15              -1000000000000000 3b00038d7ea4c67fff
 16             -10000000000000000 3b002386f26fc0ffff
 17            -100000000000000000 3b016345785d89ffff
 18           -1000000000000000000 3b0de0b6b3a763ffff

Major 010/011: Type Séquence binaire ou Chaîne de caractères

Les séquences binaires et les chaînes de caractères ont le même comportement.
Le type majeur est respectivement 010 et 011.
Il est suivi par la longueur de la séquence ou de la chaîne.
Le même type de codage que pour les entiers est utilisé :

  • si la longueur est inférieure à 23, elle est codée dans la suite du premier octet.
    On trouve ensuite le nombre d'octets ou de caractères correspondant à cette longueur ;
  • si la longueur peut être codée dans 1 octet (donc inférieure à 255), la suite du premier octet contient 24 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.
  • si la longueur peut être codée dans 2 octets (donc inférieure à 65535), la suite du premier octet contient 25 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.
  • si la longueur peut être codée dans 4 octets, la suite du premier octet contient 26 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.
  • si la longueur peut être codée dans 8 octets, la suite du premier octet contient 27 puis l'octet suivant contient la longueur suivie du nombre d'octets ou de caractères correspondant.

Ce codage est aussi assez optimal. Il est rare d'envoyer plus de 23 caractères.

Le programme cbor-string.py montre la représentation de chaînes de caractères de longueur croissante ainsi qu'une séquence binaire.

import cbor2 as cbor
 
for i in range (1, 10):
    c = cbor.dumps("LoRaWAN"*i)
 
    print ("{0:3} {1}".format(i, c.hex()))
 
bs = cbor.dumps(b"\x01\x02\x03")
print (bs.hex())

Le résultat est le suivant :

  1 674c6f526157414e
  2 6e4c6f526157414e4c6f526157414e
  3 754c6f526157414e4c6f526157414e4c6f526157414e
  4 781c4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  5 78234c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  6 782a4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  7 78314c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  8 78384c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
  9 783f4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e4c6f526157414e
43010203

Major 100: Type tableau

Le type tableau va regrouper un ensemble d'éléments. Chacun de ces éléments étant une structure CBOR, la seule information nécessaire pour connaître le début et la fin d'un tableau est son nombre d'éléments.
Le type majeur est 100. Il existe deux méthodes pour coder la longueur d'un tableau :

  1. si celle-ci est connue au moment du codage, il suffit de l'indiquer avec un codage identique à celui utilisé pour indiquer la longueur d'une chaîne de caractères ;
  2. si celle-ci n'est pas connue au moment du codage, il existe un code spécial pour indiquer la fin du tableau.
import cbor2 as cbor
 
c1 = cbor.dumps([1,2,3,4])
print (c1.hex())
print ()
 
c2 = cbor.dumps([1,[2, 3], 4])
print (c2.hex())
print()
 
c3 = cbor.dumps([1000, +20, -10, +100, -30, -50, 12])
print (c3.hex())

Affichage:

8401020304

830182020304

871903e814291864381d38310c
  • [1,2,3,4] devient 8401020304. On peut deviner la structure du message CBOR : 0x84 indique un tableau de 4 éléments (8 pour Major 100 et 4 nb éléments)(attention le décodage n'est pas toujours aussi simple). Les 4 éléments sont des entiers inférieurs à 23 ;
  • [1,[2, 3], 4] devient 830182020304. Il s'agit d'un tableau de 3 éléments dont le deuxième est un tableau de deux éléments ;
  • [1000, +20, -10, +100, -30, -50, 12] devient 871903e814291864381d38310c.

Le site web cbor.me permet de faire automatiquement le codage dans un sens ou dans l'autre.



On peut calculer le degré de compression de CBOR. Ainsi, dans le premier exemple, le tableau JSON [1,2,3,4] faisait 9 caractères tandis que la représentation CBOR n'en faisait que 5. Pour le dernier exemple (illustration ci dessus), les 13 octets de la représentation CBOR sont transformés en 34 caractères (donc 34 octets) en JSON.

Major 101: Type objets (Liste de paires)

Le type Liste de paires ou Dictionnaire est indiqué par la valeur 101.
Il fonctionne de la même manière que les tableaux en comptant le nombre d'éléments.
Mais cette fois-ci, la valeur représente une paire, c'est-à-dire deux objets CBOR.

Le programme cbor-mapped.py donne un exemple. La structure à encoder est la suivante :

import cbor2 as cbor
 
c1 = {"type" : "hamster",
      "taille" : 300,
      2 : "test",
      0x0F: 0b01110001,
      2 : "program"};
 
print (cbor.dumps(c1).hex())

Affichage:

a464747970656768616d73746572667461696c6c6519012c026770726f6772616d0f1871

Le résultat est a464747970656768616d73746572667461696c6c6519012c026770726f6772616d0f1871 ; ce que le site cbor.me (entrez la chaîne de caractères dans la partie droite du site) retranscrit en :



ou, en notation JSON :

{“type”: “hamster”, “taille”: 300, 2: “program”, 15: 113}

On peut voir des différences entre JSON, CBOR et la représentation des variables en Python. Les codages hexadécimaux et binaires de Python ont été convertis en décimal pour JSON.

De plus, même si JSON n'autorise que des clés en ASCII pour indexer les paires , nous avons pu mettre des clés numériques. Néanmoins Python a effacé la première clé 2 par la dernière. CBOR définit un mode strict dans lequel ces clés doivent être codées en ASCII pour être compatibles avec JSON mais autorise également des représentations qui diffèrent de JSON en enlevant les contraintes sur les clés.

Major 110: Type étiquette

CBOR enrichit le typage des données ; ce qui permet de manipuler plus facilement des données. Par exemple, une chaîne de caractères peut représenter une date, une URI, voire une URI codée en base 64. Le type 110 peut être suivi d'une valeur dont une liste exhaustive est donnée ici: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml

import cbor2 as cbor
from datetime import date, timezone
 
print (date.today())
c1 = cbor.dumps(date.today(), timezone=timezone.utc, date_as_datetime=True)
 
print (c1.hex())
 
print (cbor.loads(c1))
print (type(cbor.loads(c1)))

affiche la date du jour, la convertit en notation CBOR et la reconvertit en variable Python grâce à la méthode loads.:

2021-11-23
c074323032312d31312d32335430303a30303a30305a
2021-11-23 00:00:00+00:00
<class 'datetime.datetime'>


c074323032312d31312d32335430303a30303a30305a



Major 111: Le type flottant et valeurs particulières

Le dernier type majeur (111) permet de coder les nombres flottants en utilisant la représentation définie par l'IEEE 754. Suivant la taille de la représentation, la suite de l'octet contient les valeurs 25 (demi précision sur 16 bits), 26 (simple précision sur 32 bits) ou 27 (double précision sur 64 bits).

Ce type permet également de coder les valeurs définies par JSON : True (valeur 20), False (valeur 21) ou None (valeur 22).

Finalement, ce type peut indiquer la fin d'un tableau ou d'une liste de paires quand la taille n'est pas connue au début du codage.

SenML

SenML (Sensor Measuring List) est une une spécification qui exploite JSON ou CBOR. Elle liste un ensemble de noms/unités/mesures et les standardise en un nom de clé unique. En utilisant cette standardisation, on facilite l'interopérabilité. Les clés et valeurs sont donc règlementées et typées pour éviter tous conflits d'interopérabilité. Le format est défini dans la [RFC8428] et repose sur une structure de tableau regroupant des objets comme le montre la figure suivante tirée de la RFC.

[
 {"bn" : "urn:dev:ow:10e2073a01080063", "bt":1.320067464e+09,
  "bu" : "\%RH", "v":21.2},
 {"t":10, "v":21.3},
 {"t":20, "v":21.4},
 {"t":30, "v":21.4},
...

SenML définit les clés utilisées dans l’objet. Pour avoir une notation compacte, elles sont limitées à 1 ou 2 caractères. Parmi elles, ”bn” indique un nom de base et ”n” le nom d’un appareil. Si plusieurs appareils envoient la partie commune de l’identifiant de l’appareil, on peut mettre le ”bn” pour éviter de le répéter à chaque fois.

Le temps de base (ou ”bt”) est également un moyen de compacter la notation du temps. Le temps (”t”) donne le décalage et conduit à une valeur plus petite comme on le voit dans l’exemple.

L’unité de base (”bu”) indique l’unité par défaut si les autres objets ne portent pas de mot clé indiquant l’unité (”u”).

La [RFC8428] définit une liste d’unités telles que le kilogramme (”kg”), le volt (”V”), etc. Dans l’exemple, ”%RH” désigne un pourcentage d’humidité relative. Une valeur numérique utilise la lettre ”v”, une chaîne de caractères utilise la touche ”vs”.

CBOR utilise la même structure mais les petits nombres entiers positifs et négatifs sont substitués dans les clés des objets de CBOR : ”bn”, ”bt”, ”bu” seront respectivement représentés par -1, -2 et -3 et ”n”, ”t”, ”u” par +0, +2 et +6.

TP séries temporelles

Coté client

Voici un exemple de transmission de l'humidité par tableau CBOR:

30 92 [2945, 3040, 3330, 3506, 3741, 3613, 3785, 4079, 3832, 3705, 3411, 3294, 3456, 3245, 3199, 3215, 3401, ..

Le premier élément (30) indique le nombre d’éléments du tableau, le second (92) la taille en octets du tableau codé en CBOR puis les éléments du tableau.
Version optimisée par delta par rapport à la 1ère mesure:

1 4 [2422]
2 5 [2422, 2]
3 7 [2422, 2, -152]
4 10 [2422, 2, -152, -272]
5 12 [2422, 2, -152, -272, 88]
6 14 [2422, 2, -152, -272, 88, 182]
...
30 59 [2422, 2, -152, -272, 88, 182, -48, 247, -300, 129, -34, -193, -31, 216, 29, -202, -65, -18, 204, 155, ...

permet de coder les valeurs 2422, 2424, 2272…
Ceci met en valeur deux souplesses de CBOR :

  1. la taille du tableau est dynamique. Si l’on change le nombre de valeurs à transmettre, le tableau l’indique et l’on n’a pas besoin de modifier le code du récepteur ;
  2. la taille des données dépend de leur valeur. Pour les variations entre -24 et +23, un seul octet sera nécessaire. On le voit sur l’exemple précédent : l’ajout de la valeur '2' dans le tableau fait passer la taille de la représentation CBOR de 4 à 5 octets. Les valeurs entre 256 et +255 sont transmises sur 2 octets ; il est donc possible de cette manière d’optimiser la transmission sans ajouter de contrainte. S’il y avait une brusque variation de l’humidité, la représentation CBOR s’adapterait pour la transmettre.


architecture globale:



from virtual_sensor import virtual_sensor 
import time
import socket
import cbor2 as cbor
 
humidity = virtual_sensor(start=30, variation = 3, min=20, max=80) 
 
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
NB_ELEMENT = 30
h_history = []
 
while True:
 
    h = int(humidity.read_value()*100)
 
    # No more room to store value, send it.
    if len(h_history) == 0:
        h_history = [h]
    elif len(h_history) >= NB_ELEMENT:
        print ("send")
        s.sendto (cbor.dumps(h_history), ("127.0.0.1", 33033))
        h_history = [h]
    else:
        h_history.append(h-prev)
 
    prev = h
 
    print (len(h_history), len(cbor.dumps(h_history)), h_history)
 
    time.sleep(10)

Côté serveur

l'interface graphique sera une appli web: https://beebotte.com
La transmission ce fait vers une URI au format JSON via une API REST.
L'appli Web fourni un login de connexion pour l'API REST que l'on note dans un fichier externe config_bbt.py.
Exemple:

API_KEY    = "GAJ3SFmUZSXmB2zqdcmzcuXc"
SECRET_KEY = "4NCsrM1cfmFdMZF4E47aTfmCaU3UfyQo"

On cré un canal dans lequel on défini les objets correspondant aux capteurs:

  • Channels
  • Create New



Cochez également Public avant de créer le canal.

Coté Python, on installe le module beebotte avec

pip3 install beebotte

Le programme display_server.py va prendre la structure CBOR envoyée par le capteur et la transformer dans le format JSON attendu par beebotte.

import socket
import binascii
import cbor2 as cbor
import beebotte
import config_bbt #secret keys 
import datetime
import time
import pprint
 
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', 33033))
 
bbt = beebotte.BBT(config_bbt.API_KEY, config_bbt.SECRET_KEY)
 
def to_bbt(channel, res_name, cbor_msg, factor=1, period=10, epoch=None):
    global bbt
 
    prev_value = 0
    data_list = []
    if epoch:
        back_time = epoch
    else:
        back_time = time.mktime(datetime.datetime.now().timetuple())
 
    back_time -= len(cbor_msg)*period
 
    for e in cbor_msg:
        prev_value += e
 
        back_time += period
 
        data_list.append({"resource": res_name,
                          "data" : prev_value*factor,
                          "ts": back_time*1000} )
 
    pprint.pprint (data_list)
 
    bbt.writeBulk(channel, data_list)
 
 
while True:
    data, addr = s.recvfrom(1500)
 
    j = cbor.loads(data)
    to_bbt("capteurs", "humidity", j, factor=0.01)

La fonction to_bbt transforme le tableau JSON suivant:

[3311, 124, -144, -188, -94, 289, -1, -72, 1 ...

en un tableau de dictionnaire :

[{'data': 33.11, 'resource': 'humidity', 'ts': 1596730115000.0},
 {'data': 34.35, 'resource': 'humidity', 'ts': 1596730125000.0},
 {'data': 32.91, 'resource': 'humidity', 'ts': 1596730135000.0},
 {'data': 31.03, 'resource': 'humidity', 'ts': 1596730145000.0},

Chaque dictionnaire contient trois éléments imposés par beebotte :

  1. le nom de la ressource (resource) telle qu’elle a été définie sur l’interface pour le canal ;
  2. la valeur associée pour cette ressource (data) ;
  3. ’instant à laquelle cette mesure à été faite (ts). Le temps est représenté suivant le format Epoch qui compte le nombre de secondes depuis le 1er Janvier 1970 (voir https://www.epochconverter.com/ pour les conversions).

Le calcul du timestamp (ts) est l’opération la plus complexe de cette fonction mais le module datetime facilite le calcul. Ligne 21 et 22, on calcule le temps à laquelle la première mesure du tableau a été faite : on prend le temps actuel (cela suppose que l’on néglige le temps de traitement et de transmission) auquel on retranche la durée de la capture, c’est-à-dire comme le nombre d’éléments du tableau multiplié par l’intervalle entre chaque mesure (period). Il est possible d’estampiller chaque mesure mais il faut convertir le format date en un nombre, ce qui est fait ligne 27.

On peut vérifier que beebotte a reçu des données en visualisant le canal capteurs sur l’interface Web.



On peut aussi visualiser graphiquement les données via le dashboard. Cliquez sur le menu de gauche sur Dashboard et create Dashboard. Puis allez dans Add Widget et sélectionnez Multi-line chart.



configurez le widget en définissant le canal et la ressource de ce canal:



Dans le dashboard, on peut voir l’évolution de l’humidité au cours du temps.



SenML

Lors de la configuration des ressources sur le site de Beebotte, vous avez pu également remarquer que l’on doit préciser la nature de la mesure : par exemple, s’il s’agit d’une température, d’un taux d’humidité… Il faut également parfois indiquer le type de la mesure (texte, entier, flottant…) voire les unités.

SenML (pour Sensor Measurement Lists ) RFC 8428 propose une structuration des données fournie par le capteur. Pour réduire l’impact de la transmission, les noms des champs ont été choisis pour être le plus compact possible. Par exemple, la lettre v va indiquer une valeur (à comparer avec la clé “data” utilisée lors de la communication avec Beebotte). Pour être encore plus compact, la représentation en CBOR utilisera des entiers courts au lieu de caractères.

Il est également possible de transporter l’unité de la mesure avec le mot clé u .

SenML ne définit pas que des unités du système international, mais également des unités secondaires pour limiter la taille de la représentation. Il sera plus compact de transmettre :

{"u": "MHz", "v": 868}

que:

{"u": Hz", "v": 868000000}

Le standard définit aussi des temps de base et des valeurs de base auxquelles les temps et les valeurs vont se référer ; ce qui permet également de réduire la taille des valeurs. Finalement, le ou les objets peuvent s’identifier dans les données transmises en définissant un nom de base (”bn” : base name), le nom du capteur (”n” : name) vient compléter le nom de base.

Mise en œuvre

Le programme minimal_senml_client.py montre comment un enregistrement senML peut être fait en JSON et en CBOR.

from virtual_sensor import virtual_sensor 
import time
import socket
import json
import kpn_senml as senml
import pprint
import binascii
 
temperature = virtual_sensor(start=20, variation = 0.1)
pressure    = virtual_sensor(start=1000, variation = 1) 
humidity    = virtual_sensor(start=30, variation = 3, min=20, max=80) 
 
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 
sens = senml.SenmlPack("device1") 
 
t = temperature.read_value()
p = pressure.read_value()
h = humidity.read_value()
 
rec = senml.SenmlRecord("temperature", 
    unit=senml.SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, 
    value=t)         
sens.add(rec)
 
 
 
print(len(sens.to_json()), sens.to_json())
print(len(sens.to_cbor()), binascii.hexlify(sens.to_cbor()))

CoAP

CoAP: Constrained Application Protocol

Pourquoi CoAP ?

Le but de CoAP est de définir un protocole beaucoup plus strict qui sera donc plus facile à mettre en œuvre mais qui pourra interopérer avec HTTP afin de préserver les principes définis par l’architecture REST et profiter du nommage des ressources pour que les ressources contraintes participent à la grande toile d'araignée mondiale.

La massagerie CoAP

L’en-tête des messages CoAP est de taille variable mais très structurée, comme le montre la figure suivante :



  • Le premier mot de 32 bits est présent dans tous les messages CoAP :
  • le champ Ver, sur 2 bits, contient le numéro de version du protocole, qui vaut 01 dans la version actuelle ;
  • le champ T pour Type, également sur 2 bits, indique la nature du message (00 : CONfirmable, 01 : NON confirmable, 10 : Acquittement, 11 : Reset) ;
  • le champ TKL, sur 4 bits, donne la longueur en octets du champ token démarrant au deuxième mot de 32 bits. Si la valeur est 0, ce champ est absent. Les valeurs de 1 à 8 indiquent la longueur. Les valeurs de 9 à 15 ne sont pas autorisées ;
  • le champ Code, sur 1 octet, permet un codage assez subtil de la nature de la requête ou de la réponse (cf. chapitre suivant) ;
  • le champ Message ID, sur 2 octets, identifie les requêtes.

Codage de code

Dans beaucoup de protocoles applicatifs comme FTP ou HTTP, le serveur renvoie un code sur 3 caractères indiquant si la requête s'est exécutée correctement ou non. Les codes commençant par le chiffre :

  • 1 informent que la requête est en train d’être traitée normalement. Ce type de notification n’est pas pris en compte avec CoAP ;
  • 2 indiquent que la requête a été acceptée et traitée correctement ;
  • 3 permettent d’indiquer une indirection ;
  • 4 font référence à une erreur du coté client, due à une mauvaise syntaxe ou une requête qui ne peut être traitée. Ainsi la célèbre erreur 404 indique que le client a demandé une page qui n’existe pas sur le serveur ;
  • 5 désignent une erreur du coté du serveur.

Le site web de l'IANA (http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml\#http-status-codes-1) donne les erreurs que l’on retrouve dans le protocole HTTP. Comme indiqué précédemment, le chiffre de gauche varie entre 1 et 5 tandis que les deux chiffres de droite, précisant la raison de la notification, varient généralement entre 0 et 31.

Pour permettre une représentation plus compacte, CoAP va coder cette chaîne de caractères dans un octet. Les trois bits de gauche désignent la nature du code et les 5 à droite donneront la raison. Ainsi le code d’erreur HTTP 415 (Unsupported Media Type) se note en CoAP 4.15, s’écrit en binaire 100.01111 et en décimal 143. Cette notation concerne les réponses aux requêtes mais elle laisse de la place pour coder également les requêtes. En effet, le code avec les trois premiers bits à 0 n’est pas utilisé pour coder les notifications. Plusieurs requêtes compatibles avec l’architecture REST peuvent être codées :

  • GET, codé 0x01, retrouve le contenu d’une ressource présente sur le serveur et désignée par un URI ;
  • POST, codé 0x02, stocke une valeur sur une ressource existante présente sur le serveur ;
  • PUT, codé 0x03, crée une ressource sur le serveur et lui affecte une valeur ;
  • DELETE, codé 0x04, supprime une ressource sur le serveur.

Notez que la valeur 0x00 peut être utilisée dans certains cas.

Message ID

Le champ Message ID sur 2 octets sert à identifier les messages CoAP afin de détecter les duplicatas. Cette valeur est recopiée dans les acquittements pour permettre de savoir quel message est acquitté. Ils ne doivent pas être réutilisés pendant une période fixée.

Tokens

CoAP utilise le protocole UDP pour communiquer. Contrairement à TCP, il n’y a pas de notion d’établissement de connexion. Il est donc difficile de faire le lien entre les établissements et les réponses, surtout si elles ne sont pas immédiates. La figure suivante illustre ce phénomène. Une requête GET est envoyée par un client à un serveur.



La réponse ne peut pas être immédiate (par exemple il faut lire une valeur sur un capteur qui demande d’être activé). Le message d'acquittement ne peut pas être différé sinon, le client ne voyant pas sa requête acquittée, la retransmettrait. Le serveur acquitte avec un message Ack vide. Quand le serveur peut envoyer la ressource, il le fait à son tour dans un message de type CON qui sera à son tour acquitté. Vous pouvez remarquer que les valeurs du champ Message ID sont complètement décorrélées. Pour faire le lien entre la requête et la réponse, un token fourni par le client est recopié par le serveur. C'est pour cela que l'on peut considérer une valeur de token comme une “connexion” entre le client et le serveur.

Le token est une séquence binaire facultative dont la taille est comprise entre 0 (pas de token) et 8 octets. La longueur est indiquée au début de l’en-tête dans le champ ToKen Length (TKL) et la valeur suit immédiatement l'en-tête obligatoire avant les options.

Les options CoAP

Le champ Option va contenir des options qui vont soit servir à améliorer le protocole de transfert des données entre le client et le serveur, soit servir à coder les en-têtes des requêtes et des réponses en garantissant une certaine compatibilité avec les en-têtes HTTP.

La structure utilisée est dite TLV pour Type Longueur Valeur. Chaque champ contient au moins ces deux informations :

  • Type indiquant la nature de l’option ;
  • Longueur indiquant la taille des données en octets. Si ce dernier n’est pas nul, les données vont se trouver après.




CoAP complique un peu la chose en optant pour un codage différentiel de la valeur de l’option. Ainsi, si l’on doit envoyer une option de type 5 puis deux de type 6, le codage contiendra ∆T = 5, ∆T = 1, ∆T = 0.

Mais comme le champ ∆T ne fait que 4 bits, on ne peut pas aller bien loin pour coder ces valeurs. Un mécanisme d'échappement est mis en place pour les différences supérieures à 13. Dans ce cas, la valeur 13 est mise dans le champ ∆T et l'octet suivant code la différence moins 13.

Par exemple, si l'on doit coder deux options de valeurs 5 et 20, la différence est de 15. La première option est codée normalement avec le ∆T à 5. Pour la seconde option, le ∆T est mis à 13 et l'octet suivant prendra la valeur 2.

Notez que la valeur 14 mise dans le champ ∆T indique que la différence nécessite deux octets pour être codée.

Pour la longueur on retrouve le même principe : les longueurs inférieures à 13 sont codées directement ; si elles sont supérieures ou égale à 13, la valeur moins 13 est codée dans un octet supplémentaire. Une valeur de 14 indique que deux octets sont utilisés pour coder la longueur moins 269.

La figure en haut de page illustre le codage d’une option dans l’en-tête d’un message CoAP.

Il se peut qu’il y ait des données après les options. Dans ce cas, un séparateur avec la valeur 0xFF est inséré. Il ne peut pas être confondu avec le codage d’une option puisque les champs ∆T et Longueur n'évoluent qu'entre 0 et 14.

S’il n’y a pas de données à transmettre (par exemple dans le cas d’une requête GET), le message CoAP se termine après les options.

Options CoAP

Le premier bit sert à coder le type décrit. Quand il est positionné à 1, ce type doit être connu du récepteur (critique). Dans ce cas, si un destinataire reçoit une option de ce type et qu’il ne la connaît pas, il doit produire un message d’erreur. Dans le cas contraire, cette option est ignorée du récepteur qui poursuit le traitement des options suivantes. Ainsi, les options paires sont facultatives et les impaires, critiques. Les options apparaissant en gras seront traitées plus en détail dans la suite de ce chapitre.

Valeur Nom Type Nature répété Commentaire
0 Réserve
1 If-Match opaque critique oui Utilisé pour indiquer au serveur de n'effectuer la requête que sous certaines conditions.
3 Uri-Host string critique Contient le nom du serveur d'une URI (nom, adresse IPv4 ou IPv6). Généralement, il n'est pas nécessaire de le préciser puisque les messages CoAP sont envoyés à cette adresse.
4 ETag opaque facultative oui Utilisé pour gérer la mise en cache des ressources.
5 If-None-Match vide critique Utilisé pour indiquer au serveur de n'effectuer la requête que sous certaines conditions.
6 Observe entier facultative Permet à un serveur d'envoyer une requête aux changements d'état d'une ressource. Dans la réponse, la valeur doit toujours augmenter.
7 Uri-Port entier critique Contient le numéro du port UDP sur lequel CoAP est lancé. Généralement, ce champ n'est pas nécessaire vu que le serveur CoAP attend déjà des messages sur ce port.
8 Location-Path string facultative oui Utilisé en réponse à une requête POST pour indiquer un segment du chemin de la ressource.
11 Uri-Path string facultative oui Contient un des segments de l'URI.
12 Content-Format entier facultative Définit le format dans lequel sont codées des données.
14 Max-Age entier facultative Durée pendant laquelle la ressource peut être mise en cache.
15 Uri-Query string critique oui Contient les segments d'interrogation que l'on retrouve dans les URI.
17 Accept entier critique Indique les formats que le client peut accepter.
20 Location-Query string facultative oui Utilisé en réponse à une requête POST pour indiquer le chemin de la ressource.
35 Proxy-Uri string critique Contient une URI qui doit être prise en compte par le proxy.
35 Proxy-Scheme string critique Indique le format de l'URI et le protocole dans une URL.
60 Size1 entier facultative Indique la taille de la ressource.
258 No-Response entier facultative Limite les notifications REST.

Message ID

Le champ Message ID, sur 2 octets, sert à identifier les messages CoAP afin de détecter les duplicatas liés à des retransmissions, ou à fiabiliser la transmission des messages confirmables. Dans ce cas, la valeur est recopiée dans les acquittements.

Dans l'exemple suivant, la première transaction se déroule correctement. L'émetteur envoie un message confirmable avec la valeur de message ID 0x1A8E, arme un temporisateur, et reçoit un acquittement avec la même valeur de Message ID avant que le temporisateur n'expire. La donnée est donc acquittée côté émetteur.

Dans le deuxième échange, la donnée n'est pas acquittée dans la durée allouée par le temporisateur ; elle est donc retransmise.

Le standard RFC 7252 suggère un temporisateur initial de 2 secondes dont la valeur double à chaque retransmission, et 4 transmissions sont proposées. Ces valeurs peuvent être changées pour s'adapter au contexte.



Les messages non confirmés possèdent également un message ID unique qui permet surtout d'éviter les duplications qui pourraient survenir lors du transport du message.

Pour rejeter les doublons, le récepteur doit garder une copie des Message ID émis par une source. Mais pour éviter de saturer la mémoire du récepteur avec ces valeurs, un simple calcul permet de définir la période de rétention des valeurs.

Un peu d’algèbre élémentaire permet de calculer cette durée. La figure suivante montre le calcul du pire cas. On prend le délai de propagation maximum dans les deux directions et on suppose que toutes les requêtes se perdent sauf la dernière qui est complète et on ajoute un temps de traitement important dans le récepteur.



Le standard prévoit que, par défaut, la durée d’activité d’un Message ID est d’environ 5 minutes (247 s) pour les messages confirmés, et 2,5 minutes (145 s) pour les messages non confirmés.

Avoir cette notion en tête peut vous éviter des heures de débogage. Supposons qu'un client commence toujours par numéroter ses messages à 1. Le récepteur va donc garder les valeurs des messages ID pendant 5 minutes. Si vous redémarrez l'objet, il va émettre de nouveaux messages, mais avec les mêmes Messages ID qui seront ignorés par le récepteur.

Finalement, la notion d'émetteur/récepteur est dissociée du concept REST de client/serveur. Un client peut être émetteur ou récepteur ; de même pour le serveur.

Tokens

CoAP utilise le protocole UDP pour communiquer. Contrairement à TCP, il n’y a pas de notion d’établissement de connexion. Il est donc difficile de faire le lien entre les établissements et les réponses, surtout si elles ne sont pas immédiates. La figure suivante illustre ce phénomène. Une requête GET est envoyée par un client à un serveur.



La réponse ne peut pas être immédiate (par exemple il faut lire une valeur sur un capteur qui demande d’être activé). Le message d'acquittement ne peut pas être différé sinon, le client ne voyant pas sa requête acquittée, il la retransmettrait. Le serveur acquitte avec un message Ack vide.

Quand le serveur peut envoyer la ressource, il le fait à son tour dans un message de type CON qui sera à son tour acquitté. Vous pouvez remarquer que les valeurs du champ Message ID sont complètement décorrélées et que client et serveur sont à tour de rôle émetteur et récepteur.

Pour faire le lien entre la requête et la réponse, un token fourni par le client est recopié par le serveur. On peut considérer une valeur de token comme une connexion entre le client et le serveur.

Le token est une séquence binaire dont la taille est comprise entre 1 et 8 octets. La longueur est indiquée au début de l’en-tête obligatoire et la valeur suit immédiatement cet en-tête obligatoire.

Représentation des URI

iot/cours_fun_mooc_iot.txt · Dernière modification: 2021/12/20 19:28 de jc_online