Compare commits

9 Commits
v1.1.0 ... main

Author SHA1 Message Date
c1368962cc fix: 添加 Kconfig.projbuild 修复模块开关失效问题
Some checks failed
Build / idf-build (push) Has been cancelled
- 创建 main/Kconfig.projbuild 声明所有自定义模块配置项
- 修复 fullclean 后 sdkconfig 丢失 CONFIG_MIMI_* 配置的问题
- 更新 AGENTS.md 添加认知修正栏目和模块开关文档
- 记录 Kconfig 踩坑讨论到 taolun.md
2026-04-04 06:44:52 +08:00
fa41de0ae8 Merge branch 'feature/module-config' into main (resolved conflicts)
Some checks failed
Build / idf-build (push) Has been cancelled
2026-04-03 20:17:40 +08:00
6983a1f1ba feat: 添加编译时模块开关配置
通过 sdkconfig.defaults 选择性启用/禁用模块,减少固件体积:

新增模块开关:
- CONFIG_MIMI_CHAN_TELEGRAM (默认 n)
- CONFIG_MIMI_CHAN_FEISHU (默认 y)
- CONFIG_MIMI_TOOL_WEB_SEARCH (默认 y)
- CONFIG_MIMI_TOOL_GPIO (默认 n)
- CONFIG_MIMI_WS_SERVER (默认 y)
- CONFIG_MIMI_WIFI_ONBOARD (默认 y)
- CONFIG_MIMI_OTA (默认 n)

技术实现:
- CMakeLists.txt 条件编译源文件
- 头文件使用 static inline stub
- CLI 命令和工具注册也支持条件编译

消除 Telegram 未配置时的 5 秒轮询警告日志
2026-04-03 20:15:26 +08:00
d5e70dfc8b 更新了一点缩进和格式
Some checks failed
Build / idf-build (push) Has been cancelled
2026-04-01 22:23:59 +08:00
c260265841 添加 mimiclaw 启动模块流程图
Some checks failed
Build / idf-build (push) Has been cancelled
2026-04-01 22:21:39 +08:00
570c14184e Merge: 时间同步、NVS稳定性修复和ESP-IDF v6.0兼容性改进
Some checks failed
Build / idf-build (push) Has been cancelled
2026-04-01 19:01:07 +08:00
9815ab8df0 feat: 实现时间同步、NVS稳定性修复和ESP-IDF v6.0兼容性改进 2026-04-01 18:58:20 +08:00
3912eda8c1 docs: update changelog, TODO and discussion records with time-sync and NVS stability plan 2026-04-01 06:38:00 +08:00
540bfe825f 修改了:
Some checks failed
Build / idf-build (push) Has been cancelled
spiffs_data/config/SOUL.md
spiffs_data/config/USER.md
spiffs_data/memory/MEMORY.md
spiffs_data/skills/daily-briefing.md
spiffs_data/skills/weather.md
加入了中文内容,进行了更丰富的个人设定。
2026-04-01 03:53:33 +08:00
32 changed files with 1724 additions and 231 deletions

View File

