Plot Real-time Chart display of Sensor Readings – ESP8266/ESP32

Posted on

by

in

,

Plot Real-time Chart display of Sensor Readings – ESP8266/ESP32

Introduction

Would you like to learn how to display a real-time chart of your sensor readings using the ESP8266/ESP32 Microcontroller? This post will explain to you how you could do it and how you could apply it to your own project.

If you would like to see this project in action or you want a video tutorial then please watch the following video on my youtube channel.

Project Overview

Plot Real-time Chart display of Sensor Readings - ESP8266/ESP32 - Demo

We are going to show an HTML page with a Line Chart of our sensor readings that are asynchronously updated in real-time. I have chosen a sensor that changes instantaneously like the photoresistor or the Light Dependent Resistor (LDR). This will be a good scenario for how we could read and display the sensor readings at the same time in the millisecond range.

If we have used other sensors like Temperature, Pressure, or Humidity sensors then it does not change instantaneously then we can just update our HTML page after some seconds have passed.

The HTML page that displays the Line chart is served by our NodeMCU ESP8266/ESP32 Microcontrollers(MCU) Webserver and we have used WebSocket for the real-time message exchange.

Prerequisites

The following are the components and materials needed to follow along with this post.

  • ESP8266/ESP32 (I used NodeMCU ESP8266)
  • LDR or Photoresistor
  • Resistor (10K Ohms)
  • Breadboard and Wires

I have used Visual Studio Code with PlatformIO extensions in developing this project.

Design

Plot Real-time Chart display of Sensor Readings – ESP8266-ESP32- Design

We are creating a Webserver inside our ESP8266/ESP32 MCU. At the same time, we have a Websocket server created as well on the same port as our webserver.

When the HTML page loads, it opens a WebSocket connection to our Websocket server. This WebSocket connection allows us to pass messages in real-time and in a bidirectional manner.

When the ESP8266/ESP32 MCU senses that the LDR sensor values changes then it pushes a Websocket message to all clients. When all clients receive these messages then it updates its user interface asynchronously.

WebSocket is a very versatile protocol and you could use it in different applications that require real-time updates. If you want to know more about WebSocket then please look at my Using WebSocket in the Internet of Things (IOT) projects. I have used this protocol in different projects like:

Wiring/Schematic

ESP8266 LDR/Photoresistor Wiring
ESP8266 LDR/Photoresistor Wiring
Plot Real-time Chart display of Sensor Readings – ESP8266-ESP32- Schematic

If you are not familiar with how to interface the ESP8266 with an LDR sensor then please see this Interfacing Photoresistor or LDR into NodeMCU ESP8266.

Code

The whole code for this project is available in my Github account which you can access from here. Let us discuss what files are important and how it works.

main.cpp


/*
  Title:  Plot Real-time Chart display of Sensor Readings – ESP8266/ESP32
  Description:  Displays sensor readings in real time
  Author: donsky
  For:    www.donskytech.com
  Date:   September 20, 2022
*/

#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#include "SPIFFS.h"
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <LittleFS.h>
#endif
#include <ESPAsyncWebServer.h>


/*
  Replace the SSID and Password according to your wifi
*/
const char *ssid = "<REPLACE_WITH_YOUR_WIFI_SSID>";
const char *password = "<REPLACE_WITH_YOUR_WIFI_PASSWORD>";


// Webserver and Websockets setup
AsyncWebServer server(80);
AsyncWebSocket webSocket("/ws");

// LDR Pin
static int sensorVal = 0;
const int ANALOG_READ_PIN = A0; // or A0

// Function to handle request that is not serve by our web server
void notFound(AsyncWebServerRequest *request)
{
  request->send(404, "text/plain", "Not found");
}

// Callback function for our websocket message
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
{
  if (type == WS_EVT_CONNECT)
  {
    // client connected
    Serial.println("Client connected....");
    os_printf("ws[%s][%u] connect\n", server->url(), client->id());
    client->ping();
  }
  else if (type == WS_EVT_DISCONNECT)
  {
    // client disconnected
    os_printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id());
  }
  else if (type == WS_EVT_ERROR)
  {
    // error was received from the other end
    os_printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
  }
  else if (type == WS_EVT_PONG)
  {
    // pong message was received (in response to a ping request maybe)
    os_printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
  }
  else if (type == WS_EVT_DATA)
  {
    // do nothing as client is not sending message to server
    os_printf("ws[%s][%u] data received\n", server->url(), client->id());
  }
}

