Add IPv6-aware Wi-Fi state, HTTPS/SSH listeners, mDNS service reconciliation, and browser Wi-Fi administration. Include a guarded build-local fix for mDNS 1.12.0 membership handling, focused regression suites, and Phase 12 acceptance documentation.
Phase 12 mDNS owner host regression
Run python3 tests/mdns_phase12/run.py from the repository root. Uses the host C11 compiler and temporary fake SDK headers; compiles the actual production module. No PlatformIO, device operations, or generated firmware assets.
Covers notifications before initialization and while the project mutex is held; default unavailable SSH; fixed HTTPS443/SSH22 and empty TXT; repeated starts without duplicate records or hostname churn; record add/remove failures and retry; offline withdrawal; offline rename and retry; latest-state coalescing; init/hostname/instance failure latches and cleanup. Family tests cover lost IPv4 with surviving IPv6, IPv6-only/link-local readiness, no valid IPv6, netif down, stop with stale addresses, missed GOT_IP6, explicitly failed and silently dropped action submission, delayed upstream disable, missing STA netif, and no enable churn during ordinary healthy polls. TCP/IP fakes assert address reads occur in TCP/IP context; netif actions assert neither project mutex nor publication critical section is held. Actual multicast, component tasks, DNS conflicts, and sockets are not simulated.
Integration contract
- Listener owners call
void mdns_service_set_https_available(bool)/void mdns_service_set_ssh_available(bool)after successful listener creation and on unavailability. Calls use tiny portMUX critical sections, safe before initialization, with no blocking semaphore, allocation, callbacks, or component calls. Xtensa does not promise lock-free C11 atomic bool, so no atomics are required. - The sole Wi-Fi owner calls
esp_err_t mdns_service_reconcile(void)periodically, even offline. No new arguments are needed: it looks up the permanentWIFI_STA_DEFnetif and samples addresses viaesp_netif_tcpip_exec. No project mutex or publication mux is held across SDK/component calls. - Reconciliation never initializes the responder. Before module init it returns invalid-state; after module init but before responder startup it is a successful no-op. Once initialized it applies listener states and family readiness, retrying failed operations on subsequent passes. Notifications converge on a later pass, not synchronously; an in-flight pass can briefly reflect an older state.
- Existing start/reannounce also reconcile. Start remains gated by usable STA in either family. Stop now clears announcement expectation and requests family disable, even if netif still holds nonzero addresses; it does not destroy/reinitialize the responder. Errors are retained in the snapshot. Do not call lifecycle operations concurrently or while holding another service lock. The manager owns start/stop, not event callbacks.
- Required effective SDK options remain
CONFIG_MDNS_PREDEF_NETIF_STA=y, AP/ETH predefined interfaces disabled,CONFIG_LWIP_IPV4=y,CONFIG_LWIP_IPV6=y,CONFIG_MDNS_MAX_SERVICES >= 2. No configuration change was made for the repair.
Exact dependency contracts and limitations
Inspected installed Espressif mDNS 1.12.0 and ESP-IDF 5.5.0:
mdns_netif.cinitializes IPv6 fromesp_netif_get_ip6_linklocal, independently of IPv4. Its predefined handlers process disconnect and address acquisition, but not STA_LOST_IP.mdns_networking_lwip.ctracks family readiness separately from current addresses.mdns_send.cchecks readiness before emitting A/AAAA, but can emit zero A if readiness remains true after IPv4 loss. Explicit family disable repairs that stale readiness, including when IPv6 survives.mdns_send.cusesesp_netif_get_all_ip6, whose IDF implementation includes valid addresses, including deprecated addresses, excludes invalid/tentative/zero addresses, and reads lwIP state directly. Our sampling therefore runs in TCP/IP context and intentionally follows valid-address semantics, not preferred-only semantics. No address cache is passed to mDNS; it still reads addresses when serializing records.mdns_netif_actionis asynchronous, has no public readiness getter or acknowledgement, andpost_custom_actionreturnsESP_OKeven if action enqueue fails. A cached successful API return is not proof of applied state. Allocation errors retry on the next pass; silent losses require reassertion. Missing-family disables are idempotent and reasserted every poll. Present-family enables restart probes, so they are submitted on family-mask transitions and at a 30-second repair cadence, not every healthy interval. This also repairs late upstream disconnect actions and failed internal PCB initialization. There is no responder restart or service-record churn.- Public-only control cannot simultaneously guarantee acknowledged readiness and absolutely no periodic enable reprobes. Healthy families re-probe every 30 seconds as the explicit tradeoff. Recovery from silent dropped enables is on a subsequent repair cadence once the component queue/network resources make progress, not a hard deadline under sustained failure. Missing-family disable retries each poll. Polling, concurrent network changes, and component queuing leave a transient window in which stale/zero A responses can still escape; this is convergence, not an atomic packet-level filter. No upstream patch is included.
mdns_responder.cadds/removes records synchronously under its own mutex. Hostname setting waits for the component worker; instance setting queues work. Work per pass is one bounded address scan, at most one family action, and two record decisions; no additional tasks, queues, dynamic application storage, or retry loops. This is not a hard wall-clock guarantee for upstream blocking calls.
Target validation
In addition to the host suite, the updated src/mdns_service.c was compiled to a temporary object using its exact .pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json command and the installed Xtensa compiler / IDF 5.5 headers. Compilation passed. No full PlatformIO build, link, or hardware validation was performed.