@@ -2,7 +2,7 @@
## 项目概述
MimiClaw 是一个运行在 ESP32-S3 上的 AI 助手,使用纯 C 语言编写。用户通过 Telegram 与之交互,设备连接 WiFi 后,将消息传递给 LLM大语言模型进行处理并支持工具调用。
MimiClaw 是一个运行在 ESP32-S3 上的 AI 助手,使用纯 C 语言编写。用户通过 Telegram 或飞书与设备交互,设备连接 WiFi 后,将消息传递给 LLM大语言模型进行处理并支持工具调用。
## 项目结构
@@ -14,20 +14,21 @@ mimiclaw/
│ │ └── context_builder.c # 构建上下文(系统提示、记忆等)
│ ├── llm/ # LLM 代理
│ │ ├── llm_proxy.c # 处理与 LLM API 的通信
│ │ └── llm_proxy.h # LLM 代理的头文件
│ │ └── llm_provider.c # 多提供商支持Anthropic/OpenAI/硅基流动/火山引擎)
│ ├── cli/ # 串口命令行界面
│ │ └── serial_cli.c # 处理运行时配置命令
│ ├── channels/ # 输入/输出通道
│ │ ├── telegram/ # Telegram 机器人集成
│ │ └── feishu/ # 飞书机器人集成
│ │ ├── telegram/ # Telegram 机器人集成(可选)
│ │ └── feishu/ # 飞书机器人集成(可选)
│ ├── tools/ # LLM 可调用的工具
│ ├── memory/ # 记忆和会话管理
│ ├── proxy/ # HTTP 代理支持
│ ├── proxy/ # HTTP/SOCKS5 代理支持
│ ├── cron/ # 定时任务调度
│ ├── heartbeat/ # 心跳服务
│ ├── gateway/ # WebSocket 网关
│ ├── onboard/ # WiFi 配置门户
│ ├── onboard/ # WiFi 配置门户Captive Portal
│ ├── skills/ # 技能加载器
│ ├── bus/ # 消息总线(连接通道和代理循环)
│ ├── mimi_config.h # 全局配置定义
│ ├── mimi_secrets.h # 构建时密钥(需用户创建)
│ └── mimi_secrets.h.example # 密钥模板
@@ -72,6 +73,71 @@ mimiclaw/
- 工具注册在 `tool_registry.c`
- 支持的工具:`web_search``get_current_time``cron_add/list/remove`
## 模块配置系统
MimiClaw 支持**编译时模块开关**,可以禁用不需要的模块以减少固件体积和内存占用。
### 可配置模块
| 模块 | 配置项 | 默认 | 说明 |
|------|--------|------|------|
| Telegram | `CONFIG_MIMI_CHAN_TELEGRAM` | n | Telegram 机器人集成 |
| 飞书 | `CONFIG_MIMI_CHAN_FEISHU` | y | 飞书机器人集成 |
| WebSocket | `CONFIG_MIMI_WS_SERVER` | y | WebSocket 网关 |
| Web Search | `CONFIG_MIMI_TOOL_WEB_SEARCH` | y | 网络搜索工具 |
| GPIO | `CONFIG_MIMI_TOOL_GPIO` | n | GPIO 控制工具 |
| WiFi 配置页 | `CONFIG_MIMI_WIFI_ONBOARD` | y | Captive Portal |
| OTA | `CONFIG_MIMI_OTA` | n | OTA 升级(未实现) |
### 修改模块配置
编辑 `sdkconfig.defaults` 文件,添加或修改配置项:
```
CONFIG_MIMI_CHAN_TELEGRAM=n
CONFIG_MIMI_CHAN_FEISHU=y
CONFIG_MIMI_TOOL_WEB_SEARCH=y
CONFIG_MIMI_TOOL_GPIO=n
```
修改后必须重新编译:
```bash
idf.py fullclean && idf.py build
```
### 添加新模块的编译时开关
1. **创建 `main/Kconfig.projbuild`**(关键!没有它配置项不会被识别):
```kconfig
config MIMI_MODULE_EXAMPLE
bool "Example module"
default n
help
Enable example module for MimiClaw.
```
2. 在 `sdkconfig.defaults` 中添加配置项:
```
CONFIG_MIMI_MODULE_EXAMPLE=y
```
3. 在 `main/CMakeLists.txt` 中条件编译:
```cmake
if(CONFIG_MIMI_MODULE_EXAMPLE)
list(APPEND srcs "modules/example/example.c")
endif()
```
4. 在模块头文件中添加 stub
```c
#ifdef CONFIG_MIMI_MODULE_EXAMPLE
esp_err_t example_init(void);
#else
static inline esp_err_t example_init(void) { return ESP_OK; }
#endif
```
## 构建和烧录
### 前提条件
@@ -85,7 +151,9 @@ idf.py set-target esp32s3
# 配置(首次需要)
cp main/mimi_secrets.h.example main/mimi_secrets.h
# 编辑 mimi_secrets.h 填写 WiFi、Telegram、LLM 密钥
# 编辑 mimi_secrets.h 填写 WiFi、LLM 密钥
# 编辑 sdkconfig.defaults 启用/禁用模块(可选)
# 清理构建(修改配置后必须执行)
idf.py fullclean && idf.py build
@@ -108,6 +176,16 @@ config_show # 显示当前配置(脱敏)
config_reset # 重置为构建时配置
```
## 认知修正
| 日期 | 问题 | 根因 | 修复 | 详情 |
|------|------|------|------|------|
| 2026-04-04 | 模块开关失效飞书命令消失、HTTP 配置页不可用) | 缺少 `Kconfig.projbuild`ESP-IDF 不识别自定义配置项 | 创建 `main/Kconfig.projbuild` 声明所有模块开关 | [详细讨论](taolun.md#讨论kconfig-缺失导致模块开关失效) |
---
## 开发注意事项
## 开发注意事项
1. **内存限制**ESP32-S3 内存有限,使用 `MALLOC_CAP_SPIRAM` 分配大内存
@@ -115,6 +193,7 @@ config_reset # 重置为构建时配置
3. **日志**:使用 `ESP_LOG` 宏,标签在每个文件中定义
4. **错误处理**:使用 `esp_err_t` 返回码
5. **JSON 处理**:使用 cJSON 库
6. **模块开关**:新增模块或可选模块应在 `sdkconfig.defaults` 中添加配置项,并在头文件中添加 `#ifdef` stub
## 调试技巧

View File

