ESP8266 and the Arduino IDE Part 9: Websockets

In the previous post I looked at how webpages could be made to auto reload and auto update and by using Javascript how specific parts could be updated without the need to load the whole page.

Although the Javascript makes the webpage appear slicker the website still uses the client request method as before (the webpage still had to request new data). The Javascript just made the experience nicer. This post starts to look at true asynchronous or two-way communication where either side can send data without being asked for it. This is achieved by using websockets.

 

Websockets

Websockets are a way for a server and client (server and webpage) to talk to each other freely and either side can send data at any time (asynchronous) without being asked for it. They do this by keeping open a connection unlike HTML and AJAZ which makes a new connection every time they want something. Websockets are part of HTML 5, have a Javascript api, and work in all modern browsers.

Websocket is a protocol just like HTML and just like HTML it sits on top of TCP. Trying to implement Websockets yourself can be messy but thanks to the work of Markus Sattler AKA Links2004 there is a library just for the occasion. The library is called arduinoWebSockets and is available from Github: arduinoWebSockets or through the Arduino library manager.
ESP8266Part9_WebsocketLibrary

 

Using the Websocket Library

As with most libraries you need to let the compiler know you want to use it with an include statement. The library has to be installed first otherwise you will get a “we don’t know what you want” error (or something).

#include <WebSocketsServer.h>

Then you need to create an instance/initialize it. Here we are using a websocket server on port 81 (there is also a websocket client, this is different).

WebSocketsServer webSocket = WebSocketsServer(81);

Within the setup() function start the websocket server

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

and we also set up the onEvent function.

webSocket.onEvent(webSocketEvent);

webSocket.onEvent() fires whenever anything happens with the websocket server. The event can be one of many things such as; a connection, disconnection, an error, or data is received (there are a few more). In the below examples onEvent calls another function called webSocketEvent (you can use any function name you wish). of course you also need the webSocketEvent() function.

void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
{
  if(type == WStype_TEXT)
  {
      if (payload[0] == '0')
      {
          digitalWrite(pin_led, LOW);
          Serial.println("LED=off");        
      }
      else if (payload[0] == '1')
      {
          digitalWrite(pin_led, HIGH);
          Serial.println("LED=on");        
      }
  }
 
  else  // event is not TEXT. Display the details in the serial monitor
  {
    Serial.print("WStype = ");   Serial.println(type);  
    Serial.print("WS payload = ");
// since payload is a pointer we need to type cast to char
    for(int i = 0; i < length; i++) { Serial.print((char) payload[i]); }
    Serial.println();
  }
}

In the examples below I am using ascii as control codes; “1” for on and “0” for off. This means we are using text and TEXT is one of the event types. If “type == WStype_TEXT” then we know we have received some text data. There is a brief explaination of WStype_t below. When there is any data it can be found in payload. Caveat: payload in not a char array. It is a pointer to the data.

If the event is not TEXT then I display the details in the serial monitor. This is not really required but lets you see what else is happening (such as connection or disconnection event).

num
num is the current client/connection number/ID. The arduinoWebsocket library allows a maximum of 5 at any one time.

type
type is the response type:
0 – WStype_ERROR
1 – WStype_DISCONNECTED
2 – WStype_CONNECTED
3 – WStype_TEXT
4 – WStype_BIN
5 – WStype_FRAGMENT_TEXT_START
6 – WStype_FRAGMENT_BIN_START
7 – WStype_FRAGMENT
8 – WStype_FRAGMENT_FIN
9 – WStype_PING
10- WStype_PONG

You can see that there is also a BIN data type.

If you look at the picture of the serial monitor below you can see:
WStype = 2 – this is the connection event
WStype = 10 – PONG, the reply from a PING
and not displayed would be WStype = 3 for TEXT.Since we are using ascii characters as control codes we are using TEXT.

payload
payload is the received data (when there is received data). This is a pointer not a char or char array.

length
length is the size of the data.

