WebSocket communication with an ESP8266 or Arduino in Python. Test with the ws4py library on Raspberry Pi

The Websocket is a much faster communication protocol than the REST protocol that uses standard HTTP requests. The Websocket allows to open a bi-directional communication channel between two devices. In this case, it will be between an ES8266 (but it could also be an Arduino or ESP32) and a Raspberry Pi 3. In this tutorial, we will start a WebScoket server on an ESP8266 (Wemos d1 R2). The client will be developed in Python and will be hosted on a Raspberry Pi. We will re-use this architecture very soon to drive a robot arm in WiFi using a Gamepad (in the previous article, we saw how to intercept the shares).

The advantages of Websocket compared to the classic HTTP REST API

The websocket has been developed for applications that require quick or interactive answers. HTTP was developed in the prehistory of the Web (by CERN Geneva). The HTTP protocol is used to make the internet sites work but also the mobile applications (for example). REST APIs are also based on HTTP. HTTP is not suitable for applications that require quick or interactive responses. Indeed, every time the client makes a request to the server, we must open a connection, wait for the response from the server and close the connection which is resource-consuming and takes time to process.

We will not take into account the Ajax technology that allows the content of a web page to be updated asynchronously. Ajax can be used in Arduino / ESP8266 projects (and equivalent) on the web client side, ie in the HTML interface of the project (read these articles to find out more). An example of realization here.

The Websocket aims to solve these problems. The Websocket opens a communication tunnel between two devices. This tunnel remains open until the client disconnects. At any time the client can send messages (JSON, binary, text …) and vice versa.

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

 

To summarize, the Websocket has the following advantages:

  • Bi-directional: The HTTP protocol is unidirectional, ie the client sends a request to which the server responds. The customer then consumes the answer and so on. WebSocket is a bidirectional protocol in which there are no predefined message templates such as request / response. The client or server can send a message to the other party.
  • Full-duplex: server and client can send messages at any time regardless of current processes.
  • Single TCP Connection: Typically, a new TCP connection is started for an HTTP request and ends after the response is received. A new TCP connection must be established for another HTTP request / response. With the WebSocket, the client and the server communicate on the same TCP connection until the client or server closes the connection.
  • Lightweight: the Websocket focuses on the essential unlike the HTTP which loads a lot of information to each question / answer
    In terms of performance, the Websocket is much faster as shown in this study by developer Arun Gupta in 2014. Arun measured the time needed to send packets of messages. Each message weighs 1000 bytes.
websocket rest messages comparison speed

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

The difference may seem insignificant (30% faster) for a very small number of messages. You will see that to control a robotic arm or LED lighting from a mobile application, the difference is really significant!

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

Installing the Websocket library for ESP8266 on the Aduino IDE
By doing a search on the websocket keyword from the library manager, there are several libraries compatible with ESP8266 modules. However, I advise you to use the bookstore developed by Markus Sattler. It can start a server or turn the ESP8266 into a Websocket client. It is available on GitHub here. It is compatible with the following cards (damage, it is not compatible with the ESP32):

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

Start a Websocket server on an ESP8266 (also works on Arduino and ESP32)

The Websocket library contains several examples (client, server). This bookstore is very well done. It allows to connect a callback function that will be called each time a message is received. It is then very easy to connect the associated processing by decoding the messages (we will see how to do in the next paragraph).

We start by creating an object that will contain the Websocket server

WebSocketsServer webSocket = WebSocketsServer(81);

In the setup, we start the server and we indicate the callback procedure that will be called for each new message.

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

The library allows to know the type of message received:

  • WStype_DISCONNECTED, the client has disconnected
  • WStype_CONNECTED, a customer just signed in
  • WStype_TEXT, a message of type text (string, JSON ..) has just arrived
  • WStype_BIN, a binary message has just arrived

Finally, we add as for a web server, updating the server each passage in the loop loop. It is better not to put delay in this loop to not slow down the server and therefore the receipt of messages.

webSocket.loop();

Create a new sketch and paste the code below by changing the WiFi settings.

#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>

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);
}

How to decode WebSocket messages