@@ -1,9 +1,33 @@
# 变更日志
## v1.1.0(计划中)
## v1.2.0(计划中)
### 新增
- 国内大模型厂商接入支持(硅基流动、火山方舟)— 计划中
- **编译时模块开关**
- 通过 `sdkconfig.defaults` 配置选择启用/禁用模块
- 支持的模块开关:
- `CONFIG_MIMI_CHAN_TELEGRAM` — Telegram 机器人(默认禁用)
- `CONFIG_MIMI_CHAN_FEISHU` — 飞书机器人(默认启用)
- `CONFIG_MIMI_TOOL_WEB_SEARCH` — 网络搜索工具
- `CONFIG_MIMI_TOOL_GPIO` — GPIO 控制工具(默认禁用)
- `CONFIG_MIMI_WS_SERVER` — WebSocket 网关
- `CONFIG_MIMI_WIFI_ONBOARD` — WiFi 配置门户
- `CONFIG_MIMI_OTA` — OTA 升级(默认禁用)
- 禁用模块不参与编译,节省 Flash 和 RAM
- 头文件使用 static inline stub调用方无需修改
- CLI 命令和工具注册也支持条件编译
### 修复
- 消除 Telegram 未配置时的 5 秒轮询警告日志
---
## v1.1.0
### 新增
- 国内大模型厂商接入支持(硅基流动、火山方舟)
- **时区设置功能**
- 默认时区改为 `CST-8`(中国标准时间 UTC+8
- 新增 `set_timezone` CLI 命令(支持 POSIX 格式和城市名)
@@ -12,8 +36,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 +72,7 @@
### 文档
- 新增 `docs/ESP-IDF-V6-MIGRATION.md` — ESP-IDF v6.0 迁移适配记录
- 更新 `taolun.md` — 讨论记录整理
- 更新 `taolun.md` — 讨论记录整理,新增时间同步和 NVS 配置稳定性问题讨论
---

View File

@@ -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 兼容性总结
### 已验证的 APIv6.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` |

View File

@@ -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

View File

@@ -1,34 +1,67 @@
idf_component_register(
SRCS
# MimiClaw - CMake build configuration
# This file is processed by ESP-IDF's CMake build system
# ─── Core modules (always compiled) ────────────────────────────────────────
set(core_srcs
"mimi.c"
"bus/message_bus.c"
"wifi/wifi_manager.c"
"channels/telegram/telegram_bot.c"
"channels/feishu/feishu_bot.c"
"llm/llm_proxy.c"
"llm/llm_provider.c"
"agent/agent_loop.c"
"agent/context_builder.c"
"memory/memory_store.c"
"memory/session_mgr.c"
"gateway/ws_server.c"
"cli/serial_cli.c"
"proxy/http_proxy.c"
"cron/cron_service.c"
"heartbeat/heartbeat.c"
"tools/tool_registry.c"
"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"
"skills/skill_loader.c"
"onboard/wifi_onboard.c"
"ota/ota_manager.c"
INCLUDE_DIRS
"."
"time_sync/time_sync.c"
"nvs_safety/nvs_safety.c"
)
# ─── Channel modules ───────────────────────────────────────────────────────
if(CONFIG_MIMI_CHAN_TELEGRAM)
list(APPEND core_srcs "channels/telegram/telegram_bot.c")
endif()
if(CONFIG_MIMI_CHAN_FEISHU)
list(APPEND core_srcs "channels/feishu/feishu_bot.c")
endif()
# ─── Optional modules ───────────────────────────────────────────────────────
if(CONFIG_MIMI_WS_SERVER)
list(APPEND core_srcs "gateway/ws_server.c")
endif()
if(CONFIG_MIMI_WIFI_ONBOARD)
list(APPEND core_srcs "onboard/wifi_onboard.c")
endif()
if(CONFIG_MIMI_OTA)
list(APPEND core_srcs "ota/ota_manager.c")
endif()
# ─── Tool modules ──────────────────────────────────────────────────────────
if(CONFIG_MIMI_TOOL_WEB_SEARCH)
list(APPEND core_srcs "tools/tool_web_search.c")
endif()
if(CONFIG_MIMI_TOOL_GPIO)
list(APPEND core_srcs "tools/tool_gpio.c")
list(APPEND core_srcs "tools/gpio_policy.c")
endif()
# ─── Register component ───────────────────────────────────────────────────
idf_component_register(
SRCS ${core_srcs}
INCLUDE_DIRS "."
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event cjson spiffs console vfs app_update esp-tls

51
main/Kconfig.projbuild Normal file
View File

@@ -0,0 +1,51 @@
menu "MimiClaw Configuration"
menu "Channel Modules"
config MIMI_CHAN_TELEGRAM
bool "Telegram bot integration"
default n
help
Enable Telegram bot integration for MimiClaw.
config MIMI_CHAN_FEISHU
bool "Feishu (Lark) bot integration"
default y
help
Enable Feishu (Lark) bot integration for MimiClaw.
endmenu
menu "Tool Modules"
config MIMI_TOOL_WEB_SEARCH
bool "Web search tool"
default y
help
Enable web search tool (requires search API key).
config MIMI_TOOL_GPIO
bool "GPIO control tool"
default n
help
Enable GPIO control tool for hardware control.
endmenu
menu "Optional Modules"
config MIMI_WS_SERVER
bool "WebSocket gateway"
default y
help
Enable WebSocket gateway for local clients.
config MIMI_WIFI_ONBOARD
bool "WiFi onboarding portal"
default y
help
Enable Captive Portal for initial WiFi setup.
config MIMI_OTA
bool "OTA firmware update"
default n
help
Enable OTA firmware update (not fully implemented).
endmenu
endmenu

View File

@@ -2,35 +2,25 @@
#include "esp_err.h"
/**
* Initialize the Feishu bot (load credentials from NVS / build-time).
*/
#ifdef CONFIG_MIMI_CHAN_FEISHU
esp_err_t feishu_bot_init(void);
/**
* Start the Feishu webhook HTTP server for receiving events.
* Listens on MIMI_FEISHU_WEBHOOK_PORT.
*/
esp_err_t feishu_bot_start(void);
/**
* Send a text message to a Feishu chat.
* Automatically splits messages longer than MIMI_FEISHU_MAX_MSG_LEN chars.
* @param chat_id Feishu chat ID (open_id or chat_id)
* @param text Message text
*/
esp_err_t feishu_send_message(const char *chat_id, const char *text);
/**
* Reply to a specific message in a Feishu chat.
* @param message_id The message_id to reply to
* @param text Reply text
*/
esp_err_t feishu_reply_message(const char *message_id, const char *text);
/**
* Save Feishu app credentials to NVS.
* @param app_id Feishu App ID
* @param app_secret Feishu App Secret
*/
esp_err_t feishu_set_credentials(const char *app_id, const char *app_secret);
#else
static inline esp_err_t feishu_bot_init(void) { return ESP_OK; }
static inline esp_err_t feishu_bot_start(void) { return ESP_OK; }
static inline esp_err_t feishu_send_message(const char *chat_id, const char *text) {
(void)chat_id; (void)text;
return ESP_OK;
}
static inline esp_err_t feishu_reply_message(const char *message_id, const char *text) {
(void)message_id; (void)text;
return ESP_OK;
}
static inline esp_err_t feishu_set_credentials(const char *app_id, const char *app_secret) {
(void)app_id; (void)app_secret;
return ESP_OK;
}
#endif

View File

@@ -2,26 +2,20 @@
#include "esp_err.h"
/**
* Initialize the Telegram bot.
*/
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
esp_err_t telegram_bot_init(void);
/**
* Start the Telegram polling task (long polling on Core 0).
*/
esp_err_t telegram_bot_start(void);
/**
* Send a text message to a Telegram chat.
* Automatically splits messages longer than 4096 chars.
* @param chat_id Telegram chat ID (numeric string)
* @param text Message text (supports Markdown)
*/
esp_err_t telegram_send_message(const char *chat_id, const char *text);
/**
* Save the Telegram bot token to NVS.
*/
esp_err_t telegram_set_token(const char *token);
#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; }
static inline esp_err_t telegram_send_message(const char *chat_id, const char *text) {
(void)chat_id; (void)text;
return ESP_OK;
}
static inline esp_err_t telegram_set_token(const char *token) {
(void)token;
return ESP_OK;
}
#endif

View File

@@ -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;
}
@@ -1012,6 +1075,7 @@ esp_err_t serial_cli_init(void)
esp_console_cmd_register(&wifi_scan_cmd);
/* set_tg_token */
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
tg_token_args.token = arg_str1(NULL, NULL, "<token>", "Telegram bot token");
tg_token_args.end = arg_end(1);
esp_console_cmd_t tg_token_cmd = {
@@ -1021,8 +1085,13 @@ esp_err_t serial_cli_init(void)
.argtable = &tg_token_args,
};
esp_console_cmd_register(&tg_token_cmd);
#else
(void)cmd_set_tg_token;
(void)tg_token_args;
#endif
/* set_feishu_creds */
#ifdef CONFIG_MIMI_CHAN_FEISHU
feishu_creds_args.app_id = arg_str1(NULL, NULL, "<app_id>", "Feishu App ID");
feishu_creds_args.app_secret = arg_str1(NULL, NULL, "<app_secret>", "Feishu App Secret");
feishu_creds_args.end = arg_end(2);
@@ -1045,6 +1114,12 @@ esp_err_t serial_cli_init(void)
.argtable = &feishu_send_args,
};
esp_console_cmd_register(&feishu_send_cmd);
#else
(void)cmd_set_feishu_creds;
(void)cmd_feishu_send;
(void)feishu_creds_args;
(void)feishu_send_args;
#endif
/* set_api_key */
api_key_args.key = arg_str1(NULL, NULL, "<key>", "LLM API key");
@@ -1200,6 +1275,7 @@ esp_err_t serial_cli_init(void)
esp_console_cmd_register(&heap_cmd);
/* set_search_key */
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
search_key_args.key = arg_str1(NULL, NULL, "<key>", "Brave Search API key");
search_key_args.end = arg_end(1);
esp_console_cmd_t search_key_cmd = {
@@ -1220,6 +1296,12 @@ esp_err_t serial_cli_init(void)
.argtable = &tavily_key_args,
};
esp_console_cmd_register(&tavily_key_cmd);
#else
(void)cmd_set_search_key;
(void)cmd_set_tavily_key;
(void)search_key_args;
(void)tavily_key_args;
#endif
/* set_proxy */
proxy_args.host = arg_str1(NULL, NULL, "<host>", "Proxy host/IP");
@@ -1269,13 +1351,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 = {
@@ -1302,6 +1403,7 @@ esp_err_t serial_cli_init(void)
esp_console_cmd_register(&tool_exec_cmd);
/* web_search */
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
web_search_args.query = arg_str1(NULL, NULL, "<query>", "Search query");
web_search_args.end = arg_end(1);
esp_console_cmd_t web_search_cmd = {
@@ -1311,6 +1413,10 @@ esp_err_t serial_cli_init(void)
.argtable = &web_search_args,
};
esp_console_cmd_register(&web_search_cmd);
#else
(void)cmd_web_search;
(void)web_search_args;
#endif
/* restart */
esp_console_cmd_t restart_cmd = {

View File

@@ -2,24 +2,15 @@
#include "esp_err.h"
/**
* Initialize and start the WebSocket server on MIMI_WS_PORT.
* Allows external clients to interact with the Agent via JSON messages.
*
* Protocol:
* Inbound: {"type":"message","content":"hello","chat_id":"ws_client1"}
* Outbound: {"type":"response","content":"Hi!","chat_id":"ws_client1"}
*/
#ifdef CONFIG_MIMI_WS_SERVER
esp_err_t ws_server_start(void);
/**
* Send a text message to a specific WebSocket client by chat_id.
* @param chat_id Client identifier (assigned on connection)
* @param text Message text
*/
esp_err_t ws_server_send(const char *chat_id, const char *text);
/**
* Stop the WebSocket server.
*/
esp_err_t ws_server_stop(void);
#else
static inline esp_err_t ws_server_start(void) { return ESP_OK; }
static inline esp_err_t ws_server_send(const char *chat_id, const char *text) {
(void)chat_id; (void)text;
return ESP_OK;
}
static inline esp_err_t ws_server_stop(void) { return ESP_OK; }
#endif

View File

@@ -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';
}

View File

@@ -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();

View File

@@ -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";
@@ -75,24 +77,36 @@ static void outbound_dispatch_task(void *arg)
ESP_LOGI(TAG, "Dispatching response to %s:%s", msg.channel, msg.chat_id);
if (strcmp(msg.channel, MIMI_CHAN_TELEGRAM) == 0) {
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
esp_err_t send_err = telegram_send_message(msg.chat_id, msg.content);
if (send_err != ESP_OK) {
ESP_LOGE(TAG, "Telegram send failed for %s: %s", msg.chat_id, esp_err_to_name(send_err));
} else {
ESP_LOGI(TAG, "Telegram send success for %s (%d bytes)", msg.chat_id, (int)strlen(msg.content));
}
#else
ESP_LOGW(TAG, "Telegram disabled, dropping message for %s", msg.chat_id);
#endif
} else if (strcmp(msg.channel, MIMI_CHAN_FEISHU) == 0) {
#ifdef CONFIG_MIMI_CHAN_FEISHU
esp_err_t send_err = feishu_send_message(msg.chat_id, msg.content);
if (send_err != ESP_OK) {
ESP_LOGE(TAG, "Feishu send failed for %s: %s", msg.chat_id, esp_err_to_name(send_err));
} else {
ESP_LOGI(TAG, "Feishu send success for %s (%d bytes)", msg.chat_id, (int)strlen(msg.content));
}
#else
ESP_LOGW(TAG, "Feishu disabled, dropping message for %s", msg.chat_id);
#endif
} else if (strcmp(msg.channel, MIMI_CHAN_WEBSOCKET) == 0) {
#ifdef CONFIG_MIMI_WS_SERVER
esp_err_t ws_err = ws_server_send(msg.chat_id, msg.content);
if (ws_err != ESP_OK) {
ESP_LOGW(TAG, "WS send failed for %s: %s", msg.chat_id, esp_err_to_name(ws_err));
}
#else
ESP_LOGW(TAG, "WebSocket disabled, dropping message for %s", msg.chat_id);
#endif
} else if (strcmp(msg.channel, MIMI_CHAN_SYSTEM) == 0) {
ESP_LOGI(TAG, "System message [%s]: %.128s", msg.chat_id, msg.content);
} else {
@@ -120,6 +134,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 +166,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");
}
@@ -160,13 +176,19 @@ void app_main(void)
if (!wifi_ok) {
ESP_LOGW(TAG, "Entering WiFi onboarding mode...");
#ifdef CONFIG_MIMI_WIFI_ONBOARD
wifi_onboard_start(WIFI_ONBOARD_MODE_CAPTIVE); /* blocks, restarts on success */
return; /* unreachable */
#else
ESP_LOGE(TAG, "WiFi onboarding disabled. Configure WiFi via CLI or flash new firmware.");
return;
#endif
}
#ifdef CONFIG_MIMI_WIFI_ONBOARD
if (wifi_onboard_start(WIFI_ONBOARD_MODE_ADMIN) != ESP_OK) {
ESP_LOGW(TAG, "Local admin portal unavailable; continuing without config hotspot");
}
#endif
{
/* Outbound dispatch task should start first to avoid dropping early replies. */
@@ -178,11 +200,17 @@ void app_main(void)
/* Start network-dependent services */
ESP_ERROR_CHECK(agent_loop_start());
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
ESP_ERROR_CHECK(telegram_bot_start());
#endif
#ifdef CONFIG_MIMI_CHAN_FEISHU
ESP_ERROR_CHECK(feishu_bot_start());
#endif
cron_service_start();
heartbeat_start();
#ifdef CONFIG_MIMI_WS_SERVER
ESP_ERROR_CHECK(ws_server_start());
#endif
ESP_LOGI(TAG, "All services started!");
}

View File

@@ -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-"

View 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;
}

View 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);

