interface-web-para-el-esp8266-o-esp32-con-vue-y-mqtt

Interface web para el ESP8266 o ESP32 con Vue y MQTT

Nueva entrada sobre el ESP8266/ESP32 dedicada a ver formas de comunicación donde vamos a ver cómo hacer un interface Web controlado a través de MQTT.

Y con esta entrada (por fin!) terminamos esta serie de 40 tutoriales y más de tres añazos en los que hemos visto todas las formas de comunicación con un ESP8266/ESP32, desde peticiones HTTP a MQTT, pasando por llamadas Ajax y Websockets, entre otros.

¡Pero tranquilos! Esto no significa que sea el final de los tutoriales sobre el ESP32 en el blog. Al contrario, nos queda mucho mucho de que hablar sobre nuestro amigo el ESP32. Ahora que hemos terminado esta (larguísima) serie de entradas ¡estamos libres para hablar muchas más cosas!

Y como no podía ser de otra cosa, vamos a cerrar con nuestro ejercicio “estrella”, que como sabemos simular un proyecto de control web completo del ESP8266/ESP32. Ya vimos este mismo proyecto en su versión de Websocket y ahora toca hacerlo a través de MQTT.

Así que sín más marear la perdiz, ¡vamos al lio!.

En este caso, nuestro fichero principal es muy similar al que ya teníamos en el caso de Websockets, unicamente cambian los #includes. Si tenéis alguna duda, echarle un ojo al tutorial del caso websockets.

#include <WiFi.h>
#include <SPIFFS.h>
#include <ESPAsyncWebServer.h>
#include <AsyncMqttClient.h>
#include <ArduinoJson.h>

#include "config.h" // Sustituir con datos de vuestra red
#include "API.hpp"
#include "MQTT.hpp"
#include "Server.hpp"
#include "ESP32_Utils.hpp"
#include "ESP32_Utils_MQTT_Async.hpp"
#include "ReactiveArduinoLib.h"

auto obsD0 = Reactive::FromDigitalInput(0);
auto obsD5 = Reactive::FromDigitalInput(5);
auto obsD6 = Reactive::FromDigitalInput(6);
auto obsD7 = Reactive::FromDigitalInput(7);

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

  delay(500);

  WiFi.onEvent(WiFiEvent);
  InitMqtt();

  ConnectWiFi_STA();
  InitServer();

  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()
{
  obsD0.Next();
  obsD5.Next();
  obsD6.Next();
  obsD7.Next();
}

En el fichero API.hpp tenemos las funciones que emulan las acciones a realizar al pulsar al interface.

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

Por tro lado, el fichero MQTT.hpp contiene toda la lógica asociada a la comunicación del proyecto.


#pragma once

const IPAddress MQTT_HOST(192, 168, 1, 150);
const int MQTT_PORT = 1883;

AsyncMqttClient mqttClient;

String GetPayloadContent(char* data, size_t len)
{
  String content = "";
  for(size_t i = 0; i < len; i++)
  {
    content.concat(data[i]);
  }
  return content;
}

void SuscribeMqtt()
{
  uint16_t packetIdSub = mqttClient.subscribe("device/0/#", 0);
  Serial.print("Subscribing at QoS 2, packetId: ");
  Serial.println(packetIdSub);
}

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

  mqttClient.publish("device/0/GPIO", 0, true, (char*)payload.c_str());

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

void OnMqttReceived(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
{
  Serial.print("Received on ");
  Serial.print(topic);
  Serial.print(": ");

  String content = GetPayloadContent(payload, len);

  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, content);
  if(error) return;

  int id = doc["id"];
  bool ledStatus = doc["status"];

  String command = doc["command"];
  if(content.indexOf("GPIO") > 0 && command == "setGPIO")
    setGPIO(doc["id"], (bool)doc["status"]);
  else if(content.indexOf("PWM") > 0 && command == "setPWM")
    setPWM(doc["id"], (int)doc["pwm"]);
  else if(content.indexOf("Action") > 0 && command == "doAction")
    doAction(doc["id"]);
  else
  {
    //otras acciones
  }
}

Por el lado del cliente

La página ‘index.html’ contiene la definición de nuestro interface.

<!doctype html>
<html lang="">

