Librairie Python ws4py. Communication WebSocket entre Raspberry Pi et ESP8266, ESP32 ou Arduino • Domotique et objets connectés à faire soi-même

Le Websocket est un protocole de communication beaucoup plus rapide que le protocole REST qui utilise des requêtes HTTP classiques. Le Websocket permet d’ouvrir un canal de communication bi-directionnel entre deux appareils. Dans le cas présent, ce sera entre un ES8266 (mais cela pourrait également être un Arduino ou un ESP32) et un Raspberry Pi 3.

Dans ce tutoriel, nous allons démarrer un serveur WebSocket sur un ESP8266 (Wemos d1 R2). Le client sera développé en Python et sera hébergé sur un Raspberry Pi. Nous ré-utiliserons cette architecture très prochainement pour piloter un bras robotique en WiFi à l’aide d’un Gamepad.

Dans l’article précédent, nous avons vu comment intercepter les actions sur un Gamepad à l’aide de la librairie Python evdev (uniquement sur Linux).

Les avantages du Websocket par rapport à l’API REST HTTP classique

Le websocket a été élaboré pour les applications qui nécessitent des réponses rapide ou interactives. Le HTTP a été élaboré à la préhistoire du Web (par le CERN de Genève). Le protocole HTTP est employé pour faire fonctionner les sites internets mais également les applications mobiles (par exemple). Les API REST sont également basées sur l’HTTP. L’HTTP n’est pas adapté aux applications qui nécessitent des réponses rapides ou interactives. En effet, à chaque fois que le client fait une requête au serveur, on doit ouvrir une connexion, attendre la réponse du serveur puis refermer la connexion ce qui est consommateur de ressources et prend du temps de traitement.

On ne tiendra pas compte ici de la technologie Ajax qui permet d’actualiser le contenu d’une page web de manière asynchrone. Ajax pourra être utilisée dans les projets Arduino / ESP8266 (et équivalent) coté client web, c’est à dire au niveau de l’interface HTML du projet (lisez ces articles pour en savoir plus). Un exemple de réalisation ici.

Le Websocket vise à résoudre ces problèmes. Le Websocket ouvre un tunnel de communication entre deux appareils. Ce tunnel reste ouvert jusqu’à ce que le client se déconnecte. A n’importe quel moment le client peut envoyer des messages (JSON, binaire, texte…) et vis versa.

yzqqqmdxso8in8uzznf0-5198716

Source : https://www.pubnub.com/blog/2015-01-05-websockets-vs-rest-api-understanding-the-difference/

Pour résumer, le Websocket présente les avantages suivants :

  • Bi-directionnel: le protocole HTTP est unidirectionnel, c’est à dire que le client envoi une requête à laquelle le serveur répond ensuite. Le client consomme ensuite la réponse et ainsi de suite. WebSocket est un protocole bidirectionnel dans lequel il n’y a pas de modèles de message prédéfinis tels que demande / réponse. Le client ou le serveur peut envoyer un message à l’autre partie.
  • Full-duplex: serveur et client peuvent s’envoyer des messages à n’importe quel moment indépendamment des traitements en cours.
  • Connexion TCP unique: Généralement, une nouvelle connexion TCP est lancée pour une requête HTTP et se termine après la réception de la réponse. Une nouvelle connexion TCP doit être établie pour une autre requête / réponse HTTP. Avec le WebSocket, le client et le serveur communiquent sur la même connexion TCP jusqu’à ce que le client ou le serveur ferme la connexion.
  • Léger : le Websocket se concentre sur l’essentiel contrairement à l’HTTP qui embarque de nombreuses informations à chaque question / réponse

En terme de performance, le Websocket est beaucoup plus rapide comme le montre cette étude réalisée par le développeur Arun Gupta en 2014. Arun a mesuré le temps nécessaire pour envoyer des paquets de messages. Chaque message pèse 1000 bytes.

oifcvmeum5ikskbkycbx-1432306

Source : http://blog.arungupta.me/rest-vs-websocket-comparison-benchmarks/

La différence peut sembler insignifiante (30% plus rapide) pour un nombre très faible de message. Vous verrez que pour piloter un bras robotique ou un éclairage à LED depuis une application mobile, le différence est vraiment significative !

ltf4uzkpc2delk1ftsnj-9229390

Source : http://blog.arungupta.me/rest-vs-websocket-comparison-benchmarks/

Installation de la librairie Websocket pour ESP8266 sur l’IDE Aduino

