commit
a10c58311d
4 changed files with 354 additions and 0 deletions
@ -0,0 +1,57 @@
@@ -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 @@
@@ -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 @@
@@ -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