From e79e4e4932c0f694463edb5e66de95ea3da4d443 Mon Sep 17 00:00:00 2001 From: Bo Date: Tue, 10 Feb 2026 01:16:00 +0800 Subject: [PATCH] feat: add wifi scaner in uart. Signed-off-by: Bo --- .gitignore | 1 + main/cli/serial_cli.c | 51 +++- main/llm/llm_proxy.c | 555 +++++++++++++++++++++++++++++------- main/llm/llm_proxy.h | 11 +- main/mimi.c | 2 + main/mimi_config.h | 7 +- main/mimi_secrets.h.example | 1 + main/wifi/wifi_manager.c | 84 ++++++ main/wifi/wifi_manager.h | 5 + 9 files changed, 606 insertions(+), 111 deletions(-) diff --git a/.gitignore b/.gitignore index cdeda01..242b00b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ managed_components/ main/assets/lang_config.h main/mmap_generate_emoji.h mmap_generate_*.h +mimi_secrets.h # IDE / Editor .vscode/ diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 7eee34c..29a0eb0 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -102,6 +102,24 @@ static int cmd_set_model(int argc, char **argv) return 0; } +/* --- set_model_provider command --- */ +static struct { + struct arg_str *provider; + struct arg_end *end; +} provider_args; + +static int cmd_set_model_provider(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&provider_args); + if (nerrors != 0) { + arg_print_errors(stderr, provider_args.end, argv[0]); + return 1; + } + llm_set_provider(provider_args.provider->sval[0]); + printf("Model provider set.\n"); + return 0; +} + /* --- memory_read command --- */ static int cmd_memory_read(int argc, char **argv) { @@ -223,6 +241,15 @@ static int cmd_set_search_key(int argc, char **argv) return 0; } +/* --- wifi_scan command --- */ +static int cmd_wifi_scan(int argc, char **argv) +{ + (void)argc; + (void)argv; + wifi_manager_scan_and_print(); + return 0; +} + /* --- config_show command --- */ static void print_config(const char *label, const char *ns, const char *key, const char *build_val, bool mask) @@ -263,6 +290,7 @@ static int cmd_config_show(int argc, char **argv) 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("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("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true); @@ -332,6 +360,14 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&wifi_status_cmd); + /* wifi_scan */ + esp_console_cmd_t wifi_scan_cmd = { + .command = "wifi_scan", + .help = "Scan and list nearby WiFi APs", + .func = &cmd_wifi_scan, + }; + esp_console_cmd_register(&wifi_scan_cmd); + /* set_tg_token */ tg_token_args.token = arg_str1(NULL, NULL, "", "Telegram bot token"); tg_token_args.end = arg_end(1); @@ -344,11 +380,11 @@ esp_err_t serial_cli_init(void) esp_console_cmd_register(&tg_token_cmd); /* set_api_key */ - api_key_args.key = arg_str1(NULL, NULL, "", "Anthropic API key"); + api_key_args.key = arg_str1(NULL, NULL, "", "LLM 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", + .help = "Set LLM API key", .func = &cmd_set_api_key, .argtable = &api_key_args, }; @@ -365,6 +401,17 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&model_cmd); + /* set_model_provider */ + provider_args.provider = arg_str1(NULL, NULL, "", "Model provider (anthropic|openai)"); + provider_args.end = arg_end(1); + esp_console_cmd_t provider_cmd = { + .command = "set_model_provider", + .help = "Set LLM model provider (default: " MIMI_LLM_PROVIDER_DEFAULT ")", + .func = &cmd_set_model_provider, + .argtable = &provider_args, + }; + esp_console_cmd_register(&provider_cmd); + /* memory_read */ esp_console_cmd_t mem_read_cmd = { .command = "memory_read", diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index 82e80b6..2835cd4 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -15,6 +15,17 @@ static const char *TAG = "llm"; static char s_api_key[128] = {0}; static char s_model[64] = MIMI_LLM_DEFAULT_MODEL; +static char s_provider[16] = MIMI_LLM_PROVIDER_DEFAULT; + +static void safe_copy(char *dst, size_t dst_size, const char *src) +{ + if (!dst || dst_size == 0) return; + if (!src) { + dst[0] = '\0'; + return; + } + snprintf(dst, dst_size, "%s", src); +} /* ── Response buffer ──────────────────────────────────────────── */ @@ -67,16 +78,41 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt) return ESP_OK; } +/* ── Provider helpers ──────────────────────────────────────────── */ + +static bool provider_is_openai(void) +{ + return strcmp(s_provider, "openai") == 0; +} + +static const char *llm_api_url(void) +{ + return provider_is_openai() ? MIMI_OPENAI_API_URL : MIMI_LLM_API_URL; +} + +static const char *llm_api_host(void) +{ + return provider_is_openai() ? "api.openai.com" : "api.anthropic.com"; +} + +static const char *llm_api_path(void) +{ + return provider_is_openai() ? "/v1/chat/completions" : "/v1/messages"; +} + /* ── Init ─────────────────────────────────────────────────────── */ 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); + safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_API_KEY); } if (MIMI_SECRET_MODEL[0] != '\0') { - strncpy(s_model, MIMI_SECRET_MODEL, sizeof(s_model) - 1); + safe_copy(s_model, sizeof(s_model), MIMI_SECRET_MODEL); + } + if (MIMI_SECRET_MODEL_PROVIDER[0] != '\0') { + safe_copy(s_provider, sizeof(s_provider), MIMI_SECRET_MODEL_PROVIDER); } /* NVS overrides take highest priority (set via CLI) */ @@ -85,18 +121,23 @@ esp_err_t llm_proxy_init(void) 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); + safe_copy(s_api_key, sizeof(s_api_key), tmp); } 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); + safe_copy(s_model, sizeof(s_model), tmp); + } + len = sizeof(tmp); + memset(tmp, 0, sizeof(tmp)); + if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, tmp, &len) == ESP_OK && tmp[0]) { + safe_copy(s_provider, sizeof(s_provider), tmp); } nvs_close(nvs); } if (s_api_key[0]) { - ESP_LOGI(TAG, "LLM proxy initialized (model: %s)", s_model); + ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model); } else { ESP_LOGW(TAG, "No API key. Use CLI: set_api_key "); } @@ -108,7 +149,7 @@ esp_err_t llm_proxy_init(void) static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out_status) { esp_http_client_config_t config = { - .url = MIMI_LLM_API_URL, + .url = llm_api_url(), .event_handler = http_event_handler, .user_data = rb, .timeout_ms = 120 * 1000, @@ -122,8 +163,16 @@ static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out esp_http_client_set_method(client, HTTP_METHOD_POST); esp_http_client_set_header(client, "Content-Type", "application/json"); - esp_http_client_set_header(client, "x-api-key", s_api_key); - esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION); + if (provider_is_openai()) { + if (s_api_key[0]) { + char auth[192]; + snprintf(auth, sizeof(auth), "Bearer %s", s_api_key); + esp_http_client_set_header(client, "Authorization", auth); + } + } else { + esp_http_client_set_header(client, "x-api-key", s_api_key); + esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION); + } esp_http_client_set_post_field(client, post_data, strlen(post_data)); esp_err_t err = esp_http_client_perform(client); @@ -136,20 +185,32 @@ static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int *out_status) { - proxy_conn_t *conn = proxy_conn_open("api.anthropic.com", 443, 30000); + proxy_conn_t *conn = proxy_conn_open(llm_api_host(), 443, 30000); if (!conn) return ESP_ERR_HTTP_CONNECT; int body_len = strlen(post_data); char header[512]; - int hlen = snprintf(header, sizeof(header), - "POST /v1/messages HTTP/1.1\r\n" - "Host: api.anthropic.com\r\n" - "Content-Type: application/json\r\n" - "x-api-key: %s\r\n" - "anthropic-version: %s\r\n" - "Content-Length: %d\r\n" - "Connection: close\r\n\r\n", - s_api_key, MIMI_LLM_API_VERSION, body_len); + int hlen = 0; + if (provider_is_openai()) { + hlen = snprintf(header, sizeof(header), + "POST %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Content-Type: application/json\r\n" + "Authorization: Bearer %s\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n\r\n", + llm_api_path(), llm_api_host(), s_api_key, body_len); + } else { + hlen = snprintf(header, sizeof(header), + "POST %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Content-Type: application/json\r\n" + "x-api-key: %s\r\n" + "anthropic-version: %s\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n\r\n", + llm_api_path(), llm_api_host(), s_api_key, MIMI_LLM_API_VERSION, body_len); + } if (proxy_conn_write(conn, header, hlen) < 0 || proxy_conn_write(conn, post_data, body_len) < 0) { @@ -199,7 +260,7 @@ static esp_err_t llm_http_call(const char *post_data, resp_buf_t *rb, int *out_s /* ── Parse text from JSON response ────────────────────────────── */ -static void extract_text(cJSON *root, char *buf, size_t size) +static void extract_text_anthropic(cJSON *root, char *buf, size_t size) { buf[0] = '\0'; cJSON *content = cJSON_GetObjectItem(root, "content"); @@ -220,6 +281,190 @@ static void extract_text(cJSON *root, char *buf, size_t size) buf[off] = '\0'; } +static void extract_text_openai(cJSON *root, char *buf, size_t size) +{ + buf[0] = '\0'; + cJSON *choices = cJSON_GetObjectItem(root, "choices"); + if (!choices || !cJSON_IsArray(choices)) return; + cJSON *choice0 = cJSON_GetArrayItem(choices, 0); + if (!choice0) return; + cJSON *message = cJSON_GetObjectItem(choice0, "message"); + if (!message) return; + cJSON *content = cJSON_GetObjectItem(message, "content"); + if (!content || !cJSON_IsString(content)) return; + strncpy(buf, content->valuestring, size - 1); + buf[size - 1] = '\0'; +} + +static cJSON *convert_tools_openai(const char *tools_json) +{ + if (!tools_json) return NULL; + cJSON *arr = cJSON_Parse(tools_json); + if (!arr || !cJSON_IsArray(arr)) { + cJSON_Delete(arr); + return NULL; + } + cJSON *out = cJSON_CreateArray(); + cJSON *tool; + cJSON_ArrayForEach(tool, arr) { + cJSON *name = cJSON_GetObjectItem(tool, "name"); + cJSON *desc = cJSON_GetObjectItem(tool, "description"); + cJSON *schema = cJSON_GetObjectItem(tool, "input_schema"); + if (!name || !cJSON_IsString(name)) continue; + + cJSON *func = cJSON_CreateObject(); + cJSON_AddStringToObject(func, "name", name->valuestring); + if (desc && cJSON_IsString(desc)) { + cJSON_AddStringToObject(func, "description", desc->valuestring); + } + if (schema) { + cJSON_AddItemToObject(func, "parameters", cJSON_Duplicate(schema, 1)); + } + + cJSON *wrap = cJSON_CreateObject(); + cJSON_AddStringToObject(wrap, "type", "function"); + cJSON_AddItemToObject(wrap, "function", func); + cJSON_AddItemToArray(out, wrap); + } + cJSON_Delete(arr); + return out; +} + +static cJSON *convert_messages_openai(const char *system_prompt, cJSON *messages) +{ + cJSON *out = cJSON_CreateArray(); + if (system_prompt && system_prompt[0]) { + cJSON *sys = cJSON_CreateObject(); + cJSON_AddStringToObject(sys, "role", "system"); + cJSON_AddStringToObject(sys, "content", system_prompt); + cJSON_AddItemToArray(out, sys); + } + + if (!messages || !cJSON_IsArray(messages)) return out; + + cJSON *msg; + cJSON_ArrayForEach(msg, messages) { + cJSON *role = cJSON_GetObjectItem(msg, "role"); + cJSON *content = cJSON_GetObjectItem(msg, "content"); + if (!role || !cJSON_IsString(role)) continue; + + if (content && cJSON_IsString(content)) { + cJSON *m = cJSON_CreateObject(); + cJSON_AddStringToObject(m, "role", role->valuestring); + cJSON_AddStringToObject(m, "content", content->valuestring); + cJSON_AddItemToArray(out, m); + continue; + } + + if (!content || !cJSON_IsArray(content)) continue; + + if (strcmp(role->valuestring, "assistant") == 0) { + cJSON *m = cJSON_CreateObject(); + cJSON_AddStringToObject(m, "role", "assistant"); + + /* collect text */ + char *text_buf = NULL; + size_t off = 0; + cJSON *block; + cJSON *tool_calls = NULL; + cJSON_ArrayForEach(block, content) { + cJSON *btype = cJSON_GetObjectItem(block, "type"); + if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "text") == 0) { + cJSON *text = cJSON_GetObjectItem(block, "text"); + if (text && cJSON_IsString(text)) { + size_t tlen = strlen(text->valuestring); + char *tmp = realloc(text_buf, off + tlen + 1); + if (tmp) { + text_buf = tmp; + memcpy(text_buf + off, text->valuestring, tlen); + off += tlen; + text_buf[off] = '\0'; + } + } + } else if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "tool_use") == 0) { + if (!tool_calls) tool_calls = cJSON_CreateArray(); + cJSON *id = cJSON_GetObjectItem(block, "id"); + cJSON *name = cJSON_GetObjectItem(block, "name"); + cJSON *input = cJSON_GetObjectItem(block, "input"); + if (!name || !cJSON_IsString(name)) continue; + + cJSON *tc = cJSON_CreateObject(); + if (id && cJSON_IsString(id)) { + cJSON_AddStringToObject(tc, "id", id->valuestring); + } + cJSON_AddStringToObject(tc, "type", "function"); + cJSON *func = cJSON_CreateObject(); + cJSON_AddStringToObject(func, "name", name->valuestring); + if (input) { + char *args = cJSON_PrintUnformatted(input); + if (args) { + cJSON_AddStringToObject(func, "arguments", args); + free(args); + } + } + cJSON_AddItemToObject(tc, "function", func); + cJSON_AddItemToArray(tool_calls, tc); + } + } + if (text_buf) { + cJSON_AddStringToObject(m, "content", text_buf); + } else { + cJSON_AddStringToObject(m, "content", ""); + } + if (tool_calls) { + cJSON_AddItemToObject(m, "tool_calls", tool_calls); + } + cJSON_AddItemToArray(out, m); + free(text_buf); + } else if (strcmp(role->valuestring, "user") == 0) { + /* tool_result blocks become role=tool */ + cJSON *block; + bool has_user_text = false; + char *text_buf = NULL; + size_t off = 0; + cJSON_ArrayForEach(block, content) { + cJSON *btype = cJSON_GetObjectItem(block, "type"); + if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "tool_result") == 0) { + cJSON *tool_id = cJSON_GetObjectItem(block, "tool_use_id"); + cJSON *tcontent = cJSON_GetObjectItem(block, "content"); + if (!tool_id || !cJSON_IsString(tool_id)) continue; + cJSON *tm = cJSON_CreateObject(); + cJSON_AddStringToObject(tm, "role", "tool"); + cJSON_AddStringToObject(tm, "tool_call_id", tool_id->valuestring); + if (tcontent && cJSON_IsString(tcontent)) { + cJSON_AddStringToObject(tm, "content", tcontent->valuestring); + } else { + cJSON_AddStringToObject(tm, "content", ""); + } + cJSON_AddItemToArray(out, tm); + } else if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "text") == 0) { + cJSON *text = cJSON_GetObjectItem(block, "text"); + if (text && cJSON_IsString(text)) { + size_t tlen = strlen(text->valuestring); + char *tmp = realloc(text_buf, off + tlen + 1); + if (tmp) { + text_buf = tmp; + memcpy(text_buf + off, text->valuestring, tlen); + off += tlen; + text_buf[off] = '\0'; + } + has_user_text = true; + } + } + } + if (has_user_text) { + cJSON *um = cJSON_CreateObject(); + cJSON_AddStringToObject(um, "role", "user"); + cJSON_AddStringToObject(um, "content", text_buf); + cJSON_AddItemToArray(out, um); + } + free(text_buf); + } + } + + return out; +} + /* ── Public: simple chat (backward compat) ────────────────────── */ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, @@ -234,18 +479,32 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, cJSON *body = cJSON_CreateObject(); cJSON_AddStringToObject(body, "model", s_model); cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS); - cJSON_AddStringToObject(body, "system", system_prompt); - cJSON *messages = cJSON_Parse(messages_json); - if (messages) { - cJSON_AddItemToObject(body, "messages", messages); + if (provider_is_openai()) { + cJSON *messages = cJSON_Parse(messages_json); + if (!messages) { + messages = cJSON_CreateArray(); + cJSON *msg = cJSON_CreateObject(); + cJSON_AddStringToObject(msg, "role", "user"); + cJSON_AddStringToObject(msg, "content", messages_json); + cJSON_AddItemToArray(messages, msg); + } + cJSON *openai_msgs = convert_messages_openai(system_prompt, messages); + cJSON_Delete(messages); + cJSON_AddItemToObject(body, "messages", openai_msgs); } else { - cJSON *arr = cJSON_CreateArray(); - cJSON *msg = cJSON_CreateObject(); - cJSON_AddStringToObject(msg, "role", "user"); - cJSON_AddStringToObject(msg, "content", messages_json); - cJSON_AddItemToArray(arr, msg); - cJSON_AddItemToObject(body, "messages", arr); + cJSON_AddStringToObject(body, "system", system_prompt); + cJSON *messages = cJSON_Parse(messages_json); + if (messages) { + cJSON_AddItemToObject(body, "messages", messages); + } else { + cJSON *arr = cJSON_CreateArray(); + cJSON *msg = cJSON_CreateObject(); + cJSON_AddStringToObject(msg, "role", "user"); + cJSON_AddStringToObject(msg, "content", messages_json); + cJSON_AddItemToArray(arr, msg); + cJSON_AddItemToObject(body, "messages", arr); + } } char *post_data = cJSON_PrintUnformatted(body); @@ -255,8 +514,8 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, return ESP_ERR_NO_MEM; } - ESP_LOGI(TAG, "Calling Claude API (model: %s, body: %d bytes)", - s_model, (int)strlen(post_data)); + ESP_LOGI(TAG, "Calling LLM API (provider: %s, model: %s, body: %d bytes)", + s_provider, s_model, (int)strlen(post_data)); resp_buf_t rb; if (resp_buf_init(&rb, MIMI_LLM_STREAM_BUF_SIZE) != ESP_OK) { @@ -294,13 +553,17 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, return ESP_FAIL; } - extract_text(root, response_buf, buf_size); + if (provider_is_openai()) { + extract_text_openai(root, response_buf, buf_size); + } else { + extract_text_anthropic(root, response_buf, buf_size); + } cJSON_Delete(root); if (response_buf[0] == '\0') { - snprintf(response_buf, buf_size, "No response from Claude API"); + snprintf(response_buf, buf_size, "No response from LLM API"); } else { - ESP_LOGI(TAG, "Claude response: %d bytes", (int)strlen(response_buf)); + ESP_LOGI(TAG, "LLM response: %d bytes", (int)strlen(response_buf)); } return ESP_OK; @@ -334,17 +597,31 @@ esp_err_t llm_chat_tools(const char *system_prompt, cJSON *body = cJSON_CreateObject(); cJSON_AddStringToObject(body, "model", s_model); cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS); - cJSON_AddStringToObject(body, "system", system_prompt); - /* Deep-copy messages so caller keeps ownership */ - cJSON *msgs_copy = cJSON_Duplicate(messages, 1); - cJSON_AddItemToObject(body, "messages", msgs_copy); + if (provider_is_openai()) { + cJSON *openai_msgs = convert_messages_openai(system_prompt, messages); + cJSON_AddItemToObject(body, "messages", openai_msgs); - /* Add tools array if provided */ - if (tools_json) { - cJSON *tools = cJSON_Parse(tools_json); - if (tools) { - cJSON_AddItemToObject(body, "tools", tools); + if (tools_json) { + cJSON *tools = convert_tools_openai(tools_json); + if (tools) { + cJSON_AddItemToObject(body, "tools", tools); + cJSON_AddStringToObject(body, "tool_choice", "auto"); + } + } + } else { + cJSON_AddStringToObject(body, "system", system_prompt); + + /* Deep-copy messages so caller keeps ownership */ + cJSON *msgs_copy = cJSON_Duplicate(messages, 1); + cJSON_AddItemToObject(body, "messages", msgs_copy); + + /* Add tools array if provided */ + if (tools_json) { + cJSON *tools = cJSON_Parse(tools_json); + if (tools) { + cJSON_AddItemToObject(body, "tools", tools); + } } } @@ -352,8 +629,8 @@ esp_err_t llm_chat_tools(const char *system_prompt, cJSON_Delete(body); if (!post_data) return ESP_ERR_NO_MEM; - ESP_LOGI(TAG, "Calling Claude API with tools (model: %s, body: %d bytes)", - s_model, (int)strlen(post_data)); + ESP_LOGI(TAG, "Calling LLM API with tools (provider: %s, model: %s, body: %d bytes)", + s_provider, s_model, (int)strlen(post_data)); /* HTTP call */ resp_buf_t rb; @@ -387,73 +664,128 @@ esp_err_t llm_chat_tools(const char *system_prompt, return ESP_FAIL; } - /* stop_reason */ - cJSON *stop_reason = cJSON_GetObjectItem(root, "stop_reason"); - if (stop_reason && cJSON_IsString(stop_reason)) { - resp->tool_use = (strcmp(stop_reason->valuestring, "tool_use") == 0); - } + if (provider_is_openai()) { + cJSON *choices = cJSON_GetObjectItem(root, "choices"); + cJSON *choice0 = choices && cJSON_IsArray(choices) ? cJSON_GetArrayItem(choices, 0) : NULL; + if (choice0) { + cJSON *finish = cJSON_GetObjectItem(choice0, "finish_reason"); + if (finish && cJSON_IsString(finish)) { + resp->tool_use = (strcmp(finish->valuestring, "tool_calls") == 0); + } - /* Iterate content blocks */ - cJSON *content = cJSON_GetObjectItem(root, "content"); - if (content && cJSON_IsArray(content)) { - /* Accumulate total text length first */ - size_t total_text = 0; - cJSON *block; - cJSON_ArrayForEach(block, content) { - cJSON *btype = cJSON_GetObjectItem(block, "type"); - if (btype && strcmp(btype->valuestring, "text") == 0) { - cJSON *text = cJSON_GetObjectItem(block, "text"); - if (text && cJSON_IsString(text)) { - total_text += strlen(text->valuestring); + cJSON *message = cJSON_GetObjectItem(choice0, "message"); + if (message) { + cJSON *content = cJSON_GetObjectItem(message, "content"); + if (content && cJSON_IsString(content)) { + size_t tlen = strlen(content->valuestring); + resp->text = calloc(1, tlen + 1); + if (resp->text) { + memcpy(resp->text, content->valuestring, tlen); + resp->text_len = tlen; + } + } + + cJSON *tool_calls = cJSON_GetObjectItem(message, "tool_calls"); + if (tool_calls && cJSON_IsArray(tool_calls)) { + cJSON *tc; + cJSON_ArrayForEach(tc, tool_calls) { + if (resp->call_count >= MIMI_MAX_TOOL_CALLS) break; + llm_tool_call_t *call = &resp->calls[resp->call_count]; + cJSON *id = cJSON_GetObjectItem(tc, "id"); + cJSON *func = cJSON_GetObjectItem(tc, "function"); + if (id && cJSON_IsString(id)) { + strncpy(call->id, id->valuestring, sizeof(call->id) - 1); + } + if (func) { + cJSON *name = cJSON_GetObjectItem(func, "name"); + cJSON *args = cJSON_GetObjectItem(func, "arguments"); + if (name && cJSON_IsString(name)) { + strncpy(call->name, name->valuestring, sizeof(call->name) - 1); + } + if (args && cJSON_IsString(args)) { + call->input = strdup(args->valuestring); + if (call->input) { + call->input_len = strlen(call->input); + } + } + } + resp->call_count++; + } + if (resp->call_count > 0) { + resp->tool_use = true; + } } } } + } else { + /* stop_reason */ + cJSON *stop_reason = cJSON_GetObjectItem(root, "stop_reason"); + if (stop_reason && cJSON_IsString(stop_reason)) { + resp->tool_use = (strcmp(stop_reason->valuestring, "tool_use") == 0); + } - /* Allocate and copy text */ - if (total_text > 0) { - resp->text = calloc(1, total_text + 1); - if (resp->text) { - cJSON_ArrayForEach(block, content) { - cJSON *btype = cJSON_GetObjectItem(block, "type"); - if (!btype || strcmp(btype->valuestring, "text") != 0) continue; + /* Iterate content blocks */ + cJSON *content = cJSON_GetObjectItem(root, "content"); + if (content && cJSON_IsArray(content)) { + /* Accumulate total text length first */ + size_t total_text = 0; + cJSON *block; + cJSON_ArrayForEach(block, content) { + cJSON *btype = cJSON_GetObjectItem(block, "type"); + if (btype && strcmp(btype->valuestring, "text") == 0) { cJSON *text = cJSON_GetObjectItem(block, "text"); - if (!text || !cJSON_IsString(text)) continue; - size_t tlen = strlen(text->valuestring); - memcpy(resp->text + resp->text_len, text->valuestring, tlen); - resp->text_len += tlen; - } - resp->text[resp->text_len] = '\0'; - } - } - - /* Extract tool_use blocks */ - cJSON_ArrayForEach(block, content) { - cJSON *btype = cJSON_GetObjectItem(block, "type"); - if (!btype || strcmp(btype->valuestring, "tool_use") != 0) continue; - if (resp->call_count >= MIMI_MAX_TOOL_CALLS) break; - - llm_tool_call_t *call = &resp->calls[resp->call_count]; - - cJSON *id = cJSON_GetObjectItem(block, "id"); - if (id && cJSON_IsString(id)) { - strncpy(call->id, id->valuestring, sizeof(call->id) - 1); - } - - cJSON *name = cJSON_GetObjectItem(block, "name"); - if (name && cJSON_IsString(name)) { - strncpy(call->name, name->valuestring, sizeof(call->name) - 1); - } - - cJSON *input = cJSON_GetObjectItem(block, "input"); - if (input) { - char *input_str = cJSON_PrintUnformatted(input); - if (input_str) { - call->input = input_str; - call->input_len = strlen(input_str); + if (text && cJSON_IsString(text)) { + total_text += strlen(text->valuestring); + } } } - resp->call_count++; + /* Allocate and copy text */ + if (total_text > 0) { + resp->text = calloc(1, total_text + 1); + if (resp->text) { + cJSON_ArrayForEach(block, content) { + cJSON *btype = cJSON_GetObjectItem(block, "type"); + if (!btype || strcmp(btype->valuestring, "text") != 0) continue; + cJSON *text = cJSON_GetObjectItem(block, "text"); + if (!text || !cJSON_IsString(text)) continue; + size_t tlen = strlen(text->valuestring); + memcpy(resp->text + resp->text_len, text->valuestring, tlen); + resp->text_len += tlen; + } + resp->text[resp->text_len] = '\0'; + } + } + + /* Extract tool_use blocks */ + cJSON_ArrayForEach(block, content) { + cJSON *btype = cJSON_GetObjectItem(block, "type"); + if (!btype || strcmp(btype->valuestring, "tool_use") != 0) continue; + if (resp->call_count >= MIMI_MAX_TOOL_CALLS) break; + + llm_tool_call_t *call = &resp->calls[resp->call_count]; + + cJSON *id = cJSON_GetObjectItem(block, "id"); + if (id && cJSON_IsString(id)) { + strncpy(call->id, id->valuestring, sizeof(call->id) - 1); + } + + cJSON *name = cJSON_GetObjectItem(block, "name"); + if (name && cJSON_IsString(name)) { + strncpy(call->name, name->valuestring, sizeof(call->name) - 1); + } + + cJSON *input = cJSON_GetObjectItem(block, "input"); + if (input) { + char *input_str = cJSON_PrintUnformatted(input); + if (input_str) { + call->input = input_str; + call->input_len = strlen(input_str); + } + } + + resp->call_count++; + } } } @@ -476,7 +808,7 @@ esp_err_t llm_set_api_key(const char *api_key) ESP_ERROR_CHECK(nvs_commit(nvs)); nvs_close(nvs); - strncpy(s_api_key, api_key, sizeof(s_api_key) - 1); + safe_copy(s_api_key, sizeof(s_api_key), api_key); ESP_LOGI(TAG, "API key saved"); return ESP_OK; } @@ -489,7 +821,20 @@ esp_err_t llm_set_model(const char *model) ESP_ERROR_CHECK(nvs_commit(nvs)); nvs_close(nvs); - strncpy(s_model, model, sizeof(s_model) - 1); + safe_copy(s_model, sizeof(s_model), model); ESP_LOGI(TAG, "Model set to: %s", s_model); return ESP_OK; } + +esp_err_t llm_set_provider(const char *provider) +{ + 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_PROVIDER, provider)); + ESP_ERROR_CHECK(nvs_commit(nvs)); + nvs_close(nvs); + + safe_copy(s_provider, sizeof(s_provider), provider); + ESP_LOGI(TAG, "Provider set to: %s", s_provider); + return ESP_OK; +} diff --git a/main/llm/llm_proxy.h b/main/llm/llm_proxy.h index 064606c..03c3967 100644 --- a/main/llm/llm_proxy.h +++ b/main/llm/llm_proxy.h @@ -13,17 +13,22 @@ esp_err_t llm_proxy_init(void); /** - * Save the Anthropic API key to NVS. + * Save the LLM API key to NVS. */ esp_err_t llm_set_api_key(const char *api_key); +/** + * Save the LLM provider to NVS. (e.g. "anthropic", "openai") + */ +esp_err_t llm_set_provider(const char *provider); + /** * 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). + * Send a chat completion request to the configured LLM API (non-streaming). * * @param system_prompt System prompt string * @param messages_json JSON array of messages: [{"role":"user","content":"..."},...] @@ -54,7 +59,7 @@ typedef struct { void llm_response_free(llm_response_t *resp); /** - * Send a chat completion request with tools to Anthropic Messages API (streaming). + * Send a chat completion request with tools to the configured LLM API (non-streaming). * * @param system_prompt System prompt string * @param messages cJSON array of messages (caller owns) diff --git a/main/mimi.c b/main/mimi.c index 0c61150..1547c3a 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -117,6 +117,8 @@ void app_main(void) /* Start WiFi */ esp_err_t wifi_err = wifi_manager_start(); 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) { ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip()); diff --git a/main/mimi_config.h b/main/mimi_config.h index 5e15e8f..079ecc0 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -22,6 +22,9 @@ #ifndef MIMI_SECRET_MODEL #define MIMI_SECRET_MODEL "" #endif +#ifndef MIMI_SECRET_MODEL_PROVIDER +#define MIMI_SECRET_MODEL_PROVIDER "anthropic" +#endif #ifndef MIMI_SECRET_PROXY_HOST #define MIMI_SECRET_PROXY_HOST "" #endif @@ -57,8 +60,10 @@ /* LLM */ #define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5" +#define MIMI_LLM_PROVIDER_DEFAULT "anthropic" #define MIMI_LLM_MAX_TOKENS 4096 #define MIMI_LLM_API_URL "https://api.anthropic.com/v1/messages" +#define MIMI_OPENAI_API_URL "https://api.openai.com/v1/chat/completions" #define MIMI_LLM_API_VERSION "2023-06-01" #define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024) @@ -101,6 +106,6 @@ #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_PROVIDER "provider" #define MIMI_NVS_KEY_PROXY_HOST "host" #define MIMI_NVS_KEY_PROXY_PORT "port" - diff --git a/main/mimi_secrets.h.example b/main/mimi_secrets.h.example index 3c579f8..bf76def 100644 --- a/main/mimi_secrets.h.example +++ b/main/mimi_secrets.h.example @@ -20,6 +20,7 @@ /* Anthropic API */ #define MIMI_SECRET_API_KEY "" #define MIMI_SECRET_MODEL "" +#define MIMI_SECRET_MODEL_PROVIDER "anthropic" /* HTTP Proxy (leave empty or set both) */ #define MIMI_SECRET_PROXY_HOST "" diff --git a/main/wifi/wifi_manager.c b/main/wifi/wifi_manager.c index 30fc968..1ccbab2 100644 --- a/main/wifi/wifi_manager.c +++ b/main/wifi/wifi_manager.c @@ -16,6 +16,23 @@ static int s_retry_count = 0; static char s_ip_str[16] = "0.0.0.0"; static bool s_connected = false; +static const char *wifi_reason_to_str(wifi_err_reason_t reason) +{ + switch (reason) { + case WIFI_REASON_AUTH_EXPIRE: return "AUTH_EXPIRE"; + case WIFI_REASON_AUTH_FAIL: return "AUTH_FAIL"; + case WIFI_REASON_ASSOC_EXPIRE: return "ASSOC_EXPIRE"; + case WIFI_REASON_ASSOC_FAIL: return "ASSOC_FAIL"; + case WIFI_REASON_HANDSHAKE_TIMEOUT: return "HANDSHAKE_TIMEOUT"; + case WIFI_REASON_NO_AP_FOUND: return "NO_AP_FOUND"; + case WIFI_REASON_BEACON_TIMEOUT: return "BEACON_TIMEOUT"; + case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: return "4WAY_HANDSHAKE_TIMEOUT"; + case WIFI_REASON_MIC_FAILURE: return "MIC_FAILURE"; + case WIFI_REASON_CONNECTION_FAIL: return "CONNECTION_FAIL"; + default: return "UNKNOWN"; + } +} + static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { @@ -23,6 +40,10 @@ static void event_handler(void *arg, esp_event_base_t event_base, esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { s_connected = false; + wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data; + 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) { /* Exponential backoff: 1s, 2s, 4s, 8s, ... capped at 30s */ uint32_t delay_ms = MIMI_WIFI_RETRY_BASE_MS << s_retry_count; @@ -148,3 +169,66 @@ EventGroupHandle_t wifi_manager_get_event_group(void) { return s_wifi_event_group; } + +void wifi_manager_scan_and_print(void) +{ + wifi_scan_config_t scan_cfg = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = true, + }; + + ESP_LOGI(TAG, "Scanning nearby APs..."); + + /* Pause auto-connect to allow scan */ + esp_wifi_disconnect(); + vTaskDelay(pdMS_TO_TICKS(200)); + + esp_err_t err = esp_wifi_scan_start(&scan_cfg, true /* block */); + if (err == ESP_ERR_WIFI_STATE) { + /* Try a quick stop/start cycle and scan again */ + esp_wifi_stop(); + vTaskDelay(pdMS_TO_TICKS(200)); + esp_wifi_start(); + vTaskDelay(pdMS_TO_TICKS(200)); + err = esp_wifi_scan_start(&scan_cfg, true /* block */); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Scan failed: %s", esp_err_to_name(err)); + esp_wifi_connect(); + return; + } + + uint16_t ap_count = 0; + esp_wifi_scan_get_ap_num(&ap_count); + if (ap_count == 0) { + ESP_LOGW(TAG, "No APs found"); + esp_wifi_connect(); + return; + } + + wifi_ap_record_t *ap_list = calloc(ap_count, sizeof(wifi_ap_record_t)); + if (!ap_list) { + ESP_LOGE(TAG, "Out of memory for AP list"); + return; + } + + uint16_t ap_max = ap_count; + if (esp_wifi_scan_get_ap_records(&ap_max, ap_list) != ESP_OK) { + ESP_LOGE(TAG, "Failed to get AP records"); + free(ap_list); + esp_wifi_connect(); + return; + } + + ESP_LOGI(TAG, "Found %u APs:", ap_max); + for (uint16_t i = 0; i < ap_max; i++) { + const wifi_ap_record_t *ap = &ap_list[i]; + ESP_LOGI(TAG, " [%u] SSID=%s RSSI=%d CH=%d Auth=%d", + i + 1, (const char *)ap->ssid, ap->rssi, ap->primary, ap->authmode); + } + + free(ap_list); + esp_wifi_connect(); +} diff --git a/main/wifi/wifi_manager.h b/main/wifi/wifi_manager.h index 503e6e8..5f2b746 100644 --- a/main/wifi/wifi_manager.h +++ b/main/wifi/wifi_manager.h @@ -44,3 +44,8 @@ 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). */ EventGroupHandle_t wifi_manager_get_event_group(void); + +/** + * Scan and print nearby APs. + */ +void wifi_manager_scan_and_print(void);