Repository navigation
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
the status row could not move it. Both now come from one factory —
ui_widgets.c — that owns slot geometry, size and colour, and the row is laid
out on a single 62 px pitch. -
Recorded dates showed 1980-01-01 06:34 instead of "unknown". With no RTC,
time() returns seconds-since-boot, so FAT records the 1980 epoch plus the
uptime — values hours past midnight, which sailed straight through a
"reject <= 1980-01-01 00:00" check. The threshold is now 2021. -
Status-bar readouts were unreadable in the High Contrast theme. Five labels
(SPL, Peak, DSP info, ambient and USB indicators) hardcoded bright colours
chosen against a dark bar; on High Contrast's light0xC8D8E8status bar the
SPL readout sat at roughly 1.2:1 contrast. Root cause was that the colours
were written at widget-creation time and the theme switcher only ever
repainted the status background and the title, so nothing else could follow a
theme change even in principle. Every scheme now names its own status
accents, applied from one place. -
The display-mode label kept the previous theme's text colour after a theme
switch — the same bug from the other direction: it was built from the palette
but never repainted. -
screen_spectrum_set_color_scheme()recoloured "every child of the screen
from index 2", silently claiming anything later added to the screen. It now
addresses the frequency tick labels directly. -
display_ui.hdocumented the LVGL lock as non-recursive. It is recursive
(xSemaphoreTakeRecursive); the rule that matters is never to block while
holding it. The old wording steered callers away from a safe pattern. -
net_mgr_add_network()wiped the whole entry when re-saving a network, so
changing a password silently discarded that network's static IP.