It is possible to send a message in JSON format and to decode it with the ArduinoJSON library presented previously in this article for example. However, this library is problematic when you receive a very large number of messages. To control the light intensity of a PWM LED or the position of a servomotor, it is best to split the message using more conventional functions that require less resources on the ESP8266. For example, we could use the C++ substring function (position, number of characters). Here, we get the first message (payload [0]) and we extract the identifier of the LED and the intensity. The separator used is “:”, which gives a message of the 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);

Then, it is better to test the presence of a string rather than doing a test of equality between two chains (==).

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

Websocket client in Python with the ws4py library

There are several Python libraries for setting up Websocket communication. Here are the main ones you can use in your projects:

  • websockets 4.x. This is the best known, the documentation is here.
  • ws4py is a library developed by Sylvain Hellegouarch (Lawouach on GitHub). It is available on GitHub here. It is referenced on PyPi, so very easy to install and update with the pip command. The documentation is here.

Here, I propose you to use ws4py which is quite well documented and which supports very well the use of the threads under Python (tasks realized in parallel independently of the main program). Start by installing the ws4py library by running the following command

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

The ws4py library exposes several callback methods that can be used to trigger processing in the code. The following methods are available:

  • opened
  • closed
  • received_message

The ws4py documentation gives an example of use. We create a DummyClient of type WebSocketClient that contains the different states of the 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

Next, we create a websocket object by passing it the websocket server ip address. For the moment, we will communicate without security on the port 81. The address of a server is composed of the prefix ws:// or wss:// if it is secured by an SSL certificate (it is not the case here). Then we find the IP address of the server, in this case it is the IP address of the ESP8266. Finally, the port, here 81. This gives for example

ws://192.168.1.65:81/

Finally, we open the communication with the server

ws.connect()

The ws.send (message) command sends a message to the server at any time. Messages from the server will be processed by the DummyClient using the received_message method. Any type of data can be sent. A character string with any data separator (|: -,), a JSON (for this we can use the JSON package for python), binary data (an image for example) …

Websocket client, full code in Python

Create a new script with the nano command wsled.py (for example) and paste the complete code below. Change the IP address of the ESP8266. Save the script with CTRL + X then Y.

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()

Connect an LED between pin D7 (GPIO15) and GND. Add a resistor according to the voltage allowable by the LED (more info here). Upload the Arduino script to the ESP8266 and wait for it to connect to the WiFi network by opening the serial monitor. As soon as ESP is connected to the network, you can start the script with the python wsled.py  command. You can monitor the reception and decoding of messages on the serial monitor. You can change the publication speed of the client side (python script) by changing the waiting time. Here, it is .10 or 100ms (in python, the duration of the time.sleep function is indicated in seconds). You can pause the script at any time with the CTRL + C key combination.

That’s it, everything is in place for the project of piloting robotic arms with the help of a Gamepad!

Subscribe to the weekly newsletter