Inside the main loop() function we need to call webSocket.loop();

webSocket.loop();

Example 1 only has communication from the webpage to the server (ESP8266). This means it only needs to process the control codes on the ESP8266. In example 2 we add local control (a switch on the EPS8266) which requires 2-way communication and means the webpage (client) has to receive and process Websocket data. This is done in Javascript behind the scenes.

In example 2, using the Javascript websocket api, in the Javascript init() function the onmessage event is set up to call the processReceivedCommand() function. The onmessage event fires whenever there is any data received.

function init() 
{
  Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
  Socket.onmessage = function(event) { processReceivedCommand(event); };
}

event and evt are javascript objects (evt is a copy of event) that, among other things, contains the received data. The data is retrieved using evt.data (oop). When data is received we know the button switch on the ESP8266 has been used (see example 2) and so the labels and button text in the webpage are updated to reflect the new LED status.

function processReceivedCommand(evt) 
{
    document.getElementById('rd').innerHTML = evt.data;
    if (evt.data ==='0') 
    {  
        document.getElementById('BTN_LED').innerHTML = 'Turn on the LED';  
        document.getElementById('LED_status').innerHTML = 'LED is off';  
    }
    if (evt.data ==='1') 
    {  
        document.getElementById('BTN_LED').innerHTML = 'Turn off the LED'; 
        document.getElementById('LED_status').innerHTML = 'LED is on';   
    }
}

If “0” is received it means the LED has been turned off. A “1” means the LED has been turned on.

 
To recap. To use websockets on the ESP8266 you to need to
– include the libraries
– initialize the websocket
– start the server
– – send data
– – wait for some kind of event and then process the event. The event can be due to new data being received.

You need to do basically the same in the webpage but using the Javascript websockets api.
– create a new websocket object
– attach a listener function to the websocket message event so you can do something about received data
– – send data

More information on the Javascript api:
MDN Web Doc: Websocket
Tutorialspoint: HTML5 – WebSockets
linode: Introduction to WebSockets

More details about the arduinoWebSockets library can be found on github.

 
 

Example 1: LED Control Basic

The first example revisits LED Control and uses a fairly simple implementation of websockets. We have a single LED and a simple web interface to turn it on and off. This is almost the same as when we started many posts ago. The difference now of course is we are controlling the LED using websockets.

For this exercise I am using a Lolin NodeMcu V3 board but any ESP8266 board should work the same.

ESP8266_Part9_Websocket_005_webBrowser

 

Circuit

Very simply circuit. An LED and resistor connected to pin D3 (not pin 3).

ESP8266_Part9_Websocket_002_800

ESP8266_Part9_Websocket_003_Circuit

 

Sketch

/*
 * Sketch: ESP8266_Part9_01_Websocket_LED
 * Intended to be run on an ESP8266
 */
 
String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
 
String html_1 = R"=====(
<!DOCTYPE html>
<html>
<head>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'/>
  <meta charset='utf-8'>
  <style>
    body    { font-size:120%;} 
    #main   { display: table; width: 300px; margin: auto;  padding: 10px 10px 10px 10px; border: 3px solid blue; border-radius: 10px; text-align:center;} 
    .button { width:200px; height:40px; font-size: 110%;  }
  </style>
  <title>Websockets</title>
</head>
<body>
  <div id='main'>
    <h3>LED CONTROL</h3>
    <div id='content'>
      <p id='LED_status'>LED is off</p>
      <button id='BTN_LED'class="button">Turn on the LED</button>
    </div>
    <br />
   </div>
</body>
 
