ESP32: построение графиков с несколькими сериями данных (Multiple Series)

В этом проекте показано, как создать веб-сервер на ESP32 для отображения показаний датчиков на графиках с несколькими сериями данных. В качестве примера мы построим график показаний температуры от четырёх датчиков DS18B20 на одном графике. Вы можете модифицировать проект для отображения любых других данных. Для создания графиков мы будем использовать JavaScript-библиотеку Highcharts.

ESP32 построение графиков показаний датчиков с несколькими сериями данных Arduino

У нас есть аналогичное руководство для платы ESP8266 NodeMCU:

Обзор проекта

В этом проекте мы создадим веб-сервер на ESP32, который отображает показания температуры от четырёх датчиков DS18B20 на одном графике — графике с несколькими сериями. График отображает максимум 40 точек данных для каждой серии, и новые показания добавляются каждые 30 секунд. Вы можете изменить эти значения в своём коде.

ESP32 веб-сервер с графиком с несколькими сериями данных

Датчик температуры DS18B20

Датчик температуры DS18B20 — это однопроводной цифровой датчик температуры. Это означает, что для связи с микроконтроллером ему требуется только одна линия данных.

DS18B20 распиновка однопроводного цифрового датчика температуры

Каждый датчик имеет уникальный 64-битный серийный номер, что означает, что вы можете подключить несколько датчиков к одному GPIO — как мы и сделаем в этом руководстве. Подробнее о датчике температуры DS18B20:

Server-Sent Events (SSE)

Показания обновляются автоматически на веб-странице с помощью Server-Sent Events (SSE).

Показания датчиков с несколькими сериями Server-Sent Events DS18B20

Подробнее о SSE можно прочитать:

Файлы в файловой системе

Для лучшей организации проекта и простоты понимания мы сохраним HTML, CSS и JavaScript файлы для построения веб-страницы в файловой системе платы (LittleFS).

Необходимые условия

Убедитесь, что вы выполнили все необходимые условия из этого раздела, прежде чем продолжить работу над проектом.

1. Установка платы ESP32 в Arduino IDE

Мы будем программировать ESP32 с помощью Arduino IDE. Поэтому у вас должно быть установлено дополнение ESP32. Если вы этого ещё не сделали, следуйте руководству:

Если вы хотите использовать VS Code с расширением PlatformIO, следуйте руководству:

2. Плагин загрузки файловой системы

Для загрузки HTML, CSS и JavaScript файлов во flash-память ESP32 (LittleFS) мы используем плагин для Arduino IDE: LittleFS Filesystem uploader. Следуйте руководству для установки плагина:

Если вы используете VS Code с расширением PlatformIO, прочитайте руководство:

3. Установка библиотек

Для сборки этого проекта вам необходимо установить следующие библиотеки:

Вы можете установить библиотеки через Arduino Library Manager. Перейдите в Sketch > Include Library > Manage Libraries и найдите библиотеки по названию.

Необходимые компоненты

Для этого руководства вам понадобятся следующие компоненты:

Если у вас нет четырёх датчиков DS18B20, вы можете использовать три или два. Также можно использовать другие датчики (нужно будет изменить код) или данные из любого другого источника (например, показания датчиков, полученные по MQTT, ESP-NOW, или случайные значения — для экспериментов с этим проектом…).

Схема подключения

Подключите четыре датчика DS18B20 к вашей плате.

ESP32 схема подключения нескольких датчиков DS18B20

Рекомендуемое чтение: ESP32 Pinout Reference: Which GPIO pins should you use?

Получение адресов датчиков DS18B20

Каждый датчик температуры DS18B20 имеет присвоенный серийный номер. Сначала вам нужно найти этот номер, чтобы правильно пометить каждый датчик. Это необходимо для того, чтобы позже вы знали, с какого датчика считываете температуру.

Загрузите следующий код в ESP32. Убедитесь, что выбраны правильная плата и COM-порт.

/*
 * Rui Santos
 * Complete Project Details https://randomnerdtutorials.com
 */

#include <OneWire.h>

// Based on the OneWire library example

OneWire ds(4);  //data wire connected to GPIO 4

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

