GroundControl
A small always-on network audio receiver: an ESP32-S3 and a DAC that play internet radio through a real stereo. It boots in about four seconds.


What it does
- Standalone radio with 23 presets: Death.FM, Metal Detector and 21 other SomaFM channels. No phone needed.
- Streams fail over to the next mirror if one drops.
- Doubles as an output for my own music library: my phone app and desktop player can hand playback over, and the device carries on with its own queue.
- Scrobbles to Last.fm itself, switchable per station.
- Two buttons and a 128x64 OLED with a ship-terminal look: scan lines on big text, a scrolling radio-link readout while tuning and optional dial static between stations.
- A phone-friendly control page and a small HTTP API on the local network, and over-the-air firmware updates.
Hardware
| Part | Role |
|---|---|
| ESP32-S3 DevKitC-1 (16 MB flash, 8 MB PSRAM) | PSRAM holds the 1 MB jitter buffer and the AAC decoder. |
| PCM5102A I2S DAC | Stereo line out, roughly 2 V at full scale like a CD player. |
| SSD1306 128x64 OLED | I2C display. |
| Two push buttons | Press to play and step stations; hold to stop or power down. |
Under the hood
- C++ on the Arduino framework, built with PlatformIO. Death.FM's HE-AAC stream is decoded with FAAD2, the SomaFM MP3 streams with libhelix.
- A supervisor task plays and reconnects, with a network ring buffer in PSRAM and an ID3 skipper.
- Two app partitions on the 16 MB flash allow wireless updates, verified with an MD5 check before the new image is accepted.
- A handoff API passes the library walk back and forth with the phone, resuming mid-track with a ranged request so the position carries over.
- Python tooling embeds the web page into the firmware, previews it against the real device and pushes updates.
The screenshots show the control page from the firmware source running against a stubbed device, so the track, channel states and address are made up. The OLED itself is not shown.