<head>
  <title>ESP32 VueJS</title>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title></title>
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

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

  <v-app id="app">
    <v-toolbar app>Mqtt ESP32</v-toolbar>
    <v-content>
      <v-container fluid grid-list-md text-xs-center>

        <v-layout row wrap>
          <v-flex xs3>
            <v-card>
              <v-toolbar color="blue" dark>
                <v-toolbar-title class="text-xs-center">Input example</v-toolbar-title>
              </v-toolbar>
              <v-list subheader>
                  <gpio-input v-for="item in gpio_input_list" :gpio="item" :key="item.id" />
              </v-list>
            </v-card>
          </v-flex>

          <v-flex xs3>
              <v-card>
                <v-toolbar color="blue" dark>
                  <v-toolbar-title class="text-xs-center">Output example</v-toolbar-title>
                </v-toolbar>
                <v-list subheader>
                    <gpio-output v-for="item in gpio_output_list" v-bind:gpio="item" v-bind:key="item.id" />
                </v-list>
              </v-card>
            </v-flex>

            <v-flex xs3>
                <v-card>
                  <v-toolbar color="blue" dark>
                    <v-toolbar-title class="text-xs-center">PWM example</v-toolbar-title>
                  </v-toolbar>
                  <v-list subheader>
                      <pwm v-for="item in pwm_list" :gpio="item" :key="item.id" />
                  </v-list>
                </v-card>
              </v-flex>

              <v-flex xs3>
                  <v-card>
                    <v-toolbar color="blue" dark>
                      <v-toolbar-title class="text-xs-center">Actions example</v-toolbar-title>
                    </v-toolbar>
                    <v-list subheader>
                        <action v-for="item in action_list" :action="item" :key="item.id" />
                    </v-list>
                  </v-card>
                </v-flex>
        </v-layout>
      </v-container>
    </v-content>
  </v-app>

  <!-- Desde CDN -->
  <!--<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>-->
  <script type="text/javascript" src="./vendor/vue.min.js"></script>
  <script type="text/javascript" src="./vendor/vuetify.min.js"></script>
  <script type="text/javascript" src="./vendor/nativeWs.min.js"></script>
  <script type="text/javascript" src="./vendor/mqttws31.min.js"></script>

  <!-- Cargamos el fichero que contiene nuestra App en Vue.JS -->
  <script type="text/javascript" src="./js/API.js"></script>
  <script type="text/javascript" src="./js/app.js"></script>
</body>
</html>

La “chicha” de nuestra web está en el fichero App.js que contiene la lógica de nuestra aplicación

Vue.component('gpio-input', {
  props: ['gpio'],
  template: `
    <v-list-tile avatar>
      <v-list-tile-content>
        <v-list-tile-title>{{gpio.text}}</v-list-tile-title>
      </v-list-tile-content>
    <v-list-tile-action>
      <v-list-tile-action-text>{{ gpio.status ? "ON " : "OFF "}}</v-list-tile-action-text>
      <v-icon :color="gpio.status ? 'teal' : 'grey'">fiber_manual_record</v-icon>
    </v-list-tile-action>
    </v-list-tile>
    `
})

Vue.component('gpio-output', {
  props: ['gpio'],
  template: ` 
    <v-list-tile avatar>
      <v-list-tile-content>
        <v-list-tile-title>{{gpio.text}}</v-list-tile-title>
      </v-list-tile-content>
      <v-list-tile-action>
        <v-switch v-model="gpio.status" class="ma-2" :label="gpio.status ? 'ON' : 'OFF'" @change="sendGPIO"></v-switch>
      </v-list-tile-action>
    </v-list-tile>
`,
  methods: {
    sendGPIO: function (evt) {
      console.log(this.gpio.text + ': ' + this.gpio.status);

      let data = {
        command: "setGPIO",
        id: this.gpio.text,
        status: this.gpio.status
      }

      let topic = 'device/0/GPIO/';
      let payload = JSON.stringify(data);
      let pubQoS = 0;
      let retain = false;
      client.send(topic, payload, Number(pubQoS), retain);
    }
  }
})

Vue.component('pwm', {
  props: ['gpio'],
  template: `     
      <v-list-tile avatar>
        <v-list-tile-content>
          <v-list-tile-title>{{gpio.text}}</v-list-tile-title>
        </v-list-tile-content>
        <v-list-tile-action>
        <v-slider thumb-label v-model="gpio.value" min="0" max="255" @change="sendPWM">
            <template v-slot:append>
              <v-text-field class="mt-0 pt-0" hide-details single-line  type="number" style="width: 50px"
                    v-model="gpio.value" @change="sendPWM"></v-text-field>
              </template>
              </v-slider>
        </v-list-tile-action>
      </v-list-tile>`,
  methods: {
    sendPWM: function (evt) {
      console.log(this.gpio.text + ': ' + this.gpio.value);

      let data = {
        command: "setPWM",
        id: this.gpio.text,
        pwm: this.gpio.value
      }

      let topic = 'device/0/PWM/';
      let payload = JSON.stringify(data);
      let pubQoS = 0;
      let retain = false;
      client.send(topic, payload, Number(pubQoS), retain);
    }
  }
})

