Dołącz do naszej grupy Telegram — tam osobiście odpowiadam na pytania, dzielę się poradami, schematami, kodem i trikami dotyczącymi montażu i konfiguracji.
Перейти в Telegram

NEWS

Interfejs webowy dla radia na ESP32S2 Mini.

W tym artykule przyjrzymy się, jak zapisać plik index.html w pamięci mikrokontrolera i wykorzystać go do zarządzania ustawieniami sieci WiFi. Dodatkowo stworzymy plik stations.json z listą stacji radiowych i zapiszemy jedną stację, która zostanie wykorzystana do testów.

LittleFS (Little File System) to lekki system plików przeznaczony do użytku z mikrokontrolerami i systemami wbudowanymi, takimi jak ESP32. Zapewnia efektywne przechowywanie i zarządzanie danymi na zewnętrznej pamięci flash, charakteryzuje się niskimi narzutami, zwiększoną odpornością na uszkodzenia i obsługą dynamicznego przydziału pamięci. LittleFS oferuje również lepszą wydajność przy pracy z dużą liczbą małych plików oraz zapewnia duplikowanie danych w celu zwiększenia niezawodności.

Poniżej znajduje się przykład kodu, który umożliwia zapisanie pliku index.html w pamięci ESP32. W przykładzie zapisujemy plik index.html bezpośrednio z kodu, wykorzystując bibliotekę LittleFS. Po zapisaniu plików w pamięci mikrokontroler będzie działał w trybie punktu dostępowego WiFi, co umożliwi połączenie się z interfejsem webowym i przetestowanie jego funkcjonalności.



#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <Preferences.h>
#include <ArduinoJson.h>
Preferences preferences; // Deklaracja globalna

const char* ssid = "esp32s2radio";
const char* password = "123456";

WebServer server(80);

