cap_lua and Lua overview
Source: cap_lua.c · header: cap_lua.h
Where Lua fits
Section titled “Where Lua fits”ESP-Claw uses Lua as the primary on-device language for programmable automation. It plays three roles:
1. LLM-programmable execution layer
Section titled “1. LLM-programmable execution layer”The LLM can author scripts (lua_write_script) and run them (lua_run_script). Lua bridges natural-language plans to hardware: GPIO, display, audio, etc.
2. Vehicle for automation actions
Section titled “2. Vehicle for automation actions”claw_event_router supports a run_script action so rules can fire Lua without going through an LLM:
That yields fully local, low-latency responses without network or model cost.
3. Rapid prototyping / extension surface
Section titled “3. Rapid prototyping / extension surface”Users who do not ship C firmware can still extend behavior via Lua. Native modules registered as lua_module_* expose peripherals through small Lua APIs.
Lua runtime sketch
Section titled “Lua runtime sketch”ESP-Claw runs Lua via cap_lua_runtime_* helpers. The runtime initializes in cap_lua_group_init, locks module registration, then starts.
Script path management
Section titled “Script path management”cap_lua runtime files must live under the Script Base Dir. In basic_demo, the default Script Base Dir is /fatfs/scripts/ (apps can override it via cap_lua_set_base_dir).
pathmust target a.luafile under Script Base Dir.pathsupports two forms:- Absolute path: e.g.
/fatfs/scripts/hello.lua(must stay under Base Dir and must not contain..). - Short relative name: e.g.
hello.lua(auto-expanded to${base_dir}/hello.lua, and/is not allowed). lua_list_scriptscurrently scans only the Base Dir top level and returns absolute paths (non-recursive).lua_list_scripts.prefixalso uses an absolute-path prefix and must stay under Base Dir.
Module registration lock
Section titled “Module registration lock”cap_lua_register_module is only valid before cap_lua_group_init. After init, s_module_registration_locked = true and further registration returns ESP_ERR_INVALID_STATE, freezing the module set at boot.
cap_lua tool surface
Section titled “cap_lua tool surface”cap_lua currently registers eight Callables:
| Tool ID | Purpose |
|---|---|
lua_list_scripts | List .lua files under the managed tree |
lua_write_script | Write (create or overwrite) a script |
lua_run_script | Sync run; block until output |
lua_run_script_async | Async enqueue; returns job_id immediately |
lua_list_async_jobs | List async jobs (status filter) |
lua_get_async_job | Fetch status/output for one job (supports lookup by job_id or name) |
lua_stop_async_job | Stop one async job (by job_id or name) |
lua_stop_all_async_jobs | Stop async jobs in bulk (optionally filter by exclusive group) |
Sync vs async
Section titled “Sync vs async”| Mode | When to use | Timeout | Returns |
|---|---|---|---|
lua_run_script | Quick work / state reads | optional timeout_ms | script output string |
lua_run_script_async | Long work (animation, waiting on sensors) | timeout_ms=0 means run until stopped (default) | job_id |
lua_run_script_async supports name, exclusive, and replace controls:
name: assign a logical name for laterlua_get_async_job/lua_stop_async_joboperations.exclusive: mutual-exclusion group (for example,"display"), commonly used for single-slot resources.replace: true: when an active job with the samenameorexclusivegroup exists, attempt to preempt and replace it.
Typical flow:
Passing arguments
Section titled “Passing arguments”Optional JSON args becomes the global args inside the script:
When lua_run_script / lua_run_script_async is triggered by the Agent inside an IM conversation, if the tool call does not explicitly provide args.channel, args.chat_id, or args.session_id, the runtime auto-merges the current session context into args.
This lets scripts read conversation context directly (for example, for replies) without passing these fields every time.
overwrite on lua_write_script
Section titled “overwrite on lua_write_script”With overwrite: false, an existing file errors instead of being clobbered. Default is true (overwrite).
Runtime limits and stability
Section titled “Runtime limits and stability”- Lua timeout detection combines an instruction-hook check (triggered every fixed instruction count) with wall-clock timeout; when timed out, it throws
execution timed out. - The hook callback proactively calls
taskYIELD()to avoid tight loops monopolizing CPU and accidentally triggering the task watchdog. - Integer-like values in JSON
argsare preserved as Lua integer type where possible, reducing type ambiguity for GPIO values, pixel coordinates, and similar parameters.
C-callable helpers
Section titled “C-callable helpers”Besides tools, cap_lua exposes C helpers for firmware code:
Async job state machine
Section titled “Async job state machine”Jobs move through:
lua_list_async_jobs filters with "all" / "queued" / "running" / "done" / "failed" / "timeout" / "stopped".
Coordination with Skill deactivation guards
Section titled “Coordination with Skill deactivation guards”In basic_demo, the cap_lua_run Skill has a deactivation guard: if Lua async jobs are still running, deactivate_skill is rejected with a structured reason (prompting you to stop jobs first via lua_stop_async_job or lua_stop_all_async_jobs).
That guard is registered in main.c’s app_main, after app_claw_start() returns (claw_skill_register_deactivate_guard("cap_lua_run", cap_lua_run_deactivate_guard)), so the guard attaches only after the Skill subsystem is fully ready.