feat: 添加时区设置功能,默认时区改为 CST-8
- 新增 set_timezone LLM 工具,支持通过对话设置时区 - 新增 set_timezone / timezone_show CLI 命令 - 默认时区从 PST 改为 CST-8(中国标准时间 UTC+8) - 支持 POSIX 格式和 18 个城市名映射(Asia/Shanghai 等) - 时区通过 NVS 持久化存储(system_config namespace) - config_show 中显示当前时区配置 - 更新 changelog.md 和 taolun.md 文档
This commit is contained in:
@@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
- 国内大模型厂商接入支持(硅基流动、火山方舟)— 计划中
|
- 国内大模型厂商接入支持(硅基流动、火山方舟)— 计划中
|
||||||
|
- **时区设置功能**
|
||||||
|
- 默认时区改为 `CST-8`(中国标准时间 UTC+8)
|
||||||
|
- 新增 `set_timezone` CLI 命令(支持 POSIX 格式和城市名)
|
||||||
|
- 新增 `timezone_show` CLI 命令
|
||||||
|
- 新增 `set_timezone` LLM 工具(可通过对话设置时区)
|
||||||
|
- 时区通过 NVS 持久化存储(`system_config` namespace)
|
||||||
|
- 支持城市名映射(Asia/Shanghai → CST-8 等 18 个预设城市)
|
||||||
|
- `config_show` 中显示当前时区配置
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
- ESP-IDF v6.0 编译适配
|
- ESP-IDF v6.0 编译适配
|
||||||
|
|||||||
@@ -57,9 +57,17 @@ Telegram App (User)
|
|||||||
│ Anthropic Messages API (HTTPS)
|
│ Anthropic Messages API (HTTPS)
|
||||||
│ + Brave Search API (HTTPS)
|
│ + Brave Search API (HTTPS)
|
||||||
▼
|
▼
|
||||||
┌───────────┐ ┌──────────────┐
|
┌───────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
│ Claude API │ │ Brave Search │
|
│ Claude API │ │ Brave Search │ │ Tavily Search│
|
||||||
└───────────┘ └──────────────┘
|
└───────────┘ └──────────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
┌───────────┐ ┌──────────────┐
|
||||||
|
│ OpenAI API │ │ SiliconFlow │
|
||||||
|
└───────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
┌───────────┐ ┌──────────────┐
|
||||||
|
│ Volcengine │ │ Feishu Bot │
|
||||||
|
└───────────┘ └──────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -106,15 +114,21 @@ main/
|
|||||||
│
|
│
|
||||||
├── wifi/
|
├── wifi/
|
||||||
│ ├── wifi_manager.h WiFi STA lifecycle API
|
│ ├── wifi_manager.h WiFi STA lifecycle API
|
||||||
│ └── wifi_manager.c Event handler, exponential backoff
|
│ └── wifi_manager.c Event handler, exponential backoff (timer-based retry)
|
||||||
│
|
│
|
||||||
├── telegram/
|
├── channels/
|
||||||
│ ├── telegram_bot.h Bot init/start, send_message API
|
│ ├── telegram/
|
||||||
│ └── telegram_bot.c Long polling loop, JSON parsing, message splitting
|
│ │ ├── telegram_bot.h Bot init/start, send_message API
|
||||||
|
│ │ └── telegram_bot.c Long polling loop, JSON parsing, message splitting
|
||||||
|
│ └── feishu/
|
||||||
|
│ ├── feishu_bot.h Feishu bot API
|
||||||
|
│ └── feishu_bot.c WebSocket event handling, message send/recv
|
||||||
│
|
│
|
||||||
├── llm/
|
├── llm/
|
||||||
│ ├── llm_proxy.h llm_chat() + llm_chat_tools() API, tool_use types
|
│ ├── llm_proxy.h llm_chat() + llm_chat_tools() API, tool_use types
|
||||||
│ └── llm_proxy.c Anthropic Messages API (non-streaming), tool_use parsing
|
│ ├── llm_proxy.c Multi-provider LLM (Anthropic + OpenAI-compatible)
|
||||||
|
│ ├── llm_provider.h Provider registry + configuration API
|
||||||
|
│ └── llm_provider.c Provider configs: anthropic, openai, siliconflow, volcengine
|
||||||
│
|
│
|
||||||
├── agent/
|
├── agent/
|
||||||
│ ├── agent_loop.h Agent task init/start
|
│ ├── agent_loop.h Agent task init/start
|
||||||
@@ -125,8 +139,17 @@ main/
|
|||||||
├── tools/
|
├── tools/
|
||||||
│ ├── tool_registry.h Tool definition struct, register/dispatch API
|
│ ├── tool_registry.h Tool definition struct, register/dispatch API
|
||||||
│ ├── tool_registry.c Tool registration, JSON schema builder, dispatch by name
|
│ ├── tool_registry.c Tool registration, JSON schema builder, dispatch by name
|
||||||
│ ├── tool_web_search.h Web search tool API
|
│ ├── tool_web_search.h Web search tool API (Tavily + Brave)
|
||||||
│ └── tool_web_search.c Brave Search API via HTTPS (direct + proxy)
|
│ ├── tool_web_search.c Brave/Tavily Search API via HTTPS
|
||||||
|
│ ├── tool_get_time.h Time tool API
|
||||||
|
│ ├── tool_get_time.c HTTP Date header parsing for time sync
|
||||||
|
│ ├── tool_cron.h Cron tool API
|
||||||
|
│ ├── tool_cron.c Cron job management
|
||||||
|
│ ├── tool_files.h File tool API
|
||||||
|
│ ├── tool_files.c read/write/edit/list files on SPIFFS
|
||||||
|
│ ├── tool_gpio.h GPIO tool API
|
||||||
|
│ ├── tool_gpio.c GPIO read/write
|
||||||
|
│ └── gpio_policy.c GPIO pin allowlist policy
|
||||||
│
|
│
|
||||||
├── memory/
|
├── memory/
|
||||||
│ ├── memory_store.h Long-term + daily memory API
|
│ ├── memory_store.h Long-term + daily memory API
|
||||||
@@ -140,12 +163,29 @@ main/
|
|||||||
│
|
│
|
||||||
├── proxy/
|
├── proxy/
|
||||||
│ ├── http_proxy.h Proxy connection API
|
│ ├── http_proxy.h Proxy connection API
|
||||||
│ └── http_proxy.c HTTP CONNECT tunnel + TLS via esp_tls
|
│ └── http_proxy.c HTTP CONNECT tunnel + SOCKS5 tunnel + TLS
|
||||||
│
|
│
|
||||||
├── cli/
|
├── cli/
|
||||||
│ ├── serial_cli.h CLI init API
|
│ ├── serial_cli.h CLI init API
|
||||||
│ └── serial_cli.c esp_console REPL with debug/maintenance commands
|
│ └── serial_cli.c esp_console REPL with debug/maintenance commands
|
||||||
│
|
│
|
||||||
|
├── cron/
|
||||||
|
│ ├── cron_service.h Cron job API
|
||||||
|
│ └── cron_service.c Cron scheduler, job persistence, execution
|
||||||
|
│
|
||||||
|
├── heartbeat/
|
||||||
|
│ ├── heartbeat.h Heartbeat API
|
||||||
|
│ └── heartbeat.c Periodic heartbeat messages
|
||||||
|
│
|
||||||
|
├── onboard/
|
||||||
|
│ ├── wifi_onboard.h WiFi onboarding portal API
|
||||||
|
│ ├── wifi_onboard.c Captive portal + Soft AP + HTTP config page
|
||||||
|
│ └── onboard_html.h Embedded HTML/CSS/JS for setup page
|
||||||
|
│
|
||||||
|
├── skills/
|
||||||
|
│ ├── skill_loader.h Skill loader API
|
||||||
|
│ └── skill_loader.c Load skill files from SPIFFS
|
||||||
|
│
|
||||||
└── ota/
|
└── ota/
|
||||||
├── ota_manager.h OTA update API
|
├── ota_manager.h OTA update API
|
||||||
└── ota_manager.c esp_https_ota wrapper
|
└── ota_manager.c esp_https_ota wrapper
|
||||||
@@ -158,9 +198,13 @@ main/
|
|||||||
| Task | Core | Priority | Stack | Description |
|
| Task | Core | Priority | Stack | Description |
|
||||||
|--------------------|------|----------|--------|--------------------------------------|
|
|--------------------|------|----------|--------|--------------------------------------|
|
||||||
| `tg_poll` | 0 | 5 | 12 KB | Telegram long polling (30s timeout) |
|
| `tg_poll` | 0 | 5 | 12 KB | Telegram long polling (30s timeout) |
|
||||||
| `agent_loop` | 1 | 6 | 12 KB | Message processing + Claude API call |
|
| `feishu_ws` | 0 | 5 | 12 KB | Feishu WebSocket event handling |
|
||||||
| `outbound` | 0 | 5 | 8 KB | Route responses to Telegram / WS |
|
| `agent_loop` | 1 | 6 | 24 KB | Message processing + LLM API call |
|
||||||
|
| `outbound` | 0 | 5 | 12 KB | Route responses to channels |
|
||||||
| `serial_cli` | 0 | 3 | 4 KB | USB serial console REPL |
|
| `serial_cli` | 0 | 3 | 4 KB | USB serial console REPL |
|
||||||
|
| `onboard_dns` | 0 | 5 | 4 KB | DNS hijack for captive portal |
|
||||||
|
| `cron_check` | 0 | 4 | 4 KB | Cron job scheduler |
|
||||||
|
| `heartbeat` | 0 | 4 | 4 KB | Periodic heartbeat |
|
||||||
| httpd (internal) | 0 | 5 | — | WebSocket server (esp_http_server) |
|
| httpd (internal) | 0 | 5 | — | WebSocket server (esp_http_server) |
|
||||||
| wifi_event (IDF) | 0 | 8 | — | WiFi event handling (ESP-IDF) |
|
| wifi_event (IDF) | 0 | 8 | — | WiFi event handling (ESP-IDF) |
|
||||||
|
|
||||||
@@ -225,20 +269,66 @@ Session files are JSONL (one JSON object per line):
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
All configuration is done exclusively through `mimi_secrets.h` at build time. There is no runtime configuration — changing any setting requires `idf.py fullclean && idf.py build`.
|
Configuration uses a multi-layer priority system:
|
||||||
|
|
||||||
| Define | Description |
|
### Build-time (`mimi_secrets.h`)
|
||||||
|------------------------------|-----------------------------------------|
|
Highest priority. Set in `mimi_secrets.h` (copy from `mimi_secrets.h.example`).
|
||||||
| `MIMI_SECRET_WIFI_SSID` | WiFi SSID |
|
|
||||||
| `MIMI_SECRET_WIFI_PASS` | WiFi password |
|
|
||||||
| `MIMI_SECRET_TG_TOKEN` | Telegram Bot API token |
|
|
||||||
| `MIMI_SECRET_API_KEY` | Anthropic API key |
|
|
||||||
| `MIMI_SECRET_MODEL` | Model ID (default: claude-opus-4-6) |
|
|
||||||
| `MIMI_SECRET_PROXY_HOST` | HTTP proxy hostname/IP (optional) |
|
|
||||||
| `MIMI_SECRET_PROXY_PORT` | HTTP proxy port (optional) |
|
|
||||||
| `MIMI_SECRET_SEARCH_KEY` | Brave Search API key (optional) |
|
|
||||||
|
|
||||||
NVS is still initialized (required by ESP-IDF WiFi internals) but is not used for application configuration.
|
| Define | Description |
|
||||||
|
|-------------------------------------|--------------------------------------------|
|
||||||
|
| `MIMI_SECRET_WIFI_SSID` | WiFi SSID |
|
||||||
|
| `MIMI_SECRET_WIFI_PASS` | WiFi password |
|
||||||
|
| `MIMI_SECRET_TG_TOKEN` | Telegram Bot API token |
|
||||||
|
| `MIMI_SECRET_FEISHU_APP_ID` | Feishu App ID |
|
||||||
|
| `MIMI_SECRET_FEISHU_APP_SECRET` | Feishu App Secret |
|
||||||
|
| `MIMI_SECRET_API_KEY` | Generic LLM API key (fallback) |
|
||||||
|
| `MIMI_SECRET_MODEL` | Model ID (default: claude-opus-4-5) |
|
||||||
|
| `MIMI_SECRET_MODEL_PROVIDER` | LLM provider: anthropic/openai/siliconflow/volcengine |
|
||||||
|
| `MIMI_SECRET_ANTHROPIC_API_KEY` | Anthropic-specific API key |
|
||||||
|
| `MIMI_SECRET_OPENAI_API_KEY` | OpenAI-specific API key |
|
||||||
|
| `MIMI_SECRET_SILICONFLOW_API_KEY` | SiliconFlow (硅基流动) API key |
|
||||||
|
| `MIMI_SECRET_SILICONFLOW_BASE_URL` | SiliconFlow Base URL |
|
||||||
|
| `MIMI_SECRET_VOLCENGINE_API_KEY` | Volcengine (火山引擎) API key |
|
||||||
|
| `MIMI_SECRET_VOLCENGINE_BASE_URL` | Volcengine Base URL |
|
||||||
|
| `MIMI_SECRET_PROXY_HOST` | HTTP proxy hostname/IP (optional) |
|
||||||
|
| `MIMI_SECRET_PROXY_PORT` | HTTP proxy port (optional) |
|
||||||
|
| `MIMI_SECRET_PROXY_TYPE` | Proxy type: http/socks5 |
|
||||||
|
| `MIMI_SECRET_SEARCH_KEY` | Brave Search API key (optional) |
|
||||||
|
| `MIMI_SECRET_TAVILY_KEY` | Tavily Search API key (optional) |
|
||||||
|
|
||||||
|
### Runtime (NVS + Onboard Portal)
|
||||||
|
Set via serial CLI or the onboard configuration portal (192.168.4.1).
|
||||||
|
|
||||||
|
| CLI Command | Description |
|
||||||
|
|------------------------------------|--------------------------------------|
|
||||||
|
| `wifi_set <SSID> <Password>` | Set WiFi credentials |
|
||||||
|
| `set_tg_token <Token>` | Set Telegram Bot token |
|
||||||
|
| `set_api_key <Key>` | Set generic LLM API key |
|
||||||
|
| `set_model_provider <Provider>` | Set provider: anthropic/openai/siliconflow/volcengine |
|
||||||
|
| `set_model <Model>` | Set model name |
|
||||||
|
| `set_siliconflow_key <Key>` | Set SiliconFlow-specific API key |
|
||||||
|
| `set_siliconflow_url <URL>` | Set SiliconFlow Base URL |
|
||||||
|
| `set_volcengine_key <Key>` | Set Volcengine-specific API key |
|
||||||
|
| `set_volcengine_url <URL>` | Set Volcengine Base URL |
|
||||||
|
| `config_show` | Show current config (masked) |
|
||||||
|
| `config_reset` | Reset to build-time defaults |
|
||||||
|
|
||||||
|
### Priority Order (highest → lowest)
|
||||||
|
1. NVS runtime config (CLI or onboard portal)
|
||||||
|
2. Provider-specific NVS key (e.g. `siliconflow_api_key`)
|
||||||
|
3. Provider-specific build-time config (e.g. `MIMI_SECRET_SILICONFLOW_API_KEY`)
|
||||||
|
4. Generic build-time config (`MIMI_SECRET_API_KEY`, `MIMI_SECRET_MODEL_PROVIDER`)
|
||||||
|
|
||||||
|
## Supported LLM Providers
|
||||||
|
|
||||||
|
| Provider | API Compatible | Default Endpoint |
|
||||||
|
|-------------|----------------|-------------------------------------------------------|
|
||||||
|
| anthropic | Anthropic | https://api.anthropic.com/v1/messages |
|
||||||
|
| openai | OpenAI | https://api.openai.com/v1/chat/completions |
|
||||||
|
| siliconflow | OpenAI | https://api.siliconflow.cn/v1/chat/completions |
|
||||||
|
| volcengine | OpenAI | https://ark.cn-beijing.volces.com/api/v3/chat/completions |
|
||||||
|
|
||||||
|
All OpenAI-compatible providers use Bearer token authentication and the same message format.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ idf_component_register(
|
|||||||
"tools/tool_cron.c"
|
"tools/tool_cron.c"
|
||||||
"tools/tool_web_search.c"
|
"tools/tool_web_search.c"
|
||||||
"tools/tool_get_time.c"
|
"tools/tool_get_time.c"
|
||||||
|
"tools/tool_set_timezone.c"
|
||||||
"tools/tool_files.c"
|
"tools/tool_files.c"
|
||||||
"tools/tool_gpio.c"
|
"tools/tool_gpio.c"
|
||||||
"tools/gpio_policy.c"
|
"tools/gpio_policy.c"
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ static size_t append_file(char *buf, size_t size, size_t offset, const char *pat
|
|||||||
FILE *f = fopen(path, "r");
|
FILE *f = fopen(path, "r");
|
||||||
if (!f) return offset;
|
if (!f) return offset;
|
||||||
|
|
||||||
if (header && offset < size - 1) {
|
if (offset >= size) return offset;
|
||||||
offset += snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
|
|
||||||
|
if (header) {
|
||||||
|
int ret = snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
|
||||||
|
if (ret > 0) offset += (size_t)ret;
|
||||||
|
if (offset >= size) { offset = size - 1; buf[offset] = '\0'; fclose(f); return offset; }
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t n = fread(buf + offset, 1, size - offset - 1, f);
|
size_t n = fread(buf + offset, 1, size - offset - 1, f);
|
||||||
@@ -79,23 +83,35 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
|
|||||||
/* Long-term memory */
|
/* Long-term memory */
|
||||||
char mem_buf[4096];
|
char mem_buf[4096];
|
||||||
if (memory_read_long_term(mem_buf, sizeof(mem_buf)) == ESP_OK && mem_buf[0]) {
|
if (memory_read_long_term(mem_buf, sizeof(mem_buf)) == ESP_OK && mem_buf[0]) {
|
||||||
off += snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
|
if (off < size) {
|
||||||
|
int ret = snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
|
||||||
|
if (ret > 0) off += (size_t)ret;
|
||||||
|
if (off >= size) off = size - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Recent daily notes (last 3 days) */
|
/* Recent daily notes (last 3 days) */
|
||||||
char recent_buf[4096];
|
char recent_buf[4096];
|
||||||
if (memory_read_recent(recent_buf, sizeof(recent_buf), 3) == ESP_OK && recent_buf[0]) {
|
if (memory_read_recent(recent_buf, sizeof(recent_buf), 3) == ESP_OK && recent_buf[0]) {
|
||||||
off += snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
|
if (off < size) {
|
||||||
|
int ret = snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
|
||||||
|
if (ret > 0) off += (size_t)ret;
|
||||||
|
if (off >= size) off = size - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Skills */
|
/* Skills */
|
||||||
char skills_buf[2048];
|
char skills_buf[2048];
|
||||||
size_t skills_len = skill_loader_build_summary(skills_buf, sizeof(skills_buf));
|
size_t skills_len = skill_loader_build_summary(skills_buf, sizeof(skills_buf));
|
||||||
if (skills_len > 0) {
|
if (skills_len > 0) {
|
||||||
off += snprintf(buf + off, size - off,
|
if (off < size) {
|
||||||
"\n## Available Skills\n\n"
|
int ret = snprintf(buf + off, size - off,
|
||||||
"Available skills (use read_file to load full instructions):\n%s\n",
|
"\n## Available Skills\n\n"
|
||||||
skills_buf);
|
"Available skills (use read_file to load full instructions):\n%s\n",
|
||||||
|
skills_buf);
|
||||||
|
if (ret > 0) off += (size_t)ret;
|
||||||
|
if (off >= size) off = size - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off);
|
ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off);
|
||||||
|
|||||||
@@ -583,6 +583,7 @@ static void feishu_ws_event_handler(void *arg, esp_event_base_t base, int32_t ev
|
|||||||
} else if (event_id == WEBSOCKET_EVENT_DISCONNECTED) {
|
} else if (event_id == WEBSOCKET_EVENT_DISCONNECTED) {
|
||||||
s_ws_connected = false;
|
s_ws_connected = false;
|
||||||
ESP_LOGW(TAG, "Feishu WS disconnected");
|
ESP_LOGW(TAG, "Feishu WS disconnected");
|
||||||
|
if (rx_buf) { free(rx_buf); rx_buf = NULL; rx_cap = 0; }
|
||||||
} else if (event_id == WEBSOCKET_EVENT_DATA) {
|
} else if (event_id == WEBSOCKET_EVENT_DATA) {
|
||||||
if (e->op_code != WS_TRANSPORT_OPCODES_BINARY) return;
|
if (e->op_code != WS_TRANSPORT_OPCODES_BINARY) return;
|
||||||
size_t need = e->payload_offset + e->data_len;
|
size_t need = e->payload_offset + e->data_len;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <time.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
@@ -654,6 +656,11 @@ static int cmd_config_show(int argc, char **argv)
|
|||||||
print_config_u16("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT);
|
print_config_u16("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT);
|
||||||
print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true);
|
print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true);
|
||||||
print_config("Tavily Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY, MIMI_SECRET_TAVILY_KEY, true);
|
print_config("Tavily Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY, MIMI_SECRET_TAVILY_KEY, true);
|
||||||
|
|
||||||
|
/* System configs */
|
||||||
|
printf(" --- System configs ---\n");
|
||||||
|
print_config("Timezone", "system_config", MIMI_NVS_KEY_TIMEZONE, MIMI_TIMEZONE, false);
|
||||||
|
|
||||||
printf("=============================\n");
|
printf("=============================\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -676,6 +683,87 @@ static int cmd_config_reset(int argc, char **argv)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- set_timezone command --- */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *tz;
|
||||||
|
struct arg_end *end;
|
||||||
|
} timezone_args;
|
||||||
|
|
||||||
|
static int cmd_set_timezone(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&timezone_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, timezone_args.end, argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *tz_str = timezone_args.tz->sval[0];
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
printf("Failed to open NVS: %s\n", esp_err_to_name(err));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
printf("Failed to save timezone: %s\n", esp_err_to_name(err));
|
||||||
|
nvs_close(nvs);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_commit(nvs);
|
||||||
|
nvs_close(nvs);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
printf("Failed to commit NVS: %s\n", esp_err_to_name(err));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setenv("TZ", tz_str, 1);
|
||||||
|
tzset();
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm tm_now;
|
||||||
|
localtime_r(&now, &tm_now);
|
||||||
|
char time_str[64];
|
||||||
|
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
|
||||||
|
|
||||||
|
printf("Timezone set to '%s'. Current time: %s. Restart to apply permanently.\n", tz_str, time_str);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- timezone_show command --- */
|
||||||
|
static int cmd_timezone_show(int argc, char **argv)
|
||||||
|
{
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
|
||||||
|
char nvs_val[64] = {0};
|
||||||
|
const char *source = "build";
|
||||||
|
const char *display = MIMI_TIMEZONE;
|
||||||
|
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) {
|
||||||
|
size_t len = sizeof(nvs_val);
|
||||||
|
if (nvs_get_str(nvs, MIMI_NVS_KEY_TIMEZONE, nvs_val, &len) == ESP_OK && nvs_val[0]) {
|
||||||
|
source = "NVS";
|
||||||
|
display = nvs_val;
|
||||||
|
}
|
||||||
|
nvs_close(nvs);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Current timezone: %s [%s]\n", display, source);
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm tm_now;
|
||||||
|
localtime_r(&now, &tm_now);
|
||||||
|
char time_str[64];
|
||||||
|
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z (%A)", &tm_now);
|
||||||
|
printf("Local time: %s\n", time_str);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- heartbeat_trigger command --- */
|
/* --- heartbeat_trigger command --- */
|
||||||
static int cmd_heartbeat_trigger(int argc, char **argv)
|
static int cmd_heartbeat_trigger(int argc, char **argv)
|
||||||
{
|
{
|
||||||
@@ -736,13 +824,22 @@ typedef struct {
|
|||||||
size_t output_size;
|
size_t output_size;
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
SemaphoreHandle_t done;
|
SemaphoreHandle_t done;
|
||||||
|
bool timed_out;
|
||||||
} web_search_task_ctx_t;
|
} web_search_task_ctx_t;
|
||||||
|
|
||||||
static void web_search_task(void *arg)
|
static void web_search_task(void *arg)
|
||||||
{
|
{
|
||||||
web_search_task_ctx_t *task_ctx = (web_search_task_ctx_t *)arg;
|
web_search_task_ctx_t *task_ctx = (web_search_task_ctx_t *)arg;
|
||||||
task_ctx->err = tool_web_search_execute(task_ctx->input_json, task_ctx->output, task_ctx->output_size);
|
task_ctx->err = tool_web_search_execute(task_ctx->input_json, task_ctx->output, task_ctx->output_size);
|
||||||
xSemaphoreGive(task_ctx->done);
|
if (!task_ctx->timed_out) {
|
||||||
|
xSemaphoreGive(task_ctx->done);
|
||||||
|
} else {
|
||||||
|
/* Main thread timed out and freed ctx, so we must clean up ourselves */
|
||||||
|
free((void *)task_ctx->input_json);
|
||||||
|
free(task_ctx->output);
|
||||||
|
vSemaphoreDelete(task_ctx->done);
|
||||||
|
free(task_ctx);
|
||||||
|
}
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,10 +935,8 @@ static int cmd_web_search(int argc, char **argv)
|
|||||||
|
|
||||||
if (xSemaphoreTake(ctx->done, pdMS_TO_TICKS(45000)) != pdTRUE) {
|
if (xSemaphoreTake(ctx->done, pdMS_TO_TICKS(45000)) != pdTRUE) {
|
||||||
printf("web_search status: timeout\n");
|
printf("web_search status: timeout\n");
|
||||||
vSemaphoreDelete(ctx->done);
|
ctx->timed_out = true;
|
||||||
free(input_copy);
|
/* Task will clean up ctx, input_copy, output, and done on its own */
|
||||||
free(ctx);
|
|
||||||
free(output);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
esp_err_t err = ctx->err;
|
esp_err_t err = ctx->err;
|
||||||
@@ -1163,6 +1258,25 @@ esp_err_t serial_cli_init(void)
|
|||||||
};
|
};
|
||||||
esp_console_cmd_register(&config_reset_cmd);
|
esp_console_cmd_register(&config_reset_cmd);
|
||||||
|
|
||||||
|
/* set_timezone */
|
||||||
|
timezone_args.tz = arg_str1(NULL, NULL, "<timezone>", "Timezone (e.g. CST-8, Asia/Shanghai)");
|
||||||
|
timezone_args.end = arg_end(1);
|
||||||
|
esp_console_cmd_t set_timezone_cmd = {
|
||||||
|
.command = "set_timezone",
|
||||||
|
.help = "Set system timezone (e.g. set_timezone CST-8)",
|
||||||
|
.func = &cmd_set_timezone,
|
||||||
|
.argtable = &timezone_args,
|
||||||
|
};
|
||||||
|
esp_console_cmd_register(&set_timezone_cmd);
|
||||||
|
|
||||||
|
/* timezone_show */
|
||||||
|
esp_console_cmd_t timezone_show_cmd = {
|
||||||
|
.command = "timezone_show",
|
||||||
|
.help = "Show current timezone and local time",
|
||||||
|
.func = &cmd_timezone_show,
|
||||||
|
};
|
||||||
|
esp_console_cmd_register(&timezone_show_cmd);
|
||||||
|
|
||||||
/* heartbeat_trigger */
|
/* heartbeat_trigger */
|
||||||
esp_console_cmd_t heartbeat_cmd = {
|
esp_console_cmd_t heartbeat_cmd = {
|
||||||
.command = "heartbeat_trigger",
|
.command = "heartbeat_trigger",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_http_client.h"
|
#include "esp_http_client.h"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_http_client.h"
|
#include "esp_http_client.h"
|
||||||
#include "esp_crt_bundle.h"
|
#include "esp_crt_bundle.h"
|
||||||
@@ -246,12 +247,25 @@ esp_err_t llm_proxy_init(void)
|
|||||||
nvs_close(nvs);
|
nvs_close(nvs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Load provider-specific API key if available */
|
/* Load provider-specific API key from NVS */
|
||||||
const char *provider_api_key = llm_provider_get_api_key(s_provider);
|
const char *provider_api_key = llm_provider_get_api_key(s_provider);
|
||||||
if (provider_api_key && provider_api_key[0]) {
|
if (provider_api_key && provider_api_key[0]) {
|
||||||
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
|
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fall back to build-time provider-specific API key if NVS key is empty */
|
||||||
|
if (s_api_key[0] == '\0') {
|
||||||
|
if (strcmp(s_provider, "siliconflow") == 0 && MIMI_SECRET_SILICONFLOW_API_KEY[0] != '\0') {
|
||||||
|
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_SILICONFLOW_API_KEY);
|
||||||
|
} else if (strcmp(s_provider, "volcengine") == 0 && MIMI_SECRET_VOLCENGINE_API_KEY[0] != '\0') {
|
||||||
|
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_VOLCENGINE_API_KEY);
|
||||||
|
} else if (strcmp(s_provider, "openai") == 0 && MIMI_SECRET_OPENAI_API_KEY[0] != '\0') {
|
||||||
|
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_OPENAI_API_KEY);
|
||||||
|
} else if (strcmp(s_provider, "anthropic") == 0 && MIMI_SECRET_ANTHROPIC_API_KEY[0] != '\0') {
|
||||||
|
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_ANTHROPIC_API_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (s_api_key[0]) {
|
if (s_api_key[0]) {
|
||||||
ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model);
|
ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ esp_err_t session_mgr_init(void)
|
|||||||
|
|
||||||
esp_err_t session_append(const char *chat_id, const char *role, const char *content)
|
esp_err_t session_append(const char *chat_id, const char *role, const char *content)
|
||||||
{
|
{
|
||||||
char path[64];
|
char path[128];
|
||||||
session_path(chat_id, path, sizeof(path));
|
session_path(chat_id, path, sizeof(path));
|
||||||
|
|
||||||
FILE *f = fopen(path, "a");
|
FILE *f = fopen(path, "a");
|
||||||
@@ -52,7 +52,7 @@ esp_err_t session_append(const char *chat_id, const char *role, const char *cont
|
|||||||
|
|
||||||
esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs)
|
esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs)
|
||||||
{
|
{
|
||||||
char path[64];
|
char path[128];
|
||||||
session_path(chat_id, path, sizeof(path));
|
session_path(chat_id, path, sizeof(path));
|
||||||
|
|
||||||
FILE *f = fopen(path, "r");
|
FILE *f = fopen(path, "r");
|
||||||
@@ -127,7 +127,7 @@ esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size,
|
|||||||
|
|
||||||
esp_err_t session_clear(const char *chat_id)
|
esp_err_t session_clear(const char *chat_id)
|
||||||
{
|
{
|
||||||
char path[64];
|
char path[128];
|
||||||
session_path(chat_id, path, sizeof(path));
|
session_path(chat_id, path, sizeof(path));
|
||||||
|
|
||||||
if (remove(path) == 0) {
|
if (remove(path) == 0) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
|||||||
@@ -47,6 +47,26 @@
|
|||||||
#define MIMI_SECRET_TAVILY_KEY ""
|
#define MIMI_SECRET_TAVILY_KEY ""
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* Provider-specific API keys (fallback when generic MIMI_SECRET_API_KEY is empty) */
|
||||||
|
#ifndef MIMI_SECRET_ANTHROPIC_API_KEY
|
||||||
|
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
|
||||||
|
#endif
|
||||||
|
#ifndef MIMI_SECRET_OPENAI_API_KEY
|
||||||
|
#define MIMI_SECRET_OPENAI_API_KEY ""
|
||||||
|
#endif
|
||||||
|
#ifndef MIMI_SECRET_SILICONFLOW_API_KEY
|
||||||
|
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
|
||||||
|
#endif
|
||||||
|
#ifndef MIMI_SECRET_SILICONFLOW_BASE_URL
|
||||||
|
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
|
||||||
|
#endif
|
||||||
|
#ifndef MIMI_SECRET_VOLCENGINE_API_KEY
|
||||||
|
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
|
||||||
|
#endif
|
||||||
|
#ifndef MIMI_SECRET_VOLCENGINE_BASE_URL
|
||||||
|
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
|
||||||
|
#endif
|
||||||
|
|
||||||
/* WiFi */
|
/* WiFi */
|
||||||
#define MIMI_WIFI_MAX_RETRY 10
|
#define MIMI_WIFI_MAX_RETRY 10
|
||||||
#define MIMI_WIFI_RETRY_BASE_MS 1000
|
#define MIMI_WIFI_RETRY_BASE_MS 1000
|
||||||
@@ -80,7 +100,7 @@
|
|||||||
#define MIMI_AGENT_SEND_WORKING_STATUS 1
|
#define MIMI_AGENT_SEND_WORKING_STATUS 1
|
||||||
|
|
||||||
/* Timezone (POSIX TZ format) */
|
/* Timezone (POSIX TZ format) */
|
||||||
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
|
#define MIMI_TIMEZONE "CST-8"
|
||||||
|
|
||||||
/* LLM */
|
/* LLM */
|
||||||
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5"
|
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5"
|
||||||
@@ -166,6 +186,9 @@
|
|||||||
#define MIMI_NVS_KEY_VOLCENGINE_API_KEY "volcengine_api_key"
|
#define MIMI_NVS_KEY_VOLCENGINE_API_KEY "volcengine_api_key"
|
||||||
#define MIMI_NVS_KEY_VOLCENGINE_BASE_URL "volcengine_base_url"
|
#define MIMI_NVS_KEY_VOLCENGINE_BASE_URL "volcengine_base_url"
|
||||||
|
|
||||||
|
/* System NVS Keys */
|
||||||
|
#define MIMI_NVS_KEY_TIMEZONE "timezone"
|
||||||
|
|
||||||
/* WiFi Onboarding (Captive Portal) */
|
/* WiFi Onboarding (Captive Portal) */
|
||||||
#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"
|
#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"
|
||||||
#define MIMI_ONBOARD_AP_PASS "" /* open network */
|
#define MIMI_ONBOARD_AP_PASS "" /* open network */
|
||||||
|
|||||||
@@ -21,16 +21,32 @@
|
|||||||
#define MIMI_SECRET_FEISHU_APP_ID ""
|
#define MIMI_SECRET_FEISHU_APP_ID ""
|
||||||
#define MIMI_SECRET_FEISHU_APP_SECRET ""
|
#define MIMI_SECRET_FEISHU_APP_SECRET ""
|
||||||
|
|
||||||
/* Anthropic API */
|
/* LLM Configuration
|
||||||
|
*
|
||||||
|
* Method 1: Generic API key (works for any provider)
|
||||||
|
* Set MIMI_SECRET_API_KEY + MIMI_SECRET_MODEL_PROVIDER + MIMI_SECRET_MODEL
|
||||||
|
*
|
||||||
|
* Method 2: Provider-specific API keys (recommended for multi-provider setups)
|
||||||
|
* Set MIMI_SECRET_<PROVIDER>_API_KEY for each provider you want to use
|
||||||
|
* Switch providers via onboard portal or CLI: set_model_provider <provider>
|
||||||
|
*
|
||||||
|
* Priority: NVS (runtime config) > provider-specific key > generic key
|
||||||
|
*/
|
||||||
#define MIMI_SECRET_API_KEY ""
|
#define MIMI_SECRET_API_KEY ""
|
||||||
#define MIMI_SECRET_MODEL ""
|
#define MIMI_SECRET_MODEL "claude-opus-4-5"
|
||||||
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
|
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
|
||||||
|
|
||||||
/* SiliconFlow API (OpenAI-compatible) */
|
/* Anthropic (Claude) */
|
||||||
|
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
|
||||||
|
|
||||||
|
/* OpenAI (GPT) */
|
||||||
|
#define MIMI_SECRET_OPENAI_API_KEY ""
|
||||||
|
|
||||||
|
/* SiliconFlow (硅基流动, OpenAI-compatible) */
|
||||||
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
|
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
|
||||||
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
|
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
|
||||||
|
|
||||||
/* Volcengine API (OpenAI-compatible, ByteDance Doubao models) */
|
/* Volcengine (火山引擎/豆包, OpenAI-compatible) */
|
||||||
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
|
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
|
||||||
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
|
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,14 @@ static const char ONBOARD_HTML[] =
|
|||||||
"<label>Model</label>"
|
"<label>Model</label>"
|
||||||
"<input id='model' placeholder='claude-opus-4-5' value='claude-opus-4-5'>"
|
"<input id='model' placeholder='claude-opus-4-5' value='claude-opus-4-5'>"
|
||||||
"<label>Provider</label>"
|
"<label>Provider</label>"
|
||||||
"<select id='provider'>"
|
"<select id='provider' onchange='onProviderChange()'>"
|
||||||
"<option value='anthropic'>Anthropic</option>"
|
"<option value='anthropic'>Anthropic</option>"
|
||||||
"<option value='openai'>OpenAI</option>"
|
"<option value='openai'>OpenAI</option>"
|
||||||
|
"<option value='siliconflow'>SiliconFlow (硅基流动)</option>"
|
||||||
|
"<option value='volcengine'>Volcengine (火山引擎)</option>"
|
||||||
"</select>"
|
"</select>"
|
||||||
|
"<label>Base URL</label>"
|
||||||
|
"<input id='base_url' placeholder='https://api.example.com/v1'>"
|
||||||
"</div></div>"
|
"</div></div>"
|
||||||
|
|
||||||
/* Telegram section */
|
/* Telegram section */
|
||||||
@@ -133,8 +137,15 @@ static const char ONBOARD_HTML[] =
|
|||||||
"btn.textContent='Scan WiFi Networks';btn.disabled=false;"
|
"btn.textContent='Scan WiFi Networks';btn.disabled=false;"
|
||||||
"}).catch(()=>{btn.textContent='Scan WiFi Networks';btn.disabled=false})}"
|
"}).catch(()=>{btn.textContent='Scan WiFi Networks';btn.disabled=false})}"
|
||||||
|
|
||||||
|
"function onProviderChange(){"
|
||||||
|
"var p=document.getElementById('provider').value;"
|
||||||
|
"var u=document.getElementById('base_url');"
|
||||||
|
"if(p==='siliconflow'){u.value='https://api.siliconflow.cn/v1'}"
|
||||||
|
"else if(p==='volcengine'){u.value='https://ark.cn-beijing.volces.com/api/v3'}"
|
||||||
|
"else{u.value=''}}"
|
||||||
|
|
||||||
"function save(){"
|
"function save(){"
|
||||||
"var fields=['ssid','password','api_key','model','provider','tg_token',"
|
"var fields=['ssid','password','api_key','model','provider','base_url','tg_token',"
|
||||||
"'feishu_app_id','feishu_app_secret','proxy_host','proxy_port','proxy_type','search_key','tavily_key'];"
|
"'feishu_app_id','feishu_app_secret','proxy_host','proxy_port','proxy_type','search_key','tavily_key'];"
|
||||||
"var data={};"
|
"var data={};"
|
||||||
"fields.forEach(f=>{data[f]=document.getElementById(f).value.trim()});"
|
"fields.forEach(f=>{data[f]=document.getElementById(f).value.trim()});"
|
||||||
|
|||||||
@@ -220,6 +220,40 @@ static esp_err_t http_get_config(httpd_req_t *req)
|
|||||||
json_add_effective_config(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY);
|
json_add_effective_config(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY);
|
||||||
json_add_effective_config(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL);
|
json_add_effective_config(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL);
|
||||||
json_add_effective_config(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER);
|
json_add_effective_config(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER);
|
||||||
|
|
||||||
|
/* Provider-specific Base URL (load from current provider's NVS key) */
|
||||||
|
{
|
||||||
|
char base_url[256] = {0};
|
||||||
|
bool found = false;
|
||||||
|
char provider[32] = {0};
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
|
||||||
|
size_t len = sizeof(provider);
|
||||||
|
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider, &len) != ESP_OK || !provider[0]) {
|
||||||
|
/* Fall back to build-time provider if NVS doesn't have one */
|
||||||
|
strlcpy(provider, MIMI_SECRET_MODEL_PROVIDER, sizeof(provider));
|
||||||
|
}
|
||||||
|
/* Try to load provider-specific base URL from NVS */
|
||||||
|
const char *url_key = NULL;
|
||||||
|
if (strcmp(provider, "anthropic") == 0) url_key = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
|
||||||
|
else if (strcmp(provider, "openai") == 0) url_key = MIMI_NVS_KEY_OPENAI_BASE_URL;
|
||||||
|
else if (strcmp(provider, "siliconflow") == 0) url_key = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
|
||||||
|
else if (strcmp(provider, "volcengine") == 0) url_key = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
|
||||||
|
if (url_key) {
|
||||||
|
len = sizeof(base_url);
|
||||||
|
if (nvs_get_str(nvs, url_key, base_url, &len) == ESP_OK && base_url[0]) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nvs_close(nvs);
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
/* Fall back to build-time defaults */
|
||||||
|
if (strcmp(provider, "siliconflow") == 0) strlcpy(base_url, MIMI_SECRET_SILICONFLOW_BASE_URL, sizeof(base_url));
|
||||||
|
else if (strcmp(provider, "volcengine") == 0) strlcpy(base_url, MIMI_SECRET_VOLCENGINE_BASE_URL, sizeof(base_url));
|
||||||
|
}
|
||||||
|
cJSON_AddStringToObject(root, "base_url", base_url);
|
||||||
|
}
|
||||||
json_add_effective_config(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN, MIMI_SECRET_TG_TOKEN);
|
json_add_effective_config(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN, MIMI_SECRET_TG_TOKEN);
|
||||||
json_add_effective_config(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID, MIMI_SECRET_FEISHU_APP_ID);
|
json_add_effective_config(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID, MIMI_SECRET_FEISHU_APP_ID);
|
||||||
json_add_effective_config(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET, MIMI_SECRET_FEISHU_APP_SECRET);
|
json_add_effective_config(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET, MIMI_SECRET_FEISHU_APP_SECRET);
|
||||||
@@ -344,6 +378,37 @@ static esp_err_t http_post_save(httpd_req_t *req)
|
|||||||
nvs_sync_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL);
|
nvs_sync_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL);
|
||||||
nvs_sync_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER);
|
nvs_sync_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER);
|
||||||
|
|
||||||
|
/* Save API key and base URL to provider-specific NVS keys */
|
||||||
|
{
|
||||||
|
cJSON *provider_item = cJSON_GetObjectItem(root, "provider");
|
||||||
|
cJSON *api_key_item = cJSON_GetObjectItem(root, "api_key");
|
||||||
|
cJSON *base_url_item = cJSON_GetObjectItem(root, "base_url");
|
||||||
|
if (provider_item && cJSON_IsString(provider_item) && provider_item->valuestring[0]) {
|
||||||
|
const char *provider = provider_item->valuestring;
|
||||||
|
const char *api_key_nvs = NULL;
|
||||||
|
const char *base_url_nvs = NULL;
|
||||||
|
if (strcmp(provider, "anthropic") == 0) {
|
||||||
|
api_key_nvs = MIMI_NVS_KEY_ANTHROPIC_API_KEY;
|
||||||
|
base_url_nvs = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
|
||||||
|
} else if (strcmp(provider, "openai") == 0) {
|
||||||
|
api_key_nvs = MIMI_NVS_KEY_OPENAI_API_KEY;
|
||||||
|
base_url_nvs = MIMI_NVS_KEY_OPENAI_BASE_URL;
|
||||||
|
} else if (strcmp(provider, "siliconflow") == 0) {
|
||||||
|
api_key_nvs = MIMI_NVS_KEY_SILICONFLOW_API_KEY;
|
||||||
|
base_url_nvs = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
|
||||||
|
} else if (strcmp(provider, "volcengine") == 0) {
|
||||||
|
api_key_nvs = MIMI_NVS_KEY_VOLCENGINE_API_KEY;
|
||||||
|
base_url_nvs = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
|
||||||
|
}
|
||||||
|
if (api_key_nvs && api_key_item && cJSON_IsString(api_key_item)) {
|
||||||
|
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, api_key_nvs);
|
||||||
|
}
|
||||||
|
if (base_url_nvs && base_url_item && cJSON_IsString(base_url_item)) {
|
||||||
|
nvs_sync_field(root, "base_url", MIMI_NVS_LLM, base_url_nvs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Telegram */
|
/* Telegram */
|
||||||
nvs_sync_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN);
|
nvs_sync_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
@@ -246,7 +247,7 @@ static int open_socks5_tunnel(const char *host, int port, int timeout_ms)
|
|||||||
free(req);
|
free(req);
|
||||||
|
|
||||||
/* Receive connect response */
|
/* Receive connect response */
|
||||||
unsigned char resp[256];
|
unsigned char resp[512];
|
||||||
int resp_len = recv(sock, resp, sizeof(resp), 0);
|
int resp_len = recv(sock, resp, sizeof(resp), 0);
|
||||||
if (resp_len < 10) {
|
if (resp_len < 10) {
|
||||||
ESP_LOGE(TAG, "No response from SOCKS5 proxy"); close(sock); return -1;
|
ESP_LOGE(TAG, "No response from SOCKS5 proxy"); close(sock); return -1;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#ifndef GPIO_IS_VALID_GPIO
|
#ifndef GPIO_IS_VALID_GPIO
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "mimi_config.h"
|
#include "mimi_config.h"
|
||||||
#include "tools/tool_web_search.h"
|
#include "tools/tool_web_search.h"
|
||||||
#include "tools/tool_get_time.h"
|
#include "tools/tool_get_time.h"
|
||||||
|
#include "tools/tool_set_timezone.h"
|
||||||
#include "tools/tool_files.h"
|
#include "tools/tool_files.h"
|
||||||
#include "tools/tool_cron.h"
|
#include "tools/tool_cron.h"
|
||||||
#include "tools/tool_gpio.h"
|
#include "tools/tool_gpio.h"
|
||||||
@@ -83,6 +84,18 @@ esp_err_t tool_registry_init(void)
|
|||||||
};
|
};
|
||||||
register_tool(>);
|
register_tool(>);
|
||||||
|
|
||||||
|
/* Register set_timezone */
|
||||||
|
mimi_tool_t stz = {
|
||||||
|
.name = "set_timezone",
|
||||||
|
.description = "Set the system timezone. Accepts POSIX format (e.g. CST-8, EST5EDT,M3.2.0,M11.1.0) or city name (e.g. Asia/Shanghai, America/New_York).",
|
||||||
|
.input_schema_json =
|
||||||
|
"{\"type\":\"object\","
|
||||||
|
"\"properties\":{\"timezone\":{\"type\":\"string\",\"description\":\"Timezone in POSIX format or city name (e.g. CST-8, Asia/Shanghai)\"}},"
|
||||||
|
"\"required\":[\"timezone\"]}",
|
||||||
|
.execute = tool_set_timezone_execute,
|
||||||
|
};
|
||||||
|
register_tool(&stz);
|
||||||
|
|
||||||
/* Register read_file */
|
/* Register read_file */
|
||||||
mimi_tool_t rf = {
|
mimi_tool_t rf = {
|
||||||
.name = "read_file",
|
.name = "read_file",
|
||||||
|
|||||||
145
main/tools/tool_set_timezone.c
Normal file
145
main/tools/tool_set_timezone.c
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#include "tool_set_timezone.h"
|
||||||
|
#include "mimi_config.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
|
||||||
|
static const char *TAG = "tool_timezone";
|
||||||
|
|
||||||
|
/* Common timezone mappings for user-friendly names */
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
const char *posix_tz;
|
||||||
|
} tz_mapping_t;
|
||||||
|
|
||||||
|
static const tz_mapping_t tz_mappings[] = {
|
||||||
|
{ "Asia/Shanghai", "CST-8" },
|
||||||
|
{ "Asia/Beijing", "CST-8" },
|
||||||
|
{ "Asia/Hong_Kong", "HKT-8" },
|
||||||
|
{ "Asia/Tokyo", "JST-9" },
|
||||||
|
{ "Asia/Seoul", "KST-9" },
|
||||||
|
{ "Asia/Singapore", "SGT-8" },
|
||||||
|
{ "Asia/Kolkata", "IST-5:30" },
|
||||||
|
{ "Asia/Dubai", "GST-4" },
|
||||||
|
{ "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" },
|
||||||
|
{ "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" },
|
||||||
|
{ "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" },
|
||||||
|
{ "America/New_York", "EST5EDT,M3.2.0,M11.1.0" },
|
||||||
|
{ "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" },
|
||||||
|
{ "America/Denver", "MST7MDT,M3.2.0,M11.1.0" },
|
||||||
|
{ "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" },
|
||||||
|
{ "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" },
|
||||||
|
{ "UTC", "UTC0" },
|
||||||
|
{ "GMT", "GMT0" },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *resolve_timezone(const char *tz_str)
|
||||||
|
{
|
||||||
|
if (!tz_str || !tz_str[0]) return NULL;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
|
||||||
|
if (strcmp(tz_str, tz_mappings[i].name) == 0) {
|
||||||
|
return tz_mappings[i].posix_tz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tz_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool validate_timezone(const char *tz_str)
|
||||||
|
{
|
||||||
|
if (!tz_str || !tz_str[0]) return false;
|
||||||
|
if (strlen(tz_str) > 64) return false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
|
||||||
|
if (strcmp(tz_str, tz_mappings[i].name) == 0) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strchr(tz_str, '+') || strchr(tz_str, '-') ||
|
||||||
|
strcmp(tz_str, "UTC0") == 0 || strcmp(tz_str, "GMT0") == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t save_timezone_nvs(const char *tz_str)
|
||||||
|
{
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to save timezone: %s", esp_err_to_name(err));
|
||||||
|
nvs_close(nvs);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_commit(nvs);
|
||||||
|
nvs_close(nvs);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Setting timezone...");
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(input_json);
|
||||||
|
if (!root) {
|
||||||
|
snprintf(output, output_size, "Error: invalid JSON input");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *tz_item = cJSON_GetObjectItem(root, "timezone");
|
||||||
|
if (!tz_item || !cJSON_IsString(tz_item)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
snprintf(output, output_size, "Error: 'timezone' field required (string)");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *input_tz = tz_item->valuestring;
|
||||||
|
const char *resolved_tz = resolve_timezone(input_tz);
|
||||||
|
|
||||||
|
if (!resolved_tz || !validate_timezone(resolved_tz)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
snprintf(output, output_size, "Error: invalid timezone format '%s'. Use POSIX format (e.g. CST-8) or city name (e.g. Asia/Shanghai)", input_tz);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = save_timezone_nvs(resolved_tz);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
snprintf(output, output_size, "Error: failed to save timezone (%s)", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
setenv("TZ", resolved_tz, 1);
|
||||||
|
tzset();
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm tm_now;
|
||||||
|
localtime_r(&now, &tm_now);
|
||||||
|
char time_str[64];
|
||||||
|
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
snprintf(output, output_size, "Timezone set to '%s'. Current time: %s", resolved_tz, time_str);
|
||||||
|
ESP_LOGI(TAG, "Timezone set to: %s, current time: %s", resolved_tz, time_str);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
11
main/tools/tool_set_timezone.h
Normal file
11
main/tools/tool_set_timezone.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute set_timezone tool.
|
||||||
|
* Sets the system timezone via NVS and updates the TZ environment variable.
|
||||||
|
* Input JSON: {"timezone": "CST-8"} or {"timezone": "Asia/Shanghai"}
|
||||||
|
*/
|
||||||
|
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size);
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "mimi_config.h"
|
#include "mimi_config.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "freertos/event_groups.h"
|
#include "freertos/event_groups.h"
|
||||||
|
#include "freertos/timers.h"
|
||||||
|
|
||||||
static const char *TAG = "wifi";
|
static const char *TAG = "wifi";
|
||||||
|
|
||||||
@@ -20,6 +22,15 @@ static int s_retry_count = 0;
|
|||||||
static char s_ip_str[16] = "0.0.0.0";
|
static char s_ip_str[16] = "0.0.0.0";
|
||||||
static bool s_connected = false;
|
static bool s_connected = false;
|
||||||
static bool s_reconnect_enabled = true;
|
static bool s_reconnect_enabled = true;
|
||||||
|
static TimerHandle_t s_retry_timer = NULL;
|
||||||
|
|
||||||
|
static void retry_timer_callback(TimerHandle_t xTimer)
|
||||||
|
{
|
||||||
|
(void)xTimer;
|
||||||
|
if (s_reconnect_enabled && !s_connected) {
|
||||||
|
esp_wifi_connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static const char *wifi_reason_to_str(wifi_err_reason_t reason)
|
static const char *wifi_reason_to_str(wifi_err_reason_t reason)
|
||||||
{
|
{
|
||||||
@@ -77,9 +88,10 @@ static void event_handler(void *arg, esp_event_base_t event_base,
|
|||||||
}
|
}
|
||||||
ESP_LOGW(TAG, "Disconnected, retry %d/%d in %" PRIu32 "ms",
|
ESP_LOGW(TAG, "Disconnected, retry %d/%d in %" PRIu32 "ms",
|
||||||
s_retry_count + 1, MIMI_WIFI_MAX_RETRY, delay_ms);
|
s_retry_count + 1, MIMI_WIFI_MAX_RETRY, delay_ms);
|
||||||
vTaskDelay(pdMS_TO_TICKS(delay_ms));
|
|
||||||
esp_wifi_connect();
|
|
||||||
s_retry_count++;
|
s_retry_count++;
|
||||||
|
/* Use timer instead of blocking vTaskDelay in event handler */
|
||||||
|
if (s_retry_timer) xTimerStop(s_retry_timer, 0);
|
||||||
|
xTimerChangePeriod(s_retry_timer, pdMS_TO_TICKS(delay_ms), 0);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "Failed to connect after %d retries", MIMI_WIFI_MAX_RETRY);
|
ESP_LOGE(TAG, "Failed to connect after %d retries", MIMI_WIFI_MAX_RETRY);
|
||||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||||
@@ -98,6 +110,7 @@ static void event_handler(void *arg, esp_event_base_t event_base,
|
|||||||
esp_err_t wifi_manager_init(void)
|
esp_err_t wifi_manager_init(void)
|
||||||
{
|
{
|
||||||
s_wifi_event_group = xEventGroupCreate();
|
s_wifi_event_group = xEventGroupCreate();
|
||||||
|
s_retry_timer = xTimerCreate("wifi_retry", pdMS_TO_TICKS(1000), pdFALSE, NULL, retry_timer_callback);
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_netif_init());
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
esp_netif_create_default_wifi_sta();
|
esp_netif_create_default_wifi_sta();
|
||||||
|
|||||||
44
taolun.md
44
taolun.md
@@ -80,3 +80,47 @@ idf.py -p COMx flash monitor
|
|||||||
1. 认证方式差异确认
|
1. 认证方式差异确认
|
||||||
2. 模型名称规范
|
2. 模型名称规范
|
||||||
3. 工具调用格式兼容性验证
|
3. 工具调用格式兼容性验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 讨论:时区设置功能
|
||||||
|
|
||||||
|
**日期**:2026-04-01
|
||||||
|
**目标**:为 MimiClaw 添加可配置的时区支持,默认改为中国时区
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
- 原默认时区为 `PST8PDT,M3.2.0,M11.1.0`(太平洋时间)
|
||||||
|
- 需要支持用户自定义时区,特别是中国用户(UTC+8)
|
||||||
|
- 交互方式从 Telegram 改为飞书
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
|
||||||
|
#### 存储方式
|
||||||
|
- **NVS 存储**:使用 `system_config` namespace,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` / `timezone_show` 命令 |
|
||||||
|
| `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 个预设
|
||||||
|
|||||||
Reference in New Issue
Block a user