BERTH 08 — HARDWARE & EMBEDDED

SoundNav

Architect Dial OS · 1.28" Round Display · ESP32-S3

Custom firmware for a CrowPanel round display with rotary encoder. One physical dial controls Sonos music, WiZ lights, weather, and a Discogs vinyl library — all through a unified local bridge. No cloud auth on the device.

Active C++ / PlatformIO ESP32-S3 Node.js bridge LovyanGFX
STACK
HARDWARE
ESP32-S3R8 (dual-core, 8 MB PSRAM)
GC9A01 240×240 round IPS (SPI)
CST816D capacitive touch
Rotary encoder + push-button
WS2812 RGB · SSD1306 OLED
FIRMWARE
C++ · Arduino framework
PlatformIO · LovyanGFX
SPIFFS · PSRAM sprites
IApp / AppManager framework
ISR-safe input pipeline
BRIDGE
Node.js · pm2 (process supervision)
node-sonos-http-api (local)
Discogs API · library.json
WiZ local UDP (port 38899)
Static IP host · pinned LAN
EXTERNAL APIs
Open-Meteo (weather)
NOAA CO-OPS (tides)
NDBC buoy (sea surface)
Discogs (vinyl catalog)
ARCHITECTURE

The device never talks directly to cloud services. All external integrations live behind a single Node.js bridge on the home PC, which exposes three namespaces on one local port. The ESP32 makes one type of HTTP call — to one address — and the bridge handles credentials, protocols, and data normalization upstream.

DIAL ESP32-S3 HTTP / LAN BRIDGE Node.js · pm2 /sonos/* /vinyl/* /wiz/* JPEG normalization pipeline library.json · queue · history Sonos node-sonos-http-api · local Discogs API · album art · catalog WiZ Bulbs UDP port 38899 · local Weather / Tides Open-Meteo · NOAA · NDBC

The firmware is structured around a small IApp interface and an AppManager that owns the launcher, routes input events, and runs a non-blocking main loop: poll input → drain ISR-safe event queue → update active app → service Wi-Fi → render. Input is interrupt-driven with a quadrature decode table and debounced long-press detection. Apps are decoupled from networking — that separation is what makes each new capability an isolated addition rather than a cross-cutting change.

Four apps are running on hardware: Sonos (room wheel, now-playing with radial progress ring, album art), Weather (four pages — Today, Sun, Moon, Tides — cycled with the dial), WiZ (brightness, color temperature, scenes), and Vinyl (Discogs-backed library with queue, play-count tracking, and history, plus a companion desktop web UI on the bridge).

ENGINEERING DECISIONS
NOTABLE PROBLEMS
The Sonos "regression" that wasn't firmware

Live Sonos control died mid-session with no code change. The instinct was to look at recent cleanup. The decisive move instead: the Weather app — a plain internet HTTP call — still worked while the Sonos bridge call failed. The same HTTP client reaching the internet but not a specific LAN address can only be a network problem. On-screen diagnostics (error code + Wi-Fi RSSI) and a Test-NetConnection to port 5005 confirmed it: the bridge host's IP had drifted via DHCP (.125.63) and the server wasn't running. Fix was pm2 persistence and a static IP, not a firmware change. Now a permanent entry in the debugging playbook: when a connected service fails, reproduce against the dependency before reading any code.

Blocking HTTP starving the input loop

Long-press-to-go-back stopped working, but only when the bridge was unreachable. A failing HTTP poll was blocking the main loop for up to five seconds — during which the input driver never ran and the button-hold timer was never serviced. Fix: short connect timeout, back off polling to 10 s when the bridge is down. On a single-threaded microcontroller, every synchronous call in the per-frame path is a potential freeze.

Flicker: three separate root causes

The WiZ screen redrew the whole frame every tick because its dirty flag was set but never cleared — fixing that plus PSRAM double-buffering solved it. The animated "Buddy" screen couldn't afford a sprite, so the solution was repainting a fixed bounding box each frame with the backdrop pixels drawn as background rather than calling fillScreen — flicker-free at zero extra RAM. A third unrelated screen was flickering due to a sprite that exceeded PSRAM limits; profiling allocated memory per-frame caught it.

Flash that lies

The board advertises 16 MB flash; 8 MB is usable. A 16 MB SPIFFS partition definition froze the Vinyl app on mount. Recovery: cap the partition inside 8 MB, switch to a non-auto-formatting mount so a cache-init failure can't block the input loop and read as broken navigation. Verify hardware specs against the actual device, not the datasheet.

LCD power-enable pins: the display that wouldn't turn on

The panel stayed black after firmware upload even though the backlight blinked and SPI transactions appeared to succeed. The GC9A01 init sequence looked correct. The actual problem: the CrowPanel routes LCD logic power through GPIO1/GPIO2; GPIO4/GPIO12 (which earlier code drove) are unrelated test I/O. Powering the wrong rails left the controller dark while everything else looked healthy. Getting the first real pixels on a round screen was the moment the project became buildable.

CURRENT STATUS
WORKING Launcher, Sonos (room wheel + now-playing + album art), Weather (Today / Sun / Moon / Tides), WiZ (brightness / color / scenes via bridge), Vinyl (desktop web UI + queue + play-count + history). Bridge supervised by pm2, survives logon.
ROUGH EDGES On-device album-art cache disabled pending a safe SPIFFS init path. Bridge cover-conversion briefly pops a PowerShell window on first normalization. Current work lives on a feature branch — not yet merged or QA-signed to main.
NEXT Merge and harden the current branch. Move WiZ discovery fully server-side. Replace PowerShell image converter with a long-lived background prewarm process. Add deliberate, user-triggered on-device art cache.