Vue.component('action', {
  props: ['action'],
  template: ` 
        <v-list-tile avatar>
        <v-list-tile-content>
          <v-list-tile-title>{{action.text}}</v-list-tile-title>
        </v-list-tile-content>
      <v-list-tile-action>
      <v-btn text small color="flat" @click="doAction">Do something</v-btn>
      </v-list-tile-action>
      </v-list-tile>
`,
  methods: {
    doAction: function (evt) {
      console.log(this.action.text + ': ' + this.action.id);
      let data = {
        command: "doAction",
        id: this.action.id,
      }

      let topic = 'device/0/Action/';
      let payload = JSON.stringify(data);
      let pubQoS = 0;
      let retain = false;
      client.send(topic, payload, Number(pubQoS), retain);
    }
  }
})

var app = new Vue({
  el: '#app',
  data: function () {
    return {
      gpio_input_list: [
        { id: 0, text: 'D0', status: 0 },
        { id: 1, text: 'D5', status: 0 },
        { id: 2, text: 'D6', status: 0 },
        { id: 3, text: 'D7', status: 0 },
      ],
      gpio_output_list: [
        { id: 0, text: 'D8', status: 1 },
        { id: 1, text: 'D9', status: 0 },
        { id: 2, text: 'D10', status: 0 },
      ],
      pwm_list: [
        { id: 0, text: 'PWM1', value: 128 },
        { id: 1, text: 'PWM2', value: 128 },
      ],
      action_list: [
        { id: 0, text: 'ACTION1', callback: () => console.log("action1") },
        { id: 1, text: 'ACTION2', callback: () => console.log("action2") },
      ]
    }
  },
  mounted() {
    client = new Paho.MQTT.Client("192.168.1.150", 9001, createGuid())
    var options = {
      onSuccess: onConnect,
      onFailure: onFailure
    };
    client.onConnectionLost = onConnectionLost;
    client.onMessageArrived = onMessageArrived;
    client.connect(options);
  }
})

Finalmente tenemos el fichero API.js, que contiene las funciones propias de MQTT usadas en el lado cliente.

function onConnect() {
  var options = {
    qos: 0,
    onSuccess: onSubSuccess,
    onFailure: onSubFailure
  };
  client.subscribe('device/0/#', options);
}

function onFailure(message) {
  console.log(message)
}

function onConnectionLost(responseObject) {
  if (responseObject.errorCode !== 0) {
    console.log("onConnectionLost:" + responseObject.errorMessage);
  }
}

function onMessageArrived(message) {
  console.log(message)
  var topic = message.destinationName;
  var json = message.payloadString;

  let payload = JSON.parse(json);
  console.log(payload)
  if(payload.command.includes("updateGPIO"))
  {
    let gpio = app.gpio_input_list.find(gpio => gpio.text == payload.id);
    gpio.status = payload.status;
  }
}

function onSubFailure(message) {
  console.log(message)
}

function onSubSuccess(message) {
  console.log(message)
}

function createGuid() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    var r = Math.random() * 16 | 0,
      v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

Resultado

Subimos todo eso a nuestro ESP8266/ESP32 y si todo ha salido bien, deberíamos ver el interface de nuestro proyecto.

esp32-mqtt-gpio-result

Asimismo, comprobamos que al realizar las acciones, estas se reciben y se muestran en puerto serie. De igual forma, si realizamos un cambio de estado en uno de los pines, veremos reflejado el cambio de estado en la aplicación del cliente.

esp32-mqtt-gpio-serial

¡Y con esto hemos terminado! Como decíamos al principio de esta entrada, terminamos esta serie entradas sobre comunicación con el ESP32. Aún nos quedan muchos más tutoriales del ESP32 por ver… Pero, al que sí le vamos a decir adiós es nuestro amigo el ESP8266. El pobrecito hace un tiempo que fue declarado obsoleto por el fabricante Espressif, por lo que de ahora en adelante el resto de entradas del blog serán sólo para el ESP32.

Nos vemos en la próxima entrada, y mientras tanto os dejo aquí todo el código de estas y las anteriores entradas para que les echéis un ojo con más calma. ¡Hasta pronto!

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