void loop(void) {
  byte i;
  byte addr[8];

  if (!ds.search(addr)) {
    Serial.println(" No more addresses.");
    Serial.println();
    ds.reset_search();
    delay(250);
    return;
  }
  Serial.print(" ROM =");
  for (i = 0; i < 8; i++) {
    Serial.write(' ');
    Serial.print(addr[i], HEX);
  }
}

Исходный код

Подключайте по одному датчику за раз, чтобы найти его адрес (или последовательно добавляйте новый датчик), чтобы вы могли идентифицировать каждый по его адресу. Затем вы можете добавить физическую метку к каждому датчику.

Откройте Serial Monitor на скорости 115200 бод, нажмите кнопку RST/EN на плате, и вы должны получить что-то подобное (но с другими адресами):

Получение адресов DS18B20 в Serial Monitor

Снимите флажок «Autoscroll», чтобы иметь возможность скопировать адреса. В нашем случае мы получили следующие адреса:

  • Sensor 1: 28 FF A0 11 33 17 3 96

  • Sensor 2: 28 FF B4 6 33 17 3 4B

  • Sensor 3: 28 FF 11 28 33 18 1 6B

  • Sensor 4: 28 FF 43 F5 32 18 2 A8

Организация файлов

Для лучшей организации проекта и простоты понимания мы создадим четыре файла для веб-сервера:

  • Скетч Arduino, который обрабатывает веб-сервер;

  • index.html: для определения содержимого веб-страницы;

  • style.css: для стилизации веб-страницы;

  • script.js: для программирования поведения веб-страницы — обработка ответов веб-сервера, событий, создание графика и т.д.

Организация файлов Arduino скетч index html css javascript

Вы должны сохранить HTML, CSS и JavaScript файлы внутри папки с именем data внутри папки скетча Arduino, как показано на предыдущей диаграмме. Мы загрузим эти файлы в файловую систему ESP32 (LittleFS).

Вы можете скачать все файлы проекта:

HTML-файл

Скопируйте следующее в файл index.html.

<!-- Complete project details: https://randomnerdtutorials.com/esp32-plot-readings-charts-multiple/ -->

<!DOCTYPE html>
<html>
  <head>
    <title>ESP IOT DASHBOARD</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/png" href="favicon.png">
    <link rel="stylesheet" type="text/css" href="style.css">
    <script src="https://code.highcharts.com/highcharts.js"></script>
  </head>
  <body>
    <div class="topnav">
      <h1>ESP WEB SERVER CHARTS</h1>
    </div>
    <div class="content">
      <div class="card-grid">
        <div class="card">
          <p class="card-title">Temperature Chart</p>
          <div id="chart-temperature" class="chart-container"></div>
        </div>
      </div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

Исходный код

HTML-файл для этого проекта очень простой. Он подключает JavaScript-библиотеку Highcharts в заголовке HTML-файла:

<script src="https://code.highcharts.com/highcharts.js"></script>

Есть секция <div> с id chart-temperature, где мы позже отрендерим наш график.

<div id="chart-temperature" class="chart-container"></div>

CSS-файл

Скопируйте следующие стили в файл style.css.

/*  Complete project details: https://randomnerdtutorials.com/esp32-plot-readings-charts-multiple/  */

