From 6c283553f943feb3b1990cfe89fc0ec03394aca5 Mon Sep 17 00:00:00 2001 From: crispyberry Date: Sun, 8 Mar 2026 12:15:26 +0800 Subject: [PATCH 1/4] feat: add WiFi onboarding captive portal When no WiFi credentials are configured or connection fails, the device automatically starts a soft AP (MimiClaw-XXXX) with a captive portal. Users can configure WiFi, LLM, Telegram, Feishu, Proxy and Search settings via a mobile-friendly web page, then the device restarts and connects with the new credentials. Co-Authored-By: Claude Opus 4.6 --- main/CMakeLists.txt | 1 + main/mimi.c | 49 +++-- main/mimi_config.h | 8 + main/onboard/onboard_html.h | 136 ++++++++++++++ main/onboard/wifi_onboard.c | 365 ++++++++++++++++++++++++++++++++++++ main/onboard/wifi_onboard.h | 10 + main/wifi/wifi_manager.c | 32 +++- main/wifi/wifi_manager.h | 10 + 8 files changed, 590 insertions(+), 21 deletions(-) create mode 100644 main/onboard/onboard_html.h create mode 100644 main/onboard/wifi_onboard.c create mode 100644 main/onboard/wifi_onboard.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 5f3fe1e..484c8bd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -21,6 +21,7 @@ idf_component_register( "tools/tool_get_time.c" "tools/tool_files.c" "skills/skill_loader.c" + "onboard/wifi_onboard.c" INCLUDE_DIRS "." REQUIRES diff --git a/main/mimi.c b/main/mimi.c index 0e8e8fa..3712b72 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -25,6 +25,7 @@ #include "cron/cron_service.h" #include "heartbeat/heartbeat.h" #include "skills/skill_loader.h" +#include "onboard/wifi_onboard.h" static const char *TAG = "mimi"; @@ -141,34 +142,44 @@ void app_main(void) /* Start WiFi */ esp_err_t wifi_err = wifi_manager_start(); + bool wifi_ok = false; if (wifi_err == ESP_OK) { ESP_LOGI(TAG, "Scanning nearby APs on boot..."); wifi_manager_scan_and_print(); ESP_LOGI(TAG, "Waiting for WiFi connection..."); if (wifi_manager_wait_connected(30000) == ESP_OK) { + wifi_ok = true; ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip()); - - /* Outbound dispatch task should start first to avoid dropping early replies. */ - ESP_ERROR_CHECK((xTaskCreatePinnedToCore( - outbound_dispatch_task, "outbound", - MIMI_OUTBOUND_STACK, NULL, - MIMI_OUTBOUND_PRIO, NULL, MIMI_OUTBOUND_CORE) == pdPASS) - ? ESP_OK : ESP_FAIL); - - /* Start network-dependent services */ - ESP_ERROR_CHECK(agent_loop_start()); - ESP_ERROR_CHECK(telegram_bot_start()); - ESP_ERROR_CHECK(feishu_bot_start()); - cron_service_start(); - heartbeat_start(); - ESP_ERROR_CHECK(ws_server_start()); - - ESP_LOGI(TAG, "All services started!"); } else { - ESP_LOGW(TAG, "WiFi connection timeout. Check MIMI_SECRET_WIFI_SSID in mimi_secrets.h"); + ESP_LOGW(TAG, "WiFi connection timeout"); } } else { - ESP_LOGW(TAG, "No WiFi credentials. Set MIMI_SECRET_WIFI_SSID in mimi_secrets.h"); + ESP_LOGW(TAG, "No WiFi credentials configured"); + } + + if (!wifi_ok) { + ESP_LOGW(TAG, "Entering WiFi onboarding mode..."); + wifi_onboard_start(); /* blocks, restarts on success */ + return; /* unreachable */ + } + + { + /* Outbound dispatch task should start first to avoid dropping early replies. */ + ESP_ERROR_CHECK((xTaskCreatePinnedToCore( + outbound_dispatch_task, "outbound", + MIMI_OUTBOUND_STACK, NULL, + MIMI_OUTBOUND_PRIO, NULL, MIMI_OUTBOUND_CORE) == pdPASS) + ? ESP_OK : ESP_FAIL); + + /* Start network-dependent services */ + ESP_ERROR_CHECK(agent_loop_start()); + ESP_ERROR_CHECK(telegram_bot_start()); + ESP_ERROR_CHECK(feishu_bot_start()); + cron_service_start(); + heartbeat_start(); + ESP_ERROR_CHECK(ws_server_start()); + + ESP_LOGI(TAG, "All services started!"); } ESP_LOGI(TAG, "MimiClaw ready. Type 'help' for CLI commands."); diff --git a/main/mimi_config.h b/main/mimi_config.h index 9be7c08..99522b6 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -149,3 +149,11 @@ #define MIMI_NVS_KEY_PROVIDER "provider" #define MIMI_NVS_KEY_PROXY_HOST "host" #define MIMI_NVS_KEY_PROXY_PORT "port" +#define MIMI_NVS_KEY_PROXY_TYPE "type" + +/* WiFi Onboarding (Captive Portal) */ +#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-" +#define MIMI_ONBOARD_AP_PASS "" /* open network */ +#define MIMI_ONBOARD_HTTP_PORT 80 +#define MIMI_ONBOARD_DNS_STACK (4 * 1024) +#define MIMI_ONBOARD_MAX_SCAN 20 diff --git a/main/onboard/onboard_html.h b/main/onboard/onboard_html.h new file mode 100644 index 0000000..7131b64 --- /dev/null +++ b/main/onboard/onboard_html.h @@ -0,0 +1,136 @@ +#pragma once + +static const char ONBOARD_HTML[] = +"" +"" +"" +"MimiClaw Setup" +"" +"

MimiClaw Setup

" + +/* WiFi section (expanded by default) */ +"
" +"
WiFi Configuration
" +"
" +"" +"" +"" +"" +"" +"" +"
" + +/* LLM section */ +"" + +/* Telegram section */ +"" + +/* Feishu section */ +"" + +/* Proxy section */ +"" + +/* Search section */ +"" + +"" +"
Saving... Device will restart.
" + +"" +""; diff --git a/main/onboard/wifi_onboard.c b/main/onboard/wifi_onboard.c new file mode 100644 index 0000000..2268539 --- /dev/null +++ b/main/onboard/wifi_onboard.c @@ -0,0 +1,365 @@ +#include "wifi_onboard.h" +#include "onboard_html.h" +#include "mimi_config.h" +#include "wifi/wifi_manager.h" + +#include +#include "esp_log.h" +#include "esp_wifi.h" +#include "esp_netif.h" +#include "esp_mac.h" +#include "esp_http_server.h" +#include "esp_system.h" +#include "nvs.h" +#include "cJSON.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "lwip/sockets.h" +#include "lwip/netdb.h" + +static const char *TAG = "onboard"; + +/* ── DNS hijack ─────────────────────────────────────────────────── */ + +/* Minimal DNS response: always answer 192.168.4.1 */ +static void dns_hijack_task(void *arg) +{ + int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + ESP_LOGE(TAG, "DNS socket error"); + vTaskDelete(NULL); + return; + } + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(53), + .sin_addr.s_addr = htonl(INADDR_ANY), + }; + + int opt = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + ESP_LOGE(TAG, "DNS bind failed"); + close(sock); + vTaskDelete(NULL); + return; + } + + ESP_LOGI(TAG, "DNS hijack listening on :53"); + + uint8_t buf[512]; + struct sockaddr_in client; + socklen_t client_len; + + while (1) { + client_len = sizeof(client); + int len = recvfrom(sock, buf, sizeof(buf), 0, + (struct sockaddr *)&client, &client_len); + if (len < 12) continue; /* too short for DNS header */ + + /* Build response: copy query, set response flags, append answer */ + uint8_t resp[512]; + if (len + 16 > (int)sizeof(resp)) continue; + + memcpy(resp, buf, len); + + /* Set QR=1 (response), AA=1 (authoritative), RA=1 */ + resp[2] = 0x85; /* QR=1, Opcode=0, AA=1, TC=0, RD=1 */ + resp[3] = 0x80; /* RA=1, Z=0, RCODE=0 (no error) */ + + /* Answer count = 1 */ + resp[6] = 0x00; + resp[7] = 0x01; + + /* Append answer: pointer to name + A record with 192.168.4.1 */ + int off = len; + resp[off++] = 0xC0; /* pointer */ + resp[off++] = 0x0C; /* offset to question name */ + resp[off++] = 0x00; resp[off++] = 0x01; /* type A */ + resp[off++] = 0x00; resp[off++] = 0x01; /* class IN */ + resp[off++] = 0x00; resp[off++] = 0x00; + resp[off++] = 0x00; resp[off++] = 0x3C; /* TTL = 60 */ + resp[off++] = 0x00; resp[off++] = 0x04; /* data length = 4 */ + resp[off++] = 192; resp[off++] = 168; + resp[off++] = 4; resp[off++] = 1; + + sendto(sock, resp, off, 0, + (struct sockaddr *)&client, client_len); + } +} + +/* ── HTTP handlers ──────────────────────────────────────────────── */ + +static esp_err_t http_get_root(httpd_req_t *req) +{ + httpd_resp_set_type(req, "text/html"); + httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + return httpd_resp_send(req, ONBOARD_HTML, sizeof(ONBOARD_HTML) - 1); +} + +/* Captive portal detection endpoints → redirect to root */ +static esp_err_t http_captive_redirect(httpd_req_t *req) +{ + httpd_resp_set_status(req, "302 Found"); + httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/"); + return httpd_resp_send(req, NULL, 0); +} + +static esp_err_t http_get_scan(httpd_req_t *req) +{ + wifi_scan_config_t scan_cfg = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = true, + }; + + esp_err_t err = esp_wifi_scan_start(&scan_cfg, true); + if (err != ESP_OK) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Scan failed"); + return ESP_FAIL; + } + + uint16_t ap_count = 0; + esp_wifi_scan_get_ap_num(&ap_count); + if (ap_count > MIMI_ONBOARD_MAX_SCAN) ap_count = MIMI_ONBOARD_MAX_SCAN; + + wifi_ap_record_t *ap_list = calloc(ap_count, sizeof(wifi_ap_record_t)); + if (!ap_list) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"); + return ESP_FAIL; + } + + uint16_t ap_max = ap_count; + esp_wifi_scan_get_ap_records(&ap_max, ap_list); + + cJSON *arr = cJSON_CreateArray(); + for (uint16_t i = 0; i < ap_max; i++) { + if (ap_list[i].ssid[0] == '\0') continue; /* skip hidden */ + cJSON *obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "ssid", (const char *)ap_list[i].ssid); + cJSON_AddNumberToObject(obj, "rssi", ap_list[i].rssi); + cJSON_AddNumberToObject(obj, "ch", ap_list[i].primary); + cJSON_AddBoolToObject(obj, "auth", ap_list[i].authmode != WIFI_AUTH_OPEN); + cJSON_AddItemToArray(arr, obj); + } + free(ap_list); + + char *json = cJSON_PrintUnformatted(arr); + cJSON_Delete(arr); + + httpd_resp_set_type(req, "application/json"); + esp_err_t ret = httpd_resp_send(req, json, strlen(json)); + free(json); + return ret; +} + +/* Helper: save a single NVS string if json field is present and non-empty */ +static void nvs_save_field(cJSON *root, const char *json_key, + const char *ns, const char *nvs_key) +{ + cJSON *item = cJSON_GetObjectItem(root, json_key); + if (!item || !cJSON_IsString(item) || item->valuestring[0] == '\0') return; + + nvs_handle_t nvs; + if (nvs_open(ns, NVS_READWRITE, &nvs) == ESP_OK) { + nvs_set_str(nvs, nvs_key, item->valuestring); + nvs_commit(nvs); + nvs_close(nvs); + ESP_LOGI(TAG, "Saved %s/%s", ns, nvs_key); + } +} + +static esp_err_t http_post_save(httpd_req_t *req) +{ + int total_len = req->content_len; + if (total_len <= 0 || total_len > 2048) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad length"); + return ESP_FAIL; + } + + char *buf = calloc(1, total_len + 1); + if (!buf) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"); + return ESP_FAIL; + } + + int received = 0; + while (received < total_len) { + int ret = httpd_req_recv(req, buf + received, total_len - received); + if (ret <= 0) { + free(buf); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Recv error"); + return ESP_FAIL; + } + received += ret; + } + + cJSON *root = cJSON_Parse(buf); + free(buf); + if (!root) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + return ESP_FAIL; + } + + /* WiFi (required) */ + nvs_save_field(root, "ssid", MIMI_NVS_WIFI, MIMI_NVS_KEY_SSID); + nvs_save_field(root, "password", MIMI_NVS_WIFI, MIMI_NVS_KEY_PASS); + + /* LLM */ + nvs_save_field(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY); + nvs_save_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL); + nvs_save_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER); + + /* Telegram */ + nvs_save_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN); + + /* Feishu */ + nvs_save_field(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID); + nvs_save_field(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET); + + /* Proxy */ + nvs_save_field(root, "proxy_host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST); + nvs_save_field(root, "proxy_port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT); + nvs_save_field(root, "proxy_type", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_TYPE); + + /* Search */ + nvs_save_field(root, "search_key", MIMI_NVS_SEARCH, "brave_key"); + nvs_save_field(root, "tavily_key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY); + + cJSON_Delete(root); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"ok\":true}", 11); + + ESP_LOGI(TAG, "Configuration saved, restarting in 2s..."); + vTaskDelay(pdMS_TO_TICKS(2000)); + esp_restart(); + + return ESP_OK; /* unreachable */ +} + +/* ── Soft AP + HTTP server startup ──────────────────────────────── */ + +static esp_err_t start_softap(void) +{ + /* Get last 2 bytes of MAC for unique SSID suffix */ + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP); + char ssid[32]; + snprintf(ssid, sizeof(ssid), "%s%02X%02X", MIMI_ONBOARD_AP_PREFIX, mac[4], mac[5]); + + /* Create AP netif if not already present */ + static esp_netif_t *ap_netif = NULL; + if (!ap_netif) { + ap_netif = esp_netif_create_default_wifi_ap(); + } + + /* Switch to APSTA so we can scan while serving */ + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); + + wifi_config_t ap_cfg = { + .ap = { + .max_connection = 4, + .authmode = WIFI_AUTH_OPEN, + .channel = 1, + }, + }; + strncpy((char *)ap_cfg.ap.ssid, ssid, sizeof(ap_cfg.ap.ssid) - 1); + ap_cfg.ap.ssid_len = strlen(ssid); + + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg)); + ESP_ERROR_CHECK(esp_wifi_start()); + + ESP_LOGI(TAG, "Soft AP started: %s (open)", ssid); + return ESP_OK; +} + +static httpd_handle_t start_http_server(void) +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = MIMI_ONBOARD_HTTP_PORT; + config.max_uri_handlers = 10; + config.stack_size = 8192; + config.lru_purge_enable = true; + + httpd_handle_t server = NULL; + if (httpd_start(&server, &config) != ESP_OK) { + ESP_LOGE(TAG, "Failed to start HTTP server"); + return NULL; + } + + /* Main page */ + httpd_uri_t uri_root = { + .uri = "/", .method = HTTP_GET, .handler = http_get_root, + }; + httpd_register_uri_handler(server, &uri_root); + + /* WiFi scan */ + httpd_uri_t uri_scan = { + .uri = "/scan", .method = HTTP_GET, .handler = http_get_scan, + }; + httpd_register_uri_handler(server, &uri_scan); + + /* Save config */ + httpd_uri_t uri_save = { + .uri = "/save", .method = HTTP_POST, .handler = http_post_save, + }; + httpd_register_uri_handler(server, &uri_save); + + /* Captive portal detection endpoints */ + const char *captive_uris[] = { + "/generate_204", /* Android */ + "/gen_204", /* Android alt */ + "/hotspot-detect.html", /* iOS/macOS */ + "/library/test/success.html", /* iOS alt */ + "/connecttest.txt", /* Windows */ + "/redirect", /* Windows alt */ + }; + for (int i = 0; i < sizeof(captive_uris) / sizeof(captive_uris[0]); i++) { + httpd_uri_t uri_captive = { + .uri = captive_uris[i], + .method = HTTP_GET, + .handler = http_captive_redirect, + }; + httpd_register_uri_handler(server, &uri_captive); + } + + ESP_LOGI(TAG, "HTTP server started on port %d", MIMI_ONBOARD_HTTP_PORT); + return server; +} + +/* ── Public API ─────────────────────────────────────────────────── */ + +esp_err_t wifi_onboard_start(void) +{ + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " Starting WiFi Onboarding Portal"); + ESP_LOGI(TAG, "========================================"); + + /* Stop STA if it was running */ + wifi_manager_stop(); + + /* Start soft AP */ + esp_err_t err = start_softap(); + if (err != ESP_OK) return err; + + /* Start DNS hijack task */ + xTaskCreate(dns_hijack_task, "dns_hijack", + MIMI_ONBOARD_DNS_STACK, NULL, 5, NULL); + + /* Start HTTP server */ + httpd_handle_t server = start_http_server(); + if (!server) return ESP_FAIL; + + ESP_LOGI(TAG, "Connect to MimiClaw-XXXX WiFi, then open http://192.168.4.1"); + + /* Block forever — onboarding ends with esp_restart() in /save handler */ + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + return ESP_OK; /* unreachable */ +} diff --git a/main/onboard/wifi_onboard.h b/main/onboard/wifi_onboard.h new file mode 100644 index 0000000..79b53b3 --- /dev/null +++ b/main/onboard/wifi_onboard.h @@ -0,0 +1,10 @@ +#pragma once + +#include "esp_err.h" + +/** + * Start WiFi onboarding captive portal. + * Opens a soft AP, DNS hijacker, and HTTP configuration server. + * Blocks until the user submits credentials, then saves to NVS and restarts. + */ +esp_err_t wifi_onboard_start(void); diff --git a/main/wifi/wifi_manager.c b/main/wifi/wifi_manager.c index 1ccbab2..0bb34b6 100644 --- a/main/wifi/wifi_manager.c +++ b/main/wifi/wifi_manager.c @@ -85,8 +85,6 @@ esp_err_t wifi_manager_init(void) ESP_ERROR_CHECK(esp_event_handler_instance_register( IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL)); - ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); - ESP_LOGI(TAG, "WiFi manager initialized"); return ESP_OK; } @@ -124,6 +122,7 @@ esp_err_t wifi_manager_start(void) ESP_LOGI(TAG, "Connecting to SSID: %s", wifi_cfg.sta.ssid); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg)); ESP_ERROR_CHECK(esp_wifi_start()); @@ -232,3 +231,32 @@ void wifi_manager_scan_and_print(void) free(ap_list); esp_wifi_connect(); } + +bool wifi_manager_has_credentials(void) +{ + /* Check NVS first */ + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_WIFI, NVS_READONLY, &nvs) == ESP_OK) { + char ssid[33] = {0}; + size_t len = sizeof(ssid); + esp_err_t err = nvs_get_str(nvs, MIMI_NVS_KEY_SSID, ssid, &len); + nvs_close(nvs); + if (err == ESP_OK && ssid[0] != '\0') return true; + } + + /* Fall back to build-time secrets */ + if (MIMI_SECRET_WIFI_SSID[0] != '\0') return true; + + return false; +} + +esp_err_t wifi_manager_stop(void) +{ + esp_wifi_disconnect(); + esp_wifi_stop(); + s_connected = false; + s_retry_count = 0; + xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); + ESP_LOGI(TAG, "WiFi stopped"); + return ESP_OK; +} diff --git a/main/wifi/wifi_manager.h b/main/wifi/wifi_manager.h index 5f2b746..d25e316 100644 --- a/main/wifi/wifi_manager.h +++ b/main/wifi/wifi_manager.h @@ -49,3 +49,13 @@ EventGroupHandle_t wifi_manager_get_event_group(void); * Scan and print nearby APs. */ void wifi_manager_scan_and_print(void); + +/** + * Check if WiFi credentials exist (NVS or build-time secrets). + */ +bool wifi_manager_has_credentials(void); + +/** + * Stop WiFi (for mode switching during onboarding). + */ +esp_err_t wifi_manager_stop(void); From 163f946e502945d118b2db045ba3b69a5fd849cd Mon Sep 17 00:00:00 2001 From: Asklv Date: Sun, 8 Mar 2026 23:04:14 +0800 Subject: [PATCH 2/4] feat: keep admin portal online --- main/mimi.c | 6 +++++- main/onboard/wifi_onboard.h | 13 +++++++++---- main/wifi/wifi_manager.c | 13 ++++++++++++- main/wifi/wifi_manager.h | 5 +++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/main/mimi.c b/main/mimi.c index 3712b72..b949970 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -159,10 +159,14 @@ void app_main(void) if (!wifi_ok) { ESP_LOGW(TAG, "Entering WiFi onboarding mode..."); - wifi_onboard_start(); /* blocks, restarts on success */ + wifi_onboard_start(WIFI_ONBOARD_MODE_CAPTIVE); /* blocks, restarts on success */ return; /* unreachable */ } + if (wifi_onboard_start(WIFI_ONBOARD_MODE_ADMIN) != ESP_OK) { + ESP_LOGW(TAG, "Local admin portal unavailable; continuing without config hotspot"); + } + { /* Outbound dispatch task should start first to avoid dropping early replies. */ ESP_ERROR_CHECK((xTaskCreatePinnedToCore( diff --git a/main/onboard/wifi_onboard.h b/main/onboard/wifi_onboard.h index 79b53b3..a6e0d66 100644 --- a/main/onboard/wifi_onboard.h +++ b/main/onboard/wifi_onboard.h @@ -2,9 +2,14 @@ #include "esp_err.h" +typedef enum { + WIFI_ONBOARD_MODE_CAPTIVE = 0, + WIFI_ONBOARD_MODE_ADMIN, +} wifi_onboard_mode_t; + /** - * Start WiFi onboarding captive portal. - * Opens a soft AP, DNS hijacker, and HTTP configuration server. - * Blocks until the user submits credentials, then saves to NVS and restarts. + * Start WiFi onboarding/configuration portal. + * CAPTIVE mode opens DNS hijack + config page and blocks forever. + * ADMIN mode keeps a local config hotspot alive without captive redirects. */ -esp_err_t wifi_onboard_start(void); +esp_err_t wifi_onboard_start(wifi_onboard_mode_t mode); diff --git a/main/wifi/wifi_manager.c b/main/wifi/wifi_manager.c index 0bb34b6..9ca967b 100644 --- a/main/wifi/wifi_manager.c +++ b/main/wifi/wifi_manager.c @@ -15,6 +15,7 @@ static EventGroupHandle_t s_wifi_event_group; static int s_retry_count = 0; static char s_ip_str[16] = "0.0.0.0"; static bool s_connected = false; +static bool s_reconnect_enabled = true; static const char *wifi_reason_to_str(wifi_err_reason_t reason) { @@ -44,7 +45,7 @@ static void event_handler(void *arg, esp_event_base_t event_base, if (disc) { ESP_LOGW(TAG, "Disconnected (reason=%d:%s)", disc->reason, wifi_reason_to_str(disc->reason)); } - if (s_retry_count < MIMI_WIFI_MAX_RETRY) { + if (s_reconnect_enabled && s_retry_count < MIMI_WIFI_MAX_RETRY) { /* Exponential backoff: 1s, 2s, 4s, 8s, ... capped at 30s */ uint32_t delay_ms = MIMI_WIFI_RETRY_BASE_MS << s_retry_count; if (delay_ms > MIMI_WIFI_RETRY_MAX_MS) { @@ -120,6 +121,7 @@ esp_err_t wifi_manager_start(void) return ESP_ERR_NOT_FOUND; } + s_reconnect_enabled = true; ESP_LOGI(TAG, "Connecting to SSID: %s", wifi_cfg.sta.ssid); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); @@ -252,6 +254,7 @@ bool wifi_manager_has_credentials(void) esp_err_t wifi_manager_stop(void) { + s_reconnect_enabled = false; esp_wifi_disconnect(); esp_wifi_stop(); s_connected = false; @@ -260,3 +263,11 @@ esp_err_t wifi_manager_stop(void) ESP_LOGI(TAG, "WiFi stopped"); return ESP_OK; } + +void wifi_manager_set_reconnect_enabled(bool enabled) +{ + s_reconnect_enabled = enabled; + if (!enabled) { + s_retry_count = 0; + } +} diff --git a/main/wifi/wifi_manager.h b/main/wifi/wifi_manager.h index d25e316..e6f6661 100644 --- a/main/wifi/wifi_manager.h +++ b/main/wifi/wifi_manager.h @@ -59,3 +59,8 @@ bool wifi_manager_has_credentials(void); * Stop WiFi (for mode switching during onboarding). */ esp_err_t wifi_manager_stop(void); + +/** + * Enable or disable STA auto-reconnect on disconnect events. + */ +void wifi_manager_set_reconnect_enabled(bool enabled); From f0e26fd338eb5f25966d3ccb48d253ca2b06ffda Mon Sep 17 00:00:00 2001 From: Asklv Date: Sun, 8 Mar 2026 23:04:19 +0800 Subject: [PATCH 3/4] feat: prefill portal settings --- main/onboard/onboard_html.h | 15 +- main/onboard/wifi_onboard.c | 266 +++++++++++++++++++++++++++++------- 2 files changed, 226 insertions(+), 55 deletions(-) diff --git a/main/onboard/onboard_html.h b/main/onboard/onboard_html.h index 7131b64..ac4fb02 100644 --- a/main/onboard/onboard_html.h +++ b/main/onboard/onboard_html.h @@ -31,6 +31,9 @@ static const char ONBOARD_HTML[] = ".status{text-align:center;padding:20px;color:#1a73e8;font-size:1.1em;display:none}" "" "

MimiClaw Setup

" +"

" +"This local portal remains available at 192.168.4.1 for later updates." +"

" /* WiFi section (expanded by default) */ "
" @@ -110,6 +113,14 @@ static const char ONBOARD_HTML[] = "function toggle(el){" "el.parentElement.classList.toggle('collapsed')}" +"function loadConfig(){" +"fetch('/config').then(r=>r.json()).then(cfg=>{" +"Object.keys(cfg).forEach(k=>{" +"var el=document.getElementById(k);" +"if(el && cfg[k] !== undefined && cfg[k] !== null){el.value=cfg[k]}" +"})" +"}).catch(()=>{})}" + "function scan(){" "var btn=event.target;btn.textContent='Scanning...';btn.disabled=true;" "fetch('/scan').then(r=>r.json()).then(list=>{" @@ -126,11 +137,11 @@ static const char ONBOARD_HTML[] = "var fields=['ssid','password','api_key','model','provider','tg_token'," "'feishu_app_id','feishu_app_secret','proxy_host','proxy_port','proxy_type','search_key','tavily_key'];" "var data={};" -"fields.forEach(f=>{var v=document.getElementById(f).value.trim();if(v)data[f]=v});" -"if(!data.ssid){alert('WiFi SSID is required');return}" +"fields.forEach(f=>{data[f]=document.getElementById(f).value.trim()});" "document.getElementById('status').style.display='block';" "fetch('/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})" ".then(()=>{document.getElementById('status').textContent='Saved! Restarting...';})" ".catch(()=>{document.getElementById('status').textContent='Error. Please try again.';})}" +"loadConfig();" "" ""; diff --git a/main/onboard/wifi_onboard.c b/main/onboard/wifi_onboard.c index 2268539..6b00429 100644 --- a/main/onboard/wifi_onboard.c +++ b/main/onboard/wifi_onboard.c @@ -3,6 +3,8 @@ #include "mimi_config.h" #include "wifi/wifi_manager.h" +#include +#include #include #include "esp_log.h" #include "esp_wifi.h" @@ -18,6 +20,55 @@ #include "lwip/netdb.h" static const char *TAG = "onboard"; +static httpd_handle_t s_server = NULL; +static bool s_captive_mode = false; + +static void json_add_effective_config(cJSON *root, const char *json_key, + const char *ns, const char *nvs_key, + const char *build_val) +{ + char value[256] = {0}; + bool found = false; + + nvs_handle_t nvs; + if (nvs_open(ns, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(value); + if (nvs_get_str(nvs, nvs_key, value, &len) == ESP_OK) { + found = true; + } + nvs_close(nvs); + } + + if (!found && build_val) { + strlcpy(value, build_val, sizeof(value)); + } + + cJSON_AddStringToObject(root, json_key, value); +} + +static void json_add_effective_config_u16(cJSON *root, const char *json_key, + const char *ns, const char *nvs_key, + const char *build_val) +{ + char value[16] = {0}; + bool found = false; + + nvs_handle_t nvs; + if (nvs_open(ns, NVS_READONLY, &nvs) == ESP_OK) { + uint16_t port = 0; + if (nvs_get_u16(nvs, nvs_key, &port) == ESP_OK && port > 0) { + snprintf(value, sizeof(value), "%u", (unsigned)port); + found = true; + } + nvs_close(nvs); + } + + if (!found && build_val) { + strlcpy(value, build_val, sizeof(value)); + } + + cJSON_AddStringToObject(root, json_key, value); +} /* ── DNS hijack ─────────────────────────────────────────────────── */ @@ -156,19 +207,99 @@ static esp_err_t http_get_scan(httpd_req_t *req) return ret; } -/* Helper: save a single NVS string if json field is present and non-empty */ -static void nvs_save_field(cJSON *root, const char *json_key, +static esp_err_t http_get_config(httpd_req_t *req) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"); + return ESP_FAIL; + } + + json_add_effective_config(root, "ssid", MIMI_NVS_WIFI, MIMI_NVS_KEY_SSID, MIMI_SECRET_WIFI_SSID); + json_add_effective_config(root, "password", MIMI_NVS_WIFI, MIMI_NVS_KEY_PASS, MIMI_SECRET_WIFI_PASS); + json_add_effective_config(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY); + json_add_effective_config(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL); + json_add_effective_config(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER); + json_add_effective_config(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN, MIMI_SECRET_TG_TOKEN); + json_add_effective_config(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID, MIMI_SECRET_FEISHU_APP_ID); + json_add_effective_config(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET, MIMI_SECRET_FEISHU_APP_SECRET); + json_add_effective_config(root, "proxy_host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST, MIMI_SECRET_PROXY_HOST); + json_add_effective_config_u16(root, "proxy_port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT); + json_add_effective_config(root, "proxy_type", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_TYPE, MIMI_SECRET_PROXY_TYPE); + json_add_effective_config(root, "search_key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY); + json_add_effective_config(root, "tavily_key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY, MIMI_SECRET_TAVILY_KEY); + + char *json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (!json) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"); + return ESP_FAIL; + } + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + esp_err_t ret = httpd_resp_send(req, json, strlen(json)); + free(json); + return ret; +} + +/* + * Sync one JSON string field into NVS. + * - missing field: leave current NVS value unchanged + * - empty string: erase current NVS value + * - non-empty string: save/update current NVS value + */ +static void nvs_sync_field(cJSON *root, const char *json_key, const char *ns, const char *nvs_key) { cJSON *item = cJSON_GetObjectItem(root, json_key); - if (!item || !cJSON_IsString(item) || item->valuestring[0] == '\0') return; + if (!item || !cJSON_IsString(item)) return; nvs_handle_t nvs; if (nvs_open(ns, NVS_READWRITE, &nvs) == ESP_OK) { - nvs_set_str(nvs, nvs_key, item->valuestring); + if (item->valuestring[0] == '\0') { + esp_err_t err = nvs_erase_key(nvs, nvs_key); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Cleared %s/%s", ns, nvs_key); + } else if (err != ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGW(TAG, "Failed clearing %s/%s: %s", ns, nvs_key, esp_err_to_name(err)); + } + } else { + nvs_set_str(nvs, nvs_key, item->valuestring); + ESP_LOGI(TAG, "Saved %s/%s", ns, nvs_key); + } + nvs_commit(nvs); + nvs_close(nvs); + } +} + +static void nvs_sync_u16_field(cJSON *root, const char *json_key, + const char *ns, const char *nvs_key) +{ + cJSON *item = cJSON_GetObjectItem(root, json_key); + if (!item || !cJSON_IsString(item)) return; + + nvs_handle_t nvs; + if (nvs_open(ns, NVS_READWRITE, &nvs) == ESP_OK) { + if (item->valuestring[0] == '\0') { + esp_err_t err = nvs_erase_key(nvs, nvs_key); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Cleared %s/%s", ns, nvs_key); + } else if (err != ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGW(TAG, "Failed clearing %s/%s: %s", ns, nvs_key, esp_err_to_name(err)); + } + } else { + char *end = NULL; + unsigned long value = strtoul(item->valuestring, &end, 10); + if (end == item->valuestring || *end != '\0' || value > UINT16_MAX) { + ESP_LOGW(TAG, "Ignoring invalid %s value: %s", json_key, item->valuestring); + } else { + ESP_ERROR_CHECK(nvs_set_u16(nvs, nvs_key, (uint16_t)value)); + ESP_LOGI(TAG, "Saved %s/%s", ns, nvs_key); + } + } nvs_commit(nvs); nvs_close(nvs); - ESP_LOGI(TAG, "Saved %s/%s", ns, nvs_key); } } @@ -205,29 +336,29 @@ static esp_err_t http_post_save(httpd_req_t *req) } /* WiFi (required) */ - nvs_save_field(root, "ssid", MIMI_NVS_WIFI, MIMI_NVS_KEY_SSID); - nvs_save_field(root, "password", MIMI_NVS_WIFI, MIMI_NVS_KEY_PASS); + nvs_sync_field(root, "ssid", MIMI_NVS_WIFI, MIMI_NVS_KEY_SSID); + nvs_sync_field(root, "password", MIMI_NVS_WIFI, MIMI_NVS_KEY_PASS); /* LLM */ - nvs_save_field(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY); - nvs_save_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL); - nvs_save_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER); + nvs_sync_field(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY); + nvs_sync_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL); + nvs_sync_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER); /* Telegram */ - nvs_save_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN); + nvs_sync_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN); /* Feishu */ - nvs_save_field(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID); - nvs_save_field(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET); + nvs_sync_field(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID); + nvs_sync_field(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET); /* Proxy */ - nvs_save_field(root, "proxy_host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST); - nvs_save_field(root, "proxy_port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT); - nvs_save_field(root, "proxy_type", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_TYPE); + nvs_sync_field(root, "proxy_host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST); + nvs_sync_u16_field(root, "proxy_port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT); + nvs_sync_field(root, "proxy_type", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_TYPE); /* Search */ - nvs_save_field(root, "search_key", MIMI_NVS_SEARCH, "brave_key"); - nvs_save_field(root, "tavily_key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY); + nvs_sync_field(root, "search_key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY); + nvs_sync_field(root, "tavily_key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY); cJSON_Delete(root); @@ -243,7 +374,7 @@ static esp_err_t http_post_save(httpd_req_t *req) /* ── Soft AP + HTTP server startup ──────────────────────────────── */ -static esp_err_t start_softap(void) +static esp_err_t start_softap(bool keep_sta) { /* Get last 2 bytes of MAC for unique SSID suffix */ uint8_t mac[6]; @@ -257,7 +388,8 @@ static esp_err_t start_softap(void) ap_netif = esp_netif_create_default_wifi_ap(); } - /* Switch to APSTA so we can scan while serving */ + /* APSTA lets the local config AP coexist with WiFi scanning/STA usage. */ + (void)keep_sta; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); wifi_config_t ap_cfg = { @@ -271,22 +403,31 @@ static esp_err_t start_softap(void) ap_cfg.ap.ssid_len = strlen(ssid); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg)); - ESP_ERROR_CHECK(esp_wifi_start()); + esp_err_t err = esp_wifi_start(); + if (err != ESP_OK && !(keep_sta && err == ESP_ERR_WIFI_CONN)) { + return err; + } ESP_LOGI(TAG, "Soft AP started: %s (open)", ssid); return ESP_OK; } -static httpd_handle_t start_http_server(void) +static httpd_handle_t start_http_server(bool captive) { + if (s_server) { + if (captive && !s_captive_mode) { + ESP_LOGW(TAG, "HTTP server already running without captive redirects"); + } + return s_server; + } + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = MIMI_ONBOARD_HTTP_PORT; - config.max_uri_handlers = 10; + config.max_uri_handlers = captive ? 16 : 8; config.stack_size = 8192; config.lru_purge_enable = true; - httpd_handle_t server = NULL; - if (httpd_start(&server, &config) != ESP_OK) { + if (httpd_start(&s_server, &config) != ESP_OK) { ESP_LOGE(TAG, "Failed to start HTTP server"); return NULL; } @@ -295,67 +436,86 @@ static httpd_handle_t start_http_server(void) httpd_uri_t uri_root = { .uri = "/", .method = HTTP_GET, .handler = http_get_root, }; - httpd_register_uri_handler(server, &uri_root); + httpd_register_uri_handler(s_server, &uri_root); + + httpd_uri_t uri_config = { + .uri = "/config", .method = HTTP_GET, .handler = http_get_config, + }; + httpd_register_uri_handler(s_server, &uri_config); /* WiFi scan */ httpd_uri_t uri_scan = { .uri = "/scan", .method = HTTP_GET, .handler = http_get_scan, }; - httpd_register_uri_handler(server, &uri_scan); + httpd_register_uri_handler(s_server, &uri_scan); /* Save config */ httpd_uri_t uri_save = { .uri = "/save", .method = HTTP_POST, .handler = http_post_save, }; - httpd_register_uri_handler(server, &uri_save); + httpd_register_uri_handler(s_server, &uri_save); - /* Captive portal detection endpoints */ - const char *captive_uris[] = { - "/generate_204", /* Android */ - "/gen_204", /* Android alt */ - "/hotspot-detect.html", /* iOS/macOS */ - "/library/test/success.html", /* iOS alt */ - "/connecttest.txt", /* Windows */ - "/redirect", /* Windows alt */ - }; - for (int i = 0; i < sizeof(captive_uris) / sizeof(captive_uris[0]); i++) { - httpd_uri_t uri_captive = { - .uri = captive_uris[i], - .method = HTTP_GET, - .handler = http_captive_redirect, + if (captive) { + /* Captive portal detection endpoints */ + const char *captive_uris[] = { + "/generate_204", /* Android */ + "/gen_204", /* Android alt */ + "/hotspot-detect.html", /* iOS/macOS */ + "/library/test/success.html", /* iOS alt */ + "/connecttest.txt", /* Windows */ + "/redirect", /* Windows alt */ }; - httpd_register_uri_handler(server, &uri_captive); + for (int i = 0; i < sizeof(captive_uris) / sizeof(captive_uris[0]); i++) { + httpd_uri_t uri_captive = { + .uri = captive_uris[i], + .method = HTTP_GET, + .handler = http_captive_redirect, + }; + httpd_register_uri_handler(s_server, &uri_captive); + } } + s_captive_mode = captive; ESP_LOGI(TAG, "HTTP server started on port %d", MIMI_ONBOARD_HTTP_PORT); - return server; + return s_server; } /* ── Public API ─────────────────────────────────────────────────── */ -esp_err_t wifi_onboard_start(void) +esp_err_t wifi_onboard_start(wifi_onboard_mode_t mode) { ESP_LOGI(TAG, "========================================"); - ESP_LOGI(TAG, " Starting WiFi Onboarding Portal"); + ESP_LOGI(TAG, " Starting WiFi Configuration Portal"); ESP_LOGI(TAG, "========================================"); - /* Stop STA if it was running */ - wifi_manager_stop(); + bool captive = (mode == WIFI_ONBOARD_MODE_CAPTIVE); + if (captive) { + /* Stop STA retries before starting captive portal. */ + wifi_manager_set_reconnect_enabled(false); + wifi_manager_stop(); + } /* Start soft AP */ - esp_err_t err = start_softap(); + esp_err_t err = start_softap(!captive); if (err != ESP_OK) return err; - /* Start DNS hijack task */ - xTaskCreate(dns_hijack_task, "dns_hijack", - MIMI_ONBOARD_DNS_STACK, NULL, 5, NULL); + if (captive) { + /* Start DNS hijack only for true captive portal mode. */ + xTaskCreate(dns_hijack_task, "dns_hijack", + MIMI_ONBOARD_DNS_STACK, NULL, 5, NULL); + } /* Start HTTP server */ - httpd_handle_t server = start_http_server(); + httpd_handle_t server = start_http_server(captive); if (!server) return ESP_FAIL; ESP_LOGI(TAG, "Connect to MimiClaw-XXXX WiFi, then open http://192.168.4.1"); + if (!captive) { + ESP_LOGI(TAG, "Local admin portal stays available while STA is connected"); + return ESP_OK; + } + /* Block forever — onboarding ends with esp_restart() in /save handler */ while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); From c0ea5f22fbf3f24baa1a680ab8911b75ab0d2630 Mon Sep 17 00:00:00 2001 From: Asklv Date: Sun, 8 Mar 2026 23:04:24 +0800 Subject: [PATCH 4/4] feat: align portal NVS keys --- main/cli/serial_cli.c | 28 +++++++++++++++++++++++++++- main/mimi_config.h | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 4968ff7..a77fe28 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -528,6 +528,32 @@ static void print_config(const char *label, const char *ns, const char *key, } } +static void print_config_u16(const char *label, const char *ns, const char *key, + const char *build_val) +{ + char nvs_val[16] = {0}; + const char *source = "not set"; + const char *display = "(empty)"; + + nvs_handle_t nvs; + if (nvs_open(ns, NVS_READONLY, &nvs) == ESP_OK) { + uint16_t value = 0; + if (nvs_get_u16(nvs, key, &value) == ESP_OK && value > 0) { + snprintf(nvs_val, sizeof(nvs_val), "%u", (unsigned)value); + source = "NVS"; + display = nvs_val; + } + nvs_close(nvs); + } + + if (strcmp(source, "not set") == 0 && build_val[0] != '\0') { + source = "build"; + display = build_val; + } + + printf(" %-14s: %s [%s]\n", label, display, source); +} + static int cmd_config_show(int argc, char **argv) { printf("=== Current Configuration ===\n"); @@ -538,7 +564,7 @@ static int cmd_config_show(int argc, char **argv) print_config("Model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL, false); print_config("Provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER, false); print_config("Proxy Host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST, MIMI_SECRET_PROXY_HOST, false); - print_config("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT, false); + print_config_u16("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT); print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true); print_config("Tavily Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY, MIMI_SECRET_TAVILY_KEY, true); printf("=============================\n"); diff --git a/main/mimi_config.h b/main/mimi_config.h index 99522b6..fbdbb03 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -149,7 +149,7 @@ #define MIMI_NVS_KEY_PROVIDER "provider" #define MIMI_NVS_KEY_PROXY_HOST "host" #define MIMI_NVS_KEY_PROXY_PORT "port" -#define MIMI_NVS_KEY_PROXY_TYPE "type" +#define MIMI_NVS_KEY_PROXY_TYPE "proxy_type" /* WiFi Onboarding (Captive Portal) */ #define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"