feat: 添加时区设置功能,默认时区改为 CST-8
Some checks failed
Build / idf-build (push) Has been cancelled
Build & Release / build (push) Has been cancelled

- 新增 set_timezone LLM 工具,支持通过对话设置时区
- 新增 set_timezone / timezone_show CLI 命令
- 默认时区从 PST 改为 CST-8(中国标准时间 UTC+8)
- 支持 POSIX 格式和 18 个城市名映射(Asia/Shanghai 等)
- 时区通过 NVS 持久化存储(system_config namespace)
- config_show 中显示当前时区配置
- 更新 changelog.md 和 taolun.md 文档
This commit is contained in:
2026-04-01 00:50:41 +08:00
parent eedc6757d8
commit 7dc4122778
24 changed files with 645 additions and 52 deletions

View File

@@ -20,6 +20,7 @@ idf_component_register(
"tools/tool_cron.c"
"tools/tool_web_search.c"
"tools/tool_get_time.c"
"tools/tool_set_timezone.c"
"tools/tool_files.c"
"tools/tool_gpio.c"
"tools/gpio_policy.c"

View File

@@ -14,8 +14,12 @@ static size_t append_file(char *buf, size_t size, size_t offset, const char *pat
FILE *f = fopen(path, "r");
if (!f) return offset;
if (header && offset < size - 1) {
offset += snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
if (offset >= size) return offset;
if (header) {
int ret = snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
if (ret > 0) offset += (size_t)ret;
if (offset >= size) { offset = size - 1; buf[offset] = '\0'; fclose(f); return offset; }
}
size_t n = fread(buf + offset, 1, size - offset - 1, f);
@@ -79,23 +83,35 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
/* Long-term memory */
char mem_buf[4096];
if (memory_read_long_term(mem_buf, sizeof(mem_buf)) == ESP_OK && mem_buf[0]) {
off += snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
if (off < size) {
int ret = snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
if (ret > 0) off += (size_t)ret;
if (off >= size) off = size - 1;
}
}
/* Recent daily notes (last 3 days) */
char recent_buf[4096];
if (memory_read_recent(recent_buf, sizeof(recent_buf), 3) == ESP_OK && recent_buf[0]) {
off += snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
if (off < size) {
int ret = snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
if (ret > 0) off += (size_t)ret;
if (off >= size) off = size - 1;
}
}
/* Skills */
char skills_buf[2048];
size_t skills_len = skill_loader_build_summary(skills_buf, sizeof(skills_buf));
if (skills_len > 0) {
off += snprintf(buf + off, size - off,
"\n## Available Skills\n\n"
"Available skills (use read_file to load full instructions):\n%s\n",
skills_buf);
if (off < size) {
int ret = snprintf(buf + off, size - off,
"\n## Available Skills\n\n"
"Available skills (use read_file to load full instructions):\n%s\n",
skills_buf);
if (ret > 0) off += (size_t)ret;
if (off >= size) off = size - 1;
}
}
ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off);

View File

@@ -583,6 +583,7 @@ static void feishu_ws_event_handler(void *arg, esp_event_base_t base, int32_t ev
} else if (event_id == WEBSOCKET_EVENT_DISCONNECTED) {
s_ws_connected = false;
ESP_LOGW(TAG, "Feishu WS disconnected");
if (rx_buf) { free(rx_buf); rx_buf = NULL; rx_cap = 0; }
} else if (event_id == WEBSOCKET_EVENT_DATA) {
if (e->op_code != WS_TRANSPORT_OPCODES_BINARY) return;
size_t need = e->payload_offset + e->data_len;

View File

@@ -17,6 +17,8 @@
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <stdbool.h>
#include <time.h>
#include <dirent.h>
#include "esp_log.h"
#include "esp_console.h"
@@ -654,6 +656,11 @@ static int cmd_config_show(int argc, char **argv)
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);
/* System configs */
printf(" --- System configs ---\n");
print_config("Timezone", "system_config", MIMI_NVS_KEY_TIMEZONE, MIMI_TIMEZONE, false);
printf("=============================\n");
return 0;
}
@@ -676,6 +683,87 @@ static int cmd_config_reset(int argc, char **argv)
return 0;
}
/* --- set_timezone command --- */
static struct {
struct arg_str *tz;
struct arg_end *end;
} timezone_args;
static int cmd_set_timezone(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&timezone_args);
if (nerrors != 0) {
arg_print_errors(stderr, timezone_args.end, argv[0]);
return 1;
}
const char *tz_str = timezone_args.tz->sval[0];
nvs_handle_t nvs;
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
if (err != ESP_OK) {
printf("Failed to open NVS: %s\n", esp_err_to_name(err));
return 1;
}
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
if (err != ESP_OK) {
printf("Failed to save timezone: %s\n", esp_err_to_name(err));
nvs_close(nvs);
return 1;
}
err = nvs_commit(nvs);
nvs_close(nvs);
if (err != ESP_OK) {
printf("Failed to commit NVS: %s\n", esp_err_to_name(err));
return 1;
}
setenv("TZ", tz_str, 1);
tzset();
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
printf("Timezone set to '%s'. Current time: %s. Restart to apply permanently.\n", tz_str, time_str);
return 0;
}
/* --- timezone_show command --- */
static int cmd_timezone_show(int argc, char **argv)
{
(void)argc;
(void)argv;
char nvs_val[64] = {0};
const char *source = "build";
const char *display = MIMI_TIMEZONE;
nvs_handle_t nvs;
if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(nvs_val);
if (nvs_get_str(nvs, MIMI_NVS_KEY_TIMEZONE, nvs_val, &len) == ESP_OK && nvs_val[0]) {
source = "NVS";
display = nvs_val;
}
nvs_close(nvs);
}
printf("Current timezone: %s [%s]\n", display, source);
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z (%A)", &tm_now);
printf("Local time: %s\n", time_str);
return 0;
}
/* --- heartbeat_trigger command --- */
static int cmd_heartbeat_trigger(int argc, char **argv)
{
@@ -736,13 +824,22 @@ typedef struct {
size_t output_size;
esp_err_t err;
SemaphoreHandle_t done;
bool timed_out;
} web_search_task_ctx_t;
static void web_search_task(void *arg)
{
web_search_task_ctx_t *task_ctx = (web_search_task_ctx_t *)arg;
task_ctx->err = tool_web_search_execute(task_ctx->input_json, task_ctx->output, task_ctx->output_size);
xSemaphoreGive(task_ctx->done);
if (!task_ctx->timed_out) {
xSemaphoreGive(task_ctx->done);
} else {
/* Main thread timed out and freed ctx, so we must clean up ourselves */
free((void *)task_ctx->input_json);
free(task_ctx->output);
vSemaphoreDelete(task_ctx->done);
free(task_ctx);
}
vTaskDelete(NULL);
}
@@ -838,10 +935,8 @@ static int cmd_web_search(int argc, char **argv)
if (xSemaphoreTake(ctx->done, pdMS_TO_TICKS(45000)) != pdTRUE) {
printf("web_search status: timeout\n");
vSemaphoreDelete(ctx->done);
free(input_copy);
free(ctx);
free(output);
ctx->timed_out = true;
/* Task will clean up ctx, input_copy, output, and done on its own */
return 1;
}
esp_err_t err = ctx->err;
@@ -1163,6 +1258,25 @@ esp_err_t serial_cli_init(void)
};
esp_console_cmd_register(&config_reset_cmd);
/* set_timezone */
timezone_args.tz = arg_str1(NULL, NULL, "<timezone>", "Timezone (e.g. CST-8, Asia/Shanghai)");
timezone_args.end = arg_end(1);
esp_console_cmd_t set_timezone_cmd = {
.command = "set_timezone",
.help = "Set system timezone (e.g. set_timezone CST-8)",
.func = &cmd_set_timezone,
.argtable = &timezone_args,
};
esp_console_cmd_register(&set_timezone_cmd);
/* timezone_show */
esp_console_cmd_t timezone_show_cmd = {
.command = "timezone_show",
.help = "Show current timezone and local time",
.func = &cmd_timezone_show,
};
esp_console_cmd_register(&timezone_show_cmd);
/* heartbeat_trigger */
esp_console_cmd_t heartbeat_cmd = {
.command = "heartbeat_trigger",

View File

@@ -4,6 +4,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include "freertos/FreeRTOS.h"

View File

@@ -4,7 +4,9 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "esp_log.h"
#include "esp_http_server.h"
#include "cJSON.h"

View File

@@ -5,6 +5,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include "esp_log.h"
#include "esp_err.h"
#include "esp_http_client.h"

View File

@@ -5,6 +5,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include "esp_log.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
@@ -246,12 +247,25 @@ esp_err_t llm_proxy_init(void)
nvs_close(nvs);
}
/* Load provider-specific API key if available */
/* Load provider-specific API key from NVS */
const char *provider_api_key = llm_provider_get_api_key(s_provider);
if (provider_api_key && provider_api_key[0]) {
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
}
/* Fall back to build-time provider-specific API key if NVS key is empty */
if (s_api_key[0] == '\0') {
if (strcmp(s_provider, "siliconflow") == 0 && MIMI_SECRET_SILICONFLOW_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_SILICONFLOW_API_KEY);
} else if (strcmp(s_provider, "volcengine") == 0 && MIMI_SECRET_VOLCENGINE_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_VOLCENGINE_API_KEY);
} else if (strcmp(s_provider, "openai") == 0 && MIMI_SECRET_OPENAI_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_OPENAI_API_KEY);
} else if (strcmp(s_provider, "anthropic") == 0 && MIMI_SECRET_ANTHROPIC_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_ANTHROPIC_API_KEY);
}
}
if (s_api_key[0]) {
ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model);
} else {

View File

@@ -24,7 +24,7 @@ esp_err_t session_mgr_init(void)
esp_err_t session_append(const char *chat_id, const char *role, const char *content)
{
char path[64];
char path[128];
session_path(chat_id, path, sizeof(path));
FILE *f = fopen(path, "a");
@@ -52,7 +52,7 @@ esp_err_t session_append(const char *chat_id, const char *role, const char *cont
esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs)
{
char path[64];
char path[128];
session_path(chat_id, path, sizeof(path));
FILE *f = fopen(path, "r");
@@ -127,7 +127,7 @@ esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size,
esp_err_t session_clear(const char *chat_id)
{
char path[64];
char path[128];
session_path(chat_id, path, sizeof(path));
if (remove(path) == 0) {

View File

@@ -1,5 +1,6 @@
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

View File

@@ -47,6 +47,26 @@
#define MIMI_SECRET_TAVILY_KEY ""
#endif
/* Provider-specific API keys (fallback when generic MIMI_SECRET_API_KEY is empty) */
#ifndef MIMI_SECRET_ANTHROPIC_API_KEY
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
#endif
#ifndef MIMI_SECRET_OPENAI_API_KEY
#define MIMI_SECRET_OPENAI_API_KEY ""
#endif
#ifndef MIMI_SECRET_SILICONFLOW_API_KEY
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#endif
#ifndef MIMI_SECRET_SILICONFLOW_BASE_URL
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
#endif
#ifndef MIMI_SECRET_VOLCENGINE_API_KEY
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#endif
#ifndef MIMI_SECRET_VOLCENGINE_BASE_URL
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
#endif
/* WiFi */
#define MIMI_WIFI_MAX_RETRY 10
#define MIMI_WIFI_RETRY_BASE_MS 1000
@@ -80,7 +100,7 @@
#define MIMI_AGENT_SEND_WORKING_STATUS 1
/* Timezone (POSIX TZ format) */
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
#define MIMI_TIMEZONE "CST-8"
/* LLM */
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5"
@@ -166,6 +186,9 @@
#define MIMI_NVS_KEY_VOLCENGINE_API_KEY "volcengine_api_key"
#define MIMI_NVS_KEY_VOLCENGINE_BASE_URL "volcengine_base_url"
/* System NVS Keys */
#define MIMI_NVS_KEY_TIMEZONE "timezone"
/* WiFi Onboarding (Captive Portal) */
#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"
#define MIMI_ONBOARD_AP_PASS "" /* open network */

View File

@@ -21,16 +21,32 @@
#define MIMI_SECRET_FEISHU_APP_ID ""
#define MIMI_SECRET_FEISHU_APP_SECRET ""
/* Anthropic API */
/* LLM Configuration
*
* Method 1: Generic API key (works for any provider)
* Set MIMI_SECRET_API_KEY + MIMI_SECRET_MODEL_PROVIDER + MIMI_SECRET_MODEL
*
* Method 2: Provider-specific API keys (recommended for multi-provider setups)
* Set MIMI_SECRET_<PROVIDER>_API_KEY for each provider you want to use
* Switch providers via onboard portal or CLI: set_model_provider <provider>
*
* Priority: NVS (runtime config) > provider-specific key > generic key
*/
#define MIMI_SECRET_API_KEY ""
#define MIMI_SECRET_MODEL ""
#define MIMI_SECRET_MODEL "claude-opus-4-5"
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
/* SiliconFlow API (OpenAI-compatible) */
/* Anthropic (Claude) */
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
/* OpenAI (GPT) */
#define MIMI_SECRET_OPENAI_API_KEY ""
/* SiliconFlow (硅基流动, OpenAI-compatible) */
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
/* Volcengine API (OpenAI-compatible, ByteDance Doubao models) */
/* Volcengine (火山引擎/豆包, OpenAI-compatible) */
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"

View File

@@ -56,10 +56,14 @@ static const char ONBOARD_HTML[] =
"<label>Model</label>"
"<input id='model' placeholder='claude-opus-4-5' value='claude-opus-4-5'>"
"<label>Provider</label>"
"<select id='provider'>"
"<select id='provider' onchange='onProviderChange()'>"
"<option value='anthropic'>Anthropic</option>"
"<option value='openai'>OpenAI</option>"
"<option value='siliconflow'>SiliconFlow (硅基流动)</option>"
"<option value='volcengine'>Volcengine (火山引擎)</option>"
"</select>"
"<label>Base URL</label>"
"<input id='base_url' placeholder='https://api.example.com/v1'>"
"</div></div>"
/* Telegram section */
@@ -133,8 +137,15 @@ static const char ONBOARD_HTML[] =
"btn.textContent='Scan WiFi Networks';btn.disabled=false;"
"}).catch(()=>{btn.textContent='Scan WiFi Networks';btn.disabled=false})}"
"function onProviderChange(){"
"var p=document.getElementById('provider').value;"
"var u=document.getElementById('base_url');"
"if(p==='siliconflow'){u.value='https://api.siliconflow.cn/v1'}"
"else if(p==='volcengine'){u.value='https://ark.cn-beijing.volces.com/api/v3'}"
"else{u.value=''}}"
"function save(){"
"var fields=['ssid','password','api_key','model','provider','tg_token',"
"var fields=['ssid','password','api_key','model','provider','base_url','tg_token',"
"'feishu_app_id','feishu_app_secret','proxy_host','proxy_port','proxy_type','search_key','tavily_key'];"
"var data={};"
"fields.forEach(f=>{data[f]=document.getElementById(f).value.trim()});"

View File

@@ -220,6 +220,40 @@ static esp_err_t http_get_config(httpd_req_t *req)
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);
/* Provider-specific Base URL (load from current provider's NVS key) */
{
char base_url[256] = {0};
bool found = false;
char provider[32] = {0};
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(provider);
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider, &len) != ESP_OK || !provider[0]) {
/* Fall back to build-time provider if NVS doesn't have one */
strlcpy(provider, MIMI_SECRET_MODEL_PROVIDER, sizeof(provider));
}
/* Try to load provider-specific base URL from NVS */
const char *url_key = NULL;
if (strcmp(provider, "anthropic") == 0) url_key = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
else if (strcmp(provider, "openai") == 0) url_key = MIMI_NVS_KEY_OPENAI_BASE_URL;
else if (strcmp(provider, "siliconflow") == 0) url_key = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
else if (strcmp(provider, "volcengine") == 0) url_key = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
if (url_key) {
len = sizeof(base_url);
if (nvs_get_str(nvs, url_key, base_url, &len) == ESP_OK && base_url[0]) {
found = true;
}
}
nvs_close(nvs);
}
if (!found) {
/* Fall back to build-time defaults */
if (strcmp(provider, "siliconflow") == 0) strlcpy(base_url, MIMI_SECRET_SILICONFLOW_BASE_URL, sizeof(base_url));
else if (strcmp(provider, "volcengine") == 0) strlcpy(base_url, MIMI_SECRET_VOLCENGINE_BASE_URL, sizeof(base_url));
}
cJSON_AddStringToObject(root, "base_url", base_url);
}
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);
@@ -344,6 +378,37 @@ static esp_err_t http_post_save(httpd_req_t *req)
nvs_sync_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL);
nvs_sync_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER);
/* Save API key and base URL to provider-specific NVS keys */
{
cJSON *provider_item = cJSON_GetObjectItem(root, "provider");
cJSON *api_key_item = cJSON_GetObjectItem(root, "api_key");
cJSON *base_url_item = cJSON_GetObjectItem(root, "base_url");
if (provider_item && cJSON_IsString(provider_item) && provider_item->valuestring[0]) {
const char *provider = provider_item->valuestring;
const char *api_key_nvs = NULL;
const char *base_url_nvs = NULL;
if (strcmp(provider, "anthropic") == 0) {
api_key_nvs = MIMI_NVS_KEY_ANTHROPIC_API_KEY;
base_url_nvs = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
} else if (strcmp(provider, "openai") == 0) {
api_key_nvs = MIMI_NVS_KEY_OPENAI_API_KEY;
base_url_nvs = MIMI_NVS_KEY_OPENAI_BASE_URL;
} else if (strcmp(provider, "siliconflow") == 0) {
api_key_nvs = MIMI_NVS_KEY_SILICONFLOW_API_KEY;
base_url_nvs = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
} else if (strcmp(provider, "volcengine") == 0) {
api_key_nvs = MIMI_NVS_KEY_VOLCENGINE_API_KEY;
base_url_nvs = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
}
if (api_key_nvs && api_key_item && cJSON_IsString(api_key_item)) {
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, api_key_nvs);
}
if (base_url_nvs && base_url_item && cJSON_IsString(base_url_item)) {
nvs_sync_field(root, "base_url", MIMI_NVS_LLM, base_url_nvs);
}
}
}
/* Telegram */
nvs_sync_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN);