<script>
  var Socket;
  function init() 
  {
    Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
  }
 
  document.getElementById('BTN_LED').addEventListener('click', buttonClicked);
  function buttonClicked()
  {   
    var btn = document.getElementById('BTN_LED')
    var btnText = btn.textContent || btn.innerText;
    if (btnText ==='Turn on the LED') { btn.innerHTML = "Turn off the LED"; document.getElementById('LED_status').innerHTML = 'LED is on';  sendText('1'); }  
    else                              { btn.innerHTML = "Turn on the LED";  document.getElementById('LED_status').innerHTML = 'LED is off'; sendText('0'); }
  }
 
  function sendText(data)
  {
    Socket.send(data);
  }
 
  window.onload = function(e)
  { 
    init();
  }
</script>
</html>
)=====";
 
#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>
 
WiFiServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
 
byte pin_led = D3;
 
char ssid[] = "ssid";  // use your own network ssid and password
char pass[] = "pass";
 
void setup()
{
  pinMode(pin_led, OUTPUT);
  digitalWrite(pin_led,LOW);
 
  Serial.begin(115200);
  Serial.println();
  Serial.println("Serial started at 115200");
  Serial.println();
 
  // Connect to a WiFi network
  Serial.print(F("Connecting to "));  Serial.println(ssid);
  WiFi.begin(ssid,pass);
 
  // connection with timeout
  int count = 0; 
  while ( (WiFi.status() != WL_CONNECTED) && count < 17) 
  {
      Serial.print(".");  delay(500);  count++;
  }
 
  if (WiFi.status() != WL_CONNECTED)
  { 
     Serial.println("");  Serial.print("Failed to connect to ");  Serial.println(ssid);
     while(1);
  }
 
  Serial.println("");
  Serial.println(F("[CONNECTED]"));   Serial.print("[IP ");  Serial.print(WiFi.localIP()); 
  Serial.println("]");
 
  // start a server
  server.begin();
  Serial.println("Server started");
 
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
}
 
void loop()
{
    webSocket.loop();
 
    WiFiClient client = server.available();     // Check if a client has connected
    if (!client)  {  return;  }
 
    client.flush();
    client.print( header );
    client.print( html_1 ); 
    Serial.println("New page served");
 
    delay(5);
}
 
void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
{
  if(type == WStype_TEXT)
  {
      if (payload[0] == '0')
      {
          digitalWrite(pin_led, LOW);
          Serial.println("LED=off");        
      }
      else if (payload[0] == '1')
      {
          digitalWrite(pin_led, HIGH);
          Serial.println("LED=on");        
      }
  }
 
  else 
  {
    Serial.print("WStype = ");   Serial.println(type);  
    Serial.print("WS payload = ");
    for(int i = 0; i < length; i++) { Serial.print((char) payload[i]); }
    Serial.println();
 
  }
}

 

A look at the sketch

As with the previous examples all the code for the webpage is contained in the String variable “html_1″.
This and the header codes are sent to the web browser when a request is received. I like to have the HTML at the top of the sketch so that it is seperate to the main ESP8266 code.

client.print( header );
client.print( html_1 ); 
Serial.println("New page served");

include the libraries and initiate instances.

#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>
 
WiFiServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);

Try to connect to the local network. If you have followed along with the other guides you may nothice extra code. Here I have added a timeout. If a connection cannot be established in around 8 seconds (16 * 500ms) the sketch gives up. If the connection attempt is successful a connected message is printed in the serial monitor alond with the ESP8266’s ip address.

  // connection with timeout
  int count = 0; 
  while ( (WiFi.status() != WL_CONNECTED) && count < 17) 
  {
      Serial.print(".");  delay(500);  count++;
  }
 
  if (WiFi.status() != WL_CONNECTED)
  { 
     Serial.println("");  Serial.print("Failed to connect to ");  Serial.println(ssid);
     while(1);
  }
 
  Serial.println("");
  Serial.println(F("[CONNECTED]"));   Serial.print("[IP ");  Serial.print(WiFi.localIP()); 
  Serial.println("]");

After the connection is made the servers are started.

  // start a server
  server.begin();
  Serial.println("Server started");
 
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);

ESP8266_Part9_Websocket_004_serialMonitor

