From 3feb3b5916981ae9db6397369cb472e30102a5d4 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 31 Aug 2026 04:14:38 +0200 Subject: [PATCH] Add configurable Wi-Fi station mDNS hostname support. Mini-Feature #1 --- dependencies.lock | 13 +++- docs/agent/architecture.md | 3 +- docs/agent/code-map.md | 5 +- docs/agent/current-state.md | 14 ++-- docs/command_reference.md | 11 +++ src/CMakeLists.txt | 4 + src/console_completion.c | 10 ++- src/idf_component.yml | 1 + src/main.c | 23 ++++++ src/mdns_config.c | 145 +++++++++++++++++++++++++++++++++++ src/mdns_config.h | 29 +++++++ src/mdns_console.c | 149 ++++++++++++++++++++++++++++++++++++ src/mdns_console.h | 6 ++ src/mdns_service.c | 133 ++++++++++++++++++++++++++++++++ src/mdns_service.h | 26 +++++++ src/wifi_manager.c | 27 +++++++ src/wifi_manager.h | 2 + 17 files changed, 589 insertions(+), 12 deletions(-) create mode 100644 src/mdns_config.c create mode 100644 src/mdns_config.h create mode 100644 src/mdns_console.c create mode 100644 src/mdns_console.h create mode 100644 src/mdns_service.c create mode 100644 src/mdns_service.h diff --git a/dependencies.lock b/dependencies.lock index 0761c32..bec197d 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -29,6 +29,16 @@ dependencies: registry_url: https://components.espressif.com/ type: service 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: component_hash: a72b7d67472914ab76309340fd50d578b31e310963d45ad0f81144bde3314752 dependencies: @@ -66,9 +76,10 @@ dependencies: direct_dependencies: - espressif/esp_tinyusb - espressif/led_strip +- espressif/mdns - idf - wolfssl/wolfssh - wolfssl/wolfssl -manifest_hash: 34e93560f67a22e2a43647a4663b450e07ecd9fa3fdc2ac38343c1a76b513f5d +manifest_hash: 4b6fa5a7e06122f9194adfc282a4baf4247d8a114b905e65f6397503e59f0898 target: esp32s3 version: 2.0.0 diff --git a/docs/agent/architecture.md b/docs/agent/architecture.md index c42a9b0..27eaf5a 100644 --- a/docs/agent/architecture.md +++ b/docs/agent/architecture.md @@ -159,12 +159,13 @@ Remote reboot, SSH stop/disconnect, and host-key rotate/reset use deferred contr `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` announces the configured hostname only after a validated STA `GOT_IP` transition and is stopped when that IPv4 reachability is lost or the radio is stopped; 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: - `serial/config`; - `wifi_app/config`; +- `mdns_cfg/config`; - `local_ui/config`; - `web_sec/material`; - `user_db/database`; diff --git a/docs/agent/code-map.md b/docs/agent/code-map.md index 70d4ec2..a7328b4 100644 --- a/docs/agent/code-map.md +++ b/docs/agent/code-map.md @@ -112,10 +112,10 @@ This is a semantic map, not a complete file inventory. Start here, then read the **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 - 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. - 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 | | 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 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 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 | diff --git a/docs/agent/current-state.md b/docs/agent/current-state.md index 47b57fa..d7fc406 100644 --- a/docs/agent/current-state.md +++ b/docs/agent/current-state.md @@ -46,13 +46,13 @@ These observations should be checked when touching the relevant area; they are n ## 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. -- **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. -- **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. -- **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. -- **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. -- **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. -- **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. +- **Objective:** Announce a configurable `sak-.local` hostname through mDNS when Wi-Fi STA has an IPv4 address, without changing the Wi-Fi NVS blob schema. +- **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:** `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:** 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 starts mDNS after validating `IP_EVENT_STA_GOT_IP`, stops it on IPv4 loss, STA disconnect, and radio stop, and performs online reannouncements requested after configuration changes. mDNS errors are logged but cannot fail Wi-Fi, UART0, UART1, or native USB. +- **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. `pio run` passes. +- **Remaining work:** Target-hardware verification: associate a station and resolve the default `sak-.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:** 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. ### Handoff template diff --git a/docs/command_reference.md b/docs/command_reference.md index d8cc831..3604745 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -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. +## mDNS + +| Command | Description | +|---|---| +| `mdns status` | Show the configured `sak-.local` hostname and announcement state. | +| `mdns suffix ` | 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-.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 | Command | Description | diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a7d6d74..480689a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,6 +38,9 @@ idf_component_register( "wifi_config.c" "wifi_manager.c" "wifi_console.c" + "mdns_config.c" + "mdns_service.c" + "mdns_console.c" INCLUDE_DIRS "." REQUIRES bootloader_support @@ -58,6 +61,7 @@ idf_component_register( led_strip lwip mbedtls + mdns nvs_flash wolfssl__wolfssh wolfssl__wolfssl diff --git a/src/console_completion.c b/src/console_completion.c index 85a5be8..bb22583 100644 --- a/src/console_completion.c +++ b/src/console_completion.c @@ -12,7 +12,7 @@ static const char *const s_root_candidates[] = { "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. */ @@ -169,6 +169,14 @@ static const char *const s_completion_candidates[] = { "wifi nslookup", "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. */ "web help", "web status", diff --git a/src/idf_component.yml b/src/idf_component.yml index df70281..fa77fcd 100644 --- a/src/idf_component.yml +++ b/src/idf_component.yml @@ -4,6 +4,7 @@ dependencies: idf: ">=5.3.0" espressif/led_strip: "^3.0.3" espressif/esp_tinyusb: "^2.2.1" + espressif/mdns: "^1.8.2" # Exact official registry versions form the reviewed Phase 6 integration baseline. wolfssl/wolfssl: "5.8.2~1" wolfssl/wolfssh: "1.4.20" diff --git a/src/main.c b/src/main.c index c91b73f..57c298e 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,9 @@ #include "local_ui_config.h" #include "local_ui_console.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_port_owner.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"); } + 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; if (wifi_config_error == ESP_OK) { 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(user_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(ssh_console_register_commands()); ESP_ERROR_CHECK(network_console_register_root_commands()); diff --git a/src/mdns_config.c b/src/mdns_config.c new file mode 100644 index 0000000..b8216c6 --- /dev/null +++ b/src/mdns_config.c @@ -0,0 +1,145 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Versioned persistent configuration for the station mDNS hostname. */ + +#include "mdns_config.h" + +#include +#include + +#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); +} diff --git a/src/mdns_config.h b/src/mdns_config.h new file mode 100644 index 0000000..0a165b6 --- /dev/null +++ b/src/mdns_config.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Versioned persistent configuration for the station mDNS hostname. */ + +#pragma once + +#include +#include + +#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); diff --git a/src/mdns_console.c b/src/mdns_console.c new file mode 100644 index 0000000..92ed33a --- /dev/null +++ b/src/mdns_console.c @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Administration console for the station mDNS hostname. */ + +#include "mdns_console.h" + +#include +#include + +#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 |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); +} diff --git a/src/mdns_console.h b/src/mdns_console.h new file mode 100644 index 0000000..63ac1ee --- /dev/null +++ b/src/mdns_console.h @@ -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); diff --git a/src/mdns_service.c b/src/mdns_service.c new file mode 100644 index 0000000..a8884c0 --- /dev/null +++ b/src/mdns_service.c @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* mDNS runtime service; its lifecycle is owned by wifi_manager. */ + +#include "mdns_service.h" + +#include +#include + +#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_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_announced) { + unlock_service(); + return ESP_OK; + } + 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_announced = error == ESP_OK; + s_last_error = error; + unlock_service(); + return error; +} + +void mdns_service_stop(void) +{ + if (s_mutex == NULL) { + return; + } + lock_service(); + bool announced = s_announced; + s_announced = false; + unlock_service(); + if (announced) { + mdns_free(); + } +} diff --git a/src/mdns_service.h b/src/mdns_service.h new file mode 100644 index 0000000..2455a5f --- /dev/null +++ b/src/mdns_service.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* mDNS runtime service; its lifecycle is owned by wifi_manager. */ + +#pragma once + +#include + +#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); diff --git a/src/wifi_manager.c b/src/wifi_manager.c index 5e4436b..62941fd 100644 --- a/src/wifi_manager.c +++ b/src/wifi_manager.c @@ -17,6 +17,7 @@ #include "freertos/queue.h" #include "freertos/semphr.h" #include "freertos/task.h" +#include "mdns_service.h" #define WIFI_MANAGER_QUEUE_LENGTH 16U #define WIFI_MANAGER_TASK_STACK_SIZE 6144U @@ -35,6 +36,7 @@ typedef enum { MESSAGE_COMMAND_APPLY, MESSAGE_COMMAND_RECONNECT, MESSAGE_COMMAND_NEXT_PROFILE, + MESSAGE_COMMAND_MDNS_REANNOUNCE, MESSAGE_STA_CONNECTED, MESSAGE_STA_DISCONNECTED, 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_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) { (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) { + mdns_service_stop(); if (!runtime->radio_started) { return; } @@ -744,6 +756,7 @@ static void handle_got_ip(manager_runtime_t *runtime, unlock_shared(); wifi_config_secure_wipe(&config, sizeof(config)); + start_mdns_announcement(); } 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 was_online = runtime->online; + mdns_service_stop(); runtime->associated = false; runtime->online = false; runtime->attempt_deadline = 0; @@ -868,6 +882,13 @@ static void handle_message(manager_runtime_t *runtime, } break; + case MESSAGE_COMMAND_MDNS_REANNOUNCE: + if (runtime->online) { + mdns_service_stop(); + start_mdns_announcement(); + } + break; + case MESSAGE_STA_CONNECTED: if (!manager_is_started() || !connected_event_matches_active_profile(message)) { @@ -900,6 +921,7 @@ static void handle_message(manager_runtime_t *runtime, /* Ignore a delayed loss event after a newer DHCP lease. */ break; } + mdns_service_stop(); runtime->online = false; runtime->stable_deadline = 0; runtime->attempt_deadline = esp_timer_get_time() + WIFI_MANAGER_ATTEMPT_US; @@ -1465,6 +1487,11 @@ esp_err_t wifi_manager_next_profile(void) 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) { if (snapshot == NULL) { diff --git a/src/wifi_manager.h b/src/wifi_manager.h index 7ca9b71..9ab85a7 100644 --- a/src/wifi_manager.h +++ b/src/wifi_manager.h @@ -95,6 +95,8 @@ esp_err_t wifi_manager_stop(void); esp_err_t wifi_manager_reconnect(void); /* Advance to the next enabled station profile in priority order, wrapping safely. */ 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. */ esp_err_t wifi_manager_get_snapshot(wifi_manager_snapshot_t *snapshot);