diff --git a/application/basic_demo/main/app_claw.c b/application/basic_demo/main/app_claw.c index 1f35adc..d4c8049 100644 --- a/application/basic_demo/main/app_claw.c +++ b/application/basic_demo/main/app_claw.c @@ -408,6 +408,7 @@ esp_err_t app_claw_start(const basic_demo_settings_t *settings) ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_skill_active_skill_docs_provider), TAG, "Failed to add active skill docs provider"); ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_cap_tools_provider), TAG, "Failed to add cap tools provider"); ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&cap_lua_async_jobs_provider), TAG, "Failed to add Lua async jobs provider"); + ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&cap_time_context_provider), TAG, "Failed to add time context provider"); ESP_RETURN_ON_ERROR(claw_core_add_completion_observer(cap_lua_honesty_observe_completion, NULL), TAG, "Failed to install Lua honesty observer"); ESP_RETURN_ON_ERROR(claw_core_start(), TAG, "Failed to start claw_core"); diff --git a/application/basic_demo/sdkconfig.defaults b/application/basic_demo/sdkconfig.defaults index 94d3255..604e4f9 100644 --- a/application/basic_demo/sdkconfig.defaults +++ b/application/basic_demo/sdkconfig.defaults @@ -29,3 +29,4 @@ CONFIG_MMAP_FILE_NAME_LENGTH=32 CONFIG_SPIRAM_XIP_FROM_PSRAM=y CONFIG_EMOTE_DEF_BG_COLOR=0x171617 CONFIG_CLAW_CORE_STAGE_VERBOSITY_VERBOSE=y +CONFIG_LWIP_SNTP_MAX_SERVERS=2 diff --git a/components/claw_capabilities/cap_time/CMakeLists.txt b/components/claw_capabilities/cap_time/CMakeLists.txt index 37d9215..3a1027d 100644 --- a/components/claw_capabilities/cap_time/CMakeLists.txt +++ b/components/claw_capabilities/cap_time/CMakeLists.txt @@ -6,6 +6,7 @@ idf_component_register( "include" REQUIRES claw_cap - esp_http_client + claw_core + esp_netif console ) diff --git a/components/claw_capabilities/cap_time/include/cap_time.h b/components/claw_capabilities/cap_time/include/cap_time.h index 417a51d..f8f7aa3 100644 --- a/components/claw_capabilities/cap_time/include/cap_time.h +++ b/components/claw_capabilities/cap_time/include/cap_time.h @@ -9,6 +9,7 @@ #include #include +#include "claw_core.h" #include "esp_err.h" #ifdef __cplusplus @@ -30,11 +31,14 @@ typedef struct { esp_err_t cap_time_register_group(void); // TODO: improve the workflow of cap_scheduler skill esp_err_t cap_time_get_timezone(char *timezone, size_t timezone_size); +esp_err_t cap_time_get_current(char *output, size_t output_size); esp_err_t cap_time_sync_now(char *output, size_t output_size); bool cap_time_is_valid(void); esp_err_t cap_time_sync_service_start(const cap_time_sync_service_config_t *config); esp_err_t cap_time_sync_service_stop(void); +extern const claw_core_context_provider_t cap_time_context_provider; + #ifdef __cplusplus } #endif diff --git a/components/claw_capabilities/cap_time/src/cap_time.c b/components/claw_capabilities/cap_time/src/cap_time.c index c1b85b2..0cb4638 100644 --- a/components/claw_capabilities/cap_time/src/cap_time.c +++ b/components/claw_capabilities/cap_time/src/cap_time.c @@ -3,45 +3,33 @@ * * SPDX-License-Identifier: Apache-2.0 */ -#include "cap_time.h" - #include #include #include #include -#include #include - -#include "claw_cap.h" -#include "claw_task.h" -#include "esp_check.h" -#include "esp_crt_bundle.h" -#include "esp_http_client.h" -#include "esp_log.h" +#include #include "freertos/FreeRTOS.h" -#include "freertos/idf_additions.h" #include "freertos/semphr.h" #include "freertos/task.h" +#include "esp_check.h" +#include "esp_log.h" +#include "esp_netif_sntp.h" +#include "claw_cap.h" +#include "claw_task.h" +#include "cap_time.h" + static const char *TAG = "cap_time"; -#define CAP_TIME_SOURCE_URL "https://api.telegram.org/" -#define CAP_TIME_HTTP_TIMEOUT_MS 10000 +#define CAP_TIME_SNTP_PRIMARY_SERVER "pool.ntp.org" +#define CAP_TIME_SNTP_SECONDARY_SERVER "time.windows.com" +#define CAP_TIME_SNTP_WAIT_MS 3000 +#define CAP_TIME_SNTP_RETRY_COUNT 15 #define CAP_TIME_MIN_VALID_EPOCH 1704067200 -#define CAP_TIME_UTC_TIMEZONE "UTC0" #define CAP_TIME_DEFAULT_DISCONNECTED_RETRY_MS 5000 #define CAP_TIME_DEFAULT_SYNC_RETRY_MS 30000 -static const char *s_month_names[] = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", -}; - -typedef struct { - char date_header[64]; -} cap_time_state_t; - -static cap_time_state_t s_time = {0}; static SemaphoreHandle_t s_time_mutex = NULL; static struct { TaskHandle_t task_handle; @@ -57,23 +45,6 @@ static esp_err_t cap_time_ensure_mutex(void) return s_time_mutex ? ESP_OK : ESP_ERR_NO_MEM; } -static int cap_time_month_to_index(const char *month) -{ - size_t i; - - if (!month) { - return -1; - } - - for (i = 0; i < sizeof(s_month_names) / sizeof(s_month_names[0]); i++) { - if (strcmp(month, s_month_names[i]) == 0) { - return (int)i; - } - } - - return -1; -} - static esp_err_t cap_time_read_timezone(char *timezone, size_t timezone_size) { const char *configured_tz = NULL; @@ -91,140 +62,170 @@ static esp_err_t cap_time_read_timezone(char *timezone, size_t timezone_size) return ESP_ERR_INVALID_STATE; } -static bool cap_time_parse_and_set_clock(const char *date_header, - char *output, - size_t output_size) +static void cap_time_sync_notification_cb(struct timeval *tv) { - int day = 0; - int year = 0; - int hour = 0; - int minute = 0; - int second = 0; - int month = -1; - char month_name[4] = {0}; - struct tm utc_tm = {0}; - struct timeval tv = {0}; - struct tm local_tm = {0}; - char local_tz[64] = {0}; - bool ok = false; - time_t epoch; - - if (!date_header || !output || output_size == 0) { - return false; - } - - if (sscanf(date_header, - "%*[^,], %d %3s %d %d:%d:%d", - &day, - month_name, - &year, - &hour, - &minute, - &second) != 6) { - return false; - } - - month = cap_time_month_to_index(month_name); - if (month < 0) { - return false; - } - - utc_tm.tm_mday = day; - utc_tm.tm_mon = month; - utc_tm.tm_year = year - 1900; - utc_tm.tm_hour = hour; - utc_tm.tm_min = minute; - utc_tm.tm_sec = second; - - if (cap_time_read_timezone(local_tz, sizeof(local_tz)) != ESP_OK) { - return false; - } - - if (setenv("TZ", CAP_TIME_UTC_TIMEZONE, 1) != 0) { - return false; - } - tzset(); - epoch = mktime(&utc_tm); - if (epoch < 0) { - goto done; - } - - tv.tv_sec = epoch; - if (settimeofday(&tv, NULL) != 0) { - goto done; - } - - setenv("TZ", local_tz, 1); - tzset(); - localtime_r(&epoch, &local_tm); - if (strftime(output, output_size, "%Y-%m-%d %H:%M:%S %Z (%A)", &local_tm) == 0) { - goto done; - } - - ok = true; - -done: - setenv("TZ", local_tz, 1); - tzset(); - return ok; + (void)tv; + ESP_LOGI(TAG, "SNTP time synchronization event received"); } -static esp_err_t cap_time_http_event_handler(esp_http_client_event_t *event) +static esp_err_t cap_time_build_prompt_block(char *output, size_t output_size) { - if (!event || !event->user_data) { - return ESP_OK; + time_t now = 0; + struct tm local_tm = {0}; + struct timeval tv = {0}; + char formatted_time[64] = {0}; + int written; + bool time_valid; + + if (!output || output_size == 0) { + return ESP_ERR_INVALID_ARG; } - if (event->event_id == HTTP_EVENT_ON_HEADER && - event->header_key && - event->header_value && - strcasecmp(event->header_key, "Date") == 0) { - cap_time_state_t *state = (cap_time_state_t *)event->user_data; + time(&now); + gettimeofday(&tv, NULL); + time_valid = cap_time_is_valid(); + if (time_valid) { + if (!localtime_r(&now, &local_tm)) { + return ESP_FAIL; + } + if (strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%d %H:%M:%S %Z (%A)", &local_tm) == 0) { + return ESP_ERR_INVALID_SIZE; + } + } else { + strlcpy(formatted_time, "(invalid)", sizeof(formatted_time)); + } - strlcpy(state->date_header, event->header_value, sizeof(state->date_header)); + written = snprintf(output, output_size, + "- current_local_time: %s\n" + "- unix_timestamp: %lld\n" + "- note: Use this device time context for relative date reasoning such as today, tomorrow, tonight, and next week.", + formatted_time, (long long)tv.tv_sec); + if (written < 0 || (size_t)written >= output_size) { + return ESP_ERR_INVALID_SIZE; } return ESP_OK; } -static esp_err_t cap_time_fetch_current(char *output, size_t output_size) +static esp_err_t cap_time_format_current_time(char *output, size_t output_size) +{ + time_t now = 0; + struct tm local_tm = {0}; + + if (!output || output_size == 0) { + return ESP_ERR_INVALID_ARG; + } + + time(&now); + if (now < CAP_TIME_MIN_VALID_EPOCH) { + return ESP_ERR_INVALID_STATE; + } + if (!localtime_r(&now, &local_tm)) { + return ESP_FAIL; + } + if (strftime(output, output_size, "%Y-%m-%d %H:%M:%S %Z (%A)", &local_tm) == 0) { + return ESP_ERR_INVALID_SIZE; + } + + return ESP_OK; +} + +static esp_err_t cap_time_sync_with_sntp(char *output, size_t output_size) +{ + esp_err_t err = ESP_OK; + esp_err_t wait_err = ESP_OK; + int retry = 0; +#if CONFIG_LWIP_SNTP_MAX_SERVERS > 1 + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG_MULTIPLE( + 2, + ESP_SNTP_SERVER_LIST(CAP_TIME_SNTP_PRIMARY_SERVER, CAP_TIME_SNTP_SECONDARY_SERVER) + ); +#else + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(CAP_TIME_SNTP_PRIMARY_SERVER); +#endif + + if (!output || output_size == 0) { + return ESP_ERR_INVALID_ARG; + } + + config.sync_cb = cap_time_sync_notification_cb; + err = esp_netif_sntp_init(&config); + if (err != ESP_OK) { + return err; + } + + while ((wait_err = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(CAP_TIME_SNTP_WAIT_MS))) == ESP_ERR_TIMEOUT && + ++retry < CAP_TIME_SNTP_RETRY_COUNT) { + ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, CAP_TIME_SNTP_RETRY_COUNT); + } + + if (wait_err != ESP_OK) { + err = wait_err; + goto done; + } + + err = cap_time_format_current_time(output, output_size); + +done: + esp_netif_sntp_deinit(); + if (err == ESP_OK) { + return ESP_OK; + } + + return err; +} + +static esp_err_t cap_time_context_collect(const claw_core_request_t *request, claw_core_context_t *out_context, void *user_ctx) +{ + char *content = NULL; + esp_err_t err; + const size_t content_size = 384; + + (void)request; + (void)user_ctx; + + if (!out_context) { + return ESP_ERR_INVALID_ARG; + } + memset(out_context, 0, sizeof(*out_context)); + + content = calloc(1, content_size); + if (!content) { + return ESP_ERR_NO_MEM; + } + + err = cap_time_ensure_mutex(); + if (err != ESP_OK) { + free(content); + ESP_LOGE(TAG, "Failed to create time mutex"); + return err; + } + xSemaphoreTake(s_time_mutex, portMAX_DELAY); + err = cap_time_build_prompt_block(content, content_size); + xSemaphoreGive(s_time_mutex); + if (err != ESP_OK) { + free(content); + return err; + } + + out_context->kind = CLAW_CORE_CONTEXT_KIND_SYSTEM_PROMPT; + out_context->content = content; + return ESP_OK; +} + +esp_err_t cap_time_get_current(char *output, size_t output_size) { - esp_http_client_config_t config = { - .url = CAP_TIME_SOURCE_URL, - .method = HTTP_METHOD_HEAD, - .timeout_ms = CAP_TIME_HTTP_TIMEOUT_MS, - .crt_bundle_attach = esp_crt_bundle_attach, - .event_handler = cap_time_http_event_handler, - .user_data = &s_time, - }; - esp_http_client_handle_t client = NULL; esp_err_t err; if (!output || output_size == 0) { return ESP_ERR_INVALID_ARG; } - s_time.date_header[0] = '\0'; - client = esp_http_client_init(&config); - if (!client) { - return ESP_FAIL; - } - - err = esp_http_client_perform(client); - esp_http_client_cleanup(client); - if (err != ESP_OK) { - return err; - } - - if (s_time.date_header[0] == '\0') { - return ESP_ERR_NOT_FOUND; - } - - if (!cap_time_parse_and_set_clock(s_time.date_header, output, output_size)) { - return ESP_FAIL; - } - - return ESP_OK; + ESP_RETURN_ON_ERROR(cap_time_ensure_mutex(), TAG, "Failed to create time mutex"); + xSemaphoreTake(s_time_mutex, portMAX_DELAY); + err = cap_time_is_valid() ? cap_time_format_current_time(output, output_size) : cap_time_sync_with_sntp(output, output_size); + xSemaphoreGive(s_time_mutex); + return err; } esp_err_t cap_time_sync_now(char *output, size_t output_size) @@ -236,7 +237,7 @@ esp_err_t cap_time_sync_now(char *output, size_t output_size) } ESP_RETURN_ON_ERROR(cap_time_ensure_mutex(), TAG, "Failed to create time mutex"); xSemaphoreTake(s_time_mutex, portMAX_DELAY); - err = cap_time_fetch_current(output, output_size); + err = cap_time_sync_with_sntp(output, output_size); xSemaphoreGive(s_time_mutex); return err; } @@ -275,8 +276,7 @@ static bool cap_time_network_ready(void) static void cap_time_notify_sync_success(bool had_valid_time) { if (s_time_service.config.on_sync_success) { - s_time_service.config.on_sync_success(had_valid_time, - s_time_service.config.on_sync_success_ctx); + s_time_service.config.on_sync_success(had_valid_time, s_time_service.config.on_sync_success_ctx); } } @@ -324,10 +324,7 @@ static void cap_time_sync_service_task(void *arg) claw_task_delete(NULL); } -static esp_err_t cap_time_execute(const char *input_json, - const claw_cap_call_context_t *ctx, - char *output, - size_t output_size) +static esp_err_t cap_time_execute(const char *input_json, const claw_cap_call_context_t *ctx, char *output, size_t output_size) { esp_err_t err; @@ -338,14 +335,14 @@ static esp_err_t cap_time_execute(const char *input_json, return ESP_ERR_INVALID_ARG; } - err = cap_time_sync_now(output, output_size); + err = cap_time_get_current(output, output_size); if (err != ESP_OK) { - snprintf(output, output_size, "Error: failed to fetch time (%s)", esp_err_to_name(err)); + snprintf(output, output_size, "Error: failed to get time (%s)", esp_err_to_name(err)); ESP_LOGE(TAG, "%s", output); return err; } - ESP_LOGI(TAG, "Time synced: %s", output); + ESP_LOGI(TAG, "Current time: %s", output); return ESP_OK; } @@ -354,7 +351,7 @@ static const claw_cap_descriptor_t s_time_descriptors[] = { .id = "get_current_time", .name = "get_current_time", .family = "system", - .description = "Fetch current network time, update the local clock, and return formatted local time.", + .description = "Return formatted current local time", .kind = CLAW_CAP_KIND_CALLABLE, .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM, .input_schema_json = "{\"type\":\"object\",\"properties\":{}}", @@ -410,10 +407,7 @@ esp_err_t cap_time_sync_service_start(const cap_time_sync_service_config_t *conf .priority = 5, .core_id = tskNO_AFFINITY, .stack_policy = CLAW_TASK_STACK_PREFER_PSRAM, - }, - cap_time_sync_service_task, - NULL, - &s_time_service.task_handle); + }, cap_time_sync_service_task, NULL, &s_time_service.task_handle); if (ok != pdPASS || !s_time_service.task_handle) { s_time_service.running = false; s_time_service.task_handle = NULL; @@ -437,3 +431,9 @@ esp_err_t cap_time_sync_service_stop(void) } return ESP_OK; } + +const claw_core_context_provider_t cap_time_context_provider = { + .name = "Time Context", + .collect = cap_time_context_collect, + .user_ctx = NULL, +}; diff --git a/components/claw_capabilities/cap_time/src/cmd_cap_time.c b/components/claw_capabilities/cap_time/src/cmd_cap_time.c index 99eef20..d686278 100644 --- a/components/claw_capabilities/cap_time/src/cmd_cap_time.c +++ b/components/claw_capabilities/cap_time/src/cmd_cap_time.c @@ -27,13 +27,13 @@ static int time_func(int argc, char **argv) return 1; } if (!time_args.now->count) { - printf("Specify '--now' to sync and print current network time\n"); + printf("Specify '--now' to print current local time and sync with SNTP only when needed\n"); return 1; } - err = cap_time_sync_now(output, sizeof(output)); + err = cap_time_get_current(output, sizeof(output)); if (err != ESP_OK) { - printf("time sync failed: %s\n", esp_err_to_name(err)); + printf("failed to get time: %s\n", esp_err_to_name(err)); return 1; } @@ -43,7 +43,7 @@ static int time_func(int argc, char **argv) void register_cap_time(void) { - time_args.now = arg_lit0(NULL, "now", "Fetch current network time and sync the local clock"); + time_args.now = arg_lit0(NULL, "now", "Print current local time and sync with SNTP only when needed"); time_args.end = arg_end(2); const esp_console_cmd_t time_cmd = {