Compare commits
2
Commits
06bf33b3cf
...
4449131079
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4449131079 | ||
|
|
3feb3b5916 |
@@ -14,14 +14,14 @@ ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one
|
|||||||
|
|
||||||
## Development status
|
## Development status
|
||||||
|
|
||||||
Hardware characterization, the serial core, USB CDC-ACM, Wi-Fi, HTTPS/WebSocket, SSH serial transport, and the local display/control interface are implemented and Phase 7 target-hardware validated. Phase 8A's bounded role-based user database and UART0 administration are complete. Phase 8B's role-aware HTTPS passwords, SSH passwords/public keys, and per-account session revocation are target-hardware validated. Phase 8C adds a bounded administrator SSH shell backed by the same serialized command registry as UART0 and awaits target-hardware validation. See the [Roadmap](docs/roadmap.md) for phase status and validation details.
|
Hardware characterization, the serial core, USB CDC-ACM, Wi-Fi, HTTPS/WebSocket, SSH serial transport, and the local display/control interface are implemented and Phase 7 target-hardware validated. Phase 8A's bounded role-based user database and UART0 administration are complete. Phase 8B's role-aware HTTPS passwords, SSH passwords/public keys, and per-account session revocation are target-hardware validated. Phase 8C adds a bounded administrator SSH shell backed by the same serialized command registry as UART0 and awaits target-hardware validation. Configurable STA-only mDNS naming as `sak-<suffix>.local` is implemented with independent NVS persistence; target-hardware validation is pending. See the [Roadmap](docs/roadmap.md) for phase status and validation details.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Hardware wiring](docs/wiring.md): hardware profile, GPIO assignments, connector guidance, and safety notes.
|
- [Hardware wiring](docs/wiring.md): hardware profile, GPIO assignments, connector guidance, and safety notes.
|
||||||
- [Electrical tests](docs/electrical_tests.md): OLED/buttons, MAX3243, UART loopback, and session-broker verification procedures.
|
- [Electrical tests](docs/electrical_tests.md): OLED/buttons, MAX3243, UART loopback, and session-broker verification procedures.
|
||||||
- [Role-based user database and UART0 administration](docs/user_administration_tests.md): user migration and administration, HTTPS/SSH authentication, and session-revocation validation.
|
- [Role-based user database and UART0 administration](docs/user_administration_tests.md): user migration and administration, HTTPS/SSH authentication, and session-revocation validation.
|
||||||
- [Command reference](docs/command_reference.md): UART0 administration, serial, broker, USB, Wi-Fi, web, SSH, and diagnostic commands.
|
- [Command reference](docs/command_reference.md): UART0/admin-SSH administration, serial, broker, USB, Wi-Fi, mDNS, web, SSH, and diagnostic commands.
|
||||||
|
|
||||||
## Flash partition layout
|
## Flash partition layout
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the cus
|
|||||||
|
|
||||||
| Partition | Offset | Size | Purpose |
|
| Partition | Offset | Size | Purpose |
|
||||||
|---|---:|---:|---|
|
|---|---:|---:|---|
|
||||||
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, local-display, role-based user, legacy recovery credential, HTTPS identity, and SSH host-key data |
|
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, mDNS hostname, local-display, role-based user, legacy recovery credential, HTTPS identity, and SSH host-key data |
|
||||||
| `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
|
| `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
|
||||||
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
|
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
|
||||||
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
|
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
|
||||||
@@ -73,7 +73,7 @@ The firmware provides an interactive UART0 console at `serial-tool>`. Run `help`
|
|||||||
|
|
||||||
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
|
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
|
||||||
|
|
||||||
Serial configuration and Wi-Fi edits remain in RAM until explicitly saved with `serial save` or `wifi save`. Authenticated admin SSH sessions expose the shared operational administration registry, including interactive secrets, recovery-material management, network diagnostics, and deferred reboot/SSH lifecycle commands. Initial administrator bootstrap and explicit recovery of an unavailable user database remain UART0-only. An administrator also cannot generate a replacement password for its own account over SSH, preventing the one-time value from being lost when that mutation revokes the session. `web credentials show` exposes only the legacy migration/recovery credential, not an active Phase 8B network login.
|
Serial, Wi-Fi, and mDNS hostname edits remain in RAM until explicitly saved with `serial save`, `wifi save`, or `mdns save`. Authenticated admin SSH sessions expose the shared operational administration registry, including interactive secrets, recovery-material management, network diagnostics, and deferred reboot/SSH lifecycle commands. Initial administrator bootstrap and explicit recovery of an unavailable user database remain UART0-only. An administrator also cannot generate a replacement password for its own account over SSH, preventing the one-time value from being lost when that mutation revokes the session. `web credentials show` exposes only the legacy migration/recovery credential, not an active Phase 8B network login.
|
||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
@@ -81,4 +81,4 @@ The HTTPS interface uses a device-specific self-signed certificate and role-awar
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the [GNU General Public License version 3 only](LICENSE) (`GPL-3.0-only`). Third-party components remain subject to their respective licenses. The integration baseline uses Espressif registry components `wolfssl/wolfssl` `5.8.2~1` and `wolfssl/wolfssh` `1.4.20`; review upstream security releases before production use.
|
This project is licensed under the [GNU General Public License version 3 only](LICENSE) (`GPL-3.0-only`). Third-party components remain subject to their respective licenses. The integration baseline uses Espressif registry components `espressif/mdns` `1.12.0`, `wolfssl/wolfssl` `5.8.2~1`, and `wolfssl/wolfssh` `1.4.20`; review upstream security releases before production use.
|
||||||
|
|||||||
+12
-1
@@ -29,6 +29,16 @@ dependencies:
|
|||||||
registry_url: https://components.espressif.com/
|
registry_url: https://components.espressif.com/
|
||||||
type: service
|
type: service
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
espressif/mdns:
|
||||||
|
component_hash: 3ba256ac95e07c274be53cbd73f06cb846c403b61e8fbdf1be57bdb79db7a63e
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.0'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.12.0
|
||||||
espressif/tinyusb:
|
espressif/tinyusb:
|
||||||
component_hash: a72b7d67472914ab76309340fd50d578b31e310963d45ad0f81144bde3314752
|
component_hash: a72b7d67472914ab76309340fd50d578b31e310963d45ad0f81144bde3314752
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -66,9 +76,10 @@ dependencies:
|
|||||||
direct_dependencies:
|
direct_dependencies:
|
||||||
- espressif/esp_tinyusb
|
- espressif/esp_tinyusb
|
||||||
- espressif/led_strip
|
- espressif/led_strip
|
||||||
|
- espressif/mdns
|
||||||
- idf
|
- idf
|
||||||
- wolfssl/wolfssh
|
- wolfssl/wolfssh
|
||||||
- wolfssl/wolfssl
|
- wolfssl/wolfssl
|
||||||
manifest_hash: 34e93560f67a22e2a43647a4663b450e07ecd9fa3fdc2ac38343c1a76b513f5d
|
manifest_hash: 4b6fa5a7e06122f9194adfc282a4baf4247d8a114b905e65f6397503e59f0898
|
||||||
target: esp32s3
|
target: esp32s3
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ SSH role=admin ------> shared administration dispatcher <------ UART0
|
|||||||
5. Initialize the serial service, session broker, and permanent USB transport task. UART1 is not started automatically here.
|
5. Initialize the serial service, session broker, and permanent USB transport task. UART1 is not started automatically here.
|
||||||
6. Load/generate HTTPS material, then initialize the user database using the legacy web credential for first migration when available. User-database failure makes network authentication fail closed.
|
6. Load/generate HTTPS material, then initialize the user database using the legacy web credential for first migration when available. User-database failure makes network authentication fail closed.
|
||||||
7. Initialize the HTTPS runtime, SSH host-key material, and permanent SSH owner task.
|
7. Initialize the HTTPS runtime, SSH host-key material, and permanent SSH owner task.
|
||||||
8. Load Wi-Fi configuration, persist generated first-boot defaults when appropriate, initialize its manager, and start it when configured for boot.
|
8. Load Wi-Fi configuration and the independent mDNS hostname configuration, persist generated first-boot Wi-Fi defaults when appropriate, initialize the nonfatal mDNS configuration service and Wi-Fi manager, and start Wi-Fi when configured for boot. The Wi-Fi manager owns subsequent mDNS announcement transitions.
|
||||||
9. Start HTTPS and SSH only when their startup gates pass. The Wi-Fi portion requires valid configuration and successful manager initialization and, when enabled at boot, successful submission of its asynchronous start request; it does not require association, an IP address, or reachability. Both gates also require HTTPS security readiness, and SSH additionally requires its own security/runtime readiness. The HTTPS-security gate on SSH is an implemented dependency even though SSH has a separate host key.
|
9. Start HTTPS and SSH only when their startup gates pass. The Wi-Fi portion requires valid configuration and successful manager initialization and, when enabled at boot, successful submission of its asynchronous start request; it does not require association, an IP address, or reachability. Both gates also require HTTPS security readiness, and SSH additionally requires its own security/runtime readiness. The HTTPS-security gate on SSH is an implemented dependency even though SSH has a separate host key.
|
||||||
10. Start the local status/control task if button initialization succeeded.
|
10. Start the local status/control task if button initialization succeeded.
|
||||||
11. Construct ESP-IDF's UART REPL to initialize `esp_console`, but do not start the stock REPL task. Register command groups, install completion, and start the custom UART frontend that feeds the shared dispatcher.
|
11. Construct ESP-IDF's UART REPL to initialize `esp_console`, but do not start the stock REPL task. Register command groups, install completion, and start the custom UART frontend that feeds the shared dispatcher.
|
||||||
@@ -102,7 +102,7 @@ HTTP Basic authentication uses `user_database`. Before administrator bootstrap,
|
|||||||
|
|
||||||
The boot-local Basic-authentication cache has four RAM entries and a five-minute sliding lifetime. It stores a keyed digest of the complete `Authorization` header rather than the raw header, and every hit revalidates principal currentness. Its current lack of locking relies on the single-HTTPD-owner execution model.
|
The boot-local Basic-authentication cache has four RAM entries and a five-minute sliding lifetime. It stores a keyed digest of the complete `Authorization` header rather than the raw header, and every hit revalidates principal currentness. Its current lack of locking relies on the single-HTTPD-owner execution model.
|
||||||
|
|
||||||
A WebSocket connection requires a one-time, principal-bound ticket with a maximum 30-second lifetime. Only four tickets can be outstanding; minting another evicts the live entry with the earliest expiry. Ticket issuance and upgrade also validate a supplied `Origin` against `https://<Host>`; absence of `Origin` is accepted for non-browser clients. Tickets are stored as digests, consumed before currentness validation, and are never persisted. An admitted session starts the serial service if necessary, creates a broker client, and opportunistically requests writer ownership. The web transport has two fixed session slots. Binary frames carry serial data; small text messages request or release writer ownership. HTTPD owns socket send/close operations, while the web transport task mediates broker work through bounded scheduling.
|
A WebSocket connection requires a one-time, principal-bound ticket with a maximum 30-second lifetime. Only four tickets can be outstanding; minting another evicts the live entry with the earliest expiry. Ticket issuance and upgrade also validate a supplied `Origin` against `https://<Host>`; absence of `Origin` is accepted for non-browser clients. Tickets are stored as digests, consumed before currentness validation, and are never persisted. An admitted session starts the serial service if necessary, creates a broker client, and opportunistically requests writer ownership. The web transport has two fixed session slots. Binary frames carry serial data; small text messages request or release writer ownership. HTTPD owns socket send/close operations, while the web transport task mediates broker work through bounded scheduling. Browser Disconnect closes the WebSocket and pauses automatic reconnect until the user explicitly selects Reconnect.
|
||||||
|
|
||||||
Web serial initialization is failure-isolated from the base HTTPS service: if the transport cannot initialize, `web_server_init()` can still succeed and serve authenticated non-WebSocket routes.
|
Web serial initialization is failure-isolated from the base HTTPS service: if the transport cannot initialize, `web_server_init()` can still succeed and serve authenticated non-WebSocket routes.
|
||||||
|
|
||||||
@@ -153,18 +153,19 @@ The dispatcher is the sole caller of `esp_console_run()`, serializing UART0 and
|
|||||||
|
|
||||||
For SSH, standard output/error is redirected to the invoking session's bounded output ring. `console_input` routes visible or hidden prompts to UART0 or the active SSH session. `exit` and Ctrl+D on an empty admin SSH line use bounded deferred self-disconnect after their acknowledgement drains; role-`user` SSH remains a binary-transparent serial stream. Session tokens include slot and generation so late queued work cannot attach to a reused SSH slot. Only the SSH owner task moves ring output through wolfSSH.
|
For SSH, standard output/error is redirected to the invoking session's bounded output ring. `console_input` routes visible or hidden prompts to UART0 or the active SSH session. `exit` and Ctrl+D on an empty admin SSH line use bounded deferred self-disconnect after their acknowledgement drains; role-`user` SSH remains a binary-transparent serial stream. Session tokens include slot and generation so late queued work cannot attach to a reused SSH slot. Only the SSH owner task moves ring output through wolfSSH.
|
||||||
|
|
||||||
Remote reboot, SSH stop/disconnect, and host-key rotate/reset use deferred control. The control task waits up to ten seconds for command state plus administration and transport application buffers to clear, then adds a short delay; this is a bounded best-effort heuristic, not peer-delivery confirmation. UART0 invokes these actions synchronously. User mutations and their revocations are not part of this mechanism. UART0 linenoise and the SSH editor consume the same manually maintained completion matcher and candidate formatter, so the two administration routes cannot drift in offered or displayed ambiguous completions; the hints can still drift from command registration and are not an authorization list.
|
Admin SSH `exit`, remote reboot, SSH stop/disconnect, and host-key rotate/reset use deferred control. The control task waits up to ten seconds for command state plus administration and transport application buffers to clear, then adds a short delay; this is a bounded best-effort heuristic, not peer-delivery confirmation. UART0 invokes these actions synchronously. User mutations and their revocations are not part of this mechanism. UART0 linenoise and the SSH editor consume the same manually maintained completion matcher and candidate formatter, so the two administration routes cannot drift in offered or displayed ambiguous completions; the hints can still drift from command registration and are not an authorization list.
|
||||||
|
|
||||||
## Wi-Fi and persistence
|
## Wi-Fi and persistence
|
||||||
|
|
||||||
`wifi_config` owns a fixed-width versioned NVS schema with four prioritized station profiles and AP policy `off`, `fallback`, or `always`. Missing configuration generates per-device defaults including a random AP password. Invalid stored data is generally left untouched while RAM defaults are used.
|
`wifi_config` owns a fixed-width versioned NVS schema with four prioritized station profiles and AP policy `off`, `fallback`, or `always`. Missing configuration generates per-device defaults including a random AP password. Invalid stored data is generally left untouched while RAM defaults are used.
|
||||||
|
|
||||||
`wifi_manager` is a permanent task with one bounded command/event queue. ESP-IDF callbacks only copy compact events into the queue. The task owns association, DHCP deadlines, profile failover, AP policy, retries/backoff, and next-profile requests. It also reconciles against authoritative driver/netif state so dropped events do not permanently wedge policy. ESP-IDF Wi-Fi storage is RAM-only; the application blob is authoritative, and edits require explicit save. Edits to disabled station profiles are staged in RAM without restarting the radio; enabling/disabling a profile or changing enabled station/AP policy restarts it asynchronously. Start/stop—including local controls—intentionally update the RAM `enabled_at_boot` field. Working-configuration copies contain PSKs and must be securely wiped; routine status and the local UI use secret-free snapshots.
|
`wifi_manager` is a permanent task with one bounded command/event queue. ESP-IDF callbacks only copy compact events into the queue. The task owns association, DHCP deadlines, profile failover, AP policy, retries/backoff, next-profile requests, and the mDNS announcement lifecycle. `mdns_service` initializes the responder at most once after a validated STA `GOT_IP`; the managed component's own event handlers withdraw and restore the STA announcement across transient connectivity changes, while the project tracks whether announcement is currently expected. Initialization failure is latched rather than retried because partial upstream low-memory initialization is not safely recoverable; mDNS failure is nonfatal. It also reconciles against authoritative driver/netif state so dropped events do not permanently wedge policy. ESP-IDF Wi-Fi storage is RAM-only; the application blob is authoritative, and edits require explicit save. Edits to disabled station profiles are staged in RAM without restarting the radio; enabling/disabling a profile or changing enabled station/AP policy restarts it asynchronously. Start/stop—including local controls—intentionally update the RAM `enabled_at_boot` field. Working-configuration copies contain PSKs and must be securely wiped; routine status and the local UI use secret-free snapshots.
|
||||||
|
|
||||||
Persistent namespaces/blobs include:
|
Persistent namespaces/blobs include:
|
||||||
|
|
||||||
- `serial/config`;
|
- `serial/config`;
|
||||||
- `wifi_app/config`;
|
- `wifi_app/config`;
|
||||||
|
- `mdns_cfg/config`;
|
||||||
- `local_ui/config`;
|
- `local_ui/config`;
|
||||||
- `web_sec/material`;
|
- `web_sec/material`;
|
||||||
- `user_db/database`;
|
- `user_db/database`;
|
||||||
|
|||||||
@@ -105,17 +105,17 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
|||||||
- Dependencies: ESP-IDF console/linenoise, all command handlers, user-principal currentness
|
- Dependencies: ESP-IDF console/linenoise, all command handlers, user-principal currentness
|
||||||
- Flow: `UART0/admin SSH -> bounded request queue -> one dispatcher -> esp_console_run()`
|
- Flow: `UART0/admin SSH -> bounded request queue -> one dispatcher -> esp_console_run()`
|
||||||
- Ownership: dispatcher is sole `esp_console_run()` caller; the SSH owner exclusively performs post-initialization wolfSSH runtime calls.
|
- Ownership: dispatcher is sole `esp_console_run()` caller; the SSH owner exclusively performs post-initialization wolfSSH runtime calls.
|
||||||
- Lifecycle: remote session tokens include slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns.
|
- Lifecycle: remote session tokens include slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns. Admin SSH `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort output draining.
|
||||||
- Constraint: one slow command or prompt serializes all administration. Admin SSH is unavailable until command registration and UART frontend creation complete; supported deferred actions wait only for a bounded application-buffer drain heuristic.
|
- Constraint: one slow command or prompt serializes all administration. Admin SSH is unavailable until command registration and UART frontend creation complete; supported deferred actions wait only for a bounded application-buffer drain heuristic.
|
||||||
|
|
||||||
## Wi-Fi
|
## Wi-Fi
|
||||||
|
|
||||||
**Responsibility:** persist station/AP policy and own asynchronous ESP-NETIF/Wi-Fi state transitions.
|
**Responsibility:** persist station/AP policy and own asynchronous ESP-NETIF/Wi-Fi state transitions.
|
||||||
|
|
||||||
- Files: `src/wifi_config.{h,c}`, `src/wifi_manager.{h,c}`, `src/wifi_console.{h,c}`, `src/network_console.{h,c}`
|
- Files: `src/wifi_config.{h,c}`, `src/wifi_manager.{h,c}`, `src/wifi_console.{h,c}`, `src/mdns_config.{h,c}`, `src/mdns_service.{h,c}`, `src/mdns_console.{h,c}`, `src/network_console.{h,c}`
|
||||||
- Interfaces: config defaults/validate/load/save; manager init/start/stop/apply/reconnect/next-profile/snapshot
|
- Interfaces: config defaults/validate/load/save; manager init/start/stop/apply/reconnect/next-profile/snapshot
|
||||||
- Called by: startup, console, local UI, ESP event callbacks
|
- Called by: startup, console, local UI, ESP event callbacks
|
||||||
- Dependencies: secure random for default AP password, NVS, ESP-NETIF/Wi-Fi/events, lwIP diagnostics
|
- Dependencies: secure random for default AP password, NVS, ESP-NETIF/Wi-Fi/events, Espressif mDNS, lwIP diagnostics
|
||||||
- Lifecycle: permanent manager task and bounded queue; callbacks enqueue compact events only.
|
- Lifecycle: permanent manager task and bounded queue; callbacks enqueue compact events only.
|
||||||
- Constraint: application NVS is authoritative (`WIFI_STORAGE_RAM`); working edits are not persisted until save. Start/stop, including local controls, intentionally update the RAM `enabled_at_boot` field. Working-config copies contain PSKs and must be tightly scoped and wiped; routine status/local UI must use secret-free snapshots.
|
- Constraint: application NVS is authoritative (`WIFI_STORAGE_RAM`); working edits are not persisted until save. Start/stop, including local controls, intentionally update the RAM `enabled_at_boot` field. Working-config copies contain PSKs and must be tightly scoped and wiped; routine status/local UI must use secret-free snapshots.
|
||||||
|
|
||||||
@@ -157,6 +157,7 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
|||||||
| Add or change a command | relevant `*_console.c`, `console_completion.c`, `admin_ssh_console.c` policy/deferred handling |
|
| Add or change a command | relevant `*_console.c`, `console_completion.c`, `admin_ssh_console.c` policy/deferred handling |
|
||||||
| Change account roles/passwords/keys | `user_database.*`, `user_console.c`, transport revocation APIs |
|
| Change account roles/passwords/keys | `user_database.*`, `user_console.c`, transport revocation APIs |
|
||||||
| Change Wi-Fi policy or profile persistence | `wifi_manager.*`, `wifi_config.*`, `wifi_console.c` |
|
| Change Wi-Fi policy or profile persistence | `wifi_manager.*`, `wifi_config.*`, `wifi_console.c` |
|
||||||
|
| Change station mDNS hostname or persistence | `mdns_service.*`, `mdns_config.*`, `mdns_console.c`, then `wifi_manager.c` |
|
||||||
| Change OLED rendering or buttons | `local_status_ui.c`, `local_display.*`, `local_ui_config.*` |
|
| Change OLED rendering or buttons | `local_status_ui.c`, `local_display.*`, `local_ui_config.*` |
|
||||||
| Change board GPIO or electrical tests | `board_pins.h`, hardware test module, `docs/wiring.md` |
|
| Change board GPIO or electrical tests | `board_pins.h`, hardware test module, `docs/wiring.md` |
|
||||||
| Change embedded browser assets | `web_assets/SOURCES.md`, generator, then generated data only as an explicit regeneration task |
|
| Change embedded browser assets | `web_assets/SOURCES.md`, generator, then generated data only as an explicit regeneration task |
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Based on checked-in source plus `README.md` and `docs/roadmap.md`:
|
|||||||
- UART and admin-SSH completion formatter buffers were reduced from 2 KiB to 1 KiB each; current worst-case output is 890 bytes and overflow remains fail-closed.
|
- UART and admin-SSH completion formatter buffers were reduced from 2 KiB to 1 KiB each; current worst-case output is 890 bytes and overflow remains fail-closed.
|
||||||
- Linked RAM fell from 99,508 to 92,188 bytes (7,320 bytes). PSRAM placement of serial payloads additionally removes about 24 KiB of normal internal-heap pressure on the target.
|
- Linked RAM fell from 99,508 to 92,188 bytes (7,320 bytes). PSRAM placement of serial payloads additionally removes about 24 KiB of normal internal-heap pressure on the target.
|
||||||
- `pio run` passes. A preliminary target run reports significantly more free memory and stable, improved operation after these changes. This is useful evidence but not completion of Phase 8C validation.
|
- `pio run` passes. A preliminary target run reports significantly more free memory and stable, improved operation after these changes. This is useful evidence but not completion of Phase 8C validation.
|
||||||
|
- The reviewed mDNS-enabled build uses 94,532 bytes of linked static RAM, 2,344 bytes above the earlier 92,188-byte baseline, and 1,599,765 bytes of flash. Minimizing the managed component saved 112 bytes of linked RAM and about 5.9 KiB flash versus the first mDNS build. Its 4 KiB task stack remains internal, while checked-in settings move general mDNS allocations to PSRAM and disable unused browse, component CLI, AP/ETH, and multiple-instance features. Runtime heap impact still requires target measurement.
|
||||||
- Remaining targeted checks include stored/migrated/recovered user-database mutations, USB enumeration, HTTPS start/stop failure recovery where injectable, SSH initialization/login, completion display, and sustained multi-transport serial traffic while checking `memory` telemetry.
|
- Remaining targeted checks include stored/migrated/recovered user-database mutations, USB enumeration, HTTPS start/stop failure recovery where injectable, SSH initialization/login, completion display, and sustained multi-transport serial traffic while checking `memory` telemetry.
|
||||||
|
|
||||||
## Clearly incomplete or transitional areas
|
## Clearly incomplete or transitional areas
|
||||||
@@ -46,13 +47,13 @@ These observations should be checked when touching the relevant area; they are n
|
|||||||
|
|
||||||
## Active Task
|
## Active Task
|
||||||
|
|
||||||
- **Objective:** Keep UART1 configuration independent of USB CDC host line coding, keep Phase 0 RS-232 status under `debug`, avoid disconnecting Wi-Fi for edits to disabled profiles, and add explicit web/admin-SSH session exit controls.
|
- **Objective:** Announce a configurable `sak-<suffix>.local` hostname through mDNS when Wi-Fi STA has an IPv4 address, without changing the Wi-Fi NVS blob schema.
|
||||||
- **Relevant files:** `src/usb_cdc_transport.{c,h}`, `src/usb_console.c`, `src/rs232_hw_test.c`, `src/console_completion.c`, `src/wifi_manager.{c,h}`, `src/web_ui.c`, `src/admin_ssh_console.{c,h}`, `src/main.c`, related documentation.
|
- **Relevant files:** `src/mdns_config.{c,h}`, `src/mdns_service.{c,h}`, `src/mdns_console.{c,h}`, `src/wifi_manager.{c,h}`, `src/main.c`, `src/CMakeLists.txt`, `src/idf_component.yml`, `dependencies.lock`, completion and command documentation.
|
||||||
- **Findings:** The USB transport converted cached CDC line coding into `serial_config_t` and called `serial_service_apply_config()` after writer acquisition. Root `status` was a Phase 0 diagnostic that needed to claim the RS-232 port and therefore failed while the production serial service owned it. Every Wi-Fi working-configuration update queued a radio restart, including edits to profiles disabled in both the old and new configurations.
|
- **Findings:** `wifi_manager` already serializes all meaningful STA transitions through its permanent task; callbacks only enqueue events. This is the appropriate lifecycle owner for mDNS, while a separate configuration module preserves the existing `wifi_app/config` wire format.
|
||||||
- **Decision:** Treat CDC line coding as diagnostic metadata only. UART1 configuration remains exclusively controlled by explicit `serial` commands and their NVS persistence. Keep all Phase 0 RS-232 diagnostics, including signal status, under `debug`. Treat disabled Wi-Fi profiles as staged configuration: their edits do not restart the radio; enable/disable transitions and changes to enabled station/AP policy retain controlled asynchronous restart behavior. Browser Disconnect pauses reconnect until Reconnect is selected. `exit`/empty-line Ctrl+D are admin-SSH-only deferred self-disconnect actions; role-`user` SSH remains binary-transparent.
|
- **Decision:** Persist a fixed v1 record under `mdns_cfg/config`, separate from Wi-Fi configuration. Defaults derive a safe lower-case hexadecimal suffix from the STA MAC. The manager initializes mDNS at most once after validating `IP_EVENT_STA_GOT_IP`; the managed component's own handlers withdraw/restore the STA responder across connectivity changes, and online hostname changes use `mdns_hostname_set()` without teardown. Initialization failure is latched instead of retried because the resolved upstream 1.12.0 component has an unsafe partial low-memory initialization path. mDNS errors cannot fail Wi-Fi, UART0, UART1, or native USB.
|
||||||
- **Changes completed:** Removed the pending line-coding apply path and its counters; retained the latest host setting for `usb status`. Replaced root `status` with `debug status`, updated completion and documentation. Added effective Wi-Fi policy comparison before queueing `MESSAGE_COMMAND_APPLY`. Added browser Disconnect and admin SSH `exit`/Ctrl+D session closing through the existing bounded drain path.
|
- **Changes completed:** Added the `espressif/mdns` managed dependency (resolved to 1.12.0 on IDF 5.5), mDNS config/service/console modules, `mdns status|suffix|save|load|defaults|reset`, completion, CMake integration, and command/architecture documentation. Minimized the component to STA-only responder use, moved general allocations to PSRAM, retained the internal task stack, and removed reconnect-time free/reinit churn. Final `pio run` passes at 94,532 bytes linked RAM and 1,599,765 bytes flash.
|
||||||
- **Remaining work:** Target-hardware verification: acquire/release USB writer ownership after changing a host terminal's line coding and confirm UART1 remains at the configured framing. Verify `debug status` works after `serial stop` and root `status` is unknown. While connected through Wi-Fi, edit a disabled profile and its secret without a reconnect; then enable it and verify the expected reconnect. Verify browser Disconnect pauses automatic reconnect until Reconnect is selected, and admin SSH `exit`/empty Ctrl+D close cleanly.
|
- **Remaining work:** Target-hardware verification: associate a station and resolve the default `sak-<mac>.local`; change/save/load a suffix and confirm live reannouncement plus reboot persistence; stop Wi-Fi or remove the STA lease and confirm the record withdraws. Verify serial, native USB, and UART0 remain available if mDNS initialization fails.
|
||||||
- **Risks / things to remember:** Host terminal line-coding selectors no longer configure the physical RS-232 port; use `serial set`/`serial save` instead. `debug status` remains ownership-protected and requires UART1 to be stopped. Enabling a profile may disconnect an SSH administrative session. `pio run` passed after all changes.
|
- **Risks / things to remember:** Hostnames are STA-only and are intentionally not announced by fallback AP mode. NVS changes to `mdns_cfg/config` are independent of the unchanged `wifi_app/config` blob. mDNS remains allocated after first successful initialization (including its internal 4 KiB task stack) to avoid fragmentation and unsafe repeated initialization; measure free/minimum/largest internal heap and mDNS stack margin during reconnect stress.
|
||||||
|
|
||||||
### Handoff template
|
### Handoff template
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ Only constraints supported by implementation or current project documentation be
|
|||||||
|
|
||||||
## Selected self-affecting admin SSH actions use bounded deferred control
|
## Selected self-affecting admin SSH actions use bounded deferred control
|
||||||
|
|
||||||
**Decision:** Remote reboot, SSH stop/disconnect, and host-key rotate/reset are deferred until command state and administration/transport application buffers appear drained, with a ten-second limit and short final delay.
|
**Decision:** Admin SSH `exit`, remote reboot, SSH stop/disconnect, and host-key rotate/reset are deferred until command state and administration/transport application buffers appear drained, with a ten-second limit and short final delay.
|
||||||
|
|
||||||
**Rationale/evidence:** `admin_ssh_console` has a separate bounded control task and pending-action state. The check is a best-effort application-buffer heuristic, not peer-delivery confirmation. User account mutations and their immediate revocation calls do not use this path.
|
**Rationale/evidence:** `admin_ssh_console` has a separate bounded control task and pending-action state. The check is a best-effort application-buffer heuristic, not peer-delivery confirmation. User account mutations and their immediate revocation calls do not use this path.
|
||||||
|
|
||||||
@@ -94,13 +94,13 @@ Only constraints supported by implementation or current project documentation be
|
|||||||
|
|
||||||
## Security material and configuration use bounded, versioned NVS records
|
## Security material and configuration use bounded, versioned NVS records
|
||||||
|
|
||||||
**Decision:** Application settings, users, and identities use separate fixed/versioned NVS blobs. Serial, Wi-Fi, and local-UI working edits are RAM-only until explicitly saved. User mutations and HTTPS/SSH identity changes commit directly as part of the operation. Invalid ordinary configuration generally selects RAM defaults without erasing storage; malformed security material fails closed and needs explicit reset.
|
**Decision:** Application settings, users, and identities use separate fixed/versioned NVS blobs. Serial, Wi-Fi, mDNS-hostname, and local-UI working edits are RAM-only until explicitly saved. User mutations and HTTPS/SSH identity changes commit directly as part of the operation. Invalid ordinary configuration generally selects RAM defaults without erasing storage; malformed security material fails closed and needs explicit reset.
|
||||||
|
|
||||||
**Rationale/evidence:** Serial, Wi-Fi, local UI, web security, users, and SSH security each validate schema/size and own their namespace. User/security mutations build and validate candidate state before committing it; security modules avoid silently replacing an established identity. The live user database remains internal while its 5,360-byte candidate is a persistent PSRAM-preferred allocation with internal fallback and is wiped after every transaction.
|
**Rationale/evidence:** Serial, Wi-Fi, local UI, web security, users, and SSH security each validate schema/size and own their namespace. User/security mutations build and validate candidate state before committing it; security modules avoid silently replacing an established identity. The live user database remains internal while its 5,360-byte candidate is a persistent PSRAM-preferred allocation with internal fallback and is wiped after every transaction.
|
||||||
|
|
||||||
**Consequence for future changes:** Add schema versions and transactional candidate validation. Do not overwrite unknown records automatically; provide explicit migration/reset behavior. Preserve the distinct persistence contracts: explicit save/load/default/reset for working configuration and per-blob commit-before-live-install for user and identity mutation. Keep candidate ownership mutex-local and wipe/free it on initialization or recovery failure. Recheck external-buffer staging in the flash/NVS implementation when upgrading from the pinned ESP-IDF 5.5 baseline. Pre-bootstrap legacy credential rotation spans `web_sec/material` and `user_db/database`, is not cross-namespace atomic, and relies on boot reconciliation after interruption.
|
**Consequence for future changes:** Add schema versions and transactional candidate validation. Do not overwrite unknown records automatically; provide explicit migration/reset behavior. Preserve the distinct persistence contracts: explicit save/load/default/reset for working configuration and per-blob commit-before-live-install for user and identity mutation. Keep candidate ownership mutex-local and wipe/free it on initialization or recovery failure. Recheck external-buffer staging in the flash/NVS implementation when upgrading from the pinned ESP-IDF 5.5 baseline. Pre-bootstrap legacy credential rotation spans `web_sec/material` and `user_db/database`, is not cross-namespace atomic, and relies on boot reconciliation after interruption.
|
||||||
|
|
||||||
**Relevant files:** `src/serial_config.c`, `src/wifi_config.c`, `src/local_ui_config.c`, `src/web_security.c`, `src/user_database.c`, `src/ssh_security.c`
|
**Relevant files:** `src/serial_config.c`, `src/wifi_config.c`, `src/mdns_config.c`, `src/mdns_service.c`, `src/local_ui_config.c`, `src/web_security.c`, `src/user_database.c`, `src/ssh_security.c`
|
||||||
|
|
||||||
## NVS is persistence, not a physical security boundary
|
## NVS is persistence, not a physical security boundary
|
||||||
|
|
||||||
@@ -114,13 +114,13 @@ Only constraints supported by implementation or current project documentation be
|
|||||||
|
|
||||||
## Wi-Fi callbacks enqueue; the manager owns policy
|
## Wi-Fi callbacks enqueue; the manager owns policy
|
||||||
|
|
||||||
**Decision:** ESP event callbacks copy bounded event data into the Wi-Fi manager queue. A permanent manager task performs driver operations, profile/AP policy, deadlines, and reconciliation.
|
**Decision:** ESP event callbacks copy bounded event data into the Wi-Fi manager queue. A permanent manager task performs driver operations, profile/AP policy, deadlines, reconciliation, and station mDNS announcement transitions. mDNS initializes at most once, remains allocated across transient disconnects while its component handlers withdraw/re-enable the STA interface, and treats failure as nonfatal.
|
||||||
|
|
||||||
**Rationale/evidence:** Callback paths avoid blocking, NVS, and policy work. Manager deadlines consult authoritative driver/netif state so dropped events are recoverable.
|
**Rationale/evidence:** Callback paths avoid blocking, NVS, and policy work. Manager deadlines consult authoritative driver/netif state so dropped events are recoverable.
|
||||||
|
|
||||||
**Consequence for future changes:** Keep callbacks short and nonblocking. Add state transitions to the manager rather than directly invoking Wi-Fi policy from consoles, UI, or callbacks. Preserve queue-drop observability.
|
**Consequence for future changes:** Keep callbacks short and nonblocking. Add state transitions to the manager rather than directly invoking Wi-Fi policy from consoles, UI, or callbacks. Preserve queue-drop observability.
|
||||||
|
|
||||||
**Relevant files:** `src/wifi_manager.{h,c}`, `src/wifi_config.{h,c}`
|
**Relevant files:** `src/wifi_manager.{h,c}`, `src/wifi_config.{h,c}`, `src/mdns_service.{h,c}`, `src/mdns_config.{h,c}`
|
||||||
|
|
||||||
## Optional local UI cannot become a core dependency
|
## Optional local UI cannot become a core dependency
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,17 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
|
|||||||
|
|
||||||
`ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. Edits to a disabled profile's SSID, priority, security mode, or secret are staged in RAM and do not interrupt the current Wi-Fi connection. Enabling or disabling a profile, changing an enabled profile, or changing AP policy/configuration applies the new radio policy and may reconnect Wi-Fi. Use `wifi save` to persist working changes. Passwords are not displayed by ordinary status output.
|
`ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. Edits to a disabled profile's SSID, priority, security mode, or secret are staged in RAM and do not interrupt the current Wi-Fi connection. Enabling or disabling a profile, changing an enabled profile, or changing AP policy/configuration applies the new radio policy and may reconnect Wi-Fi. Use `wifi save` to persist working changes. Passwords are not displayed by ordinary status output.
|
||||||
|
|
||||||
|
## mDNS
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `mdns status` | Show the configured `sak-<suffix>.local` hostname and announcement state. |
|
||||||
|
| `mdns suffix <value>` | Set a 1–55-character lowercase hostname suffix in RAM. |
|
||||||
|
| `mdns save` / `mdns load` | Save the working suffix to its independent NVS record or load it. |
|
||||||
|
| `mdns defaults` / `mdns reset` | Restore the MAC-derived suffix in RAM, or restore and persist it. |
|
||||||
|
|
||||||
|
When the Wi-Fi station receives an IPv4 address, the Wi-Fi manager announces `sak-<suffix>.local`. The default suffix is the lower-case hexadecimal STA MAC address. Suffixes may contain lowercase ASCII letters, digits, and internal hyphens only. Changing a suffix while online causes a best-effort reannouncement; mDNS failures do not stop Wi-Fi, UART0, UART1, or native USB access.
|
||||||
|
|
||||||
## HTTPS web terminal
|
## HTTPS web terminal
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
@@ -122,7 +133,7 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
|
|||||||
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. |
|
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. |
|
||||||
| `web reset --force` | Explicitly replace missing, incompatible, or damaged legacy credentials and web material. |
|
| `web reset --force` | Explicitly replace missing, incompatible, or damaged legacy credentials and web material. |
|
||||||
|
|
||||||
HTTPS listens on port 443 only. Authenticate with any current user-database username/password; both `user` and `admin` roles receive the existing status and browser-terminal interface. The device serves vendored xterm.js without Internet access. Browser sessions use one-time account-bound tickets, binary WebSocket frames, and the broker's one-writer rule. Account mutations revoke only that account's tickets and sessions.
|
HTTPS listens on port 443 only. Authenticate with any current user-database username/password; both `user` and `admin` roles receive the existing status and browser-terminal interface. The device serves vendored xterm.js without Internet access. Browser sessions use one-time account-bound tickets, binary WebSocket frames, and the broker's one-writer rule. Selecting **Disconnect** closes the current WebSocket and pauses automatic reconnect; select **Reconnect** to resume connection attempts. Account mutations revoke only that account's tickets and sessions.
|
||||||
|
|
||||||
## SSH serial transport
|
## SSH serial transport
|
||||||
|
|
||||||
@@ -144,7 +155,7 @@ UART0 and admin SSH submit to one bounded queue, and one dispatcher task is the
|
|||||||
|
|
||||||
UART0 and admin SSH use shared whole-line Tab completion. A unique/common prefix expands inline; a Tab that cannot extend an ambiguous prefix prints the matching candidates and redraws the unchanged input line instead of cycling candidates. Admin SSH additionally supports four-entry per-session command history with Up/Down, inline cursor editing with Left/Right, Home/End (including Pos1/Ende terminal sequences), Backspace/Delete, Ctrl-C, and visible or no-echo interactive prompts. Its history is RAM-only, private to the session, and wiped on disconnect. Ping callbacks enqueue bounded typed results so all formatting remains on the dispatcher task.
|
UART0 and admin SSH use shared whole-line Tab completion. A unique/common prefix expands inline; a Tab that cannot extend an ambiguous prefix prints the matching candidates and redraws the unchanged input line instead of cycling candidates. Admin SSH additionally supports four-entry per-session command history with Up/Down, inline cursor editing with Left/Right, Home/End (including Pos1/Ende terminal sequences), Backspace/Delete, Ctrl-C, and visible or no-echo interactive prompts. Its history is RAM-only, private to the session, and wiped on disconnect. Ping callbacks enqueue bounded typed results so all formatting remains on the dispatcher task.
|
||||||
|
|
||||||
`exit`, `reboot`, `ssh stop`, session disconnect, and SSH host-key reset/rotation are deferred until the command acknowledgement has left both the administration output ring and transport TX buffer. The shell stops accepting another command while such an action is pending. SSH host-key replacement or service stop closes all SSH sessions; reconnect and verify the new fingerprint where applicable. Web recovery credentials/certificates, Wi-Fi secrets, and interactive user passwords/keys are available to authenticated administrators and must therefore be treated as remotely accessible administrative material. `user bootstrap` and `user recover --force` remain UART0-only. A connected administrator also cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
`exit`, `reboot`, `ssh stop`, session disconnect, and SSH host-key reset/rotation use bounded deferred control. The firmware waits on a best-effort basis for the administration output ring and transport TX buffer to drain before acting; this is not confirmation that the peer received the acknowledgement. The shell stops accepting another command while such an action is pending. SSH host-key replacement or service stop closes all SSH sessions; reconnect and verify the new fingerprint where applicable. Web recovery credentials/certificates, Wi-Fi secrets, and interactive user passwords/keys are available to authenticated administrators and must therefore be treated as remotely accessible administrative material. `user bootstrap` and `user recover --force` remain UART0-only. A connected administrator also cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
||||||
|
|
||||||
## Hardware diagnostics
|
## Hardware diagnostics
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -109,6 +109,11 @@ Implemented and hardware-validated:
|
|||||||
- Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`.
|
- Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`.
|
||||||
- Wi-Fi/lwIP payload allocation configured to prefer PSRAM with capacities pinned in `sdkconfig.defaults`.
|
- 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-<suffix>.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.
|
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
|
### Phase 5A — Authenticated HTTPS foundation
|
||||||
@@ -136,6 +141,8 @@ Implemented and hardware-validated:
|
|||||||
- Viewport-constrained terminal fitting without recursive page growth.
|
- Viewport-constrained terminal fitting without recursive page growth.
|
||||||
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
|
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
|
||||||
|
|
||||||
|
Post-validation enhancement implemented; browser regression validation pending: Disconnect explicitly closes the terminal WebSocket and pauses automatic reconnect until Reconnect is selected.
|
||||||
|
|
||||||
### Phase 6 — Authenticated SSH serial transport
|
### Phase 6 — Authenticated SSH serial transport
|
||||||
|
|
||||||
Implemented and target-hardware validated:
|
Implemented and target-hardware validated:
|
||||||
@@ -188,7 +195,7 @@ Implementation sequence:
|
|||||||
- 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.
|
- 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.
|
- 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 recovery-secret display, 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. Initial `user bootstrap` and explicit `user recover --force` remain physical-UART0 operations; admin SSH also rejects generating a replacement password for its own account.
|
- Authenticated administrators receive the operational registry, including recovery-secret display, 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. Initial `user bootstrap` and explicit `user recover --force` remain physical-UART0 operations; 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. Admin sessions are checked for a current `admin` principal before command execution and during the active-session reconciliation.
|
- `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.
|
- Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled.
|
||||||
- Pending target-hardware validation: route separation from the broker, 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.
|
- Pending target-hardware validation: route separation from the broker, 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.
|
||||||
4. **Phase 8D — Web user administration — Planned**
|
4. **Phase 8D — Web user administration — Planned**
|
||||||
@@ -265,7 +272,7 @@ BLE remains subordinate to stable Wi-Fi and serial operation; it should be omitt
|
|||||||
|
|
||||||
These features are candidates, not current commitments:
|
These features are candidates, not current commitments:
|
||||||
|
|
||||||
- mDNS advertisement for the certificate’s device-specific `.local` name and selected services.
|
- Additional DNS-SD service advertisement and any certificate-name integration beyond the implemented configurable STA hostname.
|
||||||
- Enterprise Wi-Fi support, subject to credential-storage and certificate-validation design.
|
- Enterprise Wi-Fi support, subject to credential-storage and certificate-validation design.
|
||||||
- IPv6 behavior and diagnostics beyond the current basic support.
|
- IPv6 behavior and diagnostics beyond the current basic support.
|
||||||
- WireGuard feasibility evaluation, including RAM, CPU, licensing, key storage, routing, and recovery impact.
|
- WireGuard feasibility evaluation, including RAM, CPU, licensing, key storage, routing, and recovery impact.
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ CONFIG_ESP_ENABLE_WOLFSSH=y
|
|||||||
# The managed component emits a generic RSA stack warning although this target disables RSA.
|
# The managed component emits a generic RSA stack warning although this target disables RSA.
|
||||||
CONFIG_ESP_WOLFSSL_NO_STACK_SIZE_BUILD_WARNING=y
|
CONFIG_ESP_WOLFSSL_NO_STACK_SIZE_BUILD_WARNING=y
|
||||||
|
|
||||||
|
# Keep configurable hostname discovery STA-only and bounded. The responder task
|
||||||
|
# remains internal; general mDNS metadata prefers PSRAM to protect internal heap.
|
||||||
|
CONFIG_MDNS_MAX_INTERFACES=1
|
||||||
|
CONFIG_MDNS_MAX_SERVICES=1
|
||||||
|
CONFIG_MDNS_PREDEF_NETIF_STA=y
|
||||||
|
# CONFIG_MDNS_PREDEF_NETIF_AP is not set
|
||||||
|
# CONFIG_MDNS_PREDEF_NETIF_ETH is not set
|
||||||
|
# CONFIG_MDNS_ENABLE_CONSOLE_CLI is not set
|
||||||
|
# CONFIG_MDNS_ENABLE_BROWSE is not set
|
||||||
|
# CONFIG_MDNS_MULTIPLE_INSTANCE is not set
|
||||||
|
CONFIG_MDNS_TASK_CREATE_FROM_INTERNAL=y
|
||||||
|
CONFIG_MDNS_MEMORY_ALLOC_SPIRAM=y
|
||||||
|
# CONFIG_MDNS_MEMORY_ALLOC_INTERNAL is not set
|
||||||
|
|
||||||
# Support WPA3-SAE for station profiles and the WPA2/WPA3 fallback AP.
|
# Support WPA3-SAE for station profiles and the WPA2/WPA3 fallback AP.
|
||||||
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
|
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
|
||||||
CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y
|
CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ idf_component_register(
|
|||||||
"wifi_config.c"
|
"wifi_config.c"
|
||||||
"wifi_manager.c"
|
"wifi_manager.c"
|
||||||
"wifi_console.c"
|
"wifi_console.c"
|
||||||
|
"mdns_config.c"
|
||||||
|
"mdns_service.c"
|
||||||
|
"mdns_console.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES
|
REQUIRES
|
||||||
bootloader_support
|
bootloader_support
|
||||||
@@ -58,6 +61,7 @@ idf_component_register(
|
|||||||
led_strip
|
led_strip
|
||||||
lwip
|
lwip
|
||||||
mbedtls
|
mbedtls
|
||||||
|
mdns
|
||||||
nvs_flash
|
nvs_flash
|
||||||
wolfssl__wolfssh
|
wolfssl__wolfssh
|
||||||
wolfssl__wolfssl
|
wolfssl__wolfssl
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
static const char *const s_root_candidates[] = {
|
static const char *const s_root_candidates[] = {
|
||||||
"help", "exit", "debug", "display", "serial", "broker", "usb", "user",
|
"help", "exit", "debug", "display", "serial", "broker", "usb", "user",
|
||||||
"wifi", "web", "ssh", "ping", "nslookup", "traceroute", "reboot", "memory",
|
"wifi", "mdns", "web", "ssh", "ping", "nslookup", "traceroute", "reboot", "memory",
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Keep full-line candidate strings grouped by their registered root command. */
|
/* Keep full-line candidate strings grouped by their registered root command. */
|
||||||
@@ -169,6 +169,14 @@ static const char *const s_completion_candidates[] = {
|
|||||||
"wifi nslookup",
|
"wifi nslookup",
|
||||||
"wifi traceroute",
|
"wifi traceroute",
|
||||||
|
|
||||||
|
/* Station mDNS hostname configuration. */
|
||||||
|
"mdns status",
|
||||||
|
"mdns suffix",
|
||||||
|
"mdns save",
|
||||||
|
"mdns load",
|
||||||
|
"mdns defaults",
|
||||||
|
"mdns reset",
|
||||||
|
|
||||||
/* Authenticated HTTPS lifecycle and physical-admin recovery operations. */
|
/* Authenticated HTTPS lifecycle and physical-admin recovery operations. */
|
||||||
"web help",
|
"web help",
|
||||||
"web status",
|
"web status",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ dependencies:
|
|||||||
idf: ">=5.3.0"
|
idf: ">=5.3.0"
|
||||||
espressif/led_strip: "^3.0.3"
|
espressif/led_strip: "^3.0.3"
|
||||||
espressif/esp_tinyusb: "^2.2.1"
|
espressif/esp_tinyusb: "^2.2.1"
|
||||||
|
espressif/mdns: "^1.8.2"
|
||||||
# Exact official registry versions form the reviewed Phase 6 integration baseline.
|
# Exact official registry versions form the reviewed Phase 6 integration baseline.
|
||||||
wolfssl/wolfssl: "5.8.2~1"
|
wolfssl/wolfssl: "5.8.2~1"
|
||||||
wolfssl/wolfssh: "1.4.20"
|
wolfssl/wolfssh: "1.4.20"
|
||||||
|
|||||||
+23
@@ -14,6 +14,9 @@
|
|||||||
#include "local_ui_config.h"
|
#include "local_ui_config.h"
|
||||||
#include "local_ui_console.h"
|
#include "local_ui_console.h"
|
||||||
#include "local_ui_hw_test.h"
|
#include "local_ui_hw_test.h"
|
||||||
|
#include "mdns_config.h"
|
||||||
|
#include "mdns_console.h"
|
||||||
|
#include "mdns_service.h"
|
||||||
#include "rs232_hw_test.h"
|
#include "rs232_hw_test.h"
|
||||||
#include "rs232_port_owner.h"
|
#include "rs232_port_owner.h"
|
||||||
#include "secure_random.h"
|
#include "secure_random.h"
|
||||||
@@ -215,6 +218,23 @@ void app_main(void)
|
|||||||
"Stored Wi-Fi configuration is incompatible; using RAM defaults without overwriting it");
|
"Stored Wi-Fi configuration is incompatible; using RAM defaults without overwriting it");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mdns_config_t mdns_config;
|
||||||
|
bool used_stored_mdns_config = false;
|
||||||
|
esp_err_t mdns_config_error = mdns_config_load(&mdns_config, &used_stored_mdns_config);
|
||||||
|
if (mdns_config_error != ESP_OK) {
|
||||||
|
mdns_config_defaults(&mdns_config);
|
||||||
|
ESP_LOGW(TAG, "NVS mDNS configuration unavailable (%s); using RAM defaults",
|
||||||
|
esp_err_to_name(mdns_config_error));
|
||||||
|
}
|
||||||
|
esp_err_t mdns_service_error = mdns_service_init(&mdns_config);
|
||||||
|
if (mdns_service_error != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "mDNS configuration service unavailable: %s; Wi-Fi will continue",
|
||||||
|
esp_err_to_name(mdns_service_error));
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Using %s mDNS suffix sak-%s.local",
|
||||||
|
used_stored_mdns_config ? "stored" : "default", mdns_config.suffix);
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t wifi_error = wifi_config_error;
|
esp_err_t wifi_error = wifi_config_error;
|
||||||
if (wifi_config_error == ESP_OK) {
|
if (wifi_config_error == ESP_OK) {
|
||||||
wifi_error = wifi_manager_init(&wifi_config);
|
wifi_error = wifi_manager_init(&wifi_config);
|
||||||
@@ -301,6 +321,9 @@ void app_main(void)
|
|||||||
ESP_ERROR_CHECK(usb_console_register_commands());
|
ESP_ERROR_CHECK(usb_console_register_commands());
|
||||||
ESP_ERROR_CHECK(user_console_register_commands());
|
ESP_ERROR_CHECK(user_console_register_commands());
|
||||||
ESP_ERROR_CHECK(wifi_console_register_commands());
|
ESP_ERROR_CHECK(wifi_console_register_commands());
|
||||||
|
if (mdns_service_error == ESP_OK) {
|
||||||
|
ESP_ERROR_CHECK(mdns_console_register_commands());
|
||||||
|
}
|
||||||
ESP_ERROR_CHECK(web_console_register_commands());
|
ESP_ERROR_CHECK(web_console_register_commands());
|
||||||
ESP_ERROR_CHECK(ssh_console_register_commands());
|
ESP_ERROR_CHECK(ssh_console_register_commands());
|
||||||
ESP_ERROR_CHECK(network_console_register_root_commands());
|
ESP_ERROR_CHECK(network_console_register_root_commands());
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* Versioned persistent configuration for the station mDNS hostname. */
|
||||||
|
|
||||||
|
#include "mdns_config.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "esp_mac.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
|
||||||
|
_Static_assert(sizeof(mdns_config_t) == MDNS_CONFIG_BLOB_SIZE,
|
||||||
|
"mDNS config schema size changed");
|
||||||
|
|
||||||
|
static bool suffix_character_is_valid(char character)
|
||||||
|
{
|
||||||
|
return (character >= 'a' && character <= 'z') ||
|
||||||
|
(character >= '0' && character <= '9') || character == '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
void mdns_config_defaults(mdns_config_t *config)
|
||||||
|
{
|
||||||
|
static const char hex[] = "0123456789abcdef";
|
||||||
|
uint8_t mac[6] = {0};
|
||||||
|
|
||||||
|
if (config == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(config, 0, sizeof(*config));
|
||||||
|
config->schema_version = MDNS_CONFIG_SCHEMA_VERSION;
|
||||||
|
config->blob_size = MDNS_CONFIG_BLOB_SIZE;
|
||||||
|
if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (size_t index = 0U; index < sizeof(mac); ++index) {
|
||||||
|
config->suffix[index * 2U] = hex[mac[index] >> 4U];
|
||||||
|
config->suffix[index * 2U + 1U] = hex[mac[index] & 0x0fU];
|
||||||
|
}
|
||||||
|
config->suffix_len = sizeof(mac) * 2U;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_config_validate(const mdns_config_t *config)
|
||||||
|
{
|
||||||
|
if (config == NULL || config->schema_version != MDNS_CONFIG_SCHEMA_VERSION ||
|
||||||
|
config->blob_size != MDNS_CONFIG_BLOB_SIZE || config->suffix_len == 0U ||
|
||||||
|
config->suffix_len > MDNS_CONFIG_SUFFIX_MAX_LEN || config->reserved != 0U ||
|
||||||
|
config->suffix[config->suffix_len] != '\0') {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (config->suffix[0] == '-' || config->suffix[config->suffix_len - 1U] == '-') {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
for (size_t index = 0U; index < MDNS_CONFIG_SUFFIX_MAX_LEN; ++index) {
|
||||||
|
if (index < config->suffix_len) {
|
||||||
|
if (!suffix_character_is_valid(config->suffix[index])) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
} else if (config->suffix[index] != '\0') {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_config_load(mdns_config_t *config, bool *used_stored_config)
|
||||||
|
{
|
||||||
|
if (config == NULL || used_stored_config == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
mdns_config_defaults(config);
|
||||||
|
*used_stored_config = false;
|
||||||
|
if (mdns_config_validate(config) != ESP_OK) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t error = nvs_flash_init();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
nvs_handle_t handle;
|
||||||
|
error = nvs_open(MDNS_CONFIG_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||||
|
if (error == ESP_ERR_NVS_NOT_FOUND) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
size_t size = 0U;
|
||||||
|
error = nvs_get_blob(handle, MDNS_CONFIG_NVS_BLOB_KEY, NULL, &size);
|
||||||
|
if (error == ESP_ERR_NVS_NOT_FOUND || error == ESP_ERR_NVS_TYPE_MISMATCH ||
|
||||||
|
(error == ESP_OK && size != sizeof(*config))) {
|
||||||
|
nvs_close(handle);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
nvs_close(handle);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
mdns_config_t stored = {0};
|
||||||
|
error = nvs_get_blob(handle, MDNS_CONFIG_NVS_BLOB_KEY, &stored, &size);
|
||||||
|
nvs_close(handle);
|
||||||
|
if (error == ESP_ERR_NVS_INVALID_LENGTH) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (size == sizeof(stored) && mdns_config_validate(&stored) == ESP_OK) {
|
||||||
|
*config = stored;
|
||||||
|
*used_stored_config = true;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_config_save(const mdns_config_t *config)
|
||||||
|
{
|
||||||
|
esp_err_t error = mdns_config_validate(config);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
error = nvs_flash_init();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
nvs_handle_t handle;
|
||||||
|
error = nvs_open(MDNS_CONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
error = nvs_set_blob(handle, MDNS_CONFIG_NVS_BLOB_KEY, config, sizeof(*config));
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = nvs_commit(handle);
|
||||||
|
}
|
||||||
|
nvs_close(handle);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_config_reset_storage(void)
|
||||||
|
{
|
||||||
|
mdns_config_t config;
|
||||||
|
mdns_config_defaults(&config);
|
||||||
|
return mdns_config_save(&config);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* Versioned persistent configuration for the station mDNS hostname. */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#define MDNS_CONFIG_SCHEMA_VERSION 1U
|
||||||
|
#define MDNS_CONFIG_BLOB_SIZE 64U
|
||||||
|
#define MDNS_CONFIG_SUFFIX_MAX_LEN 55U
|
||||||
|
#define MDNS_CONFIG_NVS_NAMESPACE "mdns_cfg"
|
||||||
|
#define MDNS_CONFIG_NVS_BLOB_KEY "config"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t schema_version;
|
||||||
|
uint16_t blob_size;
|
||||||
|
uint8_t suffix_len;
|
||||||
|
uint8_t reserved;
|
||||||
|
char suffix[MDNS_CONFIG_SUFFIX_MAX_LEN + 1U];
|
||||||
|
} mdns_config_t;
|
||||||
|
|
||||||
|
void mdns_config_defaults(mdns_config_t *config);
|
||||||
|
esp_err_t mdns_config_validate(const mdns_config_t *config);
|
||||||
|
esp_err_t mdns_config_load(mdns_config_t *config, bool *used_stored_config);
|
||||||
|
esp_err_t mdns_config_save(const mdns_config_t *config);
|
||||||
|
esp_err_t mdns_config_reset_storage(void);
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* Administration console for the station mDNS hostname. */
|
||||||
|
|
||||||
|
#include "mdns_console.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "esp_console.h"
|
||||||
|
#include "mdns_config.h"
|
||||||
|
#include "mdns_service.h"
|
||||||
|
#include "wifi_manager.h"
|
||||||
|
|
||||||
|
static void print_usage(void)
|
||||||
|
{
|
||||||
|
printf("Usage: mdns status|suffix <lowercase-suffix>|save|load|defaults|reset\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int request_reannounce(void)
|
||||||
|
{
|
||||||
|
esp_err_t error = wifi_manager_mdns_reannounce();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("mDNS configuration updated; it will be used when Wi-Fi STA receives an IP (%s).\n",
|
||||||
|
esp_err_to_name(error));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int show_status(void)
|
||||||
|
{
|
||||||
|
mdns_service_snapshot_t snapshot;
|
||||||
|
esp_err_t error = mdns_service_get_snapshot(&snapshot);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("mDNS unavailable: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("mDNS: initialized=%s announced=%s hostname=%s.local suffix=%s last-error=%s\n",
|
||||||
|
snapshot.initialized ? "yes" : "no", snapshot.announced ? "yes" : "no",
|
||||||
|
snapshot.hostname, snapshot.suffix, esp_err_to_name(snapshot.last_error));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int set_suffix(const char *suffix)
|
||||||
|
{
|
||||||
|
size_t length = strlen(suffix);
|
||||||
|
mdns_config_t config;
|
||||||
|
esp_err_t error = mdns_service_get_config(&config);
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
memset(config.suffix, 0, sizeof(config.suffix));
|
||||||
|
if (length <= MDNS_CONFIG_SUFFIX_MAX_LEN) {
|
||||||
|
memcpy(config.suffix, suffix, length);
|
||||||
|
config.suffix_len = (uint8_t)length;
|
||||||
|
error = mdns_service_set_config(&config);
|
||||||
|
} else {
|
||||||
|
error = ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Suffix must be 1..%u lowercase letters, digits, or hyphens, and cannot begin or end with a hyphen.\n",
|
||||||
|
MDNS_CONFIG_SUFFIX_MAX_LEN);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("mDNS suffix updated in RAM; hostname is sak-%s.local; use 'mdns save' to persist it.\n",
|
||||||
|
suffix);
|
||||||
|
return request_reannounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int save_config(void)
|
||||||
|
{
|
||||||
|
mdns_config_t config;
|
||||||
|
esp_err_t error = mdns_service_get_config(&config);
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = mdns_config_save(&config);
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not save mDNS configuration: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("mDNS configuration saved to NVS.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int load_config(void)
|
||||||
|
{
|
||||||
|
mdns_config_t config;
|
||||||
|
bool stored = false;
|
||||||
|
esp_err_t error = mdns_config_load(&config, &stored);
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = mdns_service_set_config(&config);
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not load mDNS configuration: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("Loaded %s mDNS configuration into RAM.\n", stored ? "stored" : "default");
|
||||||
|
return request_reannounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int apply_defaults(bool persist)
|
||||||
|
{
|
||||||
|
mdns_config_t config;
|
||||||
|
mdns_config_defaults(&config);
|
||||||
|
esp_err_t error = mdns_service_set_config(&config);
|
||||||
|
if (error == ESP_OK && persist) {
|
||||||
|
error = mdns_config_save(&config);
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
printf("Could not apply mDNS defaults: %s\n", esp_err_to_name(error));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("MAC-derived mDNS defaults applied%s.\n", persist ? " and saved" : " in RAM");
|
||||||
|
return request_reannounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int command_mdns(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
|
||||||
|
return show_status();
|
||||||
|
}
|
||||||
|
if (argc == 3 && strcmp(argv[1], "suffix") == 0) {
|
||||||
|
return set_suffix(argv[2]);
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "save") == 0) {
|
||||||
|
return save_config();
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "load") == 0) {
|
||||||
|
return load_config();
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "defaults") == 0) {
|
||||||
|
return apply_defaults(false);
|
||||||
|
}
|
||||||
|
if (argc == 2 && strcmp(argv[1], "reset") == 0) {
|
||||||
|
return apply_defaults(true);
|
||||||
|
}
|
||||||
|
print_usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_console_register_commands(void)
|
||||||
|
{
|
||||||
|
const esp_console_cmd_t command = {
|
||||||
|
.command = "mdns",
|
||||||
|
.help = "Configure the STA mDNS hostname; use 'mdns' for status",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &command_mdns,
|
||||||
|
.argtable = NULL,
|
||||||
|
};
|
||||||
|
return esp_console_cmd_register(&command);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
esp_err_t mdns_console_register_commands(void);
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* mDNS runtime service; its lifecycle is owned by wifi_manager. */
|
||||||
|
|
||||||
|
#include "mdns_service.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "mdns.h"
|
||||||
|
|
||||||
|
static SemaphoreHandle_t s_mutex;
|
||||||
|
static mdns_config_t s_config;
|
||||||
|
static bool s_component_initialized;
|
||||||
|
static bool s_initialization_failed;
|
||||||
|
static bool s_announced;
|
||||||
|
static esp_err_t s_last_error;
|
||||||
|
|
||||||
|
static void lock_service(void)
|
||||||
|
{
|
||||||
|
(void)xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void unlock_service(void)
|
||||||
|
{
|
||||||
|
(void)xSemaphoreGive(s_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void make_hostname(const mdns_config_t *config, char *hostname, size_t size)
|
||||||
|
{
|
||||||
|
(void)snprintf(hostname, size, "sak-%s", config->suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_service_init(const mdns_config_t *config)
|
||||||
|
{
|
||||||
|
if (mdns_config_validate(config) != ESP_OK) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (s_mutex != NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
s_mutex = xSemaphoreCreateMutex();
|
||||||
|
if (s_mutex == NULL) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
s_config = *config;
|
||||||
|
s_last_error = ESP_OK;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_service_get_config(mdns_config_t *config)
|
||||||
|
{
|
||||||
|
if (config == NULL || s_mutex == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
lock_service();
|
||||||
|
*config = s_config;
|
||||||
|
unlock_service();
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_service_set_config(const mdns_config_t *config)
|
||||||
|
{
|
||||||
|
if (s_mutex == NULL || mdns_config_validate(config) != ESP_OK) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
lock_service();
|
||||||
|
s_config = *config;
|
||||||
|
unlock_service();
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_service_get_snapshot(mdns_service_snapshot_t *snapshot)
|
||||||
|
{
|
||||||
|
if (snapshot == NULL || s_mutex == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
lock_service();
|
||||||
|
memset(snapshot, 0, sizeof(*snapshot));
|
||||||
|
snapshot->initialized = true;
|
||||||
|
snapshot->announced = s_announced;
|
||||||
|
memcpy(snapshot->suffix, s_config.suffix, s_config.suffix_len);
|
||||||
|
make_hostname(&s_config, snapshot->hostname, sizeof(snapshot->hostname));
|
||||||
|
snapshot->last_error = s_last_error;
|
||||||
|
unlock_service();
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_service_start(void)
|
||||||
|
{
|
||||||
|
if (s_mutex == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
lock_service();
|
||||||
|
if (s_component_initialized) {
|
||||||
|
s_announced = true;
|
||||||
|
unlock_service();
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (s_initialization_failed) {
|
||||||
|
esp_err_t error = s_last_error;
|
||||||
|
unlock_service();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
mdns_config_t config = s_config;
|
||||||
|
unlock_service();
|
||||||
|
|
||||||
|
esp_err_t error = mdns_init();
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
char hostname[MDNS_CONFIG_SUFFIX_MAX_LEN + 5U] = {0};
|
||||||
|
make_hostname(&config, hostname, sizeof(hostname));
|
||||||
|
error = mdns_hostname_set(hostname);
|
||||||
|
if (error == ESP_OK) {
|
||||||
|
error = mdns_instance_name_set("ESP32 Serial Swiss Army Knife");
|
||||||
|
}
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
mdns_free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock_service();
|
||||||
|
s_component_initialized = error == ESP_OK;
|
||||||
|
s_initialization_failed = error != ESP_OK;
|
||||||
|
s_announced = error == ESP_OK;
|
||||||
|
s_last_error = error;
|
||||||
|
unlock_service();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mdns_service_stop(void)
|
||||||
|
{
|
||||||
|
if (s_mutex == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lock_service();
|
||||||
|
s_announced = false;
|
||||||
|
unlock_service();
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t mdns_service_reannounce(void)
|
||||||
|
{
|
||||||
|
if (s_mutex == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
lock_service();
|
||||||
|
if (!s_component_initialized) {
|
||||||
|
esp_err_t error = s_initialization_failed ? s_last_error : ESP_ERR_INVALID_STATE;
|
||||||
|
unlock_service();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
mdns_config_t config = s_config;
|
||||||
|
unlock_service();
|
||||||
|
|
||||||
|
char hostname[MDNS_CONFIG_SUFFIX_MAX_LEN + 5U] = {0};
|
||||||
|
make_hostname(&config, hostname, sizeof(hostname));
|
||||||
|
esp_err_t error = mdns_hostname_set(hostname);
|
||||||
|
|
||||||
|
lock_service();
|
||||||
|
s_last_error = error;
|
||||||
|
unlock_service();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||||
|
/* mDNS runtime service; its lifecycle is owned by wifi_manager. */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "mdns_config.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool initialized;
|
||||||
|
bool announced;
|
||||||
|
char suffix[MDNS_CONFIG_SUFFIX_MAX_LEN + 1U];
|
||||||
|
char hostname[MDNS_CONFIG_SUFFIX_MAX_LEN + 5U];
|
||||||
|
esp_err_t last_error;
|
||||||
|
} mdns_service_snapshot_t;
|
||||||
|
|
||||||
|
esp_err_t mdns_service_init(const mdns_config_t *config);
|
||||||
|
esp_err_t mdns_service_get_config(mdns_config_t *config);
|
||||||
|
esp_err_t mdns_service_set_config(const mdns_config_t *config);
|
||||||
|
esp_err_t mdns_service_get_snapshot(mdns_service_snapshot_t *snapshot);
|
||||||
|
|
||||||
|
/* Only wifi_manager may call these lifecycle operations. */
|
||||||
|
esp_err_t mdns_service_start(void);
|
||||||
|
void mdns_service_stop(void);
|
||||||
|
esp_err_t mdns_service_reannounce(void);
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
#include "mdns_service.h"
|
||||||
|
|
||||||
#define WIFI_MANAGER_QUEUE_LENGTH 16U
|
#define WIFI_MANAGER_QUEUE_LENGTH 16U
|
||||||
#define WIFI_MANAGER_TASK_STACK_SIZE 6144U
|
#define WIFI_MANAGER_TASK_STACK_SIZE 6144U
|
||||||
@@ -35,6 +36,7 @@ typedef enum {
|
|||||||
MESSAGE_COMMAND_APPLY,
|
MESSAGE_COMMAND_APPLY,
|
||||||
MESSAGE_COMMAND_RECONNECT,
|
MESSAGE_COMMAND_RECONNECT,
|
||||||
MESSAGE_COMMAND_NEXT_PROFILE,
|
MESSAGE_COMMAND_NEXT_PROFILE,
|
||||||
|
MESSAGE_COMMAND_MDNS_REANNOUNCE,
|
||||||
MESSAGE_STA_CONNECTED,
|
MESSAGE_STA_CONNECTED,
|
||||||
MESSAGE_STA_DISCONNECTED,
|
MESSAGE_STA_DISCONNECTED,
|
||||||
MESSAGE_STA_GOT_IP,
|
MESSAGE_STA_GOT_IP,
|
||||||
@@ -109,6 +111,15 @@ static void manager_task(void *context);
|
|||||||
static void start_profile_cycle(manager_runtime_t *runtime);
|
static void start_profile_cycle(manager_runtime_t *runtime);
|
||||||
static void start_next_profile(manager_runtime_t *runtime);
|
static void start_next_profile(manager_runtime_t *runtime);
|
||||||
|
|
||||||
|
static void start_mdns_announcement(void)
|
||||||
|
{
|
||||||
|
esp_err_t error = mdns_service_start();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
/* Name discovery is optional; never make network or serial recovery depend on it. */
|
||||||
|
ESP_LOGW(TAG, "mDNS announcement unavailable: %s", esp_err_to_name(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void lock_shared(void)
|
static void lock_shared(void)
|
||||||
{
|
{
|
||||||
(void)xSemaphoreTake(s_mutex, portMAX_DELAY);
|
(void)xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||||
@@ -410,6 +421,7 @@ static void mark_intentional_disconnect(manager_runtime_t *runtime)
|
|||||||
|
|
||||||
static void stop_radio(manager_runtime_t *runtime)
|
static void stop_radio(manager_runtime_t *runtime)
|
||||||
{
|
{
|
||||||
|
mdns_service_stop();
|
||||||
if (!runtime->radio_started) {
|
if (!runtime->radio_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -744,6 +756,7 @@ static void handle_got_ip(manager_runtime_t *runtime,
|
|||||||
unlock_shared();
|
unlock_shared();
|
||||||
|
|
||||||
wifi_config_secure_wipe(&config, sizeof(config));
|
wifi_config_secure_wipe(&config, sizeof(config));
|
||||||
|
start_mdns_announcement();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handle_sta_disconnected(manager_runtime_t *runtime,
|
static void handle_sta_disconnected(manager_runtime_t *runtime,
|
||||||
@@ -764,6 +777,7 @@ static void handle_sta_disconnected(manager_runtime_t *runtime,
|
|||||||
bool had_attempt = runtime->attempt_deadline != 0;
|
bool had_attempt = runtime->attempt_deadline != 0;
|
||||||
bool was_online = runtime->online;
|
bool was_online = runtime->online;
|
||||||
|
|
||||||
|
mdns_service_stop();
|
||||||
runtime->associated = false;
|
runtime->associated = false;
|
||||||
runtime->online = false;
|
runtime->online = false;
|
||||||
runtime->attempt_deadline = 0;
|
runtime->attempt_deadline = 0;
|
||||||
@@ -868,6 +882,16 @@ static void handle_message(manager_runtime_t *runtime,
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MESSAGE_COMMAND_MDNS_REANNOUNCE:
|
||||||
|
if (runtime->online) {
|
||||||
|
esp_err_t error = mdns_service_reannounce();
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "mDNS reannouncement unavailable: %s",
|
||||||
|
esp_err_to_name(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case MESSAGE_STA_CONNECTED:
|
case MESSAGE_STA_CONNECTED:
|
||||||
if (!manager_is_started() ||
|
if (!manager_is_started() ||
|
||||||
!connected_event_matches_active_profile(message)) {
|
!connected_event_matches_active_profile(message)) {
|
||||||
@@ -900,6 +924,7 @@ static void handle_message(manager_runtime_t *runtime,
|
|||||||
/* Ignore a delayed loss event after a newer DHCP lease. */
|
/* Ignore a delayed loss event after a newer DHCP lease. */
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
mdns_service_stop();
|
||||||
runtime->online = false;
|
runtime->online = false;
|
||||||
runtime->stable_deadline = 0;
|
runtime->stable_deadline = 0;
|
||||||
runtime->attempt_deadline = esp_timer_get_time() + WIFI_MANAGER_ATTEMPT_US;
|
runtime->attempt_deadline = esp_timer_get_time() + WIFI_MANAGER_ATTEMPT_US;
|
||||||
@@ -1465,6 +1490,11 @@ esp_err_t wifi_manager_next_profile(void)
|
|||||||
return enqueue_lifecycle_command(MESSAGE_COMMAND_NEXT_PROFILE, -1);
|
return enqueue_lifecycle_command(MESSAGE_COMMAND_NEXT_PROFILE, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_manager_mdns_reannounce(void)
|
||||||
|
{
|
||||||
|
return enqueue_lifecycle_command(MESSAGE_COMMAND_MDNS_REANNOUNCE, -1);
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot)
|
esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot)
|
||||||
{
|
{
|
||||||
if (snapshot == NULL) {
|
if (snapshot == NULL) {
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ esp_err_t wifi_manager_stop(void);
|
|||||||
esp_err_t wifi_manager_reconnect(void);
|
esp_err_t wifi_manager_reconnect(void);
|
||||||
/* Advance to the next enabled station profile in priority order, wrapping safely. */
|
/* Advance to the next enabled station profile in priority order, wrapping safely. */
|
||||||
esp_err_t wifi_manager_next_profile(void);
|
esp_err_t wifi_manager_next_profile(void);
|
||||||
|
/* Reannounce the configured hostname when the manager currently has a STA IP. */
|
||||||
|
esp_err_t wifi_manager_mdns_reannounce(void);
|
||||||
|
|
||||||
/* Snapshot data never contains station or AP passwords. */
|
/* Snapshot data never contains station or AP passwords. */
|
||||||
esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot);
|
esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot);
|
||||||
|
|||||||
Reference in New Issue
Block a user