// function to read LDR value
void readLDRValue()
{

  // Read 
  int tempSensorVal = analogRead(ANALOG_READ_PIN);

  // Check if value read is different then send a websocket message to the client
  if (tempSensorVal != sensorVal)
  {
    Serial.println(tempSensorVal);
    sensorVal = tempSensorVal;

    // send message to clients when Connected
    webSocket.printfAll(std::to_string(sensorVal).c_str());

    // adding a little delay
    delay(10);
  }
}

void setup()
{

  Serial.begin(115200);
  Serial.println("Starting the Real-time Chart display of Sensor Readings ..");

  // Begin LittleFS for ESP8266 or SPIFFS for ESP32
  if (!LittleFS.begin())
  {
    Serial.println("An Error has occurred while mounting LittleFS");
    return;
  }

  // Connect to WIFI
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED)
  {
    Serial.printf("WiFi Failed!\n");
    return;
  }

  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // attach AsyncWebSocket
  webSocket.onEvent(onEvent);
  server.addHandler(&webSocket);

  // Route for root index.html
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/index.html", "text/html"); });

  // Route for root index.css
  server.on("/index.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/index.css", "text/css"); });

  // Route for root entireframework.min.css
  server.on("/entireframework.min.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/entireframework.min.css", "text/css"); });

  // Route for root index.js
  server.on("/index.js", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/index.js", "text/javascript"); });

  server.onNotFound(notFound);

  // Start the server
  server.begin();
}

void loop()
{
  // Read the LDR values continously
  readLDRValue();
}

This file creates our Webserver and the Websocket server. It also reads the values from our photoresistor or our LDR and sends them to all our Websocket clients. Let us go over what each line of the code does.

#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#include "SPIFFS.h"
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <LittleFS.h>
#endif
#include <ESPAsyncWebServer.h>

Imports the necessary header file for ESP32 and ESP8266.

/*
  Replace the SSID and Password according to your wifi
*/
const char *ssid = "<REPLACE_WITH_YOUR_WIFI_SSID>";
const char *password = "<REPLACE_WITH_YOUR_WIFI_PASSWORD>";

Change this to point to your WIFI SSID and credential.

// Webserver and Websockets setup
AsyncWebServer server(80);
AsyncWebSocket webSocket("/ws");

Declare our Webserver and Websocket server here.

// LDR Pin
static int sensorVal = 0;
const int ANALOG_READ_PIN = A0; // or A0

Set the pin to read our photoresistor or LDR.

// Function to handle request that is not serve by our web server
void notFound(AsyncWebServerRequest *request)
{
  request->send(404, "text/plain", "Not found");
}

// Callback function for our websocket message
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
{
  if (type == WS_EVT_CONNECT)
  {
    // client connected
    Serial.println("Client connected....");
    os_printf("ws[%s][%u] connect\n", server->url(), client->id());
    client->ping();
  }
  else if (type == WS_EVT_DISCONNECT)
  {
    // client disconnected
    os_printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id());
  }
  else if (type == WS_EVT_ERROR)
  {
    // error was received from the other end
    os_printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
  }
  else if (type == WS_EVT_PONG)
  {
    // pong message was received (in response to a ping request maybe)
    os_printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
  }
  else if (type == WS_EVT_DATA)
  {
    // do nothing as client is not sending message to server
    os_printf("ws[%s][%u] data received\n", server->url(), client->id());
  }
}

The notFound function responds to requests that are not present in our Webserver. The onEvent is a callback for Websocket message exchange. This is not doing anything right now but just prints the Websocket events.

// function to read LDR value
void readLDRValue()
{

  // Read 
  int tempSensorVal = analogRead(ANALOG_READ_PIN);

  // Check if value read is different then send a websocket message to the client
  if (tempSensorVal != sensorVal)
  {
    Serial.println(tempSensorVal);
    sensorVal = tempSensorVal;

    // send message to clients when Connected
    webSocket.printfAll(std::to_string(sensorVal).c_str());

    // adding a little delay
    delay(10);
  }
}

Reads our LDR or photoresistor. If the value read is different from the previous read then we send it to all Websocket clients otherwise we skip it.

void setup()
{

  Serial.begin(115200);
  Serial.println("Starting the Real-time Chart display of Sensor Readings ..");

  // Begin LittleFS for ESP8266 or SPIFFS for ESP32
  if (!LittleFS.begin())
  {
    Serial.println("An Error has occurred while mounting LittleFS");
    return;
  }

  // Connect to WIFI
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED)
  {
    Serial.printf("WiFi Failed!\n");
    return;
  }

  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

In the setup() function of the Arduino, we initialize the baud rate of our serial monitor. We begin starting our LittleFS filesystem and connect to our Wifi.

