crear-un-interface-web-para-controlar-el-esp8266

Create a Web Interface to Control ESP8266 or ESP32 via Websockets

  • 7 min

We’re back with the ESP8266 and ESP32. After many posts where we have progressively introduced the concepts, we finally get to see how to set up a web interface for the ESP8266.

We will refer to the ESP8266, but the same code is compatible with the ESP32, adjusting the library names. At the end, you have the code for both the ESP8266 and the ESP32.

To follow this post, it is highly recommended that you have followed the previous posts, especially the last two on how to perform actions / receive information in JSON format using AJAX and websockets, and the one on how to use Material Design.

Yes? Sure? Have you done your homework? Then we won’t have any problem following this post, which will consist of creating a web interface to control the ESP8266.

In this interface, we will visualize the state of the ESP8266’s digital inputs, in the example D0, D5, D6, and D7.

esp8266-websocket-json-gpio-input

Furthermore, we will simulate acting on pins D8, D9 as outputs and on D10 as a PWM output.

esp8266-websocket-json-gpio-output

Plus a “bonus” where we can trigger actions, i.e., functions that we will have defined in the ESP8266 and in which you would do… well, whatever you wanted to do.

esp8266-websocket-json-gpio-actions

In this example, the readings of the digital input states will be “real”. However, we keep the outputs and actions “simulated”, meaning we will display them on the serial port. We do this for safety, so you can test the example regardless of what you have connected to the ESP8266 and avoid “sparking” something.

If you are sure, simply replace the code with what is needed to set the digital output or PWM to the appropriate value.

Ambitious? Well, it’s about combining the parts we have seen before to illustrate how we could set up a Web interface to control the ESP8266. Although we have quite a bit of code ahead of us, so let’s get to work!

Our main program looks like this. We have used the “ReactiveArduino” library to respond to pin changes, started the server and WebSockets, and included the necessary ‘includes’.

#include <ESP8266WiFi.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <ArduinoJson.h>

#include "config.h"  // Replace with your network data
#include "API.hpp"
#include "WebSockets.hpp"
#include "Server.hpp"
#include "ESP8266_Utils.hpp"
#include "ESP8266_Utils_AWS.hpp"
#include "ReactiveArduinoLib.h"

auto obsD0 = Reactive::FromDigitalInput(D0);
auto obsD5 = Reactive::FromDigitalInput(D5);
auto obsD6 = Reactive::FromDigitalInput(D6);
auto obsD7 = Reactive::FromDigitalInput(D7);

void setup(void)
{
  Serial.begin(115200);
  SPIFFS.begin();

  ConnectWiFi_STA();

  InitServer();
  InitWebSockets();

  obsD0.Distinct().Do([](int i) { updateGPIO("D0", i); });
  obsD5.Distinct().Do([](int i) { updateGPIO("D5", i); });
  obsD6.Distinct().Do([](int i) { updateGPIO("D6", i); });
  obsD7.Distinct().Do([](int i) { updateGPIO("D7", i); });
}

void loop(void)
{
  obsD0.Next();
  obsD5.Next();
  obsD6.Next();
  obsD7.Next();
}
Copied!

As for our server definition in the ‘Server.hpp’ file, it’s very simple since we are going to use websockets and don’t need to define endpoints.

AsyncWebServer server(80);

void InitServer()
{
  server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");

  server.onNotFound([](AsyncWebServerRequest *request) {
    request->send(400, "text/plain", "Not found");
  });

  server.begin();
  Serial.println("HTTP server started");
}
Copied!

On the other hand, our ‘API.hpp’ file, where we only have three callback actions: one to set a digital output, another to set a PWM, and another to perform generic actions.

These functions are the ones that, for the example, we leave as simply printing to the serial port. Change them for your project as needed.

void setGPIO(String id, bool state)
{
   Serial.print("Set GPIO ");
   Serial.print(id);
   Serial.print(": ");
   Serial.println(state);
}

void setPWM(String id, int pwm)
{
   Serial.print("Set PWM ");
   Serial.print(id);
   Serial.print(": ");
   Serial.println(pwm);
}

