Ten artykuł jest częścią projektu „Radio internetowe na ESP32-S2 Mini”
Instrukcja konfiguracji i podłączenia
Aby zapewnić stabilną pracę Twojego internetowego radia opartego na ESP32s2, należy postępować zgodnie z kilkoma ważnymi krokami. Jednym z kluczowych aspektów jest zamontowanie radiatora na mikrokontrolerze, aby uniknąć przegrzania i niestabilnej pracy. Podczas testów wykazano, że bez radiatora urządzenie może działać niestabilnie.
Przygotowanie i zapis interfejsu webowego
Przed użyciem internetowego radia należy zapisać plik index.html w pamięci mikrokontrolera. Ten plik jest interfejsem webowym, za pomocą którego można skonfigurować nazwę sieci i hasło, a także edytować listę stacji radiowych. Postępując zgodnie z instrukcjami z poprzedniego artykułu, łatwo poradzisz sobie z tym zadaniem i od razu będziesz mógł korzystać z interfejsu webowego do konfiguracji urządzenia.
Podstawowe funkcje urządzenia
Po zainstalowaniu interfejsu webowego i wgraniu kodu Twoje urządzenie zyskuje następujące funkcje:
- Regulacja głośności i wyciszenie przyciskami: Wygodne sterowanie głośnością i możliwość wyciszenia dźwięku poprzez naciśnięcie przycisku.
- Przełączanie stacji radiowych: Długie naciśnięcie (1 sekunda) przycisku zwiększania lub zmniejszania głośności pozwala przełączać się między stacjami radiowymi.
- Wielofunkcyjny przycisk na pinie 12: Ten przycisk wyłącza diody LED lub, przy dłuższym naciśnięciu, wyłącza internetowe radio i przełącza mikrokontroler w tryb punktu dostępowego w celu edycji listy stacji radiowych lub konfiguracji WiFi.
- Stany włączenia: Białe świecenie / odczyt konfiguracji WiFi z pamięci: Pomarańczowe świecenie / połączenie z WiFi: Zielone świecenie / Odtwarzanie pierwszej stacji z listy: Efekt ognia.
- Włączenie konfiguracji: Niebieskie świecenie / Uruchomienie punktu dostępowego: Efekt tęczy.
Komponenty do podłączenia
Do złożenia i działania urządzenia będą potrzebne następujące komponenty:
- ESP32s2 mini: Mikrokontroler zarządzający całym systemem.
- Moduł wzmacniacza na MAX98357: Zapewnia wysoką jakość dźwięku.
- Pierścień LED na 12 adresowalnych diod WS2812: Do efektów wizualnych i wskazywania stanu.
- Głośnik 3 W (średnica 57 mm): Do odtwarzania dźwięku.
- Cztery przyciski taktowe 6x6x5 mm.
- Bateria 18350: Zapewnia zasilanie urządzenia i kompaktowe wymiary.
- Moduł ładowania baterii TP4056 Type-C z funkcją ochrony: Do bezpiecznego ładowania baterii.
- Moduł podwyższający napięcie DC-DC na 5 woltów: Zapewnia stabilne napięcie dla urządzenia.
- Przełącznik dwupozycyjny: Do włączania i wyłączania urządzenia.
- Złącza do baterii 18350.
- Wydrukować obudowę (pliki dostępne na stronie) lub zamówić już wydrukowaną na drukarce 3D obudowę.
Podłączenie komponentów
Podłączenie wszystkich komponentów nie powinno sprawiać trudności, ponieważ teoretycznie rozgryzienie przeznaczenia pinów na podstawie kodu nie będzie skomplikowane. Możesz również zmienić przypisanie pinów według własnych preferencji.
Po złożeniu wszystkich komponentów i wykonaniu wszystkich kroków konfiguracyjnych otrzymasz miniaturowe i funkcjonalne internetowe radio sterowane mikrokontrolerem ESP32s2mini. Ciesz się swoim nowym urządzeniem i dziel się wynikami!
#include <WiFi.h>
#include <LittleFS.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#include <WebServer.h>
#include "AudioFileSourceHTTPStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
#include <FastLED.h>
#include <vector>
const char* defaultStreamUrl = "http://zt01.cdn.eurozet.pl/zet-net.mp3";
std::vector<String> radioStations;
int currentStationIndex = 0;
// Obiekty do pracy z Wi-Fi i pamięcią
Preferences preferences;
WebServer server(80);
AudioGeneratorMP3 *mp3;
AudioFileSourceHTTPStream *file;
AudioFileSourceBuffer *buff;
AudioOutputI2S *out;
#define MUTE_PIN 35
#define VOL_UP 37
#define VOL_DOWN 33
#define BLU_PIN 15
#define LED_PIN 11
#define RES_PIN 12
#define NUM_LEDS 12
#define BRIGHTNESS 30
CRGB leds[NUM_LEDS];
unsigned long lastReconnectAttempt = 0;
unsigned long lastLedUpdate = 0;
unsigned long lastBlinkTime = 0;
bool ledOn = true;
bool wifiStatus = false;
bool isMuted = false;
float currentVolume = 0.09;
unsigned long lastButtonPressTime = 0;
bool enableLedEffect = true;
String ledMode = "Blue";
void reconnectStream() {
if (radioStations.empty()) {
Serial.println("No radio stations available.");
return;
}
String stationUrl = radioStations[currentStationIndex];
Serial.println("Starting playback of station: " + stationUrl);
if (mp3) {
mp3->stop();
delete mp3;
mp3 = nullptr;
}
if (buff) {
delete buff;
buff = nullptr;
}
if (file) {
delete file;
file = nullptr;
}
file = new AudioFileSourceHTTPStream(stationUrl.c_str());
size_t bufferSize = ESP.getFreeHeap() / 4;
if (bufferSize < 1024) bufferSize = 1024;
buff = new AudioFileSourceBuffer(file, bufferSize);
mp3 = new AudioGeneratorMP3();
mp3->begin(buff, out);
ledMode = "Fire";
}
void connectToWiFi(String ssid, String password) {
WiFi.begin(ssid.c_str(), password.c_str());
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 10000) {
delay(500);
}
wifiStatus = (WiFi.status() == WL_CONNECTED);
if (wifiStatus) {
Serial.println("WiFi connected!");
fill_solid(leds, NUM_LEDS, CRGB::Green);
FastLED.show();
delay (1000);
} else {
Serial.println("WiFi connection failed.");
fill_solid(leds, NUM_LEDS, CRGB::Red);
FastLED.show();
delay (1000);
startAccessPoint();
}
}
void loadWiFiCredentials() {
preferences.begin("WiFi", true);
String savedSSID = preferences.getString("wifiSSID", "");
String savedPassword = preferences.getString("wifiPassword", "");
preferences.end();
if (savedSSID.length() > 0 && savedPassword.length() > 0) {
fill_solid(leds, NUM_LEDS, CRGB::Orange);
FastLED.show();
delay(1000);
connectToWiFi(savedSSID, savedPassword);
} else {
fill_solid(leds, NUM_LEDS, CRGB::Blue);
FastLED.show();
delay (1000);
startAccessPoint();
}
}
void loadStations() {
if (LittleFS.exists("/stations.json")) {
File file = LittleFS.open("/stations.json", "r");
if (file) {
StaticJsonDocument<1024> doc;
DeserializationError error = deserializeJson(doc, file);
if (!error) {
JsonArray stationArray = doc.as<JsonArray>();
for (JsonVariant v : stationArray) {
if (v.containsKey("url")) {
String url = v["url"].as<String>();
Serial.println("Found station: " + url);
radioStations.push_back(url);
}
}
if (radioStations.empty()) {
Serial.println("Station list is empty, using default.");
radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL, jeśli lista jest pusta
}
Serial.println("Loaded radio stations.");
} else {
Serial.print("Failed to parse stations.json: ");
Serial.println(error.c_str());
radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL
}
file.close();
} else {
Serial.println("Failed to open stations.json, using default.");
radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL
}
} else {
Serial.println("stations.json not found, using default.");
radioStations.push_back(defaultStreamUrl); // Dodajemy domyślny URL
}
// Wyświetlamy wszystkie załadowane stacje do weryfikacji
Serial.println("Radio Stations:");
for (size_t i = 0; i < radioStations.size(); i++) {
Serial.printf("Station %d: %s\n", i + 1, radioStations[i].c_str());
}
}
// Funkcja do uruchamiania punktu dostępowego
void startAccessPoint() {
setSolidColor(CRGB::Blue);
delay (500);
WiFi.softAP("esp32radio", "123456");
Serial.println("Access Point mode enabled.");
server.on("/", handleRoot);
server.on("/saveWiFi", handleSaveWiFi);
server.on("/addStation", handleAddStation);
server.on("/getStations", handleGetStations);
server.on("/deleteStation", handleDeleteStation);
server.on("/restart", handleRestart);
server.begin();
Serial.println("HTTP server started.");
Serial.print("ESP32 IP Address: ");
Serial.println(WiFi.softAPIP());
delay (500);
}
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/html");
file.close();
Serial.println("Index file sent to client.");
}
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 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.begin("WiFi", false);
preferences.putString("wifiSSID", ssid);
preferences.putString("wifiPassword", password);
server.send(200, "text/plain", "Wi-Fi settings saved");
}
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;
// Jeśli plik istnieje i jest poprawny
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>();
}
// Sprawdzenie, czy liczba stacji <= 8
if (stations.size() >= 8) {
server.send(400, "text/plain", "Station list is full (maximum 8 stations)");
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", "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); // Dajemy czas na wysłanie odpowiedzi do klienta
ESP.restart();
}
void setup() {
Serial.begin(9600);
pinMode(BLU_PIN, OUTPUT);
pinMode(MUTE_PIN, INPUT_PULLUP);
pinMode(VOL_UP, INPUT_PULLUP);
pinMode(VOL_DOWN, INPUT_PULLUP);
pinMode(RES_PIN, INPUT_PULLUP);
digitalWrite(BLU_PIN, HIGH);
delay (1000);
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
fill_solid(leds, NUM_LEDS, CRGB::White);
FastLED.show();
delay (1000);
// Inicjalizacja LittleFS
if (!LittleFS.begin()) {
Serial.println("Failed to mount LittleFS!");
return;
}
loadWiFiCredentials();
if (wifiStatus) {
loadStations();
delay(500);
out = new AudioOutputI2S();
out->SetPinout(5, 3, 7);
out->SetGain(currentVolume);
delay(500);
reconnectStream();
}
digitalWrite(BLU_PIN, LOW);
}
void setSolidColor(const CRGB& color) {
fill_solid(leds, NUM_LEDS, color);
FastLED.show();
}
void updateLeds() {
// Aktualizacja co 100 ms
if (millis() - lastLedUpdate < 100) return;
lastLedUpdate = millis();
// Jeśli efekty są wyłączone, gasimy diody i wychodzimy
if (!enableLedEffect) {
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
return;
}
if (isMuted) {
setSolidColor(CRGB::Green); // Logika diod przy wyciszeniu dźwięku
return;
}
if (ledMode == "White") {
setSolidColor(CRGB::White);
} else if (ledMode == "Blue") {
setSolidColor(CRGB::Blue);
} else if (ledMode == "Green") {
setSolidColor(CRGB::Green);
} else if (ledMode == "Red") {
setSolidColor(CRGB::Red);
} else if (ledMode == "Fire") {
static uint8_t heat[NUM_LEDS];
for (int i = 0; i < NUM_LEDS; i++) {
heat[i] = qsub8(heat[i], random(0, 50));
}
for (int i = NUM_LEDS - 1; i >= 2; i--) {
heat[i] = (heat[i - 1] + heat[i - 2] + heat[i - 2]) / 3;
}
if (random(0, 10) > 7) {
int sparkIndex = random(0, 3);
heat[sparkIndex] = qadd8(heat[sparkIndex], random(160, 255));
}
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = HeatColor(heat[i]);
}
} else if (ledMode == "Rainbow") {
static uint8_t hue = 0;
fill_rainbow(leds, NUM_LEDS, hue, 7); // 7 - krok zmiany koloru
hue += 1; // Przesunięcie dla animacji
} else if (ledMode == "Strobe") {
static uint32_t lastBlinkTime = 0;
static bool ledOn = false;
if (millis() - lastBlinkTime >= 100) { // Interwał migania 100 ms
lastBlinkTime = millis();
ledOn = !ledOn;
setSolidColor(ledOn ? CRGB::White : CRGB::Black);
}
return; // Strobe już zaktualizował FastLED
}
// Aktualizujemy diody na końcu (jeśli nie wróciliśmy wcześniej)
FastLED.show();
}
void blinkRedLeds() {
while (true) {
// Aktualizacja co 100 ms
if (millis() - lastLedUpdate >= 100) {
lastLedUpdate = millis();
// Interwał migania 500 ms
if (millis() - lastBlinkTime >= 500) {
lastBlinkTime = millis();
ledOn = !ledOn;
fill_solid(leds, NUM_LEDS, ledOn ? CRGB::Red : CRGB::Black);
FastLED.show();
}
}
}
}
void handleStationChange(int direction) {
if (direction > 0) {
currentStationIndex = (currentStationIndex + 1) % radioStations.size();
} else {
currentStationIndex = (currentStationIndex - 1 + radioStations.size()) % radioStations.size();
}
Serial.println("Switched to station: " + radioStations[currentStationIndex]);
reconnectStream();
}
void handleButtons() {
static unsigned long volUpPressTime = 0;
static unsigned long volDownPressTime = 0;
static unsigned long resPressTime = 0;
int muteState = digitalRead(MUTE_PIN);
int nextState = digitalRead(VOL_UP);
int prevState = digitalRead(VOL_DOWN);
int resState = digitalRead(RES_PIN); // Odczyt stanu pinu RES_PIN
unsigned long currentTime = millis();
// Obsługa przycisku MUTE
if (muteState == LOW && currentTime - lastButtonPressTime >= 300) {
isMuted = !isMuted;
if (isMuted) {
enableLedEffect = true;
ledMode = "Green";
out->SetGain(0);
reconnectStream();
} else {
enableLedEffect = true;
ledMode = "RGB";
out->SetGain(currentVolume);
reconnectStream();
}
lastButtonPressTime = currentTime;
}
// Przytrzymanie przycisku VOL_UP
if (nextState == LOW) {
if (volUpPressTime == 0) volUpPressTime = currentTime; // Zapamiętujemy czas rozpoczęcia przytrzymania
if (currentTime - volUpPressTime >= 1000) { // Jeśli przytrzymano dłużej niż 1 sekundę
handleStationChange(1); // Przełączamy na następną stację radiową
volUpPressTime = 0; // Zerujemy czas przytrzymania
lastButtonPressTime = currentTime; // Zerujemy opóźnienie dla debounce
}
} else if (volUpPressTime > 0) { // Jeśli przycisk został zwolniony
if (currentTime - volUpPressTime < 1000) { // Jeśli przytrzymano krócej niż 1 sekundę
if (isMuted) {
isMuted = false;
ledMode = "RGB";
out->SetGain(currentVolume);
reconnectStream();
}
if (currentVolume < 1.0) {
currentVolume += 0.03;
if (currentVolume > 1.0) currentVolume = 1.0;
out->SetGain(currentVolume);
}
}
volUpPressTime = 0;
}
// Przytrzymanie przycisku VOL_DOWN
if (prevState == LOW) {
if (volDownPressTime == 0) volDownPressTime = currentTime; // Zapamiętujemy czas rozpoczęcia przytrzymania
if (currentTime - volDownPressTime >= 1000) { // Jeśli przytrzymano dłużej niż 1 sekundę
handleStationChange(-1); // Przełączamy na poprzednią stację radiową
volDownPressTime = 0; // Zerujemy czas przytrzymania
lastButtonPressTime = currentTime; // Zerujemy opóźnienie dla debounce
}
} else if (volDownPressTime > 0) { // Jeśli przycisk został zwolniony
if (currentTime - volDownPressTime < 1000) { // Jeśli przytrzymano krócej niż 1 sekundę
if (isMuted) {
isMuted = false;
ledMode = "RGB";
out->SetGain(currentVolume);
reconnectStream();
}
if (currentVolume > 0.0) {
currentVolume -= 0.03;
if (currentVolume < 0.0) currentVolume = 0.0;
out->SetGain(currentVolume);
}
}
volDownPressTime = 0; // Zerujemy czas przytrzymania
}
// Obsługa RES_PIN
if (resState == LOW) {
if (resPressTime == 0) resPressTime = currentTime; // Zapamiętujemy czas rozpoczęcia przytrzymania
if (currentTime - resPressTime >= 1000) { // Jeśli przytrzymano dłużej niż 1 sekundę
stopAudioStream(); // Zatrzymanie odtwarzania strumienia audio
WiFi.disconnect(); // Rozłączenie z Wi-Fi
WiFi.mode(WIFI_OFF);
wifiStatus = false;
startAccessPoint(); // Uruchomienie punktu dostępowego
Serial.println("Access Point mode activated.");
resPressTime = 0; // Zerujemy czas przytrzymania
}
} else if (resPressTime > 0) { // Jeśli przycisk został zwolniony
if (currentTime - resPressTime < 1000) { // Krótkie naciśnięcie
if (!enableLedEffect) {
enableLedEffect = true;
Serial.println("LED effects enabled.");
} else {
enableLedEffect = false;
Serial.println("LED effects disabled.");
}
}
resPressTime = 0; // Zerujemy czas przytrzymania
}
}
void stopAudioStream() {
// Zatrzymujemy generator MP3
if (mp3) {
if (mp3->isRunning()) {
mp3->stop(); // Zatrzymujemy odtwarzanie
}
delete mp3; // Usuwamy obiekt
mp3 = nullptr; // Zerujemy wskaźnik
}
// Czyszczenie bufora
if (buff) {
delete buff; // Usuwamy obiekt bufora
buff = nullptr; // Zerujemy wskaźnik
}
// Czyszczenie źródła danych
if (file) {
delete file; // Usuwamy obiekt źródła danych
file = nullptr; // Zerujemy wskaźnik
}
// Zerowanie głośności na wyjściu
if (out) {
out->SetGain(0); // Ustawiamy głośność na 0
out->stop(); // Zatrzymujemy strumień wyjściowy
}
// Logowanie zakończenia
Serial.println("Audio stream stopped successfully.");
}
void loop() {
if (wifiStatus) {
if (mp3 && mp3->isRunning() && !isMuted) {
if (!mp3->loop()) {
setSolidColor(CRGB::Red);
delay(250);
reconnectStream();
}
} else {
if (ledMode != "Fire") {
ledMode = "Fire";
}
}
handleButtons();
} else {
if (ledMode != "Blue") {
ledMode = "Blue";
}
server.handleClient(); // Obsługa przychodzących żądań HTTP
delay(10);
}
updateLeds();
}
- W kodzie do sterowania diodami LED zaprogramowano kilka efektów, na przykład FIRE, który można łatwo zastąpić innym trybem, zmieniając 2 linie w pętli loop. Efekty i tryby pracy LED mają zapas na przyszłe warianty wizualizacji stanu kontrolera.
- Mam nadzieję, że kod jest wystarczająco logicznie zorganizowany, a funkcje mają zrozumiałe nazwy, co powinno ułatwić czytanie i zrozumienie.
- Używane są standardowe biblioteki i komponenty, więc kod powinien być uniwersalny i zrozumiały dla innych programistów.
- Jeśli masz dodatkowe pytania lub potrzebujesz pomocy, daj znać! Powodzenia z Twoim projektem!