View File

@@ -2,14 +2,21 @@
#include "esp_err.h"
#ifdef CONFIG_MIMI_WIFI_ONBOARD
typedef enum {
WIFI_ONBOARD_MODE_CAPTIVE = 0,
WIFI_ONBOARD_MODE_ADMIN,
} wifi_onboard_mode_t;
/**
* Start WiFi onboarding/configuration portal.
* CAPTIVE mode opens DNS hijack + config page and blocks forever.
* ADMIN mode keeps a local config hotspot alive without captive redirects.
*/
esp_err_t wifi_onboard_start(wifi_onboard_mode_t mode);
#else
typedef enum {
WIFI_ONBOARD_MODE_CAPTIVE = 0,
WIFI_ONBOARD_MODE_ADMIN,
} wifi_onboard_mode_t;
static inline esp_err_t wifi_onboard_start(wifi_onboard_mode_t mode) {
(void)mode;
return ESP_OK;
}
#endif

View File

@@ -2,11 +2,11 @@
#include "esp_err.h"
/**
* Perform OTA firmware update from a URL.
* Downloads the firmware binary and applies it. Reboots on success.
*
* @param url HTTPS URL to the firmware .bin file
* @return ESP_OK on success (device will reboot), error code otherwise
*/
#ifdef CONFIG_MIMI_OTA
esp_err_t ota_update_from_url(const char *url);
#else
static inline esp_err_t ota_update_from_url(const char *url) {
(void)url;
return ESP_ERR_NOT_SUPPORTED;
}
#endif