En faisant une recherche sur le mot clé websocket depuis le gestionnaire de bibliothèque, on trouve plusieurs librairies compatibles avec les modules ESP8266. Je vous conseille toutefois d’utiliser la librairie développée par Markus Sattler. Elle permet de démarrer un serveur ou de transformer l’ESP8266 en client Websocket. Elle est disponible sur GitHub ici. Elle est compatible avec les cartes suivantes (dommage, elle n’est compatible avec l’ESP32) : 

  • Arduino
  • ESP8266
  • ESP31B
  • Particle avec STM32 ARM Cortex M3
  • ATmega328
  • ATmega2560

nts8b9pukhf2rpguid6w-5058896

Démarrer un serveur Websocket sur un ESP8266 (fonctionne également sur Arduino et ESP32)

La librairie Websocket contient plusieurs exemples (client, serveur). Cette librairie est très bien faite. Elle permet de brancher une fonction callback qui sera appelée à chaque fois qu’un message est reçu. Il est alors très facile de brancher les traitements associés en décodant les messages (nous verrons comment faire dans le prochain paragraphe).

On commence par créer un objet qui contiendra le serveur Websocket

WebSocketsServer webSocket = WebSocketsServer(81);

Dans le setup, on démarre le serveur et on indique la procédure callback qui sera appelée à chaque nouveau message.

webSocket.begin();
webSocket.onEvent(webSocketEvent);

La librairie permet de connaître le type de message reçu :

  • WStype_DISCONNECTED, le client s’est déconnecté
  • WStype_CONNECTED, un client vient de se connecter
  • WStype_TEXT, un message de type text (chaine, JSON..) vient d’arriver
  • WStype_BIN, un message de type binaire vient d’arriver

Enfin, on ajoute comme pour un serveur web, l’actualisation du serveur  chaque passage dans la boucle loop. Il est donc préférable de ne pas mettre de delay dans cette boucle pour ne pas ralentir le serveur et donc la réception des messages.

webSocket.loop();

Créez un nouveau croquis et collez le code ci-dessous en modifiant les paramètres WiFi.

#include 
#include 

const char* ssid     = "XXXX";
const char* password = "XXXX";
const int pinLed0 = 13; 

WebSocketsServer webSocket = WebSocketsServer(81);

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght) {
    Serial.printf("[%u] get Message: %s\r\n", num, payload);
    switch(type) {
        case WStype_DISCONNECTED:      
            break;
        case WStype_CONNECTED: 
            {
              IPAddress ip = webSocket.remoteIP(num);
              Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\r\n", num, ip[0], ip[1], ip[2], ip[3], payload);    
            }
            break;
        
        case WStype_TEXT:
            {
              //Serial.printf("[%u] get Text: %s\r\n", num, payload);
              String _payload = String((char *) &payload[0]);
              //Serial.println(_payload);
              
              String idLed = (_payload.substring(0,4));
              String intensity = (_payload.substring(_payload.indexOf(":")+1,_payload.length()));
              int intLed = intensity.toInt();
              Serial.print("Intensity: "); Serial.print(intensity); Serial.print(" to int "); Serial.println(intLed);
              updateLed (idLed, intLed);
              
            }   
            break;     
             
        case WStype_BIN:
            {
              hexdump(payload, lenght);
            }
            // echo data back to browser
            webSocket.sendBIN(num, payload, lenght);
            break;
  
    }
}

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(pinLed0, OUTPUT); 
  WiFi.begin(ssid, password);

  while(WiFi.status() != WL_CONNECTED) {
     Serial.print(".");
     delay(200);
  }
    
  Serial.println("");
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  delay(500);  
   
  Serial.println("Start Websocket Server");
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
}

void loop() {
  webSocket.loop();
}

void updateLed(String idLed, int intLed){
  int valPWM = map(intLed, 0, 99, 0, 254);
  Serial.println(valPWM);
  analogWrite(pinLed0, valPWM);
}

Comment décoder les messages WebSocket

Il est possible d’envoyer un message au format JSON et de le décoder avec la librairie ArduinoJSON présenté précédemment dans cet article par exemple. Cette librairie pose toutefois problème dès qu’on reçoit un très grand nombre de messages. Pour piloter l’intensité lumineuse d’un LED en PWM ou la position d’un servomoteur, il est préférable de découper le message à l’aide de fsonctions plus classiques qui nécessitent moins de ressources sur l’ESP8266. On pourra par exemple utiliser la fonction C++ substring(position, nombre de caractères). Ici, on récupère le premier message (payload[0]) puis on extrait l’identifiant de la LED ainsi que l’intensité. Le séparateur utilisé est “:”, ce qui donne un message du type led0:58.

String _payload = String((char *) &payload[0]);
Serial.println(_payload);
              
String numLed=(_payload.substring(0,4));
String dirServo=(_payload.substring(_payload.indexOf(":"),_payload.length()));

moveServos(numServo, dirServo);

Ensuite, il est préférable de tester la présence d’une chaîne plutôt que de faire un test d’égalité entre deux chaînes (==).

