Ця стаття є частиною проєкту «Інтернет-радіо на ESP32-S2 Mini»
У цій статті ми розглянемо, як записати файл index.html у пам’ять мікроконтролера та використовувати його для налаштування мережі WiFi. Також створимо файл stations.json зі списком радіостанцій і запишемо одну радіостанцію для тестування.
LittleFS (Little File System) — це легка файлова система, розроблена для використання з мікроконтролерами та вбудованими системами, такими як ESP32. Вона забезпечує ефективне зберігання та управління даними на зовнішній флеш-пам’яті, має низькі накладні витрати, підвищену стійкість до пошкоджень і підтримку динамічного виділення пам’яті. LittleFS також характеризується високою продуктивністю при роботі з великою кількістю дрібних файлів і забезпечує дублювання даних для підвищення надійності.
Нижче наведено приклад коду, який дозволяє записати файл index.html у пам’ять ESP32. У прикладі файли записуються безпосередньо з коду за допомогою бібліотеки LittleFS. Після запису файлів у пам’ять мікроконтролер переходить у режим точки доступу WiFi, що дає змогу підключитися до веб-інтерфейсу і протестувати його роботу.
#include <wifi.h>
#include <webserver.h>
#include <littlefs.h>
#include <preferences.h>
#include <arduinojson.h>
Preferences preferences; // Глобальное объявление
const char* ssid = "esp32s2radio";
const char* password = "123456";
WebServer server(80);
void setup() {
Serial.begin(115200);
delay(1000);
preferences.begin("wifi-settings", false);
// Инициализация LittleFS
if (!LittleFS.begin(true)) {
Serial.println("Ошибка инициализации LittleFS");
return;
}
// Создание файла index.html, если он отсутствует
if (!LittleFS.exists("/index.html")) {
File file = LittleFS.open("/index.html", FILE_WRITE);
if (file) {
file.print(R"rawliteral(
<meta charset="UTF-8">
<title>ESP32 интернет радио</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
text-align: center;
background-color: #f4f4f9;
}
h1 {
background-color: #ffc107;
color: white;
margin: 0;
padding: 15px;
}
form, table {
margin: 20px auto;
width: 80%;
max-width: 600px;
}
input, button {
width: calc(100% - 20px);
margin: 10px auto;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
button {
background-color: #5898e7;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #ffc107;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
}
th {
background-color: #f2f2f2;
}
.delete-btn {
background-color: #f44336;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
.delete-btn:hover {
background-color: #d32f2f;
}
.btnrest {
height: 40px;
width: 200px;
}
</style>
<h1>ESP32 Радио Mini</h1>
<h2>Настройка Wi-Fi</h2>
<form id="wifiForm">
<input type="text" id="wifiSSID" placeholder="Имя сети (SSID)" required="">
<input type="password" id="wifiPassword" placeholder="Пароль сети">
<button type="button" onclick="saveWiFi()">Сохранить Wi-Fi настройки</button>
</form>
<h2>Управление радиостанциями</h2>
<form id="stationForm">
<input type="text" id="stationURL" placeholder="URL потока" required="">
<button type="button" onclick="addStation()">Добавить радиостанцию</button>
</form>
<table>
<thead>
<tr>
<th>URL</th>
<th>Действие</th>
</tr>
</thead>
<tbody id="stationTable"></tbody>
</table>
<h2>Перезагрузка</h2>
<button class="btnrest" type="button" onclick="restartDevice()">RESTART</button>
<script>
document.addEventListener("DOMContentLoaded", () => {
loadStations();
});
function saveWiFi() {
const ssid = document.getElementById("wifiSSID").value;
const password = document.getElementById("wifiPassword").value;
fetch("/saveWiFi", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ssid, password }),
}).then(response => {
if (response.ok) {
alert("Wi-Fi настройки сохранены!");
} else {
alert("Ошибка сохранения Wi-Fi настроек.");
}
});
}
function loadStations() {
fetch("/getStations")
.then(response => response.json())
.then(data => {
const table = document.getElementById("stationTable");
table.innerHTML = "";
data.forEach((station, index) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${station.url}</td>
<td>
<button class="delete-btn" onclick="deleteStation(${index})">Удалить</button>
</td>
`;
table.appendChild(row);
});
})
.catch(error => console.error("Ошибка загрузки радиостанций:", error));
}
function addStation() {
const url = document.getElementById("stationURL").value;
fetch("/addStation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
}).then(response => {
if (response.ok) {
alert("Радиостанция добавлена!");
loadStations();
document.getElementById("stationURL").value = "";
} else {
alert("Ошибка добавления радиостанции.");
}
});
}
function deleteStation(index) {
fetch(`/deleteStation?index=${index}`, { method: "DELETE" })
.then(response => {
if (response.ok) {
alert("Радиостанция удалена!");
loadStations();
} else {
alert("Ошибка удаления радиостанции.");
}
})
.catch(error => console.error("Ошибка удаления радиостанции:", error));
}
function restartDevice() {
fetch("/restart", { method: "POST" })
.then(response => {
if (response.ok) {
alert("ESP32 перезагружается...");
} else {
alert("Ошибка перезагрузки.");
}
})
.catch(error => console.error("Ошибка при запросе перезагрузки:", error));
}
</script>
)rawliteral");
file.close();
Serial.println("Файл index.html создан");
}
}
// Создание файла stations.json, если он отсутствует
if (!LittleFS.exists("/stations.json")) {
File file = LittleFS.open("/stations.json", FILE_WRITE);
if (file) {
file.print("[{\"url\":\"http://zt01.cdn.eurozet.pl/zet-net.mp3\"}]");
file.close();
Serial.println("Файл stations.json создан с начальной радиостанцией");
}
}
// Создание точки доступа Wi-Fi
WiFi.softAP(ssid, password);
Serial.println("Точка доступа создана.");
Serial.print("IP адрес: ");
Serial.println(WiFi.softAPIP());
// Обработка HTTP-запросов
server.on("/", handleRoot);
server.on("/saveWiFi", handleSaveWiFi);
server.on("/getStations", handleGetStations);
server.on("/addStation", handleAddStation);
server.on("/deleteStation", handleDeleteStation);
server.on("/restart", handleRestart);
server.begin();
Serial.println("HTTP сервер запущен.");
}
void handleRoot() {
Serial.println("Handling root request...");
File file = LittleFS.open("/index.html", "r");
if (!file) {
server.send(500, "text/plain", "Error opening index file");
return;
}
server.streamFile(file, "text/plain"); // ИЗМЕНИТЬ НА text/html !!!!!!!!!!!!!!!!!!!!!!!!!!!
file.close();
Serial.println("Index file sent to client.");
}
void handleSaveWiFi() {
if (server.hasArg("plain") == false) {
server.send(400, "text/plain", "Body not received");
return;
}
String body = server.arg("plain");
Serial.println("Received body: " + body);
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, body);
if (error) {
server.send(400, "text/plain", "Invalid JSON");
return;
}
const char* ssid = doc["ssid"];
const char* password = doc["password"];
Serial.println("Received WiFi settings: SSID=" + String(ssid) + ", Password=" + String(password));
preferences.putString("wifiSSID", ssid);
preferences.putString("wifiPassword", password);
server.send(200, "text/plain", "Wi-Fi settings saved");
}
void handleGetStations() {
File file = LittleFS.open("/stations.json", "r");
if (!file) {
server.send(500, "text/plain", "Error opening stations file");
return;
}
StaticJsonDocument<1024> doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
server.send(500, "text/plain", "Error parsing stations file");
return;
}
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void handleAddStation() {
if (server.hasArg("plain") == false) {
server.send(400, "text/plain", "Body not received");
return;
}
String body = server.arg("plain");
Serial.println("Received body: " + body);
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, body);
if (error) {
server.send(400, "text/plain", "Invalid JSON");
return;
}
if (!doc.containsKey("url")) {
server.send(400, "text/plain", "Missing URL");
return;
}
const char* url = doc["url"];
Serial.println("Received station URL: " + String(url));
File file = LittleFS.open("/stations.json", "r");
StaticJsonDocument<1024> stationsDoc;
JsonArray stations;
// Если файл существует и валиден
if (file) {
DeserializationError readError = deserializeJson(stationsDoc, file);
file.close();
if (readError) {
Serial.println("Error parsing stations file, creating new one.");
stations = stationsDoc.to<jsonarray>();
} else {
stations = stationsDoc.as<jsonarray>();
}
} else {
Serial.println("No stations file found, creating new one.");
stations = stationsDoc.to<jsonarray>();
}
// Проверка на количество станций <= 8
if (stations.size() >= 8) {
server.send(400, "text/plain", "Station list is full (maximum 8 stations)");
return;
}
// Добавление новой станции
JsonObject newStation = stations.createNestedObject();
newStation["url"] = url;
// Сохранение обновленного списка в файл
file = LittleFS.open("/stations.json", "w");
if (!file) {
server.send(500, "text/plain", "Error opening stations file for writing");
return;
}
serializeJson(stationsDoc, file);
file.close();
server.send(200, "text/plain", "Station added");
}
void handleDeleteStation() {
if (!server.hasArg("index")) {
server.send(400, "text/plain", "Index not provided");
return;
}
int index = server.arg("index").toInt();
File file = LittleFS.open("/stations.json", "r");
if (!file) {
server.send(500, "text/plain", "Error opening stations file");
return;
}
StaticJsonDocument<1024> stationsDoc;
deserializeJson(stationsDoc, file);
file.close();
JsonArray stations = stationsDoc.as<jsonarray>();
if (index < 0 || index >= stations.size()) {
server.send(400, "text/plain", "Invalid index");
return;
}
stations.remove(index);
file = LittleFS.open("/stations.json", "w");
if (!file) {
server.send(500, "text/plain", "Error writing stations file");
return;
}
serializeJson(stationsDoc, file);
file.close();
server.send(200, "text/plain", "Station deleted");
}
void handleRestart() {
Serial.println("Handling restart request...");
server.send(200, "text/plain", "Restarting...");
delay(1000); // Даем время отправить ответ клиенту
ESP.restart();
}
void loop() {
server.handleClient(); // Обработка входящих HTTP-запросов
}
Протестовано на ESP32s2 mini
Підсумок:
Записаний у пам'ять мікроконтролера файл index.html ми зможемо переглядати в браузері, підключившись до ESP32 через WiFi, створений мікроконтролером, і ввівши в адресний рядок браузера IP-адресу пристрою. Таким нехитрим способом ми зможемо в майбутньому редагувати список радіостанцій через веб-інтерфейс.
Якщо вам потрібно відформатувати файлову систему мікроконтролера від інших файлів, нижче наведено код для очищення:
Очищення:
#include <arduino.h>
#include <littlefs.h>
void setup() {
// Инициализация последовательного порта для вывода сообщений
Serial.begin(115200);
delay(1000);
// Инициализация файловой системы
if (!LittleFS.begin()) {
Serial.println("Ошибка монтирования файловой системы");
return;
}
// Форматирование файловой системы
Serial.println("Форматирование файловой системы...");
if (LittleFS.format()) {
Serial.println("Форматирование завершено успешно");
} else {
Serial.println("Ошибка форматирования файловой системы");
}
// Демонтирование файловой системы
LittleFS.end();
}
void loop() {
// Пусто, так как форматирование выполняется один раз в setup()
}
P.S. Следите за анонсами!