VueJS, Vuetify y WebSockets permiten crear un panel GPIO para controlar un ESP32 desde el navegador.
Este ejemplo reúne varias piezas del curso: comunicación por WebSockets, mensajes JSON, VueJS para organizar la interfaz y Vuetify para darle un aspecto más cuidado.
La base es una interfaz web capaz de leer GPIO, modificar salidas PWM y lanzar acciones. Vuetify entra en la ecuación para que la demo sea más parecida a un panel real de proyecto.
La buena noticia es que toda la parte del backend mantiene la misma estructura que en el ejemplo anterior. Aquí nos centraremos en adaptar el frontend para integrar Vuetify.
La parte que va a cambiar es el frontend.
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>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>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/[email protected]/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.

Si has llegado hasta aquí, tienes un ejemplo totalmente funcional de cómo emplear una aplicación en VueJS + Vuetify, servida desde un ESP32, y comunicada por medio de WebSockets asíncronos.
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.
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 para paneles web de control y monitorización.
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.
Versión para el ESP8266: https://github.com/luisllamasbinaburo/ESP8266-Examples
Versión para el ESP32: https://github.com/luisllamasbinaburo/ESP32-Examples