if ( numServo.indexOf("0") > 0 ) {
   if ( dirServo.indexOf("left") > 0 ) {
     // traitement
   }
}

Client Websocket en Python avec la librairie ws4py

Il existe plusieurs librairies Python permettant de mettre en place une communication Websocket. Voici les principales que vous pouvez utiliser dans vos projets :

  • websockets 4.x. C’est la plus connue, la documentation est ici.
  • ws4py est une librairie développée par Sylvain Hellegouarch (Lawouach sur GitHub). Elle est disponible sur GitHub ici. Elle est référencée sur PyPi, donc très facile à installer et à mettre à jour avec la commande pip. La documentation se trouve ici.

Ici, je vous propose d’utiliser ws4py qui est assez bien documenté et qui supporte très bien l’utilisation des threads sous Python (tâches réalisées en parallèle indépendamment du programme principal). Commencez par installer la librairie ws4py en exécutant la commande suivante

pip install ws4py

Ici, on va créer en client Websocket. Pour cela, on va importer la classe WebSocketClient de la librairie ws4py.

from ws4py.client.threadedclient import WebSocketClient

La librairie ws4py expose plusieurs méthodes callback qui pourront servir à déclencher des traitements dans le code. On dispose des méthodes suivantes :

  • opened
  • closed
  • received_message

La documentation de ws4py donne un exemple d’utilisation. On créé un DummyClient de type WebSocketClient qui contient les différents états du client.

class DummyClient(WebSocketClient):
    def opened(self):
        print("Websocket open")
    def closed(self, code, reason=None):
        print "Connexion closed down", code, reason
    def received_message(self, m):
        print m

Ensuite, on créé un objet websocket en lui passant en paramètre l’adresse ip de serveur websocket. Pour le moment, on va communiquer sans sécurité sur le port 81. L’adresse d’un serveur est composée du préfixe ws:// ou wss:// s’il est sécurisé par un certificat SSL (ce n’est pas le cas ici). Ensuite, on trouve l’adresse IP du serveur, dans le cas présent, c’est l’adresse IP de l’ESP8266. Enfin, le port, ici 81. Cela donne par exemple

ws://192.168.1.65:81/

Enfin, on ouvre la communication avec le serveur

ws.connect()

La commande ws.send(message) permet d’envoyer à n’importe quel moment un message au serveur. Les messages en provenance du serveur seront traités par le DummyClient à l’aide de la méthode received_message. On peut envoyer n’importe quel type de données. Une chaine de caractère avec un séparateur de données quelconque (| : – , ), un JSON (pour cela on pourra utiliser le package JSON pour python), des données binaires (une image par exemple)…

Code Python complet du client websocket

Créez un nouveau script avec la commande nano wsled.py (par exemple) et collez le code complet ci-dessous. Modifiez l’adresse IP de l’ESP8266. Enregistrez le script avec CTRL  X puis Y ou O (en français).

from ws4py.client.threadedclient import WebSocketClient
import time, requests

esp8266host = "ws://192.168.1.65:81/"

class DummyClient(WebSocketClient):
    def opened(self):
        print("Websocket open")
    def closed(self, code, reason=None):
        print "Connexion closed down", code, reason
    def received_message(self, m):
        print m

if __name__ == '__main__':
    try:
        ws = DummyClient(esp8266host)
        ws.connect()
        print("Ready !")
        
        i = 0
        while i < 101:
          payload = "led0:" + str(i)
          ws.send(payload)
          time.sleep(.20)
          i +=1

        print("Demo finish, close Websocket connexion now and exit script")
        ws.send("led0:0")
        ws.close()
        exit()

    except KeyboardInterrupt:
        ws.send("led0:0")
        ws.close()

Branchez une LED entre la broche D7 (GPIO15) et le GND. Ajoutez une résistance en fonction de la tension admissible par la LED (plus d’info ici). Téléversez le script Arduino sur l’ESP8266 et attendez que celui-ci soit connecté au réseau WiFi en ouvrant le moniteur série. Dès que l’ESP est connecté au réseau, vous pouvez lancer le script avec exécutant la commande python wsled.py. Vous pouvez suivre sur le moniteur série la réception et le décodage des messages. Vous pouvez modifier la vitesse de publication des coté client (script python) en modifiant le temps d’attente. Ici, il est de .10 soit 100ms (en python, la durée d’attente de la fonction time.sleep est indiquée en seconde). Vous pouvez interrompre le script à n’importe quel moment avec la combinaison de touche CTRL + C.

Voilà, tout est en place pour le projet de pilotage de bras robotique à l’aide d’un Gamepad !

D’autres projets robotiques

Voici d’autres projets robotiques qui pourraient vous intéresser

Avez-vous aimé cet article ?