View File

@@ -3,6 +3,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/time.h>
@@ -246,7 +247,7 @@ static int open_socks5_tunnel(const char *host, int port, int timeout_ms)
free(req);
/* Receive connect response */
unsigned char resp[256];
unsigned char resp[512];
int resp_len = recv(sock, resp, sizeof(resp), 0);
if (resp_len < 10) {
ESP_LOGE(TAG, "No response from SOCKS5 proxy"); close(sock); return -1;

View File

@@ -4,6 +4,7 @@
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#ifndef GPIO_IS_VALID_GPIO

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <sys/time.h>
#include "esp_log.h"

View File

@@ -2,6 +2,7 @@
#include "mimi_config.h"
#include "tools/tool_web_search.h"
#include "tools/tool_get_time.h"
#include "tools/tool_set_timezone.h"
#include "tools/tool_files.h"
#include "tools/tool_cron.h"
#include "tools/tool_gpio.h"
@@ -83,6 +84,18 @@ esp_err_t tool_registry_init(void)
};
register_tool(&gt);
/* Register set_timezone */
mimi_tool_t stz = {
.name = "set_timezone",
.description = "Set the system timezone. Accepts POSIX format (e.g. CST-8, EST5EDT,M3.2.0,M11.1.0) or city name (e.g. Asia/Shanghai, America/New_York).",
.input_schema_json =
"{\"type\":\"object\","
"\"properties\":{\"timezone\":{\"type\":\"string\",\"description\":\"Timezone in POSIX format or city name (e.g. CST-8, Asia/Shanghai)\"}},"
"\"required\":[\"timezone\"]}",
.execute = tool_set_timezone_execute,
};
register_tool(&stz);
/* Register read_file */
mimi_tool_t rf = {
.name = "read_file",

View File

@@ -0,0 +1,145 @@
#include "tool_set_timezone.h"
#include "mimi_config.h"
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include "esp_log.h"
#include "nvs.h"
#include "cJSON.h"
static const char *TAG = "tool_timezone";
/* Common timezone mappings for user-friendly names */
typedef struct {
const char *name;
const char *posix_tz;
} tz_mapping_t;
static const tz_mapping_t tz_mappings[] = {
{ "Asia/Shanghai", "CST-8" },
{ "Asia/Beijing", "CST-8" },
{ "Asia/Hong_Kong", "HKT-8" },
{ "Asia/Tokyo", "JST-9" },
{ "Asia/Seoul", "KST-9" },
{ "Asia/Singapore", "SGT-8" },
{ "Asia/Kolkata", "IST-5:30" },
{ "Asia/Dubai", "GST-4" },
{ "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" },
{ "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" },
{ "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" },
{ "America/New_York", "EST5EDT,M3.2.0,M11.1.0" },
{ "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" },
{ "America/Denver", "MST7MDT,M3.2.0,M11.1.0" },
{ "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" },
{ "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" },
{ "UTC", "UTC0" },
{ "GMT", "GMT0" },
};
static const char *resolve_timezone(const char *tz_str)
{
if (!tz_str || !tz_str[0]) return NULL;
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
if (strcmp(tz_str, tz_mappings[i].name) == 0) {
return tz_mappings[i].posix_tz;
}
}
return tz_str;
}
static bool validate_timezone(const char *tz_str)
{
if (!tz_str || !tz_str[0]) return false;
if (strlen(tz_str) > 64) return false;
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
if (strcmp(tz_str, tz_mappings[i].name) == 0) return true;
}
if (strchr(tz_str, '+') || strchr(tz_str, '-') ||
strcmp(tz_str, "UTC0") == 0 || strcmp(tz_str, "GMT0") == 0) {
return true;
}
return false;
}
static esp_err_t save_timezone_nvs(const char *tz_str)
{
nvs_handle_t nvs;
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err));
return err;
}
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save timezone: %s", esp_err_to_name(err));
nvs_close(nvs);
return err;
}
err = nvs_commit(nvs);
nvs_close(nvs);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size)
{
ESP_LOGI(TAG, "Setting timezone...");
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}
cJSON *tz_item = cJSON_GetObjectItem(root, "timezone");
if (!tz_item || !cJSON_IsString(tz_item)) {
cJSON_Delete(root);
snprintf(output, output_size, "Error: 'timezone' field required (string)");
return ESP_ERR_INVALID_ARG;
}
const char *input_tz = tz_item->valuestring;
const char *resolved_tz = resolve_timezone(input_tz);
if (!resolved_tz || !validate_timezone(resolved_tz)) {
cJSON_Delete(root);
snprintf(output, output_size, "Error: invalid timezone format '%s'. Use POSIX format (e.g. CST-8) or city name (e.g. Asia/Shanghai)", input_tz);
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = save_timezone_nvs(resolved_tz);
if (err != ESP_OK) {
cJSON_Delete(root);
snprintf(output, output_size, "Error: failed to save timezone (%s)", esp_err_to_name(err));
return err;
}
setenv("TZ", resolved_tz, 1);
tzset();
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
cJSON_Delete(root);
snprintf(output, output_size, "Timezone set to '%s'. Current time: %s", resolved_tz, time_str);
ESP_LOGI(TAG, "Timezone set to: %s, current time: %s", resolved_tz, time_str);
return ESP_OK;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "esp_err.h"
#include <stddef.h>
/**
* Execute set_timezone tool.
* Sets the system timezone via NVS and updates the TZ environment variable.
* Input JSON: {"timezone": "CST-8"} or {"timezone": "Asia/Shanghai"}
*/
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size);

