feat: 实现时间同步、NVS稳定性修复和ESP-IDF v6.0兼容性改进
This commit is contained in:
18
changelog.md
18
changelog.md
@@ -12,8 +12,24 @@
|
||||
- 时区通过 NVS 持久化存储(`system_config` namespace)
|
||||
- 支持城市名映射(Asia/Shanghai → CST-8 等 18 个预设城市)
|
||||
- `config_show` 中显示当前时区配置
|
||||
- **SNTP 自动时间同步**(新增)
|
||||
- WiFi 连接成功后自动从 NTP 服务器同步系统时间
|
||||
- 新增 `time_sync` 模块(`main/time_sync/`)
|
||||
- 新增 `ntp_status` CLI 命令(查看时区、本地时间、同步状态、NTP 服务器、上次同步时间)
|
||||
- 新增 `ntp_sync` CLI 命令(手动触发时间同步)
|
||||
- 新增 `ntp_set <server>` CLI 命令(自定义 NTP 服务器,NVS 持久化)
|
||||
- 默认 NTP 服务器:`ntp.ntsc.ac.cn`(中国科学院国家授时中心)
|
||||
- 同步状态:`synced`(已同步)、`syncing`(同步中)、`not_synced`(未同步)
|
||||
- `set_timezone` 工具在设置时区后自动检测时间有效性,必要时触发 HTTP 时间同步
|
||||
- 记录上次同步时间戳,`ntp_status` 可查看
|
||||
- **NVS 配置安全机制**(新增)
|
||||
- 启动时自动校验关键 NVS 命名空间完整性
|
||||
- 检测并修复损坏的 NVS 条目
|
||||
- 启用 ESP32-S3 Brownout Detection 防止供电不足导致 Flash 写入中断
|
||||
|
||||
### 修复
|
||||
- **LLM Provider 初始化 Bug** — 修复 `llm_provider_init()` 中 provider-specific API key 和 Base URL 无法从 NVS 加载的问题(`llm_provider_get_api_key` 对当前 provider 直接返回内存缓存值,导致 NVS 数据永远不会被读取)
|
||||
- **换 USB 口后配置失效** — 启用 Brownout Detection 防止供电不足时 NVS 写入中断,添加启动时 NVS 完整性校验
|
||||
- ESP-IDF v6.0 编译适配
|
||||
- 修复 flash 大小配置(2MB → 16MB)
|
||||
- 修复 WiFi 断开原因码未定义问题(添加 `#ifdef` 保护)
|
||||
@@ -32,7 +48,7 @@
|
||||
|
||||
### 文档
|
||||
- 新增 `docs/ESP-IDF-V6-MIGRATION.md` — ESP-IDF v6.0 迁移适配记录
|
||||
- 更新 `taolun.md` — 讨论记录整理
|
||||
- 更新 `taolun.md` — 讨论记录整理,新增时间同步和 NVS 配置稳定性问题讨论
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -94,10 +94,9 @@
|
||||
- **MimiClaw**: Not implemented
|
||||
- **Recommendation**: Simple FreeRTOS timer that periodically checks HEARTBEAT.md
|
||||
|
||||
### [ ] Multi-LLM Provider Support
|
||||
### [x] ~~Multi-LLM Provider Support~~
|
||||
- **nanobot**: `providers/litellm_provider.py` — supports OpenRouter, Anthropic, OpenAI, Gemini, DeepSeek, Groq, Zhipu, vLLM via LiteLLM
|
||||
- **MimiClaw**: Hardcoded to Anthropic Messages API
|
||||
- **Recommendation**: Abstract LLM interface, support OpenAI-compatible API (most providers are compatible)
|
||||
- **MimiClaw**: Supports Anthropic, OpenAI, SiliconFlow (硅基流动), Volcengine (火山方舟) — abstracted via `llm_provider.c`
|
||||
|
||||
### [ ] Voice Transcription
|
||||
- **nanobot**: `providers/transcription.py` — Groq Whisper API
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <string.h>
|
||||
#include <stdio.h>
|
||||
@@ -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 <server>' 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 <server>\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, "<server>", "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 = {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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-"
|
||||
|
||||
101
main/nvs_safety/nvs_safety.c
Normal file
101
main/nvs_safety/nvs_safety.c
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "nvs_safety.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
11
main/nvs_safety/nvs_safety.h
Normal file
11
main/nvs_safety/nvs_safety.h
Normal file
@@ -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);
|
||||
153
main/time_sync/time_sync.c
Normal file
153
main/time_sync/time_sync.c
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "time_sync.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#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;
|
||||
}
|
||||
50
main/time_sync/time_sync.h
Normal file
50
main/time_sync/time_sync.h
Normal file
@@ -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);
|
||||
@@ -1,16 +1,131 @@
|
||||
#include "tool_set_timezone.h"
|
||||
#include "mimi_config.h"
|
||||
#include "proxy/http_proxy.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#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];
|
||||
|
||||
@@ -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
|
||||
|
||||
255
taolun.md
255
taolun.md
@@ -2,10 +2,214 @@
|
||||
|
||||
---
|
||||
|
||||
## 讨论:ESP-IDF v6.0 编译适配
|
||||
## 实施记录:时间同步 + NVS 稳定性 + LLM Provider Bug 修复
|
||||
|
||||
**日期**:2026-03-31
|
||||
**目标**:解决 ESP-IDF v6.0 编译失败问题,完成固件烧录
|
||||
**日期**: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 <server>` | 设置 NTP 服务器(存 NVS,重启后生效) |
|
||||
|
||||
### 实施过程中的潜在问题与修复
|
||||
|
||||
#### 问题 1: `tool_set_timezone.c` 缺少 `<strings.h>`
|
||||
|
||||
**发现**:`strcasecmp()` 和 `strcasestr()` 在 POSIX 标准中定义在 `<strings.h>` 而非 `<string.h>` 中。某些编译器会报错。
|
||||
|
||||
**修复**:添加 `#include <strings.h>`。
|
||||
|
||||
#### 问题 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` | 修改 | 添加 `<strings.h>`,设置时区后检测时间有效性 |
|
||||
| `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
|
||||
**目标**:修复两个关键问题 — 时间显示 1970、换 USB 口后配置不生效
|
||||
|
||||
### 问题 1:时间显示 1970-01-01
|
||||
|
||||
**现象**:
|
||||
```
|
||||
Current timezone: Asia/Shanghai [NVS]
|
||||
Local time: 1970-01-01 00:00:23 GMT (Thursday)
|
||||
```
|
||||
|
||||
**根因**:代码中没有初始化 SNTP/NTP 客户端。ESP32 上电后 RTC 时钟从 0 开始计时,`time(NULL)` 返回的就是 1970 年以来的秒数。目前只有 LLM 调用 `get_current_time` 工具时才会通过 HTTP 同步时间。
|
||||
|
||||
**修复方案**:
|
||||
- 新建 `main/time_sync/time_sync.c`,使用 ESP-IDF 内置 SNTP 组件
|
||||
- WiFi 连接成功后自动从 `ntp.ntsc.ac.cn` 同步时间
|
||||
- 同步成功后自动应用已保存的时区配置
|
||||
- `ntp_status` 命令显示完整时间状态(时区、本地时间、同步状态、NTP 服务器、上次同步时间)
|
||||
|
||||
### 问题 2:换 USB 口/电脑后不工作 + 模型配置不加载
|
||||
|
||||
**现象**:
|
||||
- 插入其他 Type-C 口或电脑,设备不正常工作
|
||||
- 需要在 Web 界面重新填写大模型 ID 和密钥,保存重启后才正常
|
||||
- 命令行中模型状态显示异常
|
||||
|
||||
**根因分析**:
|
||||
|
||||
#### 2.1 LLM Provider 初始化 Bug(核心问题)
|
||||
|
||||
在 `llm_provider.c:260-284` 中,`llm_provider_init()` 通过 `llm_provider_get_api_key()` 加载当前 provider 的 API key:
|
||||
|
||||
```c
|
||||
void llm_provider_init(void) {
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
但 `llm_provider_get_api_key()` 的逻辑是(`llm_provider.c:209-214`):
|
||||
|
||||
```c
|
||||
const char *llm_provider_get_api_key(const char *provider_name) {
|
||||
if (strcmp(provider_name, s_current_provider->name) == 0) {
|
||||
return s_api_key; // 直接返回内存中的值,不从 NVS 读!
|
||||
}
|
||||
// 否则才从 NVS 加载...
|
||||
```
|
||||
|
||||
**形成死循环**:`llm_provider_init()` 想从 NVS 加载 key → 调用 `llm_provider_get_api_key` → 发现是当前 provider → 直接返回内存中的 `s_api_key`(初始为空)→ 把空值复制给自己 → **NVS 中的 provider-specific key(如 `siliconflow_api_key`)永远不会被加载到内存**。
|
||||
|
||||
同理,`s_base_url` 也存在相同问题。
|
||||
|
||||
**为什么 Web 界面保存后能正常工作?**
|
||||
|
||||
Web 界面的 `/save` 处理函数(`wifi_onboard.c:377-408`)会**同时保存两份**:
|
||||
```c
|
||||
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY); // 通用 key
|
||||
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, api_key_nvs); // provider-specific key
|
||||
```
|
||||
|
||||
而 `llm_proxy_init()` 能正确加载 `MIMI_NVS_KEY_API_KEY`(通用 key),所以 Web 保存后能用。但如果只通过 CLI 的 `set_siliconflow_key` 设置(只保存到 `siliconflow_api_key`),上电后就不会被加载。
|
||||
|
||||
#### 2.2 Brownout(欠压)导致 NVS 损坏
|
||||
|
||||
ESP32-S3 开启 WiFi 时峰值电流可达 300-500mA,不同 USB 端口的供电能力差异很大。供电不足时:
|
||||
- 可能导致静默重启或 Flash 写入中断
|
||||
- `nvs_commit` 过程中断电会导致 NVS 数据损坏
|
||||
- WiFi 不稳定但日志看起来"正常"
|
||||
|
||||
**修复方案**:
|
||||
1. 修复 `llm_provider_init()` — 直接从 NVS 读取当前 provider 的 key 和 Base URL,绕过 `llm_provider_get_api_key` 的内存缓存
|
||||
2. 新建 `main/nvs_safety/nvs_safety.c` — 启动时校验关键 NVS 命名空间的完整性,检测并修复损坏条目
|
||||
3. 在 `sdkconfig.defaults` 中启用 ESP32-S3 的 Brownout Detection
|
||||
|
||||
### 改动清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `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` 增加 SNTP 同步状态 |
|
||||
| `main/CMakeLists.txt` | 修改 | 添加新源文件和 `esp_netif` 依赖 |
|
||||
| `sdkconfig.defaults` | 修改 | 启用 Brownout Detection |
|
||||
|
||||
### 问题清单
|
||||
|
||||
@@ -83,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 <server>` | 设置 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
|
||||
@@ -118,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 |
|
||||
|
||||
### 支持的时区格式
|
||||
|
||||
Reference in New Issue
Block a user