If you are not familiar with what LittleFS is then please take a look ESP8266 LittleFS Tutorial Series.

  // attach AsyncWebSocket
  webSocket.onEvent(onEvent);
  server.addHandler(&webSocket);

  // Route for root index.html
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/index.html", "text/html"); });

  // Route for root index.css
  server.on("/index.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/index.css", "text/css"); });

  // Route for root entireframework.min.css
  server.on("/entireframework.min.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/entireframework.min.css", "text/css"); });

  // Route for root index.js
  server.on("/index.js", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(LittleFS, "/index.js", "text/javascript"); });

  server.onNotFound(notFound);

  // Start the server
  server.begin();

Attach handler for our WebSocket server configure the resource that we serve from our Webserver using the server.on. Afterwhich, we start our server.

void loop()
{
  // Read the LDR values continously
  readLDRValue();
}

We read our photoresistor or LDR in the loop() function continuously.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Plot Real-time Chart display of Sensor Readings - ESP8266/ESP32</title>
    <link rel="stylesheet" href="index.css" />
    <link rel="stylesheet" href="entireframework.min.css" />
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"
      integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    ></script>
    <script src="index.js"></script>
  </head>
  <body>
    <nav class="nav" tabindex="-1" onclick="this.focus()">
      <div class="container">
        <a class="pagename current" href="#">www.donskytech.com</a>
        <a href="#">One</a>
      </div>
    </nav>
    <button class="btn-close btn btn-sm">×</button>
    <div class="container">
      <div class="hero">
        <h1>Real-time Chart display of Sensor Readings</h1>
        <div class="chart-container">
          <canvas id="myChart" width="1000" height="600"></canvas>
        </div>
      </div>
    </div>
  </body>
</html>

HTML page that we serve thru our Webserver.

  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Plot Real-time Chart display of Sensor Readings - ESP8266/ESP32</title>
    <link rel="stylesheet" href="index.css" />
    <link rel="stylesheet" href="entireframework.min.css" />
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"
      integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    ></script>
    <script src="index.js"></script>
  </head>

Import the necessary scripts and packages for our HTML page. We are using the following library in this project.

  • Chart.js in displaying the sensor readings in a graphical line chart.
  • mincss to make our web page render better on mobile
  <body>
    <nav class="nav" tabindex="-1" onclick="this.focus()">
      <div class="container">
        <a class="pagename current" href="#">www.donskytech.com</a>
        <a href="#">One</a>
      </div>
    </nav>
    <button class="btn-close btn btn-sm">×</button>
    <div class="container">
      <div class="hero">
        <h1>Real-time Chart display of Sensor Readings</h1>
        <div class="chart-container">
          <canvas id="myChart" width="1000" height="600"></canvas>
        </div>
      </div>
    </div>
  </body>

Displays the User Interface of our Webserver. The important part of this HTML fragment is <canvas id="myChart" width="1000" height="600"></canvas> which Chart.js is used to display the content.

index.js

// Declare variables
var targetUrl = `ws://${window.location.hostname}/ws`;
var websocket;
var ctx;
var myChart;
const MAX_DATA_COUNT = 20;

// Add function for body load
window.addEventListener("load", onLoad);

// Function to run during on load of the HTML Page
function onLoad() {
  initializeSocket();
  initializeChart();
}

// Initialize the chart
function initializeChart(){
  ctx = document.getElementById("myChart").getContext("2d");
  myChart = new Chart(ctx, {
    type: "line",
    data: {
      datasets: [{ label: "LDR" }],
    },
    options: {
      borderWidth: 3,
      borderColor: ["rgba(255, 99, 132, 1)"],
    },
  });
}

// Initialize the Websockets
function initializeSocket() {
  console.log(
    `Opening WebSocket connection to Microcontroller :: ${targetUrl}`
  );
  websocket = new WebSocket(targetUrl);
  websocket.onopen = onOpen;
  websocket.onclose = onClose;
  websocket.onmessage = onMessage;
}
// Websocket Callback Client function 
function onOpen(event) {
  console.log("Starting connection to server..");
}
function onClose(event) {
  console.log("Closing connection to server..");
  setTimeout(initializeSocket, 2000);
}
function onMessage(event) {
  console.log("WebSocket message received:", event);
  // Show only MAX_DATA_COUNT data
  if (myChart.data.labels.length > MAX_DATA_COUNT) {
    removeFirstData();
  }
  addData(getCurrentDateTime(), event.data);
}

// Get the current date time.  This will serve as the x-axis of our sensor data
function getCurrentDateTime() {
  var today = new Date();
  var date =
    today.getFullYear() + "-" + (today.getMonth() + 1) + "-" + today.getDate();
  var time =
    today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
  var dateTime = date + " " + time;
  return dateTime;
}

