Веб-сервер ESP8266 с использованием SPIFFS (SPI Flash File System) – NodeMCU
В этом руководстве показано, как создать веб-сервер, который обслуживает HTML и CSS файлы, хранящиеся в файловой системе ESP8266 NodeMCU (SPIFFS), с использованием Arduino IDE. Вместо того чтобы записывать HTML и CSS текст прямо в скетч Arduino, мы создадим отдельные HTML и CSS файлы.
Веб-сервер, который мы создадим, показывает, как управлять выходами ESP8266 и как отображать показания датчиков. В качестве примера мы будем управлять светодиодом и отображать показания датчика BME280.
Вы можете использовать концепции, изученные в этом руководстве, для управления любым выходом или отображения показаний других датчиков.
Рекомендуемое чтение: Веб-сервер ESP32 с использованием SPIFFS
Обзор проекта
Прежде чем перейти непосредственно к проекту, важно описать, что будет делать наш веб-сервер, чтобы потом было легче понять.
Веб-сервер управляет светодиодом, подключённым к GPIO 2 ESP8266. Это встроенный светодиод ESP8266. Вы можете управлять любым другим GPIO;
На странице веб-сервера отображаются две кнопки: ON и OFF – для включения и выключения GPIO 2;
На странице веб-сервера также отображается текущее состояние GPIO;
Вы также будете использовать датчик BME280 для отображения показаний датчиков (температура, влажность и давление).
На следующем рисунке показана упрощённая схема, демонстрирующая, как всё работает.
Предварительные требования
Перед тем как приступить к этому проекту, убедитесь, что вы выполнили все следующие предварительные требования.
1. Установка платы ESP8266 в Arduino IDE
Мы будем программировать ESP8266 с помощью Arduino IDE, поэтому у вас должно быть установлено дополнение ESP8266. Следуйте следующему руководству для его установки:
2. Плагин загрузчика файловой системы
Для загрузки файлов в файловую систему SPI Flash (SPIFFS) ESP8266 мы будем использовать плагин Filesystem Uploader. Установите плагин в вашу Arduino IDE:
3. Установка библиотек
Один из самых простых способов создать веб-сервер с использованием файлов из файловой системы – это использование библиотеки ESPAsyncWebServer.
Установка библиотеки ESPAsyncWebServer
Эта библиотека недоступна для загрузки через менеджер библиотек Arduino IDE. Поэтому вам нужно выполнить следующие шаги для установки библиотеки:
Нажмите здесь, чтобы скачать библиотеку ESPAsyncWebServer. В папке Загрузки у вас должен появиться .zip файл
Разархивируйте .zip файл, и вы должны получить папку ESPAsyncWebServer-master
Переименуйте папку из ESPAsyncWebServer-master в ESPAsyncWebServer
Переместите папку ESPAsyncWebServer в папку библиотек вашей Arduino IDE
Альтернативно, вы можете перейти в Sketch > Include Library > .zip Library и выбрать ранее загруженную библиотеку.
Установка ESPAsyncTCP
Библиотека ESPAsyncWebServer также нуждается в библиотеке ESPAsyncTCP для правильной работы. Выполните следующие шаги для установки библиотеки ESPAsyncTCP:
Нажмите здесь, чтобы скачать библиотеку ESPAsyncTCP. В папке Загрузки у вас должен появиться .zip файл
Разархивируйте .zip файл, и вы должны получить папку ESPAsyncTCP-master
Переименуйте папку из ESPAsyncTCP-master в ESPAsyncTCP
Переместите папку ESPAsyncTCP в папку библиотек вашей Arduino IDE
Наконец, перезапустите вашу Arduino IDE
Альтернативно, вы можете перейти в Sketch > Include Library > .zip Library и выбрать ранее загруженную библиотеку.
Установка библиотек BME280
В этом руководстве мы будем отображать показания датчика BME280 (Руководство по ESP8266). Вам нужно установить следующие библиотеки:
Вы можете установить эти библиотеки через менеджер библиотек Arduino IDE. Перейдите в Sketch > Include Libraries > Manage Libraries. Затем найдите название библиотеки для её установки.
Необходимые компоненты
Для выполнения этого проекта вам потребуются следующие компоненты:
ESP8266 (читайте Лучшие платы разработки ESP8266)
Вы можете воспользоваться приведёнными ссылками или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!
Схема подключения
Подключите все компоненты, следуя схеме ниже.
BME280 |
ESP8266 |
|---|---|
Vin |
3.3V |
GND |
GND |
SCL |
GPIO 5 |
SDA |
GPIO 4 |
Организация файлов
Для создания веб-сервера вам потребуются три разных файла. Скетч Arduino, HTML файл и CSS файл. HTML и CSS файлы должны быть сохранены внутри папки с именем data внутри папки скетча Arduino, как показано ниже:
Создание HTML файла
Создайте файл index.html со следующим содержимым или скачайте все файлы проекта здесь:
<!DOCTYPE html>
<!--
Rui Santos
Complete project details at https://RandomNerdTutorials.com
-->
<html>
<head>
<title>ESP8266 Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>ESP8266 Web Server</h1>
<p>GPIO state<strong> %STATE%</strong></p>
<p>
<a href="/on"><button class="button">ON</button></a>
<a href="/off"><button class="button button2">OFF</button></a>
</p>
<p>
<span class="sensor-labels">Temperature</span>
<span id="temperature">%TEMPERATURE%</span>
<sup class="units">°C</sup>
</p>
<p>
<span class="sensor-labels">Humidity</span>
<span id="humidity">%HUMIDITY%</span>
<sup class="units">%</sup>
</p>
<p>
<span class="sensor-labels">Pressure</span>
<span id="pressure">%PRESSURE%</span>
<sup class="units">hPa</sup>
</p>
</body>
<script>
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("temperature").textContent = this.responseText;
}
};
xhttp.open("GET", "/temperature", true);
xhttp.send();
}, 10000 ) ;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("humidity").textContent = this.responseText;
}
};
xhttp.open("GET", "/humidity", true);
xhttp.send();
}, 10000 ) ;
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("pressure").textContent = this.responseText;
}
};
xhttp.open("GET", "/pressure", true);
xhttp.send();
}, 10000 ) ;
</script>
</html>
Поскольку мы используем CSS и HTML в разных файлах, нам нужно указать ссылку на CSS файл в HTML тексте.
<link rel="stylesheet" type="text/css" href="style.css">
Тег <link> сообщает HTML файлу, что вы используете внешнюю таблицу стилей для форматирования внешнего вида страницы. Атрибут rel указывает характер внешнего файла, в данном случае это stylesheet – CSS файл, который будет использоваться для изменения внешнего вида страницы.
Атрибут type установлен в «text/css», чтобы указать, что вы используете CSS файл для стилей. Атрибут href указывает расположение файла; поскольку оба файла CSS и HTML будут находиться в одной папке, вам нужно просто указать имя файла: style.css.
В следующей строке мы пишем первый заголовок нашей веб-страницы. В данном случае это «ESP8266 Web Server». Вы можете изменить заголовок на любой текст:
<h1>ESP8266 Web Server</h1>
Затем добавляем абзац с текстом «GPIO state: «, за которым следует состояние GPIO. Поскольку состояние GPIO изменяется в соответствии с состоянием вывода, мы можем добавить заполнитель (placeholder), который затем будет заменён на любое значение, которое мы зададим в скетче Arduino.
Чтобы добавить заполнитель, используйте знаки %. Для создания заполнителя состояния вы можете использовать %STATE%, например.
<p>GPIO state<strong> %STATE%</strong></p>
Вы присваиваете значение заполнителю STATE в скетче Arduino.
Затем создаём кнопки ON и OFF. Когда вы нажимаете кнопку ON, мы перенаправляем веб-страницу на корневой URL, за которым следует /on. Когда вы нажимаете кнопку OFF, вы перенаправляетесь на URL /off.
<a href="/on"><button class="button">ON</button></a>
<a href="/off"><button class="button button2">OFF</button></a>
Наконец, создаём три абзаца для отображения температуры, влажности и давления.
<p>
<span class="sensor-labels">Temperature</span>
<span id="temperature">%TEMPERATURE%</span>
<sup class="units">°C</sup>
</p>
<p>
<span class="sensor-labels">Pressure</span>
<span id="pressure">%PRESSURE%</span>
<sup class="units">hPa</sup>
</p>
<p>
<span class="sensor-labels">Humidity</span>
<span id="humidity">%HUMIDITY%</span>
<sup class="units">%</sup>
</p>
Мы используем заполнители %TEMPERATURE%, %HUMIDITY% и %PRESSURE%. Они затем будут заменены фактическими показаниями температуры в скетче Arduino.
Автоматическое обновление
Мы также добавляем немного JavaScript в наш HTML файл, который отвечает за обновление показаний температуры без необходимости обновлять веб-страницу.
Следующий фрагмент кода отвечает за температуру.
setInterval(function ( ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("temperature").textContent = this.responseText;
}
};
xhttp.open("GET", "/temperature", true);
xhttp.send();
}, 10000 ) ;
Для обновления температуры у нас есть функция setInterval(), которая выполняется каждые 10 секунд.
По сути, она делает запрос на URL /temperature для получения последнего показания температуры.
xhttp.open("GET", "/temperature", true);
xhttp.send();
}, 10000 ) ;
Когда значение получено, оно обновляет HTML элемент с идентификатором temperature.
if (this.readyState == 4 && this.status == 200) {
document.getElementById("temperature").textContent = this.responseText;
}
Подводя итог, предыдущий раздел отвечает за асинхронное обновление температуры. Тот же процесс повторяется для показаний влажности и давления.
Создание CSS файла
Создайте файл style.css со следующим содержимым или скачайте все файлы проекта здесь:
/***
Rui Santos
Complete project details at https://RandomNerdTutorials.com
***/
html {
font-family: Arial;
display: inline-block;
margin: 0px auto;
text-align: center;
}
h1 {
color: #0F3376;
padding: 2vh;
}
p {
font-size: 1.5rem;
}
.button {
display: inline-block;
background-color: #008CBA;
border: none;
border-radius: 4px;
color: white;
padding: 16px 40px;
text-decoration: none;
font-size: 30px;
margin: 2px;
cursor: pointer;
}
.button2 {
background-color: #f44336;
}
.units {
font-size: 1.2rem;
}
.sensor-labels {
font-size: 1.5rem;
vertical-align:middle;
padding-bottom: 15px;
}
Это просто базовый CSS файл для настройки размера шрифта, стиля и цвета кнопок, а также для выравнивания страницы. Мы не будем объяснять, как работает CSS. Хорошее место для изучения CSS – это сайт W3Schools.
Скетч асинхронного веб-сервера ESP8266
Скопируйте следующий код в Arduino IDE или скачайте все файлы проекта здесь. Затем вам нужно ввести учётные данные вашей сети (SSID и пароль) для подключения ESP8266 к вашей локальной сети.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com
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.
*/
// Import required libraries
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme; // I2C
//Adafruit_BME280 bme(BME_CS); // hardware SPI
//Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK); // software SPI
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Set LED GPIO
const int ledPin = 2;
// Stores LED state
String ledState;
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
String getTemperature() {
float temperature = bme.readTemperature();
// Read temperature as Fahrenheit (isFahrenheit = true)
//float temperature = 1.8 * bme.readTemperature() + 32;
Serial.println(temperature);
return String(temperature);
}
String getHumidity() {
float humidity = bme.readHumidity();
Serial.println(humidity);
return String(humidity);
}
String getPressure() {
float pressure = bme.readPressure()/ 100.0F;
Serial.println(pressure);
return String(pressure);
}
// Replaces placeholder with LED state value
String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
if(digitalRead(ledPin)){
ledState = "ON";
}
else{
ledState = "OFF";
}
Serial.print(ledState);
return ledState;
}
else if (var == "TEMPERATURE"){
return getTemperature();
}
else if (var == "HUMIDITY"){
return getHumidity();
}
else if (var == "PRESSURE"){
return getPressure();
}
return String();
}
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
// Initialize the sensor
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
// Initialize SPIFFS
if(!SPIFFS.begin()){
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
// Print ESP32 Local IP Address
Serial.println(WiFi.localIP());
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", String(), false, processor);
});
// Route to load style.css file
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/style.css", "text/css");
});
// Route to set GPIO to HIGH
server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){
digitalWrite(ledPin, HIGH);
request->send(SPIFFS, "/index.html", String(), false, processor);
});
// Route to set GPIO to LOW
server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){
digitalWrite(ledPin, LOW);
request->send(SPIFFS, "/index.html", String(), false, processor);
});
server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", getTemperature().c_str());
});
server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", getHumidity().c_str());
});
server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", getPressure().c_str());
});
// Start server
server.begin();
}
void loop(){
}
Как работает код
Продолжайте чтение, чтобы узнать, как работает код, или перейдите к следующему разделу.
Сначала подключаем необходимые библиотеки:
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Вам нужно ввести учётные данные вашей сети в следующие переменные:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Создаём экземпляр, ссылающийся на датчик BME280, под именем bme:
Adafruit_BME280 bme; // I2C
Далее создаём переменную, ссылающуюся на GPIO 2, с именем ledPin, и строковую переменную для хранения состояния светодиода: ledState.
const int ledPin = 2;
String ledState;
Создаём объект AsyncWebServer с именем server, который слушает порт 80.
AsyncWebServer server(80);
Получение показаний датчиков
Мы создаём три функции для возврата показаний датчиков в виде строк: функции getTemperature(), getHumidity() и getPressure().
Вот как выглядит функция getTemperature() (остальные функции аналогичны).
String getTemperature() {
float temperature = bme.readTemperature();
// Read temperature as Fahrenheit (isFahrenheit = true)
//float temperature = 1.8 * bme.readTemperature() + 32;
Serial.println(temperature);
return String(temperature);
}
Если вы хотите отображать температуру в градусах Фаренгейта, вам просто нужно раскомментировать соответствующую строку в функции getTemperature():
float temperature = 1.8 * bme.readTemperature() + 32;
Чтобы узнать больше о работе с датчиком BME280 и ESP8266, вы можете прочитать следующее руководство:
processor()
Функция processor() присваивает значение заполнителям, которые мы создали в HTML файле. Она принимает в качестве аргумента заполнитель и должна возвращать строку (String), которая заменит этот заполнитель. Функция processor() должна иметь следующую структуру:
String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
if(digitalRead(ledPin)){
ledState = "ON";
}
else{
ledState = "OFF";
}
Serial.print(ledState);
return ledState;
}
else if (var == "TEMPERATURE"){
return getTemperature();
}
else if (var == "HUMIDITY"){
return getHumidity();
}
else if (var == "PRESSURE"){
return getPressure();
}
}
Эта функция сначала проверяет, является ли заполнитель тем STATE, который мы создали в HTML файле.
if(var == "STATE"){
Если да, то в зависимости от состояния светодиода мы устанавливаем переменную ledState в значение ON или OFF.
if(digitalRead(ledPin)){
ledState = "ON";
}
else{
ledState = "OFF";
}
Наконец, мы возвращаем переменную ledState. Это заменяет заполнитель STATE строковым значением ledState.
return ledState;
Если функция находит заполнитель %TEMPERATURE%, мы возвращаем температуру, вызывая созданную ранее функцию getTemperature().
else if (var == "TEMPERATURE"){
return getTemperature();
}
То же самое происходит для заполнителей %HUMIDITY% и %PRESSURE% при вызове соответствующих функций:
else if (var == "TEMPERATURE"){
return getTemperature();
}
else if (var == "HUMIDITY"){
return getHumidity();
}
else if (var == "PRESSURE"){
return getPressure();
}
setup()
В функции setup() начинаем с инициализации Serial Monitor и установки GPIO в качестве выхода.
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
Инициализируем датчик BME280:
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
Инициализируем SPIFFS:
if(!SPIFFS.begin()){
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
Подключение к Wi-Fi
Подключаемся к Wi-Fi и выводим адрес ESP8266:
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
Serial.println(WiFi.localIP());
Асинхронный веб-сервер
Библиотека ESPAsyncWebServer позволяет нам настраивать маршруты, на которых сервер будет ожидать входящие HTTP-запросы, и выполнять функции при получении запроса на этом маршруте. Для этого используйте метод on объекта server следующим образом:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", String(), false, processor);
});
Когда сервер получает запрос по корневому URL «/», он отправляет файл index.html клиенту. Последний аргумент функции send() – это processor, чтобы мы могли заменить заполнитель нужным значением – в данном случае ledState.
Поскольку мы указали ссылку на CSS файл в HTML файле, клиент сделает запрос на CSS файл. Когда это произойдёт, CSS файл будет отправлен клиенту:
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/style.css","text/css");
});
Вам также нужно определить, что происходит при обращении к маршрутам /on и /off. Когда запрос поступает на эти маршруты, светодиод либо включается, либо выключается, и ESP32 отправляет HTML файл.
server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){
digitalWrite(ledPin, HIGH);
request->send(SPIFFS, "/index.html", String(),false, processor);
});
server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){
digitalWrite(ledPin, LOW);
request->send(SPIFFS, "/index.html", String(),false, processor);
});
В HTML файле мы написали JavaScript код, который запрашивает температуру, влажность и давление по маршрутам /temperature, /humidity, /pressure соответственно, каждые 10 секунд. Поэтому нам также нужно определить, что происходит при получении запроса на эти маршруты.
Нам просто нужно отправить обновлённые показания датчиков. Обновлённые показания возвращаются функциями getTemperature(), getHumidity() и getPressure(), которые мы создали ранее.
Показания являются простым текстом и должны быть отправлены как char, поэтому мы используем метод c_str().
server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/plain", getTemperature().c_str());
});
server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/plain", getHumidity().c_str());
});
server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/plain", getPressure().c_str());
});
В конце мы используем метод begin() объекта server, чтобы сервер начал прослушивать входящих клиентов.
server.begin();
Поскольку это асинхронный веб-сервер, вы можете определить все запросы в функции setup(). Затем вы можете добавить другой код в loop(), пока сервер прослушивает входящих клиентов.
Загрузка кода и файлов
Сохраните скетч Arduino как ESP8266_SPIFFS_Web_Server или скачайте все файлы проекта здесь.
Перейдите в Sketch > Show Sketch folder и создайте папку с именем data. Сохраните HTML и CSS файлы внутри этой папки;
В Tools > Board выберите используемую плату ESP8266;
Затем перейдите в Tools > Flash size и выберите 4M (1M SPIFFS).
Наконец, загрузите файлы на вашу плату. Перейдите в Tools > ESP8266 Data Sketch Upload и дождитесь загрузки файлов.
Затем нажмите кнопку загрузки в Arduino IDE, чтобы загрузить код на ESP8266.
Когда всё успешно загружено, откройте Serial Monitor на скорости 115200 бод. Нажмите встроенную кнопку RST на ESP8266, и он должен вывести IP-адрес ESP8266.
Демонстрация
Откройте браузер и введите IP-адрес вашего ESP8266. Должна загрузиться следующая веб-страница.
Нажмите кнопки ON и OFF для управления встроенным светодиодом ESP8266. Вы также можете видеть последние показания датчиков. Показания датчиков обновляются автоматически без необходимости обновлять веб-страницу.
Заключение
Использование файловой системы SPI Flash (SPIFFS) ESP8266 особенно полезно для хранения HTML и CSS файлов для обслуживания клиенту – вместо того чтобы записывать весь код прямо в скетч Arduino.
У нас есть другие связанные проекты, которые могут вам понравиться:
Узнайте больше об ESP8266:
Спасибо за чтение.