html {
  font-family: Arial, Helvetica, sans-serif;
  display: inline-block;
  text-align: center;
}
h1 {
  font-size: 1.8rem;
  color: white;
}
p {
  font-size: 1.4rem;
}
.topnav {
  overflow: hidden;
  background-color: #0A1128;
}
body {
  margin: 0;
}
.content {
  padding: 5%;
}
.card-grid {
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card {
  background-color: white;
  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
  font-size: 1.2rem;
  font-weight: bold;
  color: #034078
}
.chart-container {
  padding-right: 5%;
  padding-left: 5%;
}

Исходный код

JavaScript-файл (создание графиков)

Скопируйте следующее в файл script.js. Вот список того, что делает этот код:

  • инициализация протокола источника событий;

  • добавление слушателя событий для события new_readings;

  • создание графика;

  • получение последних показаний датчиков из события new_readings и их отображение на графике;

  • выполнение HTTP GET-запроса для получения текущих показаний датчиков при первом обращении к веб-странице.

// Complete project details: https://randomnerdtutorials.com/esp32-plot-readings-charts-multiple/

// Get current sensor readings when the page loads
window.addEventListener('load', getReadings);

// Create Temperature Chart
var chartT = new Highcharts.Chart({
  chart:{
    renderTo:'chart-temperature'
  },
  series: [
    {
      name: 'Temperature #1',
      type: 'line',
      color: '#101D42',
      marker: {
        symbol: 'circle',
        radius: 3,
        fillColor: '#101D42',
      }
    },
    {
      name: 'Temperature #2',
      type: 'line',
      color: '#00A6A6',
      marker: {
        symbol: 'square',
        radius: 3,
        fillColor: '#00A6A6',
      }
    },
    {
      name: 'Temperature #3',
      type: 'line',
      color: '#8B2635',
      marker: {
        symbol: 'triangle',
        radius: 3,
        fillColor: '#8B2635',
      }
    },
    {
      name: 'Temperature #4',
      type: 'line',
      color: '#71B48D',
      marker: {
        symbol: 'triangle-down',
        radius: 3,
        fillColor: '#71B48D',
      }
    },
  ],
  title: {
    text: undefined
  },
  xAxis: {
    type: 'datetime',
    dateTimeLabelFormats: { second: '%H:%M:%S' }
  },
  yAxis: {
    title: {
      text: 'Temperature Celsius Degrees'
    }
  },
  credits: {
    enabled: false
  }
});

//Plot temperature in the temperature chart
function plotTemperature(jsonValue) {

  var keys = Object.keys(jsonValue);
  console.log(keys);
  console.log(keys.length);

  for (var i = 0; i < keys.length; i++){
    var x = (new Date()).getTime();
    console.log(x);
    const key = keys[i];
    var y = Number(jsonValue[key]);
    console.log(y);

    if(chartT.series[i].data.length > 40) {
      chartT.series[i].addPoint([x, y], true, true, true);
    } else {
      chartT.series[i].addPoint([x, y], true, false, true);
    }

  }
}

// Function to get current readings on the webpage when it loads for the first time
function getReadings(){
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var myObj = JSON.parse(this.responseText);
      console.log(myObj);
      plotTemperature(myObj);
    }
  };
  xhr.open("GET", "/readings", true);
  xhr.send();
}

if (!!window.EventSource) {
  var source = new EventSource('/events');

  source.addEventListener('open', function(e) {
    console.log("Events Connected");
  }, false);

  source.addEventListener('error', function(e) {
    if (e.target.readyState != EventSource.OPEN) {
      console.log("Events Disconnected");
    }
  }, false);

  source.addEventListener('message', function(e) {
    console.log("message", e.data);
  }, false);

  source.addEventListener('new_readings', function(e) {
    console.log("new_readings", e.data);
    var myObj = JSON.parse(e.data);
    console.log(myObj);
    plotTemperature(myObj);
  }, false);
}

Исходный код

Получение показаний

При первом обращении к веб-странице мы запрашиваем у сервера текущие показания датчиков. В противном случае нам пришлось бы ждать поступления новых показаний (через Server-Sent Events), что может занять некоторое время в зависимости от интервала, установленного на сервере.

Добавьте слушатель событий, который вызывает функцию getReadings при загрузке веб-страницы.

// Get current sensor readings when the page loads
window.addEventListener('load', getReadings);

Объект window представляет открытое окно в браузере. Метод addEventListener() настраивает функцию, которая будет вызываться при наступлении определённого события. В данном случае мы вызовем функцию getReadings при загрузке страницы („load“) для получения текущих показаний датчиков.

Теперь давайте рассмотрим функцию getReadings. Создайте новый объект XMLHttpRequest. Затем отправьте GET-запрос на сервер по URL /readings, используя методы open() и send().

function getReadings() {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/readings", true);
  xhr.send();
}