// add sensor data to chart
function addData(label, data) {
  myChart.data.labels.push(label);
  myChart.data.datasets.forEach((dataset) => {
    dataset.data.push(data);
  });
  myChart.update();
}

// Remove the first data
function removeFirstData() {
  myChart.data.labels.splice(0, 1);
  myChart.data.datasets.forEach((dataset) => {
    dataset.data.shift();
  });
}

The javascript file that handles the WebSocket messages coming from our Webserver. It also creates and updates the chart with new data. Let us go over what each line of the code does.

// Declare variables
var targetUrl = `ws://${window.location.hostname}/ws`;
var websocket;
var ctx;
var myChart;
const MAX_DATA_COUNT = 20;

Declare the variables we used in this javascript file.

// Add function for body load
window.addEventListener("load", onLoad);

// Function to run during on load of the HTML Page
function onLoad() {
  initializeSocket();
  initializeChart();
}

Add the listener for the loading of our HTML page.

// Initialize the chart
function initializeChart(){
  ctx = document.getElementById("myChart").getContext("2d");
  myChart = new Chart(ctx, {
    type: "line",
    data: {
      datasets: [{ label: "LDR" }],
    },
    options: {
      borderWidth: 3,
      borderColor: ["rgba(255, 99, 132, 1)"],
    },
  });
}

Initializes our Chart.js Line chart and set the options.

// Initialize the Websockets
function initializeSocket() {
  console.log(
    `Opening WebSocket connection to Microcontroller :: ${targetUrl}`
  );
  websocket = new WebSocket(targetUrl);
  websocket.onopen = onOpen;
  websocket.onclose = onClose;
  websocket.onmessage = onMessage;
}
// Websocket Callback Client function 
function onOpen(event) {
  console.log("Starting connection to server..");
}
function onClose(event) {
  console.log("Closing connection to server..");
  setTimeout(initializeSocket, 2000);
}
function onMessage(event) {
  console.log("WebSocket message received:", event);
  // Show only MAX_DATA_COUNT data
  if (myChart.data.labels.length > MAX_DATA_COUNT) {
    removeFirstData();
  }
  addData(getCurrentDateTime(), event.data);
}

We initialize our Websocket connections here and attach the needed callback functions. The onMessage function is called whenever our HTML page receives Websocket messages from our Webserver.

In this function, it checks first if the data exceed our maximum data count which is 20 then it removes the very first otherwise it adds data to our existing chart. This is the function that automatically updates our Line Chart from Websocket messages coming from our ESP8266/ESP32 MCU.

// Get the current date time.  This will serve as the x-axis of our sensor data
function getCurrentDateTime() {
  var today = new Date();
  var date =
    today.getFullYear() + "-" + (today.getMonth() + 1) + "-" + today.getDate();
  var time =
    today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
  var dateTime = date + " " + time;
  return dateTime;
}

The function is used to retrieve the current date and time. We use this in the plotting of our line chart.

// add sensor data to chart
function addData(label, data) {
  myChart.data.labels.push(label);
  myChart.data.datasets.forEach((dataset) => {
    dataset.data.push(data);
  });
  myChart.update();
}

// Remove the first data
function removeFirstData() {
  myChart.data.labels.splice(0, 1);
  myChart.data.datasets.forEach((dataset) => {
    dataset.data.shift();
  });
}

addData and removeFirstData are used in adding data to our line chart. It uses the Chart.js API.

index.css

.hero {
  background: #eee;
  padding: 20px;
  border-radius: 10px;
  margin-top: 1em;
}

.hero h1 {
  margin-top: 0;
  margin-bottom: 0.3em;
  text-align: center;
}

.chart-container {
  max-width: 800px;
  margin: 0 auto;
}

The file that we use for styling purposes.

entireframework.min.css

This is the mincss CSS framework library that we are using for styling. You can check out the example page on how to use this awesome CSS framework.

Notes when using ESP32

When deploying this to ESP32 then replace all “LittleFS” with “SPIFFS” in the setup() function. We are using different Filesystems in both MCU. If you are not familiar with both concepts then please read this ESP8266 LittleFS Tutorial Series – Overview.

Deploy to ESP8266/ESP32

Once you understand the code then we can start deploying to your MCU.

First, upload the File System Image

PlatformIO - Upload File System Image

Then, click upload and monitor. Wait for the assigned IP address for your MCU to be displayed and then open your browser and type in that IP address.

PlatformIO - Upload and Monitor

Wrap

We have discussed how to plot your sensor readings in a chart and automatically update in real-time using your ESP8266/ESP32 microcontroller in this post.

I hope you learn something! Happy Exploring!

If you like my post then please consider sharing this. Thanks!

Leave a Reply

Your email address will not be published.