Language: EN

controlar-gpio-y-pwm-del-esp8266-esp32-desde-interface-web-con-vue-y-websockets

Control GPIO and PWM of ESP8266/ESP32 from Web Interface with Vue and Websockets

We continue with the entries dedicated to ESP8266 and ESP32, seeing how to control our device from an application in Vue served through Websockets.

Remember that in this post we made a web interface for Ajax calls. On the other hand, in the previous entry we saw how to integrate communication via Websockets in Vue thanks to the Native Websocket library.

Of course, if you have any doubts about these concepts, needless to say, it is a good time to review them before moving forward.

Our web interface will have the following form, where we include functions to read and write the state of the GPIO, set PWM signals, and perform actions.

esp-gpio-interface

Of course, some of these functions are going to be mocks. Our goal is to present how to structure the parts. Your part is to adapt them to the real needs of our project.

Ready? Are we ready? Well, let’s go for it. As always, we have the code divided into files to make it easier to understand and reuse the code.

Our main ‘ino’ file looks like this.

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

As usual, we are using the AsyncWebSocket library. Also, we use AsyncWebServer to serve our web page. The functions InitServer() and InitWebSockets() are defined in our corresponding ‘utils’ functions. They are the same as in the rest of the examples, so we won’t copy them again.

On the other hand, we are using the ReactiveArduino library (since it’s mine, I use it) to detect changes in the GPIO and trigger callback actions. And a little more in our main loop.

We come to the interesting part of this example in terms of the backend, the ‘websocket.hpp’ file. Here we define the logic associated with communication via Websockets, and specific to our project.

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;
  serializeJson(doc, response);

  ws.textAll(response);

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

As we can see, in this example we have two functions. One of ‘ProcessRequest’, which is responsible for receiving the websocket calls from the client to the ESP8266/ESP32. Here we receive a Json file and, depending on the received command, we call the corresponding function of our API.

On the other hand, we have the only function in this example that sends data from the backend to the client, which corresponds to updating the client on the state of a GPIO. This function is called in response to state changes using ReactiveArduino, which we saw in the main loop.

The last “piece” we have left to see in our example is the ‘API.hpp’ file, which contains the definition of the functions that are called by the ‘ProcessRequest’ function of the Websocket file.

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

As we can see, in this example we only display the data through the serial port. It is up to you to adapt these demo functions to your hardware and project.

We have finished with the backend part, now let’s go to the front part. Here we will use our well-known VueJS for the client application, combined with the Material Design CSS that we saw in this post.

So our ‘index.html’ file looks like this.

<!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="vendor/google-fonts.css">
    <link rel="stylesheet" href="vendor/material.css">
    <link rel="stylesheet" href="css/main.css">

    <div id="app">
        <div class="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"></div>
            <div class="mdl-grid">
                <div class="mdl-cell mdl-cell--3-col">
                    <h6>Input example:</h6>
                    <ul class="mdl-list mdl-shadow--2dp">
                        <gpio-input v-for="item in gpio_input_list" :gpio="item" :key="item.id" />
                    </ul>
                </div>

                <div class="mdl-cell mdl-cell--3-col">
                    <h6>Ouput example:</h6>
                    <ul class="mdl-list mdl-shadow--2dp">
                        <gpio-output v-for="item in gpio_output_list" v-bind:gpio="item" v-bind:key="item.id" />
                    </ul>

                </div>

                <div class="mdl-cell mdl-cell--3-col">
                    <h6>PWM example:</h6>

                    <ul class="mdl-list mdl-shadow--2dp">
                        <pwm v-for="item in pwm_list" :gpio="item" :key="item.id" />
                    </ul>
                </div>

                <div class="mdl-cell mdl-cell--3-col">
                    <h6>Actions example:</h6>
                    <ul class="mdl-list mdl-shadow--2dp">
                        <action v-for="item in action_list" :action="item" :key="item.id" />
                    </ul>
                </div>
            </div>
        </div>
    </div>
    </div>

    <!-- From 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/material.js"></script>
    <script type="text/javascript" src="vendor/nativeWs.min.js"></script>

    <!-- Load the file containing our Vue.JS App -->
    <script type="text/javascript" src="js/app.js"></script>
