feat: add cron scheduled task service with tool_use integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
crispyberry
2026-02-09 01:02:33 +08:00
committed by Bo
parent 680f41133a
commit dbab65bd47
10 changed files with 744 additions and 2 deletions

View File

@@ -26,6 +26,8 @@ idf_component_register(
"tools/tool_web_search.c" "tools/tool_web_search.c"
"tools/tool_get_time.c" "tools/tool_get_time.c"
"tools/tool_files.c" "tools/tool_files.c"
"tools/tool_cron.c"
"cron/cron_service.c"
INCLUDE_DIRS INCLUDE_DIRS
"." "."
EMBED_FILES EMBED_FILES

View File

@@ -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" "- read_file: Read a file from SPIFFS (path must start with /spiffs/).\n"
"- write_file: Write/overwrite a file on SPIFFS.\n" "- write_file: Write/overwrite a file on SPIFFS.\n"
"- edit_file: Find-and-replace edit 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" "Use tools when needed. Provide your final answer as text after using tools.\n\n"
"## Memory\n" "## Memory\n"
"You have persistent memory stored on local flash:\n" "You have persistent memory stored on local flash:\n"

View File

@@ -8,6 +8,7 @@
#define MIMI_CHAN_TELEGRAM "telegram" #define MIMI_CHAN_TELEGRAM "telegram"
#define MIMI_CHAN_WEBSOCKET "websocket" #define MIMI_CHAN_WEBSOCKET "websocket"
#define MIMI_CHAN_CLI "cli" #define MIMI_CHAN_CLI "cli"
#define MIMI_CHAN_SYSTEM "system"
/* Message types on the bus */ /* Message types on the bus */
typedef struct { typedef struct {

410
main/cron/cron_service.c Normal file
View File

@@ -0,0 +1,410 @@
#include "cron/cron_service.h"
#include "mimi_config.h"
#include "bus/message_bus.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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;
}

63
main/cron/cron_service.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/* 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);

View File

@@ -26,6 +26,7 @@
#include "ui/config_screen.h" #include "ui/config_screen.h"
#include "imu/imu_manager.h" #include "imu/imu_manager.h"
#include "rgb/rgb.h" #include "rgb/rgb.h"
#include "cron/cron_service.h"
static const char *TAG = "mimi"; static const char *TAG = "mimi";
@@ -77,6 +78,8 @@ static void outbound_dispatch_task(void *arg)
telegram_send_message(msg.chat_id, msg.content); telegram_send_message(msg.chat_id, msg.content);
} else if (strcmp(msg.channel, MIMI_CHAN_WEBSOCKET) == 0) { } else if (strcmp(msg.channel, MIMI_CHAN_WEBSOCKET) == 0) {
ws_server_send(msg.chat_id, msg.content); 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 { } else {
ESP_LOGW(TAG, "Unknown channel: %s", msg.channel); 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(telegram_bot_init());
ESP_ERROR_CHECK(llm_proxy_init()); ESP_ERROR_CHECK(llm_proxy_init());
ESP_ERROR_CHECK(tool_registry_init()); ESP_ERROR_CHECK(tool_registry_init());
ESP_ERROR_CHECK(cron_service_init());
ESP_ERROR_CHECK(agent_loop_init()); ESP_ERROR_CHECK(agent_loop_init());
/* Start Serial CLI first (works without WiFi) */ /* Start Serial CLI first (works without WiFi) */
@@ -141,6 +145,7 @@ void app_main(void)
/* Start network-dependent services */ /* Start network-dependent services */
ESP_ERROR_CHECK(telegram_bot_start()); ESP_ERROR_CHECK(telegram_bot_start());
ESP_ERROR_CHECK(agent_loop_start()); ESP_ERROR_CHECK(agent_loop_start());
cron_service_start();
ESP_ERROR_CHECK(ws_server_start()); ESP_ERROR_CHECK(ws_server_start());
/* Outbound dispatch task */ /* Outbound dispatch task */

View File

@@ -84,6 +84,11 @@
#define MIMI_CONTEXT_BUF_SIZE (16 * 1024) #define MIMI_CONTEXT_BUF_SIZE (16 * 1024)
#define MIMI_SESSION_MAX_MSGS 20 #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 */ /* WebSocket Gateway */
#define MIMI_WS_PORT 18789 #define MIMI_WS_PORT 18789
#define MIMI_WS_MAX_CLIENTS 4 #define MIMI_WS_MAX_CLIENTS 4

186
main/tools/tool_cron.c Normal file
View File

@@ -0,0 +1,186 @@
#include "tools/tool_cron.h"
#include "cron/cron_service.h"
#include <string.h>
#include <time.h>
#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;
}

22
main/tools/tool_cron.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include "esp_err.h"
#include <stddef.h>
/**
* 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);

View File

@@ -2,6 +2,7 @@
#include "tools/tool_web_search.h" #include "tools/tool_web_search.h"
#include "tools/tool_get_time.h" #include "tools/tool_get_time.h"
#include "tools/tool_files.h" #include "tools/tool_files.h"
#include "tools/tool_cron.h"
#include <string.h> #include <string.h>
#include "esp_log.h" #include "esp_log.h"
@@ -9,7 +10,7 @@
static const char *TAG = "tools"; static const char *TAG = "tools";
#define MAX_TOOLS 8 #define MAX_TOOLS 12
static mimi_tool_t s_tools[MAX_TOOLS]; static mimi_tool_t s_tools[MAX_TOOLS];
static int s_tool_count = 0; static int s_tool_count = 0;
@@ -130,6 +131,50 @@ esp_err_t tool_registry_init(void)
}; };
register_tool(&ld); 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(); build_tools_json();
ESP_LOGI(TAG, "Tool registry initialized"); ESP_LOGI(TAG, "Tool registry initialized");