ESP32/ESP8266: Веб-приложение Firebase для логирования данных (датчики, графики и таблица)

В этом проекте вы создадите веб-приложение Firebase, которое отображает все показания датчиков, сохранённые в Firebase Realtime Database. Мы создадим веб-интерфейс с индикаторами (gauges), графиками и таблицей для отображения всех ваших записей данных. Мы также добавим кнопку, позволяющую удалить все данные из базы данных, и чекбоксы для настройки пользовательского интерфейса. Это веб-приложение будет защищено аутентификацией (с использованием email и пароля), а все данные ограничены для пользователя с помощью правил базы данных.

ESP32 ESP8266 NodeMCU Firebase Data Logging Web App с индикаторами, графиками и таблицей

Обновлено 15 мая 2025 г.

Этот проект является Частью 2 следующего руководства (есть версия для ESP32 и версия для ESP8266):

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

Вот краткий обзор функций веб-приложения:

  • вход с помощью email и пароля

  • отображение времени последнего обновления

  • карточки для отображения последних показаний датчиков

  • индикаторы (gauges) для отображения последних показаний датчиков

  • графики, отображающие историю данных с метками времени

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

  • чекбоксы для включения/отключения различных вариантов отображения

  • таблица, отображающая все показания, сохранённые в базе данных

  • кнопка для удаления данных из базы данных

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

В этом руководстве (Часть 2) вы создадите веб-приложение для отображения показаний датчиков, залогированных с метками времени в Firebase Realtime Database (прочитайте предыдущее руководство — версия для ESP32 / версия для ESP8266).

Следующее видео показывает проект веб-приложения, который мы создадим — программирование ESP32/ESP8266 и настройка проекта Firebase были выполнены в Части 1 (ESP32 Часть 1; ESP8266 Часть 1).

  • Firebase размещает ваше веб-приложение через глобальный CDN с использованием Firebase Hosting и предоставляет SSL-сертификат. Вы можете получить доступ к вашему веб-приложению откуда угодно, используя доменное имя, сгенерированное Firebase.

  • При первом доступе к веб-приложению вам нужно аутентифицироваться с авторизованным email-адресом и паролем. Вы уже настроили этого пользователя и метод аутентификации в Части 1.

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

  • Есть кнопка для показа/скрытия всех показаний, сохранённых в базе данных, в таблице с метками времени.

  • Также есть кнопка Delete, которая позволяет удалить все данные из базы данных.

  • Все данные ограничены с помощью правил базы данных.

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

Прежде чем приступить к созданию веб-приложения Firebase, вам нужно проверить следующие предварительные условия:

Создание проекта Firebase

Вы должны были следовать одному из следующих руководств:

ESP32/ESP8266 должен выполнять код, предоставленный в этом руководстве. Realtime Database и аутентификация должны быть также настроены, как показано в руководстве.

Установка необходимого ПО

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


1) Добавление приложения в проект Firebase

1) Перейдите в консоль вашего проекта Firebase и добавьте приложение в проект, нажав кнопку +Add app.

Добавление приложения в проект Firebase

2) Выберите иконку веб-приложения.

3) Дайте вашему приложению имя. Затем отметьте галочку рядом с √ Also set up Firebase Hosting for this App. Нажмите Register app.

Firebase добавление веб-приложения в проект с Hosting

4) Затем скопируйте объект firebaseConfig и сохраните его, потому что он понадобится вам позже.

firebaseConfig объект конфигурации копирование сохранение

После этого вы также можете получить доступ к объекту firebaseConfig, если перейдёте в настройки проекта (Project settings) в консоли Firebase.

5) Нажмите Next на следующих шагах, и наконец Continue to console.


2) Настройка проекта Firebase Web App (VS Code)

Выполните следующие шаги для создания проекта Firebase Web App с использованием VS Code.

1) Создание папки проекта

1) Создайте папку на вашем компьютере, где вы хотите сохранить проект Firebase — например, Firebase-Project на Рабочем столе.

2) Откройте VS Code. Перейдите в File > Open Folder… и выберите только что созданную папку.

3) Перейдите в Terminal > New Terminal. Должно открыться новое окно терминала в пути вашего проекта.

Окно терминала Firebase папка проекта

2) Firebase Login

4) В предыдущем окне терминала введите следующее:

firebase login

5) Вас спросят, хотите ли вы собирать информацию об использовании CLI и отчёты об ошибках. Введите «n» и нажмите Enter, чтобы отказать.

Логин Firebase VS Code окно терминала

Примечание

Если вы уже вошли в систему, появится сообщение: «Already logged in as user@gmail.com».

6) После этого откроется новое окно в вашем браузере для входа в аккаунт Firebase.

Вход в аккаунт Firebase

7) Разрешите Firebase CLI доступ к вашему аккаунту Google.

