- 创建 main/Kconfig.projbuild 声明所有自定义模块配置项 - 修复 fullclean 后 sdkconfig 丢失 CONFIG_MIMI_* 配置的问题 - 更新 AGENTS.md 添加认知修正栏目和模块开关文档 - 记录 Kconfig 踩坑讨论到 taolun.md
21 KiB
讨论记录
实施记录:时间同步 + 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 <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 迁移要点
- SNTP API:
sntp_*→esp_sntp_*,组件属于lwip而非独立组件 - NVS API:迭代器操作需要指针参数,记得释放迭代器
- 类型系统:
arg_str1是函数,结构体成员用struct arg_str - 组件依赖:v6.0 中
esp_sntp组件不存在,无需添加
常见错误模式
- 结构体类型错误:混淆函数名和类型名
- API 签名变化:参数数量或类型改变
- 组件重组:功能移动到其他组件
- 未使用代码:及时清理未使用的函数和变量
预防措施
- 使用
#ifdef保护平台相关常量(如WIFI_REASON_*) - 检查函数返回值,特别是迭代器操作
- 定期清理未使用的代码
- 参考官方迁移指南和头文件声明
编译错误修复速查表
| 问题类型 | 错误示例 | 修复方案 | 涉及文件 |
|---|---|---|---|
| 结构体类型 | 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:
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):
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)会同时保存两份:
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 不稳定但日志看起来"正常"
修复方案:
- 修复
llm_provider_init()— 直接从 NVS 读取当前 provider 的 key 和 Base URL,绕过llm_provider_get_api_key的内存缓存 - 新建
main/nvs_safety/nvs_safety.c— 启动时校验关键 NVS 命名空间的完整性,检测并修复损坏条目 - 在
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 |
问题清单
1. Flash 大小配置错误
- 错误:分区表需要 16MB,但 sdkconfig 配置为 2MB
- 修复:
sdkconfig中CONFIG_ESPTOOLPY_FLASHSIZE改为 16MB
2. WiFi 断开原因码未定义
- 错误:
WIFI_REASON_ASSOC_EXPIRE等符号在 v6.0 中未定义 - 修复:
wifi_manager.c中所有 reason code 添加#ifdef保护
3. CMakeLists.txt 缺少源文件
- 错误:
llm_provider.c未加入编译列表,导致链接错误 - 修复:添加
ota/ota_manager.c到 SRCS
4. 头文件缺失(共 16 处)
| 文件 | 缺失头文件 | 原因 |
|---|---|---|
cli/serial_cli.c |
llm/llm_provider.h |
llm_provider_set_api_key |
llm/llm_provider.c |
esp_http_client.h |
esp_http_client_set_header |
bus/message_bus.c |
freertos/FreeRTOS.h, freertos/queue.h |
xQueueCreate, QueueHandle_t |
wifi/wifi_manager.c |
esp_event.h |
esp_event_handler_instance_register |
wifi/wifi_manager.c |
freertos/FreeRTOS.h, freertos/task.h, freertos/event_groups.h |
xEventGroupCreate, vTaskDelay |
ota/ota_manager.c |
esp_system.h |
esp_restart |
channels/telegram/telegram_bot.c |
freertos/FreeRTOS.h, freertos/task.h |
xTaskCreatePinnedToCore, vTaskDelay |
tools/tool_registry.c |
<stdlib.h> |
free() |
proxy/http_proxy.c |
<sys/time.h> |
struct timeval |
gateway/ws_server.c |
<stdint.h> |
uint8_t |
ESP-IDF v6.0 API 兼容性验证
以下 API 在 v6.0 中仍然存在,无需修改:
esp_spiffs_info()✅esp_websocket_client_send_bin()✅esp_tls_set_conn_sockfd()/esp_tls_set_conn_state()✅esp_console_new_repl_uart()/esp_console_new_repl_usb_serial_jtag()✅esp_https_ota()+esp_https_ota_config_t✅esp_wifi_set_config()✅
烧录说明
ESP32-S3 使用 USB 口(内置 USB Serial/JTAG 控制器)烧录:
idf.py -p COMx flash monitor
- 插 USB 口(标记为
USB),不是 UART 口 - 如遇连接失败,按住 BOOT 键再插线进入下载模式
讨论:增加国内大模型厂商接入
日期:2026-03-31
目标:为 MimiClaw 增加硅基流动和火山方舟(豆包模型)接入
项目现状
- 当前支持:Anthropic (Claude)、OpenAI (GPT)
- 运行平台:ESP32-S3,纯 C 语言
- 交互方式:Telegram 机器人
国内厂商 API 兼容性
- 硅基流动:OpenAI 兼容,Base URL
https://api.siliconflow.cn/v1 - 火山方舟:OpenAI 兼容,Base URL
https://ark.cn-beijing.volces.com/api/v3
实现方案
由于两者都提供 OpenAI 兼容 API,可复用现有 OpenAI 集成代码,只需:
- 修改 Base URL
- 调整认证方式(Bearer Token)
- 处理模型名称规范
待解决问题
- 认证方式差异确认
- 模型名称规范
- 工具调用格式兼容性验证
讨论: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
目标:为 MimiClaw 添加可配置的时区支持,默认改为中国时区
背景
- 原默认时区为
PST8PDT,M3.2.0,M11.1.0(太平洋时间) - 需要支持用户自定义时区,特别是中国用户(UTC+8)
- 交互方式从 Telegram 改为飞书
实现方案
存储方式
- NVS 存储:使用
system_confignamespace,key 为timezone - Build-time 默认值:
MIMI_TIMEZONE改为"CST-8" - 优先级:NVS 值 > Build-time 值
CLI 命令
set_timezone <TZ> # 例如: set_timezone CST-8 或 set_timezone Asia/Shanghai
timezone_show # 显示当前时区配置和本地时间
LLM 工具
- 新增
set_timezone工具,LLM 可通过对话设置时区 - 支持 POSIX 格式(
CST-8)和城市名(Asia/Shanghai) - 内置 18 个城市名映射表
改动文件
| 文件 | 操作 |
|---|---|
main/mimi_config.h |
默认时区改为 CST-8,添加 MIMI_NVS_KEY_TIMEZONE |
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 / ntp_status / ntp_sync / ntp_set 命令 |
main/CMakeLists.txt |
添加 tool_set_timezone.c 到 SRCS |
支持的时区格式
- POSIX:
CST-8,JST-9,EST5EDT,M3.2.0,M11.1.0,UTC0 - 城市名: Asia/Shanghai, Asia/Tokyo, America/New_York 等 18 个预设
讨论:编译时模块开关(模块化配置)
日期:2026-04-03
分支:feature/module-config
目标:通过编译时配置选择性禁用不需要的模块,减少固件体积,消除未配置模块的警告日志
问题背景
- Task Watchdog 超时:ESP32-S3 运行一天后触发看门狗超时,怀疑是设计问题
- Telegram 警告日志:用户使用飞书但未配置 Telegram,控制台每 5 秒打印 "No bot token configured"
- 代码冗余:Telegram、OpenAI 接口等未使用的模块仍然编译进固件
用户需求
用户希望:
- 通过编译选项禁用不需要的模块(如 Telegram)
- 从源码层面直接过滤不用的组件,减少代码体积
- 禁用模块后不触发警告日志
实现方案
方案选择:编译时条件编译(方案 02)
优点:
- 直接减少 Flash 占用
- 零 RAM 占用,不创建任务
- 从源头消除警告日志
- 实现简单
缺点:
- 切换模块需要重新编译
技术实现
1. 配置文件:sdkconfig.defaults
// Channel Modules
CONFIG_MIMI_CHAN_TELEGRAM=n // 默认禁用
CONFIG_MIMI_CHAN_FEISHU=y // 默认启用
// Tool Modules
CONFIG_MIMI_TOOL_WEB_SEARCH=y
CONFIG_MIMI_TOOL_GPIO=n
// Optional Modules
CONFIG_MIMI_WS_SERVER=y
CONFIG_MIMI_WIFI_ONBOARD=y
CONFIG_MIMI_OTA=n
2. CMakeLists.txt:条件编译源文件
if(CONFIG_MIMI_CHAN_TELEGRAM)
list(APPEND srcs "channels/telegram/telegram_bot.c")
endif()
3. 头文件 stub:调用方无感知
// telegram_bot.h
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
esp_err_t telegram_bot_init(void);
esp_err_t telegram_bot_start(void);
#else
static inline esp_err_t telegram_bot_init(void) { return ESP_OK; }
static inline esp_err_t telegram_bot_start(void) { return ESP_OK; }
#endif
修改文件清单
| 文件 | 修改内容 |
|---|---|
sdkconfig.defaults |
添加模块开关配置 |
main/CMakeLists.txt |
条件编译源文件 |
main/channels/telegram/telegram_bot.h |
添加 stub |
main/channels/feishu/feishu_bot.h |
添加 stub |
main/gateway/ws_server.h |
添加 stub |
main/onboard/wifi_onboard.h |
添加 stub |
main/ota/ota_manager.h |
添加 stub |
main/tools/tool_web_search.h |
添加 stub |
main/tools/tool_gpio.h |
添加 stub |
main/tools/tool_registry.c |
条件注册工具 |
main/cli/serial_cli.c |
条件注册 CLI 命令 |
main/mimi.c |
条件调用模块 |
AGENTS.md |
更新文档 |
模块依赖关系
-
Channel 模块(telegram、feishu):
- 被
mimi.c调用(init/start) - 被
outbound_dispatch_task调用(send) - CLI 命令也需要条件编译
- 被
-
Tool 模块(web_search、gpio):
- 被
tool_registry.c调用(注册 + 执行) - CLI 命令也需要条件编译
- 被
-
可选模块(ws_server、wifi_onboard、ota):
- 被
mimi.c调用 wifi_onboard禁用时需特殊处理(不能进入 captive portal)
- 被
注意事项
- OTA 模块:目前未初始化,但已编译
- 工具描述中的渠道提示:cron_add 工具描述中提到 telegram,可保留(只是描述,不影响功能)
- get_time 工具:使用 telegram.org 代理获取时间(技术原因,不属于 telegram_bot 模块)
预期效果
| 禁用项 | 效果 |
|---|---|
| Telegram | 不编译 telegram_bot.c,无 5 秒警告,节省约 15KB+ Flash |
| GPIO | 不编译 tool_gpio.c,无 gpio_* 工具,节省约 5KB Flash |
| WebSocket | 不编译 ws_server.c,节省约 10KB Flash |
| WiFi Onboard | 不能进入 captive portal 模式,需其他方式配置 WiFi |
讨论:Kconfig 缺失导致模块开关失效
日期:2026-04-04
问题:编译后飞书命令消失、HTTP 配置页面不可用
现象
在 sdkconfig.defaults 中正确配置了 CONFIG_MIMI_CHAN_FEISHU=y 和 CONFIG_MIMI_WIFI_ONBOARD=y,执行 idf.py fullclean && idf.py build 后:
- 烧录后控制台没有飞书相关命令(
set_feishu_creds等)。 - 设备没有启动 HTTP 配置服务(
192.168.4.1无法访问)。 - 检查生成的
sdkconfig文件,发现完全没有这些自定义配置项。
根因
ESP-IDF 的构建系统在生成 sdkconfig 时,只会保留有 Kconfig 声明的配置项。
sdkconfig.defaults仅用于提供默认值。- 如果项目缺少
Kconfig或Kconfig.projbuild文件来声明这些选项,ESP-IDF 会认为它们是无效配置并直接丢弃。 - 之前的版本可能
sdkconfig是手动维护的或缓存未清理,但fullclean后重新生成时就会丢失这些"无名"配置。
修复方案
创建 main/Kconfig.projbuild 文件,声明所有自定义模块开关。
认知修正
ESP-IDF 配置系统工作流:
Kconfig.projbuild:声明配置项(告诉系统"这是什么")。sdkconfig.defaults:提供默认值(告诉系统"默认选什么")。sdkconfig:构建系统根据前两者自动生成(实际编译用的配置)。CMakeLists.txt:读取sdkconfig中的值决定编译哪些文件。
结论:新增模块开关时,必须创建 Kconfig 声明,否则 sdkconfig.defaults 无效。