Когда мы отправляем этот запрос, ESP отправит ответ с необходимой информацией. Поэтому нам нужно обработать то, что происходит при получении ответа. Мы используем свойство onreadystatechange, которое определяет функцию, выполняемую при изменении свойства readyState. Свойство readyState содержит статус XMLHttpRequest. Ответ на запрос готов, когда readyState равен 4, а status равен 200.

  • readyState = 4 означает, что запрос завершён и ответ готов;

  • status = 200 означает «OK»

Итак, запрос должен выглядеть примерно так:

function getStates(){
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
       DO WHATEVER YOU WANT WITH THE RESPONSE 
    }
  };
  xhr.open("GET", "/states", true);
  xhr.send();
}

Ответ, отправляемый ESP, — это следующий текст в формате JSON.

{
  "sensor1" : "25",
  "sensor2" : "21",
  "sensor3" : "22",
  "sensor4" : "23"
}

Нам нужно преобразовать строку JSON в объект JSON с помощью метода parse(). Результат сохраняется в переменной myObj.

var myObj = JSON.parse(this.responseText);

Переменная myObj — это объект JSON, содержащий все показания температуры. Мы хотим отобразить эти показания на одном графике. Для этого мы создали функцию plotTemperature(), которая отображает температуры, хранящиеся в объекте JSON, на графике.

plotTemperature(myObj);

Вот полная функция getReadings().

function getReadings(){
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var myObj = JSON.parse(this.responseText);
      console.log(myObj);
      plotTemperature(myObj);
    }
  };
  xhr.open("GET", "/readings", true);
  xhr.send();
}

Создание графика

Следующие строки создают графики с несколькими сериями.

// Create Temperature Chart
var chartT = new Highcharts.Chart({
  chart:{
    renderTo:'chart-temperature'
  },
  series: [
    {
      name: 'Temperature #1',
      type: 'line',
      color: '#101D42',
      marker: {
        symbol: 'circle',
        radius: 3,
        fillColor: '#101D42',
      }
    },
    {
      name: 'Temperature #2',
      type: 'line',
      color: '#00A6A6',
      marker: {
        symbol: 'square',
        radius: 3,
        fillColor: '#00A6A6',
      }
    },
    {
      name: 'Temperature #3',
      type: 'line',
      color: '#8B2635',
      marker: {
        symbol: 'triangle',
        radius: 3,
        fillColor: '#8B2635',
      }
    },
    {
      name: 'Temperature #4',
      type: 'line',
      color: '#71B48D',
      marker: {
        symbol: 'triangle-down',
        radius: 3,
        fillColor: '#71B48D',
      }
    },
  ],
  title: {
    text: undefined
  },
  xAxis: {
    type: 'datetime',
    dateTimeLabelFormats: { second: '%H:%M:%S' }
  },
  yAxis: {
    title: {
      text: 'Temperature Celsius Degrees'
    }
  },
  credits: {
    enabled: false
  }
});

Для создания нового графика используйте метод new Highcharts.Chart() и передайте в качестве аргумента свойства графика.