Разрешение Firebase CLI доступа к аккаунту

8) После этого вход в Firebase CLI должен быть успешным. Вы можете закрыть окно браузера.

Firebase CLI вход успешен

3) Инициализация проекта Firebase Web App

9) После успешного входа выполните следующую команду, чтобы запустить директорию проекта Firebase в текущей папке.

firebase init

10) Вас спросят, хотите ли вы инициализировать проект Firebase в текущей директории. Введите Y и нажмите Enter.

Firebase инициализация проекта VS Code

11) Затем используйте стрелки вверх/вниз и клавишу Пробел для выбора опций. Выберите следующие опции:

  • Realtime Database: Configure security rules file for Realtime Database and (optionally) provision default instance.

  • Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys

Выбранные опции будут отображаться с зелёной звёздочкой. Затем нажмите Enter.

Опции Realtime Database и Hosting

12) Выберите опцию «Use an existing project» — она должна быть подсвечена синим — затем нажмите Enter.

Настройка проекта Firebase VS Code

13) После этого выберите проект Firebase для этой директории — это должен быть проект, созданный в предыдущем руководстве. В моём случае он называется ESP-project. Затем нажмите Enter.

Выбор проекта Firebase в VS Code

14) Затем выберите опции хостинга, как показано ниже:

  • What do you want to use as your public directory? Нажмите Enter, чтобы выбрать public.

  • Configure as a single-page app (rewrite urls to /index.html)? No

  • Set up automatic builds and deploys with GitHub? No

Инициализация Firebase завершена

15) Нажмите Enter на следующий вопрос, чтобы выбрать файл правил безопасности базы данных по умолчанию: «What file should be used for Realtime Database Security Rules?»

16) Проект Firebase теперь должен быть инициализирован успешно. Обратите внимание, что VS Code создал некоторые необходимые файлы в папке вашего проекта.

Файлы проекта Firebase созданы успешно

Файл index.html содержит некоторый HTML-текст для построения веб-страницы. Пока оставьте HTML-текст по умолчанию. Идея в том, чтобы заменить его вашим собственным HTML-текстом для создания пользовательской веб-страницы под ваши нужды. Мы сделаем это позже в этом руководстве.

17) Чтобы проверить, всё ли прошло как ожидалось, выполните следующую команду в окне терминала VS Code.

firebase deploy
Первый деплой Firebase App

Вы должны получить сообщение Deploy complete! и URL к консоли проекта и URL хостинга.

18) Скопируйте URL хостинга и вставьте его в окно веб-браузера. Вы должны увидеть следующую веб-страницу. Вы можете получить доступ к этой веб-странице откуда угодно в мире.

Настройка Firebase Hosting завершена

Веб-страница, которую вы видели ранее, построена с помощью HTML-файла, размещённого в папке public вашего проекта Firebase. Изменяя содержимое этого файла, вы можете создать собственное веб-приложение. Это то, что мы собираемся сделать в следующем разделе.


3) Создание веб-приложения Firebase

Теперь, когда вы успешно создали приложение проекта Firebase в VS Code, выполните следующие шаги для настройки приложения для отображения показаний датчиков на защищённой логином веб-странице.

index.html