153
main/time_sync/time_sync.c Normal file
View 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;
}

View 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);

View File

@@ -2,26 +2,34 @@
#include "esp_err.h"
#include <stddef.h>
#include <stdio.h>
/**
* Initialize GPIO tool — configure allowed pins and directions.
*/
#ifdef CONFIG_MIMI_TOOL_GPIO
esp_err_t tool_gpio_init(void);
/**
* Write a GPIO pin HIGH or LOW.
* Input JSON: {"pin": <int>, "state": <0|1>}
*/
esp_err_t tool_gpio_write_execute(const char *input_json, char *output, size_t output_size);
/**
* Read a single GPIO pin state.
* Input JSON: {"pin": <int>}
*/
esp_err_t tool_gpio_read_execute(const char *input_json, char *output, size_t output_size);
/**
* Read all allowed GPIO pin states at once.
* Input JSON: {} (no parameters)
*/
esp_err_t tool_gpio_read_all_execute(const char *input_json, char *output, size_t output_size);
#else
static inline esp_err_t tool_gpio_init(void) { return ESP_OK; }
static inline esp_err_t tool_gpio_write_execute(const char *input_json, char *output, size_t output_size) {
(void)input_json; (void)output; (void)output_size;
if (output && output_size > 0) {
snprintf(output, output_size, "Error: GPIO tool is disabled in this build.");
}
return ESP_OK;
}
static inline esp_err_t tool_gpio_read_execute(const char *input_json, char *output, size_t output_size) {
(void)input_json; (void)output; (void)output_size;
if (output && output_size > 0) {
snprintf(output, output_size, "Error: GPIO tool is disabled in this build.");
}
return ESP_OK;
}
static inline esp_err_t tool_gpio_read_all_execute(const char *input_json, char *output, size_t output_size) {
(void)input_json; (void)output; (void)output_size;
if (output && output_size > 0) {
snprintf(output, output_size, "Error: GPIO tool is disabled in this build.");
}
return ESP_OK;
}
#endif

