Skip to content
Open
3 changes: 3 additions & 0 deletions main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ idf_component_register(
"imu/I2C_Driver.c"
"imu/QMI8658.c"
"imu/imu_manager.c"
"sensors/env_sensor.c"
"trigger/lick_trigger.c"
"bus/message_bus.c"
"wifi/wifi_manager.c"
"telegram/telegram_bot.c"
Expand All @@ -24,6 +26,7 @@ idf_component_register(
"tools/tool_web_search.c"
"tools/tool_get_time.c"
"tools/tool_files.c"
"tools/tool_pixiviz.c"
"skills/skill_loader.c"
INCLUDE_DIRS
"."
Expand Down
4 changes: 4 additions & 0 deletions main/agent/context_builder.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
"- cron_list: List all scheduled cron jobs.\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"
"Telegram rich output rule:\n"
"- To send an image, put a separate markdown image line in your final output: ![caption](https://...)\n"
"- You can include multiple image lines; each will be sent as a Telegram photo.\n"
"- Keep image URLs public HTTPS direct links.\n\n"
"Use tools when needed. Provide your final answer as text after using tools.\n\n"
"## Memory\n"
"You have persistent memory stored on local flash:\n"
Expand Down
42 changes: 42 additions & 0 deletions main/bus/message_bus.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
#include "mimi_config.h"
#include "esp_log.h"
#include <string.h>
#include <stdbool.h>

static const char *TAG = "bus";

static QueueHandle_t s_inbound_queue;
static QueueHandle_t s_outbound_queue;
static char s_latest_channel[16];
static char s_latest_chat_id[32];
static bool s_has_latest_context = false;
static portMUX_TYPE s_ctx_lock = portMUX_INITIALIZER_UNLOCKED;

esp_err_t message_bus_init(void)
{
Expand All @@ -18,6 +23,10 @@ esp_err_t message_bus_init(void)
return ESP_ERR_NO_MEM;
}

memset(s_latest_channel, 0, sizeof(s_latest_channel));
memset(s_latest_chat_id, 0, sizeof(s_latest_chat_id));
s_has_latest_context = false;

ESP_LOGI(TAG, "Message bus initialized (queue depth %d)", MIMI_BUS_QUEUE_LEN);
return ESP_OK;
}
Expand All @@ -28,6 +37,17 @@ esp_err_t message_bus_push_inbound(const mimi_msg_t *msg)
ESP_LOGW(TAG, "Inbound queue full, dropping message");
return ESP_ERR_NO_MEM;
}

if (strcmp(msg->channel, MIMI_CHAN_SYSTEM) != 0 && msg->chat_id[0] != '\0') {
portENTER_CRITICAL(&s_ctx_lock);
strncpy(s_latest_channel, msg->channel, sizeof(s_latest_channel) - 1);
s_latest_channel[sizeof(s_latest_channel) - 1] = '\0';
strncpy(s_latest_chat_id, msg->chat_id, sizeof(s_latest_chat_id) - 1);
s_latest_chat_id[sizeof(s_latest_chat_id) - 1] = '\0';
s_has_latest_context = true;
portEXIT_CRITICAL(&s_ctx_lock);
}

return ESP_OK;
}

Expand Down Expand Up @@ -57,3 +77,25 @@ esp_err_t message_bus_pop_outbound(mimi_msg_t *msg, uint32_t timeout_ms)
}
return ESP_OK;
}

esp_err_t message_bus_get_latest_client_context(char *channel, size_t channel_size,
char *chat_id, size_t chat_id_size)
{
if (!channel || channel_size == 0 || !chat_id || chat_id_size == 0) {
return ESP_ERR_INVALID_ARG;
}

portENTER_CRITICAL(&s_ctx_lock);
if (!s_has_latest_context) {
portEXIT_CRITICAL(&s_ctx_lock);
return ESP_ERR_NOT_FOUND;
}

strncpy(channel, s_latest_channel, channel_size - 1);
channel[channel_size - 1] = '\0';
strncpy(chat_id, s_latest_chat_id, chat_id_size - 1);
chat_id[chat_id_size - 1] = '\0';
portEXIT_CRITICAL(&s_ctx_lock);

return ESP_OK;
}
8 changes: 8 additions & 0 deletions main/bus/message_bus.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <stddef.h>