</body>
</html>

There is not much to say, we simply have four columns corresponding, respectively, to Input GPIO, Output GPIO, PWM, and generic callback actions. The purpose, logically, is to display a variety of components, labels, sliders, etc. Then you adjust it to your needs.

As usual, the “magic” of our front end lies in the JavaScript, where we define our VueJS application. Here’s a little more to it.

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

Vue.component('gpio-input', {
  props: ['gpio'],
  template: `
            <li class="mdl-list__item">
                <span class="mdl-list__item-primary-content">{{gpio.text}}</span>
                <span class="mdl-list__item-secondary-action">
                    <label class="label-big" :class="{ \'On-style \': gpio.status, \'Off-style \': !gpio.status}"
                    :id="'input-label-' + gpio.id">{{ gpio.status ? "ON" : "OFF" }}</label>
                </span>
            </li> `
})

Vue.component('gpio-output', {
  props: ['gpio'],
  template: ` <li class="mdl-list__item">
                <span class="mdl-list__item-primary-content">{{gpio.text}}</span>
                <span class="mdl-list__item-secondary-action">
                    <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
                        <input type="checkbox" class="mdl-switch__input" 
                        :id="'output-switch-' + gpio.id"
                        v-model="gpio.status" @change="sendGPIO" />
                    </label>
                </span>
              </li>`,
  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: ` <li class="mdl-list__item">
                <span class="mdl-list__item-primary-content">{{gpio.text}}</span>
                <span class="mdl-list__item-secondary-action">
                    <div class="mdl-grid">
                        <div class="mdl-cell mdl-cell--10-col">
                            <input :id="'slice-pwm-' + gpio.id" class="mdl-slider mdl-js-slider"
                                type="range" min="0" max="255"
                                v-model="gpio.value" @change="sendPWM" >
                        </div>
                        <div class="mdl-cell mdl-cell--2-col">
                            <input :id="'slice-pwm-text-' + gpio.id" style="width:35px;"
                              v-model="gpio.value" @change="sendPWM" ></input>
                        </div>
                    </div>
                </span>
              </li>`,
  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: ` <li class="mdl-list__item">
                <span class="mdl-list__item-primary-content">{{action.text}}</span>
                <span class="mdl-list__item-secondary-action">
                <button class="mdl-button mdl-js-button mdl-button--primary mdl-js-ripple-effect" style="width: 160px;"
                @click="doAction">
                Do something
            </button>
                </span>
              </li>`,
  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();
    }
  }
})

// Definition of our example app
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;
    }
  }
})

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

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

It seems like a lot, but if we analyze it, we’ll see that it’s not that much. First, we have the declaration of NativeWebsocket that we already saw in the previous entry. Nothing complicated here.

Next, we have defined four Vue components, respectively for Input GPIO, Output GPIO, PWM, and callback actions. Each component has its template for visualization, and the code necessary to send the data via Json through Websockets.

On the other hand, the VueJS App, where we have the data as lists of the four types of objects we are representing.

If we upload everything to the ESP8266/ESP32 and access the served webpage, we will see our Vue application. If we change the state of one of the pins, we will see that the change is communicated to the client.

esp-gpio-interface

Similarly, if we interact with the webpage, we can check in the browser’s development console that the data is sent correctly.

At the same time, they are received by the ESP8266/ESP32, which we can check in the command console.

esp32-gpio-interface-web-consola

Come on, I’m aware that it’s a lot of code for a demo. But we’ve been introducing the components of this application gradually in many entries. If you have any doubts, look at the code calmly (you have everything below), review the previous entries, or post a comment.

In the next entry, we will change the code

Download the code

All the code in 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