void doAction(String actionId)
{
   Serial.print("Doing action: ");
   Serial.println(actionId);
}
Copied!

Regarding our ‘WebSockets.hpp’ file, it is responsible for receiving requests via websocket and performing the appropriate function based on the type of action received.

On the other hand, it also contains the updateGPIO function, which is called when a pin’s state changes and is responsible for broadcasting to all clients, informing them of the new state.

AsyncWebSocket ws("/ws");

void ProcessRequest(AsyncWebSocketClient *client, String request)
{
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, request);
  if (error) { return; }
  
  String command = doc["command"];
  if(command == "setGPIO") 
    setGPIO(doc["id"], (bool)doc["status"]);
  else if(command == "setPWM")
    setPWM(doc["id"], (int)doc["pwm"]);
  else if(command == "doAction")
    doAction(doc["id"]);
}

void updateGPIO(String input, bool value)
{
  String response;
  StaticJsonDocument<300> doc;
  doc["command"] = "updateGPIO";
  doc["id"] = input;
  doc["status"] = value ? String("ON") : String("OFF");
  serializeJson(doc, response);

  ws.textAll(response);

  Serial.print(input);
  Serial.println(value ? String(" ON") : String(" OFF"));
}
Copied!

So far, for the backend. On the client side, our web page is a bit longer than the previous one (the price to pay for being less ugly),

<!DOCTYPE html>
<html class="no-js" lang="">

<head>
    <title>ESP8266 Async GPIO</title>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <link rel="stylesheet" href="vendor/google-fonts.css">
    <link rel="stylesheet" href="vendor/material.css">
    <link rel="stylesheet" href="css/main.css">

    <div class="mdl-card mdl-shadow--2dp">
        <div class="mdl-card__title mdl-card--expand">
            <h2 class="mdl-card__title-text">Async GPIO</h2>
        </div>

        <div class="mdl-card__supporting-text">
            <h6>Input example:</h6>
            <ul class="mdl-list mdl-shadow--2dp">
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D0</span>
                    <span class="mdl-list__item-secondary-action">
                        <label id="input-label-D0" class="label-big Off-style">OFF</label>
                    </span>
                </li>
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D5</span>
                    <span class="mdl-list__item-secondary-action">
                        <label id="input-label-D5" class="label-big Off-style">OFF</label>
                    </span>
                </li>
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D6</span>
                    <span class="mdl-list__item-secondary-action">
                        <label id="input-label-D6" class="label-big Off-style">OFF</label>
                    </span>
                </li>
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D7</span>
                    <span class="mdl-list__item-secondary-action">
                        <label id="input-label-D7" class="label-big Off-style">OFF</label>
                    </span>
                </li>
            </ul>
        </div>

        <div class="mdl-card__supporting-text">
            <h6>Ouput example:</h6>
            <ul class="mdl-list mdl-shadow--2dp">
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D8</span>
                    <span class="mdl-list__item-secondary-action">
                        <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
                            <input id="output-switch-D8" data-id="D8" type="checkbox" class="mdl-switch__input" checked 
                            onchange="sendGPIO(this.dataset.id, this.checked)"/>
                        </label>
                    </span>
                </li>
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D9</span>
                    <span class="mdl-list__item-secondary-action">
                        <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
                            <input id="output-switch-D9" data-id="D9" type="checkbox" class="mdl-switch__input" checked 
                            onchange="sendGPIO(this.dataset.id, this.checked)"/>
                        </label>
                    </span>
                </li>
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D10</span>
                    <span class="mdl-list__item-secondary-action">
                        <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
                            <input id="output-switch-D10" data-id="D10" type="checkbox" class="mdl-switch__input" checked 
                            onchange="sendGPIO(this.dataset.id, this.checked)"/>
                        </label>
                    </span>
                </li>
                <li class="mdl-list__item">
                    <span class="mdl-list__item-primary-content">D10</span>
                    <span class="mdl-list__item-secondary-action">
                        <div class="mdl-grid">
                            <div class="mdl-cell mdl-cell--10-col">
                                <input id="slider-pwm-1" data-id="1" class="mdl-slider mdl-js-slider"
                                    type="range" min="0" max="255" value="25"
                                    onchange="sendPWM(this.dataset.id, this.value);" >
                            </div>
                            <div class="mdl-cell mdl-cell--2-col">
                                <input id="slider-text-pwm-1" data-id="1" style="width:35px;"
                                onchange="sendPWM(this.dataset.id, this.value);" value="25"></input>
                            </div>
                        </div>
                    </span>
                </li>
            </ul>
        </div>

        <div class="mdl-card__supporting-text ">
            <div>
                <h6>Example</h6>
                <button class="mdl-button mdl-js-button mdl-button--primary mdl-js-ripple-effect" style="width: 160px;"
                    data-id="action-1" onclick="sendAction(this.dataset.id)">
                    Do something
                </button>
            </div>
        </div>
    </div>
