diff --git a/README.md b/README.md index 415e37a..4231def 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ idf.py set-target esp32s3 ### Configure -All configuration is done through `mimi_secrets.h` at build time: +MimiClaw uses a **two-layer config** system: build-time defaults in `mimi_secrets.h`, with runtime overrides via the serial CLI. CLI values are stored in NVS flash and take priority over build-time values. ```bash cp main/mimi_secrets.h.example main/mimi_secrets.h @@ -106,7 +106,23 @@ idf.py -p PORT flash monitor ### CLI Commands -The serial CLI provides debug and maintenance commands: +Connect via serial to configure or debug. **Config commands** let you change settings without recompiling — just plug in a USB cable anywhere. + +**Runtime config** (saved to NVS, overrides build-time defaults): + +``` +mimi> wifi_set MySSID MyPassword # change WiFi network +mimi> set_tg_token 123456:ABC... # change Telegram bot token +mimi> set_api_key sk-ant-api03-... # change Anthropic API key +mimi> set_model claude-sonnet-4-5 # change LLM model +mimi> set_proxy 127.0.0.1 7897 # set HTTP proxy +mimi> clear_proxy # remove proxy +mimi> set_search_key BSA... # set Brave Search API key +mimi> config_show # show all config (masked) +mimi> config_reset # clear NVS, revert to build-time defaults +``` + +**Debug & maintenance:** ``` mimi> wifi_status # am I connected? diff --git a/README_CN.md b/README_CN.md index 62a7e48..a75403e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -71,7 +71,7 @@ idf.py set-target esp32s3 ### 配置 -所有配置通过 `mimi_secrets.h` 在编译时写入: +MimiClaw 使用**两层配置**:`mimi_secrets.h` 提供编译时默认值,串口 CLI 可在运行时覆盖。CLI 设置的值存在 NVS Flash 中,优先级高于编译时值。 ```bash cp main/mimi_secrets.h.example main/mimi_secrets.h @@ -110,13 +110,34 @@ idf.py -p PORT flash monitor **前提**:局域网内有一个支持 HTTP CONNECT 的代理(Clash Verge、V2Ray 等),并开启了「允许局域网连接」。 -在 `mimi_secrets.h` 中设置 `MIMI_SECRET_PROXY_HOST` 和 `MIMI_SECRET_PROXY_PORT`。清除代理只需把这两项改回空字符串 `""`,然后重新编译。 +可以在 `mimi_secrets.h` 中编译时设置,也可以通过串口 CLI 随时修改: + +``` +mimi> set_proxy 192.168.1.83 7897 # 设置代理 +mimi> clear_proxy # 清除代理 +``` > **提示**:确保 ESP32-S3 和代理机器在同一局域网。Clash Verge 在「设置 → 允许局域网」中开启。 ### CLI 命令 -串口 CLI 提供调试和运维命令: +通过串口连接即可配置和调试。**配置命令**让你无需重新编译就能修改设置 — 随时随地插上 USB 线就能改。 + +**运行时配置**(存入 NVS,覆盖编译时默认值): + +``` +mimi> wifi_set MySSID MyPassword # 换 WiFi +mimi> set_tg_token 123456:ABC... # 换 Telegram Bot Token +mimi> set_api_key sk-ant-api03-... # 换 Anthropic API Key +mimi> set_model claude-sonnet-4-5-20250929 # 换模型 +mimi> set_proxy 192.168.1.83 7897 # 设置代理 +mimi> clear_proxy # 清除代理 +mimi> set_search_key BSA... # 设置 Brave Search API Key +mimi> config_show # 查看所有配置(脱敏显示) +mimi> config_reset # 清除 NVS,恢复编译时默认值 +``` + +**调试与运维:** ``` mimi> wifi_status # 连上了吗? diff --git a/docs/TODO.md b/docs/TODO.md index 0e5d33f..996f851 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -104,9 +104,9 @@ - **MimiClaw**: Not implemented - **Recommendation**: Requires extra HTTPS request to Whisper API: download Telegram voice -> forward -> get text -### [x] ~~Build-time Config File~~ -- Implemented: `mimi_secrets.h` — sole configuration method (build-time only, no NVS/CLI) -- Replaces need for YAML config; suitable for MCU workflow +### [x] ~~Build-time Config File + Runtime NVS Override~~ +- Implemented: `mimi_secrets.h` as build-time defaults, NVS as runtime override via CLI +- Two-layer config: build-time secrets → NVS fallback, CLI commands to set/show/reset ### [ ] WebSocket Gateway Protocol Enhancement - **nanobot**: Gateway port 18790 + richer protocol @@ -149,7 +149,7 @@ - [x] OTA Update - [x] WiFi Manager (build-time credentials, exponential backoff) - [x] SPIFFS storage -- [x] Build-time config (`mimi_secrets.h`, sole configuration method) +- [x] Build-time config (`mimi_secrets.h`) + runtime NVS override via CLI --- diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 383ab6b..7eee34c 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -1,8 +1,12 @@ #include "serial_cli.h" #include "mimi_config.h" #include "wifi/wifi_manager.h" +#include "telegram/telegram_bot.h" +#include "llm/llm_proxy.h" #include "memory/memory_store.h" #include "memory/session_mgr.h" +#include "proxy/http_proxy.h" +#include "tools/tool_web_search.h" #include #include @@ -10,10 +14,32 @@ #include "esp_console.h" #include "esp_system.h" #include "esp_heap_caps.h" +#include "nvs_flash.h" +#include "nvs.h" #include "argtable3/argtable3.h" static const char *TAG = "cli"; +/* --- wifi_set command --- */ +static struct { + struct arg_str *ssid; + struct arg_str *password; + struct arg_end *end; +} wifi_set_args; + +static int cmd_wifi_set(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&wifi_set_args); + if (nerrors != 0) { + arg_print_errors(stderr, wifi_set_args.end, argv[0]); + return 1; + } + wifi_manager_set_credentials(wifi_set_args.ssid->sval[0], + wifi_set_args.password->sval[0]); + printf("WiFi credentials saved. Restart to apply.\n"); + return 0; +} + /* --- wifi_status command --- */ static int cmd_wifi_status(int argc, char **argv) { @@ -22,6 +48,60 @@ static int cmd_wifi_status(int argc, char **argv) return 0; } +/* --- set_tg_token command --- */ +static struct { + struct arg_str *token; + struct arg_end *end; +} tg_token_args; + +static int cmd_set_tg_token(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&tg_token_args); + if (nerrors != 0) { + arg_print_errors(stderr, tg_token_args.end, argv[0]); + return 1; + } + telegram_set_token(tg_token_args.token->sval[0]); + printf("Telegram bot token saved.\n"); + return 0; +} + +/* --- set_api_key command --- */ +static struct { + struct arg_str *key; + struct arg_end *end; +} api_key_args; + +static int cmd_set_api_key(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&api_key_args); + if (nerrors != 0) { + arg_print_errors(stderr, api_key_args.end, argv[0]); + return 1; + } + llm_set_api_key(api_key_args.key->sval[0]); + printf("API key saved.\n"); + return 0; +} + +/* --- set_model command --- */ +static struct { + struct arg_str *model; + struct arg_end *end; +} model_args; + +static int cmd_set_model(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&model_args); + if (nerrors != 0) { + arg_print_errors(stderr, model_args.end, argv[0]); + return 1; + } + llm_set_model(model_args.model->sval[0]); + printf("Model set.\n"); + return 0; +} + /* --- memory_read command --- */ static int cmd_memory_read(int argc, char **argv) { @@ -98,6 +178,116 @@ static int cmd_heap_info(int argc, char **argv) return 0; } +/* --- set_proxy command --- */ +static struct { + struct arg_str *host; + struct arg_int *port; + struct arg_end *end; +} proxy_args; + +static int cmd_set_proxy(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&proxy_args); + if (nerrors != 0) { + arg_print_errors(stderr, proxy_args.end, argv[0]); + return 1; + } + http_proxy_set(proxy_args.host->sval[0], (uint16_t)proxy_args.port->ival[0]); + printf("Proxy set. Restart to apply.\n"); + return 0; +} + +/* --- clear_proxy command --- */ +static int cmd_clear_proxy(int argc, char **argv) +{ + http_proxy_clear(); + printf("Proxy cleared. Restart to apply.\n"); + return 0; +} + +/* --- set_search_key command --- */ +static struct { + struct arg_str *key; + struct arg_end *end; +} search_key_args; + +static int cmd_set_search_key(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&search_key_args); + if (nerrors != 0) { + arg_print_errors(stderr, search_key_args.end, argv[0]); + return 1; + } + tool_web_search_set_key(search_key_args.key->sval[0]); + printf("Search API key saved.\n"); + return 0; +} + +/* --- config_show command --- */ +static void print_config(const char *label, const char *ns, const char *key, + const char *build_val, bool mask) +{ + char nvs_val[128] = {0}; + const char *source = "not set"; + const char *display = "(empty)"; + + /* NVS takes highest priority */ + nvs_handle_t nvs; + if (nvs_open(ns, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(nvs_val); + if (nvs_get_str(nvs, key, nvs_val, &len) == ESP_OK && nvs_val[0]) { + source = "NVS"; + display = nvs_val; + } + nvs_close(nvs); + } + + /* Fall back to build-time value */ + if (strcmp(source, "not set") == 0 && build_val[0] != '\0') { + source = "build"; + display = build_val; + } + + if (mask && strlen(display) > 6 && strcmp(display, "(empty)") != 0) { + printf(" %-14s: %.4s**** [%s]\n", label, display, source); + } else { + printf(" %-14s: %s [%s]\n", label, display, source); + } +} + +static int cmd_config_show(int argc, char **argv) +{ + printf("=== Current Configuration ===\n"); + print_config("WiFi SSID", MIMI_NVS_WIFI, MIMI_NVS_KEY_SSID, MIMI_SECRET_WIFI_SSID, false); + print_config("WiFi Pass", MIMI_NVS_WIFI, MIMI_NVS_KEY_PASS, MIMI_SECRET_WIFI_PASS, true); + print_config("TG Token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN, MIMI_SECRET_TG_TOKEN, true); + print_config("API Key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY, true); + print_config("Model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL, 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("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true); + printf("=============================\n"); + return 0; +} + +/* --- config_reset command --- */ +static int cmd_config_reset(int argc, char **argv) +{ + const char *namespaces[] = { + MIMI_NVS_WIFI, MIMI_NVS_TG, MIMI_NVS_LLM, MIMI_NVS_PROXY, MIMI_NVS_SEARCH + }; + for (int i = 0; i < 5; i++) { + nvs_handle_t nvs; + if (nvs_open(namespaces[i], NVS_READWRITE, &nvs) == ESP_OK) { + nvs_erase_all(nvs); + nvs_commit(nvs); + nvs_close(nvs); + } + } + printf("All NVS config cleared. Build-time defaults will be used on restart.\n"); + return 0; +} + /* --- restart command --- */ static int cmd_restart(int argc, char **argv) { @@ -120,6 +310,19 @@ esp_err_t serial_cli_init(void) ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl)); /* Register commands */ + esp_console_register_help_command(); + + /* wifi_set */ + wifi_set_args.ssid = arg_str1(NULL, NULL, "", "WiFi SSID"); + wifi_set_args.password = arg_str1(NULL, NULL, "", "WiFi password"); + wifi_set_args.end = arg_end(2); + esp_console_cmd_t wifi_set_cmd = { + .command = "wifi_set", + .help = "Set WiFi SSID and password", + .func = &cmd_wifi_set, + .argtable = &wifi_set_args, + }; + esp_console_cmd_register(&wifi_set_cmd); /* wifi_status */ esp_console_cmd_t wifi_status_cmd = { @@ -129,6 +332,39 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&wifi_status_cmd); + /* set_tg_token */ + tg_token_args.token = arg_str1(NULL, NULL, "", "Telegram bot token"); + tg_token_args.end = arg_end(1); + esp_console_cmd_t tg_token_cmd = { + .command = "set_tg_token", + .help = "Set Telegram bot token", + .func = &cmd_set_tg_token, + .argtable = &tg_token_args, + }; + esp_console_cmd_register(&tg_token_cmd); + + /* set_api_key */ + api_key_args.key = arg_str1(NULL, NULL, "", "Anthropic API key"); + api_key_args.end = arg_end(1); + esp_console_cmd_t api_key_cmd = { + .command = "set_api_key", + .help = "Set Claude API key", + .func = &cmd_set_api_key, + .argtable = &api_key_args, + }; + esp_console_cmd_register(&api_key_cmd); + + /* set_model */ + model_args.model = arg_str1(NULL, NULL, "", "Model identifier"); + model_args.end = arg_end(1); + esp_console_cmd_t model_cmd = { + .command = "set_model", + .help = "Set LLM model (default: " MIMI_LLM_DEFAULT_MODEL ")", + .func = &cmd_set_model, + .argtable = &model_args, + }; + esp_console_cmd_register(&model_cmd); + /* memory_read */ esp_console_cmd_t mem_read_cmd = { .command = "memory_read", @@ -175,6 +411,53 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&heap_cmd); + /* set_search_key */ + search_key_args.key = arg_str1(NULL, NULL, "", "Brave Search API key"); + search_key_args.end = arg_end(1); + esp_console_cmd_t search_key_cmd = { + .command = "set_search_key", + .help = "Set Brave Search API key for web_search tool", + .func = &cmd_set_search_key, + .argtable = &search_key_args, + }; + esp_console_cmd_register(&search_key_cmd); + + /* set_proxy */ + proxy_args.host = arg_str1(NULL, NULL, "", "Proxy host/IP"); + proxy_args.port = arg_int1(NULL, NULL, "", "Proxy port"); + proxy_args.end = arg_end(2); + esp_console_cmd_t proxy_cmd = { + .command = "set_proxy", + .help = "Set HTTP proxy (e.g. set_proxy 192.168.1.83 7897)", + .func = &cmd_set_proxy, + .argtable = &proxy_args, + }; + esp_console_cmd_register(&proxy_cmd); + + /* clear_proxy */ + esp_console_cmd_t clear_proxy_cmd = { + .command = "clear_proxy", + .help = "Remove proxy configuration", + .func = &cmd_clear_proxy, + }; + esp_console_cmd_register(&clear_proxy_cmd); + + /* config_show */ + esp_console_cmd_t config_show_cmd = { + .command = "config_show", + .help = "Show current configuration (build-time + NVS)", + .func = &cmd_config_show, + }; + esp_console_cmd_register(&config_show_cmd); + + /* config_reset */ + esp_console_cmd_t config_reset_cmd = { + .command = "config_reset", + .help = "Clear all NVS overrides, revert to build-time defaults", + .func = &cmd_config_reset, + }; + esp_console_cmd_register(&config_reset_cmd); + /* restart */ esp_console_cmd_t restart_cmd = { .command = "restart", diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index df12e31..82e80b6 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -8,6 +8,7 @@ #include "esp_http_client.h" #include "esp_crt_bundle.h" #include "esp_heap_caps.h" +#include "nvs.h" #include "cJSON.h" static const char *TAG = "llm"; @@ -70,6 +71,7 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt) esp_err_t llm_proxy_init(void) { + /* Start with build-time defaults */ if (MIMI_SECRET_API_KEY[0] != '\0') { strncpy(s_api_key, MIMI_SECRET_API_KEY, sizeof(s_api_key) - 1); } @@ -77,10 +79,26 @@ esp_err_t llm_proxy_init(void) strncpy(s_model, MIMI_SECRET_MODEL, sizeof(s_model) - 1); } + /* NVS overrides take highest priority (set via CLI) */ + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) { + char tmp[128] = {0}; + size_t len = sizeof(tmp); + if (nvs_get_str(nvs, MIMI_NVS_KEY_API_KEY, tmp, &len) == ESP_OK && tmp[0]) { + strncpy(s_api_key, tmp, sizeof(s_api_key) - 1); + } + len = sizeof(tmp); + memset(tmp, 0, sizeof(tmp)); + if (nvs_get_str(nvs, MIMI_NVS_KEY_MODEL, tmp, &len) == ESP_OK && tmp[0]) { + strncpy(s_model, tmp, sizeof(s_model) - 1); + } + nvs_close(nvs); + } + if (s_api_key[0]) { ESP_LOGI(TAG, "LLM proxy initialized (model: %s)", s_model); } else { - ESP_LOGW(TAG, "No API key. Set MIMI_SECRET_API_KEY in mimi_secrets.h"); + ESP_LOGW(TAG, "No API key. Use CLI: set_api_key "); } return ESP_OK; } @@ -448,3 +466,30 @@ esp_err_t llm_chat_tools(const char *system_prompt, return ESP_OK; } +/* ── NVS helpers ──────────────────────────────────────────────── */ + +esp_err_t llm_set_api_key(const char *api_key) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_LLM, NVS_READWRITE, &nvs)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_API_KEY, api_key)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + + strncpy(s_api_key, api_key, sizeof(s_api_key) - 1); + ESP_LOGI(TAG, "API key saved"); + return ESP_OK; +} + +esp_err_t llm_set_model(const char *model) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_LLM, NVS_READWRITE, &nvs)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_MODEL, model)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + + strncpy(s_model, model, sizeof(s_model) - 1); + ESP_LOGI(TAG, "Model set to: %s", s_model); + return ESP_OK; +} diff --git a/main/llm/llm_proxy.h b/main/llm/llm_proxy.h index 43c8f5c..064606c 100644 --- a/main/llm/llm_proxy.h +++ b/main/llm/llm_proxy.h @@ -8,10 +8,20 @@ #include "mimi_config.h" /** - * Initialize the LLM proxy. + * Initialize the LLM proxy. Reads API key and model from build-time secrets, then NVS. */ esp_err_t llm_proxy_init(void); +/** + * Save the Anthropic API key to NVS. + */ +esp_err_t llm_set_api_key(const char *api_key); + +/** + * Save the model identifier to NVS. + */ +esp_err_t llm_set_model(const char *model); + /** * Send a chat completion request to Anthropic Messages API (streaming). * diff --git a/main/mimi_config.h b/main/mimi_config.h index 1979d4f..5e15e8f 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -2,7 +2,7 @@ /* MimiClaw Global Configuration */ -/* Build-time secrets (sole configuration method) */ +/* Build-time secrets (highest priority, override NVS) */ #if __has_include("mimi_secrets.h") #include "mimi_secrets.h" #endif @@ -88,3 +88,19 @@ #define MIMI_CLI_PRIO 3 #define MIMI_CLI_CORE 0 +/* NVS Namespaces */ +#define MIMI_NVS_WIFI "wifi_config" +#define MIMI_NVS_TG "tg_config" +#define MIMI_NVS_LLM "llm_config" +#define MIMI_NVS_PROXY "proxy_config" +#define MIMI_NVS_SEARCH "search_config" + +/* NVS Keys */ +#define MIMI_NVS_KEY_SSID "ssid" +#define MIMI_NVS_KEY_PASS "password" +#define MIMI_NVS_KEY_TG_TOKEN "bot_token" +#define MIMI_NVS_KEY_API_KEY "api_key" +#define MIMI_NVS_KEY_MODEL "model" +#define MIMI_NVS_KEY_PROXY_HOST "host" +#define MIMI_NVS_KEY_PROXY_PORT "port" + diff --git a/main/proxy/http_proxy.c b/main/proxy/http_proxy.c index 4d71e22..a9e4e55 100644 --- a/main/proxy/http_proxy.c +++ b/main/proxy/http_proxy.c @@ -9,6 +9,7 @@ #include #include "esp_log.h" +#include "nvs.h" #include "esp_tls.h" #include "esp_crt_bundle.h" @@ -25,11 +26,27 @@ static uint16_t s_proxy_port = 0; esp_err_t http_proxy_init(void) { + /* Start with build-time defaults */ if (MIMI_SECRET_PROXY_HOST[0] != '\0' && MIMI_SECRET_PROXY_PORT[0] != '\0') { strncpy(s_proxy_host, MIMI_SECRET_PROXY_HOST, sizeof(s_proxy_host) - 1); s_proxy_port = (uint16_t)atoi(MIMI_SECRET_PROXY_PORT); } + /* NVS overrides take highest priority (set via CLI) */ + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_PROXY, NVS_READONLY, &nvs) == ESP_OK) { + char tmp[64] = {0}; + size_t len = sizeof(tmp); + if (nvs_get_str(nvs, MIMI_NVS_KEY_PROXY_HOST, tmp, &len) == ESP_OK && tmp[0]) { + strncpy(s_proxy_host, tmp, sizeof(s_proxy_host) - 1); + uint16_t port = 0; + if (nvs_get_u16(nvs, MIMI_NVS_KEY_PROXY_PORT, &port) == ESP_OK && port) { + s_proxy_port = port; + } + } + nvs_close(nvs); + } + if (s_proxy_host[0] && s_proxy_port) { ESP_LOGI(TAG, "Proxy configured: %s:%d", s_proxy_host, s_proxy_port); } else { @@ -38,6 +55,36 @@ esp_err_t http_proxy_init(void) return ESP_OK; } +esp_err_t http_proxy_set(const char *host, uint16_t port) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_PROXY, NVS_READWRITE, &nvs)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_PROXY_HOST, host)); + ESP_ERROR_CHECK(nvs_set_u16(nvs, MIMI_NVS_KEY_PROXY_PORT, port)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + + strncpy(s_proxy_host, host, sizeof(s_proxy_host) - 1); + s_proxy_port = port; + ESP_LOGI(TAG, "Proxy set to %s:%d", s_proxy_host, s_proxy_port); + return ESP_OK; +} + +esp_err_t http_proxy_clear(void) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_PROXY, NVS_READWRITE, &nvs)); + nvs_erase_key(nvs, MIMI_NVS_KEY_PROXY_HOST); + nvs_erase_key(nvs, MIMI_NVS_KEY_PROXY_PORT); + nvs_commit(nvs); + nvs_close(nvs); + + s_proxy_host[0] = '\0'; + s_proxy_port = 0; + ESP_LOGI(TAG, "Proxy cleared"); + return ESP_OK; +} + bool http_proxy_is_enabled(void) { return s_proxy_host[0] != '\0' && s_proxy_port != 0; diff --git a/main/proxy/http_proxy.h b/main/proxy/http_proxy.h index bf1e746..f5243a4 100644 --- a/main/proxy/http_proxy.h +++ b/main/proxy/http_proxy.h @@ -14,6 +14,16 @@ esp_err_t http_proxy_init(void); */ bool http_proxy_is_enabled(void); +/** + * Save proxy host and port to NVS. + */ +esp_err_t http_proxy_set(const char *host, uint16_t port); + +/** + * Remove proxy config from NVS. + */ +esp_err_t http_proxy_clear(void); + /* ── Proxied HTTPS connection ─────────────────────────────────── */ typedef struct proxy_conn proxy_conn_t; diff --git a/main/telegram/telegram_bot.c b/main/telegram/telegram_bot.c index 763c5e6..2610f0e 100644 --- a/main/telegram/telegram_bot.c +++ b/main/telegram/telegram_bot.c @@ -8,6 +8,7 @@ #include "esp_log.h" #include "esp_http_client.h" #include "esp_crt_bundle.h" +#include "nvs.h" #include "cJSON.h" static const char *TAG = "telegram"; @@ -256,10 +257,23 @@ static void telegram_poll_task(void *arg) esp_err_t telegram_bot_init(void) { + /* NVS overrides take highest priority (set via CLI) */ + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_TG, NVS_READONLY, &nvs) == ESP_OK) { + char tmp[128] = {0}; + size_t len = sizeof(tmp); + if (nvs_get_str(nvs, MIMI_NVS_KEY_TG_TOKEN, tmp, &len) == ESP_OK && tmp[0]) { + strncpy(s_bot_token, tmp, sizeof(s_bot_token) - 1); + } + nvs_close(nvs); + } + + /* s_bot_token is already initialized from MIMI_SECRET_TG_TOKEN as fallback */ + if (s_bot_token[0]) { ESP_LOGI(TAG, "Telegram bot token loaded (len=%d)", (int)strlen(s_bot_token)); } else { - ESP_LOGW(TAG, "No Telegram bot token. Set MIMI_SECRET_TG_TOKEN in mimi_secrets.h"); + ESP_LOGW(TAG, "No Telegram bot token. Use CLI: set_tg_token "); } return ESP_OK; } @@ -357,3 +371,15 @@ esp_err_t telegram_send_message(const char *chat_id, const char *text) return ESP_OK; } +esp_err_t telegram_set_token(const char *token) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_TG, NVS_READWRITE, &nvs)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_TG_TOKEN, token)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + + strncpy(s_bot_token, token, sizeof(s_bot_token) - 1); + ESP_LOGI(TAG, "Telegram bot token saved"); + return ESP_OK; +} diff --git a/main/telegram/telegram_bot.h b/main/telegram/telegram_bot.h index f0a6e13..e0ba36e 100644 --- a/main/telegram/telegram_bot.h +++ b/main/telegram/telegram_bot.h @@ -20,3 +20,8 @@ esp_err_t telegram_bot_start(void); */ esp_err_t telegram_send_message(const char *chat_id, const char *text); +/** + * Save the Telegram bot token to NVS. + */ +esp_err_t telegram_set_token(const char *token); + diff --git a/main/tools/tool_web_search.c b/main/tools/tool_web_search.c index b5f8f1d..d1b8ecf 100644 --- a/main/tools/tool_web_search.c +++ b/main/tools/tool_web_search.c @@ -8,6 +8,7 @@ #include "esp_http_client.h" #include "esp_crt_bundle.h" #include "esp_heap_caps.h" +#include "nvs.h" #include "cJSON.h" static const char *TAG = "web_search"; @@ -43,14 +44,26 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt) esp_err_t tool_web_search_init(void) { + /* Start with build-time default */ if (MIMI_SECRET_SEARCH_KEY[0] != '\0') { strncpy(s_search_key, MIMI_SECRET_SEARCH_KEY, sizeof(s_search_key) - 1); } + /* NVS overrides take highest priority (set via CLI) */ + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_SEARCH, NVS_READONLY, &nvs) == ESP_OK) { + char tmp[128] = {0}; + size_t len = sizeof(tmp); + if (nvs_get_str(nvs, MIMI_NVS_KEY_API_KEY, tmp, &len) == ESP_OK && tmp[0]) { + strncpy(s_search_key, tmp, sizeof(s_search_key) - 1); + } + nvs_close(nvs); + } + if (s_search_key[0]) { ESP_LOGI(TAG, "Web search initialized (key configured)"); } else { - ESP_LOGW(TAG, "No search API key. Set MIMI_SECRET_SEARCH_KEY in mimi_secrets.h"); + ESP_LOGW(TAG, "No search API key. Use CLI: set_search_key "); } return ESP_OK; } @@ -283,3 +296,16 @@ esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t o ESP_LOGI(TAG, "Search complete, %d bytes result", (int)strlen(output)); return ESP_OK; } + +esp_err_t tool_web_search_set_key(const char *api_key) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_SEARCH, NVS_READWRITE, &nvs)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_API_KEY, api_key)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + + strncpy(s_search_key, api_key, sizeof(s_search_key) - 1); + ESP_LOGI(TAG, "Search API key saved"); + return ESP_OK; +} diff --git a/main/tools/tool_web_search.h b/main/tools/tool_web_search.h index 371dc14..ba5b87b 100644 --- a/main/tools/tool_web_search.h +++ b/main/tools/tool_web_search.h @@ -18,3 +18,7 @@ esp_err_t tool_web_search_init(void); */ esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t output_size); +/** + * Save Brave Search API key to NVS. + */ +esp_err_t tool_web_search_set_key(const char *api_key); diff --git a/main/wifi/wifi_manager.c b/main/wifi/wifi_manager.c index 20884ba..30fc968 100644 --- a/main/wifi/wifi_manager.c +++ b/main/wifi/wifi_manager.c @@ -6,6 +6,8 @@ #include "esp_log.h" #include "esp_wifi.h" #include "esp_netif.h" +#include "nvs_flash.h" +#include "nvs.h" static const char *TAG = "wifi"; @@ -70,14 +72,34 @@ esp_err_t wifi_manager_init(void) esp_err_t wifi_manager_start(void) { - if (MIMI_SECRET_WIFI_SSID[0] == '\0') { - ESP_LOGW(TAG, "No WiFi credentials. Set MIMI_SECRET_WIFI_SSID in mimi_secrets.h"); - return ESP_ERR_NOT_FOUND; + wifi_config_t wifi_cfg = {0}; + bool found = false; + + /* NVS overrides take highest priority (set via CLI) */ + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_WIFI, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(wifi_cfg.sta.ssid); + if (nvs_get_str(nvs, MIMI_NVS_KEY_SSID, (char *)wifi_cfg.sta.ssid, &len) == ESP_OK) { + len = sizeof(wifi_cfg.sta.password); + nvs_get_str(nvs, MIMI_NVS_KEY_PASS, (char *)wifi_cfg.sta.password, &len); + found = true; + } + nvs_close(nvs); } - wifi_config_t wifi_cfg = {0}; - strncpy((char *)wifi_cfg.sta.ssid, MIMI_SECRET_WIFI_SSID, sizeof(wifi_cfg.sta.ssid) - 1); - strncpy((char *)wifi_cfg.sta.password, MIMI_SECRET_WIFI_PASS, sizeof(wifi_cfg.sta.password) - 1); + /* Fall back to build-time secrets */ + if (!found) { + if (MIMI_SECRET_WIFI_SSID[0] != '\0') { + strncpy((char *)wifi_cfg.sta.ssid, MIMI_SECRET_WIFI_SSID, sizeof(wifi_cfg.sta.ssid) - 1); + strncpy((char *)wifi_cfg.sta.password, MIMI_SECRET_WIFI_PASS, sizeof(wifi_cfg.sta.password) - 1); + found = true; + } + } + + if (!found) { + ESP_LOGW(TAG, "No WiFi credentials. Use CLI: wifi_set "); + return ESP_ERR_NOT_FOUND; + } ESP_LOGI(TAG, "Connecting to SSID: %s", wifi_cfg.sta.ssid); @@ -110,6 +132,18 @@ const char *wifi_manager_get_ip(void) return s_ip_str; } +esp_err_t wifi_manager_set_credentials(const char *ssid, const char *password) +{ + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open(MIMI_NVS_WIFI, NVS_READWRITE, &nvs)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_SSID, ssid)); + ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_PASS, password)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + ESP_LOGI(TAG, "WiFi credentials saved for SSID: %s", ssid); + return ESP_OK; +} + EventGroupHandle_t wifi_manager_get_event_group(void) { return s_wifi_event_group; diff --git a/main/wifi/wifi_manager.h b/main/wifi/wifi_manager.h index 1d8a205..503e6e8 100644 --- a/main/wifi/wifi_manager.h +++ b/main/wifi/wifi_manager.h @@ -35,6 +35,11 @@ bool wifi_manager_is_connected(void); */ const char *wifi_manager_get_ip(void); +/** + * Save WiFi credentials to NVS. + */ +esp_err_t wifi_manager_set_credentials(const char *ssid, const char *password); + /** * Get the event group for WiFi state (WIFI_CONNECTED_BIT / WIFI_FAIL_BIT). */