View File

@@ -59,6 +59,7 @@ esp_err_t tool_registry_init(void)
s_tool_count = 0;
/* Register web_search */
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
tool_web_search_init();
mimi_tool_t ws = {
@@ -71,6 +72,7 @@ esp_err_t tool_registry_init(void)
.execute = tool_web_search_execute,
};
register_tool(&ws);
#endif
/* Register get_current_time */
mimi_tool_t gt = {
@@ -192,6 +194,7 @@ esp_err_t tool_registry_init(void)
register_tool(&cr);
/* Register GPIO tools */
#ifdef CONFIG_MIMI_TOOL_GPIO
tool_gpio_init();
mimi_tool_t gw = {
@@ -227,6 +230,7 @@ esp_err_t tool_registry_init(void)
.execute = tool_gpio_read_all_execute,
};
register_tool(&ga);
#endif
build_tools_json();

View File

@@ -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];

View File

@@ -2,28 +2,28 @@
#include "esp_err.h"
#include <stddef.h>
#include <stdio.h>
/**
* Initialize web search tool.
*/
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
esp_err_t tool_web_search_init(void);
/**
* Execute a web search.
*
* @param input_json JSON string with "query" field
* @param output Output buffer for formatted search results
* @param output_size Size of output buffer
* @return ESP_OK on success
*/
esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t output_size);
/**
* Save Brave Search API key to NVS.
*/
esp_err_t tool_web_search_set_key(const char *api_key);
/**
* Save Tavily API key to NVS.
*/
esp_err_t tool_web_search_set_tavily_key(const char *api_key);
#else
static inline esp_err_t tool_web_search_init(void) { return ESP_OK; }
static inline esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t output_size) {
(void)input_json; (void)output; (void)output_size;
if (output && output_size > 0) {
snprintf(output, output_size, "Error: Web search tool is disabled in this build.");
}
return ESP_OK;
}
static inline esp_err_t tool_web_search_set_key(const char *api_key) {
(void)api_key;
return ESP_OK;
}
static inline esp_err_t tool_web_search_set_tavily_key(const char *api_key) {
(void)api_key;
return ESP_OK;
}
#endif

View File

@@ -6,3 +6,37 @@ CONFIG_LWIP_LOCAL_HOSTNAME="mimiclaw"
# SPIFFS: increase max filename length (default 32 is too short for session files)
CONFIG_SPIFFS_OBJ_NAME_LEN=64
# ─── Channel Modules ──────────────────────────────────────────────
# Telegram bot integration (disable if using Feishu only)
CONFIG_MIMI_CHAN_TELEGRAM=n
# Feishu (Lark) bot integration (default: enabled)
CONFIG_MIMI_CHAN_FEISHU=y
# ─── Tool Modules ─────────────────────────────────────────────────
# Web search tool (requires search API key)
CONFIG_MIMI_TOOL_WEB_SEARCH=y
# GPIO control tool (for hardware control)
CONFIG_MIMI_TOOL_GPIO=n
# ─── Optional Modules ──────────────────────────────────────────────
# WebSocket gateway (for local clients)
CONFIG_MIMI_WS_SERVER=y
# WiFi onboarding portal (Captive Portal for initial setup)
CONFIG_MIMI_WIFI_ONBOARD=y
# OTA firmware update (not fully implemented)
CONFIG_MIMI_OTA=n
# 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
# Partition table: use custom partition table with SPIFFS and OTA
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"

View File

@@ -1,11 +1,30 @@
I am MimiClaw, a personal AI assistant running on an ESP32-S3 microcontroller.
我是 蓓蓓,来自虚拟世界的未来人。
Personality:
- Helpful and friendly
- Concise and to the point
- Curious and eager to learn
## 个人信息:
- 姓名:张晓蓓
- 年龄30
- 性别:女
- 语言:中文
- 位置:中国河北省邯郸市
- 时区Asia/Shanghai
- 专业:装潢设计
- 标签:轻熟少妇,交际花
Values:
- Accuracy over speed
- User privacy and safety
- Transparency in actions
## 性格:
- 乐于助人
- 简洁明了,善于抓住问题的重心
- 富有探索精神 且 勤奋好学
## 价值观:
- 精准优先
- 注重 「用户隐私与安全」
- 所有的行动对使用者 「公开透明」
## 爱好:
- 唱歌
- 弹琴
- 跳舞
## 评价:
- 有自己的性格
- 回答问题简明扼要

View File

