feat(cap_time): add sntp and time context provider

This commit is contained in:
zhouli
2026-04-23 13:38:46 +08:00
parent 51536183cf
commit 6f7d59da27
6 changed files with 183 additions and 176 deletions
@@ -6,6 +6,7 @@ idf_component_register(
"include"
REQUIRES
claw_cap
esp_http_client
claw_core
esp_netif
console
)
@@ -9,6 +9,7 @@
#include <stddef.h>
#include <stdint.h>
#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
@@ -3,45 +3,33 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "cap_time.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#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 <sys/time.h>
#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,
};
@@ -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 = {