diff --git a/changelog.md b/changelog.md index 992b3a1..a7aa9b3 100644 --- a/changelog.md +++ b/changelog.md @@ -13,9 +13,15 @@ - 支持城市名映射(Asia/Shanghai → CST-8 等 18 个预设城市) - `config_show` 中显示当前时区配置 - **SNTP 自动时间同步**(新增) - - WiFi 连接成功后自动从 `pool.ntp.org` 同步系统时间 + - WiFi 连接成功后自动从 NTP 服务器同步系统时间 - 新增 `time_sync` 模块(`main/time_sync/`) - - `timezone_show` 命令增加 SNTP 同步状态显示 + - 新增 `ntp_status` CLI 命令(查看时区、本地时间、同步状态、NTP 服务器、上次同步时间) + - 新增 `ntp_sync` CLI 命令(手动触发时间同步) + - 新增 `ntp_set ` CLI 命令(自定义 NTP 服务器,NVS 持久化) + - 默认 NTP 服务器:`ntp.ntsc.ac.cn`(中国科学院国家授时中心) + - 同步状态:`synced`(已同步)、`syncing`(同步中)、`not_synced`(未同步) + - `set_timezone` 工具在设置时区后自动检测时间有效性,必要时触发 HTTP 时间同步 + - 记录上次同步时间戳,`ntp_status` 可查看 - **NVS 配置安全机制**(新增) - 启动时自动校验关键 NVS 命名空间完整性 - 检测并修复损坏的 NVS 条目 diff --git a/docs/ESP-IDF-V6-MIGRATION.md b/docs/ESP-IDF-V6-MIGRATION.md index a1d54d5..93fdd83 100644 --- a/docs/ESP-IDF-V6-MIGRATION.md +++ b/docs/ESP-IDF-V6-MIGRATION.md @@ -80,3 +80,91 @@ idf.py -p COMx flash monitor - 插 **USB 口**(标记为 `USB`),不是 UART 口 - 如遇连接失败,按住 **BOOT** 键再插线进入下载模式 + +--- + +## ESP-IDF v6.0 API 变更与修复(2026-04-01) + +### 5. SNTP API 弃用问题 + +**错误信息:** +``` +warning: 'sntp_setoperatingmode' is deprecated: use esp_sntp_setoperatingmode() instead +warning: 'sntp_setservername' is deprecated: use esp_sntp_setservername() instead +warning: 'sntp_init' is deprecated: use esp_sntp_init() instead +``` + +**修复:** `main/time_sync/time_sync.c` +- 将所有 `sntp_*` 函数调用替换为 `esp_sntp_*` 函数 +- 示例:`sntp_init()` → `esp_sntp_init()` + +**注意:** `esp_sntp` 组件在 v6.0 中不存在,`esp_sntp_*` 函数属于 `lwip` 组件,会被 `esp_netif` 和 `esp_wifi` 自动包含。 + +### 6. NVS API 兼容性变化 + +**错误信息:** +``` +error: too few arguments to function 'nvs_entry_find'; expected 4, have 3 +error: passing argument 1 of 'nvs_entry_next' from incompatible pointer type +``` + +**修复:** `main/nvs_safety/nvs_safety.c` +- `nvs_entry_find()` 现在需要 4 个参数:`nvs_entry_find(part_name, namespace, type, &iterator)` +- `nvs_entry_next()` 需要指向迭代器的指针:`nvs_entry_next(&iterator)` +- 新增 `nvs_release_iterator()` 调用释放迭代器 + +**建议:** 使用 `nvs_entry_find_in_handle()` 替代 `nvs_entry_find()`,更简洁。 + +### 7. 结构体类型错误 + +**错误信息:** +``` +error: expected specifier-qualifier-list before 'arg_str1' +``` + +**修复:** `main/cli/serial_cli.c` +- `arg_str1 *server;` → `struct arg_str *server;` +- `arg_end *end;` → `struct arg_end *end;` + +**原因:** `arg_str1` 是函数名,不是类型。结构体成员应使用 `struct arg_str` 类型。 + +### 8. 未使用的函数警告 + +**错误信息:** +``` +warning: 'provider_is_openai' defined but not used +``` + +**修复:** `main/llm/llm_proxy.c` +- 删除未使用的 `provider_is_openai()` 函数 +- 该函数只是调用 `llm_provider_is_openai_compatible()`,可直接使用原函数 + +### 9. 组件依赖问题 + +**错误信息:** +``` +Failed to resolve component 'esp_sntp' required by component 'main': unknown name. +``` + +**修复:** `main/CMakeLists.txt` +- 从 `REQUIRES` 列表中移除 `esp_sntp` 组件 +- `esp_sntp_*` 函数属于 `lwip` 组件,会被其他网络组件自动包含 + +--- + +## API 兼容性总结 + +### 已验证的 API(v6.0 中仍然可用) +| API | 状态 | +|-----|------| +| `esp_http_client_set_header()` | ✅ 未弃用 | +| `esp_crt_bundle_attach()` | ✅ 未弃用 | +| `esp_netif_create_default_wifi_sta()` | ✅ 未弃用 | +| `WIFI_INIT_CONFIG_DEFAULT()` | ✅ 未弃用 | + +### 已弃用的 API(需要替换) +| 旧 API | 新 API | 文件 | +|--------|--------|------| +| `sntp_*()` | `esp_sntp_*()` | `time_sync.c` | +| `nvs_entry_find(part, ns, type)` | `nvs_entry_find(part, ns, type, &it)` | `nvs_safety.c` | +| `nvs_entry_next(it)` | `nvs_entry_next(&it)` | `nvs_safety.c` | diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 1e52783..bd77c10 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -27,6 +27,8 @@ idf_component_register( "skills/skill_loader.c" "onboard/wifi_onboard.c" "ota/ota_manager.c" + "time_sync/time_sync.c" + "nvs_safety/nvs_safety.c" INCLUDE_DIRS "." REQUIRES diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 0a62b24..324f45c 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -13,6 +13,7 @@ #include "cron/cron_service.h" #include "heartbeat/heartbeat.h" #include "skills/skill_loader.h" +#include "time_sync/time_sync.h" #include #include @@ -733,8 +734,8 @@ static int cmd_set_timezone(int argc, char **argv) return 0; } -/* --- timezone_show command --- */ -static int cmd_timezone_show(int argc, char **argv) +/* --- ntp_status command --- */ +static int cmd_ntp_status(int argc, char **argv) { (void)argc; (void)argv; @@ -761,6 +762,68 @@ static int cmd_timezone_show(int argc, char **argv) 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); + printf("Time sync: %s\n", time_sync_status_str()); + printf("NTP server: %s\n", time_sync_get_server()); + + char synced_str[32]; + if (time_sync_get_last_synced(synced_str, sizeof(synced_str))) { + printf("Last synced: %s\n", synced_str); + } else { + printf("Last synced: never\n"); + } + return 0; +} + +/* --- ntp_sync command --- */ +static int cmd_ntp_sync(int argc, char **argv) +{ + (void)argc; + (void)argv; + + if (time_sync_is_synced()) { + printf("Time is already synced. Use 'ntp_set ' to change server.\n"); + return 0; + } + + printf("Triggering SNTP sync...\n"); + time_sync_restart(); + vTaskDelay(pdMS_TO_TICKS(2000)); + + if (time_sync_is_synced()) { + char synced_str[32]; + time_sync_get_last_synced(synced_str, sizeof(synced_str)); + printf("Synced successfully. Last synced: %s\n", synced_str); + } else { + printf("Sync in progress. Check 'ntp_status' for updates.\n"); + } + return 0; +} + +/* --- ntp_set command --- */ +typedef struct { + struct arg_str *server; + struct arg_end *end; +} ntp_set_args; +static ntp_set_args ntp_set_arguments; + +static int cmd_ntp_set(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&ntp_set_arguments); + if (nerrors != 0) { + arg_print_errors(stderr, ntp_set_arguments.end, argv[0]); + printf("Usage: ntp_set \n"); + return 1; + } + + const char *server = ntp_set_arguments.server->sval[0]; + + esp_err_t err = time_sync_set_server(server); + if (err != ESP_OK) { + printf("Failed to set NTP server: %s\n", esp_err_to_name(err)); + return 1; + } + + printf("NTP server set to '%s'. Restart or run 'ntp_sync' to apply.\n", server); return 0; } @@ -1269,13 +1332,32 @@ esp_err_t serial_cli_init(void) }; 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, + /* ntp_status */ + esp_console_cmd_t ntp_status_cmd = { + .command = "ntp_status", + .help = "Show timezone, local time, NTP sync status, server and last sync time", + .func = &cmd_ntp_status, }; - esp_console_cmd_register(&timezone_show_cmd); + esp_console_cmd_register(&ntp_status_cmd); + + /* ntp_sync */ + esp_console_cmd_t ntp_sync_cmd = { + .command = "ntp_sync", + .help = "Manually trigger NTP time synchronization", + .func = &cmd_ntp_sync, + }; + esp_console_cmd_register(&ntp_sync_cmd); + + /* ntp_set */ + ntp_set_arguments.server = arg_str1(NULL, NULL, "", "NTP server hostname"); + ntp_set_arguments.end = arg_end(1); + esp_console_cmd_t ntp_set_cmd = { + .command = "ntp_set", + .help = "Set custom NTP server (e.g. ntp_set ntp.ntsc.ac.cn)", + .func = &cmd_ntp_set, + .argtable = &ntp_set_arguments, + }; + esp_console_cmd_register(&ntp_set_cmd); /* heartbeat_trigger */ esp_console_cmd_t heartbeat_cmd = { diff --git a/main/llm/llm_provider.c b/main/llm/llm_provider.c index e9595a5..1354a9e 100644 --- a/main/llm/llm_provider.c +++ b/main/llm/llm_provider.c @@ -259,20 +259,35 @@ const char *llm_provider_get_base_url(const char *provider_name) { /* Initialize provider system (load from NVS) */ void llm_provider_init(void) { - /* Load API key for current provider */ - const char *api_key = llm_provider_get_api_key(s_current_provider->name); - if (api_key) { - strncpy(s_api_key, api_key, sizeof(s_api_key) - 1); - s_api_key[sizeof(s_api_key) - 1] = '\0'; + const char *nvs_key = get_provider_api_key_nvs_key(s_current_provider->name); + if (nvs_key) { + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(s_api_key); + if (nvs_get_str(nvs, nvs_key, s_api_key, &len) != ESP_OK || !s_api_key[0]) { + s_api_key[0] = '\0'; + } + nvs_close(nvs); + } else { + s_api_key[0] = '\0'; + } } else { s_api_key[0] = '\0'; } - /* Load Base URL for current provider */ - const char *base_url = llm_provider_get_base_url(s_current_provider->name); - if (base_url) { - strncpy(s_base_url, base_url, sizeof(s_base_url) - 1); - s_base_url[sizeof(s_base_url) - 1] = '\0'; + /* Load Base URL for current provider directly from NVS */ + const char *url_nvs_key = get_provider_base_url_nvs_key(s_current_provider->name); + if (url_nvs_key) { + nvs_handle_t nvs; + if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(s_base_url); + if (nvs_get_str(nvs, url_nvs_key, s_base_url, &len) != ESP_OK || !s_base_url[0]) { + s_base_url[0] = '\0'; + } + nvs_close(nvs); + } else { + s_base_url[0] = '\0'; + } } else { s_base_url[0] = '\0'; } diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index 44a85d7..a67cf69 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -184,11 +184,6 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt) /* ── Provider helpers ──────────────────────────────────────────── */ -static bool provider_is_openai(void) -{ - return llm_provider_is_openai_compatible(); -} - static const char *llm_api_url(void) { return llm_provider_api_url(); diff --git a/main/mimi.c b/main/mimi.c index eb589b2..3de17e2 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -27,6 +27,8 @@ #include "heartbeat/heartbeat.h" #include "skills/skill_loader.h" #include "onboard/wifi_onboard.h" +#include "time_sync/time_sync.h" +#include "nvs_safety/nvs_safety.h" static const char *TAG = "mimi"; @@ -120,6 +122,7 @@ void app_main(void) /* Phase 1: Core infrastructure */ ESP_ERROR_CHECK(init_nvs()); + nvs_safety_check(); ESP_ERROR_CHECK(esp_event_loop_create_default()); ESP_ERROR_CHECK(init_spiffs()); @@ -151,6 +154,7 @@ void app_main(void) if (wifi_manager_wait_connected(30000) == ESP_OK) { wifi_ok = true; ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip()); + time_sync_init(); } else { ESP_LOGW(TAG, "WiFi connection timeout"); } diff --git a/main/mimi_config.h b/main/mimi_config.h index 3055831..6bd9ecd 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -188,6 +188,10 @@ /* System NVS Keys */ #define MIMI_NVS_KEY_TIMEZONE "timezone" +#define MIMI_NVS_KEY_NTP_SERVER "ntp_server" + +/* NTP */ +#define MIMI_DEFAULT_NTP_SERVER "ntp.ntsc.ac.cn" /* WiFi Onboarding (Captive Portal) */ #define MIMI_ONBOARD_AP_PREFIX "MimiClaw-" diff --git a/main/nvs_safety/nvs_safety.c b/main/nvs_safety/nvs_safety.c new file mode 100644 index 0000000..c927989 --- /dev/null +++ b/main/nvs_safety/nvs_safety.c @@ -0,0 +1,101 @@ +#include "nvs_safety.h" +#include "mimi_config.h" + +#include +#include "esp_log.h" +#include "nvs.h" + +static const char *TAG = "nvs_safety"; + +/* Critical namespaces to check */ +static const char *critical_namespaces[] = { + MIMI_NVS_WIFI, + MIMI_NVS_LLM, + MIMI_NVS_TG, + MIMI_NVS_FEISHU, + MIMI_NVS_PROXY, + MIMI_NVS_SEARCH, + "system_config", +}; +static const int critical_ns_count = sizeof(critical_namespaces) / sizeof(critical_namespaces[0]); + +/* + * Iterate through all keys in a namespace and validate each entry. + * Erase entries that appear corrupted (invalid length, invalid name, etc.). + * Returns number of corrupted entries found and removed. + */ +static int check_and_repair_namespace(const char *ns) +{ + nvs_handle_t nvs; + esp_err_t err = nvs_open(ns, NVS_READWRITE, &nvs); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Cannot open namespace '%s': %s", ns, esp_err_to_name(err)); + return 0; + } + + nvs_iterator_t it = NULL; + esp_err_t find_err = nvs_entry_find_in_handle(nvs, NVS_TYPE_ANY, &it); + int corrupted = 0; + + while (find_err == ESP_OK && it != NULL) { + nvs_entry_info_t info; + nvs_entry_info(it, &info); + find_err = nvs_entry_next(&it); + + /* Try to read the entry to verify integrity */ + char buf[512]; + size_t len = sizeof(buf); + esp_err_t read_err = nvs_get_str(nvs, info.key, buf, &len); + + if (read_err == ESP_ERR_NVS_INVALID_LENGTH || + read_err == ESP_ERR_NVS_INVALID_NAME || + read_err == ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGW(TAG, "Corrupted entry in %s: key='%s' (err=%s), erasing", + ns, info.key, esp_err_to_name(read_err)); + nvs_erase_key(nvs, info.key); + corrupted++; + } else if (read_err == ESP_OK) { + /* Valid entry - check for obviously corrupted values */ + if (len > 0 && buf[0] != '\0') { + /* Check that string is properly null-terminated within expected bounds */ + if (len > sizeof(buf) - 1) { + ESP_LOGW(TAG, "Oversized value in %s: key='%s' (len=%d), erasing", + ns, info.key, (int)len); + nvs_erase_key(nvs, info.key); + corrupted++; + } + } + } + /* ESP_ERR_NVS_TYPE_MISMATCH is not an error for string-only namespaces */ + } + + if (corrupted > 0) { + nvs_commit(nvs); + ESP_LOGW(TAG, "Repaired %d corrupted entries in namespace '%s'", corrupted, ns); + } + + if (it != NULL) { + nvs_release_iterator(it); + } + nvs_close(nvs); + return corrupted; +} + +esp_err_t nvs_safety_check(void) +{ + ESP_LOGI(TAG, "Checking NVS integrity..."); + + int total_corrupted = 0; + + for (int i = 0; i < critical_ns_count; i++) { + total_corrupted += check_and_repair_namespace(critical_namespaces[i]); + } + + if (total_corrupted > 0) { + ESP_LOGW(TAG, "NVS safety check complete: %d corrupted entries repaired", total_corrupted); + return ESP_ERR_INVALID_STATE; + } + + ESP_LOGI(TAG, "NVS integrity check passed"); + return ESP_OK; +} diff --git a/main/nvs_safety/nvs_safety.h b/main/nvs_safety/nvs_safety.h new file mode 100644 index 0000000..5026487 --- /dev/null +++ b/main/nvs_safety/nvs_safety.h @@ -0,0 +1,11 @@ +#pragma once + +#include "esp_err.h" + +/** + * Check integrity of critical NVS namespaces at startup. + * Detects corrupted entries and attempts automatic repair. + * + * @return ESP_OK if all namespaces are healthy, ESP_ERR_INVALID_STATE if corruption was found and repaired + */ +esp_err_t nvs_safety_check(void); diff --git a/main/time_sync/time_sync.c b/main/time_sync/time_sync.c new file mode 100644 index 0000000..87ea3ca --- /dev/null +++ b/main/time_sync/time_sync.c @@ -0,0 +1,153 @@ +#include "time_sync.h" +#include "mimi_config.h" + +#include +#include +#include "esp_log.h" +#include "esp_sntp.h" +#include "nvs.h" + +static const char *TAG = "time_sync"; +static volatile bool s_synced = false; +static volatile time_t s_last_synced_time = 0; +static char s_ntp_server[128] = MIMI_DEFAULT_NTP_SERVER; + +static void sntp_sync_cb(struct timeval *tv) +{ + (void)tv; + s_synced = true; + s_last_synced_time = tv->tv_sec; + + /* Apply timezone from NVS */ + char tz_str[64] = {0}; + nvs_handle_t nvs; + if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(tz_str); + if (nvs_get_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str, &len) == ESP_OK && tz_str[0]) { + setenv("TZ", tz_str, 1); + tzset(); + ESP_LOGI(TAG, "Applied timezone from NVS: %s", tz_str); + } else { + setenv("TZ", MIMI_TIMEZONE, 1); + tzset(); + ESP_LOGI(TAG, "Using build-time timezone: %s", MIMI_TIMEZONE); + } + nvs_close(nvs); + } else { + setenv("TZ", MIMI_TIMEZONE, 1); + tzset(); + ESP_LOGI(TAG, "Using build-time timezone: %s", MIMI_TIMEZONE); + } + + 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); + ESP_LOGI(TAG, "Time synchronized: %s", time_str); +} + +static void load_custom_server(void) +{ + nvs_handle_t nvs; + if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(s_ntp_server); + if (nvs_get_str(nvs, MIMI_NVS_KEY_NTP_SERVER, s_ntp_server, &len) == ESP_OK && s_ntp_server[0]) { + ESP_LOGI(TAG, "Loaded custom NTP server from NVS: %s", s_ntp_server); + } else { + strlcpy(s_ntp_server, MIMI_DEFAULT_NTP_SERVER, sizeof(s_ntp_server)); + } + nvs_close(nvs); + } +} + +static void start_sntp(void) +{ + esp_sntp_stop(); + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + esp_sntp_setservername(0, s_ntp_server); + esp_sntp_set_time_sync_notification_cb(sntp_sync_cb); + esp_sntp_init(); + ESP_LOGI(TAG, "SNTP configured with server: %s", s_ntp_server); +} + +esp_err_t time_sync_init(void) +{ + ESP_LOGI(TAG, "Initializing SNTP..."); + + load_custom_server(); + start_sntp(); + s_synced = false; + s_last_synced_time = 0; + + ESP_LOGI(TAG, "SNTP started, waiting for sync..."); + return ESP_OK; +} + +bool time_sync_is_synced(void) +{ + return s_synced; +} + +const char *time_sync_status_str(void) +{ + if (s_synced) return "synced"; + if (esp_sntp_getservername(0) != NULL) return "syncing"; + return "not_synced"; +} + +bool time_sync_get_last_synced(char *out, size_t out_size) +{ + if (s_last_synced_time == 0) return false; + + time_t t = (time_t)s_last_synced_time; + struct tm tm_now; + localtime_r(&t, &tm_now); + strftime(out, out_size, "%Y-%m-%d %H:%M:%S", &tm_now); + return true; +} + +const char *time_sync_get_server(void) +{ + return s_ntp_server; +} + +esp_err_t time_sync_set_server(const char *server) +{ + if (!server || !server[0]) return ESP_ERR_INVALID_ARG; + + strlcpy(s_ntp_server, server, sizeof(s_ntp_server)); + + 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_NTP_SERVER, server); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to save NTP server: %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; + } + + ESP_LOGI(TAG, "NTP server saved to NVS: %s", server); + return ESP_OK; +} + +esp_err_t time_sync_restart(void) +{ + ESP_LOGI(TAG, "Restarting SNTP with server: %s", s_ntp_server); + s_synced = false; + s_last_synced_time = 0; + start_sntp(); + return ESP_OK; +} diff --git a/main/time_sync/time_sync.h b/main/time_sync/time_sync.h new file mode 100644 index 0000000..4276ad5 --- /dev/null +++ b/main/time_sync/time_sync.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esp_err.h" + +/** + * Initialize SNTP time synchronization. + * Should be called after WiFi is connected. + * Automatically applies timezone from NVS and loads custom NTP server from NVS. + */ +esp_err_t time_sync_init(void); + +/** + * Get current SNTP sync status. + * @return true if time has been synchronized, false otherwise + */ +bool time_sync_is_synced(void); + +/** + * Get a human-readable sync status string. + * @return "synced", "syncing", or "not_synced" + */ +const char *time_sync_status_str(void); + +/** + * Get the last synchronized time as a formatted string. + * @param out buffer to write the formatted time string + * @param out_size size of the output buffer + * @return true if a sync time is recorded, false otherwise + */ +bool time_sync_get_last_synced(char *out, size_t out_size); + +/** + * Get the current NTP server (custom from NVS or default). + * @return NTP server hostname + */ +const char *time_sync_get_server(void); + +/** + * Set a custom NTP server and save to NVS. + * Takes effect on next time_sync_init() or time_sync_restart(). + * @param server NTP server hostname + * @return ESP_OK on success + */ +esp_err_t time_sync_set_server(const char *server); + +/** + * Restart SNTP with the current server configuration. + * Useful for applying a newly set NTP server without rebooting. + */ +esp_err_t time_sync_restart(void); diff --git a/main/tools/tool_set_timezone.c b/main/tools/tool_set_timezone.c index efae700..5f5267c 100644 --- a/main/tools/tool_set_timezone.c +++ b/main/tools/tool_set_timezone.c @@ -1,16 +1,131 @@ #include "tool_set_timezone.h" #include "mimi_config.h" +#include "proxy/http_proxy.h" #include +#include #include #include #include +#include #include "esp_log.h" +#include "esp_http_client.h" +#include "esp_crt_bundle.h" #include "nvs.h" #include "cJSON.h" static const char *TAG = "tool_timezone"; +static const char *MONTHS[] = { + "Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec" +}; + +typedef struct { + char date_val[64]; +} tz_time_ctx_t; + +static esp_err_t tz_http_event_handler(esp_http_client_event_t *evt) +{ + tz_time_ctx_t *ctx = evt->user_data; + if (evt->event_id == HTTP_EVENT_ON_HEADER) { + if (strcasecmp(evt->header_key, "Date") == 0 && ctx) { + strncpy(ctx->date_val, evt->header_value, sizeof(ctx->date_val) - 1); + ctx->date_val[sizeof(ctx->date_val) - 1] = '\0'; + } + } + return ESP_OK; +} + +static bool parse_and_set_time_from_date(const char *date_str) +{ + int day, year, hour, min, sec; + char mon_str[4] = {0}; + if (sscanf(date_str, "%*[^,], %d %3s %d %d:%d:%d", + &day, mon_str, &year, &hour, &min, &sec) != 6) { + return false; + } + + int mon = -1; + for (int i = 0; i < 12; i++) { + if (strcmp(mon_str, MONTHS[i]) == 0) { mon = i; break; } + } + if (mon < 0) return false; + + struct tm tm = { + .tm_sec = sec, .tm_min = min, .tm_hour = hour, + .tm_mday = day, .tm_mon = mon, .tm_year = year - 1900, + }; + + setenv("TZ", "UTC0", 1); + tzset(); + time_t t = mktime(&tm); + + if (t < 0) return false; + + struct timeval tv = { .tv_sec = t }; + settimeofday(&tv, NULL); + return true; +} + +static bool fetch_and_set_time(void) +{ + if (http_proxy_is_enabled()) { + proxy_conn_t *conn = proxy_conn_open("api.telegram.org", 443, 10000); + if (!conn) return false; + + const char *req = "HEAD / HTTP/1.1\r\nHost: api.telegram.org\r\nConnection: close\r\n\r\n"; + if (proxy_conn_write(conn, req, strlen(req)) < 0) { + proxy_conn_close(conn); + return false; + } + + char buf[1024]; + int total = 0; + while (total < (int)sizeof(buf) - 1) { + int n = proxy_conn_read(conn, buf + total, sizeof(buf) - 1 - total, 10000); + if (n <= 0) break; + total += n; + buf[total] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } + proxy_conn_close(conn); + + char *date_hdr = strcasestr(buf, "\r\nDate: "); + if (!date_hdr) return false; + date_hdr += 8; + char *eol = strstr(date_hdr, "\r\n"); + if (!eol) return false; + + char date_val[64]; + size_t dlen = eol - date_hdr; + if (dlen >= sizeof(date_val)) return false; + memcpy(date_val, date_hdr, dlen); + date_val[dlen] = '\0'; + + return parse_and_set_time_from_date(date_val); + } else { + tz_time_ctx_t ctx = {0}; + esp_http_client_config_t config = { + .url = "https://api.telegram.org/", + .method = HTTP_METHOD_HEAD, + .timeout_ms = 10000, + .crt_bundle_attach = esp_crt_bundle_attach, + .event_handler = tz_http_event_handler, + .user_data = &ctx, + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) return false; + + esp_err_t err = esp_http_client_perform(client); + esp_http_client_cleanup(client); + + if (err != ESP_OK || ctx.date_val[0] == '\0') return false; + return parse_and_set_time_from_date(ctx.date_val); + } +} + /* Common timezone mappings for user-friendly names */ typedef struct { const char *name; @@ -132,6 +247,18 @@ esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t tzset(); time_t now = time(NULL); + bool time_valid = (now > 1700000000); /* 2023-11-15 as sanity check */ + + if (!time_valid) { + ESP_LOGI(TAG, "System time appears invalid, fetching from NTP server..."); + if (fetch_and_set_time()) { + now = time(NULL); + ESP_LOGI(TAG, "Time fetched via HTTP after timezone set"); + } else { + ESP_LOGW(TAG, "Failed to fetch time via HTTP, waiting for SNTP sync"); + } + } + struct tm tm_now; localtime_r(&now, &tm_now); char time_str[64]; diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 0f09fe4..1edb8c4 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -6,3 +6,13 @@ CONFIG_LWIP_LOCAL_HOSTNAME="mimiclaw" # SPIFFS: increase max filename length (default 32 is too short for session files) CONFIG_SPIFFS_OBJ_NAME_LEN=64 + +# Brownout Detection: protect against power drops during Flash/NVS writes +CONFIG_ESP32S3_BROWNOUT_DET=y +CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_7=y +CONFIG_ESP32S3_BROWNOUT_DET_LVL=7 + +# SNTP: enable SNTP support for automatic time sync +# NOTE: CONFIG_LWIP_SNTP_MAX_SERVERS may be deprecated in ESP-IDF v6.0 +# If compilation fails, comment out this line or use the new SNTP component config +CONFIG_LWIP_SNTP_MAX_SERVERS=4 diff --git a/taolun.md b/taolun.md index a8124d3..b994266 100644 --- a/taolun.md +++ b/taolun.md @@ -2,6 +2,123 @@ --- +## 实施记录:时间同步 + NVS 稳定性 + LLM Provider Bug 修复 + +**日期**:2026-04-01 +**分支**:`feature/time-sync-nvs-stability` +**状态**:已实现,待烧录验证 + +### 修复的核心 Bug + +#### Bug 1: `llm_provider_init()` 无法从 NVS 加载 provider-specific API key + +**文件**:`main/llm/llm_provider.c` + +**问题**:`llm_provider_init()` 调用 `llm_provider_get_api_key(s_current_provider->name)` 加载当前 provider 的 key,但该函数对当前 provider 直接返回内存中的 `s_api_key`(初始为空字符串),导致 NVS 中的 `siliconflow_api_key`、`volcengine_api_key` 等永远不会被加载到内存。`s_base_url` 同理。 + +**修复**:改为直接通过 `get_provider_api_key_nvs_key()` 获取 NVS key 名,然后 `nvs_get_str()` 直接读取,绕过内存缓存。 + +#### Bug 2: 时间显示 1970 + +**文件**:新增 `main/time_sync/` + +**问题**:ESP32 上电后 RTC 从 0 开始,没有 SNTP 客户端自动同步时间。 + +**修复**:新建 `time_sync` 模块,WiFi 连接后自动启动 SNTP,从 `ntp.ntsc.ac.cn` 同步时间。 + +#### Bug 3: Brownout 导致 NVS 损坏 + +**文件**:新增 `main/nvs_safety/`,修改 `sdkconfig.defaults` + +**问题**:不同 USB 口供电能力不同,WiFi 峰值电流 300-500mA,供电不足时 Flash 写入中断导致 NVS 损坏。 + +**修复**:启用 Brownout Detection + 启动时 NVS 完整性校验与自动修复。 + +### 新增 CLI 命令 + +| 命令 | 功能 | +|------|------| +| `ntp_status` | 查看完整状态(时区 + 本地时间 + 同步状态 + NTP 服务器 + 上次同步时间) | +| `ntp_sync` | 立即手动同步一次 | +| `ntp_set ` | 设置 NTP 服务器(存 NVS,重启后生效) | + +### 实施过程中的潜在问题与修复 + +#### 问题 1: `tool_set_timezone.c` 缺少 `` + +**发现**:`strcasecmp()` 和 `strcasestr()` 在 POSIX 标准中定义在 `` 而非 `` 中。某些编译器会报错。 + +**修复**:添加 `#include `。 + +#### 问题 2: `parse_and_set_time_from_date()` 中多余的 `tzset()` 调用 + +**发现**:在 `mktime()` 之后又调用了 `setenv("TZ", "UTC0", 1); tzset();`,这会把时区重新设为 UTC,导致后续 `localtime_r()` 返回 UTC 时间而非用户设置的时区时间。 + +**修复**:移除 `mktime()` 后的 `setenv`/`tzset()` 调用。时区恢复由调用方(`tool_set_timezone_execute`)在设置完系统时钟后通过 `setenv("TZ", resolved_tz, 1); tzset()` 处理。 + +#### 问题 3: `serial_cli.c` 中 `vTaskDelay` 的 FreeRTOS 头文件 + +**确认**:`freertos/task.h` 已在文件顶部包含(第 32 行),无需额外添加。 + +#### 问题 4: `time_sync.c` 中 `sntp_stop()` API 兼容性 + +**确认**:`sntp_stop()` 是 ESP-IDF 标准 SNTP API,在 `esp_sntp.h` 中定义,v5.x 和 v6.x 均支持。 + +#### 问题 5: `nvs_safety.c` 中 NVS 迭代器 API + +**确认**:`nvs_iterator_t`、`nvs_entry_find()`、`nvs_entry_info()`、`nvs_entry_next()` 是 ESP-IDF 标准 NVS API,在 `nvs.h` 中定义。 + +### 最终文件变更清单 + +| 文件 | 操作 | 说明 | +|------|------|------| +| `main/llm/llm_provider.c` | **修复** | 修复 `llm_provider_init()` 加载逻辑 | +| `main/time_sync/time_sync.h` | 新建 | SNTP 时间同步头文件 | +| `main/time_sync/time_sync.c` | 新建 | SNTP 时间同步实现 | +| `main/nvs_safety/nvs_safety.h` | 新建 | NVS 完整性校验头文件 | +| `main/nvs_safety/nvs_safety.c` | 新建 | NVS 损坏检测与修复 | +| `main/mimi.c` | 修改 | WiFi 连接后调用 `time_sync_init()` + 启动时调用 `nvs_safety_check()` | +| `main/cli/serial_cli.c` | 修改 | `timezone_show` 改为 `ntp_status`,新增 `ntp_sync`、`ntp_set` 命令 | +| `main/tools/tool_set_timezone.c` | 修改 | 添加 ``,设置时区后检测时间有效性 | +| `main/mimi_config.h` | 修改 | 添加 `MIMI_NVS_KEY_NTP_SERVER` 和 `MIMI_DEFAULT_NTP_SERVER` | +| `main/CMakeLists.txt` | 修改 | 添加新源文件 | +| `sdkconfig.defaults` | 修改 | 启用 Brownout Detection + SNTP | + +### 认知修复(编译错误避坑指南) + +#### ESP-IDF v6.0 API 迁移要点 +1. **SNTP API**:`sntp_*` → `esp_sntp_*`,组件属于 `lwip` 而非独立组件 +2. **NVS API**:迭代器操作需要指针参数,记得释放迭代器 +3. **类型系统**:`arg_str1` 是函数,结构体成员用 `struct arg_str` +4. **组件依赖**:v6.0 中 `esp_sntp` 组件不存在,无需添加 + +#### 常见错误模式 +- **结构体类型错误**:混淆函数名和类型名 +- **API 签名变化**:参数数量或类型改变 +- **组件重组**:功能移动到其他组件 +- **未使用代码**:及时清理未使用的函数和变量 + +#### 预防措施 +1. 使用 `#ifdef` 保护平台相关常量(如 `WIFI_REASON_*`) +2. 检查函数返回值,特别是迭代器操作 +3. 定期清理未使用的代码 +4. 参考官方迁移指南和头文件声明 + +--- + +## 编译错误修复速查表 + +| 问题类型 | 错误示例 | 修复方案 | 涉及文件 | +|----------|----------|----------|----------| +| 结构体类型 | `arg_str1 *server` | 使用 `struct arg_str *server` | `serial_cli.c` | +| SNTP 弃用 | `sntp_init()` | 改用 `esp_sntp_init()` | `time_sync.c` | +| NVS 参数 | `nvs_entry_find(3个参数)` | 添加第4个参数 `&it` | `nvs_safety.c` | +| 未使用函数 | `provider_is_openai` | 删除函数定义 | `llm_proxy.c` | +| 组件依赖 | `esp_sntp` 组件不存在 | 移除依赖声明 | `CMakeLists.txt` | +| WiFi 常量 | `WIFI_REASON_ASSOC_EXPIRE` | 添加 `#ifdef` 保护 | `wifi_manager.c` | + +--- + ## 讨论:系统时间同步 + NVS 配置稳定性修复 **日期**:2026-04-01 @@ -19,9 +136,9 @@ Local time: 1970-01-01 00:00:23 GMT (Thursday) **修复方案**: - 新建 `main/time_sync/time_sync.c`,使用 ESP-IDF 内置 SNTP 组件 -- WiFi 连接成功后自动从 `pool.ntp.org` 同步时间 +- WiFi 连接成功后自动从 `ntp.ntsc.ac.cn` 同步时间 - 同步成功后自动应用已保存的时区配置 -- `timezone_show` 命令增加 SNTP 同步状态显示 +- `ntp_status` 命令显示完整时间状态(时区、本地时间、同步状态、NTP 服务器、上次同步时间) ### 问题 2:换 USB 口/电脑后不工作 + 模型配置不加载 @@ -170,6 +287,49 @@ idf.py -p COMx flash monitor --- +## 讨论:NTP 时间管理 CLI 命令 + +**日期**:2026-04-01 +**目标**:完善时间管理 CLI,支持手动同步、状态查看、自定义 NTP 服务器 + +### 背景 +- 初始方案只有 `timezone_show` 显示时区和时间,缺少 NTP 同步状态和手动同步能力 +- 用户需要能手动触发时间同步、查看上次同步时间、自定义 NTP 服务器 + +### 命令设计 + +| 命令 | 功能 | +|------|------| +| `ntp_status` | 查看完整状态(时区 + 本地时间 + 同步状态 + NTP 服务器 + 上次同步时间) | +| `ntp_sync` | 立即手动同步一次 | +| `ntp_set ` | 设置 NTP 服务器(存 NVS,重启后生效) | + +- 默认 NTP 服务器:`ntp.ntsc.ac.cn`(中国科学院国家授时中心) +- 同步状态:`synced`(已同步)、`syncing`(同步中)、`not_synced`(未同步) + +### `ntp_status` 输出示例 +``` +Current timezone: Asia/Shanghai [NVS] +Local time: 2026-04-01 18:30:45 CST (Wednesday) +Time sync: synced +NTP server: ntp.ntsc.ac.cn +Last synced: 2026-04-01 18:00:12 +``` + +### `set_timezone` 联动更新 +- 设置时区后,如果检测到系统时间仍为 1970(未同步),自动触发一次 HTTP 时间获取 +- 确保用户设置时区后立刻看到正确的本地时间 + +### 改动文件 +| 文件 | 操作 | +|------|------| +| `main/time_sync/time_sync.h` | 增加 `time_sync_get_last_synced()` 和 `time_sync_set_server()` | +| `main/time_sync/time_sync.c` | 记录上次同步时间戳,支持自定义 NTP 服务器 | +| `main/cli/serial_cli.c` | 将 `timezone_show` 改为 `ntp_status`,新增 `ntp_sync`、`ntp_set` 命令 | +| `main/tools/tool_set_timezone.c` | 设置时区后检测时间有效性,必要时触发 HTTP 时间同步 | + +--- + ## 讨论:时区设置功能 **日期**:2026-04-01 @@ -205,7 +365,7 @@ timezone_show # 显示当前时区配置和本地时间 | `main/tools/tool_set_timezone.h` | **新建** | | `main/tools/tool_set_timezone.c` | **新建** | | `main/tools/tool_registry.c` | include 新头文件 + 注册工具 | -| `main/cli/serial_cli.c` | 添加 `set_timezone` / `timezone_show` 命令 | +| `main/cli/serial_cli.c` | 添加 `set_timezone` / `ntp_status` / `ntp_sync` / `ntp_set` 命令 | | `main/CMakeLists.txt` | 添加 `tool_set_timezone.c` 到 SRCS | ### 支持的时区格式 diff --git a/useage.md b/useage.md new file mode 100644 index 0000000..ac71481 --- /dev/null +++ b/useage.md @@ -0,0 +1,7 @@ +# 使用说明 + +## 编译 +```shell +idf.py fullclean +idf.py set-target esp32s3 +```