/* Channel identifiers */
#define MIMI_CHAN_TELEGRAM "telegram"
Expand Down Expand Up @@ -45,3 +46,10 @@ esp_err_t message_bus_push_outbound(const mimi_msg_t *msg);
* Caller must free msg->content when done.
*/
esp_err_t message_bus_pop_outbound(mimi_msg_t *msg, uint32_t timeout_ms);

/**
* Get the latest non-system source context seen on inbound bus.
* Useful for routing device-triggered prompts back to the active chat.
*/
esp_err_t message_bus_get_latest_client_context(char *channel, size_t channel_size,
char *chat_id, size_t chat_id_size);
10 changes: 10 additions & 0 deletions main/buttons/button_driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ static void Timer_Callback(void *arg){

struct Button BUTTON1;
PressEvent BOOT_KEY_State;
static button_event_cb_t s_event_cb = NULL;

void button_set_event_callback(button_event_cb_t cb)
{
s_event_cb = cb;
}

static uint8_t Read_Button_GPIO_Level(uint8_t button_id)
{
if(!button_id)
Expand All @@ -25,18 +32,21 @@ static void Button_SINGLE_CLICK_Callback(void* btn){
struct Button *user_button = (struct Button *)btn;
if(user_button == &BUTTON1){
BOOT_KEY_State = SINGLE_CLICK;
if (s_event_cb) s_event_cb(SINGLE_CLICK);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

s_event_cb fires inside the esp_timer task — blocking I2C reads here will stall button debouncing.

To maintain predictable and timely execution of tasks, callbacks should never attempt block (waiting for resources) or yield (give up control) operations, because such operations disrupt the serialized execution of callbacks. The chain is: esp_timer ISR → Timer_Callbackbutton_ticks()Button_*_Callbacks_event_cb. Timer callbacks are dispatched from a high-priority esp_timer task; it is recommended to only do the minimal possible amount of work from the callback itself, posting an event to a lower priority task using a queue instead.

Per the PR description, the registered callback is intended to perform AHT20 I2C reads, which are blocking. Blocking inside this chain will delay subsequent 5 ms tick callbacks, corrupting debounce state and causing missed/phantom button events.

Recommended pattern: signal a FreeRTOS semaphore or queue in s_event_cb, then perform the I2C read in a dedicated lower-priority task that waits on that signal.

💡 Sketch of the non-blocking pattern (in the lick_trigger consumer)
// In lick_trigger.c
static SemaphoreHandle_t s_trigger_sem;

static void on_button_event(PressEvent event) {
    // Non-blocking: just signal the sensor task
    if (event == SINGLE_CLICK) {
        xSemaphoreGiveFromISR(s_trigger_sem, NULL);
        // portYIELD_FROM_ISR only if dispatching from true ISR context;
        // for ESP_TIMER_TASK dispatch a plain xSemaphoreGive suffices.
    }
}

static void sensor_task(void *arg) {
    for (;;) {
        xSemaphoreTake(s_trigger_sem, portMAX_DELAY);
        // Safe to do blocking I2C here
        aht20_read(...);
    }
}

Also applies to: 42-42, 49-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/buttons/button_driver.c` at line 35, The callback invocation inside the
esp_timer context (s_event_cb called from button_ticks/Timer_Callback) must not
perform blocking operations like I2C reads; change the design so s_event_cb only
signals a lower-priority consumer (e.g. give a FreeRTOS semaphore or push an
event to a queue) and move the blocking AHT20 I2C read into a dedicated task
that waits on that semaphore/queue; update the button registration and any
Button_*_Callback implementations to call xSemaphoreGive/xQueueSend (or FromISR
variant if used in true ISR) and implement a sensor_task that does the
aht20_read when signalled.

}
}
static void Button_DOUBLE_CLICK_Callback(void* btn){
struct Button *user_button = (struct Button *)btn;
if(user_button == &BUTTON1){
BOOT_KEY_State = DOUBLE_CLICK;
if (s_event_cb) s_event_cb(DOUBLE_CLICK);
}
}
static void Button_LONG_PRESS_START_Callback(void* btn){
struct Button *user_button = (struct Button *)btn;
if(user_button == &BUTTON1){
BOOT_KEY_State= LONG_PRESS_START;
if (s_event_cb) s_event_cb(LONG_PRESS_START);
}
}
void button_Init(void)
Expand Down
2 changes: 2 additions & 0 deletions main/buttons/button_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@
extern PressEvent BOOT_KEY_State;

void button_Init(void);
typedef void (*button_event_cb_t)(PressEvent event);
void button_set_event_callback(button_event_cb_t cb);

#endif
6 changes: 3 additions & 3 deletions main/cli/serial_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -555,13 +555,13 @@ esp_err_t serial_cli_init(void)
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
#elif CONFIG_ESP_CONSOLE_USB_CDC
esp_console_dev_usb_cdc_config_t hw_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&hw_config, &repl_config, &repl));
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
esp_console_dev_usb_serial_jtag_config_t hw_config =
ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl));
#elif CONFIG_ESP_CONSOLE_USB_CDC
esp_console_dev_usb_cdc_config_t hw_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&hw_config, &repl_config, &repl));
#else
ESP_LOGE(TAG, "No supported console backend is enabled");
return ESP_ERR_NOT_SUPPORTED;
Expand Down
16 changes: 16 additions & 0 deletions main/imu/QMI8658.c
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,22 @@ void getGyroscope(void)
Gyro.z = Gyro.z * gyroScales;
}

esp_err_t QMI8658_Read_Temperature(float *temp_c)
{
if (!temp_c) {
return ESP_ERR_INVALID_ARG;
}

uint8_t buf[2] = {0};
esp_err_t err = I2C_Read(Device_addr, QMI8658_TEMP_L, buf, 2);
if (err != ESP_OK) {
return err;
}

int16_t raw = (int16_t)(((uint16_t)buf[1] << 8) | buf[0]);
*temp_c = ((float)raw / 256.0f) + 25.0f;
return ESP_OK;
}
Comment on lines +288 to +303
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

QMI8658 temperature register conversion formula datasheet

💡 Result:

For the QMI8658 temperature registers:

  • Read TEMPERATURE_L (0x33) and TEMPERATURE_H (0x34), combine as a signed 16-bit little-endian value:
    raw = (int16_t)((TEMP_H << 8) | TEMP_L).[1]

  • Datasheet scale: 1 LSB = 1/256 °C (i.e., 256 LSB/°C).[1][2]

Conversion formula:

[
T_{^\circ C} = \frac{\text{raw}}{256}
]

Sources: NuttX QMI8658 driver documentation (registers + scale) [1] and a QMI8658 driver doc noting the same datasheet scale [2].

[1] (mail-archive.com)
[2] (docs.rs)

Citations:


Remove the + 25.0f offset from the temperature conversion formula.

The QMI8658 datasheet specifies the temperature conversion as T = raw / 256 with no offset. The current implementation's raw / 256.0f + 25.0f will bias all temperature readings by 25°C above the actual value. Update line 302 to:

*temp_c = (float)raw / 256.0f;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/imu/QMI8658.c` around lines 288 - 303, The temperature conversion in
QMI8658_Read_Temperature incorrectly adds a 25°C offset; update the assignment
to temp_c to use the datasheet formula by removing the +25.0f bias so that
*temp_c is computed as raw / 256.0f (use the existing raw variable and temp_c
pointer in QMI8658_Read_Temperature).




Expand Down
4 changes: 3 additions & 1 deletion main/imu/QMI8658.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "I2C_Driver.h"
#include "esp_err.h"

//device address
#define QMI8658_L_SLAVE_ADDRESS (0x6B)
Expand Down Expand Up @@ -158,4 +159,5 @@ float getGyroX();
float getGyroY();
float getGyroZ();
void getAccelerometer(void);
void getGyroscope(void);
void getGyroscope(void);
esp_err_t QMI8658_Read_Temperature(float *temp_c);
96 changes: 95 additions & 1 deletion main/mimi.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
Expand All @@ -26,9 +28,100 @@
#include "buttons/button_driver.h"
#include "imu/imu_manager.h"
#include "skills/skill_loader.h"
#include "trigger/lick_trigger.h"

static const char *TAG = "mimi";

static char *trim_left(char *s)
{
while (s && *s && isspace((unsigned char)*s)) s++;
return s;
}

/* Parse one markdown image line: ![caption](https://...)
* Returns true when parsed and values are copied into out buffers.
*/
static bool parse_markdown_image_line(const char *line, char *out_url, size_t out_url_sz,
char *out_caption, size_t out_caption_sz)
{
if (!line || strncmp(line, "![", 2) != 0) return false;
const char *cap_end = strstr(line + 2, "](");
if (!cap_end) return false;
const char *url_start = cap_end + 2;
const char *url_end = strchr(url_start, ')');
if (!url_end || url_end <= url_start) return false;

size_t cap_len = (size_t)(cap_end - (line + 2));
size_t url_len = (size_t)(url_end - url_start);
if (url_len == 0 || url_len >= out_url_sz) return false;

memcpy(out_url, url_start, url_len);
out_url[url_len] = '\0';
if (strncmp(out_url, "https://", 8) != 0) return false;

if (out_caption && out_caption_sz > 0) {
size_t cpy = cap_len < (out_caption_sz - 1) ? cap_len : (out_caption_sz - 1);
memcpy(out_caption, line + 2, cpy);
out_caption[cpy] = '\0';
}
return true;
}

/* Telegram dispatcher with image support.
* If content includes markdown image lines (![]()), images are sent via sendPhoto.
* Remaining text is sent via sendMessage.
*/
static esp_err_t telegram_send_rich(const char *chat_id, const char *content)
{
if (!chat_id || !content) return ESP_ERR_INVALID_ARG;

size_t n = strlen(content);
char *scratch = malloc(n + 1);
if (!scratch) return ESP_ERR_NO_MEM;
memcpy(scratch, content, n + 1);

char *text_buf = calloc(1, n + 1);
if (!text_buf) {
free(scratch);
return ESP_ERR_NO_MEM;
}

bool any_fail = false;
size_t text_off = 0;
char *save = NULL;
for (char *line = strtok_r(scratch, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
char *trim = trim_left(line);
char image_url[384] = {0};
char caption[128] = {0};

if (parse_markdown_image_line(trim, image_url, sizeof(image_url), caption, sizeof(caption))) {
esp_err_t e = telegram_send_photo(chat_id, image_url, caption[0] ? caption : NULL);
if (e != ESP_OK) {
any_fail = true;
ESP_LOGW(TAG, "sendPhoto failed, fallback to text URL");
text_off += snprintf(text_buf + text_off, n + 1 - text_off, "%s\n", image_url);
}
continue;
}

text_off += snprintf(text_buf + text_off, n + 1 - text_off, "%s\n", line);
}

esp_err_t text_err = ESP_OK;
if (text_buf[0] != '\0') {
text_err = telegram_send_message(chat_id, text_buf);
}

free(text_buf);
free(scratch);

if (any_fail) {
ESP_LOGW(TAG, "One or more sendPhoto calls failed; link-text fallback was used");
}
if (text_err != ESP_OK) return ESP_FAIL;
return ESP_OK;
}

static esp_err_t init_nvs(void)
{
esp_err_t ret = nvs_flash_init();
Expand Down Expand Up @@ -74,7 +167,7 @@ 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) {
esp_err_t send_err = telegram_send_message(msg.chat_id, msg.content);
esp_err_t send_err = telegram_send_rich(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 {
Expand Down Expand Up @@ -122,6 +215,7 @@ void app_main(void)

/* Initialize subsystems */
ESP_ERROR_CHECK(message_bus_init());
ESP_ERROR_CHECK(lick_trigger_init());
ESP_ERROR_CHECK(memory_store_init());
ESP_ERROR_CHECK(skill_loader_init());
ESP_ERROR_CHECK(session_mgr_init());
Expand Down
Loading