At this point the ESP8266 is connected to the netwrok and the server is running waiting for somebody to visit. So on your web browser of choice (I am using Chrome) go to the ip address shown in the serial monitor. For me that is 192.168.2.107.
ESP8266_Part9_Websocket_005_webBrowser
We are now able to magically control the LED with a simple click of a finger. Not quite Thanos level but close enough.

ESP8266_Part9_LEDOFF_web
ESP8266_Part9_LEDON_web

ESP8266_Part9_Websocket_006_serialMonitor

And that’s the first part done.

 

 
 

Example 2: LED Control Two-way

The first example works well but is not different to one we did many guides ago. So what’s so special? The next bit is. Next we introduce two-way communication using Websockets.

In this example we add a button switch to the ESP8266 to allow local control of the LED. This means we will be able to control the LED from either a hardware switch or a soft switch in the web page. So why are Websockets required for this? Websockets are used to allow the ESP8266 to send data to the webpage telling it the LED status has changed. The webpage can then update the label and button text to reflect the new LED status.

GIF_Example-2_005

 

Circuit

ESP8266_Part9_Websocket_010_Example2_Circuit_800
The GNDs and VCCs are connected. You just can’t see the wires!

ESP8266_Part9_Websocket_011_Example2_Circuit

 

Sketch

/*
 * Sketch: ESP8266_Part9_02_Websocket_LED_2Way
 * Intended to be run on an ESP8266
 */
 
String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
 
String html_1 = R"=====(
<!DOCTYPE html>
<html>
<head>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'/>
  <meta charset='utf-8'>
  <style>
    body     { font-size:120%;} 
    #main    { display: table; width: 300px; margin: auto;  padding: 10px 10px 10px 10px; border: 3px solid blue; border-radius: 10px; text-align:center;} 
    #BTN_LED { width:200px; height:40px; font-size: 110%;  }
    p        { font-size: 75%; }
  </style>
 
  <title>Websockets</title>
</head>
<body>
  <div id='main'>
    <h3>LED CONTROL</h3>
    <div id='content'>
      <p id='LED_status'>LED is off</p>
      <button id='BTN_LED'class="button">Turn on the LED</button>
    </div>
    <p>Recieved data = <span id='rd'>---</span> </p>
    <br />
   </div>
</body>
 
<script>
  var Socket;
  function init() 
  {
    Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
    Socket.onmessage = function(event) { processReceivedCommand(event); };
  }
 
 
function processReceivedCommand(evt) 
{
    document.getElementById('rd').innerHTML = evt.data;
    if (evt.data ==='0') 
    {  
        document.getElementById('BTN_LED').innerHTML = 'Turn on the LED';  
        document.getElementById('LED_status').innerHTML = 'LED is off';  
    }
    if (evt.data ==='1') 
    {  
        document.getElementById('BTN_LED').innerHTML = 'Turn off the LED'; 
        document.getElementById('LED_status').innerHTML = 'LED is on';   
    }
}
 
 
  document.getElementById('BTN_LED').addEventListener('click', buttonClicked);
  function buttonClicked()
  {   
    var btn = document.getElementById('BTN_LED')
    var btnText = btn.textContent || btn.innerText;
    if (btnText ==='Turn on the LED') { btn.innerHTML = 'Turn off the LED'; document.getElementById('LED_status').innerHTML = 'LED is on';  sendText('1'); }  
    else                              { btn.innerHTML = 'Turn on the LED';  document.getElementById('LED_status').innerHTML = 'LED is off'; sendText('0'); }
  }
 
  function sendText(data)
  {
    Socket.send(data);
  }
 
 
  window.onload = function(e)
  { 
    init();
  }
</script>
 
 
</html>
)=====";
 
#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>
 
WiFiServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
 
byte pin_led = D3;
byte pin_switch = D6;
 
boolean LEDstatus = LOW;
boolean oldSwitchState = LOW;
boolean newSwitchState1 = LOW;
boolean newSwitchState2 = LOW;
boolean newSwitchState3 = LOW;
 
 
char ssid[] = "ssid";
char pass[]= "password";
 
