Merge pull request #55 from IRONICBo/fix/fix-unexcept-in-tg
This commit is contained in:
@@ -22,7 +22,10 @@ idf_component_register(
|
|||||||
"cli/serial_cli.c"
|
"cli/serial_cli.c"
|
||||||
"ota/ota_manager.c"
|
"ota/ota_manager.c"
|
||||||
"proxy/http_proxy.c"
|
"proxy/http_proxy.c"
|
||||||
|
"cron/cron_service.c"
|
||||||
|
"heartbeat/heartbeat.c"
|
||||||
"tools/tool_registry.c"
|
"tools/tool_registry.c"
|
||||||
|
"tools/tool_cron.c"
|
||||||
"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"
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_random.h"
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
static const char *TAG = "agent";
|
static const char *TAG = "agent";
|
||||||
@@ -52,17 +53,107 @@ static cJSON *build_assistant_content(const llm_response_t *resp)
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void json_set_string(cJSON *obj, const char *key, const char *value)
|
||||||
|
{
|
||||||
|
if (!obj || !key || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cJSON_DeleteItemFromObject(obj, key);
|
||||||
|
cJSON_AddStringToObject(obj, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void append_turn_context_prompt(char *prompt, size_t size, const mimi_msg_t *msg)
|
||||||
|
{
|
||||||
|
if (!prompt || size == 0 || !msg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t off = strnlen(prompt, size - 1);
|
||||||
|
if (off >= size - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = snprintf(
|
||||||
|
prompt + off, size - off,
|
||||||
|
"\n## Current Turn Context\n"
|
||||||
|
"- source_channel: %s\n"
|
||||||
|
"- source_chat_id: %s\n"
|
||||||
|
"- If using cron_add for Telegram in this turn, set channel='telegram' and chat_id to source_chat_id.\n"
|
||||||
|
"- Never use chat_id 'cron' for Telegram messages.\n",
|
||||||
|
msg->channel[0] ? msg->channel : "(unknown)",
|
||||||
|
msg->chat_id[0] ? msg->chat_id : "(empty)");
|
||||||
|
|
||||||
|
if (n < 0 || (size_t)n >= (size - off)) {
|
||||||
|
prompt[size - 1] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *patch_tool_input_with_context(const llm_tool_call_t *call, const mimi_msg_t *msg)
|
||||||
|
{
|
||||||
|
if (!call || !msg || strcmp(call->name, "cron_add") != 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(call->input ? call->input : "{}");
|
||||||
|
if (!root || !cJSON_IsObject(root)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
root = cJSON_CreateObject();
|
||||||
|
}
|
||||||
|
if (!root) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
cJSON *channel_item = cJSON_GetObjectItem(root, "channel");
|
||||||
|
const char *channel = cJSON_IsString(channel_item) ? channel_item->valuestring : NULL;
|
||||||
|
|
||||||
|
if ((!channel || channel[0] == '\0') && msg->channel[0] != '\0') {
|
||||||
|
json_set_string(root, "channel", msg->channel);
|
||||||
|
channel = msg->channel;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel && strcmp(channel, MIMI_CHAN_TELEGRAM) == 0 &&
|
||||||
|
strcmp(msg->channel, MIMI_CHAN_TELEGRAM) == 0 && msg->chat_id[0] != '\0') {
|
||||||
|
cJSON *chat_item = cJSON_GetObjectItem(root, "chat_id");
|
||||||
|
const char *chat_id = cJSON_IsString(chat_item) ? chat_item->valuestring : NULL;
|
||||||
|
if (!chat_id || chat_id[0] == '\0' || strcmp(chat_id, "cron") == 0) {
|
||||||
|
json_set_string(root, "chat_id", msg->chat_id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char *patched = NULL;
|
||||||
|
if (changed) {
|
||||||
|
patched = cJSON_PrintUnformatted(root);
|
||||||
|
if (patched) {
|
||||||
|
ESP_LOGI(TAG, "Patched cron_add target to %s:%s", msg->channel, msg->chat_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return patched;
|
||||||
|
}
|
||||||
|
|
||||||
/* Build the user message with tool_result blocks */
|
/* Build the user message with tool_result blocks */
|
||||||
static cJSON *build_tool_results(const llm_response_t *resp, char *tool_output, size_t tool_output_size)
|
static cJSON *build_tool_results(const llm_response_t *resp, const mimi_msg_t *msg,
|
||||||
|
char *tool_output, size_t tool_output_size)
|
||||||
{
|
{
|
||||||
cJSON *content = cJSON_CreateArray();
|
cJSON *content = cJSON_CreateArray();
|
||||||
|
|
||||||
for (int i = 0; i < resp->call_count; i++) {
|
for (int i = 0; i < resp->call_count; i++) {
|
||||||
const llm_tool_call_t *call = &resp->calls[i];
|
const llm_tool_call_t *call = &resp->calls[i];
|
||||||
|
const char *tool_input = call->input ? call->input : "{}";
|
||||||
|
char *patched_input = patch_tool_input_with_context(call, msg);
|
||||||
|
if (patched_input) {
|
||||||
|
tool_input = patched_input;
|
||||||
|
}
|
||||||
|
|
||||||
/* Execute tool */
|
/* Execute tool */
|
||||||
tool_output[0] = '\0';
|
tool_output[0] = '\0';
|
||||||
tool_registry_execute(call->name, call->input, tool_output, tool_output_size);
|
tool_registry_execute(call->name, tool_input, tool_output, tool_output_size);
|
||||||
|
free(patched_input);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Tool %s result: %d bytes", call->name, (int)strlen(tool_output));
|
ESP_LOGI(TAG, "Tool %s result: %d bytes", call->name, (int)strlen(tool_output));
|
||||||
|
|
||||||
@@ -103,6 +194,8 @@ static void agent_loop_task(void *arg)
|
|||||||
|
|
||||||
/* 1. Build system prompt */
|
/* 1. Build system prompt */
|
||||||
context_build_system_prompt(system_prompt, MIMI_CONTEXT_BUF_SIZE);
|
context_build_system_prompt(system_prompt, MIMI_CONTEXT_BUF_SIZE);
|
||||||
|
append_turn_context_prompt(system_prompt, MIMI_CONTEXT_BUF_SIZE, &msg);
|
||||||
|
ESP_LOGI(TAG, "LLM turn context: channel=%s chat_id=%s", msg.channel, msg.chat_id);
|
||||||
|
|
||||||
/* 2. Load session history into cJSON array */
|
/* 2. Load session history into cJSON array */
|
||||||
session_get_history_json(msg.chat_id, history_json,
|
session_get_history_json(msg.chat_id, history_json,
|
||||||
@@ -120,24 +213,26 @@ static void agent_loop_task(void *arg)
|
|||||||
/* 4. ReAct loop */
|
/* 4. ReAct loop */
|
||||||
char *final_text = NULL;
|
char *final_text = NULL;
|
||||||
int iteration = 0;
|
int iteration = 0;
|
||||||
|
bool sent_working_status = false;
|
||||||
|
|
||||||
while (iteration < MIMI_AGENT_MAX_TOOL_ITER) {
|
while (iteration < MIMI_AGENT_MAX_TOOL_ITER) {
|
||||||
/* Send "working" indicator before each API call */
|
/* Send "working" indicator before each API call */
|
||||||
{
|
#if MIMI_AGENT_SEND_WORKING_STATUS
|
||||||
static const char *working_phrases[] = {
|
if (!sent_working_status && strcmp(msg.channel, MIMI_CHAN_SYSTEM) != 0) {
|
||||||
"mimi\xF0\x9F\x98\x97is working...",
|
|
||||||
"mimi\xF0\x9F\x90\xBE is thinking...",
|
|
||||||
"mimi\xF0\x9F\x92\xAD is pondering...",
|
|
||||||
"mimi\xF0\x9F\x8C\x99 is on it...",
|
|
||||||
"mimi\xE2\x9C\xA8 is cooking...",
|
|
||||||
};
|
|
||||||
const int phrase_count = sizeof(working_phrases) / sizeof(working_phrases[0]);
|
|
||||||
mimi_msg_t status = {0};
|
mimi_msg_t status = {0};
|
||||||
strncpy(status.channel, msg.channel, sizeof(status.channel) - 1);
|
strncpy(status.channel, msg.channel, sizeof(status.channel) - 1);
|
||||||
strncpy(status.chat_id, msg.chat_id, sizeof(status.chat_id) - 1);
|
strncpy(status.chat_id, msg.chat_id, sizeof(status.chat_id) - 1);
|
||||||
status.content = strdup(working_phrases[esp_random() % phrase_count]);
|
status.content = strdup("\xF0\x9F\x90\xB1mimi is working...");
|
||||||
if (status.content) message_bus_push_outbound(&status);
|
if (status.content) {
|
||||||
|
if (message_bus_push_outbound(&status) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Outbound queue full, drop working status");
|
||||||
|
free(status.content);
|
||||||
|
} else {
|
||||||
|
sent_working_status = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
llm_response_t resp;
|
llm_response_t resp;
|
||||||
err = llm_chat_tools(system_prompt, messages, tools_json, &resp);
|
err = llm_chat_tools(system_prompt, messages, tools_json, &resp);
|
||||||
@@ -165,7 +260,7 @@ static void agent_loop_task(void *arg)
|
|||||||
cJSON_AddItemToArray(messages, asst_msg);
|
cJSON_AddItemToArray(messages, asst_msg);
|
||||||
|
|
||||||
/* Execute tools and append results */
|
/* Execute tools and append results */
|
||||||
cJSON *tool_results = build_tool_results(&resp, tool_output, TOOL_OUTPUT_SIZE);
|
cJSON *tool_results = build_tool_results(&resp, &msg, tool_output, TOOL_OUTPUT_SIZE);
|
||||||
cJSON *result_msg = cJSON_CreateObject();
|
cJSON *result_msg = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(result_msg, "role", "user");
|
cJSON_AddStringToObject(result_msg, "role", "user");
|
||||||
cJSON_AddItemToObject(result_msg, "content", tool_results);
|
cJSON_AddItemToObject(result_msg, "content", tool_results);
|
||||||
@@ -180,15 +275,30 @@ static void agent_loop_task(void *arg)
|
|||||||
/* 5. Send response */
|
/* 5. Send response */
|
||||||
if (final_text && final_text[0]) {
|
if (final_text && final_text[0]) {
|
||||||
/* Save to session (only user text + final assistant text) */
|
/* Save to session (only user text + final assistant text) */
|
||||||
session_append(msg.chat_id, "user", msg.content);
|
esp_err_t save_user = session_append(msg.chat_id, "user", msg.content);
|
||||||
session_append(msg.chat_id, "assistant", final_text);
|
esp_err_t save_asst = session_append(msg.chat_id, "assistant", final_text);
|
||||||
|
if (save_user != ESP_OK || save_asst != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Session save failed for chat %s (user=%s, assistant=%s)",
|
||||||
|
msg.chat_id,
|
||||||
|
esp_err_to_name(save_user),
|
||||||
|
esp_err_to_name(save_asst));
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Session saved for chat %s", msg.chat_id);
|
||||||
|
}
|
||||||
|
|
||||||
/* Push response to outbound */
|
/* Push response to outbound */
|
||||||
mimi_msg_t out = {0};
|
mimi_msg_t out = {0};
|
||||||
strncpy(out.channel, msg.channel, sizeof(out.channel) - 1);
|
strncpy(out.channel, msg.channel, sizeof(out.channel) - 1);
|
||||||
strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1);
|
strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1);
|
||||||
out.content = final_text; /* transfer ownership */
|
out.content = final_text; /* transfer ownership */
|
||||||
message_bus_push_outbound(&out);
|
ESP_LOGI(TAG, "Queue final response to %s:%s (%d bytes)",
|
||||||
|
out.channel, out.chat_id, (int)strlen(final_text));
|
||||||
|
if (message_bus_push_outbound(&out) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Outbound queue full, drop final response");
|
||||||
|
free(final_text);
|
||||||
|
} else {
|
||||||
|
final_text = NULL;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Error or empty response */
|
/* Error or empty response */
|
||||||
free(final_text);
|
free(final_text);
|
||||||
@@ -197,7 +307,10 @@ static void agent_loop_task(void *arg)
|
|||||||
strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1);
|
strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1);
|
||||||
out.content = strdup("Sorry, I encountered an error.");
|
out.content = strdup("Sorry, I encountered an error.");
|
||||||
if (out.content) {
|
if (out.content) {
|
||||||
message_bus_push_outbound(&out);
|
if (message_bus_push_outbound(&out) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Outbound queue full, drop error response");
|
||||||
|
free(out.content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,10 +331,32 @@ esp_err_t agent_loop_init(void)
|
|||||||
|
|
||||||
esp_err_t agent_loop_start(void)
|
esp_err_t agent_loop_start(void)
|
||||||
{
|
{
|
||||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
const uint32_t stack_candidates[] = {
|
||||||
agent_loop_task, "agent_loop",
|
MIMI_AGENT_STACK,
|
||||||
MIMI_AGENT_STACK, NULL,
|
20 * 1024,
|
||||||
MIMI_AGENT_PRIO, NULL, MIMI_AGENT_CORE);
|
16 * 1024,
|
||||||
|
14 * 1024,
|
||||||
|
12 * 1024,
|
||||||
|
};
|
||||||
|
|
||||||
return (ret == pdPASS) ? ESP_OK : ESP_FAIL;
|
for (size_t i = 0; i < (sizeof(stack_candidates) / sizeof(stack_candidates[0])); i++) {
|
||||||
|
uint32_t stack_size = stack_candidates[i];
|
||||||
|
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||||
|
agent_loop_task, "agent_loop",
|
||||||
|
stack_size, NULL,
|
||||||
|
MIMI_AGENT_PRIO, NULL, MIMI_AGENT_CORE);
|
||||||
|
|
||||||
|
if (ret == pdPASS) {
|
||||||
|
ESP_LOGI(TAG, "agent_loop task created with stack=%u bytes", (unsigned)stack_size);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"agent_loop create failed (stack=%u, free_internal=%u, largest_internal=%u), retrying...",
|
||||||
|
(unsigned)stack_size,
|
||||||
|
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
|
||||||
|
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
|
|||||||
"- cron_add: Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.\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_list: List all scheduled cron jobs.\n"
|
||||||
"- cron_remove: Remove a scheduled cron job by ID.\n\n"
|
"- cron_remove: Remove a scheduled cron job by ID.\n\n"
|
||||||
|
"When using cron_add for Telegram delivery, always set channel='telegram' and a valid numeric chat_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"
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
#include "memory/memory_store.h"
|
#include "memory/memory_store.h"
|
||||||
#include "memory/session_mgr.h"
|
#include "memory/session_mgr.h"
|
||||||
#include "proxy/http_proxy.h"
|
#include "proxy/http_proxy.h"
|
||||||
|
#include "tools/tool_registry.h"
|
||||||
#include "tools/tool_web_search.h"
|
#include "tools/tool_web_search.h"
|
||||||
|
#include "cron/cron_service.h"
|
||||||
|
#include "heartbeat/heartbeat.h"
|
||||||
#include "skills/skill_loader.h"
|
#include "skills/skill_loader.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|||||||
@@ -20,6 +20,36 @@ static cron_job_t s_jobs[MAX_CRON_JOBS];
|
|||||||
static int s_job_count = 0;
|
static int s_job_count = 0;
|
||||||
static TaskHandle_t s_cron_task = NULL;
|
static TaskHandle_t s_cron_task = NULL;
|
||||||
|
|
||||||
|
static esp_err_t cron_save_jobs(void);
|
||||||
|
|
||||||
|
static bool cron_sanitize_destination(cron_job_t *job)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
if (!job) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job->channel[0] == '\0') {
|
||||||
|
strncpy(job->channel, MIMI_CHAN_SYSTEM, sizeof(job->channel) - 1);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(job->channel, MIMI_CHAN_TELEGRAM) == 0) {
|
||||||
|
if (job->chat_id[0] == '\0' || strcmp(job->chat_id, "cron") == 0) {
|
||||||
|
ESP_LOGW(TAG, "Cron job %s has invalid telegram chat_id, fallback to system:cron",
|
||||||
|
job->id[0] ? job->id : "<new>");
|
||||||
|
strncpy(job->channel, MIMI_CHAN_SYSTEM, sizeof(job->channel) - 1);
|
||||||
|
strncpy(job->chat_id, "cron", sizeof(job->chat_id) - 1);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if (job->chat_id[0] == '\0') {
|
||||||
|
strncpy(job->chat_id, "cron", sizeof(job->chat_id) - 1);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Persistence ──────────────────────────────────────────────── */
|
/* ── Persistence ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
static void cron_generate_id(char *id_buf)
|
static void cron_generate_id(char *id_buf)
|
||||||
@@ -77,6 +107,7 @@ static esp_err_t cron_load_jobs(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
s_job_count = 0;
|
s_job_count = 0;
|
||||||
|
bool repaired = false;
|
||||||
cJSON *item;
|
cJSON *item;
|
||||||
cJSON_ArrayForEach(item, jobs_arr) {
|
cJSON_ArrayForEach(item, jobs_arr) {
|
||||||
if (s_job_count >= MAX_CRON_JOBS) break;
|
if (s_job_count >= MAX_CRON_JOBS) break;
|
||||||
@@ -100,6 +131,9 @@ static esp_err_t cron_load_jobs(void)
|
|||||||
sizeof(job->channel) - 1);
|
sizeof(job->channel) - 1);
|
||||||
strncpy(job->chat_id, chat_id ? chat_id : "cron",
|
strncpy(job->chat_id, chat_id ? chat_id : "cron",
|
||||||
sizeof(job->chat_id) - 1);
|
sizeof(job->chat_id) - 1);
|
||||||
|
if (cron_sanitize_destination(job)) {
|
||||||
|
repaired = true;
|
||||||
|
}
|
||||||
|
|
||||||
cJSON *enabled_j = cJSON_GetObjectItem(item, "enabled");
|
cJSON *enabled_j = cJSON_GetObjectItem(item, "enabled");
|
||||||
job->enabled = enabled_j ? cJSON_IsTrue(enabled_j) : true;
|
job->enabled = enabled_j ? cJSON_IsTrue(enabled_j) : true;
|
||||||
@@ -133,6 +167,9 @@ static esp_err_t cron_load_jobs(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
|
if (repaired) {
|
||||||
|
cron_save_jobs();
|
||||||
|
}
|
||||||
ESP_LOGI(TAG, "Loaded %d cron jobs", s_job_count);
|
ESP_LOGI(TAG, "Loaded %d cron jobs", s_job_count);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -354,13 +391,8 @@ esp_err_t cron_add_job(cron_job_t *job)
|
|||||||
/* Generate ID */
|
/* Generate ID */
|
||||||
cron_generate_id(job->id);
|
cron_generate_id(job->id);
|
||||||
|
|
||||||
/* Set defaults for channel/chat_id if empty */
|
/* Validate/sanitize channel and chat_id before storing. */
|
||||||
if (job->channel[0] == '\0') {
|
cron_sanitize_destination(job);
|
||||||
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 */
|
/* Compute initial next_run */
|
||||||
job->enabled = true;
|
job->enabled = true;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
|
#include "mimi_config.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "freertos/timers.h"
|
||||||
#include "esp_check.h"
|
#include "esp_check.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "driver/ledc.h"
|
#include "driver/ledc.h"
|
||||||
@@ -51,6 +55,18 @@ static const char *TAG = "display";
|
|||||||
static esp_lcd_panel_handle_t panel_handle = NULL;
|
static esp_lcd_panel_handle_t panel_handle = NULL;
|
||||||
static uint8_t backlight_percent = 50;
|
static uint8_t backlight_percent = 50;
|
||||||
static uint16_t *framebuffer = NULL;
|
static uint16_t *framebuffer = NULL;
|
||||||
|
static SemaphoreHandle_t s_display_lock = NULL;
|
||||||
|
static TimerHandle_t s_card_hide_timer = NULL;
|
||||||
|
static uint32_t s_card_generation = 0;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SCREEN_KIND_NONE = 0,
|
||||||
|
SCREEN_KIND_BANNER,
|
||||||
|
SCREEN_KIND_CONFIG,
|
||||||
|
SCREEN_KIND_CARD,
|
||||||
|
} screen_kind_t;
|
||||||
|
|
||||||
|
static screen_kind_t s_screen_kind = SCREEN_KIND_NONE;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int x;
|
int x;
|
||||||
@@ -69,6 +85,48 @@ static inline uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b)
|
|||||||
return (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
|
return (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool display_lock_take(TickType_t wait_ticks)
|
||||||
|
{
|
||||||
|
if (!s_display_lock) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return xSemaphoreTake(s_display_lock, wait_ticks) == pdTRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void display_lock_give(void)
|
||||||
|
{
|
||||||
|
if (s_display_lock) {
|
||||||
|
xSemaphoreGive(s_display_lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_framebuffer_locked(void)
|
||||||
|
{
|
||||||
|
if (!panel_handle || !framebuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_err_t err = esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, BANNER_W, BANNER_H, framebuffer);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "panel draw failed: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void card_hide_timer_cb(TimerHandle_t timer)
|
||||||
|
{
|
||||||
|
uint32_t generation = (uint32_t)(uintptr_t)pvTimerGetTimerID(timer);
|
||||||
|
bool should_hide = false;
|
||||||
|
|
||||||
|
if (!display_lock_take(pdMS_TO_TICKS(30))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
should_hide = (s_screen_kind == SCREEN_KIND_CARD && s_card_generation == generation);
|
||||||
|
display_lock_give();
|
||||||
|
|
||||||
|
if (should_hide) {
|
||||||
|
display_show_banner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void fb_ensure(void)
|
static void fb_ensure(void)
|
||||||
{
|
{
|
||||||
if (!framebuffer) {
|
if (!framebuffer) {
|
||||||
@@ -208,6 +266,13 @@ esp_err_t display_init(void)
|
|||||||
{
|
{
|
||||||
esp_err_t ret = ESP_OK;
|
esp_err_t ret = ESP_OK;
|
||||||
|
|
||||||
|
if (!s_display_lock) {
|
||||||
|
s_display_lock = xSemaphoreCreateMutex();
|
||||||
|
if (!s_display_lock) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
spi_bus_config_t buscfg = {
|
spi_bus_config_t buscfg = {
|
||||||
.sclk_io_num = LCD_PIN_SCLK,
|
.sclk_io_num = LCD_PIN_SCLK,
|
||||||
.mosi_io_num = LCD_PIN_MOSI,
|
.mosi_io_num = LCD_PIN_MOSI,
|
||||||
@@ -249,13 +314,28 @@ esp_err_t display_init(void)
|
|||||||
backlight_ledc_init();
|
backlight_ledc_init();
|
||||||
display_set_backlight_percent(backlight_percent);
|
display_set_backlight_percent(backlight_percent);
|
||||||
|
|
||||||
|
if (!s_card_hide_timer) {
|
||||||
|
s_card_hide_timer = xTimerCreate("card_hide",
|
||||||
|
pdMS_TO_TICKS(MIMI_TG_CARD_SHOW_MS),
|
||||||
|
pdFALSE, NULL, card_hide_timer_cb);
|
||||||
|
if (!s_card_hide_timer) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void display_show_banner(void)
|
void display_show_banner(void)
|
||||||
{
|
{
|
||||||
|
if (!display_lock_take(pdMS_TO_TICKS(200))) {
|
||||||
|
ESP_LOGW(TAG, "display lock timeout (banner)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!panel_handle) {
|
if (!panel_handle) {
|
||||||
ESP_LOGW(TAG, "display not initialized");
|
ESP_LOGW(TAG, "display not initialized");
|
||||||
|
display_lock_give();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,10 +345,20 @@ void display_show_banner(void)
|
|||||||
size_t expected = (size_t)BANNER_W * (size_t)BANNER_H * 2;
|
size_t expected = (size_t)BANNER_W * (size_t)BANNER_H * 2;
|
||||||
if (len < expected) {
|
if (len < expected) {
|
||||||
ESP_LOGW(TAG, "banner data too small (%u < %u)", (unsigned)len, (unsigned)expected);
|
ESP_LOGW(TAG, "banner data too small (%u < %u)", (unsigned)len, (unsigned)expected);
|
||||||
|
display_lock_give();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, BANNER_W, BANNER_H, start));
|
esp_err_t err = esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, BANNER_W, BANNER_H, start);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "banner draw failed: %s", esp_err_to_name(err));
|
||||||
|
} else {
|
||||||
|
s_screen_kind = SCREEN_KIND_BANNER;
|
||||||
|
}
|
||||||
|
if (s_card_hide_timer) {
|
||||||
|
xTimerStop(s_card_hide_timer, 0);
|
||||||
|
}
|
||||||
|
display_lock_give();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void qr_draw_cb(esp_qrcode_handle_t qrcode)
|
static void qr_draw_cb(esp_qrcode_handle_t qrcode)
|
||||||
@@ -296,17 +386,25 @@ void display_show_config_screen(const char *qr_text, const char *ip_text,
|
|||||||
const char **lines, size_t line_count, size_t scroll,
|
const char **lines, size_t line_count, size_t scroll,
|
||||||
size_t selected, int selected_offset_px)
|
size_t selected, int selected_offset_px)
|
||||||
{
|
{
|
||||||
if (!panel_handle) {
|
if (!qr_text || !ip_text || !lines) {
|
||||||
ESP_LOGW(TAG, "display not initialized");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!qr_text || !ip_text || !lines) {
|
|
||||||
|
if (!display_lock_take(pdMS_TO_TICKS(200))) {
|
||||||
|
ESP_LOGW(TAG, "display lock timeout (config)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!panel_handle) {
|
||||||
|
ESP_LOGW(TAG, "display not initialized");
|
||||||
|
display_lock_give();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fb_ensure();
|
fb_ensure();
|
||||||
if (!framebuffer) {
|
if (!framebuffer) {
|
||||||
ESP_LOGW(TAG, "framebuffer alloc failed");
|
ESP_LOGW(TAG, "framebuffer alloc failed");
|
||||||
|
display_lock_give();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +466,106 @@ void display_show_config_screen(const char *qr_text, const char *ip_text,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, BANNER_W, BANNER_H, framebuffer));
|
draw_framebuffer_locked();
|
||||||
|
s_screen_kind = SCREEN_KIND_CONFIG;
|
||||||
|
if (s_card_hide_timer) {
|
||||||
|
xTimerStop(s_card_hide_timer, 0);
|
||||||
|
}
|
||||||
|
display_lock_give();
|
||||||
|
}
|
||||||
|
|
||||||
|
void display_show_message_card(const char *title, const char *body)
|
||||||
|
{
|
||||||
|
if (!title || !body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!display_lock_take(pdMS_TO_TICKS(200))) {
|
||||||
|
ESP_LOGW(TAG, "display lock timeout (card)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!panel_handle) {
|
||||||
|
display_lock_give();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fb_ensure();
|
||||||
|
if (!framebuffer) {
|
||||||
|
ESP_LOGW(TAG, "framebuffer alloc failed");
|
||||||
|
display_lock_give();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t color_bg = rgb565(0, 0, 0);
|
||||||
|
const uint16_t color_title = rgb565(100, 200, 255);
|
||||||
|
const uint16_t color_fg = rgb565(255, 255, 255);
|
||||||
|
|
||||||
|
const int body_scale = (MIMI_TG_CARD_BODY_SCALE < 1) ? 1 : MIMI_TG_CARD_BODY_SCALE;
|
||||||
|
const int title_scale = 2;
|
||||||
|
const int title_line_h = (FONT5X7_HEIGHT + 1) * title_scale;
|
||||||
|
const int body_line_h = (FONT5X7_HEIGHT + 1) * body_scale + 1;
|
||||||
|
const int body_y = 10 + title_line_h;
|
||||||
|
const int max_cols = (BANNER_W - 12) / ((FONT5X7_WIDTH + 1) * body_scale);
|
||||||
|
int max_lines = (BANNER_H - body_y - 6) / body_line_h;
|
||||||
|
if (max_lines < 1) {
|
||||||
|
max_lines = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fb_fill_rect(0, 0, BANNER_W, BANNER_H, color_bg);
|
||||||
|
fb_draw_text_clipped(6, 6, title, color_title, title_line_h, title_scale, 0, BANNER_W);
|
||||||
|
|
||||||
|
char wrapped[512];
|
||||||
|
size_t w = 0;
|
||||||
|
int cols = 0;
|
||||||
|
int lines = 1;
|
||||||
|
|
||||||
|
for (size_t i = 0; body[i] != '\0'; i++) {
|
||||||
|
if (w >= sizeof(wrapped) - 2 || lines > max_lines) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = body[i];
|
||||||
|
if (c == '\r') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '\n') {
|
||||||
|
wrapped[w++] = '\n';
|
||||||
|
cols = 0;
|
||||||
|
lines++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cols >= max_cols) {
|
||||||
|
wrapped[w++] = '\n';
|
||||||
|
cols = 0;
|
||||||
|
lines++;
|
||||||
|
if (lines > max_lines || w >= sizeof(wrapped) - 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wrapped[w++] = c;
|
||||||
|
cols++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w == 0) {
|
||||||
|
strncpy(wrapped, "(empty)", sizeof(wrapped) - 1);
|
||||||
|
wrapped[sizeof(wrapped) - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
wrapped[w] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
fb_draw_text_clipped(6, body_y, wrapped, color_fg, body_line_h, body_scale, 0, BANNER_W);
|
||||||
|
draw_framebuffer_locked();
|
||||||
|
|
||||||
|
s_screen_kind = SCREEN_KIND_CARD;
|
||||||
|
s_card_generation++;
|
||||||
|
uint32_t generation = s_card_generation;
|
||||||
|
if (s_card_hide_timer) {
|
||||||
|
vTimerSetTimerID(s_card_hide_timer, (void *)(uintptr_t)generation);
|
||||||
|
xTimerStop(s_card_hide_timer, 0);
|
||||||
|
xTimerChangePeriod(s_card_hide_timer, pdMS_TO_TICKS(MIMI_TG_CARD_SHOW_MS), 0);
|
||||||
|
xTimerStart(s_card_hide_timer, 0);
|
||||||
|
}
|
||||||
|
display_lock_give();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool display_get_banner_center_rgb(uint8_t *r, uint8_t *g, uint8_t *b)
|
bool display_get_banner_center_rgb(uint8_t *r, uint8_t *g, uint8_t *b)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ bool display_get_banner_center_rgb(uint8_t *r, uint8_t *g, uint8_t *b);
|
|||||||
void display_show_config_screen(const char *qr_text, const char *ip_text,
|
void display_show_config_screen(const char *qr_text, const char *ip_text,
|
||||||
const char **lines, size_t line_count, size_t scroll,
|
const char **lines, size_t line_count, size_t scroll,
|
||||||
size_t selected, int selected_offset_px);
|
size_t selected, int selected_offset_px);
|
||||||
|
void display_show_message_card(const char *title, const char *body);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,60 @@ static const char *TAG = "llm";
|
|||||||
|
|
||||||
#define LLM_API_KEY_MAX_LEN 320
|
#define LLM_API_KEY_MAX_LEN 320
|
||||||
#define LLM_MODEL_MAX_LEN 64
|
#define LLM_MODEL_MAX_LEN 64
|
||||||
|
#define LLM_DUMP_MAX_BYTES (16 * 1024)
|
||||||
|
#define LLM_DUMP_CHUNK_BYTES 320
|
||||||
|
|
||||||
static char s_api_key[LLM_API_KEY_MAX_LEN] = {0};
|
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_model[LLM_MODEL_MAX_LEN] = MIMI_LLM_DEFAULT_MODEL;
|
||||||
static char s_provider[16] = MIMI_LLM_PROVIDER_DEFAULT;
|
static char s_provider[16] = MIMI_LLM_PROVIDER_DEFAULT;
|
||||||
|
|
||||||
|
static void llm_log_payload(const char *label, const char *payload)
|
||||||
|
{
|
||||||
|
if (!payload) {
|
||||||
|
ESP_LOGI(TAG, "%s: <null>", label);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t total = strlen(payload);
|
||||||
|
#if MIMI_LLM_LOG_VERBOSE_PAYLOAD
|
||||||
|
size_t shown = total > LLM_DUMP_MAX_BYTES ? LLM_DUMP_MAX_BYTES : total;
|
||||||
|
ESP_LOGI(TAG, "%s (%u bytes)%s",
|
||||||
|
label,
|
||||||
|
(unsigned)total,
|
||||||
|
(shown < total) ? " [truncated]" : "");
|
||||||
|
|
||||||
|
char chunk[LLM_DUMP_CHUNK_BYTES + 1];
|
||||||
|
for (size_t off = 0; off < shown; off += LLM_DUMP_CHUNK_BYTES) {
|
||||||
|
size_t n = shown - off;
|
||||||
|
if (n > LLM_DUMP_CHUNK_BYTES) {
|
||||||
|
n = LLM_DUMP_CHUNK_BYTES;
|
||||||
|
}
|
||||||
|
memcpy(chunk, payload + off, n);
|
||||||
|
chunk[n] = '\0';
|
||||||
|
ESP_LOGI(TAG, "%s[%u]: %s", label, (unsigned)off, chunk);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (MIMI_LLM_LOG_PREVIEW_BYTES > 0) {
|
||||||
|
size_t shown = total > MIMI_LLM_LOG_PREVIEW_BYTES ? MIMI_LLM_LOG_PREVIEW_BYTES : total;
|
||||||
|
char preview[MIMI_LLM_LOG_PREVIEW_BYTES + 1];
|
||||||
|
memcpy(preview, payload, shown);
|
||||||
|
preview[shown] = '\0';
|
||||||
|
for (size_t i = 0; i < shown; i++) {
|
||||||
|
if (preview[i] == '\n' || preview[i] == '\r' || preview[i] == '\t') {
|
||||||
|
preview[i] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "%s (%u bytes): %s%s",
|
||||||
|
label,
|
||||||
|
(unsigned)total,
|
||||||
|
preview,
|
||||||
|
(shown < total) ? " ..." : "");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "%s (%u bytes)", label, (unsigned)total);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static void safe_copy(char *dst, size_t dst_size, const char *src)
|
static void safe_copy(char *dst, size_t dst_size, const char *src)
|
||||||
{
|
{
|
||||||
if (!dst || dst_size == 0) return;
|
if (!dst || dst_size == 0) return;
|
||||||
@@ -483,7 +532,11 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
|||||||
/* Build request body (non-streaming) */
|
/* Build request body (non-streaming) */
|
||||||
cJSON *body = cJSON_CreateObject();
|
cJSON *body = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(body, "model", s_model);
|
cJSON_AddStringToObject(body, "model", s_model);
|
||||||
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
|
if (provider_is_openai()) {
|
||||||
|
cJSON_AddNumberToObject(body, "max_completion_tokens", MIMI_LLM_MAX_TOKENS);
|
||||||
|
} else {
|
||||||
|
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
|
||||||
|
}
|
||||||
|
|
||||||
if (provider_is_openai()) {
|
if (provider_is_openai()) {
|
||||||
cJSON *messages = cJSON_Parse(messages_json);
|
cJSON *messages = cJSON_Parse(messages_json);
|
||||||
@@ -521,6 +574,7 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
|||||||
|
|
||||||
ESP_LOGI(TAG, "Calling LLM API (provider: %s, model: %s, body: %d bytes)",
|
ESP_LOGI(TAG, "Calling LLM API (provider: %s, model: %s, body: %d bytes)",
|
||||||
s_provider, s_model, (int)strlen(post_data));
|
s_provider, s_model, (int)strlen(post_data));
|
||||||
|
llm_log_payload("LLM request", post_data);
|
||||||
|
|
||||||
resp_buf_t rb;
|
resp_buf_t rb;
|
||||||
if (resp_buf_init(&rb, MIMI_LLM_STREAM_BUF_SIZE) != ESP_OK) {
|
if (resp_buf_init(&rb, MIMI_LLM_STREAM_BUF_SIZE) != ESP_OK) {
|
||||||
@@ -535,12 +589,15 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
|||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
|
||||||
|
llm_log_payload("LLM partial response", rb.data);
|
||||||
resp_buf_free(&rb);
|
resp_buf_free(&rb);
|
||||||
snprintf(response_buf, buf_size, "Error: HTTP request failed (%s)",
|
snprintf(response_buf, buf_size, "Error: HTTP request failed (%s)",
|
||||||
esp_err_to_name(err));
|
esp_err_to_name(err));
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
llm_log_payload("LLM raw response", rb.data);
|
||||||
|
|
||||||
if (status != 200) {
|
if (status != 200) {
|
||||||
ESP_LOGE(TAG, "API returned status %d", status);
|
ESP_LOGE(TAG, "API returned status %d", status);
|
||||||
snprintf(response_buf, buf_size, "API error (HTTP %d): %.200s",
|
snprintf(response_buf, buf_size, "API error (HTTP %d): %.200s",
|
||||||
@@ -601,7 +658,11 @@ esp_err_t llm_chat_tools(const char *system_prompt,
|
|||||||
/* Build request body (non-streaming) */
|
/* Build request body (non-streaming) */
|
||||||
cJSON *body = cJSON_CreateObject();
|
cJSON *body = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(body, "model", s_model);
|
cJSON_AddStringToObject(body, "model", s_model);
|
||||||
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
|
if (provider_is_openai()) {
|
||||||
|
cJSON_AddNumberToObject(body, "max_completion_tokens", MIMI_LLM_MAX_TOKENS);
|
||||||
|
} else {
|
||||||
|
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
|
||||||
|
}
|
||||||
|
|
||||||
if (provider_is_openai()) {
|
if (provider_is_openai()) {
|
||||||
cJSON *openai_msgs = convert_messages_openai(system_prompt, messages);
|
cJSON *openai_msgs = convert_messages_openai(system_prompt, messages);
|
||||||
@@ -636,6 +697,7 @@ esp_err_t llm_chat_tools(const char *system_prompt,
|
|||||||
|
|
||||||
ESP_LOGI(TAG, "Calling LLM API with tools (provider: %s, model: %s, body: %d bytes)",
|
ESP_LOGI(TAG, "Calling LLM API with tools (provider: %s, model: %s, body: %d bytes)",
|
||||||
s_provider, s_model, (int)strlen(post_data));
|
s_provider, s_model, (int)strlen(post_data));
|
||||||
|
llm_log_payload("LLM tools request", post_data);
|
||||||
|
|
||||||
/* HTTP call */
|
/* HTTP call */
|
||||||
resp_buf_t rb;
|
resp_buf_t rb;
|
||||||
@@ -650,10 +712,13 @@ esp_err_t llm_chat_tools(const char *system_prompt,
|
|||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
|
||||||
|
llm_log_payload("LLM tools partial response", rb.data);
|
||||||
resp_buf_free(&rb);
|
resp_buf_free(&rb);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
llm_log_payload("LLM tools raw response", rb.data);
|
||||||
|
|
||||||
if (status != 200) {
|
if (status != 200) {
|
||||||
ESP_LOGE(TAG, "API error %d: %.500s", status, rb.data ? rb.data : "");
|
ESP_LOGE(TAG, "API error %d: %.500s", status, rb.data ? rb.data : "");
|
||||||
resp_buf_free(&rb);
|
resp_buf_free(&rb);
|
||||||
|
|||||||
36
main/mimi.c
36
main/mimi.c
@@ -21,6 +21,8 @@
|
|||||||
#include "cli/serial_cli.h"
|
#include "cli/serial_cli.h"
|
||||||
#include "proxy/http_proxy.h"
|
#include "proxy/http_proxy.h"
|
||||||
#include "tools/tool_registry.h"
|
#include "tools/tool_registry.h"
|
||||||
|
#include "cron/cron_service.h"
|
||||||
|
#include "heartbeat/heartbeat.h"
|
||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include "buttons/button_driver.h"
|
#include "buttons/button_driver.h"
|
||||||
#include "ui/config_screen.h"
|
#include "ui/config_screen.h"
|
||||||
@@ -75,9 +77,23 @@ static void outbound_dispatch_task(void *arg)
|
|||||||
ESP_LOGI(TAG, "Dispatching response to %s:%s", msg.channel, msg.chat_id);
|
ESP_LOGI(TAG, "Dispatching response to %s:%s", msg.channel, msg.chat_id);
|
||||||
|
|
||||||
if (strcmp(msg.channel, MIMI_CHAN_TELEGRAM) == 0) {
|
if (strcmp(msg.channel, MIMI_CHAN_TELEGRAM) == 0) {
|
||||||
telegram_send_message(msg.chat_id, msg.content);
|
esp_err_t send_err = telegram_send_message(msg.chat_id, msg.content);
|
||||||
|
if (send_err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Telegram send failed for %s: %s", msg.chat_id, esp_err_to_name(send_err));
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Telegram send success for %s (%d bytes)", msg.chat_id, (int)strlen(msg.content));
|
||||||
|
}
|
||||||
|
if (config_screen_is_active()) {
|
||||||
|
config_screen_toggle();
|
||||||
|
}
|
||||||
|
char title[48];
|
||||||
|
snprintf(title, sizeof(title), "TG OUT %s", msg.chat_id);
|
||||||
|
display_show_message_card(title, 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);
|
esp_err_t ws_err = ws_server_send(msg.chat_id, msg.content);
|
||||||
|
if (ws_err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "WS send failed for %s: %s", msg.chat_id, esp_err_to_name(ws_err));
|
||||||
|
}
|
||||||
} else if (strcmp(msg.channel, MIMI_CHAN_SYSTEM) == 0) {
|
} else if (strcmp(msg.channel, MIMI_CHAN_SYSTEM) == 0) {
|
||||||
ESP_LOGI(TAG, "System message [%s]: %.128s", msg.chat_id, msg.content);
|
ESP_LOGI(TAG, "System message [%s]: %.128s", msg.chat_id, msg.content);
|
||||||
} else {
|
} else {
|
||||||
@@ -92,6 +108,7 @@ void app_main(void)
|
|||||||
{
|
{
|
||||||
/* Silence noisy components */
|
/* Silence noisy components */
|
||||||
esp_log_level_set("esp-x509-crt-bundle", ESP_LOG_WARN);
|
esp_log_level_set("esp-x509-crt-bundle", ESP_LOG_WARN);
|
||||||
|
esp_log_level_set("QRCODE", ESP_LOG_WARN);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "========================================");
|
ESP_LOGI(TAG, "========================================");
|
||||||
ESP_LOGI(TAG, " MimiClaw - ESP32-S3 AI Agent");
|
ESP_LOGI(TAG, " MimiClaw - ESP32-S3 AI Agent");
|
||||||
@@ -144,19 +161,20 @@ void app_main(void)
|
|||||||
if (wifi_manager_wait_connected(30000) == ESP_OK) {
|
if (wifi_manager_wait_connected(30000) == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip());
|
ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip());
|
||||||
|
|
||||||
|
/* Outbound dispatch task should start first to avoid dropping early replies. */
|
||||||
|
ESP_ERROR_CHECK((xTaskCreatePinnedToCore(
|
||||||
|
outbound_dispatch_task, "outbound",
|
||||||
|
MIMI_OUTBOUND_STACK, NULL,
|
||||||
|
MIMI_OUTBOUND_PRIO, NULL, MIMI_OUTBOUND_CORE) == pdPASS)
|
||||||
|
? ESP_OK : ESP_FAIL);
|
||||||
|
|
||||||
/* Start network-dependent services */
|
/* Start network-dependent services */
|
||||||
ESP_ERROR_CHECK(telegram_bot_start());
|
|
||||||
ESP_ERROR_CHECK(agent_loop_start());
|
ESP_ERROR_CHECK(agent_loop_start());
|
||||||
|
ESP_ERROR_CHECK(telegram_bot_start());
|
||||||
cron_service_start();
|
cron_service_start();
|
||||||
heartbeat_start();
|
heartbeat_start();
|
||||||
ESP_ERROR_CHECK(ws_server_start());
|
ESP_ERROR_CHECK(ws_server_start());
|
||||||
|
|
||||||
/* Outbound dispatch task */
|
|
||||||
xTaskCreatePinnedToCore(
|
|
||||||
outbound_dispatch_task, "outbound",
|
|
||||||
MIMI_OUTBOUND_STACK, NULL,
|
|
||||||
MIMI_OUTBOUND_PRIO, NULL, MIMI_OUTBOUND_CORE);
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "All services started!");
|
ESP_LOGI(TAG, "All services started!");
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "WiFi connection timeout. Check MIMI_SECRET_WIFI_SSID in mimi_secrets.h");
|
ESP_LOGW(TAG, "WiFi connection timeout. Check MIMI_SECRET_WIFI_SSID in mimi_secrets.h");
|
||||||
|
|||||||
@@ -46,14 +46,17 @@
|
|||||||
#define MIMI_TG_POLL_STACK (12 * 1024)
|
#define MIMI_TG_POLL_STACK (12 * 1024)
|
||||||
#define MIMI_TG_POLL_PRIO 5
|
#define MIMI_TG_POLL_PRIO 5
|
||||||
#define MIMI_TG_POLL_CORE 0
|
#define MIMI_TG_POLL_CORE 0
|
||||||
|
#define MIMI_TG_CARD_SHOW_MS 3000
|
||||||
|
#define MIMI_TG_CARD_BODY_SCALE 3
|
||||||
|
|
||||||
/* Agent Loop */
|
/* Agent Loop */
|
||||||
#define MIMI_AGENT_STACK (12 * 1024)
|
#define MIMI_AGENT_STACK (24 * 1024)
|
||||||
#define MIMI_AGENT_PRIO 6
|
#define MIMI_AGENT_PRIO 6
|
||||||
#define MIMI_AGENT_CORE 1
|
#define MIMI_AGENT_CORE 1
|
||||||
#define MIMI_AGENT_MAX_HISTORY 20
|
#define MIMI_AGENT_MAX_HISTORY 20
|
||||||
#define MIMI_AGENT_MAX_TOOL_ITER 10
|
#define MIMI_AGENT_MAX_TOOL_ITER 10
|
||||||
#define MIMI_MAX_TOOL_CALLS 4
|
#define MIMI_MAX_TOOL_CALLS 4
|
||||||
|
#define MIMI_AGENT_SEND_WORKING_STATUS 1
|
||||||
|
|
||||||
/* Timezone (POSIX TZ format) */
|
/* Timezone (POSIX TZ format) */
|
||||||
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
|
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
|
||||||
@@ -66,10 +69,12 @@
|
|||||||
#define MIMI_OPENAI_API_URL "https://api.openai.com/v1/chat/completions"
|
#define MIMI_OPENAI_API_URL "https://api.openai.com/v1/chat/completions"
|
||||||
#define MIMI_LLM_API_VERSION "2023-06-01"
|
#define MIMI_LLM_API_VERSION "2023-06-01"
|
||||||
#define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024)
|
#define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024)
|
||||||
|
#define MIMI_LLM_LOG_VERBOSE_PAYLOAD 0
|
||||||
|
#define MIMI_LLM_LOG_PREVIEW_BYTES 160
|
||||||
|
|
||||||
/* Message Bus */
|
/* Message Bus */
|
||||||
#define MIMI_BUS_QUEUE_LEN 8
|
#define MIMI_BUS_QUEUE_LEN 16
|
||||||
#define MIMI_OUTBOUND_STACK (8 * 1024)
|
#define MIMI_OUTBOUND_STACK (12 * 1024)
|
||||||
#define MIMI_OUTBOUND_PRIO 5
|
#define MIMI_OUTBOUND_PRIO 5
|
||||||
#define MIMI_OUTBOUND_CORE 0
|
#define MIMI_OUTBOUND_CORE 0
|
||||||
|
|
||||||
@@ -84,6 +89,13 @@
|
|||||||
#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 / Heartbeat */
|
||||||
|
#define MIMI_CRON_FILE "/spiffs/cron.json"
|
||||||
|
#define MIMI_CRON_MAX_JOBS 16
|
||||||
|
#define MIMI_CRON_CHECK_INTERVAL_MS (60 * 1000)
|
||||||
|
#define MIMI_HEARTBEAT_FILE "/spiffs/HEARTBEAT.md"
|
||||||
|
#define MIMI_HEARTBEAT_INTERVAL_MS (30 * 60 * 1000)
|
||||||
|
|
||||||
/* Skills */
|
/* Skills */
|
||||||
#define MIMI_SKILLS_PREFIX "/spiffs/skills/"
|
#define MIMI_SKILLS_PREFIX "/spiffs/skills/"
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
#include "mimi_config.h"
|
#include "mimi_config.h"
|
||||||
#include "bus/message_bus.h"
|
#include "bus/message_bus.h"
|
||||||
#include "proxy/http_proxy.h"
|
#include "proxy/http_proxy.h"
|
||||||
|
#include "display/display.h"
|
||||||
|
#include "ui/config_screen.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_http_client.h"
|
#include "esp_http_client.h"
|
||||||
#include "esp_crt_bundle.h"
|
#include "esp_crt_bundle.h"
|
||||||
@@ -167,6 +170,37 @@ static char *tg_api_call(const char *method, const char *post_data)
|
|||||||
return tg_api_call_direct(method, post_data);
|
return tg_api_call_direct(method, post_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool tg_response_is_ok(const char *resp, const char **out_desc)
|
||||||
|
{
|
||||||
|
if (out_desc) {
|
||||||
|
*out_desc = NULL;
|
||||||
|
}
|
||||||
|
if (!resp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(resp);
|
||||||
|
if (root) {
|
||||||
|
cJSON *ok_field = cJSON_GetObjectItem(root, "ok");
|
||||||
|
bool ok = cJSON_IsTrue(ok_field);
|
||||||
|
if (!ok && out_desc) {
|
||||||
|
cJSON *desc = cJSON_GetObjectItem(root, "description");
|
||||||
|
if (desc && cJSON_IsString(desc)) {
|
||||||
|
*out_desc = desc->valuestring;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Proxy or gateway can occasionally return non-standard payload framing. */
|
||||||
|
if (strstr(resp, "\"ok\":true") != NULL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static void process_updates(const char *json_str)
|
static void process_updates(const char *json_str)
|
||||||
{
|
{
|
||||||
cJSON *root = cJSON_Parse(json_str);
|
cJSON *root = cJSON_Parse(json_str);
|
||||||
@@ -186,13 +220,17 @@ static void process_updates(const char *json_str)
|
|||||||
|
|
||||||
cJSON *update;
|
cJSON *update;
|
||||||
cJSON_ArrayForEach(update, result) {
|
cJSON_ArrayForEach(update, result) {
|
||||||
/* Track offset */
|
/* Track offset and skip stale/duplicate updates */
|
||||||
cJSON *update_id = cJSON_GetObjectItem(update, "update_id");
|
cJSON *update_id = cJSON_GetObjectItem(update, "update_id");
|
||||||
|
int64_t uid = -1;
|
||||||
if (cJSON_IsNumber(update_id)) {
|
if (cJSON_IsNumber(update_id)) {
|
||||||
int64_t uid = (int64_t)update_id->valuedouble;
|
uid = (int64_t)update_id->valuedouble;
|
||||||
if (uid >= s_update_offset) {
|
}
|
||||||
s_update_offset = uid + 1;
|
if (uid >= 0) {
|
||||||
|
if (uid < s_update_offset) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
s_update_offset = uid + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extract message */
|
/* Extract message */
|
||||||
@@ -209,17 +247,34 @@ static void process_updates(const char *json_str)
|
|||||||
if (!chat_id) continue;
|
if (!chat_id) continue;
|
||||||
|
|
||||||
char chat_id_str[32];
|
char chat_id_str[32];
|
||||||
snprintf(chat_id_str, sizeof(chat_id_str), "%.0f", chat_id->valuedouble);
|
if (cJSON_IsString(chat_id) && chat_id->valuestring) {
|
||||||
|
strncpy(chat_id_str, chat_id->valuestring, sizeof(chat_id_str) - 1);
|
||||||
|
chat_id_str[sizeof(chat_id_str) - 1] = '\0';
|
||||||
|
} else if (cJSON_IsNumber(chat_id)) {
|
||||||
|
snprintf(chat_id_str, sizeof(chat_id_str), "%.0f", chat_id->valuedouble);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Message from chat %s: %.40s...", chat_id_str, text->valuestring);
|
ESP_LOGI(TAG, "Message from chat %s: %.40s...", chat_id_str, text->valuestring);
|
||||||
|
|
||||||
|
if (config_screen_is_active()) {
|
||||||
|
config_screen_toggle();
|
||||||
|
}
|
||||||
|
char title[48];
|
||||||
|
snprintf(title, sizeof(title), "TG IN %s", chat_id_str);
|
||||||
|
display_show_message_card(title, text->valuestring);
|
||||||
|
|
||||||
/* Push to inbound bus */
|
/* Push to inbound bus */
|
||||||
mimi_msg_t msg = {0};
|
mimi_msg_t msg = {0};
|
||||||
strncpy(msg.channel, MIMI_CHAN_TELEGRAM, sizeof(msg.channel) - 1);
|
strncpy(msg.channel, MIMI_CHAN_TELEGRAM, sizeof(msg.channel) - 1);
|
||||||
strncpy(msg.chat_id, chat_id_str, sizeof(msg.chat_id) - 1);
|
strncpy(msg.chat_id, chat_id_str, sizeof(msg.chat_id) - 1);
|
||||||
msg.content = strdup(text->valuestring);
|
msg.content = strdup(text->valuestring);
|
||||||
if (msg.content) {
|
if (msg.content) {
|
||||||
message_bus_push_inbound(&msg);
|
if (message_bus_push_inbound(&msg) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Inbound queue full, drop telegram message");
|
||||||
|
free(msg.content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +353,7 @@ esp_err_t telegram_send_message(const char *chat_id, const char *text)
|
|||||||
/* Split long messages at 4096-char boundary */
|
/* Split long messages at 4096-char boundary */
|
||||||
size_t text_len = strlen(text);
|
size_t text_len = strlen(text);
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
|
int all_ok = 1;
|
||||||
|
|
||||||
while (offset < text_len) {
|
while (offset < text_len) {
|
||||||
size_t chunk = text_len - offset;
|
size_t chunk = text_len - offset;
|
||||||
@@ -325,50 +381,74 @@ esp_err_t telegram_send_message(const char *chat_id, const char *text)
|
|||||||
cJSON_Delete(body);
|
cJSON_Delete(body);
|
||||||
free(segment);
|
free(segment);
|
||||||
|
|
||||||
if (json_str) {
|
if (!json_str) {
|
||||||
char *resp = tg_api_call("sendMessage", json_str);
|
all_ok = 0;
|
||||||
free(json_str);
|
offset += chunk;
|
||||||
if (resp) {
|
continue;
|
||||||
/* Check for Markdown parse error, retry as plain text */
|
}
|
||||||
cJSON *root = cJSON_Parse(resp);
|
|
||||||
if (root) {
|
|
||||||
cJSON *ok_field = cJSON_GetObjectItem(root, "ok");
|
|
||||||
if (!cJSON_IsTrue(ok_field)) {
|
|
||||||
ESP_LOGW(TAG, "Markdown send failed, retrying plain");
|
|
||||||
cJSON_Delete(root);
|
|
||||||
free(resp);
|
|
||||||
|
|
||||||
/* Retry without parse_mode */
|
ESP_LOGI(TAG, "Sending telegram chunk to %s (%d bytes)", chat_id, (int)chunk);
|
||||||
cJSON *body2 = cJSON_CreateObject();
|
char *resp = tg_api_call("sendMessage", json_str);
|
||||||
cJSON_AddStringToObject(body2, "chat_id", chat_id);
|
free(json_str);
|
||||||
char *seg2 = malloc(chunk + 1);
|
|
||||||
if (seg2) {
|
int sent_ok = 0;
|
||||||
memcpy(seg2, text + offset, chunk);
|
bool markdown_failed = false;
|
||||||
seg2[chunk] = '\0';
|
if (resp) {
|
||||||
cJSON_AddStringToObject(body2, "text", seg2);
|
const char *desc = NULL;
|
||||||
free(seg2);
|
sent_ok = tg_response_is_ok(resp, &desc);
|
||||||
}
|
if (!sent_ok) {
|
||||||
char *json2 = cJSON_PrintUnformatted(body2);
|
markdown_failed = true;
|
||||||
cJSON_Delete(body2);
|
ESP_LOGI(TAG, "Markdown rejected by Telegram for %s: %s",
|
||||||
if (json2) {
|
chat_id, desc ? desc : "unknown");
|
||||||
char *resp2 = tg_api_call("sendMessage", json2);
|
|
||||||
free(json2);
|
|
||||||
free(resp2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cJSON_Delete(root);
|
|
||||||
free(resp);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
free(resp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sent_ok) {
|
||||||
|
/* Retry without parse_mode */
|
||||||
|
cJSON *body2 = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(body2, "chat_id", chat_id);
|
||||||
|
char *seg2 = malloc(chunk + 1);
|
||||||
|
if (seg2) {
|
||||||
|
memcpy(seg2, text + offset, chunk);
|
||||||
|
seg2[chunk] = '\0';
|
||||||
|
cJSON_AddStringToObject(body2, "text", seg2);
|
||||||
|
free(seg2);
|
||||||
|
}
|
||||||
|
char *json2 = cJSON_PrintUnformatted(body2);
|
||||||
|
cJSON_Delete(body2);
|
||||||
|
if (json2) {
|
||||||
|
char *resp2 = tg_api_call("sendMessage", json2);
|
||||||
|
free(json2);
|
||||||
|
if (resp2) {
|
||||||
|
const char *desc2 = NULL;
|
||||||
|
sent_ok = tg_response_is_ok(resp2, &desc2);
|
||||||
|
if (!sent_ok) {
|
||||||
|
ESP_LOGE(TAG, "Plain send failed: %s", desc2 ? desc2 : "unknown");
|
||||||
|
ESP_LOGE(TAG, "Telegram raw response: %.300s", resp2);
|
||||||
|
}
|
||||||
|
free(resp2);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Plain send failed: no HTTP response");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Plain send failed: no JSON body");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sent_ok) {
|
||||||
|
all_ok = 0;
|
||||||
|
} else {
|
||||||
|
if (markdown_failed) {
|
||||||
|
ESP_LOGI(TAG, "Plain-text fallback succeeded for %s", chat_id);
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Telegram send success to %s (%d bytes)", chat_id, (int)chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(resp);
|
||||||
offset += chunk;
|
offset += chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ESP_OK;
|
return all_ok ? ESP_OK : ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t telegram_set_token(const char *token)
|
esp_err_t telegram_set_token(const char *token)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "tools/tool_cron.h"
|
#include "tools/tool_cron.h"
|
||||||
#include "cron/cron_service.h"
|
#include "cron/cron_service.h"
|
||||||
|
#include "bus/message_bus.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
@@ -45,6 +46,14 @@ esp_err_t tool_cron_add_execute(const char *input_json, char *output, size_t out
|
|||||||
if (channel) strncpy(job.channel, channel, sizeof(job.channel) - 1);
|
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 (chat_id) strncpy(job.chat_id, chat_id, sizeof(job.chat_id) - 1);
|
||||||
|
|
||||||
|
if (strcmp(job.channel, MIMI_CHAN_TELEGRAM) == 0 &&
|
||||||
|
(job.chat_id[0] == '\0' || strcmp(job.chat_id, "cron") == 0)) {
|
||||||
|
snprintf(output, output_size,
|
||||||
|
"Error: cron_add with channel='telegram' requires a valid chat_id");
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(schedule_type, "every") == 0) {
|
if (strcmp(schedule_type, "every") == 0) {
|
||||||
job.kind = CRON_KIND_EVERY;
|
job.kind = CRON_KIND_EVERY;
|
||||||
cJSON *interval = cJSON_GetObjectItem(root, "interval_s");
|
cJSON *interval = cJSON_GetObjectItem(root, "interval_s");
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ esp_err_t tool_registry_init(void)
|
|||||||
"\"interval_s\":{\"type\":\"integer\",\"description\":\"Interval in seconds (required for 'every')\"},"
|
"\"interval_s\":{\"type\":\"integer\",\"description\":\"Interval in seconds (required for 'every')\"},"
|
||||||
"\"at_epoch\":{\"type\":\"integer\",\"description\":\"Unix timestamp to fire at (required for 'at')\"},"
|
"\"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\"},"
|
"\"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'\"},"
|
"\"channel\":{\"type\":\"string\",\"description\":\"Optional reply channel (e.g. 'telegram'). If omitted, current turn channel is used when available\"},"
|
||||||
"\"chat_id\":{\"type\":\"string\",\"description\":\"Optional reply chat_id. Defaults to 'cron'\"}"
|
"\"chat_id\":{\"type\":\"string\",\"description\":\"Optional reply chat_id. Required when channel='telegram'. If omitted during a Telegram turn, current chat_id is used\"}"
|
||||||
"},"
|
"},"
|
||||||
"\"required\":[\"name\",\"schedule_type\",\"message\"]}",
|
"\"required\":[\"name\",\"schedule_type\",\"message\"]}",
|
||||||
.execute = tool_cron_add_execute,
|
.execute = tool_cron_add_execute,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
#include "mimi_secrets.h"
|
#include "mimi_secrets.h"
|
||||||
#include "nvs.h"
|
#include "nvs.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_timer.h"
|
|
||||||
|
|
||||||
#define CONFIG_LINE_MAX 64
|
#define CONFIG_LINE_MAX 64
|
||||||
#define CONFIG_LINES_MAX 12
|
#define CONFIG_LINES_MAX 12
|
||||||
@@ -24,8 +23,6 @@ static size_t s_scroll = 0;
|
|||||||
static bool s_active = false;
|
static bool s_active = false;
|
||||||
static size_t s_selected = 0;
|
static size_t s_selected = 0;
|
||||||
static int s_sel_offset_px = 0;
|
static int s_sel_offset_px = 0;
|
||||||
static int s_sel_dir = 1;
|
|
||||||
static esp_timer_handle_t s_scroll_timer = NULL;
|
|
||||||
|
|
||||||
#define QR_BOX 110
|
#define QR_BOX 110
|
||||||
#define LEFT_PAD 6
|
#define LEFT_PAD 6
|
||||||
@@ -103,48 +100,9 @@ static void render_config_screen(void)
|
|||||||
display_show_config_screen(qr_text, ip_text, s_line_ptrs, s_line_count, s_scroll, s_selected, s_sel_offset_px);
|
display_show_config_screen(qr_text, ip_text, s_line_ptrs, s_line_count, s_scroll, s_selected, s_sel_offset_px);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void update_selected_scroll(void *arg)
|
|
||||||
{
|
|
||||||
(void)arg;
|
|
||||||
if (!s_active || s_line_count == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *line = s_line_ptrs[s_selected];
|
|
||||||
if (!line) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int line_px = (int)strlen(line) * CHAR_W;
|
|
||||||
int max_offset = line_px - (int)RIGHT_W;
|
|
||||||
if (max_offset <= 0) {
|
|
||||||
s_sel_offset_px = 0;
|
|
||||||
s_sel_dir = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_sel_offset_px += s_sel_dir * 4;
|
|
||||||
if (s_sel_offset_px >= max_offset) {
|
|
||||||
s_sel_offset_px = max_offset;
|
|
||||||
s_sel_dir = -1;
|
|
||||||
} else if (s_sel_offset_px <= 0) {
|
|
||||||
s_sel_offset_px = 0;
|
|
||||||
s_sel_dir = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_config_screen();
|
|
||||||
}
|
|
||||||
|
|
||||||
void config_screen_init(void)
|
void config_screen_init(void)
|
||||||
{
|
{
|
||||||
build_config_lines();
|
build_config_lines();
|
||||||
const esp_timer_create_args_t timer_args = {
|
|
||||||
.callback = &update_selected_scroll,
|
|
||||||
.name = "cfg_scroll",
|
|
||||||
.arg = NULL,
|
|
||||||
};
|
|
||||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &s_scroll_timer));
|
|
||||||
ESP_ERROR_CHECK(esp_timer_start_periodic(s_scroll_timer, 150000));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void config_screen_toggle(void)
|
void config_screen_toggle(void)
|
||||||
@@ -159,7 +117,6 @@ void config_screen_toggle(void)
|
|||||||
s_scroll = 0;
|
s_scroll = 0;
|
||||||
s_selected = 0;
|
s_selected = 0;
|
||||||
s_sel_offset_px = 0;
|
s_sel_offset_px = 0;
|
||||||
s_sel_dir = 1;
|
|
||||||
s_active = true;
|
s_active = true;
|
||||||
ESP_LOGI(TAG, "Switch to config screen");
|
ESP_LOGI(TAG, "Switch to config screen");
|
||||||
render_config_screen();
|
render_config_screen();
|
||||||
@@ -182,6 +139,5 @@ void config_screen_scroll_down(void)
|
|||||||
}
|
}
|
||||||
s_selected = s_scroll;
|
s_selected = s_scroll;
|
||||||
s_sel_offset_px = 0;
|
s_sel_offset_px = 0;
|
||||||
s_sel_dir = 1;
|
|
||||||
render_config_screen();
|
render_config_screen();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user