# Implementation roadmap This document tracks the implementation and hardware-validation plan for the ESP32 Serial Swiss Army Knife. It describes intended sequencing rather than a release schedule. Completion requires implementation, documentation and build validation, plus relevant hardware tests or an explicitly recorded user waiver. A waiver is not a hardware-test pass. ## Status legend - **Complete** — implemented and accepted, with target-hardware validation or an explicit phase-specific user waiver recorded below. - **In progress** — implementation or validation is actively underway, but the overall phase is not complete. - **Implemented; validation pending** — code is present and builds, but the current implementation still needs the listed hardware checks. - **Planned** — accepted project direction, not yet implemented. - **Under evaluation** — useful candidate whose feasibility, security, or resource cost must be measured before it becomes a commitment. ## Project-wide design constraints These constraints apply across all phases: - UART0 remains the trusted physical administration and recovery console. - UART1 and the MAX3243 provide the single physical RS-232 data port. - All serial transports use the central session broker: one writer, multiple observers. - A slow or failed client must not block UART reception or unrelated clients. - Serial data paths remain binary-transparent; transports must not reserve in-band control sequences. - Network serial access must be authenticated and encrypted. No unauthenticated plaintext TCP serial service is planned. - Secrets must not be exposed by routine status output, command completion, or logs. - Resource use must remain bounded and observable on the ESP32-S3 N16R8 target. - New work must preserve USB CDC and UART0 recovery when Wi-Fi or a network service fails. - Project code remains `GPL-3.0-only`; third-party license notices and compatibility must be preserved. ## Phase overview | Phase | Scope | Status | |---|---|---| | 0 | RS-232 hardware characterization and diagnostics | **Complete** | | 1 | Persistent UART1 serial-service foundation | **Complete** | | 2 | Transport-neutral session broker | **Complete** | | 3 | Native USB CDC-ACM serial transport | **Complete** | | 4 | Wi-Fi station/AP foundation and network diagnostics | **Complete** | | 5A | Authenticated HTTPS administration foundation | **Complete** | | 5B | Offline xterm.js WebSocket serial terminal | **Complete** | | 6 | Authenticated SSH serial transport | **Complete** | | 7 | Local display and button interface | **Complete** | | 8 | Role-based users and administrative access | **Complete** | | 9 | Small intermittent-use security baseline | **Complete (user signoff 2026-09-18; new hardware check waived)** | | 10 | Simple admin web firmware upload | **Complete (explicit user acceptance 2026-09-18; upload and normal operation verified)** | | 12 | Dual-stack networking and local service discovery | **Complete (explicit user validation; fresh boot and full client mix at 230400 baud)** | ## Completed phases ### Phase 0 — RS-232 hardware characterization Implemented and hardware-validated: - Central board pin profile for the ESP32-S3 N16R8 and MAX3243 breakout. - MAX3243 enable, static driver, receiver, valid-voltage, and modem-signal checks. - UART loopback across supported framing and baud-rate combinations. - Hardware CTS transmit gating and RTS receive-backpressure tests. - Exclusive RS-232 port ownership and fault-safe cleanup. - RGB status indication for diagnostic state. - Phase 0 hardware diagnostics, including signal status, are isolated under the `debug` CLI submenu. Detailed procedures are in [Electrical tests](electrical_tests.md) and [Hardware wiring](wiring.md). ### Phase 1 — Serial-service foundation Implemented and hardware-validated: - Persistent, versioned UART configuration with safe defaults. - UART1 lifecycle management and exclusive ownership. - Nonblocking RX and TX software streams. - Baud rate, data bits, parity, stop bits, RTS/CTS, DTR policy, and RTS threshold controls. - Runtime counters and safe restart after working-configuration changes. - Explicit save/load/default/reset behavior; edits are not silently persisted. ### Phase 2 — Session broker Implemented and hardware-validated: - Up to eight generation-safe broker clients. - Exactly one writer lease with multiple observers. - Bounded per-client output and event queues. - Slow-observer isolation and drop accounting. - Writer grant, release, denial, revocation, and forced administrative reassignment. - DTR `on-connect` integration. - Partial-transfer and retryable no-progress semantics for transports. - Broker payload storage placed preferentially in PSRAM while FreeRTOS control structures remain in internal RAM. ### Phase 3 — Native USB CDC-ACM Implemented and hardware-validated: - Native ESP32-S3 USB CDC-ACM serial transport. - Automatic broker connection when the host opens the port with DTR asserted. - Opportunistic writer acquisition with observer fallback. - Binary-transparent bidirectional serial data. - Host line-coding visibility for diagnostics; UART1 remains under explicit serial configuration control. - Disconnect cleanup, bounded buffering, counters, and UART0 lifecycle controls. ### Phase 4 — Wi-Fi foundation Implemented and hardware-validated: - Four persistent station profiles with priority ordering. - WPA2/WPA3 mixed mode and WPA3-SAE-required mode. - Explicit no-echo secret entry through UART0. - AP policies `off`, `fallback`, and `always`. - Random persistent fallback-AP password and MAC-derived default SSID. - Bounded profile attempts, DHCP deadlines, failover, and retry backoff. - Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`. - Wi-Fi/lwIP payload allocation configured to prefer PSRAM with capacities pinned in `sdkconfig.defaults`. Post-validation enhancements implemented; regression validation pending: - Edits to disabled station profiles are staged without restarting the active radio; enable-state changes and edits affecting enabled radio policy apply asynchronously. - Configurable STA-only `sak-.local` mDNS hostname with an independent versioned NVS record, live hostname updates, automatic withdrawal/restoration across STA IPv4 transitions, and nonfatal failure isolation. The responder initializes at most once and keeps its task stack internal while general metadata prefers PSRAM. A WPA3-required profile correctly rejects an AP that does not advertise the required Protected Management Frame capability; mixed mode is the compatible choice for such an AP. ### Phase 5A — Authenticated HTTPS foundation Implemented and hardware-validated: - HTTPS-only service on TCP port 443 using ESP-IDF mbedTLS. - Persistent random shared `admin` credential, later retained only as migration/recovery material by Phase 8. - Persistent device-specific ECDSA P-256 certificate and private key. - Physical-console credential retrieval, rotation, certificate inspection, rotation, and recovery. - Explicit failure behavior that preserves UART0, USB, serial, and Wi-Fi recovery paths. - Credential rotation revocation hooks shared with WebSocket and SSH sessions. ### Phase 5B — Offline browser terminal Implemented and hardware-validated: - Vendored xterm.js and FitAddon assets; no Internet or CDN dependency. - CSP-compatible page structure with scripts served as separate resources. - Authenticated status and one-time WebSocket-ticket APIs. - Binary WebSocket serial frames and broker writer/observer integration. - Two bounded browser-terminal sessions. - Writer request/release controls and role indication. - Bounded TLS/socket behavior and slow-observer isolation. - Viewport-constrained terminal fitting without recursive page growth. - Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software. Post-validation enhancement implemented; browser regression validation pending: the combined Connect/Disconnect control explicitly closes the terminal WebSocket and pauses automatic reconnect until Connect is selected. ### Phase 6 — Authenticated SSH serial transport Implemented and target-hardware validated: - wolfSSH on TCP port 22 with a separate persistent ECDSA P-256 host key. - Two bounded interactive shell/PTY sessions; no `exec`, file transfer, forwarding, or subsystems. - Authenticated, binary-transparent broker forwarding with opportunistic writer acquisition and observer fallback. - Bounded handshake attempts/deadlines, receive work, buffers, and a single wolfSSH owner task pinned to CPU 1. - UART0 lifecycle, session, counter, and host-key administration. - PSRAM-preferred allocations with internal fallback and internal task stacks. - Bounded broker/WebSocket work, software mbedTLS AES, and software wolfCrypt AES/SHA to avoid the validated shared-hardware-crypto/PSRAM watchdog failure while retaining ESP-IDF SHA/MPI acceleration. - Concurrent USB, WebSocket, and SSH operation validated with USB and SSH alternately holding the writer lease. ### Phase 7 — Local display and buttons Implemented and target-hardware validated: - Optional 128×64 SSD1315-compatible I²C OLED on GPIO11/12 and active-low buttons on GPIO10/13/14; assignments remain centralized in `board_pins.h`. - Display service with a static framebuffer, bounded dirty-page commits, and separate yellow status rows 0–15 plus blue content rows 16–63. - Low-priority status/control task built from copied public snapshots; no service lock is held across I²C and the UI never becomes a broker client. - Overview, RS-232, broker, and network/service pages with no credential or key material. - Confirmation-protected local controls for service lifecycle, Wi-Fi reconnect/profile rotation, writer release, display off, and reboot; the UI cannot assign a writer or edit secrets/configuration. - Persistent dim/off settings, bounded boot animation, consumed wake presses, stuck-button quarantine, and one-shot confirmations. - Missing, disconnected, or unresponsive display hardware remains nonfatal and can recover through one bounded reprobe. - Concurrent serial, UART0, USB, WebSocket, SSH, and injected display/button fault behavior validated. See [Electrical tests](electrical_tests.md) and [Hardware wiring](wiring.md). ### Phase 8 — Role-based users and administrative access — Complete Implemented and accepted: a bounded, persistent user system replaces the single shared network credential. Both roles can use the authenticated web serial/status interface. Over SSH, `user` routes to the broker-backed serial stream while `admin` routes exclusively to the administration shell and receives no broker client or writer lease. UART0 remains the physical recovery and bootstrap authority. Completed implementation (8A–8C target-hardware validated; 8D explicitly accepted at 8D.22 on 2026-09-13): 1. **Phase 8A — User database and UART0 administration — Complete** - Versioned NVS database for up to eight `user`/`admin` accounts, random account IDs and authentication generations, final-admin protection, and three Ed25519/P-256 keys per account. - Salted PBKDF2-HMAC-SHA256 password verifiers, bounded no-echo entry, one-time generated passwords, and secret-free account/key status. - Historical migration/bootstrap supported the initial cutover. Current firmware instead commits missing storage empty, provisions the first admin with normal UART0 `user add`, and provides UART0-only unavailable-database recovery; legacy credential/bootstrap commands are removed. - Migration, bootstrap, CRUD, persistence, and command basics target-hardware validated; the full fault-injection matrix remains regression coverage. NVS remains unencrypted and offline guessing remains possible. 2. **Phase 8B — Role-aware HTTPS and SSH authentication — Complete** - HTTPS and SSH authenticate through the common database and copied secret-free principals; unavailable user storage fails closed. The initial HTTPS Basic path was replaced by cookie authentication in 8D. - One-time principal-bound WebSocket tickets and ongoing principal-currentness checks prevent stale admission or input. - Account mutations request targeted WebSocket/SSH revocation; authentication generations provide fail-safe invalidation while unrelated accounts remain connected. - Password/key login, ticket behavior, targeted revocation, recovery, and concurrent transport operation are target-hardware validated. Legacy credentials were subsequently removed without changing established TLS identity or valid users. 3. **Phase 8C — SSH administrative shell — Complete** - Authenticated `admin` SSH shell sessions route to a bounded administration worker and never create a broker client or acquire a serial writer lease. Normal `user` sessions retain the existing broker-backed serial stream. - UART0 and admin SSH now submit complete lines to one fixed-length request queue. A single dispatcher task is the sole caller of ESP-IDF's non-reentrant `esp_console_run()` and therefore executes the same registered command handlers for both entry routes. The former separately implemented reduced SSH command dispatcher has been removed. - The worker uses fixed per-session command/input and output buffers. Queue records contain copied secret-free principals and generation-tagged session tokens; late work is discarded after disconnect, slot reuse, role change, password/key mutation, or deletion. Task-local standard streams route canonical handler output into the applicable bounded SSH ring, and only the SSH owner task calls wolfSSH APIs. - Transport-neutral bounded prompts now support interactive user passwords/keys and Wi-Fi secrets over admin SSH without exposing hidden input or allowing another command while a prompt is active. Ping callbacks enqueue typed bounded events and the dispatcher alone formats their output. Four-entry per-session history and whole-line Tab completion are RAM-only and wiped on disconnect. - Authenticated administrators receive the operational registry, including HTTPS material rotation/reset, reboot, ping, and SSH lifecycle/session/host-key mutation. Self-terminating reboot and SSH actions are deferred until acknowledgement output drains, block further shell input, and execute through existing synchronous owner APIs from a separate bounded control task. First-admin provisioning and explicit `user recover --force` remain physical-UART0 operations (`user bootstrap` was later removed); admin SSH also rejects generating a replacement password for its own account. - `ssh sessions` and `ssh counters` identify broker versus admin-console routes, worker command state, queued admin output, admission failures, and input backpressure. `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort application-buffer draining. Admin sessions are checked for a current `admin` principal before command execution and during the active-session reconciliation. - Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled. - Target-hardware validation passed for route separation, history/Tab editing, interactive visible/hidden prompts, output/backpressure, generated and entered user/password/key management including the longest ECDSA P-256 import, ping event routing, deferred reboot/SSH lifecycle drain behavior, bootstrap/recovery rejection, targeted self/other-user revocation during queued work, UART0/SSH administration serialization, and concurrent USB/WebSocket/user-SSH/admin-SSH operation. Stress at 460800 baud with SSH and WebSocket clients caused substantial expected packet drops and slower display controls, but did not exhaust memory or require lowering the supported baud-rate range. 4. **Phase 8D — Integrated web administration — Complete** - User explicitly signed off tested firmware at **8D.22 on 2026-09-13**, superseding earlier per-slice pending acceptance/review gates. Cookie login/logout replaces Basic; bounded digest-only sessions, same-origin/CSRF checks, principal currentness and targeted revocation protect serial and admin routes. - Admin-only Serial/Admin selection and Settings preserve connected terminal observation and serial writer ownership. The browser shell shares the serialized UART0/admin-SSH dispatcher, with bounded output, explicit admission and retained frontend restrictions—not unrestricted shell parity. - Typed Serial, Accounts/password/authorized-key, Network/Wi-Fi/mDNS, Display, Broker and SSH settings; confirmed HTTPS stop/restart/reboot and HTTPS/SSH identity rotation. Canonical owners compare reserved service/identity/target generations, preserve commit/failure semantics and expose no stored secrets. Admitted work is not cancelled by later logout/timeout; uncertain mutations are never automatically replayed. - Accessible Serial/Wi-Fi/client/writer contextual controls reuse existing controllers. Writer transfer is explicit and generation-safe; one writer, isolated observers, binary transparency, UART0 administrative recovery and network-independent native USB UART1 remain invariants. - Scope removed by user: **8D.15** dedicated typed network diagnostics (shell diagnostics retained) and the unimplemented **8D.19** ordinary browser-session/native-USB controls (SSH controls retained). Browser identity reset/recovery/export is excluded; canonical recovery remains available. - Acceptance evidence and legacy storage compatibility are condensed below. [Web administration](web_administration.md) retains current API/owner/recovery contracts; [regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) retain practical checks, not claims of execution. #### Phase8 acceptance evidence **8D.22 signoff, 2026-09-13:** the user explicitly accepted the tested firmware, superseding earlier per-slice target, review and integration gates for the retained scope above. Earlier scoped acceptance covered browser login/logout, shared administration, Serial/account/Network presentation and legacy cleanup. Prior host regression suites and independent reviews were reported passing; this consolidation asserts no new build or test execution. Prior final Phase 8 build: **PASS, 100,556 B linked RAM / 1,828,573 B flash, CPU 160 MHz**. The combined binary WebSocket-send fix was separately accepted at **160 MHz / 230400 baud with the full client mix including browser admin**; retain combined send and bounded failed-send isolation, not the earlier frequency-only experiment. Ordinary HTTPS idle cleanup was accepted, not every possible admission failure or owner delay. Latest reported Phase 8 telemetry (bytes): | Sample / capability | Free | Minimum free | Largest block | |---|---:|---:|---:| | Boot internal 8-bit | 59,808 | 58,840 | 31,744 | | Boot internal DMA | 52,052 | 51,084 | 31,744 | | Boot PSRAM | 8,196,968 | 8,183,972 | 8,126,464 | | Loaded after burst, internal 8-bit | 31,508 | 2,052 | 18,432 | | Loaded after burst, internal DMA | 23,752 | 460 | 18,432 | | Loaded after burst, PSRAM | 8,136,624 | 8,065,972 | 7,995,392 | Loaded SSH minimum-free stack: **15,028 B**. This sample had two SSH sessions (serial/admin), two serial WebSockets and USB, with SSH as writer. Browser admin had been used then closed; it was **not active in this capture**. Web send/queue/protocol and SSH IO errors were zero; one SSH handshake failure and one session revocation were reported without an established cause. **Limits, not reopened acceptance gates:** missing latest broker/serial counters prevent an exact zero-drop or byte-integrity inference. No latest browser-admin-active full-mix, individual fault-injection, exact duration, soak, cleanup-cycle or reserve-floor result is inferred. Internal/DMA lifetime minima remain a nonblocking transient-headroom follow-up, not approved reserves or proof of allocation failure: capability pools overlap and per-region minima may be non-simultaneous, so do not sum them. HTTPD/dispatcher stack margins, correlated peaks and allocation-reserve policy remain unmeasured/unapproved here. SDK TLS `-0x004C` means NET_RECV_FAILED, not an OOM diagnosis; stale cookies as the cause of two boot authentication failures remain unconfirmed. #### Phase8 legacy credential compatibility Cleanup was accepted **2026-09-08**, with unchanged certificate fingerprint and usable preexisting test users. This is not an unreported provisioning, recovery, power-loss or all-key pass. - **Users:** missing `user_db/database` is committed as an empty v1 database, with no legacy account/password import. First-admin provisioning is physical UART0 `user add admin` (optional `--generate`); entered passwords use hidden confirmation and generated passwords appear once. UART0-only `user recover --force` rebuilds only an unavailable database empty, refuses healthy databases (including empty ones), and leaves other configuration and TLS/SSH identities untouched; follow with normal `user add`. - Valid existing v1 user bytes load without rewrite or changes to accounts, roles, IDs, authentication generations, verifiers or authorized keys; migrated `user` accounts are not promoted. Private `v1_admin_marker` preserves the old wire byte and is derived from admin count on mutation, not a new role/schema/public bootstrap state. Public bootstrap and legacy migration/synchronization APIs are removed; final-admin protection, conditional mutations, principal currentness and targeted revocation remain. - **TLS:** at unchanged `web_sec/material`, a private reader validates the **1,392-byte v1** layout and migrates to **1,340-byte TLS-only v2**, preserving exact private-key DER, certificate DER, SHA-256 fingerprint and material generation—not rotating identity. The candidate is validated and committed before publication; temporary credential-bearing input is wiped. Malformed/unknown material, read/crypto failures and migration write/commit failures fail closed without regeneration or overwrite; truly missing material may be generated normally. - `web credentials show`, `web credentials rotate --force` and `user bootstrap` are removed. `web certificate info` / `web certificate rotate --force` retain frontend policy. `web reset --force` replaces TLS identity only, not accounts/passwords/verifiers; replacement/restart may close browser sessions and require renewed certificate trust/login, without revoking unrelated SSH sessions. No public legacy credential reader, display, rotation or authentication path remains. - **Downgrade/security:** older v1-only firmware cannot read v2 HTTPS storage; user-database v1 compatibility does not make HTTPS downgrade safe or restore removed credentials. Logical NVS replacement is **not secure flash wiping**: historical plaintext legacy credentials may remain, Wi-Fi secrets and TLS/SSH private keys remain unencrypted, and PBKDF2 verifiers permit offline guessing. No erase is required or authorized by cleanup. - Prior host tests used NVS fault doubles and a generated identity wrapped in the historical layout, **not an independently captured old-device fixture**; they do not prove flash durability or power-loss behavior. Focused runners: `tests/web_security/run.py`, `tests/admin_console_boundary/accounts.py`, `tests/admin_ssh_policy/run.py`. See [legacy-removal regressions](user_administration_tests.md#current-legacy-removal-regression-procedure). ### Phase 9 — Security and production hardening **Complete — user signoff 2026-09-18; new hardware check explicitly waived.** Reduced scope: an intermittently powered wireless serial cable on a trusted network, not a permanently exposed Internet service. The earlier extensive Phase 9 implementation was abandoned in favor of the accepted pre-Phase-9 baseline. Short exposure reduces risk; it does not make upstream defects harmless or certify this firmware secure. Small implementation: - Keep the baseline PlatformIO/ESP-IDF and managed-library versions and upstream sources. No generated dependency patches, custom SSH state machine, crypto-policy replacement, allocator hooks, or SDK migration in this phase. - Explicitly disable core dumps and select silent panic reboot through standard `sdkconfig.defaults` options. Keep ordinary logging; reduced crash diagnostics are a deliberate trade-off. Existing generated configurations must be checked because defaults do not override saved selections. - Follow the [operational checklist](security_operations.md): unique credentials, trusted identity verification, no Internet forwarding, networking off/power off after use, and existing rotation/recovery procedures. - Review upstream maintenance releases occasionally and before planned upgrades. Evaluate reachable issues against actual use; any stock dependency upgrade is a separate build/device-tested task, not a reason to accumulate local backports. Preserve existing license notices. **Excluded:** secure boot, NVS/flash/PSRAM encryption, eFuse changes, partition changes, exhaustive CVE/zeroization audits, new authentication throttling, and production certification. Existing authentication, session isolation and recovery behavior remain unchanged. There is no protection against physical extraction or replacement of firmware. Phase 10 uses administrator-selected firmware, without a separate image-signing system. **Host/build evidence (2026-09-18):** normal `pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the original 20241119 toolchains: **94,196 B linked RAM / 1,828,565 B flash**. Generated configuration confirms no dumps and silent panic reboot; compilation inputs contain none of the abandoned Phase 9 source overlays or wolf crypto guards. No device operation or target validation was performed. **Acceptance (2026-09-18):** the user signed off this small Phase 9 and explicitly waived a new whole-phase hardware check of boot, UART0 recovery, native USB serial, HTTPS/SSH login and normal serial sharing. Application code is unchanged, **but crash-diagnostic defaults changed**; this is a waiver, not evidence of target or panic-path validation. The prior build/configuration evidence above remains the validation record; no new hardware test was performed. A separately controlled panic check with synthetic secrets remains optional, not a completion gate; do not add a production panic endpoint or erase flash. Disabling dumps does not erase old contents. ### Phase 10 — Simple admin web firmware upload **Complete — explicit user acceptance on 2026-09-18: firmware upload works and normal operation is verified.** The admin **Settings → HTTPS / Reboot** firmware card uploads the locally built `.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`. Install this OTA-enabled firmware by wire first, then use web application uploads. There is no release server, automatic download or update service. [Firmware update and recovery](#phase10-update-and-recovery) below covers operation; [regression guidance](#phase10-regression-guidance) covers future checks. Implemented scope: - Raw `POST /api/firmware` requires exact `Content-Type: application/octet-stream`, known exact `Content-Length`, cookie, same-origin Origin and `X-CSRF-Token`; no multipart/JSON, Basic authentication or ordinary `user` access. Reject queries, ambiguous headers, chunked transfer and `Expect`; UI code leaves browser-managed headers alone. Admission precedes body/flash work; session/principal currentness is rechecked before boot selection. The card confirms upload/reboot and shows progress/results without automatic retries. HTTPD capacity is 40 method/path handlers. - Standard ESP-IDF OTA APIs (`esp_ota_begin`, `esp_ota_write`, `esp_ota_end`, `esp_ota_set_boot_partition`) stream one upload through a 4 KiB buffer into the inactive slot (two 4 MiB app slots). A reboot task with a 2 KiB stack is preallocated before erase. Lifecycle/identity reservations and ordinary software-reboot gating exclude competing work; no custom flash protocol or whole-image RAM buffer. - Accepts an ESP32-S3 application binary for this board/layout, not an ELF, ZIP, merged full-flash image, bootloader or partition-table image. Browser hints require a nonempty `.bin` of at most 4 MiB; the server bounds raw length against actual destination capacity before trusting HTTPD's narrowed length. Checks completeness, target/header and exact SDK image length/integrity, including mandatory appended SHA-256, before selection. The digest is not publisher authentication; neither it nor filename checks prove board compatibility or a working application. - Before selection, rejected/incomplete uploads and receive/write/validation failures abort any live OTA handle without selecting the candidate; the inactive slot may be erased/partially written. Boot-metadata failure (`firmware_commit_failed`) needs inspection and carries SDK transactional uncertainty. `200 {"ok":true,"rebooting":true}` means validation/selection succeeded; successful synchronous send schedules restart after 500 ms, retaining reservations, but proves neither browser receipt nor boot success. Response failure after selection schedules no restart, releases reservations for manual reboot and latches further admissible uploads to 409 `firmware_selected_reboot_required`; the latch survives HTTPS stop/start until device reset. - Synchronous HTTPD receive/flash blocks other HTTPD work: browser sessions can stall/drop. The 120-second total receive-loop and ten-second stall checks use the existing one-second socket timeout, not an absolute deadline: synchronous SDK erase/write/validation and scheduling are not preempted. The browser's 180-second timeout cannot cancel committed work. UART0/native USB remain independent paths, not guarantees of uninterrupted serial timing during flash; reboot interrupts all transports. Software exclusion cannot prevent physical reset, power loss or panic. - **Preserve NVS:** write only the inactive application slot and the OTA selection metadata. Do not erase the chip, rewrite the partition table/bootloader, or touch `nvs`, `nvs_key`, PHY, storage or other data partitions. Existing users, passwords, Wi-Fi/serial settings and HTTPS/SSH identities remain stored, as with an application-only wired update without erase. This preserves stored bytes; the uploaded firmware must still understand the existing schemas and must not itself erase/migrate them incompatibly. - Keep wired USB-to-UART flashing documented as recovery if the uploaded application does not boot or no longer serves the web UI. Basic image validation cannot prevent a valid but broken application from requiring wired recovery. **Not in scope:** secure boot, image signatures/signing-key management, anti-rollback/version-downgrade enforcement, automatic rollback/post-boot health-confirmation machinery, remote release discovery, partition migration or NVS backup/restore. Use upstream OTA support without dependency patches. The administrator is responsible for selecting trusted, compatible firmware, including when deliberately installing an older build. **Historical integration build and host evidence (parent-reported):** `pio run` **PASS**, **94,220 B linked RAM / 1,847,645 B flash**. Against Phase 9's 94,196 B / 1,828,565 B, this is **+24 B RAM / +19,080 B flash**. Final backend 88 cases plus the pinned SDK begin/abort failure-contract test, UI 169 groups, server lifecycle 44, admin transport 25, console lifecycle, SSH runtime and cookie lifecycle checks passed. Additional cookie base/admin/display/lifecycle, HTTPD idle cleanup 18, and SSH management/runtime/security checks passed; the extra cookie `--admin` run initially lacked a reboot symbol in its fixture, corrected in test-only code before passing. Host doubles are not target evidence. The later concise-UI fix was copy-only; its latest reported UI regression passed all **169 groups**, with no rebuild after the text change. The build figures above remain historical; this documentation update ran no build or tests. **Acceptance (2026-09-18):** after the firmware upload implementation and concise-UI fix, the user confirmed: “That works perfectly. And the usual operation is also verified.” Phase 10 is complete by this explicit acceptance of working upload and verified normal operation. Do not infer specific fault-injection, NVS before/after comparisons, power-loss or wired-recovery passes. The [regression guidance](#phase10-regression-guidance) below is reusable future guidance, not an acceptance blocker. #### Phase10 update and recovery 1. Keep **USB-to-UART** available for UART0 administration/flashing; native USB CDC is network-independent UART1 access, not administration. Install the updater by wire first: older firmware without the route cannot install its own first web updater. The custom OTA layout is required; [one-time old-layout migration](../README.md#one-time-migration-from-the-default-partition-table) is separate and destructive. 2. Build trusted, schema-compatible `esp32-s3-devkitc-1-n16r8` firmware with `pio run`; choose **`.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`**, never a merged image or a renamed non-application file. Observe the [downgrade warning](../README.md#legacy-credential-removal). 3. Save desired RAM-only settings and record nonsecret configuration/public HTTPS/SSH fingerprints. Arrange a maintenance window and stable power/network; sign in as `admin` over trusted HTTPS, open **Settings → HTTPS / Reboot → Firmware update**, choose the file and confirm **Upload and reboot…**. 4. Wait for validation/reboot, not just 100% transmitted bytes. Restore network reachability, reload and explicitly sign in; verify the running application, saved configuration and identities before another upload. 5. A lost response, timeout, page close, sign-out or browser abort proves neither cancellation nor failure. The UI locks uncertain outcomes without replay; reload clears only browser locks, not the device latch. Inspect through UART0 and deliberately reboot if appropriate: any later reset can boot an already selected image. 6. For initial wired installation or recovery when boot/HTTPS fails, connect USB-to-UART, select a known-good compatible checkout, run `pio run --target upload`, then `pio device monitor -b 115200`. **Do not erase for ordinary updates or recovery**; there is no automatic recovery flashing/rollback. Replies: **400** malformed/incompatible/incomplete input; **401/403** authentication/Origin/CSRF or `admin_required`; **408** `firmware_timeout`; **409** selected-image latch; **413/415** destination capacity/content type; **500** `firmware_write_failed`/`firmware_commit_failed`; **503** busy/unavailable/resources. Unread rejected bodies close rather than drain; bounded secret-free JSON may never reach a disconnected browser. #### Phase10 regression guidance These are reusable checks, **not recorded passes or outstanding acceptance gates**. Record only performed outcomes; host flash/network/scheduling doubles are not hardware evidence. Focused host suites: `python3 tests/web_firmware_update/run.py` and `python3 tests/web_ui_session/run.py` (the SDK begin/abort case covers a live handle published on erase failure). - Exercise wired-first install, known-good upload/reboot/new build and explicit reconnect; compare saved users/roles, password/key login, Wi-Fi/serial/display/hostname settings and public HTTPS/SSH fingerprints without recording secrets. - Interrupt/cancel receive; reject corrupt, truncated, wrong-target, non-application and oversized inputs without unintended selection/restart, then deliberately perform a valid update. Reject unauthenticated/ordinary-user, wrong-Origin and missing/wrong-CSRF requests before flash. Never attempt power interruption at commit without wired recovery ready. - Exercise competing upload, HTTPS lifecycle/identity and ordinary reboot exclusion; HTTPD requests may wait rather than promptly return busy. With controlled post-selection response-failure injection, verify no automatic restart, subsequent 409 (also after HTTPS restart), then manual reboot; an arbitrary disconnect does not establish this fault case. - Observe web stalls/drops and serial/network recovery, single-writer isolation, UART0 recovery and native USB UART1 access without networking; demonstrate known-good wired recovery without erase and recheck configuration/identities. Do not claim uninterrupted traffic. ## Current and planned phases **Phases 8, 9, 10 and 12 are complete** for their accepted scopes. Phase 9 includes the explicit new-hardware-check waiver above; Phase 10 includes explicit user acceptance of upload and normal operation. Phase 12 includes explicit user validation and the fresh-boot/full-client evidence below. Optional features must not weaken completed serial and recovery paths. General release gates below guide future work, not claims that every fault, soak, recovery or reserve measurement was performed for completed phases. ### Phase 12 — Advanced network integration **Complete by explicit user validation:** “I just validated Phase 12 successfully.” Accepted baseline: - Dual-stack local access and DNS-SD discovery: advertise available HTTPS and SSH services using the shared `sak-.local` hostname, with appropriate IPv4 `A` and IPv6 `AAAA` records. Preserve IPv4 access and verify IPv6 support throughout HTTPS, WebSocket and SSH. Clients choose address-family preference and fallback; DNS-SD cannot mandate IPv6 preference. Certificate-name integration and trust remain separate concerns. - Send the configured device hostname (`sak-`, without `.local`) through DHCPv4 Host Name option 12 so a suitably configured DHCP/DNS server can publish the lease address in its own DNS zone. Set the STA netif hostname before DHCP starts, define how hostname changes reach subsequent DHCP exchanges, and verify the transmitted option and server-side DNS registration. DNS publication and the DNS domain remain server policy; mDNS does not provide this integration. - IPv6 baseline: link-local addressing and SLAAC-provided ULA/global addresses, with mDNS/DNS-SD for local discovery; no DHCPv6 requirement. Preserve link-local interface scope and keep advertisements synchronized with usable addresses and service availability. Registration of SLAAC addresses in infrastructure DNS is a separate optional feature, outside this baseline; SLAAC does not provide automatic hostname registration. - Validate IPv4-only, IPv6-only and dual-stack operation, including reconnects, address changes and service lifecycle changes. Discovery remains local-link by default; preserve UART0 and native USB recovery regardless of network state. The device is not intended to become a general-purpose router. Captive-portal interception, unauthenticated DNS redirection, NAPT, and a plaintext serial listener remain out of scope unless the project requirements are explicitly revised. #### Implementation and operational boundaries - The Wi-Fi owner installs the full configured hostname before STA connection and updates it after hostname edits. DHCPv4 option 12 uses `sak-`, not `.local`. Rename does not force DHCP restart: the next DHCP exchange carries the new name, and DNS registration/cache cleanup remain server policy. Existing hostname limits and NVS formats are unchanged. - STA enables SLAAC and IPv6 link-local creation; either IPv4 or a preferred IPv6 address establishes `ONLINE`. **Link-local-only counts as online**, prevents IPv4-only timeout/failover, and can retire the fallback AP after the existing stability interval. This indicates local address availability, not Internet reachability or a default route. Web, CLI and OLED distinguish absent IPv4 from IPv6 availability. A diagnostic follow-up adds up to three preferred numeric IPv6 addresses to the snapshot, copied and cleared with the same observation; `wifi status` and browser Network settings print them in link-local/ULA/GUA groups; overview/OLED status retains availability flags. The settings JSON is bounded to 2304 bytes, with no extra netif calls on HTTPD. - HTTPS/WebSocket use IDF's existing dual-stack listener. SSH explicitly uses one dual-stack listener, retaining the existing two-session limit. Bracketed IPv6 HTTPS authorities are canonicalized and bound to the existing Host/Origin/session checks; scoped IPv6 literal URLs are rejected. Prefer the `.local` hostname for link-local browser access, subject to client resolver support. No certificate regeneration, new trust mechanism, or automatic IPv6 preference is introduced. - STA-only mDNS advertises `_https._tcp:443` and `_ssh._tcp:22` according to listener availability, without TXT metadata. Record and address-family changes converge through the existing Wi-Fi owner, including offline reconciliation; service setters do no component work. The responder is not restarted for ordinary changes. Initialization failure remains latched/nonfatal. - mDNS 1.12.0 exposes no readiness acknowledgement and can silently drop queued family actions. Absent families are disabled on each one-second owner pass; available families are re-enabled/reprobed on a 30-second repair cadence. Upstream synchronous calls can delay this cadence. There can be a transient stale/zero-A response window after IPv4 loss before disable is processed. AAAA records follow upstream **valid-address** semantics, including deprecated-but-still-valid addresses, not preferred-only filtering. Client caches expire independently. - `cmake/mdns_membership.*` applies one version/hash-guarded, build-local source overlay to mDNS 1.12.0: balance per-family multicast leaves and unwind joins after PCB creation failure. Managed sources and dependency versions remain unchanged. An upstream mismatch fails configuration for deliberate review; see `tests/mdns_membership/README.md` for maintenance/removal and regression evidence. This is not the abandoned Phase 9 patch set. - `sdkconfig.defaults` explicitly enables IPv4/IPv6 and increases mDNS service capacity from one to two. Existing saved configurations override defaults: verify `CONFIG_LWIP_IPV4=y`, `CONFIG_LWIP_IPV6=y`, `CONFIG_MDNS_MAX_SERVICES=2` and STA-only predefined mDNS interfaces before building. The local N16R8 saved configuration was updated accordingly. No new task, transport socket, broker slot, serial buffer, partition, filesystem or generated web asset was added. - Existing diagnostic limits remain: hostname ping selects the resolver's first usable result (not Happy Eyeballs), scoped link-local ping is not newly supported, and traceroute remains IPv4-only. DHCPv6, infrastructure DNS registration of SLAAC addresses, and new IPv6 resolver provisioning are outside this baseline. #### Validation evidence and acceptance `pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the guarded mDNS source compiled: **94,396 B linked RAM / 1,853,025 B flash**, +176 B RAM / +5,380 B flash versus the recorded Phase 10 build. These are static link sizes, not runtime heap or stack headroom. Initial target compilation rejected lock-free bool atomics; the implementation now uses short portMUX sections. Existing SDK Kconfig notes remain. Host checks passed: Wi-Fi/address/hostname reconciliation, mDNS lifecycle/family repair, actual patched multicast-membership functions and CMake guards, SSH dual-stack/management/runtime/security, HTTPS authority parser (689 cases on host and actual installed lwIP parser), cookie authorization variants, Network settings, browser UI/CSP (171 groups), HTTPS lifecycle (45 groups and eight status projections), firmware upload (88 cases plus SDK contract), broker diagnostics, and session-store/serial integration. Network-settings host fakes were updated after the new netif dependency exposed a compilation failure; the rerun passed. These are not packet-level or hardware evidence. Follow-up after the user's live status report: both preferred-address flags were `yes`, while plain client `ping` selected IPv4. Source verification confirms the flags require actual preferred addresses; this is not evidence of failed SLAAC or verified IPv6 reachability. `wifi status` now prints the addresses for diagnosis. Latest follow-up `pio run` passed: **94,444 B linked RAM / 1,853,685 B flash** (+48 / +660 versus the initial Phase 12 build). Browser Admin console now permits canonical Wi-Fi commands, including disruptive edits and hidden prompts, as explicitly requested. Focused policy/browser Wi-Fi prompt, address snapshot/rendering, Network/cookie and UI regressions passed. The broader console-boundary suite passes its console/certificate stages but has a pre-existing SSH-adapter fixture compilation failure for missing `web_firmware_update_reserve_reboot`; no production change was made to hide it. Latest address-display build: `pio run` **94,444 B linked RAM / 1,854,485 B flash**; Network API/owner checks and **174 browser groups plus CSP** passed. These remain build/host results, separate from the user's device evidence below. ##### User-reported device acceptance User explicitly validated Phase 12 after confirming numeric IPv6 ping/HTTPS, Avahi AAAA resolution and normal IPv6 hostname access following the Fedora `authselect` mDNS correction. The earlier hostname failure was a client resolver configuration issue, not missing device IPv6 addresses. The acceptance capture adds: - **Fresh boot:** HTTPS, SSH and mDNS running without reported startup failures; zero SSH sessions, serial stopped, no broker clients, USB attached but not host-open. The configured UART profile is **230400 baud, 8N1, RTS/CTS, DTR active, RTS threshold 96**. - **Full client mix:** two authenticated public-key SSH sessions with IPv6 ULA peers (one serial observer, one admin console); two serial WebSockets (one writer, one observer); one active browser admin WebSocket; native USB host-open as an observer. Four broker clients, exactly one writer, all reported pending/event counts zero. Serial running with RX-available/TX-pending zero, CTS asserted and valid RS-232 voltage at observation time. - SSH: two successful handshakes, zero handshake/authentication failures or timeouts, zero stream I/O failures and zero session revocations. Stream counters **RX 68 / accepted 65 / rejected 3 / TX 161,209 bytes**; two broker writer revocations were reported. The rejected bytes are retained as evidence, not silently described as zero loss or assigned an unverified cause. - Web serial: two connections, **27 accepted RX frames/bytes**, zero rejected RX frames/bytes; **782 binary TX frames / 318,230 bytes**, 11 control frames / 948 bytes. Send/queue/protocol/close failure counters zero. Two writer requests were denied; the final broker snapshot still shows one writer. Browser admin: one connection, **106 RX / 3,721 TX bytes**, zero send/queue/protocol/authorization/backpressure failures. No response errors reported. - HTTPS and SSH both report running, not transitioning, `ESP_OK`; mDNS reports expected announcement and `ESP_OK`. Expected announcement alone is not packet-level service-record proof; the earlier successful Avahi lookup separately establishes the reported AAAA lookup. All heap figures are bytes, copied from the user's observations: | Observation | Heap capability | Free | Lifetime minimum-free | Largest block | |---|---|---:|---:|---:| | Fresh boot | Internal 8-bit | 65,880 | 64,912 | 31,744 | | Fresh boot | Internal DMA | 58,124 | 57,156 | 31,744 | | Fresh boot | External PSRAM | 8,196,732 | 8,187,980 | 8,126,464 | | Full client mix | Internal 8-bit | 35,408 | 8,528 | 22,528 | | Full client mix | Internal DMA | 27,652 | 772 | 22,528 | | Full client mix | External PSRAM | 8,111,952 | 8,072,612 | 7,995,392 | SSH owner stack: **20,480 B** configured, **18,476 B minimum-free** at fresh-boot observation and **16,284 B minimum-free** under the full mix. **Evidence limits:** minimum-free is the conservative sum of matching heap regions' lifetime minima, not a simultaneous free-space measurement or guaranteed allocation reserve. Internal 8-bit and DMA capabilities overlap and must not be added as independent pools. The **772 B DMA lifetime minimum remains a headroom watch item**, not an observed allocation failure or acceptance blocker. Boot output contains two TLS `-0x004C` receive errors and unauthenticated-request failures; the capture does not establish their cause or connect them to memory exhaustion. Command snapshots are non-atomic; differing cumulative request/authentication counts must not be treated as one aligned measurement interval. No UART overflow/per-observer drop counters, soak duration, byte-for-byte capture integrity, exhaustive fault injection, DHCP packet capture/server-zone update, or every address-family/service transition is established by this excerpt. User acceptance is recorded without inventing those passes. ##### Reusable regression guidance The following procedures are retained for future regression testing, **not outstanding acceptance blockers or a claim that every item was executed**. Keep UART0 and USB recovery available: 1. On IPv4, IPv6-only and dual-stack STA networks, exercise HTTPS login, serial/admin WebSockets, SSH user/admin access and one-writer/observer isolation. Test hostname access plus IPv4 and unscoped ULA/GUA IPv6 literals; verify SSH host keys and HTTPS identity rather than trusting discovery. 2. Capture DHCP DISCOVER/REQUEST option 12 at boot, rename/renew and reconnect, including a maximum-length suffix. With a configured DHCP/DNS server, verify the resulting zone entry and its server-controlled update/removal behavior. 3. Browse `_https._tcp` and `_ssh._tcp` and inspect A/AAAA/SRV answers over both families. Stop/restart each service; rename; drop/reacquire DHCP while IPv6 survives; change/deprecate/expire RA prefixes; disconnect/reconnect. Confirm eventual withdrawal/restoration, valid-address AAAA semantics and no persistent zero-A response. Exercise repeated family transitions and, where practical, missed-event/action repair. 4. Check link-local-only readiness and the fallback-AP transition explicitly. Confirm the client preserves interface scope; do not infer browser link-local success from `ONLINE` alone. Test multicast filtering/AP isolation separately from firmware address readiness. 5. Repeat the previously accepted 230400-baud mixed USB/WebSocket/SSH workload with discovery and address changes. Capture current/minimum internal, DMA and PSRAM availability, task stack margins, UART overflow and per-client drop counters. No new runtime memory reserve or maximum-baud performance guarantee is claimed. Phase 12 is complete by the explicit user validation above. This acceptance update changes documentation only; the agent did not run a new build, test, upload, erase or device operation. ## Cross-phase release gates Every phase should satisfy the following before being marked complete, with any user-waived check explicitly recorded in that phase rather than reported as passed: 1. A clean release build for the ESP32-S3 N16R8 target. 2. No new compiler or project diagnostics attributable to the change. 3. Focused automated tests where practical and documented hardware tests where hardware behavior is involved. 4. UART0, native USB CDC, and previously completed network transports still operate. 5. Broker writer/observer semantics and binary transparency remain intact. 6. Failure, disconnect, stop/start, reboot, and stale-session cleanup paths are exercised. 7. Runtime memory, stack, socket, and queue bounds are measured under maximum supported concurrency. 8. Secrets remain absent from ordinary status output and logs. 9. [Command reference](command_reference.md), [Hardware wiring](wiring.md), electrical tests, and this roadmap are updated as applicable. 10. New dependencies receive security, maintenance, and GPL-3.0 compatibility review. ## Explicitly deferred work The following are not implemented merely because flash partitions or library support exist: - NVS, flash, or PSRAM encryption. - Secure boot or production eFuse provisioning. - Automatic OTA downloads, image signing, post-boot health confirmation and automatic rollback. The simple admin upload is complete in Phase 10; these advanced update features remain deferred. - Core-dump collection or secret-safe core-dump processing. - Filesystem mounting. - SFTP, SCP, SSH `exec`, forwarding, or subsystems. - General routing, NAPT, captive-portal interception, or unauthenticated serial access. Deferring these features is intentional: each changes the security model, recovery behavior, or bounded-resource assumptions and therefore requires its own design and validation gate.