void setup()
{
  pinMode(pin_led, OUTPUT);
  digitalWrite(pin_led,LEDstatus);
 
  pinMode(pin_switch, INPUT);
 
  Serial.begin(115200);
  Serial.println();
  Serial.println("Serial started at 115200");
  Serial.println();
 
  // Connect to a WiFi network
  Serial.print(F("Connecting to "));  Serial.println("ssid");
  WiFi.begin(ssid,pass);
 
 
 
 
  // connection with timeout
  int count = 0; 
  while ( (WiFi.status() != WL_CONNECTED) && count < 17) 
  {
      Serial.print(".");  delay(500);  count++;
  }
 
  if (WiFi.status() != WL_CONNECTED)
  { 
     Serial.println("");  Serial.print("Failed to connect to ");  Serial.println(ssid);
     while(1);
  }
 
  Serial.println("");
  Serial.println(F("[CONNECTED]"));   Serial.print("[IP ");  Serial.print(WiFi.localIP()); 
  Serial.println("]");
 
  // start a server
  server.begin();
  Serial.println("Server started");
 
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
 
}
 
 
void loop()
{
    checkSwitch();
 
    webSocket.loop();
 
    WiFiClient client = server.available();     // Check if a client has connected
    if (!client)  {  return;  }
 
    client.flush();
    client.print( header );
    client.print( html_1 ); 
    Serial.println("New page served");
 
    delay(5);
}
 
 
void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
{
  if(type == WStype_TEXT)
  {
      if (payload[0] == '0')
      {
          digitalWrite(pin_led, LOW);
          LEDstatus = LOW;
          Serial.println("LED=off");        
      }
      else if (payload[0] == '1')
      {
          digitalWrite(pin_led, HIGH);
          LEDstatus = HIGH;
          Serial.println("LED=on");        
      }
  }
 
  else 
  {
    Serial.print("WStype = ");   Serial.println(type);  
    Serial.print("WS payload = ");
    for(int i = 0; i < length; i++) { Serial.print((char) payload[i]); }
    Serial.println();
 
  }
}
 
void checkSwitch()
{
    newSwitchState1 = digitalRead(pin_switch);    delay(1);
    newSwitchState2 = digitalRead(pin_switch);    delay(1);
    newSwitchState3 = digitalRead(pin_switch);
 
    // if all 3 values are the same we can continue
    if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
    {
        if ( newSwitchState1 != oldSwitchState )
        {
 
           // toggle the LED when the button switch is pressed rather than released
           if ( newSwitchState1 == HIGH )
           {
              LEDstatus = ! LEDstatus;
 
              if ( LEDstatus == HIGH ) { digitalWrite(pin_led, HIGH);  webSocket.broadcastTXT("1"); Serial.println("LED is ON"); }
              else                     { digitalWrite(pin_led, LOW);   webSocket.broadcastTXT("0"); Serial.println("LED is OFF"); }
           }
           oldSwitchState = newSwitchState1;
        }
   }
 
 
}

What’s New? First off I have added an extra paragraph in the HTML. This is used to show the received data. This is for debugging purposes only.

<p>Recieved data = <span id='rd'>---</span> </p>

the “—” is a holder and is replaced with any data that is received over the Websocket.

There is additional Javascript as well. Since we are now receiving data we need to … receive it. For this we use the “Socket.onmessage”. When the Javascript is initialised we tell the received data event to call a function called “processReceivedCommand()”. event is a Javascript object that contains all the event attributes including the data.

function init() 
{
  Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
  Socket.onmessage = function(event) { processReceivedCommand(event); };
}

This is where we process the incoming data and in this case it is the controls codes to turn the LED on or off. “0” for off and “1” for on.