Скопируйте следующее в ваш файл index.html (он находится внутри папки public).

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ESP Datalogging Firebase App</title>

    <!-- include highchartsjs to build the charts-->
    <script src="https://code.highcharts.com/highcharts.js"></script>
    <!-- include to use jquery-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <!--include icons from fontawesome-->
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <!-- include Gauges Javascript library-->
    <script src="https://cdn.rawgit.com/Mikhus/canvas-gauges/gh-pages/download/2.1.7/all/gauge.min.js"></script>
    <!--reference for favicon-->
    <link rel="icon" type="image/png" href="favicon.png">
    <!--reference a stylesheet-->
    <link rel="stylesheet" type="text/css" href="style.css">

  </head>

  <body>

    <!--TOP BAR-->
    <div class="topnav">
      <h1>Sensor Readings App <i class="fas fa-clipboard-list"></i></h1>
    </div>

    <!--AUTHENTICATION BAR (USER DETAILS/LOGOUT BUTTON)-->
    <div id="authentication-bar" style="display: none;">
      <p><span id="authentication-status">User logged in</span>
        <span id="user-details">USEREMAIL</span>
        <a href="/" id="logout-link">(logout)</a>
      </p>
    </div>

    <!--LOGIN FORM-->
    <form id="login-form" style="display: none;">
      <div class="form-elements-container">
        <label for="input-email"><b>Email</b></label>
        <input type="text" placeholder="Enter Username" id="input-email" required>

        <label for="input-password"><b>Password</b></label>
        <input type="password" placeholder="Enter Password" id="input-password" required>

        <button type="submit" id="login-button">Login</button>
        <p id="error-message" style="color:red;"></p>
      </div>
    </form>

    <!--CONTENT (SENSOR READINGS)-->
    <div class="content-sign-in" id="content-sign-in" style="display: none;">

      <!--LAST UPDATE-->
      <p><span class ="date-time">Last update: <span id="lastUpdate"></span></span></p>
      <p>
        Cards: <input type="checkbox" id="cards-checkbox" name="cards-checkbox" checked>
        Gauges: <input type="checkbox" id="gauges-checkbox" name="gauges-checkbox" checked>
        Charts: <input type="checkbox" id="charts-checkbox" name="charts-checkbox" unchecked>
      </p>
      <div id="cards-div">
        <div class="cards">
          <!--TEMPERATURE-->
          <div class="card">
            <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p>
            <p><span class="reading"><span id="temp"></span> &deg;C</span></p>
          </div>
          <!--HUMIDITY-->
          <div class="card">
            <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p>
            <p><span class="reading"><span id="hum"></span> &percnt;</span></p>
          </div>
          <!--PRESSURE-->
          <div class="card">
            <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p>
            <p><span class="reading"><span id="pres"></span> hPa</span></p>
          </div>
        </div>
      </div>
      <!--GAUGES-->
      <div id ="gauges-div">
        <div class="cards">
          <!--TEMPERATURE-->
          <div class="card">
            <canvas id="gauge-temperature"></canvas>
          </div>
          <!--HUMIDITY-->
          <div class="card">
            <canvas id="gauge-humidity"></canvas>
          </div>
        </div>
      </div>

      <!--CHARTS-->
      <div id="charts-div" style="display:none">
        <!--SET NUMBER OF READINGS INPUT FIELD-->
        <div>
          <p> Number of readings: <input type="number" id="charts-range"></p>
        </div>
        <!--TEMPERATURE-CHART-->
        <div class="cards">
          <div class="card">
            <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE CHART</p>
            <div id="chart-temperature" class="chart-container"></div>
          </div>
        </div>
        <!--HUMIDITY-CHART-->
        <div class="cards">
          <div class="card">
            <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY CHART</p>
            <div id="chart-humidity" class="chart-container"></div>
          </div>
        </div>
        <!--PRESSURE-CHART-->
        <div class="cards">
          <div class="card">
            <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE CHART</p>
            <div id="chart-pressure" class="chart-container"></div>
          </div>
        </div>
      </div>

    <!--BUTTONS TO HANDLE DATA-->
    <p>
      <!--View data button-->
      <button id="view-data-button">View all data</button>
      <!--Hide data button-->
      <button id="hide-data-button" style= "display:none;">Hide data</button>
      <!--Delete data button-->
      <button id="delete-button" class="deletebtn">Delete data</button>
    </p>
    <!--Modal to delete data-->
    <div id="delete-modal" class="modal" sytle="display:none">
      <span onclick = "document.getElementById('delete-modal').style.display='none'" class="close" title="Close Modal">×</span>
      <form id= "delete-data-form" class="modal-content" action="/">
        <div class="container">
          <h1>Delete Data</h1>
          <p>Are you sure you want to delete all data from database?</p>
          <div class="clearfix">
            <button type="button" onclick="document.getElementById('delete-modal').style.display='none'" class="cancelbtn">Cancel</button>
            <button type="submit" onclick="document.getElementById('delete-modal').style.display='none'" class="deletebtn">Delete</button>
          </div>
        </div>
      </form>
    </div>

    <!--TABLE WITH ALL DATA-->
    <div class ="cards">
      <div class="card" id="table-container" style= "display:none;">
        <table id="readings-table">
            <tr id="theader">
              <th>Timestamp</th>
              <th>Temp (ºC)</th>
              <th>Hum (%)</th>
              <th>Pres (hPa)</th>
            </tr>
            <tbody id="tbody">
            </tbody>
        </table>
        <p><button id="load-data" style= "display:none;">More results...</button></p>
      </div>
    </div>

  </div>

    <!--INCLUDE JS FILES-->
    <script type="module" src="scripts/auth.js"></script>
    <script type="module" src="scripts/charts-definition.js"></script>
    <script type="module" src="scripts/gauges-definition.js"></script>
    <script type="module" src="scripts/index.js"></script>

  </body>

</html>

Просмотреть исходный код

style.css

Внутри папки public создайте файл с именем style.css. Чтобы создать файл, выберите папку public, а затем нажмите на иконку +file в верхней части File Explorer. Назовите его style.css.

Создание CSS файла VS Code

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

html {
    font-family: Verdana, Geneva, Tahoma, sans-serif;
    display: inline-block;
    text-align: center;
}

body {
    margin: 0;
    width: 100%;
}

