commit
a10c58311d
4 changed files with 354 additions and 0 deletions
@ -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. |
||||||
|
|
||||||
|
 <img src="https://i.sstatic.net/yrT4Mqr0l.png" title="" alt="audio - How to make PCM5102 DAC work on Raspberry Pi ZeroW? - Raspberry Pi Stack Exchange" width="305"> |
||||||
|
|
||||||
|
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.) |
||||||
@ -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 |
||||||
@ -0,0 +1,278 @@ |
|||||||
|
void handleStart(); |
||||||
|
void handleStop(); |
||||||
|
void handleFileUpload(); |
||||||
|
|
||||||
|
#include <FS.h> |
||||||
|
#include <SD.h> |
||||||
|
#include <SPI.h> |
||||||
|
#include <WiFi.h> |
||||||
|
#include <EEPROM.h> |
||||||
|
#include <WebServer.h> |
||||||
|
#include <Audio.h> |
||||||
|
|
||||||
|
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 = "<html><title>ESP32 MP3 trigger</title><meta charset='utf-8'><body><h2>ESP32 MP3 trigger</h2>"; |
||||||
|
// Affichage d'un message si présent dans l'URL
|
||||||
|
if (server.hasArg("msg")) { |
||||||
|
html += "<div style='color:blue;font-weight:bold'>" + server.arg("msg") + "</div>"; |
||||||
|
} |
||||||
|
html += "<form method='POST' action='/set'>"; |
||||||
|
html += "<table border='1'><tr><th>GPIO</th><th>Fichier MP3</th><th>Action</th></tr>"; |
||||||
|
for (int i = 0; i < NUM_GPIO; i++) { |
||||||
|
html += "<tr><td>" + String(gpio_pins[i]) + "</td><td><select name='mp3_" + String(i) + "'>"; |
||||||
|
for (int j = 0; j < mp3_file_count; j++) { |
||||||
|
html += "<option value='" + String(j) + "'"; |
||||||
|
if (gpio_to_mp3[i] == j) html += " selected"; |
||||||
|
html += ">" + mp3_files[j] + "</option>"; |
||||||
|
} |
||||||
|
html += "</select></td>"; |
||||||
|
// Bouton Start pour chaque ligne
|
||||||
|
html += "<td><form method='POST' action='/start' style='display:inline'><input type='hidden' name='gpio' value='" + String(i) + "'><input type='submit' value='Start'></form></td>"; |
||||||
|
html += "</tr>"; |
||||||
|
} |
||||||
|
html += "</table><input type='submit' value='Enregistrer'></form>"; |
||||||
|
// Bouton Stop global
|
||||||
|
html += "<form method='POST' action='/stop' style='margin-top:10px;display:inline'><input type='submit' value='Stop'></form>"; |
||||||
|
// Ajout du formulaire d'upload
|
||||||
|
html += "<h3>Uploader un fichier MP3 sur la carte SD</h3>"; |
||||||
|
html += "<form method='POST' action='/upload' enctype='multipart/form-data'>"; |
||||||
|
html += "<input type='file' name='file' accept='.mp3'>"; |
||||||
|
html += "<input type='submit' value='Uploader'>"; |
||||||
|
html += "</form>"; |
||||||
|
html += "</body></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; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue