commit a10c58311d945072ebf9e013cff260a579f21a68 Author: scayac Date: Wed Sep 24 21:06:51 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9f3806 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.pio +.vscode diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6e4177 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Documentation du projet ESP32 MP3 GPIO Web + +Ce projet permet de piloter la lecture de fichiers MP3 stockés sur une carte SD via un ESP32, avec une interface web de configuration et des entrées GPIO déclenchant la lecture. + +## Fonctionnalités principales + +- Lecture de fichiers MP3 depuis une carte SD +- Association de chaque GPIO surveillé à un fichier MP3 +- Interface web pour configurer les associations, uploader de nouveaux fichiers MP3, et piloter la lecture (start/stop) +- Sauvegarde de la configuration dans l'EEPROM + +## Branchements + +##### Module SD + +- CS : GPIO5 +- SCK : GPIO18 +- MOSI : GPIO23 +- MISO : GPIO19 +- VCC : 5V + +##### Module audio PCM5102 + +- BCK : GPIO27 +- LRCK : GPIO26 +- DIN : GPIO25 + +Les soudures suivantes sont à réaliser pour assurer le bon fonctionnement du module. + +![紫基盤のPCM5102基盤を試してみた | 原音再生](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSWczqv2KlCRjKlbMeyDKmtGJXosmtNgfuOvQ&s) audio - How to make PCM5102 DAC work on Raspberry Pi ZeroW? - Raspberry Pi  Stack Exchange + +Alimentation du module en 3,3V possible. +Possibilité d'utiliser le DAC intégré de l'ESP32 à la place de ce module. + +##### GPIO pour les déclenchements + +Par défaut : 12, 13, 14, 15, 16, 17, 21, 22 + +## Utilisation + +1. Vérifier que le module utilise une carte SD formatée au préalable en FAT32. +2. Démarrez l'ESP32. Il crée un point d'accès WiFi (SSID et mot de passe définis dans le code). Par défaut SSID : ESP32_MP3 et mot de passe : 12345678. +3. Connectez-vous au WiFi et accédez à l'adresse 192.168.4.1 dans un navigateur. +4. Configurez les associations GPIO/MP3, uploadez de nouveaux fichiers MP3 (l'upload peut prendre du temps), et pilotez la lecture via l'interface web. +5. Les entrées GPIO déclenchent la lecture du MP3 associé lorsqu'elles sont connectées à la masse. + +## Dépendances + +- ESP32 Arduino Core +- Bibliothèques : SD, SPI, WiFi, EEPROM, WebServer, Audio (selon le matériel) + +## Schéma matériel + +- ESP32 (modèle Wrover obligatoire ou équivalent avec PSRAM) +- Module PCM5102 (I2S) +- Carte SD +- Entrées sur GPIO (boutons, capteurs, etc.) \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..4024ab7 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,17 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +lib_deps = + ESP32-audioI2S diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..cfbdcd1 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,278 @@ +void handleStart(); +void handleStop(); +void handleFileUpload(); + +#include +#include +#include +#include +#include +#include +#include + +Audio audio; + +#define SSID "ESP32_MP3" +#define PASSWORD "12345678" +#define SD_CS 5 +#define EEPROM_SIZE 512 +#define MAX_MP3_FILES 32 +#define MAX_FILENAME_LEN 64 +#define NUM_GPIO 8 +#define LED_BUILTIN 2 + +// GPIOs à surveiller +uint8_t gpio_pins[NUM_GPIO] = {12, 13, 14, 15, 16, 17, 21, 22}; + +String mp3_files[MAX_MP3_FILES]; +int mp3_file_count = 0; +int gpio_to_mp3[NUM_GPIO]; +File uploadFile; +String uploadFileName; +bool uploadFileExists = false; +volatile bool gpio_triggered[NUM_GPIO] = {false}; +WebServer server(80); + +// Scanne la carte SD et remplit la liste des fichiers MP3 disponibles +void scan_mp3_files() { + mp3_file_count = 0; + File root = SD.open("/"); + + while (true) { + File entry = root.openNextFile(); + if (!entry) break; + if (!entry.isDirectory()) { + String fname = entry.name(); + if (fname.endsWith(".mp3")) { + if (mp3_file_count < MAX_MP3_FILES) { + mp3_files[mp3_file_count++] = fname; + } + } + } + entry.close(); + } +} + +// Sauvegarde l'association GPIO <-> index MP3 dans l'EEPROM +void save_gpio_mp3_map() { + for (int i = 0; i < NUM_GPIO; i++) { + EEPROM.write(i, gpio_to_mp3[i]); + } + EEPROM.commit(); +} + +// Charge l'association GPIO <-> index MP3 depuis l'EEPROM +void load_gpio_mp3_map() { + for (int i = 0; i < NUM_GPIO; i++) { + gpio_to_mp3[i] = EEPROM.read(i); + Serial.println("GPIO " + String(gpio_pins[i]) + " -> MP3 index " + String(gpio_to_mp3[i])); + if (gpio_to_mp3[i] >= mp3_file_count) gpio_to_mp3[i] = 0; + } +} + +// Génère et envoie la page web principale (configuration, upload, actions) +void handleRoot() { + String html = "ESP32 MP3 trigger

