feat: 实现时间同步、NVS稳定性修复和ESP-IDF v6.0兼容性改进

This commit is contained in:
2026-04-01 18:58:20 +08:00
parent 540bfe825f
commit 9815ab8df0
17 changed files with 942 additions and 31 deletions

View File

@@ -27,6 +27,8 @@ idf_component_register(
"skills/skill_loader.c"
"onboard/wifi_onboard.c"
"ota/ota_manager.c"
"time_sync/time_sync.c"
"nvs_safety/nvs_safety.c"
INCLUDE_DIRS
"."
REQUIRES

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;
}
@@ -1269,13 +1332,32 @@ esp_err_t serial_cli_init(void)
};
esp_console_cmd_register(&set_timezone_cmd);
/* timezone_show */
esp_console_cmd_t timezone_show_cmd = {
.command = "timezone_show",
.help = "Show current timezone and local time",
.func = &cmd_timezone_show,
/* ntp_status */
esp_console_cmd_t ntp_status_cmd = {
.command = "ntp_status",
.help = "Show timezone, local time, NTP sync status, server and last sync time",
.func = &cmd_ntp_status,
};
esp_console_cmd_register(&timezone_show_cmd);
esp_console_cmd_register(&ntp_status_cmd);
/* ntp_sync */
esp_console_cmd_t ntp_sync_cmd = {
.command = "ntp_sync",
.help = "Manually trigger NTP time synchronization",
.func = &cmd_ntp_sync,
};
esp_console_cmd_register(&ntp_sync_cmd);
/* ntp_set */
ntp_set_arguments.server = arg_str1(NULL, NULL, "<server>", "NTP server hostname");
ntp_set_arguments.end = arg_end(1);
esp_console_cmd_t ntp_set_cmd = {
.command = "ntp_set",
.help = "Set custom NTP server (e.g. ntp_set ntp.ntsc.ac.cn)",
.func = &cmd_ntp_set,
.argtable = &ntp_set_arguments,
};
esp_console_cmd_register(&ntp_set_cmd);
/* heartbeat_trigger */
esp_console_cmd_t heartbeat_cmd = {

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";
@@ -120,6 +122,7 @@ void app_main(void)
/* Phase 1: Core infrastructure */
ESP_ERROR_CHECK(init_nvs());
nvs_safety_check();
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(init_spiffs());
@@ -151,6 +154,7 @@ void app_main(void)
if (wifi_manager_wait_connected(30000) == ESP_OK) {
wifi_ok = true;
ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip());
time_sync_init();
} else {
ESP_LOGW(TAG, "WiFi connection timeout");
}

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

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

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