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

Crear un interface Web para controlar el ESP8266 o ESP32 por websockets

Volvemos con la ESP8266 y el ESP32. Después de muchas entradas donde hemos ido introduciéndonos progresivamente los conceptos, por fin nos toca ver cómo montar un interface web para el ESP8266.

Haremos referencia al ESP8266, pero el mismo código es compatible para el ESP32, ajustando el nombre de las librerías. Al final tenéis el código tanto para el ESP8266 como para el ESP32.

Para seguir esta entrada es muy recomendable que hayáis seguido las entradas anteriores, en especial las dos últimas sobre cómo realizar acciones / recibir información en formato JSON mediante AJAX y websockets, y la de cómo usar Material Design.

¿Sí? ¿Seguro? ¿Venís con los deberes hechos? Pues entonces no tendremos ningún problema en seguir esta entrada, que va a consistir en hacer un interface web para controlar el ESP8266.

En este interface vamos a visualizar el estado de las entradas digitales del ESP8266, en el ejemplo D0, D5, D6 y D7.

esp8266-websocket-json-gpio-input

Además, podremos simularemos actuar los pines D8, D9 como salidas y sobre el D10 como salida PWM.

esp8266-websocket-json-gpio-output

Más un “plus” donde podremos disparar acciones, es decir, funciones que habremos definido en el ESP8266 y en las que haríais … pues lo que quisierais hacer.

esp8266-websocket-json-gpio-actions

En este ejemplo, las lecturas de los estados de las entradas digitales van a ser “reales”. Sin embargo las salidas y acciones las mantenemos “simuladas”, es decir, mostraremos el puerto de serie. Esto lo hacemos por seguridad, para que podáis probar el ejemplo independientemente de a lo que tengáis conectado el ESP8266 y evitemos “chispar” algo.

Si estáis seguro, simplemente sustituir el código por el necesario para poner la salida digital o el PWM al valor oportuno.

¿Parece ambicioso? Bueno, es juntar las partes que hemos visto anteriormente, para ilustrar como podríamos montar un interface Web para controlar el ESP8266. Aunque nos queda bastante código por delante, así que ¡manos a la obra!

Nuestro programa principal queda de la siguiente forma. Hemos usado la librería “ReactiveArduino” para responder a los cambios en los pins, iniciado el servidor y los Websockets, e incorporado los ‘includes’ oportunos

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

#include "config.h"  // Sustituir con datos de vuestra red
#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();
}

Por su parte, nuestra definición del servidor en el fichero ‘Server.hpp’, muy sencilla ya que vamos a ir por websocket y no necesitamos definir 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");
}

Por otro lado, nuestro fichero ‘API.hpp’, onde únicamente tenemos tres acciones de callback, una para establecer la acción de fijar una salida digital, otra para fijar un PWM, y otra para realizar acciones genéricas.

Estas funciones son las que, para el ejemplo, dejamos como simplemente mostrar por puerto serie. Cambiarlas para vuestro proyecto según necesitéis.

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

En cuanto a nuestro fichero ‘WebSockets.hpp’ se encarga de recibir las peticiones por websocket, y realizar la función oportuna en función del tipo de acción recibida.

Por otro lado, también contiene la función updateGPIO, que es llamada ante un cambio de estado un pin, y se encarga de hacer un broadcast a todos los clientes, informándoles del nuevo estado.

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

Hasta aquí, por parte del backend. En la parte del cliente, nuestra página web es un poco más larga que la anterior (el precio a pagar por ser menos fea),

<!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>
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; 
  }
.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;
}

Ahora, subimos todo este código al ESP8266 y comprobamos que el interface web se actualiza inmediatamente al cambiar el estado y, al ejecutar acciones sobre los botones, el puerto serie muestra correctamente las acciones recibidas.

Esto ya es un “buen ejemplo” de lo que podemos hacer con un ESP8266. ¡Pero siempre se puede mejorar! Hasta ahora hemos usado un vanilla Javascript (Javascript sin librerías) para la lógica del cliente. Lo cual, siempre es algo deseable.

Pero la tendencia de la programación es los lenguajes declarativos. ¿Es posible en nuestro pequeño ESP8266? ¡Pues sí! Así que, en la próxima entrada, nos adentraremos en el uso de VueJS en el ESP8266 y ESP32. ¡Hasta la próxima!

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github.

github-full

Versión para el ESP8266: https://github.com/luisllamasbinaburo/ESP8266-Examples

Versión para el ESP32: https://github.com/luisllamasbinaburo/ESP32-Examples