.topnav {
    overflow: hidden;
    background-color: #049faa;
    color: white;
    font-size: 1rem;
    padding: 5px;
}

#authentication-bar{
    background-color:mintcream;
    padding-top: 10px;
    padding-bottom: 10px;
}

#user-details{
    color: cadetblue;
}

.content {
    padding: 20px;
}

.card {
    background-color: white;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding: 5%;
}

.cards {
    max-width: 800px;
    margin: 0 auto;
    margin-bottom: 10px;
    display: grid;
    grid-gap: 2rem;
    grid-template-columns: repeat(auto-fit, minmax(200px, 2fr));
}

.reading {
    color: #193036;
}

.date-time{
    font-size: 0.8rem;
    color: #1282A2;
}

button {
    background-color: #049faa;
    color: white;
    padding: 14px 20px;
    margin: 8px 0;
    border: none;
    cursor: pointer;
    border-radius: 4px;
}
button:hover {
   opacity: 0.8;
}
.deletebtn{
    background-color: #c52c2c;
}

.form-elements-container{
    padding: 16px;
    width: 250px;
    margin: 0 auto;
}

input[type=text], input[type=password] {
    width: 100%;
    padding: 12px 20px;
    margin: 8px 0;
    display: inline-block;
    border: 1px solid #ccc;
    box-sizing: border-box;
}

table {
    width: 100%;
    text-align: center;
    font-size: 0.8rem;
}
tr, td {
    padding: 0.25rem;
}
tr:nth-child(even) {
    background-color: #f2f2f2
}
tr:hover {
    background-color: #ddd;
}
th {
    position: sticky;
    top: 0;
    background-color: #50b8b4;
    color: white;
}

/* The Modal (background) */
.modal {
    display: none; /* Hidden by default */
    position: fixed; /* Stay in place */
    z-index: 1; /* Sit on top */
    left: 0;
    top: 0;
    width: 100%; /* Full width */
    height: 100%; /* Full height */
    overflow: auto; /* Enable scroll if needed */
    background-color: #474e5d;
    padding-top: 50px;
}

/* Modal Content/Box */
.modal-content {
    background-color: #fefefe;
    margin: 5% auto 15% auto; /* 5% from the top, 15% from the bottom and centered */
    border: 1px solid #888;
    width: 80%; /* Could be more or less, depending on screen size */
}

/* Style the horizontal ruler */
hr {
    border: 1px solid #f1f1f1;
    margin-bottom: 25px;
}

/* The Modal Close Button (x) */
.close {
    position: absolute;
    right: 35px;
    top: 15px;
    font-size: 40px;
    font-weight: bold;
    color: #f1f1f1;
}

.close:hover,
.close:focus {
    color: #f44336;
    cursor: pointer;
}

/* Clear floats */
.clearfix::after {
    content: "";
    clear: both;
    display: table;
}

/* Change styles for cancel button and delete button on extra small screens */
@media screen and (max-width: 300px) {
    .cancelbtn, .deletebtn {
        width: 100%;
    }
}

Просмотреть исходный код

CSS-файл включает несколько простых стилей, чтобы наша веб-страница выглядела лучше. Мы не будем рассматривать работу CSS в этом руководстве.

JavaScript файлы

Мы создадим четыре JavaScript файла (auth.js, index.js, charts-definition.js и gauges-definition.js) внутри папки scripts внутри папки public.

  • Выберите папку public, затем нажмите на иконку +folder, чтобы создать новую папку. Назовите её scripts.

  • Затем выберите папку scripts и нажмите на иконку +file. Создайте файл с именем auth.js. Затем повторите предыдущие шаги для создания файлов index.js, charts-definition.js и gauges-definition.js.

Следующее изображение показывает, как должна выглядеть структура папок вашего проекта веб-приложения.

Структура файлов и папок проекта Firebase VS Code

auth.js

Скопируйте следующее в файл auth.js, который вы создали ранее.