ESP32 MP3 trigger

"; + // Affichage d'un message si présent dans l'URL + if (server.hasArg("msg")) { + html += "
" + server.arg("msg") + "
"; + } + html += "
"; + html += ""; + for (int i = 0; i < NUM_GPIO; i++) { + html += ""; + // Bouton Start pour chaque ligne + html += ""; + html += ""; + } + html += "
GPIOFichier MP3Action
" + String(gpio_pins[i]) + "
"; + // Bouton Stop global + html += "
"; + // Ajout du formulaire d'upload + html += "

Uploader un fichier MP3 sur la carte SD

"; + html += "
"; + html += ""; + html += ""; + html += "
"; + html += ""; + server.send(200, "text/html", html); +} + +// Démarre la lecture d'un fichier MP3 depuis la carte SD +void play_mp3(const String &filename) { + audio.stopSong(); + String path = "/" + filename; + audio.connecttoFS(SD, path.c_str()); +} + +// Handler HTTP POST /start : force la lecture du MP3 associé à un GPIO +void handleStart() { + if (!server.hasArg("gpio")) { + server.send(400, "text/plain", "GPIO non spécifié"); + return; + } + int idx = server.arg("gpio").toInt(); + if (idx < 0 || idx >= NUM_GPIO) { + server.send(400, "text/plain", "Index GPIO invalide"); + return; + } + audio.stopSong(); + play_mp3(mp3_files[gpio_to_mp3[idx]]); + server.sendHeader("Location", "/", true); + server.send(303, "text/plain", "Lecture forcée"); +} + +// Handler HTTP POST /stop : arrête la lecture audio en cours +void handleStop() { + audio.stopSong(); + server.sendHeader("Location", "/", true); + server.send(303, "text/plain", "Lecture stoppée"); +} + +// Handler d'événement d'upload multipart : gère l'enregistrement du fichier MP3 uploadé +void handleFileUpload() { + HTTPUpload& upload = server.upload(); + String msg; + if (upload.status == UPLOAD_FILE_START) { + uploadFileName = "/" + upload.filename; + if (!upload.filename.endsWith(".mp3")) { + msg = "Extension refusée : " + upload.filename; + Serial.println(msg); + uploadFileExists = true; + server.sendHeader("Location", "/?msg=" + msg, true); + server.send(303); + return; + } + if (SD.exists(uploadFileName)) { + msg = "Fichier déjà existant : " + uploadFileName; + Serial.println(msg); + uploadFileExists = true; + server.sendHeader("Location", "/?msg=" + msg, true); + server.send(303); + return; + } + uploadFile = SD.open(uploadFileName, FILE_WRITE); + uploadFileExists = false; + if (!uploadFile) { + msg = "Impossible d'ouvrir le fichier sur la SD"; + Serial.println(msg); + uploadFileExists = true; + server.sendHeader("Location", "/?msg=" + msg, true); + server.send(303); + return; + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (!uploadFileExists && uploadFile) { + uploadFile.write(upload.buf, upload.currentSize); + } + } else if (upload.status == UPLOAD_FILE_END) { + if (!uploadFileExists && uploadFile) { + uploadFile.close(); + scan_mp3_files(); + msg = "Upload terminé : " + uploadFileName; + Serial.println(msg); + server.sendHeader("Location", "/?msg=" + msg, true); + server.send(303); + } + uploadFileExists = false; + } else if (upload.status == UPLOAD_FILE_ABORTED) { + if (uploadFile) uploadFile.close(); + msg = "Upload annulé"; + Serial.println(msg); + uploadFileExists = false; + server.sendHeader("Location", "/?msg=" + msg, true); + server.send(303); + } +} + +// Handler HTTP POST /set : enregistre la configuration GPIO/MP3 depuis le formulaire web +void handleSet() { + for (int i = 0; i < NUM_GPIO; i++) { + if (server.hasArg("mp3_" + String(i))) { + gpio_to_mp3[i] = server.arg("mp3_" + String(i)).toInt(); + } + } + save_gpio_mp3_map(); + server.sendHeader("Location", "/", true); + server.send(302, "text/plain", "Updated"); +} + +// Routine d'interruption pour détecter les changements d'état sur les GPIO surveillés +void IRAM_ATTR gpio_isr() { + for (int i = 0; i < NUM_GPIO; i++) { + if (digitalRead(gpio_pins[i]) == LOW) { + gpio_triggered[i] = true; + } + } +} + +// Initialisation du système, du WiFi, de la carte SD, des GPIO, du serveur web +void setup() { + + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); // éteint la LED intégrée + + // Mode I2S PCM5102 : BCK=27, LRCK=26, DIN=25 (adapter si besoin) + audio.setPinout(27, 26, 25, -1, false); + audio.setVolume(21); // Volume max + Serial.begin(115200); + EEPROM.begin(EEPROM_SIZE); + + WiFi.softAP(SSID, PASSWORD); + Serial.println("AP démarré. Connectez-vous à: " + String(SSID)); + + if (!SD.begin(SD_CS)) { + Serial.println("SD Card Mount Failed"); + return; + } + scan_mp3_files(); + load_gpio_mp3_map(); + + for (int i = 0; i < NUM_GPIO; i++) { + pinMode(gpio_pins[i], INPUT_PULLUP); + attachInterrupt(gpio_pins[i], gpio_isr, FALLING); + gpio_triggered[i] = false; + } + + server.on("/", handleRoot); + server.on("/set", HTTP_POST, handleSet); + server.on("/start", HTTP_POST, handleStart); + server.on("/stop", HTTP_POST, handleStop); + server.on("/upload", HTTP_POST, [](){ server.send(200); }, handleFileUpload); + server.begin(); + Serial.println("Serveur web démarré sur 192.168.4.1"); +} + +// Boucle principale : gestion web, lecture audio, détection et traitement des entrées GPIO +void loop() { + server.handleClient(); + audio.loop(); + static bool led_on = false; + for (int i = 0; i < NUM_GPIO; i++) { + if (gpio_triggered[i]) { + gpio_triggered[i] = false; + // Vérifier que la broche est toujours LOW (anti-rebond) + if (digitalRead(gpio_pins[i]) == LOW) { + Serial.println("GPIO " + String(gpio_pins[i]) + " déclenchée"); + if (!audio.isRunning()) { // Ne lance la lecture que si aucune lecture n'est en cours + Serial.println("Lecture de " + mp3_files[gpio_to_mp3[i]]); + digitalWrite(LED_BUILTIN, HIGH); // allume la LED intégrée + led_on = true; + play_mp3(mp3_files[gpio_to_mp3[i]]); + delay(500); // anti-rebond + } + } + } + } + // Éteindre la LED quand la musique est terminée + if (led_on && !audio.isRunning()) { + digitalWrite(LED_BUILTIN, LOW); + led_on = false; + } +} \ No newline at end of file