interface-web-de-gpio-y-pwm-del-esp8266-o-esp32-con-vue-vuetify-y-websockets

Interface web de GPIO y PWM del ESP8266 o ESP32 con Vue, Vuetify y Websockets

Continuamos con una nueva entrada sobre el ESP8266 y ESP32 viendo como hacer un interface Web para controlar nuestro dispositivo con VueJS, Vuetify y NativeWebsockets.

Esta es una entrada especial porque va a ser una recapitulación y consolidado de todo lo que hemos visto hasta ahora. Poniendo en contexto, recordemos que para llegar hasta aquí hemos visto comunicación Ajax, Websockets asíncronos, Vue, Vuetify.

Ya en la entrada anterior juntamos todos los componentes para hacer un interface Web. Ya sólo nos queda unir Vuetify a la ecuación para tener una buena demo de cómo hacer un interface web para vuestro proyecto.

esp32-gpio-vuetify-resultado

La buena noticia ¡esta entrada no va a ser muy difícil! De hecho, toda la parte del backend es idéntica a la entrada anterior. Por lo cuál nos evitamos repetir el código y os remitimos a ella.

La parte que va a cambiar es el frontend, que debe adaptarse para integrar Vuetify.

Así nuestro fichero ‘index.html’ queda de la siguiente forma. Nada especialmente llamativo, simplemente hemos adaptado el código para hacer uso de los componentes proporcionados por el framework.

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

<head>
  <title>ESP8266 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>Async ESP8266</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>

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

Nuestro fichero JavaScript también va a cambiar, pero únicamente en la parte de los template de los componentes que teníamos definidos para Input GPIO, Output GPIO, PWM y acción de callback.

Vue.use(VueNativeSock.default, 'ws://' + location.hostname + '/ws', { format: 'json' })

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 json = JSON.stringify(data);
      this.$socket.send(json);
    }
  }
})

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 json = JSON.stringify(data);
      this.$socket.send(json);
    }
  }
})

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 json = JSON.stringify(data);
      this.$socket.send(json);

      this.action.callback();
    }
  }
})

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() {
    this.$socket.onmessage = (dr) => {
      console.log(dr);
      let json = JSON.parse(dr.data);
      let gpio = this.$data.gpio_input_list.find(gpio => gpio.text == json.id);
      gpio.status = json.status;
    }
  }
})

Resultado

Pues venga, subimos todo esto a nuestro ESP8266/ESP32 y cargamos la página en un navegador. Igual que en el ejemplo anterior, podemos modificar el estado de un pin en el ESP8266/ESP32, y vemos que los cambios se notifican a través de ReactiveArduino hasta el cliente.

Por otro lado, si interactuamos con la página web, vemos que las acciones realizas se muestran correctamente tanto en la consola del navegador, así como en el puerto serie del ESP8266/ESP32.

esp32-gpio-interface-web-consola

Si has llegado hasta aquí. ¡Enhorabuena! Tienes un ejemplo totalmente funcional de cómo emplear una aplicación en VueJS + Vuetify, servida desde un ESP8266/ESP32, y comunicada por medio de Websockets asíncronos. ¡Ahí es na!

Como decimos siempre, es solo una demo. Depende de ti adaptarlo a las necesidades de tu proyecto y los detalles de tu dispositivo. Pero es una buena base.

En la próxima entrada veremos otro aspecto muy interesante, la comunicación mediante MQTT. Y sí, modificaremos nuestro interface para ilustrar el funcionamiento ¡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