import { auth } from "./index.js";
import { signInWithEmailAndPassword, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.0/firebase-auth.js";

document.addEventListener("DOMContentLoaded", () => {
  // Listen for auth status changes
  onAuthStateChanged(auth, (user) => {
    if (user) {
      console.log("User logged in:", user.email);
      setupUI(user);
    } else {
      console.log("User logged out");
      setupUI(null);
    }
  });

  // Login
  const loginForm = document.querySelector('#login-form');
  loginForm.addEventListener('submit', async (e) => {
    e.preventDefault();
    const email = loginForm['input-email'].value;
    const password = loginForm['input-password'].value;
    try {
      await signInWithEmailAndPassword(auth, email, password);
      loginForm.reset();
      console.log("Logged in:", email);
    } catch (error) {
      document.getElementById("error-message").innerHTML = error.message;
      console.error("Login error:", error.message);
    }
  });

  // Logout
  const logoutLink = document.querySelector('#logout-link');
  logoutLink.addEventListener('click', async (e) => {
    e.preventDefault();
    try {
      await signOut(auth);
      console.log("User signed out");
    } catch (error) {
      console.error("Logout error:", error.message);
    }
  });
});

Просмотреть исходный код

Затем сохраните файл. Этот файл отвечает за всё, что связано с входом и выходом пользователя.

index.js

Файл index.js управляет пользовательским интерфейсом — он показывает нужный контент в зависимости от статуса аутентификации пользователя. Когда пользователь вошёл в систему, этот файл получает новые показания из базы данных при каждом изменении и отображает их в нужных местах.

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

import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.0/firebase-app.js";
import { getAuth } from "https://www.gstatic.com/firebasejs/11.6.0/firebase-auth.js";
import { getDatabase, ref, onValue, set, remove, query, orderByKey, limitToLast, onChildAdded, endAt, get } from "https://www.gstatic.com/firebasejs/11.6.0/firebase-database.js";
import { createTemperatureChart, createHumidityChart, createPressureChart } from "./charts-definition.js";
import { createTemperatureGauge, createHumidityGauge } from "./gauges-definition.js";

// Firebase configuration
const firebaseConfig = {
  apiKey: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  authDomain: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  databaseURL: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  projectId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  storageBucket: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  messagingSenderId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  appId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const database = getDatabase(app);

// Export auth for use in auth.js
export { auth };

const epochToJsDate = (epochTime) => new Date(epochTime * 1000);

const epochToDateTime = (epochTime) => {
  if (!epochTime) return 'N/A';
  const date = epochToJsDate(epochTime);
  return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
};

// Validate and format sensor reading
const formatReading = (value) => (typeof value === 'number' ? value.toFixed(2) : 'N/A');

const plotValues = (chart, timestamp, value) => {
  if (!timestamp || typeof value !== 'number') return;
  const x = epochToJsDate(timestamp).getTime();
  const y = Number(value);
  const shift = chart.series[0].data.length > 40;
  chart.series[0].addPoint([x, y], true, shift, true);
};

// DOM elements
const loginElement = document.querySelector('#login-form');
const contentElement = document.querySelector('#content-sign-in');
const userDetailsElement = document.querySelector('#user-details');
const authBarElement = document.querySelector('#authentication-bar');
const deleteButtonElement = document.getElementById('delete-button');
const deleteModalElement = document.getElementById('delete-modal');
const deleteDataFormElement = document.querySelector('#delete-data-form');
const viewDataButtonElement = document.getElementById('view-data-button');
const hideDataButtonElement = document.getElementById('hide-data-button');
const tableContainerElement = document.querySelector('#table-container');
const chartsRangeInputElement = document.getElementById('charts-range');
const loadDataButtonElement = document.getElementById('load-data');
const cardsCheckboxElement = document.querySelector('input[name=cards-checkbox]');
const gaugesCheckboxElement = document.querySelector('input[name=gauges-checkbox]');
const chartsCheckboxElement = document.querySelector('input[name=charts-checkbox]');
const cardsReadingsElement = document.querySelector('#cards-div');
const gaugesReadingsElement = document.querySelector('#gauges-div');
const chartsDivElement = document.querySelector('#charts-div');
const tempElement = document.getElementById('temp');
const humElement = document.getElementById('hum');
const presElement = document.getElementById('pres');
const updateElement = document.getElementById('lastUpdate');

// Chart and Gauge variables
let chartT, chartH, chartP;
let gaugeT, gaugeH;

// Manage Login/Logout UI
const setupUI = (user) => {
  console.log('setupUI called with user:', user ? user.email : null);
  if (user) {
    // Toggle UI elements for logged-in state
    loginElement.style.display = 'none';
    contentElement.style.display = 'block';
    authBarElement.style.display = 'block';
    userDetailsElement.style.display = 'block';
    userDetailsElement.innerHTML = user.email;

    const uid = user.uid;
    const dbPath = `UsersData/${uid}/readings`;
    const chartPath = `UsersData/${uid}/charts/range`;

    // Database references
    const dbRef = ref(database, dbPath);
    const chartRef = ref(database, chartPath);

    // Initialize gauges
    gaugeT = createTemperatureGauge();
    gaugeH = createHumidityGauge();
    gaugeT.draw();
    gaugeH.draw();

    // Charts
    onValue(chartRef, (snapshot) => {
      const rawValue = snapshot.val();
      // Ensure chartRange is a positive integer; default to 100 if invalid
      const chartRange = Number.isInteger(Number(rawValue)) && Number(rawValue) > 0 ? Number(rawValue) : 100;

      // Destroy existing charts
      if (chartT) chartT.destroy();
      if (chartH) chartH.destroy();
      if (chartP) chartP.destroy();

      // Create new charts
      chartT = createTemperatureChart();
      chartH = createHumidityChart();
      chartP = createPressureChart();

      // Query and plot latest readings
      const readingsQuery = query(dbRef, orderByKey(), limitToLast(chartRange));
      onChildAdded(readingsQuery, (snapshot) => {
        const data = snapshot.val();
        console.log('Chart reading:', data);
        if (data && typeof data === 'object') {
          const { temperature, humidity, pressure, timestamp } = data;
          plotValues(chartT, timestamp, temperature);
          plotValues(chartH, timestamp, humidity);
          plotValues(chartP, timestamp, pressure);
        }
      });
    });

    // Update chart range
    chartsRangeInputElement.addEventListener('change', () => {
      const newValue = Number(chartsRangeInputElement.value);
      // Only update if the new value is a positive integer
      if (Number.isInteger(newValue) && newValue > 0) {
        set(chartRef, newValue);
      } else {
        console.warn('Invalid chart range input; must be a positive integer');
        chartsRangeInputElement.value = ''; // Clear invalid input
      }
    });

    // Checkboxes
    cardsCheckboxElement.addEventListener('change', () => {
      cardsReadingsElement.style.display = cardsCheckboxElement.checked ? 'block' : 'none';
    });

    gaugesCheckboxElement.addEventListener('change', () => {
      gaugesReadingsElement.style.display = gaugesCheckboxElement.checked ? 'block' : 'none';
    });

    chartsCheckboxElement.addEventListener('change', () => {
      chartsDivElement.style.display = chartsCheckboxElement.checked ? 'block' : 'none';
    });

    // Cards
    const lastReadingQuery = query(dbRef, orderByKey(), limitToLast(1));
    onChildAdded(lastReadingQuery, (snapshot) => {
      const data = snapshot.val();
      console.log('Card reading:', data);
      if (data && typeof data === 'object') {
        const { temperature, humidity, pressure, timestamp } = data;
        tempElement.innerHTML = formatReading(temperature);
        humElement.innerHTML = formatReading(humidity);
        presElement.innerHTML = formatReading(pressure);
        updateElement.innerHTML = epochToDateTime(timestamp);
      } else {
        tempElement.innerHTML = 'N/A';
        humElement.innerHTML = 'N/A';
        presElement.innerHTML = 'N/A';
        updateElement.innerHTML = 'N/A';
      }
    });

    // Gauges
    onChildAdded(lastReadingQuery, (snapshot) => {
      const data = snapshot.val();
      console.log('Gauge reading:', data);
      if (data && typeof data === 'object') {
        const { temperature, humidity, timestamp } = data;
        gaugeT.value = typeof temperature === 'number' ? temperature : 0;
        gaugeH.value = typeof humidity === 'number' ? humidity : 0;
        updateElement.innerHTML = epochToDateTime(timestamp);
      }
    });

    // Delete Data
    deleteButtonElement.addEventListener('click', (e) => {
      e.preventDefault();
      deleteModalElement.style.display = 'block';
    });

    deleteDataFormElement.addEventListener('submit', (e) => {
      e.preventDefault();
      remove(dbRef);
      deleteModalElement.style.display = 'none';
      // Reset UI after deletion
      tempElement.innerHTML = 'N/A';
      humElement.innerHTML = 'N/A';
      presElement.innerHTML = 'N/A';
      updateElement.innerHTML = 'N/A';
      gaugeT.value = 0;
      gaugeH.value = 0;
    });

    // Table
    let lastReadingTimestamp;
    const createTable = () => {
      const tableQuery = query(dbRef, orderByKey(), limitToLast(100));
      let firstRun = true;
      onChildAdded(tableQuery, (snapshot) => {
        const data = snapshot.val();
        console.log('Table reading:', data);
        if (data && typeof data === 'object') {
          const { temperature, humidity, pressure, timestamp } = data;
          const content = `
            <tr>
              <td>${epochToDateTime(timestamp)}</td>
              <td>${formatReading(temperature)}</td>
              <td>${formatReading(humidity)}</td>
              <td>${formatReading(pressure)}</td>
            </tr>`;
          $('#tbody').prepend(content);
          if (firstRun && timestamp) {
            lastReadingTimestamp = timestamp;
            firstRun = false;
          }
        }
      });
    };

    const appendToTable = async () => {
      const tableQuery = query(dbRef, orderByKey(), limitToLast(100), endAt(String(lastReadingTimestamp)));
      const snapshot = await get(tableQuery);
      const dataList = [];
      snapshot.forEach((child) => {
        const data = child.val();
        console.log('Append table reading:', data);
        if (data && typeof data === 'object') {
          dataList.push(data);
        }
      });
      if (dataList.length > 0) {
        lastReadingTimestamp = dataList[0].timestamp;
        const reversedList = dataList.reverse();
        reversedList.forEach((element, index) => {
          if (index === 0) return; // Skip first reading
          const { temperature, humidity, pressure, timestamp } = element;
          const content = `
            <tr>
              <td>${epochToDateTime(timestamp)}</td>
              <td>${formatReading(temperature)}</td>
              <td>${formatReading(humidity)}</td>
              <td>${formatReading(pressure)}</td>
            </tr>`;
          $('#tbody').append(content);
        });
      }
    };

    viewDataButtonElement.addEventListener('click', () => {
      tableContainerElement.style.display = 'block';
      viewDataButtonElement.style.display = 'none';
      hideDataButtonElement.style.display = 'inline-block';
      loadDataButtonElement.style.display = 'inline-block';
      createTable();
    });

    loadDataButtonElement.addEventListener('click', appendToTable);

    hideDataButtonElement.addEventListener('click', () => {
      tableContainerElement.style.display = 'none';
      viewDataButtonElement.style.display = 'inline-block';
      hideDataButtonElement.style.display = 'none';
      loadDataButtonElement.style.display = 'none';
    });

    // Initialize charts/range if it doesn't exist
    get(chartRef).then((snapshot) => {
      if (!snapshot.exists()) {
        set(chartRef, 100); // Set default chart range
        console.log('Initialized charts/range to 100');
      }
    });
  } else {
    // Toggle UI elements for logged-out state
    console.log('Showing login form');
    loginElement.style.display = 'block';
    authBarElement.style.display = 'none';
    userDetailsElement.style.display = 'none';
    contentElement.style.display = 'none';
    // Destroy gauges on logout
    if (gaugeT) gaugeT.destroy();
    if (gaugeH) gaugeH.destroy();
    gaugeT = null;
    gaugeH = null;
  }
};

// Expose setupUI to global scope for auth.js
window.setupUI = setupUI;

Просмотреть исходный код

Важно: вам нужно изменить код, указав ваш собственный объект firebaseConfig — тот, который вы получили на этом шаге.

const firebaseConfig = {
  apiKey: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  authDomain: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  databaseURL: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  projectId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  storageBucket: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  messagingSenderId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION",
  appId: "REPLACE_WITH_YOUR_Firebase_CONFIGURATION"
};

charts-definition.js

Скопируйте следующее в файл charts-definition.js. Этот файл создаёт различные графики с использованием библиотеки JavaScript highcharts.

export function createTemperatureChart() {
  return Highcharts.chart('chart-temperature', {
    chart: {
      type: 'line',
      zoomType: 'x'
    },
    title: {
      text: 'Temperature'
    },
    xAxis: {
      type: 'datetime',
      title: {
        text: 'Time'
      }
    },
    yAxis: {
      title: {
        text: 'Temperature (°C)'
      }
    },
    series: [{
      name: 'Temperature',
      data: []
    }]
  });
}

export function createHumidityChart() {
  return Highcharts.chart('chart-humidity', {
    chart: {
      type: 'line',
      zoomType: 'x'
    },
    title: {
      text: 'Humidity'
    },
    xAxis: {
      type: 'datetime',
      title: {
        text: 'Time'
      }
    },
    yAxis: {
      title: {
        text: 'Humidity (%)'
      }
    },
    series: [{
      name: 'Humidity',
      data: []
    }]
  });
}

export function createPressureChart() {
  return Highcharts.chart('chart-pressure', {
    chart: {
      type: 'line',
      zoomType: 'x'
    },
    title: {
      text: 'Pressure'
    },
    xAxis: {
      type: 'datetime',
      title: {
        text: 'Time'
      }
    },
    yAxis: {
      title: {
        text: 'Pressure (hPa)'
      }
    },
    series: [{
      name: 'Pressure',
      data: []
    }]
  });
}

Просмотреть исходный код

gauges-definition.js

В нашем веб-приложении мы отобразим индикатор для температуры и другой для влажности. Файл gauges-definition.js содержит функции для создания индикаторов.

export function createTemperatureGauge() {
    return new LinearGauge({
        renderTo: 'gauge-temperature',
        width: 120,
        height: 400,
        units: "Temperature C",
        minValue: 0,
        startAngle: 90,
        ticksAngle: 180,
        maxValue: 40,
        colorValueBoxRect: "#049faa",
        colorValueBoxRectEnd: "#049faa",
        colorValueBoxBackground: "#f1fbfc",
        valueDec: 2,
        valueInt: 2,
        majorTicks: [
            "0",
            "5",
            "10",
            "15",
            "20",
            "25",
            "30",
            "35",
            "40"
        ],
        minorTicks: 4,
        strokeTicks: true,
        highlights: [
            {
                "from": 30,
                "to": 40,
                "color": "rgba(200, 50, 50, .75)"
            }
        ],
        colorPlate: "#fff",
        colorBarProgress: "#CC2936",
        colorBarProgressEnd: "#049faa",
        borderShadowWidth: 0,
        borders: false,
        needleType: "arrow",
        needleWidth: 2,
        needleCircleSize: 7,
        needleCircleOuter: true,
        needleCircleInner: false,
        animationDuration: 1500,
        animationRule: "linear",
        barWidth: 10,
    });
}

export function createHumidityGauge() {
    return new RadialGauge({
        renderTo: 'gauge-humidity',
        width: 300,
        height: 300,
        units: "Humidity (%)",
        minValue: 0,
        maxValue: 100,
        colorValueBoxRect: "#049faa",
        colorValueBoxRectEnd: "#049faa",
        colorValueBoxBackground: "#f1fbfc",
        valueInt: 2,
        majorTicks: [
            "0",
            "20",
            "40",
            "60",
            "80",
            "100"
        ],
        minorTicks: 4,
        strokeTicks: true,
        highlights: [
            {
                "from": 80,
                "to": 100,
                "color": "#03C0C1"
            }
        ],
        colorPlate: "#fff",
        borderShadowWidth: 0,
        borders: false,
        needleType: "line",
        colorNeedle: "#007F80",
        colorNeedleEnd: "#007F80",
        needleWidth: 2,
        needleCircleSize: 3,
        colorNeedleCircleOuter: "#007F80",
        needleCircleOuter: true,
        needleCircleInner: false,
        animationDuration: 1500,
        animationRule: "linear"
    });
}

Просмотреть исходный код

Файл Favicon

Для отображения favicon в вашем веб-приложении вам нужно переместить изображение, которое вы хотите использовать как favicon, в папку public. Изображение должно называться favicon.png. Вы можете просто перетащить файл favicon с вашего компьютера в папку public в VS Code.

Мы используем следующую иконку в качестве favicon для нашего веб-приложения:

Деплой вашего приложения

После сохранения HTML, CSS и JavaScript файлов разверните ваше приложение в VS Code, выполнив следующую команду в окне терминала.

firebase deploy

Терминал должен отобразить что-то вроде следующего:

Деплой веб-приложения Firebase

Firebase предлагает бесплатный хостинг для размещения ваших ресурсов и веб-приложений. Затем вы можете получить доступ к вашему веб-приложению откуда угодно.

Вы можете использовать предоставленный Hosting URL для доступа к вашему веб-приложению из любой точки мира.

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

Поздравляем! Вы успешно развернули ваше приложение. Теперь оно размещено на глобальном CDN с использованием Firebase Hosting. Вы можете получить доступ к вашему веб-приложению из любой точки мира по предоставленному Hosting URL. В моём случае это https://esp-firebase-demo.web.app.

Веб-приложение адаптивное, и вы можете получить к нему доступ с помощью смартфона, компьютера или планшета.

При первом доступе к веб-приложению вы увидите форму для ввода email-адреса и пароля.

Страница входа в веб-приложение Firebase

Введите email и пароль авторизованного пользователя, которого вы добавили в методах аутентификации Firebase. Если форма не появляется сразу, обновите веб-страницу. После этого вы можете получить доступ к веб-странице с показаниями.

Веб-приложение Firebase показания датчиков карточки и индикаторы

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

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

Веб-приложение Firebase показания датчиков графики

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

Веб-приложение Firebase таблица со всеми данными

Также есть кнопка для удаления всех данных, если вы хотите удалить все показания из базы данных.

Веб-приложение Firebase удаление данных

Подведение итогов

В этом руководстве вы создали веб-приложение Firebase с аутентификацией входа/выхода, которое отображает показания датчиков множеством различных способов. Показания датчиков сохраняются в Realtime Database. База данных защищена с помощью правил базы данных (которые вы уже настроили в предыдущем руководстве).

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

Мы не объясняли, как работают JavaScript файлы, потому что проект довольно длинный. Однако, если эта тема вызовет достаточный интерес, мы можем разбить это приложение на более мелкие проекты, чтобы вы поняли, как обрабатывать данные с помощью запросов и как отображать их различными способами.

Если вы хотите узнать больше о Firebase, мы рекомендуем ознакомиться с нашей eBook по Firebase, посвящённой этой теме:

У нас есть другие ресурсы, связанные с ESP32 и ESP8266, которые могут вам понравиться:

Спасибо за чтение.


Источник: :doc:`ESP32/ESP8266: Firebase Data Logging Web App (Gauges, Charts, and Table) <../esp32-esp8266-firebase-gauges-charts/index>` от Random Nerd Tutorials