Repository navigation
Releases: JFG3rd/SpectraLab-P4
Release list
v1.3.2 — Themed list panels, tofu glyphs removed, layout polish
Fixed
- The SSID and saved-network lists stayed stock white in every colour scheme.
lv_listis not anlv_objsubclass the theme hook was matching, so it kept
the default theme's card background — a bright white panel on, say, Red Neon.
List rows are now flat with a themed separator, and the selected row uses the
scheme's accent. - 54 characters in on-screen labels were outside Montserrat's ASCII range and
rendered as tofu boxes: 29 em dashes (—), 24 bullets (•, the password
mask on the saved-network detail screen was a row of empty squares) and one
multiplication sign. Serial-log strings keep theirs — those render in a
terminal, not in Montserrat. - The mic-sensitivity readout wrapped to two lines: "-38.0 dBV" measures about
63 px at Montserrat 14 and the value cell was 64.
Changed
.gitignorenow covers the" 2"/" 3"-suffixed duplicates a sync client
keeps creating in this working copy, so they cannot be committed by accident.
v1.3.1 — Whole-UI colour theming, three-column Settings, browser settings page
Added
- The analyzer names the board it is actually running on. A P4X (rev-3
silicon, EV board v1.6) now reads "SpectraLab-P4X" on the status bar, the
splash and inGET /api/status; the older v1.5.2 board still reads
"SpectraLab-P4". Detected from the silicon revision at runtime rather than
from the build environment, so it stays correct down the rawidf.pypath,
which skipstools/check_chip_rev.py. - Configurable boot splash. Five seconds by default (was a hardcoded 2.5), set
from the on-device Settings screen or the browser, and Off skips it
entirely. The credit line pulses rather than sitting still. - A device settings page in the browser —
/settings.html. The REST config
API has been complete since M4 and no HTML page had ever called it, so every
setting except the network was device-only. The form is built from a single
descriptor array rather than 25 hand-written fields. - Shared navigation across every web page. Navigation used to be hub-and-spoke:
each page had one link home and none to its siblings, so Files to Network was
two hops. Homebutton besideBackon the screens reached from Settings, so leaving a
nested dialog is one tap instead of three.
Changed
- The colour theme now applies to the whole UI. It used to cover only the
spectrum screen and the status-row buttons; Settings, Wi-Fi setup, the file
dialogs, the splash and the toast were permanently dark-blue, and their
dropdowns, buttons, sliders and switches were not styled at all — they
inherited LVGL's stock theme, which this project builds in its light
variant. Choosing HIGH CONTRAST turned the spectrum light and left everything
else dark. The palette moved into a sharedui_thememodule which also
installs an LVGL theme hook, so every widget picks up the scheme as it is
created — including screens added later. - Settings is three columns instead of two. The old layout wasted 160 px
down the middle, gave a 220 px dropdown to text like "50%", and pushed three
whole groups (SPL Calibration, Settings Profile, Auto Gain) below the 600 px
fold where they were easy to miss. Everything now fits on one screen. Built
with flex containers, so a row fills its column by itself. Backsits in the top-right button row on every screen that has one, in line
with the settings gear and the screenshot button, instead of at the bottom of
a column where it moved whenever a group grew.- The clock and timezone controls moved off the file browser onto the new
settings page, where a device-wide setting belongs. - The theme toggle and the browser-clock push are served once from
/app.js
instead of being pasted into all four pages.
Fixed
- Two long-standing overlaps on the Settings screen: the profile hint wrapped
into the timezone dropdown, and the Wi-Fi Setup button sat inside the mic
calibration band despite being unrelated to it. - The toast overlay was unreadable on the light scheme (fixed dark green on
dark red); it now takes its colours from the active palette. - A status-row button created before the spectrum screen came up used hardcoded
DARK colours until the next theme change.
Note
settings_tgrew a field (splash_seconds), which invalidates the NVS blob.
Devices without an SD card get a one-time settings reset on first boot; SD
settings.jsonkeeps every existing value. Expected, not a bug.
v1.3.0 — Screenshots, SD file browser, access-point mode and a real clock
Added
-
Access-point mode as a deliberate choice, not just a fallback. The analyzer
can be told to be its own Wi-Fi network permanently, for use where there is
none. Selectable on the device and in the browser; both confirm twice and
restart, because the change takes the analyzer off the LAN.
Most of this already worked — the AP was APSTA, the DHCP server was running
and the web server was never gated on the station — so the work was the three
things that did not: no way to choose it, no mDNS on the AP interface, and
no captive portal. -
Captive portal. A small DNS server answers every query with 192.168.4.1
while the AP is up, and a catch-all HTTP route redirects to the portal, so
phones and laptops open the page by themselves. One catch-all covers every
platform's detection URL, so no per-OS endpoints are needed. It is registered
last and only redirects in AP mode — in station mode an unknown URL still
returns 404, so a typo does not silently become a redirect. -
mDNS now starts on the access point too, not only on STA_GOT_IP, so
.local resolves without a router. -
net_mgr_restart_soon(), an esp_timer-based deferred reboot. LVGL timers
belong to the screen that created them, so a reboot scheduled through one
would silently never fire from a different screen. -
The analyzer learns the time. SNTP starts once the station has an address,
preferring a DHCP-advertised server over the public pool so it works on a LAN
with no route out; failing that, every web page quietly posts the browser's
clock to POST /api/time. A browser is only trusted while the device has no
time of its own, so a skewed machine cannot degrade a good sync.
Files written before the clock is known still show no date, and are never
restamped. -
Timezone, configurable on the device (Settings) and in the browser. This is
not cosmetic: FAT stores local time, so the zone decides what is written into
a file. Both selectors are built from one table in the firmware, so they
cannot offer different lists. -
GET /api/status reports time, time_source and timezone; the Wi-Fi screen
shows the device clock alongside the browser entry point. -
The status-row buttons now follow the colour scheme. They were the only part
of the status bar that ignored the palette. -
On-device saved Wi-Fi network management, reached from Wi-Fi Setup ->
"Saved Nets": the stored networks in most-recently-used order, each tagged
[DHCP]or[static]; a detail screen showing the saved password masked
behind a "Show password" toggle (re-masked whenever the screen is reopened);
and Forget, which takes two taps because deleting the network in use drops
the unit off the LAN. -
Per-network static IP configuration (IP / netmask / gateway / optional DNS),
stored against each saved network rather than globally, so a fixed address at
one site and DHCP elsewhere both work when the unit moves. Fields pre-fill
from the live DHCP lease when converting a network to static. -
Address-in-use checking: "Check & Save" ARP-probes the candidate address
(RFC 5227 style, as a DHCP client does) and refuses to save if anything
answers. Chosen over ICMP because hosts commonly firewall ping, which would
report a live address as free. Documented honestly in the UI and the User
Guide: a silent host still owns its address. -
net_mgrgainsnet_mgr_get_network(),net_mgr_set_network_ip(),
net_mgr_ip_in_use()and STA address getters. The saved-network blob moves
to v2 with an explicit v1 migration, so existing credentials survive the
upgrade instead of being silently discarded by the size check. -
Screen capture to the SD card as PNG, triggered from a status-bar button, a
long press on front-panel key 2, orPOST /api/screenshot. Files land in
/sdcard/spectrum/screenshots/asshot-NNNN.png, numbered past the highest
existing index so deleting from the middle never overwrites.
The image comes from the DPI scan-out framebuffer, because LVGL renders
through a 50-line partial draw buffer and never holds a full frame. The
framebuffer is snapshotted under the LVGL lock and encoded on a worker task —
the SD write must not happen under that lock, which has no timeout. The panel
applies mirror_x + mirror_y in hardware, so the buffer is a 180-degree
rotation of what is on screen; undoing it is folded into the RGB565 -> RGB888
conversion and costs nothing.
PNG rather than BMP because the ESP32-P4 ROM already contains miniz's
streaming deflate encoder, so real compression costs no flash and no
third-party source — and unlike BMP, the result renders in GitHub markdown.
Output streams to the card as successive IDAT chunks, so the encoded image
never has to fit in RAM. -
Browser-based SD card file browser at
/files.html: lists screenshots,
presets and calibration files with sizes, downloads any of them, and deletes
screenshots. Downloads stream with chunked transfer, so a full-screen PNG is
never buffered whole.
Deletion is confined to screenshots in the firmware, not just the page —
presets, calibration files andsettings.jsonare work that cannot be
regenerated, whereas a capture can be retaken. Directories are named by an
enum keyword rather than a path, so a request cannot express a location
outside/sdcard/spectrumat all. -
Peak readout cursor: long-press a peak in any FFT-based view to freeze its
exact frequency and level, the nearest 1/3-octave band, and the nearest note
with cents error. Long press rather than tap so a stray touch or a pinch can
never plant one. The position is stored as an FFT bin, so the cursor tracks
its peak through a pinch zoom instead of drifting, and the snap-to-peak
search window scales with the zoom. -
A-weighting and microphone sensitivity controls on the Settings screen, in a
new SPL CALIBRATION group. Both have been computed and persisted by the DSP
engine since Phase 2 M2 but had no control, so the SPL readout could only be
calibrated by hand-editingsettings.json. Sensitivity applies on release
rather than on Back, so it can be dialled in against a reference meter. -
Named settings profiles: load any saved preset directly from the Settings
screen, with the active name shown in the PRESETS group.
A profile is a label, not a save target — ordinary edits keep auto-saving to
the working configuration and never write back to the named file, so a preset
stays the snapshot it was taken as.settings_t.active_profilerecords where
the live configuration came from. -
The connected Wi-Fi network is shown in the spectrum status bar, refreshed on
its own timer so it keeps updating while the display is frozen. -
Per-network static IP configuration from the browser as well as the device,
with the same ARP address-in-use check. Saved passwords are deliberately not
exposed over the portal, which is plain HTTP with no authentication. -
The device's mDNS entry point is now visible:
http://<host>.localand the
raw address appear on the Wi-Fi screen,GET /api/statusgainshostname
andurlfields, and the landing page renders it as a bookmarkable link. -
panel_buttongains a registrable long-press callback. Key 0's long press
stays hard-wired to the restart, since that is the last-resort recovery when
the camera driver wedges.
Changed
-
Embedded web pages are served with Cache-Control: no-store. They ship inside
the firmware and change with every update, so a browser that had seen a page
before kept serving its cached copy after a flash — which hid a fixed
download button behind a stale page for an entire debugging session. -
/api/download takes the filename in the path (/api/download/
/)
rather than an X-Filename header, so an ordinary link works. Directory and
filename still pass through the same enum lookup and
settings_mgr_resolve_path(), and the guards were re-verified on hardware
after the rewrite. -
The SD file listing is a compact table — name, size, recorded date, download
and delete on one row — sorted newest first. -
max_uri_handlersraised from 16 to 24. This release adds six routes, and
esp_http_serverdrops anything past the limit without reporting it. -
README: "Project Background" merged into "Why I Built This Project" and the
truncated sentences committed into the file restored. Display Modes now lists
all eight modes (Line and Persistence were missing), and the repository
structure and changelog references were corrected. -
settings_tgainsactive_profile, so the NVS blob size check fails once and
resets to defaults on the first boot after upgrading (expected; see CLAUDE.md
gotcha 9). SDsettings.jsonkeeps existing keys, so a card-equipped board
loses nothing.
Fixed
- Screen captures were saved upside down. The panel is configured mirror_x +
mirror_y, which looks like it should mean the framebuffer is a 180-degree
rotation of the display — it is not. Those are applied by the ek79007 driver
as a MADCTL command to the panel IC, so the panel corrects its own physical
scan-out and the framebuffer already matches what is on screen. The capture
path was rotating to "undo" a mirror that had already been undone. - Screenshot downloads did nothing in Chrome. The endpoint was correct
throughout — curl pulled a byte-exact PNG from it, and Safari saved it fine —
but the response was sent with Transfer-Encoding: chunked and therefore no
Content-Length, which Chrome's download manager can silently discard. Files
here are 20-45 KB, so they are now read into PSRAM and sent in one call,
which sets Content-Length automatically; the chunked path is kept as a
fallback above 512 KB. - The screenshot button was drawn on top of the settings gear. It was created
in a different file, on a different parent, in a different coordinate system
(the LVGL top layer has no padding, the status bar has 4 px), so re-slotting...
v1.2.0 — Camera QR Wi-Fi provisioning on both board revisions
Camera QR Wi-Fi provisioning now works end to end on both ESP32-P4 board
revisions — point the on-board camera at your router's Wi-Fi QR code and the
analyzer fills in the SSID and password for you.
On the newer ESP32-P4X (silicon rev 3.x) the camera had never streamed at all.
This release fixes that, along with a UI freeze, and adds optional front-panel
hardware keys.
Highlights
Camera QR provisioning works on rev-3 silicon. The scanner reached
VIDIOC_STREAMON and failed there on every P4X. Two independent causes:
- esp_video 1.4.1 left
esp_isp_awb_config_t.subwindowuninitialised and
passed stack garbage to the ISP driver. Only chip revision ≥ 3.0 validates
that field — rev < 3 warns and ignores it — so the bug was invisible on the
older board and fatal on the newer one. Fixed by moving to esp_video 2.3.0
(2.2.0 carries Espressif's fix). - The generated ISP tuning table was never actually linked.
esp_ipa's
prebuilt library ships a placeholder built from a dummy sensor config, and
its build files let that placeholder win the link — so white-balance gains
were never set for any sensor. The project build now strips it at CMake
configure time.
The display no longer appears frozen after a scan. The camera's control bus
and the touch controller share the same two I2C pins. Starting the camera took
those pins, and teardown reset them outright, leaving touch and audio dead
until the next reboot while the screen kept drawing. The pins are now handed
back on teardown.
Real error messages. A failed scan used to report a bare "Scanner stopped"
— the actual reason was discarded before it could reach the screen. Each stage
now reports what to check, e.g. "No camera detected. Check the MIPI-CSI camera
module is connected and fully seated in its connector."
Optional front-panel keycaps. Support for up to two Seeed Grove-Mech
Keycaps — mechanical keys with RGB status LEDs, wired to the J1 header. They sit
on the GPIO matrix rather than the contended I2C bus, so they keep working while
touch is suspended for a camera scan.
- Key 1 — cycles the colour theme; aborts a running QR scan instead; a 2 s
hold restarts the board. - Key 2 — cycles the spectrum display mode.
- Each LED shows the colour of what its key currently selects, so the panel
reports state without waking the screen.
Wiring, pin rationale and a 3.3 V-only warning are in
hardware-setup.md. The feature compiles out cleanly if you
don't fit the hardware.
A bigger, undistorted QR preview — 640×420, aspect-preserved instead of
squashed, and the scanner no longer starves the UI or blocks it while stopping.
An idle scan now gives up after 45 seconds rather than holding the camera open.
Known limitation
The camera can only be started once per restart. esp_video 2.3.0's teardown
reports success but does not unregister the CSI video device, so a second scan
in the same session fails. The QR screen now detects this up front and tells you
to restart rather than opening a camera that cannot work. A scan that
successfully decodes a code reboots to join the network anyway, so the normal
path is unaffected.
Upgrade notes
espressif/esp_videomoves 1.4.1 → 2.3.0, which also pulls
esp_cam_sensor2.3.0 andesp_ipa2.2.0. Do not pin below esp_video 2.2.0
on rev-3 silicon.- Sensor-format Kconfig keys were renamed upstream
(CAMERA_SC2336_MIPI_RAW8_*→CAMERA_SC2336_MIPI_DEFAULT_FMT_RAW8_*). The
effective capture format is unchanged. - Two build environments remain, one per silicon revision:
pio run -e esp32-p4-evboardfor the v1.5.2 board (chip rev 1.x) and
pio run -e esp32-p4x-evboardfor the P4X v1.6 board (chip rev 3.x).
tools/check_chip_rev.pyblocks flashing the wrong image. - No settings migration is required.
Housekeeping
Around 130 lines of dead code removed — leftovers from three earlier refactors
(SD noise-floor persistence, spectrum pinch-zoom, and a Phase-1 status-bar stub)
where the replacement landed but the old path was never deleted. No behaviour
change.
Full detail in CHANGELOG.md.
First Public Release
### ESP32-P4 Spectrum Analyzer v1.0.0
First Public Release
I’m pleased to publish the first public release of the ESP32-P4 Spectrum Analyzer.
This project has grown far beyond its original goal of experimenting with the ESP32-P4. It has become a complete embedded audio measurement instrument featuring real-time DSP, multiple visualization modes, touchscreen operation, USB Audio support, microphone calibration, persistent settings, and an integrated browser-based management interface.
One of the goals throughout development has been to build something that feels like a real instrument rather than simply another FFT demonstration.
⸻
Highlights
Real-Time DSP
- FFT sizes from 512 to 16384 points
- Multiple window functions
- Multiple averaging modes
- Noise-floor capture and subtraction
- Microphone calibration support
Display Modes
- Spectrum Analyzer
- Waterfall
- Oscilloscope
- Mirror Display
- VU Meter
- 1/3 Octave Analyzer
Audio Sources
- ESP32-P4 onboard ES8311 codec
- USB Audio Class (UAC1)
- Runtime Left / Right / L+R selection
User Experience
- Touchscreen interface
- Pinch zoom
- Named presets
- Persistent settings
- SD card configuration
- Wi-Fi provisioning
Embedded Web Interface
The analyzer includes a responsive web interface with:
- Dark and Light themes
- Wi-Fi configuration
- Microphone calibration upload
- Remote configuration
- System status
No additional software is required.
⸻
Project Background
This project began as a personal engineering challenge while I was undergoing treatment for Acute Myeloid Leukemia (AML).
During months in hospital, I wanted to keep learning, solving engineering problems, and designing embedded systems. What started as a way to stay mentally engaged gradually evolved into a complete embedded measurement instrument.
Open source has taught me a great deal over the years, and releasing this project is my way of giving something back to the engineering community.
I hope others will find it useful, learn from it, improve it, and build upon it.
⸻
Looking Ahead
Development continues.
The next major milestone is already planned:
Version 2.0 – Distributed Stereo Analyzer
The long-term vision is to connect two ESP32-P4 analyzers into a synchronized measurement pair.
Planned capabilities include:
- Primary / Secondary operating modes
- Stereo channel separation
- Low-latency network audio streaming
- Preset synchronization
- Automatic discovery and pairing
- Synchronized displays
⸻
Thank You
If you try the project, I’d genuinely enjoy hearing about your experience.
Bug reports, suggestions, discussions and pull requests are always welcome.
Sometimes engineering isn’t just about building hardware—it can also be a way of getting through difficult times. This project is a reminder of that for me.
If you find the project useful, please consider giving the repository a ⭐ on GitHub. It helps other people discover the project and encourages continued development.
⸻
Apache 2.0 License