var chartT = new Highcharts.Chart({

В следующей строке определите, где вы хотите разместить график. В нашем примере мы хотим поместить его в HTML-элемент с id chart-temperature — см. раздел HTML-файл.

chart:{
  renderTo:'chart-temperature'
},

Затем определите параметры серий. Следующие строки создают первую серию:

series: [
  {
    name: 'Temperature #1',
    type: 'line',
    color: '#101D42',
    marker: {
      symbol: 'circle',
      radius: 3,
      fillColor: '#101D42',
  }

Свойство name определяет имя серии. Свойство type определяет тип графика — в данном случае мы хотим построить линейный график. Свойство color задаёт цвет линии — вы можете изменить его на любой желаемый цвет.

Далее определите свойства маркера. Вы можете выбрать из нескольких стандартных символов — square, circle, diamond, triangle, triangle-down. Вы также можете создать свои собственные символы. Свойство radius задаёт размер маркера, а fillColor — цвет маркера. Есть и другие свойства для настройки маркера — подробнее.

marker: {
  symbol: 'circle',
  radius: 3,
  fillColor: '#101D42',
}

Создание остальных серий аналогично, но мы выбрали разные имена, маркеры и цвета.

Есть множество других параметров для настройки серий — документация по plotOptions.

Вы также можете определить заголовок графика — в данном случае, поскольку мы уже определили заголовок в HTML-файле, мы не будем устанавливать его здесь. Заголовок отображается по умолчанию, поэтому мы должны установить его в undefined.

title: {
  text: undefined
},

Определите свойства оси X — это ось, на которой мы будем отображать дату и время. Другие параметры для настройки оси X.

xAxis: {
  type: 'datetime',
  dateTimeLabelFormats: { second: '%H:%M:%S' }
},

Мы устанавливаем заголовок для оси Y. Все доступные свойства оси Y.

yAxis: {
  title: {
    text: 'Temperature Celsius Degrees'
  }
}

Часовой пояс

Если по какой-то причине после сборки проекта графики показывают неправильный часовой пояс, добавьте следующие строки в JavaScript-файл после второй строки:

Highcharts.setOptions({
  time: {
    timezoneOffset: -60 //Add your time zone offset here in minutes
  }
});

Графики будут показывать время в UTC. Если вы хотите, чтобы отображалось ваше местное время, нужно установить параметр useUTC (параметр time) в false:

time:{
  useUTC: false
},

Добавьте это при создании графика следующим образом:

var chart = new Highcharts.Chart({
  time:{
    useUTC: false
  },
()

Подробнее об этом свойстве: https://api.highcharts.com/highcharts/time.useUTC

Наконец, установите параметр credits в false, чтобы скрыть информацию об авторских правах библиотеки Highcharts.

credits: {
  enabled: false
}

Построение температур

Мы создали функцию plotTemperature(), которая принимает в качестве аргумента объект JSON с показаниями температуры, которые мы хотим отобразить.

//Plot temperature in the temperature chart
function plotTemperature(jsonValue) {

  var keys = Object.keys(jsonValue);
  console.log(keys);
  console.log(keys.length);

  for (var i = 0; i < keys.length; i++){
    var x = (new Date()).getTime();
    console.log(x);
    const key = keys[i];
    var y = Number(jsonValue[key]);
    console.log(y);

    if(chartT.series[i].data.length > 40) {
      chartT.series[i].addPoint([x, y], true, true, true);
    } else {
      chartT.series[i].addPoint([x, y], true, false, true);
    }

  }
}

Сначала мы получаем ключи нашего объекта JSON и сохраняем их в переменной keys. Это позволяет нам перебрать все ключи в объекте.

var keys = Object.keys(jsonValue);

Переменная keys будет массивом со всеми ключами объекта JSON. В нашем случае:

["sensor1", "sensor2", "sensor3", "sensor4"]

Это работает, если у вас есть объект JSON с другим количеством ключей или с другими ключами. Затем мы перебираем все ключи (keys.length()), чтобы отобразить каждое значение на графике.

Значение x для графика — это метка времени.

var x = (new Date()).getTime()

Переменная key содержит текущий ключ в цикле. При первом проходе цикла переменная key равна «sensor1».

const key = keys[i];

Затем мы получаем значение ключа (jsonValue[key]) и сохраняем его как число в переменной y.

Наш график имеет несколько серий (индексация начинается с 0). Мы можем обратиться к первой серии графика температуры, используя: chartT.series[0], что соответствует chartT.series[i] при первом проходе цикла.

Сначала мы проверяем длину данных серии:

  • Если в серии более 40 точек: добавить и сдвинуть новую точку;

  • Или если в серии менее 40 точек: добавить новую точку.

Для добавления новой точки используйте метод addPoint(), который принимает следующие аргументы:

  • Значение для отображения. Если это одно число, точка с этим значением y добавляется к серии. Если это массив, он интерпретируется как значения x и y. В нашем случае мы передаём массив со значениями x и y;

  • Параметр redraw (boolean): установите true для перерисовки графика после добавления точки.

  • Параметр shift (boolean): Если true, точка удаляется с начала серии при добавлении новой в конец. Когда длина графика превышает 40, мы устанавливаем shift в true.

  • Параметр withEvent (boolean): Используется для запуска события addPoint серии — подробнее.

Итак, для добавления точки на график мы используем следующие строки:

if(chartT.series[i].data.length > 40) {
  chartT.series[i].addPoint([x, y], true, true, true);
} else {
  chartT.series[i].addPoint([x, y], true, false, true);
}

Обработка событий

Отображение показаний на графиках, когда клиент получает показания через событие new_readings.

Создайте новый объект EventSource и укажите URL страницы, отправляющей обновления. В нашем случае это /events.

if (!!window.EventSource) {
  var source = new EventSource('/events');

После создания экземпляра источника событий вы можете начать прослушивание сообщений от сервера с помощью addEventListener().

Это стандартные слушатели событий, как показано в документации AsyncWebServer.

source.addEventListener('open', function(e) {
  console.log("Events Connected");
}, false);

source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
}, false);

source.addEventListener('message', function(e) {
  console.log("message", e.data);
}, false);

Затем добавьте слушатель событий для new_readings.

source.addEventListener('new_readings', function(e) {

Когда доступны новые показания, ESP32 отправляет событие (new_readings) клиенту. Следующие строки обрабатывают то, что происходит, когда браузер получает это событие.

source.addEventListener('new_readings', function(e) {
  console.log("new_readings", e.data);
  var myObj = JSON.parse(e.data);
  console.log(myObj);
  plotTemperature(myObj);
}, false);

По сути, выводим новые показания в консоль браузера, преобразуем данные в объект JSON и отображаем показания на графике, вызывая функцию plotTemperature().

Скетч Arduino

Скопируйте следующий код в Arduino IDE или в файл main.cpp, если вы используете PlatformIO.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete instructions at https://RandomNerdTutorials.com/esp32-plot-readings-charts-multiple/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "LittleFS.h"
#include <Arduino_JSON.h>
#include <OneWire.h>
#include <DallasTemperature.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

// Create an Event Source on /events
AsyncEventSource events("/events");

// Json Variable to Hold Sensor Readings
JSONVar readings;

// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

// GPIO where the DS18B20 sensors are connected to
const int oneWireBus = 4;

// Setup a oneWire instance to communicate with OneWire devices (DS18B20)
OneWire oneWire(oneWireBus);

// Pass our oneWire reference to Dallas Temperature sensor
DallasTemperature sensors(&oneWire);

// Address of each sensor
DeviceAddress sensor3 = { 0x28, 0xFF, 0xA0, 0x11, 0x33, 0x17, 0x3, 0x96 };
DeviceAddress sensor1 = { 0x28, 0xFF, 0xB4, 0x6, 0x33, 0x17, 0x3, 0x4B };
DeviceAddress sensor2 = { 0x28, 0xFF, 0x43, 0xF5, 0x32, 0x18, 0x2, 0xA8 };
DeviceAddress sensor4 = { 0x28, 0xFF, 0x11, 0x28, 0x33, 0x18, 0x1, 0x6B };

// Get Sensor Readings and return JSON object
String getSensorReadings(){
  sensors.requestTemperatures();
  readings["sensor1"] = String(sensors.getTempC(sensor1));
  readings["sensor2"] = String(sensors.getTempC(sensor2));
  readings["sensor3"] = String(sensors.getTempC(sensor3));
  readings["sensor4"] = String(sensors.getTempC(sensor4));

  String jsonString = JSON.stringify(readings);
  return jsonString;
}

// Initialize LittleFS
void initLittleFS() {
  if (!LittleFS.begin()) {
    Serial.println("An error has occurred while mounting LittleFS");
  }
  else{
    Serial.println("LittleFS mounted successfully");
  }
}

// Initialize WiFi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

void setup() {
  // Serial port for debugging purposes
  Serial.begin(115200);
  initWiFi();
  initLittleFS();

  // Web Server Root URL
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", "text/html");
  });

  server.serveStatic("/", LittleFS, "/");

  // Request for the latest sensor readings
  server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
    String json = getSensorReadings();
    request->send(200, "application/json", json);
    json = String();
  });

  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);

  // Start server
  server.begin();
}

void loop() {
  if ((millis() - lastTime) > timerDelay) {
    // Send Events to the client with the Sensor Readings Every 10 seconds
    events.send("ping",NULL,millis());
    events.send(getSensorReadings().c_str(),"new_readings" ,millis());
    lastTime = millis();
  }
}

Исходный код

Как работает код

Давайте рассмотрим код и посмотрим, как он отправляет показания клиенту с помощью server-sent events.

Подключение библиотек

Библиотеки OneWire и DallasTemperature необходимы для работы с датчиками температуры DS18B20.

#include <OneWire.h>
#include <DallasTemperature.h>

Библиотеки WiFi, ESPAsyncWebServer и AsyncTCP используются для создания веб-сервера.

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

HTML, CSS и JavaScript файлы для построения веб-страницы сохранены в файловой системе ESP32 (LittleFS). Поэтому нам также нужно подключить библиотеку LittleFS.

#include "LittleFS.h"

Вам также нужно подключить библиотеку Arduino_JSON для удобной работы со строками JSON.

#include <Arduino_JSON.h>

Сетевые учётные данные

Вставьте свои сетевые учётные данные в следующие переменные, чтобы ESP32 мог подключиться к вашей локальной сети по Wi-Fi.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

AsyncWebServer и AsyncEventSource

Создайте объект AsyncWebServer на порту 80.

AsyncWebServer server(80);

Следующая строка создаёт новый источник событий на /events.

AsyncEventSource events("/events");

Объявление переменных

Переменная readings — это переменная JSON для хранения показаний датчиков в формате JSON.

JSONVar readings;

Переменные lastTime и timerDelay будут использоваться для обновления показаний датчиков каждые X секунд. В качестве примера мы будем получать новые показания каждые 30 секунд (30000 миллисекунд). Вы можете изменить это время задержки в переменной timerDelay.

// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

Датчики DS18B20

Датчики температуры DS18B20 подключены к GPIO 4.

// GPIO where the DS18B20 sensors are connected to
const int oneWireBus = 4;

Настройте экземпляр oneWire для связи с устройствами OneWire (DS18B20):

OneWire oneWire(oneWireBus);

Передайте ссылку oneWire датчику температуры Dallas:

DallasTemperature sensors(&oneWire);

Вставьте адреса ваших датчиков DS18B20 в следующие строки (см. раздел получения адресов, если у вас нет адресов ваших датчиков):

// Address of each sensor
DeviceAddress sensor3 = { 0x28, 0xFF, 0xA0, 0x11, 0x33, 0x17, 0x3, 0x96 };
DeviceAddress sensor1 = { 0x28, 0xFF, 0xB4, 0x6, 0x33, 0x17, 0x3, 0x4B };
DeviceAddress sensor2 = { 0x28, 0xFF, 0x43, 0xF5, 0x32, 0x18, 0x2, 0xA8 };
DeviceAddress sensor4 = { 0x28, 0xFF, 0x11, 0x28, 0x33, 0x18, 0x1, 0x6B };

Получение показаний DS18B20

Для получения показаний от датчиков температуры DS18B20 сначала нужно вызвать метод requestTemperatures() для объекта sensors. Затем используйте функцию getTempC() и передайте в качестве аргумента адрес датчика, температуру которого хотите получить — это возвращает температуру в градусах Цельсия.

Примечание: если вы хотите получить температуру в градусах Фаренгейта, используйте функцию getTempF().

Наконец, сохраните показания в строке JSON (переменная jsonString) и верните эту переменную.

// Get Sensor Readings and return JSON object
String getSensorReadings(){
  sensors.requestTemperatures();
  readings["sensor1"] = String(sensors.getTempC(sensor1));
  readings["sensor2"] = String(sensors.getTempC(sensor2));
  readings["sensor3"] = String(sensors.getTempC(sensor3));
  readings["sensor4"] = String(sensors.getTempC(sensor4));

  String jsonString = JSON.stringify(readings);
  return jsonString;
}

Инициализация LittleFS

Функция initLittleFS() инициализирует файловую систему LittleFS:

// Initialize LittleFS
void initLittleFS() {
  if (!LittleFS.begin()) {
    Serial.println("An error has occurred while mounting LittleFS");
  }
  else{
    Serial.println("LittleFS mounted successfully");
  }
}

Инициализация WiFi

Функция initWiFi() инициализирует Wi-Fi и выводит IP-адрес в Serial Monitor.

// Initialize WiFi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

setup()

В setup() инициализируйте Serial Monitor, Wi-Fi и файловую систему.

Serial.begin(115200);
initWiFi();
initLittleFS();

Обработка запросов

Когда вы обращаетесь к IP-адресу ESP32 по корневому URL /, отправляется текст, хранящийся в файле index.html, для построения веб-страницы.

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

Обслуживание других статических файлов, запрошенных клиентом (style.css и script.js).

server.serveStatic("/", LittleFS, "/");

Отправка строки JSON с текущими показаниями датчиков при получении запроса по URL /readings.

// Request for the latest sensor readings
server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
  String json = getSensorReadings();
  request->send(200, "application/json", json);
  json = String();
});

