src.nth.io/

summaryrefslogtreecommitdiff
path: root/TESTING.md
AgeCommit message (Collapse)AuthorFilesLines
3 daysdocs: prune TESTING.md to its forward-looking contentLuke Hoersten1-681/+168
The 730-line milestone-by-milestone bring-up log served its purpose — every milestone (M1-M9) is verified on hardware, and the per-run narratives are preserved in git history (last full version: 8eebc0e). What survives, restructured (~230 lines): - New-unit bring-up: hardware prereqs (all unknowns resolved), the flash -> panel -> touch -> Scrypted sequence, and the panel failure-mode signals table (the content you actually need the day a second viewport gets provisioned). - Regression recipes: condensed curl checks for config validation (incl. the 54-char name cap), frame push, state machine, OTA acceptance, and device-initiated POSTs. - Outstanding tests: the honestly-never-run backlog (mid-frame cable pull, 24h soak, frame storm, brown-out, multi-viewport). - Performance playbook: kept, with example log formats refreshed to the current stream instrumentation (the old examples showed the removed pre-stream snapshot path) and thresholds updated to the post-tuning baseline (24fps painted, pend_age as the backlog tripwire). README: pointers at TESTING's old structure updated; the Hardware status row no longer claims schematic confirmation is pending (those unknowns were resolved during bring-up — only the per-unit flash-size check remains).
3 daysdocs: mark M9 verified — live stream shipped as raw TCP :81, not POST /streamLuke Hoersten1-5/+7
M9 still carried the abandoned chunked-HTTP design and a pending status long after the raw TCP data-socket stream was verified at painted = sent = 24 fps (recv/decode task split d1c8d45 + TCP window/EMAC tuning). Rewrite the milestone to describe what shipped, flip it ✅, and bump the README's "M1 – M8" line to M9.
3 daysdocs: sync README + TESTING + scrypted README with v1.4.0 realityLuke Hoersten1-3/+9
Drift accumulated across the review-fix, mDNS-discovery, and tooling work: - Discovery: the design-era "use a Node mDNS library" flow is replaced with what shipped — the sandbox has no such libraries; the script does a dependency-free legacy-unicast dgram browse (no :5353 bind), host dropdowns, blank-name inheritance, and auto-heal on register failure (matched by the new mac TXT record) instead of periodic re-browsing. - /state example: add mac, ota_state, panel dims, tear_guard_engaged, temp_c, and the stream stats object; correct `configured` semantics (scrypted URL only) and the never-null MAC-derived name in /config. - /config: document the 54-char name cap (mDNS label limit). - scrypted/README: add-device + settings tables gain the discovered-host dropdown, Wake triggers on create, and the canonical "Viewport name" rename field (the Scrypted pencil doesn't propagate — by design); trigger-scoped subscriptions; device-wake immediate-204 semantics; idle=0 no longer force-slept by the safety timer; "manual IP" dropped from limitations; discovery section reframed with the diagnostic.ts probe and bridge-networking caveat. - Source map: dedupe the doubled touch.{h,c} row, add stream_server / ota / chip_temp rows, fix nvs_config + endpoint-count drift. - Build/TESTING: point iteration at `make ota` (USB stays for first flash); TXT expectations include mac; DHCP-renumber failure mode now documents the verified auto-heal path.
2026-06-20phase 6: performance review playbook in TESTING.md + UDP-vs-TCP rationale in ↵Luke Hoersten1-0/+91
stream_server Two pieces of documentation, neither changes behavior: 1. TESTING.md gains a "Performance review playbook" section: per- session capture commands for firmware serial + /state poll + Scrypted console, an annotated walkthrough of what each log shape means, an investigation-threshold table mapping user-facing symptoms to likely causes and the first thing to check, and a one-line tools list. Replaces the implicit "ask the maintainer how to debug" loop with a reproducible workflow. 2. stream_server.c gains a "Why TCP and not UDP" header comment documenting the analysis from the design phase. JPEGs are ~123 IP datagrams at 1500 MTU; on hardwired Gigabit LAN switch-fabric loss is < 1e-9/packet → per-frame corruption ≈ 1.2e-7. UDP's theoretical wins (no Nagle, latest-wins semantics) don't apply because TCP_NODELAY is on, socket.write p50 < 1ms, and the FIONREAD trick already implements latest-wins on the receive side. UDP's costs (200-400 LOC of app-layer fragmentation, loss of nc/curl debug, FIONREAD trick stops working under fragmentation) are real. Documented as reference so a future contributor doesn't re-derive the analysis from scratch.
2026-06-14Drop VIEWPORT_STATE_UNCONFIGURED — state is just awake/asleepLuke Hoersten1-13/+6
\`state\` now reports only the screen's runtime state (awake or asleep). Whether a viewport is set up to talk to Scrypted is a separate \`configured\` flag, derived from \`viewport_name && scrypted_url\`. There's no third state. Behaviour changes: - POST /state always succeeds; the previous 409 "device unconfigured" path is gone. The screen toggles regardless of /config status. - POST /config now sets \`configured\` directly from the derived predicate instead of mutating the state enum. - Outbound state-client POST to Scrypted is still gated on a scrypted URL being present — that's the only thing the configured flag now actually controls in the runtime path. GET /state JSON unchanged in shape, but \`state\` is now never "unconfigured" — that's reported through the existing \`configured\` boolean instead.
2026-06-14TESTING: M5 ✅ verified 2026-06-14Luke Hoersten1-2/+2
2026-06-14Info screen: full /config + /state dump, replaces identity screenLuke Hoersten1-30/+19
Previously the 4-line "identity" screen showed only viewport name, mDNS host, IP, and state. Expanded to 15 lines covering the full GET /config + GET /state output (name, host, ip, state, configured, scrypted, orientation, brightness, idle, fw, uptime, frames, errs, free heap, free PSRAM), label/value pairs left-aligned with auto-scaled font. Renames "identity" → "info" throughout — symbol, log messages, README/TESTING references. Also: - Move the info-screen render's big locals (~1.6 KiB: lines[16][80] + scrypt[256] + vp_name[64]) to BSS. The touch task's 3 KiB stack was overflowing on every long-press, leaving whatever frame was previously on the panel — gave the appearance of a "blue screen" after the M5 test pattern. - Drop the ≥5 s touch factory-reset gesture and remove all stale references to it in docs and comments. NVS wipe is now a USB-side `idf.py erase-flash` operation only. - M5 follow-up TODO in jpeg_decoder.c: solid green renders ~black and solid blue renders green; not a byte-order issue (BGR setting turns red into blue). Tracked as a known M5 gap.
2026-06-14http_api: bump httpd stack to 8 KiB — POST /config was overflowingLuke Hoersten1-4/+4
POST /config has ~2.4 KiB of stack locals (2 KiB body buffer + the scrypted URL + viewport name + cJSON parser frames) which overran the default 4 KiB httpd task stack and tripped the stack-protect canary mid-handler. The request body was applied to RAM + NVS, but the handler crashed before sending the response, so curl saw a connection reset and the device rebooted into "Stack protection fault". M4 + M6 ✅ verified 2026-06-14 after the bump: - POST /config full / partial → 204; survives reboot via NVS - 5 validation failure modes → 400 - POST /state wake/sleep → 204; idempotent repeats → 204 - POST /frame while asleep → 409 with expected body - idle timer fires after idle_timeout_ms; 0 correctly disables
2026-06-14TESTING: M3 / M7 / M8 ✅ verified 2026-06-14Luke Hoersten1-12/+12
Hardware bring-up of the Hosyond panel, FT5426 touch, and local screens are all confirmed working on the Waveshare ESP32-P4-ETH. Also retire the resolved hardware unknowns (DSI pin count, I²C pins, BOOT button) — they're answered in the body now. M4 / M5 / M6 still need ethernet + a curl-driven run to flip.
2026-06-14M3 WIP: DSI bring-up + state machine + identity overlayLuke Hoersten1-20/+26
DSI / panel: - LDO_VO3 acquired at 2500 mV (VDD_MIPI_DPHY) — without this the PHY PLL busy-waits forever inside esp_lcd_new_dsi_bus. - PSRAM bumped to 200 MHz (CONFIG_IDF_EXPERIMENTAL_FEATURES) to keep the DPI fed without underruns. - Pi-7"-style 800x480 panel init (REG_POWERON, PORTA/PORTB/PWM/PORTC) + 16-bit RGB565 framebuffer with portrait-mode rotation buffer in PSRAM. Currently shows garbled output — known issue, next commit switches to the TC358762-bridge-aware init sequence. State + UX: - viewport_state simplified to AWAKE/ASLEEP only; "unconfigured" is a flag, not a state. Tap always toggles; content choice (identity vs frame) is driven by the configured flag. - BOOT button arms a 15 s identity overlay via local_screens_overlay; expired callback returns to prior state. - Boot-done indicator: two backlight flashes (no usable LED GPIO on the Waveshare board — GPIO 35 BOOT is shared with EMAC TXD1). - Best-effort subsystem init in app_main; display init deferred to its own task so a panel hang can't block networking. Display test pattern: solid R/G/B for 2 s each on boot to characterize the garble independent of text rendering.
2026-06-14M1: link-loss & recovery verified 2026-06-14Luke Hoersten1-1/+11
Real production failure mode for PoE-only deployments: a switch reboot or briefly down VLAN yanks Ethernet without cutting power. Confirmed the device handles it cleanly (no panic, no reset, no state loss) and re-acquires DHCP on link-up. Serial trace from the bench: W (748316) net_eth: link down # cable pulled I (916316) net_eth: link up, mac e8:f6:0a:e0:90:94 # plugged back in I (924816) net_eth: got ip 10.0.13.83 ... # DHCP renewed ~8.5s later Same boot throughout — uptime kept climbing past the disconnect, no ESP reset. The HTTP server stayed bound and the mDNS responder task kept running through the outage; both resumed serving the moment DHCP came back. free_heap and free_psram identical to pre-disconnect (no leak from the cycle). TESTING.md M1 entry now captures this with the actual log line sequence. The integration-suite "Cable pull mid-frame" item gets a ✅ for the idle/no-Scrypted variant; the actual mid-frame Scrypted variant still pending.
2026-06-14M1 + M2 hardware-verified 2026-06-14 (Waveshare ESP32-P4-ETH)Luke Hoersten1-6/+12
First end-to-end bench verification. Board is the Waveshare ESP32-P4-ETH (Amazon B0FN7JQ2V8), 32 MB PSRAM, 32 MB flash silkscreen, ESP-IDF v5.4.1. No panel attached, just board on USB power + LAN. What passed ----------- M1 — Ethernet driver up; DHCP lease landed at 10.0.13.83 (gw 10.0.13.1, netmask 255.255.255.0). MAC e8:f6:0a:e0:90:94. 31.7 MB PSRAM free. Boot completes in ~4s after the kernel handoff. M2 — GET http://10.0.13.83/state returns the full spec JSON shape (name=null, configured=false, state=unconfigured, resolution=480x800, ip=10.0.13.83, free_heap=520995, free_psram=31730048, all counters at 0). mDNS browse from macOS (dns-sd -B _scrypted-viewport._tcp local.) finds one instance named "viewport" within ~100ms. Bare-board behavior (Ethernet unplugged) verified too: boot completes cleanly with summary line `boot complete — subsystems [EMHdJ-B] ip=(no link)`. Lowercase d = display down because no panel attached; - for touch because it shares the panel I²C bus. README status snapshot updated to reflect M1+M2 ✅ on hardware; TESTING.md M1 and M2 sections gain the 2026-06-14 verification note plus the three fixes that bring-up exposed (RXD0/RXD1 swap, mDNS hostname order, elegant subsystem degradation — already shipped in commit 220ee4c).
2026-06-14Identity screen: name + hostname + IP + state, auto-scaledLuke Hoersten1-6/+15
local_screens_show_ip() now renders four lines instead of two — all the device's runtime identity at once: line 1: viewport name ("mudroom" / "viewport" if unconfigured) line 2: mDNS hostname ("viewport-mudroom.local" / "viewport.local") line 3: IP ("192.168.1.42" / "no network") line 4: state ("awake" / "asleep" / "unconfigured") Same screen is shown on first boot, after factory reset, and as a 15s BOOT short-press overlay — operator always has the device's full identity one button-press away (find it on the LAN, confirm name + configuration without curling). Font scale is auto-picked: the largest integer scale (1×–6×) where the longest line fits within 90% of width AND all four lines plus inter-line spacing fit within 90% of height. Works for both portrait (480×800) and landscape (800×480) without separate code paths. Font expanded to cover the new strings: - All lowercase a–z (added b, f, h, j, k, m, q, s, u, x, y, z). - Punctuation: dash, slash (digits, period, colon already covered). Fallback policy unchanged: unsupported chars render as blank, which keeps the table tight and the failure mode visible rather than crashing. README "Local rendering" + TESTING.md M8 updated to describe the four-line identity layout and show both the unconfigured-boot and configured-overlay example outputs. Build verified clean against ESP-IDF 5.4 (binary ~873 KB, +1.6 KB for the expanded font).
2026-06-14Consolidate docs: merge DESIGN.md into README; sharpen status + roadmapLuke Hoersten1-5/+90
DESIGN.md was ~80% redundant with README.md (project summary, hardware, API contract, callback contract, scrypted integration, milestone acceptance criteria) and ~20% implementation guidance that's now better served by the source code itself + TESTING.md. Removed it and folded the unique parts into README. README.md additions: - "Status" table at the top: firmware, Scrypted side, hardware — each with where-we-are + what's-pending columns. - "Firmware implementation notes" section: source map (one-line per main/*.{h,c} module), memory strategy, display strategy, error handling, coding standards. Replaces DESIGN.md sections 7-11, 15, 17. - "What's next" section before Philosophy: nine items in priority order, each linking the specific TESTING.md or scrypted/ section. Makes "pick up cold" trivial. - Philosophy section absorbed DESIGN.md's "Guiding Rule" as a blockquote at the bottom. - "Related docs" rewritten — TESTING.md is now described as *the* self-contained verification reference. TESTING.md additions: - Preamble now describes the five-layer structure and the ⬜/🟡/✅ legend. - New "Status snapshot" table — milestones + code + HW at a glance. - New "Hardware prerequisites" — table of what to confirm/order, plus a numbered list of the four open unknowns (DSI FPC pin count, I²C jumper destinations, BOOT GPIO, flash silkscreen). - New "Recommended bench order" — seven stages from "board on Ethernet over USB" through "integration suite," each with the commands or where-to-look. DESIGN.md deleted. Single source of truth is now README.md (spec + implementation notes + roadmap) and TESTING.md (verification reference).
2026-06-13M8: local screens (IP + Loading) + BOOT buttonLuke Hoersten1-7/+24
local_screens.{h,c}: - Embedded 8x8 bitmap font sized to a 95-char table; only the glyphs used by today's strings (viewport.local, the IPv4 string, Loading...) are populated. Unsupported chars render blank. - 768 KB PSRAM scratch FB at panel-native dims. - local_screens_show_ip() — two centered lines: "viewport.local" <current IP> Scale 3x portrait / 4x landscape; centered vertically. - local_screens_show_loading() — centered "Loading..." at scale 4x/5x. - local_screens_restore_for_state() — repaint after BOOT-overlay expiry: UNCONFIGURED -> IP screen; AWAKE/ASLEEP -> black FB (next /frame or sleep handles the rest). - All paths go through display_present_rgb565() so orientation rotation is automatic. button.{h,c}: - Polling task at 30ms, active-low with internal pull-up. - Short press (<5s, released) → backlight on, IP overlay for 15s via esp_timer one-shot, then restore. - Hold ≥ 5s → nvs_config_reset() + esp_restart(). - PIN_BOOT_BUTTON = GPIO 0 is a TODO placeholder. ESP32-P4 strap pin GPIO35 is owned by RMII TXD1 at runtime so Waveshare must expose a separate user button on a free pin; confirm against the schematic. Fail-soft if mis-wired: input reads stuck-high and the task never fires. state_machine on AWAKE transition now calls local_screens_show_loading() right after display_wake() — so every wake (tap or POST /state) flashes a clear "Loading..." until the next /frame paints over it. app_main on boot: - Calls local_screens_init() after display_init(). - UNCONFIGURED → paint IP screen (replaces the M3 test pattern). - ASLEEP → backlight off (unchanged). - After all subsystems: button_init(). Build clean against ESP-IDF 5.4 (binary ~870 KB; the font/glyph data is under 800 bytes). TESTING.md M8 documents the visual checks, the BOOT-pin placeholder caveat, the AWAKE-overlay-overwrite behavior, the open follow-up about post-overlay backlight when ASLEEP, and the font fallback policy.
2026-06-13M7: touch + outbound /state POST to ScryptedLuke Hoersten1-14/+53
state_client.{h,c}: - Worker task drains a depth-1 queue (xQueueOverwrite gives the replace-on-full semantics from the spec — in-flight POST is never cancelled; the next queued entry is overwritten by newer state). - esp_http_client POST to <scrypted>/state with Content-Type application/ json, User-Agent ScryptedViewport/<version>, Connection: close, 1s timeout. Body: {"viewport":"<name>","state":"wake"|"sleep"}. - Any non-2xx or transport error increments state_post_failures and is otherwise ignored. Silently drops if no Scrypted URL is configured. touch.{h,c}: - FT5426 capacitive touch on the shared I2C bus at 0x38. - 30ms polling task; tracks down/up transitions, detects taps as down-then-up within 500ms with a 150ms debounce. - On tap, toggles wake/sleep via state_machine_set_local(), which drives the local transition AND fires state_client_post() at Scrypted. state_machine adds state_machine_set_local(): runs the same transition as state_machine_set() then enqueues an outbound POST. Idle-timer expiry now uses this path so Scrypted sees idle-driven sleeps too (the spec's "tighten the race with /frame 409" path stays in place as the fallback). display.h exposes display_i2c_bus(); touch.c uses it instead of re-initializing the same I2C port. app_main starts state_client right after the state machine and starts touch after display init (touch is skipped if display isn't up since they share the bus). CMakeLists.txt: add esp_http_client to REQUIRES. Build clean against ESP-IDF 5.4 (binary ~860 KB; jumped ~210 KB from M6 because esp_http_client + cJSON path pulls in tcp_transport, mbedtls, http_parser). TESTING.md M7: flask-based test receiver, tap dispatch verification, failure-path check (kill receiver, confirm counter increments), queue-coalescing behavior, no-POST-on-Scrypted-initiated, no-POST- when-unconfigured.
2026-06-13M6: state machine — POST /state, idle timer, /frame 409 guardLuke Hoersten1-9/+96
state_machine.{h,c} — central wake/sleep transitions: - state_machine_init() creates the esp_timer one-shot for the idle timer. - state_machine_set(target) is idempotent and atomic. On AWAKE: backlight on, idle timer (re)armed. On ASLEEP: idle timer cancelled, backlight off. Rejects with INVALID_STATE when the device is unconfigured. - state_machine_frame_painted() restarts the idle timer if awake; called by /frame after each successful paint. - Idle-timer callback transitions to ASLEEP. TODO M7 hook: outbound POST {viewport, state:sleep} to <scrypted>/state. http_api.c: - POST /state: parse {state}, accept "wake"/"sleep", reject others 400. Unconfigured device → 409 "device unconfigured". Already-in-state → 204 (idempotent no-op). Successful transition → 204. - POST /frame: 409 Conflict when state != AWAKE. After successful paint, call state_machine_frame_painted() so the idle clock keeps resetting while frames stream. app_main: - Initialize state_machine before http_api so the route handler can drive it from request 0. - After display_init(), reconcile the panel with the boot state: UNCONFIGURED → test pattern (placeholder until M8 IP screen) ASLEEP → display_sleep() so a configured device boots dark AWAKE → leave on (not reached on fresh boot) Disable path: idle_timeout_ms=0 in /config means the timer is never armed and a wake state persists until /state {sleep} or a power cycle. Build clean against ESP-IDF 5.4 (binary ~645 KB). TESTING.md M6 expands with idempotency checks, 409-when-asleep, 409-when- unconfigured, idle-timer firing within idle_timeout_ms+slack, /frame restarting the idle timer, idle-timer disable via idle_timeout_ms=0, and the M7 dependency note about the missing outbound sleep POST.
2026-06-13M5: POST /frame — hardware JPEG decode + orientation-aware paintLuke Hoersten1-7/+52
jpeg_decoder.{h,c} wraps esp_driver_jpeg (ESP32-P4 hardware decoder). - One-time engine + DMA-aligned PSRAM scratch buffer setup (1 MB input, 768 KB output @ panel native 800x480 RGB565). - try_lock + unlock so concurrent /frame POSTs get 503 instead of queueing, per spec. - jpeg_decoder_get_info() reports the dimensions; the http handler validates them against the effective resolution before painting. display.h adds display_present_rgb565(src, w, h): - Landscape: src is 800x480, memcpy 1:1 into the panel framebuffer. - Portrait: src is 480x800, software rotate 90° CW into the 800x480 panel framebuffer. (PPA / 2D-DMA hardware rotation is a later optimization if portrait latency matters.) http_api.c adds POST /frame: - Content-Type must be image/jpeg → else 400. - Empty body → 400. > 1 MB → 413. Display not initialized → 500. - jpeg_decoder_try_lock(0) for concurrency: second post returns 503. - Body streamed into the decoder's input buffer in chunks. - Decode failure or dimension mismatch → 400 + decode_errors++. - Paint failure → 500. - Success → frames_received++, last_frame_us = esp_timer_get_time(), 204 No Content. app_main initializes the JPEG decoder after display_init(). Both are best-effort: failures log a warning and leave the rest of the firmware running. CMakeLists.txt: add esp_driver_jpeg to REQUIRES. Known gap (M6 closes it): /frame currently paints regardless of wake/sleep state. The 409-when-asleep rule lands with POST /state in M6. Build clean against ESP-IDF 5.4 (binary ~640 KB). TESTING.md M5 expanded with portrait/landscape test commands, ImageMagick test-image recipes, the full negative matrix (wrong Content-Type, oversize, wrong dims, concurrent, garbage), and the M6 dependency note.
2026-06-13M4: NVS-backed /config with partial updates + validationLuke Hoersten1-5/+75
nvs_config.{h,c} — persist the runtime config (viewport, scrypted, idle_timeout_ms, orientation, brightness) under a single NVS namespace. nvs_config_load() applies persisted values over the in-RAM defaults on boot and flips state from UNCONFIGURED to ASLEEP once both name and Scrypted URL are present. nvs_config_save() commits the whole record atomically. http_api.c — add GET /config and POST /config: - GET serializes viewport_state to the spec's JSON shape, with null for unset string fields and defaults filled in for the rest. - POST is partial: each field is optional; only present fields are validated and applied. Validation runs on a staged copy and errors short-circuit with 400 + reason before any state mutation, so a rejected request leaves the device untouched. - Validation rules: viewport non-empty <64 chars; scrypted starts with http:// and <256 chars; idle_timeout_ms 0 or >=5000; orientation in {portrait,landscape}; brightness 0..100. - Side-effects fire after the lock + save: brightness change pushes PWM to the panel MCU; viewport/orientation change reapplies mDNS hostname + TXT. - 204 on success; 400 with a single-line reason on validation error. app_main calls nvs_config_load() right after viewport_state_init(), so mdns_service_start() and display_init() see the persisted hostname, orientation, and brightness from the first packet/PWM. Build clean against ESP-IDF 5.4 (binary ~620 KB). TESTING.md M3 now documents the Hosyond jumper wiring (5V/GND/SDA=GPIO7/ SCL=GPIO8 from board to panel header; DSI FPC carries only the high- speed lanes). M4 entry expands the validation matrix and side-effects to verify.
2026-06-13M2: HTTP server + mDNS service discovery + shared stateLuke Hoersten1-0/+284
- viewport_state.{h,c}: shared state struct (config, run-state, counters, orientation, timestamps) behind a FreeRTOS mutex. Modules update it under viewport_state_lock(); GET /state serializes a snapshot. - http_api.{h,c}: starts esp_http_server on :80, registers GET /state. Returns the full JSON shape from the spec (name, version, configured, state, uptime_ms, last_frame_ms_ago, frames_received, decode_errors, state_post_failures, resolution, ip, free_heap, free_psram). - mdns_service.{h,c}: mdns_init + advertise _scrypted-viewport._tcp.local on :80 with version/resolution/orientation/name TXT records. mdns_service_refresh() reapplies hostname + TXT after /config writes (called from M4 onward). - main/CMakeLists.txt: add esp_http_server, esp_timer, json, mdns to REQUIRES. espressif/mdns added as managed component via main/idf_component.yml. app_main calls viewport_state_init early so any module can read defaults before /config arrives. Also adds TESTING.md tracking per-milestone verification status and an integration test plan to run after M9 (races, failure modes, longevity, power cycles, negative protocol, multi-viewport). M1 promoted to "🟡 builds clean" in TESTING.md; combined HW verification of M1+M2 will be one flash session. Build verified against ESP-IDF 5.4 for target esp32p4 (binary ~550 KB).