void setup() {
  Serial.begin(115200);
  delay(1000);
  preferences.begin("wifi-settings", false);
  // Inicjalizacja LittleFS
  if (!LittleFS.begin(true)) {
    Serial.println("Błąd inicjalizacji LittleFS");
    return;
  }

  // Tworzenie pliku index.html, jeśli nie istnieje
  if (!LittleFS.exists("/index.html")) {
    File file = LittleFS.open("/index.html", FILE_WRITE);
    if (file) {
      file.print(R"rawliteral(
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>ESP32 internetowe radio</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>
</head>
<body>
  <h1>ESP32 Radio Mini</h1>

  <h2>Konfiguracja Wi-Fi</h2>
  <form id="wifiForm">
    <input type="text" id="wifiSSID" placeholder="Nazwa sieci (SSID)" required>
    <input type="password" id="wifiPassword" placeholder="Hasło sieci">
    <button type="button" onclick="saveWiFi()">Zapisz ustawienia Wi-Fi</button>
  </form>

  <h2>Zarządzanie stacjami radiowymi</h2>
  <form id="stationForm">
    <input type="text" id="stationURL" placeholder="URL strumienia" required>
    <button type="button" onclick="addStation()">Dodaj stację radiową</button>
  </form>
  <table>
    <thead>
      <tr>
        <th>URL</th>
        <th>Akcja</th>
      </tr>
    </thead>
    <tbody id="stationTable"></tbody>
  </table>

  <h2>Restart</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("Ustawienia Wi-Fi zapisane!");
        } else {
          alert("Błąd zapisywania ustawień 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})">Usuń</button>
              </td>
            `;
            table.appendChild(row);
          });
        })
        .catch(error => console.error("Błąd ładowania stacji radiowych:", 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("Stacja radiowa dodana!");
          loadStations();
          document.getElementById("stationURL").value = "";
        } else {
          alert("Błąd dodawania stacji radiowej.");
        }
      });
    }

    function deleteStation(index) {
      fetch(`/deleteStation?index=${index}`, { method: "DELETE" })
        .then(response => {
          if (response.ok) {
            alert("Stacja radiowa usunięta!");
            loadStations();
          } else {
            alert("Błąd usuwania stacji radiowej.");
          }
        })
        .catch(error => console.error("Błąd usuwania stacji radiowej:", error));
    }

    function restartDevice() {
      fetch("/restart", { method: "POST" })
        .then(response => {
          if (response.ok) {
            alert("ESP32 restartuje się...");
          } else {
            alert("Błąd restartu.");
          }
        })
        .catch(error => console.error("Błąd podczas żądania restartu:", error));
    }
  </script>
</body>
</html>
      )rawliteral");
      file.close();
      Serial.println("Plik index.html utworzony");
    }
  }

  // Tworzenie pliku stations.json, jeśli nie istnieje
  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("Plik stations.json utworzony z początkową stacją radiową");
    }
  }

  // Tworzenie punktu dostępowego Wi-Fi
  WiFi.softAP(ssid, password);
  Serial.println("Punkt dostępowy utworzony.");
  Serial.print("Adres IP: ");
  Serial.println(WiFi.softAPIP());

  // Obsługa żądań 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("Serwer HTTP uruchomiony.");
}

void handleRoot() {
  Serial.println("Obsługa żądania root...");
  File file = LittleFS.open("/index.html", "r");
  if (!file) {
    server.send(500, "text/plain", "Błąd otwierania pliku index");
    return;
  }
  server.streamFile(file, "text/plain"); // ZMIENIĆ NA text/html !!!!!!!!!!!!!!!!!!!!!!!!!!!
  file.close();
  Serial.println("Plik index wysłany do klienta.");
}

void handleSaveWiFi() {
  if (server.hasArg("plain") == false) {
    server.send(400, "text/plain", "Brak treści");
    return;
  }

  String body = server.arg("plain");
  Serial.println("Odebrana treść: " + body);
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, body);

  if (error) {
    server.send(400, "text/plain", "Nieprawidłowy JSON");
    return;
  }

  const char* ssid = doc["ssid"];
  const char* password = doc["password"];
  Serial.println("Odebrane ustawienia WiFi: SSID=" + String(ssid) + ", Hasło=" + String(password));

  preferences.putString("wifiSSID", ssid);
  preferences.putString("wifiPassword", password);

  server.send(200, "text/plain", "Ustawienia Wi-Fi zapisane");
}

void handleGetStations() {
  File file = LittleFS.open("/stations.json", "r");
  if (!file) {
    server.send(500, "text/plain", "Błąd otwierania pliku stacji");
    return;
  }

  StaticJsonDocument<1024> doc;
  DeserializationError error = deserializeJson(doc, file);
  file.close();

  if (error) {
    server.send(500, "text/plain", "Błąd parsowania pliku stacji");
    return;
  }

  String response;
  serializeJson(doc, response);
  server.send(200, "application/json", response);
}

void handleAddStation() {
  if (server.hasArg("plain") == false) {
    server.send(400, "text/plain", "Brak treści");
    return;
  }

  String body = server.arg("plain");
  Serial.println("Odebrana treść: " + body);

  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, body);

  if (error) {
    server.send(400, "text/plain", "Nieprawidłowy JSON");
    return;
  }

  if (!doc.containsKey("url")) {
    server.send(400, "text/plain", "Brak URL");
    return;
  }

  const char* url = doc["url"];
  Serial.println("Odebrany URL stacji: " + String(url));

  File file = LittleFS.open("/stations.json", "r");
  StaticJsonDocument<1024> stationsDoc;

  JsonArray stations;

  // Jeśli plik istnieje i jest prawidłowy
  if (file) {
    DeserializationError readError = deserializeJson(stationsDoc, file);
    file.close();
    if (readError) {
      Serial.println("Błąd parsowania pliku stacji, tworzenie nowego.");
      stations = stationsDoc.to<JsonArray>();
    } else {
      stations = stationsDoc.as<JsonArray>();
    }
  } else {
    Serial.println("Nie znaleziono pliku stacji, tworzenie nowego.");
    stations = stationsDoc.to<JsonArray>();
  }

  // Sprawdzenie liczby stacji <= 8
  if (stations.size() >= 8) {
    server.send(400, "text/plain", "Lista stacji jest pełna (maksymalnie 8 stacji)");
    return;
  }

  // Dodanie nowej stacji
  JsonObject newStation = stations.createNestedObject();
  newStation["url"] = url;

  // Zapisanie zaktualizowanej listy do pliku
  file = LittleFS.open("/stations.json", "w");
  if (!file) {
    server.send(500, "text/plain", "Błąd otwierania pliku stacji do zapisu");
    return;
  }

  serializeJson(stationsDoc, file);
  file.close();

  server.send(200, "text/plain", "Stacja dodana");
}

void handleDeleteStation() {
  if (!server.hasArg("index")) {
    server.send(400, "text/plain", "Nie podano indeksu");
    return;
  }

  int index = server.arg("index").toInt();

  File file = LittleFS.open("/stations.json", "r");
  if (!file) {
    server.send(500, "text/plain", "Błąd otwierania pliku stacji");
    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", "Nieprawidłowy indeks");
    return;
  }

  stations.remove(index);

  file = LittleFS.open("/stations.json", "w");
  if (!file) {
    server.send(500, "text/plain", "Błąd zapisywania pliku stacji");
    return;
  }

  serializeJson(stationsDoc, file);
  file.close();
  server.send(200, "text/plain", "Stacja usunięta");
}

void handleRestart() {
  Serial.println("Obsługa żądania restartu...");
  server.send(200, "text/plain", "Restartowanie...");
  delay(1000); // Czas na wysłanie odpowiedzi do klienta
  ESP.restart();
}

void loop() {
  server.handleClient(); // Obsługa przychodzących żądań HTTP
}


Przetestowano na ESP32s2 mini

Podsumowanie:

Plik index.html zapisany w pamięci mikrokontrolera możemy przeglądać w przeglądarce, łącząc się z ESP32 przez WiFi utworzoną przez mikrokontroler i wpisując w pasku przeglądarki adres IP urządzenia. W ten prosty sposób w przyszłości będziemy mogli edytować listę stacji radiowych przez interfejs webowy.

W przypadku konieczności sformatowania systemu plików mikrokontrolera z innych plików, poniżej przedstawiono kod do czyszczenia:

Czyszczenie:



#include <Arduino.h>
#include <LittleFS.h>

void setup() {
    // Inicjalizacja portu szeregowego do wyświetlania komunikatów
    Serial.begin(115200);
    delay(1000);

    // Inicjalizacja systemu plików
    if (!LittleFS.begin()) {
        Serial.println("Błąd montowania systemu plików");
        return;
    }

    // Formatowanie systemu plików
    Serial.println("Formatowanie systemu plików...");
    if (LittleFS.format()) {
        Serial.println("Formatowanie zakończone pomyślnie");
    } else {
        Serial.println("Błąd formatowania systemu plików");
    }

    // Demontowanie systemu plików
    LittleFS.end();
}

void loop() {
    // Puste, ponieważ formatowanie wykonuje się raz w setup()
}

P.S. Śledźcie zapowiedzi!