Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ releases/
sdkconfig
sdkconfig.old
dependencies.lock
components/
managed_components/

# Custom components — track structure, ignore generated files
components/*
!components/micropython_embed/
components/micropython_embed/generated/

# MicroPython submodule build artifacts
lib/micropython/

# Generated headers
main/assets/lang_config.h
main/mmap_generate_emoji.h
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ MimiClaw turns a tiny ESP32-S3 board into a personal AI assistant. Plug it into
- **Tiny** — No Linux, no Node.js, no bloat — just pure C
- **Handy** — Message it from Telegram, it handles the rest
- **Loyal** — Learns from memory, remembers across reboots
- **Smart** — Embedded MicroPython VM lets the AI write and run code on the chip
- **Energetic** — USB power, 0.5 W, runs 24/7
- **Lovable** — One ESP32-S3 board, $5, nothing else

Expand Down Expand Up @@ -256,12 +257,29 @@ MimiClaw supports tool calling for both Anthropic and OpenAI — the LLM can cal
|------|-------------|
| `web_search` | Search the web via Tavily (preferred) or Brave for current information |
| `get_current_time` | Fetch current date/time via HTTP and set the system clock |
| `run_python` | Execute Python code in an embedded MicroPython VM (math, json, regex, collections, and more) |
| `cron_add` | Schedule a recurring or one-shot task (the LLM creates cron jobs on its own) |
| `cron_list` | List all scheduled cron jobs |
| `cron_remove` | Remove a cron job by ID |

To enable web search, set a [Tavily API key](https://app.tavily.com/home) via `MIMI_SECRET_TAVILY_KEY` (preferred), or a [Brave Search API key](https://brave.com/search/api/) via `MIMI_SECRET_SEARCH_KEY` in `mimi_secrets.h`.

### MicroPython VM

MimiClaw embeds a sandboxed [MicroPython](https://micropython.org/) interpreter that the AI can use via the `run_python` tool. This lets the agent write and execute Python code on the fly — for calculations, data transformations, text processing, and anything else that's unreliable via pure LLM reasoning.

- **Available modules:** math, json, re, collections, struct, binascii, random, heapq
- **Sandboxed:** no network, no filesystem, no hardware access — safe to run untrusted code
- **Resource-light:** 512 KB PSRAM allocated per execution, freed immediately after
- **Timeout-protected:** default 10 s, max 30 s — infinite loops are killed automatically

The MicroPython VM requires a one-time setup step before building:

```bash
./scripts/build_micropython_embed.sh # generates the MicroPython embed files
idf.py build # then build as usual
```

## Cron Tasks

MimiClaw has a built-in cron scheduler that lets the AI schedule its own tasks. The LLM can create recurring jobs ("every N seconds") or one-shot jobs ("at unix timestamp") via the `cron_add` tool. When a job fires, its message is injected into the agent loop — so the AI wakes up, processes the task, and responds.
Expand All @@ -284,6 +302,8 @@ This turns MimiClaw into a proactive assistant — write tasks to `HEARTBEAT.md`
- **Cron scheduler** — the AI can schedule its own recurring and one-shot tasks, persisted across reboots
- **Heartbeat** — periodically checks a task file and prompts the AI to act autonomously
- **Tool use** — ReAct agent loop with tool calling for both providers
- **MicroPython VM** — embedded Python interpreter for on-device code execution by the AI
- **Skill creator** — the AI can create new skills as Markdown files, including Python-powered ones

## For Developers

Expand Down
26 changes: 26 additions & 0 deletions components/micropython_embed/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# MicroPython Embed Component for ESP-IDF
#
# Before building, generate the MicroPython embed files:
# ./scripts/build_micropython_embed.sh
#
# See: https://docs.micropython.org/en/latest/develop/embed.html

set(MP_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated")

if(EXISTS "${MP_GENERATED_DIR}/port/micropython_embed.h")
file(GLOB_RECURSE MP_SRCS "${MP_GENERATED_DIR}/*.c")
idf_component_register(
SRCS ${MP_SRCS}
INCLUDE_DIRS "${MP_GENERATED_DIR}" "${MP_GENERATED_DIR}/port" "port"
)
# Suppress warnings in generated MicroPython code
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-all -Wno-extra)
else()
# Register component with headers only so CMake config succeeds.
# Linking will fail until the embed files are generated.
idf_component_register(INCLUDE_DIRS "port")
message(WARNING
"MicroPython embed files not found in ${MP_GENERATED_DIR}.\n"
"Run: ./scripts/build_micropython_embed.sh\n"
"Then rebuild with: idf.py build")
endif()
85 changes: 85 additions & 0 deletions components/micropython_embed/port/mpconfigport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* MicroPython port configuration for MimiClaw (ESP32-S3 embed).
*
* This file is used BOTH when building the embed port (to generate
* micropython_embed.c/h) AND when compiling the generated source
* as part of the ESP-IDF project.
*
* Safety: network, filesystem, hardware, and threading modules are
* disabled to sandbox executed code.
*/
#pragma once