Переменная json содержит результат функции getSensorReadings(). Для отправки строки JSON в качестве ответа метод send() принимает первым аргументом код ответа (200), вторым — тип содержимого («application/json») и третьим — содержимое (переменная json).

Server Event Source

Настройка источника событий на сервере.

events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 second
  client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);

Наконец, запустите сервер.

server.begin();

loop()

В loop() отправляйте события браузеру с новыми показаниями датчиков для обновления веб-страницы каждые 30 секунд.

if ((millis() - lastTime) > timerDelay) {
  // Send Events to the client with the Sensor Readings Every 10 seconds
  events.send("ping",NULL,millis());
  events.send(getSensorReadings().c_str(),"new_readings" ,millis());
  lastTime = millis();
}

Используйте метод send() для объекта events и передайте в качестве аргумента содержимое, которое хотите отправить, и имя события. В данном случае мы хотим отправить строку JSON, возвращаемую функцией getSensorReadings(). Имя события — new_readings.

Загрузка кода и файлов

После ввода сетевых учётных данных сохраните код. Перейдите в Sketch > Show Sketch Folder и создайте папку с именем data.

Arduino IDE открыть папку скетча для создания папки data

Внутри этой папки вы должны сохранить HTML, CSS и JavaScript файлы.

Затем загрузите код в вашу плату ESP32. Убедитесь, что выбраны правильная плата и COM-порт. Также убедитесь, что вы добавили сетевые учётные данные и адреса датчиков в код.