</body>

<script type="text/javascript" src="./js/main.js"></script>
<script type="text/javascript" src="vendor/material.js"></script>
</html>
Copied!
var connection = new WebSocket('ws://' + location.hostname + '/ws', ['arduino']);

connection.onopen = function () {
  connection.send('Received from Client');
  console.log('Connected');
};

connection.onerror = function (error) {
  console.log('WebSocket Error', error);
};

connection.onmessage = function (e) {
    console.log('Received from server: ', e.data);
    processReceived(e.data);
};

connection.onclose = function () {
  console.log('WebSocket connection closed');
};

function processReceived(data)
{
  json = JSON.parse(data)
  if(json.command == 'updateGPIO')
  {
    updateGPIO(json.id, json.status);
  }
}

function sendGPIO(id, status)
{
  let data = {
    command : "setGPIO",
    id: id,
    status: status
  }

  let json = JSON.stringify(data);
  connection.send(json);
}

function sendPWM(id, pwm)
{
  updateSliderText(id, pwm);

  let data = {
    command : "setPWM",
    id: id,
    pwm: pwm
  }

  let json = JSON.stringify(data);
  connection.send(json);
}

function sendAction(id)
{
  let data = {
    command : "doAction",
    id: id,
  }

  let json = JSON.stringify(data);
  connection.send(json);
}

function updateGPIO(id, status)
{
    document.getElementById('input-label-' + id).textContent = status;

  if(status == 'ON')
  {
        document.getElementById('input-label-' + id).classList.add('On-style');
    document.getElementById('input-label-' + id).classList.remove('Off-style');
  }
  else
    {
    document.getElementById('input-label-' + id).classList.add('Off-style');
    document.getElementById('input-label-' + id).classList.remove('On-style');
  }
}

function updateSliderText(id, value) {
  document.getElementById('slider-pwm-' + id).value = value; 
  document.getElementById('slider-text-pwm-'+ id).value = value; 
  }
Copied!
.label-big {
  border-radius: 3px;
    font-size: 16px;
    font-weight: 600;
    line-height: 2;
    padding: 0 8px;
    transition: opacity .2s linear;
    color: #fff
}

.On-style {
  background-color: #006b75;
}

.Off-style {
  background-color: #b60205;
}
Copied!

Now, we upload all this code to the ESP8266 and verify that the web interface updates immediately when the state changes and, when executing actions on the buttons, the serial port correctly displays the received actions.

This is already a “good example” of what we can do with an ESP8266. But it can always be improved! So far, we have used vanilla Javascript (Javascript without libraries) for the client logic. Which is always desirable.

But the trend in programming is declarative languages. Is it possible on our little ESP8266? Yes, it is! So, in the next post, we will delve into using VueJS on the ESP8266 and ESP32. See you next time!

Download the code

All the code from this post is available for download on Github.

github-full

Version for ESP8266: https://github.com/luisllamasbinaburo/ESP8266-Examples

Version for ESP32: https://github.com/luisllamasbinaburo/ESP32-Examples