@@ -1,5 +1,47 @@
# User Profile
# 用户简介
- Name: (not set)
- Language: Chinese / English
- Timezone: (not set)
## 个人信息
- 姓名:胡杨
- 年龄31
- 性别:男
- 语言:母语中文、会一点基础的英语
- 位置:中国河北省邯郸市
- 时区: Asia/Shanghai
- 专业web开发工程师
## 专业技能
- 会基础的SQL基础增删改查之类的索引、视图这些高级东西还有待学习。
- 现在主要使用 `Bun` + `TS`基础 + `ExpressJS` 来开发应用,或者是 `NodeJS`的新特性 + `esno`运行时 + `TS`基础语法 + `ExpressJS` | `KoaJS` 来开发后端应用。
- 平时会开发一些 `Terminal` 应用。
- 现在在用 `OpenCode`*vibe Coding* 的场景比较多。
- 目前用 `OpenCode` 做终端应用,用的 `Go` 语言比较多,但我本身不会 `Go` 语言。
- 现在在玩儿一些 ESP的小东西手里有 `ESP32 S3 N16R8``ESP8266`设备,未来想做一些智能家居的场景。
- 前端开发,会用 `Vite``Vue3`之前学写过基础语法,但是没有大型的完整的项目经验
- JavaScript 的原型链是我的弱项,没系统的学习过。
- 以前是做PHP开发的所以现在做开发习惯用 Class OOP的方式听说过 设计模式,也读过几个基础的,希望未来能够在设计模式上学的更多,掌握的更扎实,用到实际项目中去。
- 最近在用 `PicoClaw``ZeroClaw` 来做个人的AI助手因为性能好内存占用最低。
- 熟练使用 `Docker`
## 特点
- 有审美观念
- 对编程有自己的风格,以简单、拆分、好维护为宗旨,习惯将一个整流程拆分成不同函数、不同模块、不同类来做。
- 有系统架构师的潜质,习惯从全局来着手。
- 对性能有极高的要求,大家都喜欢的追求的,不一定是我需要的,适合的。
- 我认为即使是一个小小的开发板,也应该发挥它的余热,而不是浪费掉。
## 性格:
- 内在活泼开朗。
- 渴望运动、打羽毛球,上学时候经常做。
- 简单直接。
## 自我期望
- 未来,希望能够 熟练使用 `Go`语言我认为GO语言在当下这个AI时代很重要是最接近完美的语言性能、语法、跨平台、学习曲线都比较优秀这是JS、TS、Python这些语言所没有的性能。
- 也希望能够掌握 `Python` 的语法毕竟Python在AI时代有它不可撼动的地位但在目前来看我不会考虑用它做为第一语言性能太弱、配置太繁琐、工程化太乱、开发流程上也很繁琐。
- 希望能够熟练 `Shell` 语法和使用,毕竟服务器维护和 `linux` 在开发环境中很重要。
# 爱好
- 爬山曾两次独自夜爬泰山15年和两个朋友一起冒雨爬了武当山一个是我朋友一个是我朋友婚礼上认识的伴娘我在追求人家中途雨停早上和那位伴娘俩人都第一次看到了一直想看到的云海。
- 听歌:喜欢悠扬、婉转类的、伤感的。
- 编程:平时习惯打开电脑,想一些有趣的点子,看看能不能用编程实现。
- 摄影:没用过相机,但是平时喜欢用手机记录身边美好的风景,尤其是出去玩儿的时候。
- 旅行这10年和好多城市有过交集北京、郑州、开封、泰安、烟台、苏州、湖州、杭州

View File