function processReceivedCommand(evt) 
{
    document.getElementById('rd').innerHTML = evt.data;
    if (evt.data ==='0') 
    {  
        document.getElementById('BTN_LED').innerHTML = 'Turn on the LED';  
        document.getElementById('LED_status').innerHTML = 'LED is off';  
    }
    if (evt.data ==='1') 
    {  
        document.getElementById('BTN_LED').innerHTML = 'Turn off the LED'; 
        document.getElementById('LED_status').innerHTML = 'LED is on';   
    }
}

“document.getElementById(‘rd’).innerHTML = evt.data;” simply copies the received data to the new paragraph.
If we have a “0” we know the LED has been turned off and if we have a “1” we know the LED has been turned on so we set the label and button text to match.

You may notice that we know has duplicate code; here in processReceivedCommand() function and also in the buttonClicked() function. A couple of extra functions (such as LEDon() and LEDoff() could be used to make things a little tidier. I leave that for you to implement.

Becuase we added a button switch we need to tell the ESP8266 where it is. It’s on D6. Additional variables have been added to look after the switch.

byte pin_switch = D6;
 
boolean LEDstatus = LOW;
boolean oldSwitchState = LOW;
boolean newSwitchState1 = LOW;
boolean newSwitchState2 = LOW;
boolean newSwitchState3 = LOW;

We also have a function that checks the button switch

void checkSwitch()
{
    newSwitchState1 = digitalRead(pin_switch);    delay(1);
    newSwitchState2 = digitalRead(pin_switch);    delay(1);
    newSwitchState3 = digitalRead(pin_switch);
 
    // if all 3 values are the same we can continue
    if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
    {
        if ( newSwitchState1 != oldSwitchState )
        {
 
           // toggle the LED when the button switch is pressed rather than released
          // When the switch is closed the pin goes LOW to HIGH
           if ( newSwitchState1 == HIGH )
           {
              LEDstatus = ! LEDstatus;
              if ( LEDstatus == HIGH ) { digitalWrite(pin_led, HIGH);  webSocket.broadcastTXT("1"); Serial.println("LED is ON"); }
              else                     { digitalWrite(pin_led, LOW);   webSocket.broadcastTXT("0"); Serial.println("LED is OFF"); }
           }
           oldSwitchState = newSwitchState1;
        }
   }
 
}

I don’t go in to details about how the switch code works here. If you want to know more see Switching Things On And Off With An Arduino. Check out example 3 and 3a.

Now that we can control the LED at either end we need to keep track of it and we do that by using the variable LEDstatus. LED status is LOW when the LEF is off and HIGH when it is on.

Now when there is a control code from the web page, as well as controlling the LED LEDstatus is updated as well.

  if(type == WStype_TEXT)
  {
      if (payload[0] == '0')
      {
          digitalWrite(pin_led, LOW);
          LEDstatus = LOW;
          Serial.println("LED=off");        
      }
      else if (payload[0] == '1')
      {
          digitalWrite(pin_led, HIGH);
          LEDstatus = HIGH;
          Serial.println("LED=on");        
      }
  }

 
And that’s it. We now have two-way control using Websockets.

 
 
 

4 thoughts on “ESP8266 and the Arduino IDE Part 9: Websockets

  1. Hi, nice article, I’m try to find also some info about the WStype_ERROR handling, do you have some suggest, documentation about what type of event occour with this type?
    Thank!

    • All I can suggest is Google it.

      My next guide is not ready yet and may not be published for a while; busy with work. However, it should not be too hard to implement error handling. In the above example I am only interested when type is text and ignore other types. To check for an error just add a check to see if type is WStype_ERROR

      To modify the above code, add an else if:

      void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
      {
      if(type == WStype_TEXT)
      {
      // have text – do something
      }

      else if ( type ==WStype_ERROR )
      {
      // there is an error – do something about it
      }

      else
      {
      // not text and not an error
      }

      If you want to handle all types than you can use a list of if / else if statements or a case statement.

Leave a Reply to talaat Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>