#include <stdint.h>

/* Feature level — includes basic standard library modules */
#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES)

/* Type definitions for ESP32-S3 (32-bit Xtensa) */
typedef intptr_t mp_int_t;
typedef uintptr_t mp_uint_t;
typedef long mp_off_t;

/* Compiler & GC (required for mp_embed_exec_str) */
#define MICROPY_ENABLE_COMPILER (1)
#define MICROPY_ENABLE_GC (1)

/* Scheduler — needed for KeyboardInterrupt on timeout */
#define MICROPY_ENABLE_SCHEDULER (1)
#define MICROPY_KBD_EXCEPTION (1)

/* Stack overflow checking */
#define MICROPY_STACKCHECK (1)

/* GC register scanning: use setjmp fallback on Xtensa (not natively supported) */
#define MICROPY_GCREGS_SETJMP (1)

/* Use setjmp-based NLR instead of Xtensa asm (avoids linker relocation issues) */
#define MICROPY_NLR_SETJMP (1)

/* ---- Enabled modules ---- */
#define MICROPY_PY_MATH (1)
#define MICROPY_PY_CMATH (0)
#define MICROPY_PY_JSON (1)
#define MICROPY_PY_RE (1)
#define MICROPY_PY_COLLECTIONS (1)
#define MICROPY_PY_COLLECTIONS_DEQUE (1)
#define MICROPY_PY_COLLECTIONS_ORDEREDDICT (1)
#define MICROPY_PY_IO (1)
#define MICROPY_PY_STRUCT (1)
#define MICROPY_PY_BINASCII (1)
#define MICROPY_PY_RANDOM (1)
#define MICROPY_PY_HEAPQ (1)
#define MICROPY_PY_HASHLIB (0)

/* Useful builtins */
#define MICROPY_PY_BUILTINS_HELP (0)
#define MICROPY_PY_BUILTINS_INPUT (0)

/* ---- Disabled modules (sandbox) ---- */
#define MICROPY_PY_SYS (0)
#define MICROPY_PY_OS (0)
#define MICROPY_PY_NETWORK (0)
#define MICROPY_PY_SOCKET (0)
#define MICROPY_PY_MACHINE (0)
#define MICROPY_PY_SELECT (0)
#define MICROPY_PY_THREAD (0)

/* No filesystem access */
#define MICROPY_VFS (0)
#define MICROPY_READER_VFS (0)
#define MICROPY_PY_BUILTINS_OPEN (0)

/* ---- VM hook for timeout enforcement ---- */
extern volatile int micropython_vm_timeout_flag;

#define MICROPY_VM_HOOK_LOOP \
do { \
if (micropython_vm_timeout_flag) { \
micropython_vm_timeout_flag = 0; \
mp_sched_keyboard_interrupt(); \
} \
} while (0);

