ESP32/ESP8266: Веб-приложение Firebase для логирования данных (датчики, графики и таблица)
В этом проекте вы создадите веб-приложение Firebase, которое отображает все показания датчиков, сохранённые в Firebase Realtime Database. Мы создадим веб-интерфейс с индикаторами (gauges), графиками и таблицей для отображения всех ваших записей данных. Мы также добавим кнопку, позволяющую удалить все данные из базы данных, и чекбоксы для настройки пользовательского интерфейса. Это веб-приложение будет защищено аутентификацией (с использованием email и пароля), а все данные ограничены для пользователя с помощью правил базы данных.
Обновлено 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.
2) Выберите иконку веб-приложения.
3) Дайте вашему приложению имя. Затем отметьте галочку рядом с √ Also set up Firebase Hosting for this App. Нажмите Register app.
4) Затем скопируйте объект 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. Должно открыться новое окно терминала в пути вашего проекта.
2) Firebase Login
4) В предыдущем окне терминала введите следующее:
firebase login
5) Вас спросят, хотите ли вы собирать информацию об использовании CLI и отчёты об ошибках. Введите «n» и нажмите Enter, чтобы отказать.
Примечание
Если вы уже вошли в систему, появится сообщение: «Already logged in as user@gmail.com».
6) После этого откроется новое окно в вашем браузере для входа в аккаунт Firebase.
7) Разрешите Firebase CLI доступ к вашему аккаунту Google.
8) После этого вход в Firebase CLI должен быть успешным. Вы можете закрыть окно браузера.
3) Инициализация проекта Firebase Web App
9) После успешного входа выполните следующую команду, чтобы запустить директорию проекта Firebase в текущей папке.
firebase init
10) Вас спросят, хотите ли вы инициализировать проект Firebase в текущей директории. Введите Y и нажмите Enter.
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.
12) Выберите опцию «Use an existing project» — она должна быть подсвечена синим — затем нажмите Enter.
13) После этого выберите проект Firebase для этой директории — это должен быть проект, созданный в предыдущем руководстве. В моём случае он называется ESP-project. Затем нажмите Enter.
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
15) Нажмите Enter на следующий вопрос, чтобы выбрать файл правил безопасности базы данных по умолчанию: «What file should be used for Realtime Database Security Rules?»
16) Проект Firebase теперь должен быть инициализирован успешно. Обратите внимание, что VS Code создал некоторые необходимые файлы в папке вашего проекта.
Файл index.html содержит некоторый HTML-текст для построения веб-страницы. Пока оставьте HTML-текст по умолчанию. Идея в том, чтобы заменить его вашим собственным HTML-текстом для создания пользовательской веб-страницы под ваши нужды. Мы сделаем это позже в этом руководстве.
17) Чтобы проверить, всё ли прошло как ожидалось, выполните следующую команду в окне терминала VS Code.
firebase deploy
Вы должны получить сообщение Deploy complete! и URL к консоли проекта и URL хостинга.
18) Скопируйте URL хостинга и вставьте его в окно веб-браузера. Вы должны увидеть следующую веб-страницу. Вы можете получить доступ к этой веб-странице откуда угодно в мире.
Веб-страница, которую вы видели ранее, построена с помощью 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> °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> %</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.
Затем скопируйте следующее в файл 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.
Следующее изображение показывает, как должна выглядеть структура папок вашего проекта веб-приложения.
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 предлагает бесплатный хостинг для размещения ваших ресурсов и веб-приложений. Затем вы можете получить доступ к вашему веб-приложению откуда угодно.
Вы можете использовать предоставленный Hosting URL для доступа к вашему веб-приложению из любой точки мира.
Демонстрация
Поздравляем! Вы успешно развернули ваше приложение. Теперь оно размещено на глобальном CDN с использованием Firebase Hosting. Вы можете получить доступ к вашему веб-приложению из любой точки мира по предоставленному Hosting URL. В моём случае это https://esp-firebase-demo.web.app.
Веб-приложение адаптивное, и вы можете получить к нему доступ с помощью смартфона, компьютера или планшета.
При первом доступе к веб-приложению вы увидите форму для ввода email-адреса и пароля.
Введите email и пароль авторизованного пользователя, которого вы добавили в методах аутентификации Firebase. Если форма не появляется сразу, обновите веб-страницу. После этого вы можете получить доступ к веб-странице с показаниями.
Показания отображаются в карточках, индикаторах, графиках и таблице. Вы также можете выбрать, какие интерфейсы вы хотите видеть, устанавливая/снимая чекбоксы.
Вы также можете проверить показания, отображаемые на графиках. Вы можете выбрать диапазон графиков, но имейте в виду, что выбор более 30 показаний займёт некоторое время.
Наконец, если вы хотите увидеть все показания, вы можете открыть таблицу показаний. В конце таблицы есть кнопка для загрузки дополнительных показаний, пока все показания не будут отображены.
Также есть кнопка для удаления всех данных, если вы хотите удалить все показания из базы данных.
Подведение итогов
В этом руководстве вы создали веб-приложение 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