Skip to content

Releases: JFG3rd/SpectraLab-P4

v1.3.2 — Themed list panels, tofu glyphs removed, layout polish

Choose a tag to compare

@JFG3rd JFG3rd released this 10 Aug 13:40

Fixed

  • The SSID and saved-network lists stayed stock white in every colour scheme.
    lv_list is not an lv_obj subclass 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

  • .gitignore now 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

Choose a tag to compare

@JFG3rd JFG3rd released this 10 Aug 12:10

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 in GET /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 raw idf.py path,
    which skips tools/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.
  • Home button beside Back on 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 shared ui_theme module 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.
  • Back sits 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_t grew 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.json keeps every existing value. Expected, not a bug.

v1.3.0 — Screenshots, SD file browser, access-point mode and a real clock

Choose a tag to compare

@JFG3rd JFG3rd released this 10 Aug 12:10

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_mgr gains net_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, or POST /api/screenshot. Files land in
    /sdcard/spectrum/screenshots/ as shot-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 and settings.json are 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/spectrum at 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-editing settings.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_profile records 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>.local and the
    raw address appear on the Wi-Fi screen, GET /api/status gains hostname
    and url fields, and the landing page renders it as a bookmarkable link.

  • panel_button gains 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_handlers raised from 16 to 24. This release adds six routes, and
    esp_http_server drops 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_t gains active_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). SD settings.json keeps 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...
Read more

v1.2.0 — Camera QR Wi-Fi provisioning on both board revisions

Choose a tag to compare

@JFG3rd JFG3rd released this 07 Aug 14:21

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.subwindow uninitialised 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_video moves 1.4.1 → 2.3.0, which also pulls
    esp_cam_sensor 2.3.0 and esp_ipa 2.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-evboard for the v1.5.2 board (chip rev 1.x) and
    pio run -e esp32-p4x-evboard for the P4X v1.6 board (chip rev 3.x).
    tools/check_chip_rev.py blocks 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

Choose a tag to compare

@JFG3rd JFG3rd released this 07 Jul 11:13

### 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