Ein gehäuseloser ESP32-MP3-Player. Ein Knopf. Kein Display erforderlich. Echter Shuffle ohne Doppelungen, Bluetooth-Ausgang, USB-Massenspeicher, WLAN-Webkonsole, NeoPixel-Lightshow, persistente Wiedergabe-History. Fertig.
Wir sitzen mindestens einmal die Woche mit einer Handvoll Leuten zusammen — essen, reden, spielen Schocken und hören Musik dabei.
Und letzteres ist manchmal ein bisschen anstrengend: Einer mag dieses nicht, eine mag jenes — und wenn der Sound vom Handy kommt, wird ständig dieses nervige Teil rausgekramt, die Brille rausgekramt und auf dem Schmierpad rumgetippt. Oder "Deine Röhre" kommt zum Einsatz — und die nervt mit Werbung. Und manchmal bricht ja auch im besten Deutschland aller Zeiten das Neuland weg.
Kurz: es stresst.
Also: bau einen MP3-Player, den man einfach auf die Box setzt — mit einem einzigen, für alle zugänglichen Button, mit dem man sofort jeden Song skippen kann. Einfach auf den dicken Button draufpatschen. Fertig.
Erste Versuche mit dem Heltec LoRa (weil's grad da war) stießen an Grenzen — da kam die ESP32-S3-DevKit-Variante dazu. Das OLED braucht man eh nicht; dann könnte man auch gleich ein Handy nehmen. Jetzt ist diese Variante im Einsatz, und den ersten Abenden hat sie bereits überstanden.
Eine 32-GB-Speicherkarte mit derzeit rund 1100 Titeln ist im Einsatz.
Bis zu 5000 sollten problemlos gehen. Zwei beiliegende Scripts kümmern sich um die SD-Karte:
rename_mp3.sh kopiert MP3s in die genormte Verzeichnisstruktur (mit Duplikaterkennung),
make_index.py erzeugt danach die index.csv für den Player.
Die Dateistruktur auf der SD-Karte sieht so aus:
SD:/
├── index.csv
├── history.csv ← Wiedergabe-History (automatisch)
├── 01/
│ ├── 0001.mp3
│ ├── 0002.mp3
│ └── ...
├── 02/
│ └── ...
└── 99/
└── 9999.mp3
Fisher-Yates-Algorithmus: alle Tracks werden einmal gespielt, bevor neu gemischt wird — keine Doppelungen, keine Lücken.
Shuffle-Liste wird auf der SD-Karte gespeichert, Position im NVS-Flash. Nach einem Reboot geht es genau dort weiter, wo man aufgehört hat.
reshuffle mischt die Liste sofort neu. Button beim Einschalten halten = Reshuffle ohne Serial. shuffle list [n] zeigt die nächsten n Tracks.
History-Stack mit 50 Einträgen. Zurück durch bereits gespielte Titel, vorwärts wieder raus.
Gespeichert im NVS-Flash. Überlebt Reboots. Regelbar von 0 bis 21 in beide Richtungen, mit Richtungsumkehr beim Loslassen.
Jeder gespielte Track wird in history.csv auf der SD-Karte protokolliert — mit akkumulierter Uptime als Timestamp.
Die Gesamtlaufzeit des Players wird über Reboots hinweg im NVS-Flash aufsummiert und bei jedem Track gespeichert.
Tracks bekommen in der index.csv eine Priorität 0–9 — beeinflusst die Häufigkeit innerhalb der Shuffle-Runde.
KCX_BT_EMIT Bluetooth-Transmitter als zweiter Ausgang. Laufzeit-Umschaltung zwischen I2S-DAC und BT per Kommando möglich.
SD-Karte per usb on direkt als USB-Laufwerk am PC verfügbar (S3-DevKit, nativer USB). Kein Reboot, kein Kabel umstecken.
Eigener Access Point "Patsch" (PW: patsch1234). Unter http://192.168.4.1/ steht eine vollwertige Browser-Konsole bereit — alle Kommandos, Log in Echtzeit.
17-LED-Streifen an GPIO 1: Regenbogen-Welle über Lautstärkebalken im Play-Modus, Orange-Atemeffekt bei Pause, Blitz bei Vol+/−.
index new /dd erstellt automatisch eine neue index.csv aus einem Verzeichnis und sichert die alte. index restore N stellt sie wieder her.
ESP32 WROOM (Minimalaufbau), ESP32-S3 DevKit (USB-MSC, WLAN, NeoPixel) und Heltec LoRa v3 (eingebautes OLED-Display).
Vollständiges Kommando-Interface per USB-Serial: Dateizugriff (ls/get/put/rm/mkdir), Index verwalten, BT steuern, Ausgabe umschalten.
Startet ohne SD-Karte mit Fake-Tracks — ideal zum Testen von Button-Logik, Lautstärke und Kommandos ohne echte Hardware.
Nächster Track aus der gemischten Shuffle-Liste. Kein Track kommt doppelt, bis alle gespielt wurden.
Navigiert rückwärts durch die History. Bis zu 50 Tracks zurück möglich. 2. Klick innerhalb ~900ms nach dem 1. Klick.
Wiedergabe pausieren oder fortsetzen. 3. Klick innerhalb ~900ms nach dem 2. Klick.
Rauf oder runter — je nach aktueller Richtung. Wiederholt alle 500ms bis du loslässt.
Nächster Lang-Druck geht in die andere Richtung. Kein Durchdrehen bis ans Limit nötig.
Onboard-LED (alle Boards) zeigt den Betriebszustand. Der NeoPixel-Streifen (S3-DevKit) visualisiert zusätzlich die Lautstärke.
900ms — Musik läuft
150ms — Lautstärke wird erhöht
75ms — Lautstärke wird gesenkt
Blinken stoppt — Min/Max erreicht oder Pause
Animierte Farb-Welle über den Lautstärkebalken (17 LEDs)
Orange pulsierend über den aktuellen Volume-Balken
Ganzer Streifen blau für 600ms
Ganzer Streifen rot für 600ms
Nun muss ein Gehäuse her — schnell, einfach, passend, stabil und billig. Da war doch was …
Drei Board-Varianten werden unterstützt. Nur eine Zeile in patsch.ino einkommentieren:
(Die letzten Versionen sind aber bislang nur mit dem S3-Devkit getestet worden. Eigenes Feedback gern an ralf@rbag.de)
// #define BOARD_HELTEC_LORA_V3 #define BOARD_ESP32_S3_DEVKIT // #define BOARD_ESP32_WROOM
| GPIO 14 | → | BCK |
| GPIO 15 | → | LCK |
| GPIO 16 | → | DIN |
| 3.3V / 5V | → | VIN |
| GND | → | GND, FMT |
| 3.3V | → | XSMT |
| GPIO 10 | → | CS |
| GPIO 11 | → | MOSI |
| GPIO 13 | ← | MISO |
| GPIO 12 | → | SCK |
| 3.3V | → | 3V3 |
| GND | → | GND |
| GPIO 17 | ← | TX (Modul sendet) |
| GPIO 18 | → | RX (Modul empfängt) |
| GPIO 38 | ← | LINK (HIGH=verbunden) |
| GPIO 21 | → | CON (LOW-Puls=Pairing) |
| 3.5–5V | → | +5V (Elko 100µF) |
| GND | → | PGND + AGND |
| IN_L/IN_R ← analoger Ausgang PCM5102A | ||
| GPIO 0 | — | BOOT-Taster (gegen GND) |
| GPIO 48 | — | Onboard NeoPixel |
| GPIO 1 | — | NeoPixel-Streifen DIN (17 LEDs) |
| GPIO 27 | → | BCK |
| GPIO 26 | → | LCK |
| GPIO 25 | → | DIN |
| 3.3V / 5V | → | VIN |
| GND | → | GND, FMT |
| 3.3V | → | XSMT |
| GPIO 5 | → | CS |
| GPIO 23 | → | MOSI |
| GPIO 19 | ← | MISO |
| GPIO 18 | → | SCK |
| 3.3V | → | 3V3 |
| GND | → | GND |
| GPIO 33 | — | Taster (gegen GND) |
| GPIO 2 | — | Onboard LED |
| GPIO 16 | ← | TX (Modul sendet) |
| GPIO 17 | → | RX (Modul empfängt) |
| CON/LINK: nicht verdrahtet | ||
| GPIO 14 | → | BCK |
| GPIO 15 | → | LCK |
| GPIO 13 | → | DIN |
| GPIO 46 | → | SD-SCK |
| GPIO 47 | → | SD-MOSI |
| GPIO 45 | ← | SD-MISO |
| GPIO 5 | → | SD-CS |
| GPIO 17 | — | OLED SDA |
| GPIO 18 | — | OLED SCL |
| GPIO 21 | — | OLED RST |
| GPIO 36 | — | VEXT (LOW=3.3V an) |
| GPIO 33 | — | Taster (gegen GND) |
| GPIO 35 | — | Onboard LED |
Abhängigkeiten über den Arduino Library Manager installieren:
| Library | Version | Zweck |
|---|---|---|
| ESP32-audioI2S | ≥ 2.0 | MP3-Dekodierung + I2S-Ausgabe |
| Adafruit NeoPixel | ≥ 1.11 | NeoPixel-Streifen (S3-DevKit) |
| Adafruit SSD1306 | ≥ 2.5 | OLED (nur Heltec) |
| Adafruit GFX | ≥ 1.11 | OLED (nur Heltec) |
SD-Karte befüllen mit rename_mp3.sh:
# Dry-run (zeigt was passieren würde) ./rename_mp3.sh ~/Musik /media/SDCARD # Wirklich kopieren ./rename_mp3.sh ~/Musik /media/SDCARD --run # Mit Duplikatprüfung (Dateiränder vergleichen) ./rename_mp3.sh ~/Musik /media/SDCARD --run --diff-edges # Vollständiger Byte-Vergleich (langsam aber sicher) ./rename_mp3.sh ~/Musik /media/SDCARD --run --diff-full
Danach Index generieren mit make_index.py:
# Index für die gesamte SD-Karte generieren python3 make_index.py /media/SD --priority 5 -o /media/SD/index.csv # Nur einen Ordner indizieren python3 make_index.py /media/SD --include 04 -o /media/SD/index.csv # Mit Priorität aus Ordnernamen (ambient=1 … hits=9) python3 make_index.py /media/SD --genre-from-folder -o /media/SD/index.csv # Vorschau ohne Schreiben python3 make_index.py /media/SD --dry-run
Format der index.csv:
/01/0001.mp3,,5 /01/0002.mp3,,9 /02/0001.mp3,,3
Felder: Pfad, Genre (optional), Priorität 0–9
Format der history.csv (automatisch angelegt):
uptime_ms,pfad,priorität 183742,/01/0330.mp3,5 184891,/02/0007.mp3,3
Timestamp = akkumulierte Gesamtlaufzeit des Players in Millisekunden.
Flashen mit flash.sh:
# ESP32-S3 DevKit (Port automatisch erkannt) ./patsch/flash.sh s3devkit # ESP32 WROOM ./patsch/flash.sh wroom # Heltec LoRa v3 ./patsch/flash.sh heltec # Port explizit angeben ./patsch/flash.sh s3devkit /dev/ttyACM0 # Nur kompilieren, nicht flashen ./patsch/flash.sh --only-compile
Hinweis S3-DevKit: Beim ersten Flash ohne CH343-Adapter muss der Download-Modus manuell ausgelöst werden: BOOT gedrückt halten → kurz RESET → BOOT loslassen. Mit angeschlossenem CH343-Adapter (UART0) funktioniert das Auto-Reset normal.
Der ESP32-S3 DevKit spannt beim Start einen eigenen Access Point auf:
| SSID | Patsch |
| Passwort | patsch1234 |
| IP | 192.168.4.1 |
Unter http://192.168.4.1/ steht eine Browser-Konsole bereit: Konsolen-Log in Echtzeit (1s-Polling), Eingabefeld für alle seriellen Kommandos. Funktioniert parallel zur USB-Serial-Konsole.
Über USB-Serial (Newline als Zeilenende) oder die Web-Konsole steuerbar:
next Nächster Titel (wie Knopf 1×)prev Vorheriger Titel (wie Knopf 2×)pause Pause / Weiter umschalten (wie Knopf 3×)play <n> Titel Nr. n direkt abspielen (1–Tracks)play /dd/nnnn.mp3 Titel per Pfad abspielenvol <0-21> Lautstärke direkt setzenhelp Befehlsübersicht anzeigenstatus RAM, SD, Lautstärke, Board, aktueller Trackshuffle Shuffle-Position anzeigen (pos/gesamt/verbleibend)shuffle list [n] Nächste n Tracks der Shuffle-Liste anzeigen (Default: 20)reshuffle Shuffle-Liste sofort neu mischen (auch: Button beim Einschalten halten)history Anzahl Einträge in history.csv + Gesamtuptimecat history history.csv vollständig ausgebencat index index.csv vollständig ausgebenindex Neue index.csv zeilenweise hochladen (Leerzeile = Ende)index new /dd [p] Neue index.csv aus allen .mp3 in /dd erstellen (Prio p, Default 3); alte wird gesichertindex restore N index.csv aus Backup index_N.csv wiederherstellenindex list Vorhandene index-Backups (index_N.csv) auflistenls [pfad] Verzeichnis auflisten (Default: /)get <datei> Datei als Base64 ausgebenput <datei> Datei per Base64 hochladenrm <pfad> Datei oder Verzeichnis löschenmkdir <pfad> Verzeichnis anlegenusb on SD-Karte als USB-Laufwerk freigeben (Wiedergabe pausiert)usb off USB-Massenspeicher beenden, Wiedergabe fortsetzenoutput Aktuellen Ausgang anzeigenoutput i2s Ausgang: I2S DAC (PCM5102A)output bt Ausgang: Bluetooth (KCX_BT_EMIT)kcx pair Pairing auslösen (CON-Pin LOW-Puls)kcx link LINK-Pin lesen (Verbindungsstatus)kcx on KCX Power ONkcx off KCX Power OFFkcx <cmd> AT-Kommando direkt senden (z.B. kcx AT+NAME?)btntest Tasten-Flanken-Log ein/ausschaltenbtnreset Button-State zurücksetzenreboot Uptime + Shuffle sichern, ESP32 neu starten/shuffle.csv auf der SD gespeichert; Position weiterhin im NVSindex new /dd [p], index restore N, index listskipEofPlay verhindert ungewollten Track-Sprung nach Pausekcx pair löst Pairing per GPIO-Puls auskcx link zeigt Verbindungsstatus (S3-DevKit: GPIO 38)output i2s / output bt zur Laufzeitusb on / usb off)make_index.py: Index-Generator mit Genre/Priorität-Mapping