-
Notifications
You must be signed in to change notification settings - Fork 842
feat: add embedded MicroPython VM with run_python tool #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() |
| 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 |
| 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; | ||
| } |
| 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" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wire CI is already failing on this include with 🧰 Tools🪛 GitHub Actions: Build[error] 11-11: micropython_embed.h: No such file or directory 🤖 Prompt for AI Agents |
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes.
(Separately, at the Python level, MicroPython documents Ctrl-C as raising Citations:
The timeout is catchable and not enforced.
🤖 Prompt for AI Agents |
||
|
|
||
| /* ---------- 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Propagate script failures via
🤖 Prompt for AI Agents |
||
|
|
||
| /* 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two minor cleanups in the exec teardown path.
♻️ 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 |
||
|
|
||
| unlock: | ||
| xSemaphoreGive(s_mutex); | ||
| return ret; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: memovai/mimiclaw
Length of output: 100
🏁 Script executed:
Repository: memovai/mimiclaw
Length of output: 581
🏁 Script executed:
# Check the full CMakeLists.txt to understand public header configuration cat main/CMakeLists.txtRepository: memovai/mimiclaw
Length of output: 1054
🏁 Script executed:
Repository: memovai/mimiclaw
Length of output: 469
🏁 Script executed:
Repository: memovai/mimiclaw
Length of output: 871
🏁 Script executed:
Repository: memovai/mimiclaw
Length of output: 242
Move
micropython_embedfromREQUIREStoPRIV_REQUIRES.The public header
micropython_vm.hdoes not expose any MicroPython types—it only uses standard types likeesp_err_tandsize_t. Sincemicropython_embedis only needed for internal implementation inmicropython_vm.c, it should be a private dependency to avoid unnecessarily exporting it to all components that depend onmain.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
🤖 Prompt for AI Agents