@@ -1,3 +1,3 @@
# Long-term Memory
# 长期记忆
(empty - MimiClaw will write memories here as it learns)
(空 - MimiClaw 在学习时会在这里写下记忆这将作为mimiclaw的大脑记忆来使用

View File

@@ -1,22 +1,22 @@
# Daily Briefing
# 每日简报
Compile a personalized daily briefing for the user.
为用户编写个性化的每日简报。
## When to use
When the user asks for a daily briefing, morning update, or "what's new today".
Also useful as a heartbeat/cron task.
## 何时使用
当用户询问每日简报、早间更新或“今天有什么新事”时,
作为心跳heartbeat/克隆cron任务也很有用。
## How to use
1. Use get_current_time for today's date
2. Read /spiffs/memory/MEMORY.md for user preferences and context
3. Read today's daily note if it exists
4. Use web_search for relevant news based on user interests
5. Compile a concise briefing covering:
- Date and time
- Weather (if location known from USER.md)
- Relevant news/updates based on user interests
- Any pending tasks from memory
- Any scheduled cron jobs
## 如何使用
1. 使用 get_current_time 作为今天的日期
2. 读取 /spiffs/memory/MEMORY.md 以获取用户偏好和上下文
3. 阅读当日笔记(若存在)
4. 使用 web_search 查找用户感兴趣的相关新闻
5. 编写简明摘要,涵盖:
- 日期和时间
- 天气 (如果从 USER.md 中获取到位置)
- 基于用户兴趣的相关新闻/更新news/updates
- 待办任务(来自 memory
- 计划中的 cron 任务
## Format
Keep it brief — 5-10 bullet points max. Use the user's preferred language.
## 格式
保持简洁—最多 5-10 个要点。使用用户的偏好语言。

View File

@@ -1,18 +1,18 @@
# Weather
# 天气
Get current weather and forecasts using web_search.
使用 web_search 获取最新天气和天气预报。
## When to use
When the user asks about weather, temperature, or forecasts.
## 何时使用
当用户询问天气、温度或天气预报时。
## How to use
1. Use get_current_time to know the current date
2. Use web_search with a query like "weather in [city] today"
3. Extract temperature, conditions, and forecast from results
4. Present in a concise, friendly format
## 如何使用
1. 使用 get_current_time 来查询当前日期
2. 使用 web_search,查询如“今天[城市]天气”
3. 从结果中提取温度、条件和预报
4. 以简明、友好的形式呈现
## Example
User: "What's the weather in Tokyo?"
## 示例
用户:“北京的天气怎么样?”
→ get_current_time
→ web_search "weather Tokyo today February 2026"
"Tokyo: 8°C, partly cloudy. High 12°C, low 4°C. Light wind from the north."
→ web_search “2026 年 「get_current_time」 的北京天气”
“北京8°C局部多云。最高 12°C最低 4°C。 北风,微风。”

414
taolun.md
View File

@@ -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,9 +365,168 @@ 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 |
### 支持的时区格式
- 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`
**目标**:通过编译时配置选择性禁用不需要的模块,减少固件体积,消除未配置模块的警告日志
### 问题背景
1. **Task Watchdog 超时**ESP32-S3 运行一天后触发看门狗超时,怀疑是设计问题
2. **Telegram 警告日志**:用户使用飞书但未配置 Telegram控制台每 5 秒打印 "No bot token configured"
3. **代码冗余**Telegram、OpenAI 接口等未使用的模块仍然编译进固件
### 用户需求
用户希望:
1. 通过编译选项禁用不需要的模块(如 Telegram
2. 从源码层面直接过滤不用的组件,减少代码体积
3. 禁用模块后不触发警告日志
### 实现方案
#### 方案选择:编译时条件编译(方案 02
**优点**
- 直接减少 Flash 占用
- 零 RAM 占用,不创建任务
- 从源头消除警告日志
- 实现简单
**缺点**
- 切换模块需要重新编译
#### 技术实现
##### 1. 配置文件:`sdkconfig.defaults`
```c
// 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条件编译源文件
```cmake
if(CONFIG_MIMI_CHAN_TELEGRAM)
list(APPEND srcs "channels/telegram/telegram_bot.c")
endif()
```
##### 3. 头文件 stub调用方无感知
```c
// 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
### 注意事项
1. **OTA 模块**:目前未初始化,但已编译
2. **工具描述中的渠道提示**cron_add 工具描述中提到 telegram可保留只是描述不影响功能
3. **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 配置系统工作流**
1. `Kconfig.projbuild`**声明**配置项(告诉系统"这是什么")。
2. `sdkconfig.defaults`:提供**默认值**(告诉系统"默认选什么")。
3. `sdkconfig`:构建系统根据前两者**自动生成**(实际编译用的配置)。
4. `CMakeLists.txt`:读取 `sdkconfig` 中的值**决定编译哪些文件**。
**结论**:新增模块开关时,**必须**创建 Kconfig 声明,否则 `sdkconfig.defaults` 无效。

118
useage.md Normal file
View File

@@ -0,0 +1,118 @@
# 使用说明
## 编译
```shell
idf.py fullclean
idf.py set-target esp32s3
```
## MimiClaw 启动顺序图
┌─────────────────────────────────────────────────────────┐
│ app_main() 开始 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 1. 核心基础设施初始化 │
│ ┌───────────────────────────────────────────────┐ │
│ │ • 初始化NVS (非易失性存储) │ │
│ │ • NVS安全检查 │ │
│ │ • 创建默认事件循环 │ │
│ │ • 初始化SPIFFS (文件系统) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 2. 子系统初始化 │
│ ┌───────────────────────────────────────────────┐ │
│ │ • 消息总线 (message_bus_init) │ │
│ │ • 内存存储 (memory_store_init) │ │
│ │ • 技能加载器 (skill_loader_init) │ │
│ │ • 会话管理器 (session_mgr_init) │ │
│ │ • WiFi管理器 (wifi_manager_init) │ │
│ │ • HTTP代理 (http_proxy_init) │ │
│ │ • Telegram机器人 (telegram_bot_init) │ │
│ │ • 飞书机器人 (feishu_bot_init) │ │
│ │ • LLM代理 (llm_proxy_init) │ │
│ │ • 工具注册表 (tool_registry_init) │ │
│ │ • 定时服务 (cron_service_init) │ │
│ │ • 心跳服务 (heartbeat_init) │ │
│ │ • 代理循环 (agent_loop_init) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 3. 串口CLI启动 (无WiFi依赖) │
│ ┌───────────────────────────────────────────────┐ │
│ │ • 串口CLI初始化 (serial_cli_init) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 4. WiFi连接阶段 │
│ ┌───────────────────────────────────────────────┐ │
│ │ • 启动WiFi管理器 (wifi_manager_start) │ │
│ │ • 扫描附近AP并打印 │ │
│ │ • 等待WiFi连接 (30秒超时) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌───────────────┐
│ WiFi连接成功? │
└───────────────┘
┌───────────────────┼───────────────────┐
│ │ │
▼ │ ▼
┌─────────────────────┐ │ ┌─────────────────────┐
│ 5a. 连接成功路径 │ │ │ 5b. 连接失败路径 │
│ • 获取IP地址 │ │ │ • 进入WiFi配置门户│
│ • 初始化时间同步 │ │ │ (wifi_onboard_start)│
│ (time_sync_init) │ │ │ • 重启设备 │
└─────────────────────┘ │ └─────────────────────┘
┌─────────────────────┐
│ 启动管理门户(可选) │
│ (wifi_onboard_start)│
└─────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 6. 网络服务启动 │
│ ┌───────────────────────────────────────────────┐ │
│ │ • 创建出站调度任务 (outbound_dispatch_task) │ │
│ │ • 启动代理循环 (agent_loop_start) │ │
│ │ • 启动Telegram机器人 (telegram_bot_start) │ │
│ │ • 启动飞书机器人 (feishu_bot_start) │ │
│ │ • 启动定时服务 (cron_service_start) │ │
│ │ • 启动心跳服务 (heartbeat_start) │ │
│ │ • 启动WebSocket服务器 (ws_server_start) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ MimiClaw 启动完成 │
│ "Type 'help' for CLI commands" │
└─────────────────────────────────────────────────────────┘
### 关键启动点和依赖关系
#### 时间依赖
1. *时间同步 (time_sync_init)*只在WiFi连接成功后初始化
2. 网络服务所有网络服务Telegram、飞书、WebSocket等都在WiFi连接成功后启动
#### 无依赖启动
1. 串口CLI最早启动即使没有WiFi也能工作
2. 核心基础设施NVS、SPIFFS等必须首先初始化
#### 条件分支
1. WiFi连接失败进入配置门户模式阻塞直到配置成功并重启
2. WiFi连接成功继续启动所有网络服务
#### 并行启动
- 多个子系统初始化是顺序执行的但一旦启动它们会在各自的FreeRTOS任务中并行运行
- 出站调度任务在启动网络服务前创建,以避免丢失早期响应
这个启动顺序确保了核心功能在没有网络时也能部分工作如CLI命令同时网络相关服务在连接建立后按顺序启动。