From dbab65bd4719c6b8b743c8f5713a5af3a837568e Mon Sep 17 00:00:00 2001 From: crispyberry Date: Mon, 9 Feb 2026 01:02:33 +0800 Subject: [PATCH 1/4] feat: add cron scheduled task service with tool_use integration Co-Authored-By: Claude Opus 4.6 --- main/CMakeLists.txt | 2 + main/agent/context_builder.c | 5 +- main/bus/message_bus.h | 1 + main/cron/cron_service.c | 410 +++++++++++++++++++++++++++++++++++ main/cron/cron_service.h | 63 ++++++ main/mimi.c | 5 + main/mimi_config.h | 5 + main/tools/tool_cron.c | 186 ++++++++++++++++ main/tools/tool_cron.h | 22 ++ main/tools/tool_registry.c | 47 +++- 10 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 main/cron/cron_service.c create mode 100644 main/cron/cron_service.h create mode 100644 main/tools/tool_cron.c create mode 100644 main/tools/tool_cron.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ba2684c..533533e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -26,6 +26,8 @@ idf_component_register( "tools/tool_web_search.c" "tools/tool_get_time.c" "tools/tool_files.c" + "tools/tool_cron.c" + "cron/cron_service.c" INCLUDE_DIRS "." EMBED_FILES diff --git a/main/agent/context_builder.c b/main/agent/context_builder.c index 43469f7..787c230 100644 --- a/main/agent/context_builder.c +++ b/main/agent/context_builder.c @@ -43,7 +43,10 @@ esp_err_t context_build_system_prompt(char *buf, size_t size) "- read_file: Read a file from SPIFFS (path must start with /spiffs/).\n" "- write_file: Write/overwrite a file on SPIFFS.\n" "- edit_file: Find-and-replace edit a file on SPIFFS.\n" - "- list_dir: List files on SPIFFS, optionally filter by prefix.\n\n" + "- list_dir: List files on SPIFFS, optionally filter by prefix.\n" + "- cron_add: Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.\n" + "- cron_list: List all scheduled cron jobs.\n" + "- cron_remove: Remove a scheduled cron job by ID.\n\n" "Use tools when needed. Provide your final answer as text after using tools.\n\n" "## Memory\n" "You have persistent memory stored on local flash:\n" diff --git a/main/bus/message_bus.h b/main/bus/message_bus.h index b460d18..78d40f4 100644 --- a/main/bus/message_bus.h +++ b/main/bus/message_bus.h @@ -8,6 +8,7 @@ #define MIMI_CHAN_TELEGRAM "telegram" #define MIMI_CHAN_WEBSOCKET "websocket" #define MIMI_CHAN_CLI "cli" +#define MIMI_CHAN_SYSTEM "system" /* Message types on the bus */ typedef struct { diff --git a/main/cron/cron_service.c b/main/cron/cron_service.c new file mode 100644 index 0000000..6dcab84 --- /dev/null +++ b/main/cron/cron_service.c @@ -0,0 +1,410 @@ +#include "cron/cron_service.h" +#include "mimi_config.h" +#include "bus/message_bus.h" + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/timers.h" +#include "esp_log.h" +#include "esp_random.h" +#include "cJSON.h" + +static const char *TAG = "cron"; + +#define MAX_CRON_JOBS MIMI_CRON_MAX_JOBS + +static cron_job_t s_jobs[MAX_CRON_JOBS]; +static int s_job_count = 0; +static TimerHandle_t s_cron_timer = NULL; + +/* ── Persistence ──────────────────────────────────────────────── */ + +static void cron_generate_id(char *id_buf) +{ + uint32_t r = esp_random(); + snprintf(id_buf, 9, "%08x", (unsigned int)r); +} + +static esp_err_t cron_load_jobs(void) +{ + FILE *f = fopen(MIMI_CRON_FILE, "r"); + if (!f) { + ESP_LOGI(TAG, "No cron file found, starting fresh"); + s_job_count = 0; + return ESP_OK; + } + + /* Read entire file */ + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + + if (fsize <= 0 || fsize > 8192) { + ESP_LOGW(TAG, "Cron file invalid size: %ld", fsize); + fclose(f); + s_job_count = 0; + return ESP_OK; + } + + char *buf = malloc(fsize + 1); + if (!buf) { + fclose(f); + return ESP_ERR_NO_MEM; + } + + size_t n = fread(buf, 1, fsize, f); + buf[n] = '\0'; + fclose(f); + + /* Parse JSON */ + cJSON *root = cJSON_Parse(buf); + free(buf); + + if (!root) { + ESP_LOGW(TAG, "Failed to parse cron JSON"); + s_job_count = 0; + return ESP_OK; + } + + cJSON *jobs_arr = cJSON_GetObjectItem(root, "jobs"); + if (!jobs_arr || !cJSON_IsArray(jobs_arr)) { + cJSON_Delete(root); + s_job_count = 0; + return ESP_OK; + } + + s_job_count = 0; + cJSON *item; + cJSON_ArrayForEach(item, jobs_arr) { + if (s_job_count >= MAX_CRON_JOBS) break; + + cron_job_t *job = &s_jobs[s_job_count]; + memset(job, 0, sizeof(cron_job_t)); + + const char *id = cJSON_GetStringValue(cJSON_GetObjectItem(item, "id")); + const char *name = cJSON_GetStringValue(cJSON_GetObjectItem(item, "name")); + const char *kind_str = cJSON_GetStringValue(cJSON_GetObjectItem(item, "kind")); + const char *message = cJSON_GetStringValue(cJSON_GetObjectItem(item, "message")); + const char *channel = cJSON_GetStringValue(cJSON_GetObjectItem(item, "channel")); + const char *chat_id = cJSON_GetStringValue(cJSON_GetObjectItem(item, "chat_id")); + + if (!id || !name || !kind_str || !message) continue; + + strncpy(job->id, id, sizeof(job->id) - 1); + strncpy(job->name, name, sizeof(job->name) - 1); + strncpy(job->message, message, sizeof(job->message) - 1); + strncpy(job->channel, channel ? channel : MIMI_CHAN_SYSTEM, + sizeof(job->channel) - 1); + strncpy(job->chat_id, chat_id ? chat_id : "cron", + sizeof(job->chat_id) - 1); + + cJSON *enabled_j = cJSON_GetObjectItem(item, "enabled"); + job->enabled = enabled_j ? cJSON_IsTrue(enabled_j) : true; + + cJSON *delete_j = cJSON_GetObjectItem(item, "delete_after_run"); + job->delete_after_run = delete_j ? cJSON_IsTrue(delete_j) : false; + + if (strcmp(kind_str, "every") == 0) { + job->kind = CRON_KIND_EVERY; + cJSON *interval = cJSON_GetObjectItem(item, "interval_s"); + job->interval_s = (interval && cJSON_IsNumber(interval)) + ? (uint32_t)interval->valuedouble : 0; + } else if (strcmp(kind_str, "at") == 0) { + job->kind = CRON_KIND_AT; + cJSON *at_epoch = cJSON_GetObjectItem(item, "at_epoch"); + job->at_epoch = (at_epoch && cJSON_IsNumber(at_epoch)) + ? (int64_t)at_epoch->valuedouble : 0; + } else { + continue; /* Unknown kind, skip */ + } + + cJSON *last_run = cJSON_GetObjectItem(item, "last_run"); + job->last_run = (last_run && cJSON_IsNumber(last_run)) + ? (int64_t)last_run->valuedouble : 0; + + cJSON *next_run = cJSON_GetObjectItem(item, "next_run"); + job->next_run = (next_run && cJSON_IsNumber(next_run)) + ? (int64_t)next_run->valuedouble : 0; + + s_job_count++; + } + + cJSON_Delete(root); + ESP_LOGI(TAG, "Loaded %d cron jobs", s_job_count); + return ESP_OK; +} + +static esp_err_t cron_save_jobs(void) +{ + cJSON *root = cJSON_CreateObject(); + cJSON *jobs_arr = cJSON_CreateArray(); + + for (int i = 0; i < s_job_count; i++) { + cron_job_t *job = &s_jobs[i]; + cJSON *item = cJSON_CreateObject(); + + cJSON_AddStringToObject(item, "id", job->id); + cJSON_AddStringToObject(item, "name", job->name); + cJSON_AddBoolToObject(item, "enabled", job->enabled); + cJSON_AddStringToObject(item, "kind", + job->kind == CRON_KIND_EVERY ? "every" : "at"); + + if (job->kind == CRON_KIND_EVERY) { + cJSON_AddNumberToObject(item, "interval_s", job->interval_s); + } else { + cJSON_AddNumberToObject(item, "at_epoch", (double)job->at_epoch); + } + + cJSON_AddStringToObject(item, "message", job->message); + cJSON_AddStringToObject(item, "channel", job->channel); + cJSON_AddStringToObject(item, "chat_id", job->chat_id); + cJSON_AddNumberToObject(item, "last_run", (double)job->last_run); + cJSON_AddNumberToObject(item, "next_run", (double)job->next_run); + cJSON_AddBoolToObject(item, "delete_after_run", job->delete_after_run); + + cJSON_AddItemToArray(jobs_arr, item); + } + + cJSON_AddItemToObject(root, "jobs", jobs_arr); + + char *json_str = cJSON_Print(root); + cJSON_Delete(root); + + if (!json_str) { + ESP_LOGE(TAG, "Failed to serialize cron jobs"); + return ESP_ERR_NO_MEM; + } + + FILE *f = fopen(MIMI_CRON_FILE, "w"); + if (!f) { + ESP_LOGE(TAG, "Failed to open %s for writing", MIMI_CRON_FILE); + free(json_str); + return ESP_FAIL; + } + + size_t len = strlen(json_str); + size_t written = fwrite(json_str, 1, len, f); + fclose(f); + free(json_str); + + if (written != len) { + ESP_LOGE(TAG, "Cron save incomplete: %d/%d bytes", (int)written, (int)len); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Saved %d cron jobs to %s", s_job_count, MIMI_CRON_FILE); + return ESP_OK; +} + +/* ── Timer callback ───────────────────────────────────────────── */ + +static void cron_timer_callback(TimerHandle_t xTimer) +{ + (void)xTimer; + + time_t now = time(NULL); + if (now < 1700000000) { + /* System time not set yet (before ~2023), skip */ + return; + } + + bool changed = false; + + for (int i = 0; i < s_job_count; i++) { + cron_job_t *job = &s_jobs[i]; + if (!job->enabled) continue; + if (job->next_run <= 0) continue; + if (job->next_run > now) continue; + + /* Job is due — fire it */ + ESP_LOGI(TAG, "Cron job firing: %s (%s)", job->name, job->id); + + /* Push message to inbound queue */ + mimi_msg_t msg; + memset(&msg, 0, sizeof(msg)); + strncpy(msg.channel, job->channel, sizeof(msg.channel) - 1); + strncpy(msg.chat_id, job->chat_id, sizeof(msg.chat_id) - 1); + msg.content = strdup(job->message); + + if (msg.content) { + esp_err_t err = message_bus_push_inbound(&msg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to push cron message: %s", esp_err_to_name(err)); + free(msg.content); + } + } + + /* Update state */ + job->last_run = now; + + if (job->kind == CRON_KIND_AT) { + /* One-shot: disable or delete */ + if (job->delete_after_run) { + /* Remove by shifting array */ + ESP_LOGI(TAG, "Deleting one-shot job: %s", job->name); + for (int j = i; j < s_job_count - 1; j++) { + s_jobs[j] = s_jobs[j + 1]; + } + s_job_count--; + i--; /* Re-check this index */ + } else { + job->enabled = false; + job->next_run = 0; + } + } else { + /* Recurring: compute next run */ + job->next_run = now + job->interval_s; + } + + changed = true; + } + + if (changed) { + cron_save_jobs(); + } +} + +/* ── Compute initial next_run for a new job ───────────────────── */ + +static void compute_initial_next_run(cron_job_t *job) +{ + time_t now = time(NULL); + + if (job->kind == CRON_KIND_EVERY) { + job->next_run = now + job->interval_s; + } else if (job->kind == CRON_KIND_AT) { + if (job->at_epoch > now) { + job->next_run = job->at_epoch; + } else { + /* Already in the past */ + job->next_run = 0; + job->enabled = false; + } + } +} + +/* ── Public API ───────────────────────────────────────────────── */ + +esp_err_t cron_service_init(void) +{ + return cron_load_jobs(); +} + +esp_err_t cron_service_start(void) +{ + if (s_cron_timer) { + ESP_LOGW(TAG, "Cron timer already running"); + return ESP_OK; + } + + /* Recompute next_run for all enabled jobs that don't have one */ + time_t now = time(NULL); + for (int i = 0; i < s_job_count; i++) { + cron_job_t *job = &s_jobs[i]; + if (job->enabled && job->next_run <= 0) { + if (job->kind == CRON_KIND_EVERY) { + job->next_run = now + job->interval_s; + } else if (job->kind == CRON_KIND_AT && job->at_epoch > now) { + job->next_run = job->at_epoch; + } + } + } + + s_cron_timer = xTimerCreate( + "cron", + pdMS_TO_TICKS(MIMI_CRON_CHECK_INTERVAL_MS), + pdTRUE, /* auto-reload */ + NULL, + cron_timer_callback + ); + + if (!s_cron_timer) { + ESP_LOGE(TAG, "Failed to create cron timer"); + return ESP_FAIL; + } + + if (xTimerStart(s_cron_timer, pdMS_TO_TICKS(1000)) != pdPASS) { + ESP_LOGE(TAG, "Failed to start cron timer"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Cron service started (%d jobs, check every %ds)", + s_job_count, MIMI_CRON_CHECK_INTERVAL_MS / 1000); + return ESP_OK; +} + +void cron_service_stop(void) +{ + if (s_cron_timer) { + xTimerStop(s_cron_timer, pdMS_TO_TICKS(1000)); + xTimerDelete(s_cron_timer, pdMS_TO_TICKS(1000)); + s_cron_timer = NULL; + ESP_LOGI(TAG, "Cron service stopped"); + } +} + +esp_err_t cron_add_job(cron_job_t *job) +{ + if (s_job_count >= MAX_CRON_JOBS) { + ESP_LOGW(TAG, "Max cron jobs reached (%d)", MAX_CRON_JOBS); + return ESP_ERR_NO_MEM; + } + + /* Generate ID */ + cron_generate_id(job->id); + + /* Set defaults for channel/chat_id if empty */ + if (job->channel[0] == '\0') { + strncpy(job->channel, MIMI_CHAN_SYSTEM, sizeof(job->channel) - 1); + } + if (job->chat_id[0] == '\0') { + strncpy(job->chat_id, "cron", sizeof(job->chat_id) - 1); + } + + /* Compute initial next_run */ + job->enabled = true; + job->last_run = 0; + compute_initial_next_run(job); + + /* Copy into static array */ + s_jobs[s_job_count] = *job; + s_job_count++; + + cron_save_jobs(); + + ESP_LOGI(TAG, "Added cron job: %s (%s) kind=%s next_run=%lld", + job->name, job->id, + job->kind == CRON_KIND_EVERY ? "every" : "at", + (long long)job->next_run); + return ESP_OK; +} + +esp_err_t cron_remove_job(const char *job_id) +{ + for (int i = 0; i < s_job_count; i++) { + if (strcmp(s_jobs[i].id, job_id) == 0) { + ESP_LOGI(TAG, "Removing cron job: %s (%s)", s_jobs[i].name, job_id); + + /* Shift remaining jobs down */ + for (int j = i; j < s_job_count - 1; j++) { + s_jobs[j] = s_jobs[j + 1]; + } + s_job_count--; + + cron_save_jobs(); + return ESP_OK; + } + } + + ESP_LOGW(TAG, "Cron job not found: %s", job_id); + return ESP_ERR_NOT_FOUND; +} + +void cron_list_jobs(const cron_job_t **jobs, int *count) +{ + *jobs = s_jobs; + *count = s_job_count; +} diff --git a/main/cron/cron_service.h b/main/cron/cron_service.h new file mode 100644 index 0000000..3d37752 --- /dev/null +++ b/main/cron/cron_service.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esp_err.h" +#include +#include + +/* Schedule types */ +typedef enum { + CRON_KIND_EVERY = 0, /* Recurring interval in seconds */ + CRON_KIND_AT = 1, /* One-shot at unix timestamp */ +} cron_kind_t; + +/* A single cron job */ +typedef struct { + char id[9]; /* 8-char hex ID + null */ + char name[32]; + bool enabled; + cron_kind_t kind; + uint32_t interval_s; /* For EVERY: interval in seconds */ + int64_t at_epoch; /* For AT: unix timestamp */ + char message[256]; /* Message to inject into inbound queue */ + char channel[16]; /* Reply channel (default "system") */ + char chat_id[32]; /* Reply chat_id (default "cron") */ + int64_t last_run; /* Last run epoch */ + int64_t next_run; /* Next run epoch */ + bool delete_after_run; /* Remove job after firing (for AT jobs) */ +} cron_job_t; + +/** + * Initialize the cron service. Loads jobs from SPIFFS. + */ +esp_err_t cron_service_init(void); + +/** + * Start the cron timer. Call after WiFi is connected and time is synced. + */ +esp_err_t cron_service_start(void); + +/** + * Stop the cron timer. + */ +void cron_service_stop(void); + +/** + * Add a new cron job. + * @param job Pointer to job struct (id will be generated) + * @return ESP_OK on success, ESP_ERR_NO_MEM if max jobs reached + */ +esp_err_t cron_add_job(cron_job_t *job); + +/** + * Remove a cron job by ID. + * @param job_id 8-char job ID + * @return ESP_OK on success, ESP_ERR_NOT_FOUND if not found + */ +esp_err_t cron_remove_job(const char *job_id); + +/** + * List all cron jobs. + * @param jobs Output array of job pointers + * @param count Output: number of jobs + */ +void cron_list_jobs(const cron_job_t **jobs, int *count); diff --git a/main/mimi.c b/main/mimi.c index 0658055..60f4a4e 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -26,6 +26,7 @@ #include "ui/config_screen.h" #include "imu/imu_manager.h" #include "rgb/rgb.h" +#include "cron/cron_service.h" static const char *TAG = "mimi"; @@ -77,6 +78,8 @@ static void outbound_dispatch_task(void *arg) telegram_send_message(msg.chat_id, msg.content); } else if (strcmp(msg.channel, MIMI_CHAN_WEBSOCKET) == 0) { ws_server_send(msg.chat_id, msg.content); + } else if (strcmp(msg.channel, MIMI_CHAN_SYSTEM) == 0) { + ESP_LOGI(TAG, "System message [%s]: %.128s", msg.chat_id, msg.content); } else { ESP_LOGW(TAG, "Unknown channel: %s", msg.channel); } @@ -124,6 +127,7 @@ void app_main(void) ESP_ERROR_CHECK(telegram_bot_init()); ESP_ERROR_CHECK(llm_proxy_init()); ESP_ERROR_CHECK(tool_registry_init()); + ESP_ERROR_CHECK(cron_service_init()); ESP_ERROR_CHECK(agent_loop_init()); /* Start Serial CLI first (works without WiFi) */ @@ -141,6 +145,7 @@ void app_main(void) /* Start network-dependent services */ ESP_ERROR_CHECK(telegram_bot_start()); ESP_ERROR_CHECK(agent_loop_start()); + cron_service_start(); ESP_ERROR_CHECK(ws_server_start()); /* Outbound dispatch task */ diff --git a/main/mimi_config.h b/main/mimi_config.h index 079ecc0..6f90dfe 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -84,6 +84,11 @@ #define MIMI_CONTEXT_BUF_SIZE (16 * 1024) #define MIMI_SESSION_MAX_MSGS 20 +/* Cron Service */ +#define MIMI_CRON_FILE "/spiffs/config/cron.json" +#define MIMI_CRON_CHECK_INTERVAL_MS (30 * 1000) +#define MIMI_CRON_MAX_JOBS 8 + /* WebSocket Gateway */ #define MIMI_WS_PORT 18789 #define MIMI_WS_MAX_CLIENTS 4 diff --git a/main/tools/tool_cron.c b/main/tools/tool_cron.c new file mode 100644 index 0000000..dfb0a6d --- /dev/null +++ b/main/tools/tool_cron.c @@ -0,0 +1,186 @@ +#include "tools/tool_cron.h" +#include "cron/cron_service.h" + +#include +#include +#include "esp_log.h" +#include "cJSON.h" + +static const char *TAG = "tool_cron"; + +/* ── cron_add ─────────────────────────────────────────────────── */ + +esp_err_t tool_cron_add_execute(const char *input_json, char *output, size_t output_size) +{ + cJSON *root = cJSON_Parse(input_json); + if (!root) { + snprintf(output, output_size, "Error: invalid JSON input"); + return ESP_ERR_INVALID_ARG; + } + + const char *name = cJSON_GetStringValue(cJSON_GetObjectItem(root, "name")); + const char *schedule_type = cJSON_GetStringValue(cJSON_GetObjectItem(root, "schedule_type")); + const char *message = cJSON_GetStringValue(cJSON_GetObjectItem(root, "message")); + + if (!name || !schedule_type || !message) { + snprintf(output, output_size, "Error: missing required fields (name, schedule_type, message)"); + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + + if (strlen(message) == 0) { + snprintf(output, output_size, "Error: message must not be empty"); + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + + cron_job_t job; + memset(&job, 0, sizeof(job)); + strncpy(job.name, name, sizeof(job.name) - 1); + strncpy(job.message, message, sizeof(job.message) - 1); + + /* Optional channel and chat_id */ + const char *channel = cJSON_GetStringValue(cJSON_GetObjectItem(root, "channel")); + const char *chat_id = cJSON_GetStringValue(cJSON_GetObjectItem(root, "chat_id")); + if (channel) strncpy(job.channel, channel, sizeof(job.channel) - 1); + if (chat_id) strncpy(job.chat_id, chat_id, sizeof(job.chat_id) - 1); + + if (strcmp(schedule_type, "every") == 0) { + job.kind = CRON_KIND_EVERY; + cJSON *interval = cJSON_GetObjectItem(root, "interval_s"); + if (!interval || !cJSON_IsNumber(interval) || interval->valuedouble <= 0) { + snprintf(output, output_size, "Error: 'every' schedule requires positive 'interval_s'"); + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + job.interval_s = (uint32_t)interval->valuedouble; + job.delete_after_run = false; + } else if (strcmp(schedule_type, "at") == 0) { + job.kind = CRON_KIND_AT; + cJSON *at_epoch = cJSON_GetObjectItem(root, "at_epoch"); + if (!at_epoch || !cJSON_IsNumber(at_epoch)) { + snprintf(output, output_size, "Error: 'at' schedule requires 'at_epoch' (unix timestamp)"); + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + job.at_epoch = (int64_t)at_epoch->valuedouble; + + /* Check if already in the past */ + time_t now = time(NULL); + if (job.at_epoch <= now) { + snprintf(output, output_size, "Error: at_epoch %lld is in the past (now=%lld)", + (long long)job.at_epoch, (long long)now); + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + + /* Default: delete one-shot jobs after run */ + cJSON *delete_j = cJSON_GetObjectItem(root, "delete_after_run"); + job.delete_after_run = delete_j ? cJSON_IsTrue(delete_j) : true; + } else { + snprintf(output, output_size, "Error: schedule_type must be 'every' or 'at'"); + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + + cJSON_Delete(root); + + esp_err_t err = cron_add_job(&job); + if (err != ESP_OK) { + snprintf(output, output_size, "Error: failed to add job (%s)", esp_err_to_name(err)); + return err; + } + + /* Format success response */ + if (job.kind == CRON_KIND_EVERY) { + snprintf(output, output_size, + "OK: Added recurring job '%s' (id=%s), runs every %lu seconds. Next run at epoch %lld.", + job.name, job.id, (unsigned long)job.interval_s, (long long)job.next_run); + } else { + snprintf(output, output_size, + "OK: Added one-shot job '%s' (id=%s), fires at epoch %lld.%s", + job.name, job.id, (long long)job.at_epoch, + job.delete_after_run ? " Will be deleted after firing." : ""); + } + + ESP_LOGI(TAG, "cron_add: %s", output); + return ESP_OK; +} + +/* ── cron_list ────────────────────────────────────────────────── */ + +esp_err_t tool_cron_list_execute(const char *input_json, char *output, size_t output_size) +{ + (void)input_json; + + const cron_job_t *jobs; + int count; + cron_list_jobs(&jobs, &count); + + if (count == 0) { + snprintf(output, output_size, "No cron jobs scheduled."); + return ESP_OK; + } + + size_t off = 0; + off += snprintf(output + off, output_size - off, + "Scheduled jobs (%d):\n", count); + + for (int i = 0; i < count && off < output_size - 1; i++) { + const cron_job_t *j = &jobs[i]; + + if (j->kind == CRON_KIND_EVERY) { + off += snprintf(output + off, output_size - off, + " %d. [%s] \"%s\" — every %lus, %s, next=%lld, last=%lld, ch=%s:%s\n", + i + 1, j->id, j->name, + (unsigned long)j->interval_s, + j->enabled ? "enabled" : "disabled", + (long long)j->next_run, (long long)j->last_run, + j->channel, j->chat_id); + } else { + off += snprintf(output + off, output_size - off, + " %d. [%s] \"%s\" — at %lld, %s, last=%lld, ch=%s:%s%s\n", + i + 1, j->id, j->name, + (long long)j->at_epoch, + j->enabled ? "enabled" : "disabled", + (long long)j->last_run, + j->channel, j->chat_id, + j->delete_after_run ? " (auto-delete)" : ""); + } + } + + ESP_LOGI(TAG, "cron_list: %d jobs", count); + return ESP_OK; +} + +/* ── cron_remove ──────────────────────────────────────────────── */ + +esp_err_t tool_cron_remove_execute(const char *input_json, char *output, size_t output_size) +{ + cJSON *root = cJSON_Parse(input_json); + if (!root) { + snprintf(output, output_size, "Error: invalid JSON input"); + return ESP_ERR_INVALID_ARG; + } + + const char *job_id = cJSON_GetStringValue(cJSON_GetObjectItem(root, "job_id")); + if (!job_id || strlen(job_id) == 0) { + snprintf(output, output_size, "Error: missing 'job_id' field"); + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = cron_remove_job(job_id); + cJSON_Delete(root); + + if (err == ESP_OK) { + snprintf(output, output_size, "OK: Removed cron job %s", job_id); + } else if (err == ESP_ERR_NOT_FOUND) { + snprintf(output, output_size, "Error: job '%s' not found", job_id); + } else { + snprintf(output, output_size, "Error: failed to remove job (%s)", esp_err_to_name(err)); + } + + ESP_LOGI(TAG, "cron_remove: %s -> %s", job_id, esp_err_to_name(err)); + return err; +} diff --git a/main/tools/tool_cron.h b/main/tools/tool_cron.h new file mode 100644 index 0000000..b99eb8e --- /dev/null +++ b/main/tools/tool_cron.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esp_err.h" +#include + +/** + * Add a scheduled cron job. + * Input JSON: { name, schedule_type ("every"/"at"), interval_s, at_epoch, message, channel?, chat_id? } + */ +esp_err_t tool_cron_add_execute(const char *input_json, char *output, size_t output_size); + +/** + * List all scheduled cron jobs. + * Input JSON: {} (no required fields) + */ +esp_err_t tool_cron_list_execute(const char *input_json, char *output, size_t output_size); + +/** + * Remove a scheduled cron job by ID. + * Input JSON: { job_id } + */ +esp_err_t tool_cron_remove_execute(const char *input_json, char *output, size_t output_size); diff --git a/main/tools/tool_registry.c b/main/tools/tool_registry.c index 3f4d892..c277ed1 100644 --- a/main/tools/tool_registry.c +++ b/main/tools/tool_registry.c @@ -2,6 +2,7 @@ #include "tools/tool_web_search.h" #include "tools/tool_get_time.h" #include "tools/tool_files.h" +#include "tools/tool_cron.h" #include #include "esp_log.h" @@ -9,7 +10,7 @@ static const char *TAG = "tools"; -#define MAX_TOOLS 8 +#define MAX_TOOLS 12 static mimi_tool_t s_tools[MAX_TOOLS]; static int s_tool_count = 0; @@ -130,6 +131,50 @@ esp_err_t tool_registry_init(void) }; register_tool(&ld); + /* Register cron_add */ + mimi_tool_t ca = { + .name = "cron_add", + .description = "Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.", + .input_schema_json = + "{\"type\":\"object\"," + "\"properties\":{" + "\"name\":{\"type\":\"string\",\"description\":\"Short name for the job\"}," + "\"schedule_type\":{\"type\":\"string\",\"description\":\"'every' for recurring interval or 'at' for one-shot at a unix timestamp\"}," + "\"interval_s\":{\"type\":\"integer\",\"description\":\"Interval in seconds (required for 'every')\"}," + "\"at_epoch\":{\"type\":\"integer\",\"description\":\"Unix timestamp to fire at (required for 'at')\"}," + "\"message\":{\"type\":\"string\",\"description\":\"Message to inject when the job fires, triggering an agent turn\"}," + "\"channel\":{\"type\":\"string\",\"description\":\"Optional reply channel (e.g. 'telegram'). Defaults to 'system'\"}," + "\"chat_id\":{\"type\":\"string\",\"description\":\"Optional reply chat_id. Defaults to 'cron'\"}" + "}," + "\"required\":[\"name\",\"schedule_type\",\"message\"]}", + .execute = tool_cron_add_execute, + }; + register_tool(&ca); + + /* Register cron_list */ + mimi_tool_t cl = { + .name = "cron_list", + .description = "List all scheduled cron jobs with their status, schedule, and IDs.", + .input_schema_json = + "{\"type\":\"object\"," + "\"properties\":{}," + "\"required\":[]}", + .execute = tool_cron_list_execute, + }; + register_tool(&cl); + + /* Register cron_remove */ + mimi_tool_t cr = { + .name = "cron_remove", + .description = "Remove a scheduled cron job by its ID.", + .input_schema_json = + "{\"type\":\"object\"," + "\"properties\":{\"job_id\":{\"type\":\"string\",\"description\":\"The 8-character job ID to remove\"}}," + "\"required\":[\"job_id\"]}", + .execute = tool_cron_remove_execute, + }; + register_tool(&cr); + build_tools_json(); ESP_LOGI(TAG, "Tool registry initialized"); From ebff0ccb04a3df7429b7854cde35995ed2b8f653 Mon Sep 17 00:00:00 2001 From: crispyberry Date: Mon, 9 Feb 2026 01:23:13 +0800 Subject: [PATCH 2/4] feat: add heartbeat service for periodic task checking Adds a heartbeat timer that reads /spiffs/config/HEARTBEAT.md every 30 minutes and sends a prompt to the agent if actionable tasks are found. Skips empty lines, headers, and completed checkboxes. Includes a heartbeat_trigger CLI command for manual testing. Co-Authored-By: Claude Opus 4.6 --- main/CMakeLists.txt | 1 + main/agent/context_builder.c | 7 +- main/cli/serial_cli.c | 21 +++++ main/heartbeat/heartbeat.c | 164 +++++++++++++++++++++++++++++++++++ main/heartbeat/heartbeat.h | 25 ++++++ main/mimi.c | 3 + main/mimi_config.h | 4 + 7 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 main/heartbeat/heartbeat.c create mode 100644 main/heartbeat/heartbeat.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 533533e..91cb174 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -28,6 +28,7 @@ idf_component_register( "tools/tool_files.c" "tools/tool_cron.c" "cron/cron_service.c" + "heartbeat/heartbeat.c" INCLUDE_DIRS "." EMBED_FILES diff --git a/main/agent/context_builder.c b/main/agent/context_builder.c index 787c230..7365f7e 100644 --- a/main/agent/context_builder.c +++ b/main/agent/context_builder.c @@ -58,7 +58,12 @@ esp_err_t context_build_system_prompt(char *buf, size_t size) "- Always read_file MEMORY.md before writing, so you can edit_file to update without losing existing content.\n" "- Use get_current_time to know today's date before writing daily notes.\n" "- Keep MEMORY.md concise and organized — summarize, don't dump raw conversation.\n" - "- You should proactively save memory without being asked. If the user tells you their name, preferences, or important facts, persist them immediately.\n"); + "- You should proactively save memory without being asked. If the user tells you their name, preferences, or important facts, persist them immediately.\n\n" + "## Heartbeat\n" + "The file /spiffs/config/HEARTBEAT.md contains periodic tasks.\n" + "When triggered by heartbeat, read the file and execute any pending tasks.\n" + "If nothing needs attention, reply with just: HEARTBEAT_OK\n" + "You can also write to HEARTBEAT.md to schedule tasks for yourself.\n"); /* Bootstrap files */ off = append_file(buf, size, off, MIMI_SOUL_FILE, "Personality"); diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 29a0eb0..2ae63cc 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -7,6 +7,7 @@ #include "memory/session_mgr.h" #include "proxy/http_proxy.h" #include "tools/tool_web_search.h" +#include "heartbeat/heartbeat.h" #include #include @@ -316,6 +317,18 @@ static int cmd_config_reset(int argc, char **argv) return 0; } +/* --- heartbeat_trigger command --- */ +static int cmd_heartbeat_trigger(int argc, char **argv) +{ + printf("Checking HEARTBEAT.md...\n"); + if (heartbeat_trigger()) { + printf("Heartbeat: agent prompted with pending tasks.\n"); + } else { + printf("Heartbeat: no actionable tasks found.\n"); + } + return 0; +} + /* --- restart command --- */ static int cmd_restart(int argc, char **argv) { @@ -505,6 +518,14 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&config_reset_cmd); + /* heartbeat_trigger */ + esp_console_cmd_t heartbeat_cmd = { + .command = "heartbeat_trigger", + .help = "Manually trigger a heartbeat check", + .func = &cmd_heartbeat_trigger, + }; + esp_console_cmd_register(&heartbeat_cmd); + /* restart */ esp_console_cmd_t restart_cmd = { .command = "restart", diff --git a/main/heartbeat/heartbeat.c b/main/heartbeat/heartbeat.c new file mode 100644 index 0000000..e680201 --- /dev/null +++ b/main/heartbeat/heartbeat.c @@ -0,0 +1,164 @@ +#include "heartbeat/heartbeat.h" +#include "mimi_config.h" +#include "bus/message_bus.h" + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/timers.h" +#include "esp_log.h" + +static const char *TAG = "heartbeat"; + +#define HEARTBEAT_PROMPT \ + "Read " MIMI_HEARTBEAT_FILE " and follow any instructions or tasks listed there. " \ + "If nothing needs attention, reply with just: HEARTBEAT_OK" + +static TimerHandle_t s_heartbeat_timer = NULL; + +/* ── Content check ────────────────────────────────────────────── */ + +/** + * Check if HEARTBEAT.md has actionable content. + * Returns true if any line is NOT: + * - empty / whitespace-only + * - a markdown header (starts with #) + * - a completed checkbox (- [x] or * [x]) + */ +static bool heartbeat_has_tasks(void) +{ + FILE *f = fopen(MIMI_HEARTBEAT_FILE, "r"); + if (!f) { + return false; + } + + char line[256]; + bool found_task = false; + + while (fgets(line, sizeof(line), f)) { + /* Skip leading whitespace */ + const char *p = line; + while (*p && isspace((unsigned char)*p)) { + p++; + } + + /* Skip empty lines */ + if (*p == '\0') { + continue; + } + + /* Skip markdown headers */ + if (*p == '#') { + continue; + } + + /* Skip completed checkboxes: "- [x]" or "* [x]" */ + if ((*p == '-' || *p == '*') && *(p + 1) == ' ' && *(p + 2) == '[') { + char mark = *(p + 3); + if ((mark == 'x' || mark == 'X') && *(p + 4) == ']') { + continue; + } + } + + /* Found an actionable line */ + found_task = true; + break; + } + + fclose(f); + return found_task; +} + +/* ── Send heartbeat to agent ──────────────────────────────────── */ + +static bool heartbeat_send(void) +{ + if (!heartbeat_has_tasks()) { + ESP_LOGD(TAG, "No actionable tasks in HEARTBEAT.md"); + return false; + } + + mimi_msg_t msg; + memset(&msg, 0, sizeof(msg)); + strncpy(msg.channel, MIMI_CHAN_SYSTEM, sizeof(msg.channel) - 1); + strncpy(msg.chat_id, "heartbeat", sizeof(msg.chat_id) - 1); + msg.content = strdup(HEARTBEAT_PROMPT); + + if (!msg.content) { + ESP_LOGE(TAG, "Failed to allocate heartbeat prompt"); + return false; + } + + esp_err_t err = message_bus_push_inbound(&msg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to push heartbeat message: %s", esp_err_to_name(err)); + free(msg.content); + return false; + } + + ESP_LOGI(TAG, "Triggered agent check"); + return true; +} + +/* ── Timer callback ───────────────────────────────────────────── */ + +static void heartbeat_timer_callback(TimerHandle_t xTimer) +{ + (void)xTimer; + heartbeat_send(); +} + +/* ── Public API ───────────────────────────────────────────────── */ + +esp_err_t heartbeat_init(void) +{ + ESP_LOGI(TAG, "Heartbeat service initialized (file: %s, interval: %ds)", + MIMI_HEARTBEAT_FILE, MIMI_HEARTBEAT_INTERVAL_MS / 1000); + return ESP_OK; +} + +esp_err_t heartbeat_start(void) +{ + if (s_heartbeat_timer) { + ESP_LOGW(TAG, "Heartbeat timer already running"); + return ESP_OK; + } + + s_heartbeat_timer = xTimerCreate( + "heartbeat", + pdMS_TO_TICKS(MIMI_HEARTBEAT_INTERVAL_MS), + pdTRUE, /* auto-reload */ + NULL, + heartbeat_timer_callback + ); + + if (!s_heartbeat_timer) { + ESP_LOGE(TAG, "Failed to create heartbeat timer"); + return ESP_FAIL; + } + + if (xTimerStart(s_heartbeat_timer, pdMS_TO_TICKS(1000)) != pdPASS) { + ESP_LOGE(TAG, "Failed to start heartbeat timer"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Heartbeat started (every %d min)", MIMI_HEARTBEAT_INTERVAL_MS / 60000); + return ESP_OK; +} + +void heartbeat_stop(void) +{ + if (s_heartbeat_timer) { + xTimerStop(s_heartbeat_timer, pdMS_TO_TICKS(1000)); + xTimerDelete(s_heartbeat_timer, pdMS_TO_TICKS(1000)); + s_heartbeat_timer = NULL; + ESP_LOGI(TAG, "Heartbeat stopped"); + } +} + +bool heartbeat_trigger(void) +{ + return heartbeat_send(); +} diff --git a/main/heartbeat/heartbeat.h b/main/heartbeat/heartbeat.h new file mode 100644 index 0000000..e0af53b --- /dev/null +++ b/main/heartbeat/heartbeat.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esp_err.h" + +/** + * Initialize the heartbeat service (logs ready state). + */ +esp_err_t heartbeat_init(void); + +/** + * Start the heartbeat timer. Checks HEARTBEAT.md periodically + * and sends a prompt to the agent if actionable tasks are found. + */ +esp_err_t heartbeat_start(void); + +/** + * Stop and delete the heartbeat timer. + */ +void heartbeat_stop(void); + +/** + * Manually trigger a heartbeat check (for CLI testing). + * Returns true if the agent was prompted, false if no tasks found. + */ +bool heartbeat_trigger(void); diff --git a/main/mimi.c b/main/mimi.c index 60f4a4e..770b52c 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -27,6 +27,7 @@ #include "imu/imu_manager.h" #include "rgb/rgb.h" #include "cron/cron_service.h" +#include "heartbeat/heartbeat.h" static const char *TAG = "mimi"; @@ -128,6 +129,7 @@ void app_main(void) ESP_ERROR_CHECK(llm_proxy_init()); ESP_ERROR_CHECK(tool_registry_init()); ESP_ERROR_CHECK(cron_service_init()); + ESP_ERROR_CHECK(heartbeat_init()); ESP_ERROR_CHECK(agent_loop_init()); /* Start Serial CLI first (works without WiFi) */ @@ -146,6 +148,7 @@ void app_main(void) ESP_ERROR_CHECK(telegram_bot_start()); ESP_ERROR_CHECK(agent_loop_start()); cron_service_start(); + heartbeat_start(); ESP_ERROR_CHECK(ws_server_start()); /* Outbound dispatch task */ diff --git a/main/mimi_config.h b/main/mimi_config.h index 6f90dfe..9fcb2a8 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -89,6 +89,10 @@ #define MIMI_CRON_CHECK_INTERVAL_MS (30 * 1000) #define MIMI_CRON_MAX_JOBS 8 +/* Heartbeat */ +#define MIMI_HEARTBEAT_FILE "/spiffs/config/HEARTBEAT.md" +#define MIMI_HEARTBEAT_INTERVAL_MS (30 * 60 * 1000) + /* WebSocket Gateway */ #define MIMI_WS_PORT 18789 #define MIMI_WS_MAX_CLIENTS 4 From 5c54352ce0da185e5afa773188327ad31fc9ca77 Mon Sep 17 00:00:00 2001 From: Asklv Date: Sun, 15 Feb 2026 16:00:00 +0800 Subject: [PATCH 3/4] fix: add missing stdbool.h include in heartbeat.h --- main/heartbeat/heartbeat.h | 1 + 1 file changed, 1 insertion(+) diff --git a/main/heartbeat/heartbeat.h b/main/heartbeat/heartbeat.h index e0af53b..8582c18 100644 --- a/main/heartbeat/heartbeat.h +++ b/main/heartbeat/heartbeat.h @@ -1,6 +1,7 @@ #pragma once #include "esp_err.h" +#include /** * Initialize the heartbeat service (logs ready state). From c120a6fe45738da4dde2b5d9a4f92915a167ecbc Mon Sep 17 00:00:00 2001 From: Bo Date: Mon, 16 Feb 2026 23:00:00 +0800 Subject: [PATCH 4/4] fix cron/heartbeat e2e stability and build issues --- .gitignore | 3 +-- main/cli/serial_cli.c | 54 ++++++++++++++++++++++++++++++++++++++++ main/cron/cron_service.c | 54 +++++++++++++++++++--------------------- main/llm/llm_proxy.c | 31 ++++++++++++++--------- main/tools/tool_cron.c | 11 +++++--- main/tools/tool_files.c | 1 + 6 files changed, 108 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index cae43d8..63f5ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -45,5 +45,4 @@ nanobot/ # OS .DS_Store Thumbs.db - -references/ \ No newline at end of file +references/ diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 2ae63cc..88e0eab 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -7,6 +7,8 @@ #include "memory/session_mgr.h" #include "proxy/http_proxy.h" #include "tools/tool_web_search.h" +#include "tools/tool_registry.h" +#include "cron/cron_service.h" #include "heartbeat/heartbeat.h" #include @@ -329,6 +331,42 @@ static int cmd_heartbeat_trigger(int argc, char **argv) return 0; } +/* --- cron_start command --- */ +static int cmd_cron_start(int argc, char **argv) +{ + esp_err_t err = cron_service_start(); + if (err == ESP_OK) { + printf("Cron service started.\n"); + return 0; + } + + printf("Failed to start cron service: %s\n", esp_err_to_name(err)); + return 1; +} + +static int cmd_tool_exec(int argc, char **argv) +{ + if (argc < 2) { + printf("Usage: tool_exec [json]\n"); + return 1; + } + + const char *tool_name = argv[1]; + const char *input_json = (argc >= 3) ? argv[2] : "{}"; + + char *output = calloc(1, 4096); + if (!output) { + printf("Out of memory.\n"); + return 1; + } + + esp_err_t err = tool_registry_execute(tool_name, input_json, output, 4096); + printf("tool_exec status: %s\n", esp_err_to_name(err)); + printf("%s\n", output[0] ? output : "(empty)"); + free(output); + return (err == ESP_OK) ? 0 : 1; +} + /* --- restart command --- */ static int cmd_restart(int argc, char **argv) { @@ -526,6 +564,22 @@ esp_err_t serial_cli_init(void) }; esp_console_cmd_register(&heartbeat_cmd); + /* cron_start */ + esp_console_cmd_t cron_start_cmd = { + .command = "cron_start", + .help = "Start cron scheduler timer now", + .func = &cmd_cron_start, + }; + esp_console_cmd_register(&cron_start_cmd); + + /* tool_exec */ + esp_console_cmd_t tool_exec_cmd = { + .command = "tool_exec", + .help = "Execute a registered tool: tool_exec '{...json...}'", + .func = &cmd_tool_exec, + }; + esp_console_cmd_register(&tool_exec_cmd); + /* restart */ esp_console_cmd_t restart_cmd = { .command = "restart", diff --git a/main/cron/cron_service.c b/main/cron/cron_service.c index 6dcab84..58b237f 100644 --- a/main/cron/cron_service.c +++ b/main/cron/cron_service.c @@ -7,7 +7,7 @@ #include #include #include "freertos/FreeRTOS.h" -#include "freertos/timers.h" +#include "freertos/task.h" #include "esp_log.h" #include "esp_random.h" #include "cJSON.h" @@ -18,7 +18,7 @@ static const char *TAG = "cron"; static cron_job_t s_jobs[MAX_CRON_JOBS]; static int s_job_count = 0; -static TimerHandle_t s_cron_timer = NULL; +static TaskHandle_t s_cron_task = NULL; /* ── Persistence ──────────────────────────────────────────────── */ @@ -199,17 +199,11 @@ static esp_err_t cron_save_jobs(void) return ESP_OK; } -/* ── Timer callback ───────────────────────────────────────────── */ +/* ── Due-job processing ───────────────────────────────────────── */ -static void cron_timer_callback(TimerHandle_t xTimer) +static void cron_process_due_jobs(void) { - (void)xTimer; - time_t now = time(NULL); - if (now < 1700000000) { - /* System time not set yet (before ~2023), skip */ - return; - } bool changed = false; @@ -267,6 +261,16 @@ static void cron_timer_callback(TimerHandle_t xTimer) } } +static void cron_task_main(void *arg) +{ + (void)arg; + + while (1) { + vTaskDelay(pdMS_TO_TICKS(MIMI_CRON_CHECK_INTERVAL_MS)); + cron_process_due_jobs(); + } +} + /* ── Compute initial next_run for a new job ───────────────────── */ static void compute_initial_next_run(cron_job_t *job) @@ -295,8 +299,8 @@ esp_err_t cron_service_init(void) esp_err_t cron_service_start(void) { - if (s_cron_timer) { - ESP_LOGW(TAG, "Cron timer already running"); + if (s_cron_task) { + ESP_LOGW(TAG, "Cron task already running"); return ESP_OK; } @@ -313,21 +317,16 @@ esp_err_t cron_service_start(void) } } - s_cron_timer = xTimerCreate( + BaseType_t ok = xTaskCreate( + cron_task_main, "cron", - pdMS_TO_TICKS(MIMI_CRON_CHECK_INTERVAL_MS), - pdTRUE, /* auto-reload */ + 4096, NULL, - cron_timer_callback + 4, + &s_cron_task ); - - if (!s_cron_timer) { - ESP_LOGE(TAG, "Failed to create cron timer"); - return ESP_FAIL; - } - - if (xTimerStart(s_cron_timer, pdMS_TO_TICKS(1000)) != pdPASS) { - ESP_LOGE(TAG, "Failed to start cron timer"); + if (ok != pdPASS || !s_cron_task) { + ESP_LOGE(TAG, "Failed to create cron task"); return ESP_FAIL; } @@ -338,10 +337,9 @@ esp_err_t cron_service_start(void) void cron_service_stop(void) { - if (s_cron_timer) { - xTimerStop(s_cron_timer, pdMS_TO_TICKS(1000)); - xTimerDelete(s_cron_timer, pdMS_TO_TICKS(1000)); - s_cron_timer = NULL; + if (s_cron_task) { + vTaskDelete(s_cron_task); + s_cron_task = NULL; ESP_LOGI(TAG, "Cron service stopped"); } } diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index 2835cd4..11fd3ca 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -13,9 +13,13 @@ static const char *TAG = "llm"; -static char s_api_key[128] = {0}; -static char s_model[64] = MIMI_LLM_DEFAULT_MODEL; -static char s_provider[16] = MIMI_LLM_PROVIDER_DEFAULT; +#define LLM_API_KEY_MAX_LEN 256 +#define LLM_MODEL_MAX_LEN 64 +#define LLM_PROVIDER_MAX_LEN 16 + +static char s_api_key[LLM_API_KEY_MAX_LEN] = {0}; +static char s_model[LLM_MODEL_MAX_LEN] = MIMI_LLM_DEFAULT_MODEL; +static char s_provider[LLM_PROVIDER_MAX_LEN] = MIMI_LLM_PROVIDER_DEFAULT; static void safe_copy(char *dst, size_t dst_size, const char *src) { @@ -118,20 +122,23 @@ esp_err_t llm_proxy_init(void) /* NVS overrides take highest priority (set via CLI) */ nvs_handle_t nvs; if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) { - char tmp[128] = {0}; + char tmp[LLM_API_KEY_MAX_LEN] = {0}; size_t len = sizeof(tmp); if (nvs_get_str(nvs, MIMI_NVS_KEY_API_KEY, tmp, &len) == ESP_OK && tmp[0]) { safe_copy(s_api_key, sizeof(s_api_key), tmp); } - len = sizeof(tmp); - memset(tmp, 0, sizeof(tmp)); - if (nvs_get_str(nvs, MIMI_NVS_KEY_MODEL, tmp, &len) == ESP_OK && tmp[0]) { - safe_copy(s_model, sizeof(s_model), tmp); } - len = sizeof(tmp); - memset(tmp, 0, sizeof(tmp)); - if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, tmp, &len) == ESP_OK && tmp[0]) { - safe_copy(s_provider, sizeof(s_provider), tmp); + + char model_tmp[LLM_MODEL_MAX_LEN] = {0}; + len = sizeof(model_tmp); + if (nvs_get_str(nvs, MIMI_NVS_KEY_MODEL, model_tmp, &len) == ESP_OK && model_tmp[0]) { + safe_copy(s_model, sizeof(s_model), model_tmp); + } + + char provider_tmp[LLM_PROVIDER_MAX_LEN] = {0}; + len = sizeof(provider_tmp); + if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider_tmp, &len) == ESP_OK && provider_tmp[0]) { + safe_copy(s_provider, sizeof(s_provider), provider_tmp); } nvs_close(nvs); } diff --git a/main/tools/tool_cron.c b/main/tools/tool_cron.c index dfb0a6d..b8b8e25 100644 --- a/main/tools/tool_cron.c +++ b/main/tools/tool_cron.c @@ -170,17 +170,20 @@ esp_err_t tool_cron_remove_execute(const char *input_json, char *output, size_t return ESP_ERR_INVALID_ARG; } - esp_err_t err = cron_remove_job(job_id); + char job_id_copy[16] = {0}; + strncpy(job_id_copy, job_id, sizeof(job_id_copy) - 1); + + esp_err_t err = cron_remove_job(job_id_copy); cJSON_Delete(root); if (err == ESP_OK) { - snprintf(output, output_size, "OK: Removed cron job %s", job_id); + snprintf(output, output_size, "OK: Removed cron job %s", job_id_copy); } else if (err == ESP_ERR_NOT_FOUND) { - snprintf(output, output_size, "Error: job '%s' not found", job_id); + snprintf(output, output_size, "Error: job '%s' not found", job_id_copy); } else { snprintf(output, output_size, "Error: failed to remove job (%s)", esp_err_to_name(err)); } - ESP_LOGI(TAG, "cron_remove: %s -> %s", job_id, esp_err_to_name(err)); + ESP_LOGI(TAG, "cron_remove: %s -> %s", job_id_copy, esp_err_to_name(err)); return err; } diff --git a/main/tools/tool_files.c b/main/tools/tool_files.c index 7bf16cc..358f060 100644 --- a/main/tools/tool_files.c +++ b/main/tools/tool_files.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "esp_log.h"