После загрузки кода вам нужно загрузить файлы в файловую систему.

Нажмите [Ctrl] + [Shift] + [P] в Windows или [Cmd] + [Shift] + [P] в MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.

Если у вас нет этой опции, значит вы не установили плагин загрузки файловой системы. Ознакомьтесь с руководством.

Загрузка LittleFS в ESP32 из Arduino IDE

Важно: убедитесь, что Serial Monitor закрыт перед загрузкой в файловую систему. Иначе загрузка завершится ошибкой.

Когда всё успешно загружено, откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку EN/RST на ESP32, и в мониторе должен отобразиться IP-адрес ESP32.

Демонстрация

Откройте браузер и введите IP-адрес ESP32. Вы должны получить доступ к веб-странице, отображающей показания датчиков. Подождите некоторое время, пока накопятся данные.

ESP веб-сервер демонстрация графиков температуры

Вы можете выбрать точку, чтобы увидеть её значение и метку времени.

ESP веб-сервер демонстрация графиков температуры с несколькими сериями

Заключение

В этом руководстве вы узнали, как создавать графики с несколькими сериями для отображения температуры от нескольких датчиков DS18B20. Вы можете модифицировать этот проект для создания любого количества графиков и серий и отображения данных от любых других датчиков или источников.

Вам также может быть интересно: ESP32/ESP8266 Plot Sensor Readings in Real Time Charts - Web Server

Узнайте больше об ESP32 с нашими ресурсами:

Источник: :doc:`Random Nerd Tutorials - ESP32 Plot Sensor Readings in Charts (Multiple Series) <../esp32-plot-readings-charts-multiple/index>`. Авторы: Rui Santos & Sara Santos.