Приєднуйся до нашої Telegram-групи — там я особисто відповідаю на запитання, ділюсь порадами, схемами, кодом і лайфхаками зі збирання та налаштування.
Перейти в Telegram

NEWS

Веб-інтерфейс для радіо на ESP32S2 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. Следите за анонсами!