Skip to content

Branches

Default: main

Tags

No tags yet

Pocket Radio: ESP32 × Cloudflare SFU

An ESP32-S3 streams a shared music playlist through Cloudflare Realtime SFU. Browsers listen together, watch the live spectrum and hardware telemetry, and take turns controlling playback and the onboard RGB LED. The board needs only USB power; sound plays in the browser.

The Rust firmware uses str0m to publish one stereo Opus track and two application data channels. robot carries telemetry and commands; spectrum carries 32 frequency bands. A task on core 1 decodes a copy of the music and calls ESP-DSP's 2,048-point FFT. A Worker and Durable Object coordinate sessions and control permission; media flows through the SFU. The board's Wi-Fi network can be isolated from the browser's LAN.

Watch the exhibit walkthrough

Watch the exhibit

Set up

The supported board is ESP32-S3-DevKitC-1 N32R16V: 32 MiB octal flash, 16 MiB octal PSRAM, and an RGB LED on GPIO38. Connect its native USB port. The C6 is not a supported firmware target.

Use Linux x86_64, Node 24, Python 3.11 with venv support, and Rust 1.88 or later with rustfmt and Clippy. Install the ESP-IDF Linux prerequisites. Music preparation also needs ffmpeg and ffprobe.

Create a Cloudflare Realtime SFU app and obtain its App ID and App Secret. From the repository root:

make setup
cp .credential.env.example .credential.env
# Fill in REALTIME_APP_ID, REALTIME_APP_TOKEN (the App Secret), and Wi-Fi credentials.
make secrets
export SIGNALING_URL=https://radio.example.com

Replace the example URL with your Worker's HTTPS origin. Keep it exported for firmware builds, live previews, and archive restoration. Follow the Worker deployment guide to configure your domain and deploy before starting the board.

Keep all credentials in ignored .credential.env. make secrets initializes or imports the device token and viewer password. worker/.dev.vars is generated automatically for web development and builds.

To change the viewer password, edit VIEWER_PASSWORD in .credential.env and run make deploy. For local changes, restart make dev.

Back up and flash

Your account needs serial-device access, commonly membership in dialout on Debian/Ubuntu. Start a new login session after changing group membership. Set ESP32_PORT if more than one board is connected.

Put your audio files in tracks/ before preparing the first catalog. See playlist options for ordering and metadata overrides. Before replacing the board's software:

make backup
make music TRACK=tracks/
make build-firmware
make flash FLASH_ARGS=
make monitor

make backup enters the bootloader, reads and verifies the full 32 MiB flash, then records its SHA-256. It can take several minutes and refuses to overwrite an existing backup. Files stay in artifacts/hardware-validation/; set RADIO_BACKUP_DIR to use another directory, and keep separate backups for separate boards. make backup-check checks saved files without contacting the board. Flash helpers require a valid saved backup.

make flash FLASH_ARGS= writes the firmware and prepared music. Plain make flash writes firmware only and preserves music. Verified images are cached privately under artifacts/flash-cache/, keyed by USB identity and partition layout. Later flashes write changed sectors and verify the complete resulting image; a stale baseline falls back to a full write. Use FLASH_ARGS="--firmware-only --full" for a complete firmware write when troubleshooting.

make monitor streams filtered logs until interrupted. make monitor MONITOR_ARGS="--check --reset" reboots and checks for a successful heartbeat; a failed check exits with a nonzero status. Reconnect browser listeners after the board restarts.

Open your Worker URL, enter the viewer password, and select Start listening. Take control enables shared LED/playback controls. Volume and mute affect only your browser.

Change the playlist

Put audio files in ignored tracks/, then prepare them in filename order:

make music TRACK=tracks/
make flash-music

For a custom order or metadata overrides, copy playlist.example.json to ignored playlist.json, edit its entries, and run make music PLAYLIST=playlist.json. Preparation uses 48 kHz stereo Opus with a default 96 kb/s variable-bitrate target. Set "bitrateKbps": 88 in a playlist entry to choose a different target for that song. The prepared music.json records each song's target bitrate. The catalog supports up to 32 songs of at most 10 minutes each, with a total duration of at most 60 minutes, and must fit the 16 MiB music partition. See the music format for packet and metadata limits.

The board supplies the current title, artist, and duration; no Worker deployment is needed when replacing the catalog.

The playlist loops, and the controller can use Next track to advance early. Next preserves pause; Restart begins the current song and resumes playback. The board reboots after a music flash. An installation with a 4 MiB music partition needs a full firmware/partition-table flash before make flash-music; see compatibility.

Development

make fmt formats Rust, C, and web source. The C formatter is installed by make setup; make setup-c-format installs it independently of the ESP-IDF SDK. Use make fmt-c or make check-c-format to format or check only the C adapters.

make check runs portable tests, type checks, formatting, Clippy, and rustdoc. make test-worker checks the Worker lifecycle in the Cloudflare Vitest runtime. make test-worker-bundle checks the built Worker and actual alarm scheduling. These commands need no hardware or real credentials.

Guide Contents
Worker Local preview, deployment, architecture, browser tests
Firmware Builds, task ownership, runtime and music format
Native boundary C/Rust buffer and lifetime contracts
ERRATA Integration issues and compatibility
Archives SFU bring-up, FFT benchmark, C radio, str0m probe
Third-party components Dependencies and licenses

Maintained code lives in firmware/, worker/, and scripts/. Archives are versioned but excluded from normal builds and CI. Downloads (.tools/), local experiments (experiments/), and generated outputs (artifacts/) are ignored.