View File

@@ -2,6 +2,7 @@
#include "mimi_config.h"
#include <string.h>
#include <stdbool.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_wifi.h"
@@ -12,6 +13,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "freertos/timers.h"
static const char *TAG = "wifi";
@@ -20,6 +22,15 @@ 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 TimerHandle_t s_retry_timer = NULL;
static void retry_timer_callback(TimerHandle_t xTimer)
{
(void)xTimer;
if (s_reconnect_enabled && !s_connected) {
esp_wifi_connect();
}
}
static const char *wifi_reason_to_str(wifi_err_reason_t reason)
{
@@ -77,9 +88,10 @@ static void event_handler(void *arg, esp_event_base_t event_base,
}
ESP_LOGW(TAG, "Disconnected, retry %d/%d in %" PRIu32 "ms",
s_retry_count + 1, MIMI_WIFI_MAX_RETRY, delay_ms);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
esp_wifi_connect();
s_retry_count++;
/* Use timer instead of blocking vTaskDelay in event handler */
if (s_retry_timer) xTimerStop(s_retry_timer, 0);
xTimerChangePeriod(s_retry_timer, pdMS_TO_TICKS(delay_ms), 0);
} else {
ESP_LOGE(TAG, "Failed to connect after %d retries", MIMI_WIFI_MAX_RETRY);
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
@@ -98,6 +110,7 @@ static void event_handler(void *arg, esp_event_base_t event_base,
esp_err_t wifi_manager_init(void)
{
s_wifi_event_group = xEventGroupCreate();
s_retry_timer = xTimerCreate("wifi_retry", pdMS_TO_TICKS(1000), pdFALSE, NULL, retry_timer_callback);
ESP_ERROR_CHECK(esp_netif_init());
esp_netif_create_default_wifi_sta();