/* Root pointers (none needed for embed) */
#define MICROPY_PORT_ROOT_POINTERS
9 changes: 9 additions & 0 deletions components/micropython_embed/port/mphalport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Minimal HAL port header for MicroPython embed on ESP32-S3.
* Actual HAL implementations are in main/micropython/micropython_vm.c.
*/
#pragma once

static inline void mp_hal_set_interrupt_char(int c) {
(void)c;
}
4 changes: 3 additions & 1 deletion main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ idf_component_register(
"tools/tool_web_search.c"
"tools/tool_get_time.c"
"tools/tool_files.c"
"tools/tool_run_python.c"
"micropython/micropython_vm.c"
"skills/skill_loader.c"
INCLUDE_DIRS
"."
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client
esp_timer esp_websocket_client micropython_embed
Comment on lines 28 to +31

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if MicroPython types are exposed in public headers
rg -l 'mp_int_t|mp_uint_t|mp_off_t|micropython' --type=c --type-add 'c:*.h' -g '!micropython*' main/

Repository: memovai/mimiclaw

Length of output: 100


🏁 Script executed:

# Find all header files in main/ and check which contain MicroPython references
find main/ -name "*.h" -type f | head -20

Repository: memovai/mimiclaw

Length of output: 581


🏁 Script executed:

# Check the full CMakeLists.txt to understand public header configuration
cat main/CMakeLists.txt

Repository: memovai/mimiclaw

Length of output: 1054


🏁 Script executed:

# Check if there are any includes of micropython headers in the public headers
rg 'include.*micropython|mp_int_t|mp_uint_t|mp_obj' main/ -t c --type-add 'c:*.h' | head -30

Repository: memovai/mimiclaw

Length of output: 469


🏁 Script executed:

cat main/micropython/micropython_vm.h

Repository: memovai/mimiclaw

Length of output: 871


🏁 Script executed:

# Also check if other headers in main/ that are likely public include micropython_vm.h
rg '#include.*micropython_vm' main/ -t c --type-add 'c:*.h'

Repository: memovai/mimiclaw

Length of output: 242


Move micropython_embed from REQUIRES to PRIV_REQUIRES.

The public header micropython_vm.h does not expose any MicroPython types—it only uses standard types like esp_err_t and size_t. Since micropython_embed is only needed for internal implementation in micropython_vm.c, it should be a private dependency to avoid unnecessarily exporting it to all components that depend on main.

Proposed fix
     REQUIRES
         nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
         esp_https_ota esp_event json spiffs console vfs app_update esp-tls
-        esp_timer esp_websocket_client micropython_embed
+        esp_timer esp_websocket_client
+    PRIV_REQUIRES
+        micropython_embed
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client
esp_timer esp_websocket_client micropython_embed
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client
PRIV_REQUIRES
micropython_embed
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/CMakeLists.txt` around lines 28 - 31, Move the build dependency for
micropython_embed from the public REQUIRES list to PRIV_REQUIRES in the main
CMakeLists.txt so it is only linked privately for the main target; update the
REQUIRES/PRIV_REQUIRES entries so micropython_embed is removed from REQUIRES and
added to PRIV_REQUIRES (micropython_vm.c remains able to use it while
micropython_vm.h stays dependency-free of MicroPython types).

)
4 changes: 3 additions & 1 deletion main/agent/context_builder.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
"- list_dir: List files, optionally filter by prefix.\n"
"- cron_add: Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.\n"
"- cron_list: List all scheduled cron jobs.\n"
"- cron_remove: Remove a scheduled cron job by ID.\n\n"
"- cron_remove: Remove a scheduled cron job by ID.\n"
"- run_python: Execute Python code via embedded MicroPython VM. Use print() for output. "
"Available modules: math, json, re, collections, struct, binascii, random. No network/file/hardware access.\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"
"## Memory\n"
Expand Down
175 changes: 175 additions & 0 deletions main/micropython/micropython_vm.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#include "micropython_vm.h"
#include "mimi_config.h"

#include <string.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_heap_caps.h"

#include "py/runtime.h"
#include "py/builtin.h"
#include "py/lexer.h"
#include "py/mphal.h"
#include "micropython_embed.h"

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 | 🔴 Critical

Wire micropython_embed.h into the build before merge.

CI is already failing on this include with micropython_embed.h: No such file or directory. In its current state, a clean checkout cannot compile this translation unit, so the feature still depends on a manual pre-generation step that the build is not performing.

🧰 Tools
🪛 GitHub Actions: Build

[error] 11-11: micropython_embed.h: No such file or directory

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

In `@main/micropython/micropython_vm.c` at line 11, The build fails because
micropython_vm.c includes micropython_embed.h which is not produced or not on
the compiler include path; update the build so the header is generated and/or
added to the compiler's include directories before compiling micropython_vm.c.
Modify the build rules that produce micropython_embed.h (or add a dependency) so
its generation runs prior to compiling the target that builds micropython_vm.c,
and add the directory containing micropython_embed.h to the compiler include
paths (e.g., via the project's CMakeLists/Makefile) so the compiler can locate
micropython_embed.h during the build.


static const char *TAG = "upy_vm";

static SemaphoreHandle_t s_mutex = NULL;
static esp_timer_handle_t s_timeout_timer = NULL;

/* Shared stdout capture state (protected by mutex — only one exec at a time) */
static char *s_stdout_buf = NULL;
static size_t s_stdout_pos = 0;
static size_t s_stdout_size = 0;

/* Timeout flag — set by timer callback, checked by VM hook in mpconfigport.h */
volatile int micropython_vm_timeout_flag = 0;

/* ---------- MicroPython HAL implementations ---------- */

/* Called by MicroPython for all print() / stdout output */
mp_uint_t mp_hal_stdout_tx_strn(const char *str, size_t len)
{
if (!s_stdout_buf) return 0;
size_t avail = (s_stdout_size > s_stdout_pos + 1)
? s_stdout_size - s_stdout_pos - 1
: 0;
size_t copy = (len < avail) ? len : avail;
if (copy > 0) {
memcpy(s_stdout_buf + s_stdout_pos, str, copy);
s_stdout_pos += copy;
s_stdout_buf[s_stdout_pos] = '\0';
}
return len;
}

/* Called by MicroPython for time.sleep_ms() / delays */
void mp_hal_delay_ms(mp_uint_t ms)
{
vTaskDelay(pdMS_TO_TICKS(ms));
}

/* ---------- Stubs for disabled filesystem/import features ---------- */

/* Import stat — always report "not found" since we have no filesystem imports */
mp_import_stat_t mp_import_stat(const char *path)
{
return MP_IMPORT_STAT_NO_EXIST;
}

/* Lexer from file — not supported, raise error */
mp_lexer_t *mp_lexer_new_from_file(qstr filename)
{
mp_raise_OSError(ENOENT);
}

/* Built-in open() — disabled */
mp_obj_t mp_builtin_open(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs)
{
mp_raise_OSError(EPERM);
}
MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_open_obj, 1, mp_builtin_open);

/* ---------- Timeout timer ---------- */

static void timeout_callback(void *arg)
{
micropython_vm_timeout_flag = 1;
}
Comment on lines +78 to +81

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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In MicroPython, does mp_sched_keyboard_interrupt()raise a normal catchableKeyboardInterrupt, and can execution continue after user code catches it?

💡 Result:

Yes.

  • mp_sched_keyboard_interrupt() doesn’t “hard abort” the VM; it arranges for a KeyboardInterrupt exception object to be raised the next time the VM checks pending work (“as soon as the VM resumes”). (sources.debian.org)
  • Internally, the keyboard-interrupt path sets mp_pending_exception to the preallocated keyboard-exception object and the VM later delivers it by calling nlr_raise(obj)—i.e. it’s raised like a normal Python exception. (blog.weghos.com)
  • Because it’s delivered as a normal exception, user code can catch KeyboardInterrupt with try/except KeyboardInterrupt: and execution can continue after the except block (the pending-exception slot is cleared before raising, so it won’t immediately re-trigger unless another interrupt is scheduled). (blog.weghos.com)

(Separately, at the Python level, MicroPython documents Ctrl-C as raising KeyboardInterrupt.) (micropython.org)

Citations:


The timeout is catchable and not enforced.

mp_sched_keyboard_interrupt() raises a standard KeyboardInterrupt that user code can catch with try/except and continue execution. The pending-exception flag clears before raising, so it won't re-trigger unless another interrupt is scheduled. This is not a hard execution limit. Either latch the timeout across multiple scheduling cycles until teardown, or transition to a non-catchable abort path (e.g., nlr_jump_fail() or process termination).

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

In `@main/micropython/micropython_vm.c` around lines 51 - 54, The timeout
currently sets micropython_vm_timeout_flag in timeout_callback but relies on
mp_sched_keyboard_interrupt (which raises a catchable KeyboardInterrupt) so user
code can swallow it; instead either latch the timeout until VM teardown or abort
non-catchably. Implement one of: (A) change timeout_callback to set a persistent
latched flag (e.g., add micropython_vm_timeout_latched) and ensure the scheduler
loop that calls mp_sched_keyboard_interrupt checks
micropython_vm_timeout_latched and, once set, forces teardown/stop without
clearing the latch; or (B) have timeout_callback trigger a non-catchable abort
path by calling nlr_jump_fail()/process termination directly instead of setting
micropython_vm_timeout_flag so execution cannot continue. Update uses of
micropython_vm_timeout_flag/mp_sched_keyboard_interrupt accordingly to respect
the chosen approach.


/* ---------- Public API ---------- */

esp_err_t micropython_vm_init(void)
{
s_mutex = xSemaphoreCreateMutex();
if (!s_mutex) return ESP_ERR_NO_MEM;

esp_timer_create_args_t timer_args = {
.callback = timeout_callback,
.name = "upy_timeout",
};
esp_err_t err = esp_timer_create(&timer_args, &s_timeout_timer);
if (err != ESP_OK) {
vSemaphoreDelete(s_mutex);
s_mutex = NULL;
return err;
}

ESP_LOGI(TAG, "MicroPython VM ready (heap=%dKB, timeout=%dms)",
MIMI_MICROPYTHON_HEAP_SIZE / 1024, MIMI_MICROPYTHON_TIMEOUT_MS);
return ESP_OK;
}

esp_err_t micropython_vm_exec(const char *code, char *output, size_t output_size,
int timeout_ms)
{
if (!s_mutex || !code || !output || output_size == 0) {
return ESP_ERR_INVALID_ARG;
}

/* Only one script at a time */
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
snprintf(output, output_size, "Error: MicroPython VM is busy");
return ESP_ERR_TIMEOUT;
}

esp_err_t ret = ESP_OK;

/* Allocate GC heap from PSRAM */
void *gc_heap = heap_caps_calloc(1, MIMI_MICROPYTHON_HEAP_SIZE, MALLOC_CAP_SPIRAM);
if (!gc_heap) {
snprintf(output, output_size, "Error: failed to allocate MicroPython heap (%dKB)",
MIMI_MICROPYTHON_HEAP_SIZE / 1024);
ret = ESP_ERR_NO_MEM;
goto unlock;
}

/* Setup stdout capture */
s_stdout_buf = output;
s_stdout_pos = 0;
s_stdout_size = output_size;
output[0] = '\0';

/* Reset timeout flag */
micropython_vm_timeout_flag = 0;

/* Init interpreter — stack_top is the current stack pointer */
volatile int stack_var;
mp_embed_init(gc_heap, MIMI_MICROPYTHON_HEAP_SIZE, (void *)&stack_var);

/* Start timeout timer */
if (timeout_ms > 0) {
esp_timer_start_once(s_timeout_timer, (uint64_t)timeout_ms * 1000);
}

/* Execute — exceptions are printed to stdout via mp_hal_stdout_tx_strn */
mp_embed_exec_str(code);

/* Cancel timeout */
esp_timer_stop(s_timeout_timer);
micropython_vm_timeout_flag = 0;
Comment on lines +119 to +153

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

Propagate script failures via esp_err_t.

ret never changes after mp_embed_exec_str(code), so uncaught Python exceptions and timeout-triggered failures only show up as printed text in output. Callers above this layer treat non-ESP_OK as failure, which means tracebacks/timeouts currently bubble up as successful tool results.

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

In `@main/micropython/micropython_vm.c` around lines 92 - 125, The function
currently ignores Python execution failures because ret is never updated after
calling mp_embed_exec_str; modify the code so mp_embed_exec_str returns a status
(e.g., esp_err_t or int) indicating success, exception, or timeout, then assign
that to ret (and/or set ret = ESP_ERR_TIMEOUT when micropython_vm_timeout_flag
is set) before stopping the timer and returning; update callers of
mp_embed_exec_str/mp_embed_exec_str signature and ensure symbols mentioned (ret,
mp_embed_exec_str, micropython_vm_timeout_flag, esp_timer_stop) are used to
propagate non-ESP_OK results upward.


/* Teardown interpreter */
mp_embed_deinit();

/* Free heap */
heap_caps_free(gc_heap);

/* Detach stdout capture */
s_stdout_buf = NULL;
s_stdout_size = 0;

/* If no output was produced, say so */
if (s_stdout_pos == 0) {
snprintf(output, output_size, "(no output — use print() to see results)");
}

ESP_LOGI(TAG, "Python exec done (%d bytes output)", (int)s_stdout_pos);
Comment on lines +143 to +170

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 | 🟡 Minor

Two minor cleanups in the exec teardown path.

  1. Line 152: esp_timer_stop is called unconditionally, but the timer is only started when timeout_ms > 0 (line 144). Stopping a non-running timer returns ESP_ERR_INVALID_STATE and emits an esp_timer warning. Gate the stop on the same condition (or track an armed bool).
  2. Lines 166-170: when s_stdout_pos == 0 the fallback message is written via snprintf, but s_stdout_pos is not updated, so the subsequent ESP_LOGI("%d bytes output", s_stdout_pos) reports 0 even though output is non-empty. Minor log inconsistency.
♻️ Proposed adjustments
     /* Start timeout timer */
-    if (timeout_ms > 0) {
+    bool timer_armed = (timeout_ms > 0);
+    if (timer_armed) {
         esp_timer_start_once(s_timeout_timer, (uint64_t)timeout_ms * 1000);
     }

     /* Execute — exceptions are printed to stdout via mp_hal_stdout_tx_strn */
     mp_embed_exec_str(code);

     /* Cancel timeout */
-    esp_timer_stop(s_timeout_timer);
+    if (timer_armed) {
+        esp_timer_stop(s_timeout_timer);
+    }
     micropython_vm_timeout_flag = 0;
@@
     /* If no output was produced, say so */
     if (s_stdout_pos == 0) {
-        snprintf(output, output_size, "(no output — use print() to see results)");
+        int n = snprintf(output, output_size, "(no output — use print() to see results)");
+        if (n > 0) s_stdout_pos = (size_t)n < output_size ? (size_t)n : output_size - 1;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/micropython/micropython_vm.c` around lines 143 - 170, Gate the call to
esp_timer_stop(s_timeout_timer) so it only runs when the timer was actually
started (i.e., when timeout_ms > 0 or an armed flag) to avoid
ESP_ERR_INVALID_STATE warnings; update the teardown in the same block that
started the timer (or track an 'armed' bool) before calling esp_timer_stop.
Also, after writing the fallback message with snprintf(output, output_size, "(no
output — use print() to see results)"), update s_stdout_pos to reflect the new
output length (e.g., strlen(output) or the snprintf return) so the subsequent
ESP_LOGI(TAG, "Python exec done (%d bytes output)", (int)s_stdout_pos) logs the
correct byte count.


unlock:
xSemaphoreGive(s_mutex);
return ret;
}
Loading
Loading