No spam and no other use will be made of your email. You can unsubscribe anytime.

  • Anthony Barnett

    Great tutorial and works even though I am a complete novice to all of this. Working on a Nodemcu apart from I have an exception error I need to track down – Exception (28):

    Just to let you know a couple of your prints are missing the ( )

  • Mayck Bernales

    I’m sure you can help me, this code is a small control of my house, it is supposed to turn on lights of my house is more works, but the problem is that when updating the page the color of the label is red again , but the led is still on, in addition to the other clients my son and wife, they can not see when I turn on or off the lights, they do not receive the data that I send to the webSocket, but if they can use it, I need the labels to be updated and all of us who are connected can see what someone else does, thank you very much for the help.

    pleace help me, from chile.

    ByR Hogar

    ByR Control Hogar

    RADIO
    TV
    Camara

    CONTROL DE LUCES

    COMEDOR

    ALACENA

    DORMITORIO

    LAMPARA

    BAÑO

    TOCADOR

    CONTROL DE ACCESORIOS

    CALEFACTOR

    PUERTA

    VENTILADOR

    TEMPERATURA
    24°C

    HUMEDAD
    28 %

    TECNOLOGIA Y HOGAR
    Gracias a la tecnología cada uno de nuestros artefactos eléctricos y electrónicos, en la actualidad los podemos controlar de manera inalámbrica, ya sea por “Control remoto, Bluetooth, Wifi”, y hoy lo podemos hacer via internet; Gracias al “Internet de las cosas(IOT)”, y una tecnología muy indispensable, como lo es “WebSocket”.
    Diseñado por Michel Bernales &copy 2018 ¡Todos los derechos son para uso personal.!

    http://javascript/reloj.js
    http://javascript/index.js

    javascript————————————————————

    var Socket = new WebSocket(‘ws://’ + window.location.hostname + ‘:81’);
    Socket.onopen = function(evt) {
    console.log(‘Conexión abierta’ + new Date());
    };
    Socket.onclose = function(evt) {
    console.log(‘Conexión cerrada’);
    };
    Socket.onerror = function(evt) {
    console.log(evt.data);
    };
    Socket.onmessage = function(evt){
    console.log(evt);

    var c=document.getElementById(‘StatusComedor’);
    if (StatusComedor==true) {
    c.style.background=’#2DB30B’;
    }else if(StatusComedor==false){
    c.style.background=’red’;
    }
    };

    function enviarComedor(c){
    Socket.send(c.id);
    }
    function enviarComedor() {
    var e=document.getElementById(‘comedor’);
    if(e.checked==true){
    Socket.send(‘1’);
    }else if(e.checked==false){
    Socket.send(‘0’);
    }
    }
    function enviarAlacena(){
    var e=document.getElementById(‘alacena’);
    if(e.checked==true){
    Socket.send(‘3’);
    }else if(e.checked==false){
    Socket.send(‘2’);
    }
    }
    function enviarDormitorio(){
    e=document.getElementById(‘dormitorio’);
    if(e.checked==true){
    Socket.send(‘5’);
    }else if(e.checked==false){
    Socket.send(‘4’);
    }
    }
    function enviarLampara(){
    e=document.getElementById(‘lampara’);
    if(e.checked==true){
    Socket.send(‘7’);
    }else if(e.checked==false){
    Socket.send(‘6’);
    }
    }
    function enviarBaño(){
    e=document.getElementById(‘baño’);
    if(e.checked==true){
    Socket.send(‘9’);
    }else if(e.checked==false){
    Socket.send(‘8’);
    }
    }
    function enviarTocador(){
    e=document.getElementById(‘tocador’);
    if(e.checked==true){
    Socket.send(’11’);
    }else if(e.checked==false){
    Socket.send(’10’);
    }
    }
    function enviarCalefactor(){
    e=document.getElementById(‘calefactor’);
    if(e.checked==true){
    Socket.send(’13’);
    }else if(e.checked==false){
    Socket.send(’12’);
    }
    }
    function enviarPuerta(){
    e=document.getElementById(‘puerta’);
    if(e.checked==true){
    Socket.send(’15’);
    }else if(e.checked==false){
    Socket.send(’14’);
    }
    }
    function enviarVentilador(){
    e=document.getElementById(‘ventilador’);
    if(e.checked===true){
    Socket.send(’17’);
    }else if(e.checked==false){
    Socket.send(’16’);
    }
    }

    css———————————————————————————-

    body {
    display: flex;
    background: rgba(186, 186, 199,.6);
    margin: auto;
    justify-content: center;
    text-align: center;
    max-width: 600px;
    font-family: sans-serif;
    }
    h1{
    margin: 5px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    color: #020972;
    }
    h3{
    color: #781123;
    font-weight: 600;
    margin-bottom: 2px;
    }
    .contenedor{
    width: 85%;
    background: #E4A5A0;
    padding: 3px;
    }
    hr{
    height: 5px;
    }
    .enlaces{
    width: 100%;
    margin-bottom: -14px;
    display: inline-block;
    flex-wrap: wrap;
    flex-flow:
    }
    a{
    transition: all .2s;
    float: right;
    width: 52px;
    margin-top: -4px;
    margin-right: 2px;
    height: auto;
    color: #050C5D;
    display: inline-block;
    text-decoration: none;
    text-align: center;
    font-size: 14px;
    font-weight: 600;
    flex-direction: row;
    flex-wrap: wrap;
    border-radius: 3px;
    padding: 8px;
    }
    a:hover{
    background: rgba(231, 58, 137,.7);
    }
    .wrap{
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    }
    .accesorios{
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    width: 90%;
    }
    .accesorios h3{
    width: 100%;
    margin-bottom: 5px;
    }
    .temperatura{
    width: 77%;
    height: 85px;
    margin: auto;
    display: flex;
    justify-content: space-around;
    }
    .temperatura label{
    width: 75px;
    height: 53px;
    font-size: 24px;
    background: #FFBAE3;
    color: darkgreen;
    margin-top: -12px;
    padding: 0 2px;
    border-top-left-radius: 55%;
    border-top-right-radius: 55%;
    }
    .temperatura h3{
    font-size: 13px;
    margin-bottom: 20px;
    }
    .temp label{
    margin-left: 7px;
    }
    input{
    display: none;
    }
    label{
    display: inline-block;
    background: red;
    color: #FDE4E4;
    text-align: center;
    border-radius: 5px;
    cursor: pointer;
    width: 80px;
    height: 60px;
    margin: 2px;
    font-size: 70%;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    }
    input[type=”checkbox”]:checked + label{
    background: rgba(48, 242, 0,.9);
    color: black;
    transition: all .1s;
    }

    h4{
    color: #045508;
    text-decoration-line: underline;
    margin-top: 8px;
    margin-bottom: 20px;
    font-size: 14px;
    }
    p{
    font-size: 11px;
    color: rgba(0, 0, 0,.7);
    justify-content: center;
    margin-bottom: -5px;
    }
    footer{
    font-size: 12px;
    font-weight: 600;
    margin-top: 25px;
    }

    arduino ide –esp8266————————————————

    #include
    #include
    #include
    #include
    #include
    #include

    const char *ssid = “Diana michel”;
    const char *password = “020587270572”;

    bool StatusComedor;

    const char COMEDOR_ON[]= “1”;
    const char COMEDOR_OFF[]=”0″;

    int16_t thisRead = 0;
    int16_t lastRead = 0;
    uint8_t counter = 0;

    WebSocketsServer webSocket = WebSocketsServer(81);
    ESP8266WebServer server(80);

    void setup(void){
    delay(1000);

    Serial.begin(115200);

    pinMode(16, OUTPUT);
    pinMode(5, OUTPUT);
    pinMode(4, OUTPUT);
    pinMode(0, OUTPUT);
    pinMode(2, OUTPUT);
    pinMode(12, OUTPUT);
    pinMode(15, OUTPUT);
    pinMode(3, OUTPUT);

    WiFi.begin(ssid, password);
    Serial.println(“Iniciando conexión a modem infinitum”);
    while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(“.”);
    }
    IPAddress myIP = WiFi.localIP();
    Serial.println(“”);
    Serial.print(“Direccion IP: “);
    Serial.println(myIP);

    SPIFFS.begin();

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

    server.onNotFound([](){
    if(!handleFileRead(server.uri()))
    server.send(404, “text/plain”, “FileNotFound”);
    });
    server.begin();
    Serial.println(“Servidor web iniciado”);
    }
    void loop() {
    webSocket.loop();
    server.handleClient();

    counter++;
    if(counter == 255) {
    counter=0;
    thisRead = map(analogRead(A0), 900, 20, 0, 100);

    if (thisRead > 100) thisRead = 100;
    if(thisRead < 0) thisRead = 0;

    if (thisRead != lastRead){
    String message = String(thisRead);
    webSocket.broadcastTXT(message);
    }
    lastRead = thisRead;
    }
    }
    //FUNCION PROPIA DEL OBJETO WEBSOCKET
    void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght) {
    Serial.printf("webSocketEvent(%d, %d, …)rn", num, type);
    switch(type) {
    // CASO CUANDO SE DESCONECTA UN USUARIO DEL WEBSOCKET
    case WStype_DISCONNECTED: {
    Serial.printf("Usuario #%u – Desconectado!n", num);
    break;
    }
    case WStype_CONNECTED: {
    IPAddress ip = webSocket.remoteIP(num);
    Serial.printf("Conexión establecida desde la IP: %d.%d.%d.%d Nombre: %s url: %un", ip[0], ip[1], ip[2], ip[3], payload, num);
    if(StatusComedor==true){
    webSocket.sendTXT(num, COMEDOR_ON, strlen(COMEDOR_ON));
    }else if(StatusComedor==false){
    webSocket.sendTXT(num, COMEDOR_OFF, strlen(COMEDOR_OFF));
    }

    String message = String(lastRead);
    webSocket.broadcastTXT(message);
    break;
    }
    case WStype_TEXT: {
    String incoming = "";
    for (int i = 0; i < lenght; i++) {
    incoming.concat((char)payload[i]);
    }
    Serial.print(incoming);
    Serial.println("");
    uint8_t valor = incoming.toInt();
    if(valor==1){
    digitalWrite(16, HIGH);
    StatusComedor=true;
    }else if(valor==0){
    digitalWrite(16, LOW);
    StatusComedor=false;
    }

    uint8_t valor2 = incoming.toInt();
    if(valor2==3){
    digitalWrite(5, HIGH);
    }else if(valor2==2){
    digitalWrite(5, LOW);
    }

    uint8_t valor3 = incoming.toInt();
    if(valor3==5){
    digitalWrite(4, HIGH);
    }else if(valor3==4){
    digitalWrite(4, LOW);
    }

    uint8_t valor4 = incoming.toInt();
    if(valor4==7){
    digitalWrite(0, HIGH);
    }else if(valor4==6){
    digitalWrite(0, LOW);
    }

    uint8_t valor5 = incoming.toInt();
    if(valor5==9){
    digitalWrite(2, HIGH);
    }else if(valor5==8){
    digitalWrite(2, LOW);
    }

    uint8_t valor6 = incoming.toInt();
    if(valor6==11){
    digitalWrite(14, HIGH);
    }else if(valor6==10){
    digitalWrite(14, LOW);
    }

    uint8_t valor7 = incoming.toInt();
    if(valor7==13){
    digitalWrite(12, HIGH);
    }else if(valor7==12){
    digitalWrite(12, LOW);
    }

    uint8_t valor8 = incoming.toInt();
    if(valor8==15){
    digitalWrite(15, HIGH);
    }else if(valor8==14){
    digitalWrite(15, LOW);
    }

    uint8_t valor9 = incoming.toInt();
    if(valor9==17){
    digitalWrite(3, HIGH);
    }else if(valor9==16){
    digitalWrite(3, LOW);
    }
    break;
    }
    }
    }

    // A function we use to get the content type for our HTTP responses
    String getContentType(String filename){
    if(server.hasArg("download")) return "application/octet-stream";
    else if(filename.endsWith(".htm")) return "text/html";
    else if(filename.endsWith(".html")) return "text/html";
    else if(filename.endsWith(".css")) return "text/css";
    else if(filename.endsWith(".js")) return "application/javascript";
    else if(filename.endsWith(".png")) return "image/png";
    else if(filename.endsWith(".gif")) return "image/gif";
    else if(filename.endsWith(".jpg")) return "image/jpeg";
    else if(filename.endsWith(".ico")) return "image/x-icon";
    else if(filename.endsWith(".xml")) return "text/xml";
    else if(filename.endsWith(".pdf")) return "application/x-pdf";
    else if(filename.endsWith(".zip")) return "application/x-zip";
    else if(filename.endsWith(".gz")) return "application/x-gzip";
    return "text/plain";
    }
    // Takes a URL (for example /index.html) and looks up the file in our file system,
    // Then sends it off via the HTTP server!
    bool handleFileRead(String path){
    #ifdef DEBUG
    Serial.println("handleFileRead: " + path);
    #endif
    if(path.endsWith("/")) path += "index.html";
    if(SPIFFS.exists(path)){
    File file = SPIFFS.open(path, "r");
    size_t sent = server.streamFile(file, getContentType(path));
    file.close();
    return true;
    }
    return false;
    }

DIY Projects