diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 3c7fcb9..eef9115 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -22,7 +22,10 @@ idf_component_register( "cli/serial_cli.c" "ota/ota_manager.c" "proxy/http_proxy.c" + "cron/cron_service.c" + "heartbeat/heartbeat.c" "tools/tool_registry.c" + "tools/tool_cron.c" "tools/tool_web_search.c" "tools/tool_get_time.c" "tools/tool_files.c" diff --git a/main/agent/agent_loop.c b/main/agent/agent_loop.c index 0281a1f..226cdcc 100644 --- a/main/agent/agent_loop.c +++ b/main/agent/agent_loop.c @@ -8,6 +8,8 @@ #include #include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "esp_log.h" #include "esp_heap_caps.h" #include "esp_random.h" @@ -123,6 +125,7 @@ static void agent_loop_task(void *arg) while (iteration < MIMI_AGENT_MAX_TOOL_ITER) { /* Send "working" indicator before each API call */ +#if MIMI_AGENT_SEND_WORKING_STATUS { static const char *working_phrases[] = { "mimi\xF0\x9F\x98\x97is working...", @@ -136,8 +139,14 @@ static void agent_loop_task(void *arg) strncpy(status.channel, msg.channel, sizeof(status.channel) - 1); strncpy(status.chat_id, msg.chat_id, sizeof(status.chat_id) - 1); status.content = strdup(working_phrases[esp_random() % phrase_count]); - 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); + } + } } +#endif llm_response_t resp; err = llm_chat_tools(system_prompt, messages, tools_json, &resp); @@ -188,7 +197,14 @@ static void agent_loop_task(void *arg) strncpy(out.channel, msg.channel, sizeof(out.channel) - 1); strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1); 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 { /* Error or empty response */ free(final_text); @@ -197,7 +213,10 @@ static void agent_loop_task(void *arg) strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1); out.content = strdup("Sorry, I encountered an error."); 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 +237,32 @@ esp_err_t agent_loop_init(void) esp_err_t agent_loop_start(void) { - BaseType_t ret = xTaskCreatePinnedToCore( - agent_loop_task, "agent_loop", - MIMI_AGENT_STACK, NULL, - MIMI_AGENT_PRIO, NULL, MIMI_AGENT_CORE); + const uint32_t stack_candidates[] = { + MIMI_AGENT_STACK, + 20 * 1024, + 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; } diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 225e87e..584caad 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -6,7 +6,10 @@ #include "memory/memory_store.h" #include "memory/session_mgr.h" #include "proxy/http_proxy.h" +#include "tools/tool_registry.h" #include "tools/tool_web_search.h" +#include "cron/cron_service.h" +#include "heartbeat/heartbeat.h" #include "skills/skill_loader.h" #include diff --git a/main/display/display.c b/main/display/display.c index 4370f70..0c7f5d2 100644 --- a/main/display/display.c +++ b/main/display/display.c @@ -1,8 +1,12 @@ #include "display/display.h" +#include "mimi_config.h" #include #include #include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/timers.h" #include "esp_check.h" #include "esp_log.h" #include "driver/ledc.h" @@ -51,6 +55,18 @@ static const char *TAG = "display"; static esp_lcd_panel_handle_t panel_handle = NULL; static uint8_t backlight_percent = 50; 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 { 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)); } +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) { if (!framebuffer) { @@ -208,6 +266,13 @@ esp_err_t display_init(void) { 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 = { .sclk_io_num = LCD_PIN_SCLK, .mosi_io_num = LCD_PIN_MOSI, @@ -249,13 +314,28 @@ esp_err_t display_init(void) backlight_ledc_init(); 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; } void display_show_banner(void) { + if (!display_lock_take(pdMS_TO_TICKS(200))) { + ESP_LOGW(TAG, "display lock timeout (banner)"); + return; + } + if (!panel_handle) { ESP_LOGW(TAG, "display not initialized"); + display_lock_give(); return; } @@ -265,10 +345,20 @@ void display_show_banner(void) size_t expected = (size_t)BANNER_W * (size_t)BANNER_H * 2; if (len < expected) { ESP_LOGW(TAG, "banner data too small (%u < %u)", (unsigned)len, (unsigned)expected); + display_lock_give(); 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) @@ -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, size_t selected, int selected_offset_px) { - if (!panel_handle) { - ESP_LOGW(TAG, "display not initialized"); + if (!qr_text || !ip_text || !lines) { 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; } fb_ensure(); if (!framebuffer) { ESP_LOGW(TAG, "framebuffer alloc failed"); + display_lock_give(); 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) diff --git a/main/display/display.h b/main/display/display.h index 23b6a19..e2c958a 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -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, const char **lines, size_t line_count, size_t scroll, size_t selected, int selected_offset_px); +void display_show_message_card(const char *title, const char *body); #ifdef __cplusplus } diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index b9dcae0..b5b34b8 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -15,11 +15,56 @@ static const char *TAG = "llm"; #define LLM_API_KEY_MAX_LEN 320 #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_model[LLM_MODEL_MAX_LEN] = MIMI_LLM_DEFAULT_MODEL; 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: ", 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 + 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) ? " ..." : ""); +#endif +} + static void safe_copy(char *dst, size_t dst_size, const char *src) { if (!dst || dst_size == 0) return; @@ -483,7 +528,11 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, /* Build request body (non-streaming) */ cJSON *body = cJSON_CreateObject(); 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()) { cJSON *messages = cJSON_Parse(messages_json); @@ -521,6 +570,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)", s_provider, s_model, (int)strlen(post_data)); + llm_log_payload("LLM request", post_data); resp_buf_t rb; if (resp_buf_init(&rb, MIMI_LLM_STREAM_BUF_SIZE) != ESP_OK) { @@ -535,12 +585,15 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); + llm_log_payload("LLM partial response", rb.data); resp_buf_free(&rb); snprintf(response_buf, buf_size, "Error: HTTP request failed (%s)", esp_err_to_name(err)); return err; } + llm_log_payload("LLM raw response", rb.data); + if (status != 200) { ESP_LOGE(TAG, "API returned status %d", status); snprintf(response_buf, buf_size, "API error (HTTP %d): %.200s", @@ -601,7 +654,11 @@ esp_err_t llm_chat_tools(const char *system_prompt, /* Build request body (non-streaming) */ cJSON *body = cJSON_CreateObject(); 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()) { cJSON *openai_msgs = convert_messages_openai(system_prompt, messages); @@ -636,6 +693,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)", s_provider, s_model, (int)strlen(post_data)); + llm_log_payload("LLM tools request", post_data); /* HTTP call */ resp_buf_t rb; @@ -650,10 +708,13 @@ esp_err_t llm_chat_tools(const char *system_prompt, if (err != ESP_OK) { 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); return err; } + llm_log_payload("LLM tools raw response", rb.data); + if (status != 200) { ESP_LOGE(TAG, "API error %d: %.500s", status, rb.data ? rb.data : ""); resp_buf_free(&rb); diff --git a/main/mimi.c b/main/mimi.c index 270c2ea..966534d 100644 --- a/main/mimi.c +++ b/main/mimi.c @@ -21,6 +21,8 @@ #include "cli/serial_cli.h" #include "proxy/http_proxy.h" #include "tools/tool_registry.h" +#include "cron/cron_service.h" +#include "heartbeat/heartbeat.h" #include "display/display.h" #include "buttons/button_driver.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); 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) { - 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) { ESP_LOGI(TAG, "System message [%s]: %.128s", msg.chat_id, msg.content); } else { @@ -92,6 +108,7 @@ void app_main(void) { /* Silence noisy components */ 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, " MimiClaw - ESP32-S3 AI Agent"); @@ -144,19 +161,20 @@ void app_main(void) if (wifi_manager_wait_connected(30000) == ESP_OK) { 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 */ - ESP_ERROR_CHECK(telegram_bot_start()); ESP_ERROR_CHECK(agent_loop_start()); + ESP_ERROR_CHECK(telegram_bot_start()); cron_service_start(); heartbeat_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!"); } else { ESP_LOGW(TAG, "WiFi connection timeout. Check MIMI_SECRET_WIFI_SSID in mimi_secrets.h"); diff --git a/main/mimi_config.h b/main/mimi_config.h index d1920d2..7b8fa89 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -46,14 +46,17 @@ #define MIMI_TG_POLL_STACK (12 * 1024) #define MIMI_TG_POLL_PRIO 5 #define MIMI_TG_POLL_CORE 0 +#define MIMI_TG_CARD_SHOW_MS 3000 +#define MIMI_TG_CARD_BODY_SCALE 3 /* Agent Loop */ -#define MIMI_AGENT_STACK (12 * 1024) +#define MIMI_AGENT_STACK (24 * 1024) #define MIMI_AGENT_PRIO 6 #define MIMI_AGENT_CORE 1 #define MIMI_AGENT_MAX_HISTORY 20 #define MIMI_AGENT_MAX_TOOL_ITER 10 #define MIMI_MAX_TOOL_CALLS 4 +#define MIMI_AGENT_SEND_WORKING_STATUS 0 /* Timezone (POSIX TZ format) */ #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_LLM_API_VERSION "2023-06-01" #define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024) +#define MIMI_LLM_LOG_VERBOSE_PAYLOAD 0 +#define MIMI_LLM_LOG_PREVIEW_BYTES 256 /* Message Bus */ -#define MIMI_BUS_QUEUE_LEN 8 -#define MIMI_OUTBOUND_STACK (8 * 1024) +#define MIMI_BUS_QUEUE_LEN 16 +#define MIMI_OUTBOUND_STACK (12 * 1024) #define MIMI_OUTBOUND_PRIO 5 #define MIMI_OUTBOUND_CORE 0 @@ -84,6 +89,13 @@ #define MIMI_CONTEXT_BUF_SIZE (16 * 1024) #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 */ #define MIMI_SKILLS_PREFIX "/spiffs/skills/" diff --git a/main/telegram/telegram_bot.c b/main/telegram/telegram_bot.c index 2610f0e..482b6a7 100644 --- a/main/telegram/telegram_bot.c +++ b/main/telegram/telegram_bot.c @@ -2,9 +2,12 @@ #include "mimi_config.h" #include "bus/message_bus.h" #include "proxy/http_proxy.h" +#include "display/display.h" +#include "ui/config_screen.h" #include #include +#include #include "esp_log.h" #include "esp_http_client.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); } +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) { cJSON *root = cJSON_Parse(json_str); @@ -186,13 +220,17 @@ static void process_updates(const char *json_str) cJSON *update; cJSON_ArrayForEach(update, result) { - /* Track offset */ + /* Track offset and skip stale/duplicate updates */ cJSON *update_id = cJSON_GetObjectItem(update, "update_id"); + int64_t uid = -1; if (cJSON_IsNumber(update_id)) { - int64_t uid = (int64_t)update_id->valuedouble; - if (uid >= s_update_offset) { - s_update_offset = uid + 1; + uid = (int64_t)update_id->valuedouble; + } + if (uid >= 0) { + if (uid < s_update_offset) { + continue; } + s_update_offset = uid + 1; } /* Extract message */ @@ -209,17 +247,34 @@ static void process_updates(const char *json_str) if (!chat_id) continue; 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); + 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 */ mimi_msg_t msg = {0}; strncpy(msg.channel, MIMI_CHAN_TELEGRAM, sizeof(msg.channel) - 1); strncpy(msg.chat_id, chat_id_str, sizeof(msg.chat_id) - 1); msg.content = strdup(text->valuestring); 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 */ size_t text_len = strlen(text); size_t offset = 0; + int all_ok = 1; while (offset < text_len) { size_t chunk = text_len - offset; @@ -325,50 +381,68 @@ esp_err_t telegram_send_message(const char *chat_id, const char *text) cJSON_Delete(body); free(segment); - if (json_str) { - char *resp = tg_api_call("sendMessage", json_str); - free(json_str); - if (resp) { - /* 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); + if (!json_str) { + all_ok = 0; + offset += chunk; + continue; + } - /* 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); - free(resp2); - } - } else { - cJSON_Delete(root); - free(resp); - } - } else { - free(resp); - } + ESP_LOGI(TAG, "Sending telegram chunk to %s (%d bytes)", chat_id, (int)chunk); + char *resp = tg_api_call("sendMessage", json_str); + free(json_str); + + int sent_ok = 0; + if (resp) { + const char *desc = NULL; + sent_ok = tg_response_is_ok(resp, &desc); + if (!sent_ok) { + ESP_LOGW(TAG, "Markdown send failed: %s", desc ? desc : "unknown"); } } + 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 { + ESP_LOGI(TAG, "Telegram send success to %s (%d bytes)", chat_id, (int)chunk); + } + + free(resp); offset += chunk; } - return ESP_OK; + return all_ok ? ESP_OK : ESP_FAIL; } esp_err_t telegram_set_token(const char *token) diff --git a/main/ui/config_screen.c b/main/ui/config_screen.c index 7b044db..8facf7f 100644 --- a/main/ui/config_screen.c +++ b/main/ui/config_screen.c @@ -10,7 +10,6 @@ #include "mimi_secrets.h" #include "nvs.h" #include "esp_log.h" -#include "esp_timer.h" #define CONFIG_LINE_MAX 64 #define CONFIG_LINES_MAX 12 @@ -24,8 +23,6 @@ static size_t s_scroll = 0; static bool s_active = false; static size_t s_selected = 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 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); } -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) { 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) @@ -159,7 +117,6 @@ void config_screen_toggle(void) s_scroll = 0; s_selected = 0; s_sel_offset_px = 0; - s_sel_dir = 1; s_active = true; ESP_LOGI(TAG, "Switch to config screen"); render_config_screen(); @@ -182,6 +139,5 @@ void config_screen_scroll_down(void) } s_selected = s_scroll; s_sel_offset_px = 0; - s_sel_dir = 1; render_config_screen(); }