跳转到内容

cap_lua 与 Lua 综述

源码:cap_lua.c · 头文件:cap_lua.h

ESP-Claw 将 Lua 作为设备端「可编程自动化」的首选语言。它在系统中扮演三个角色:

LLM 可以通过工具调用编写 Lua 脚本(lua_write_script),然后执行(lua_run_script)。Lua 是 LLM 与硬件外设之间的桥梁:LLM 用自然语言的逻辑生成代码,Lua 负责实际执行 GPIO 操作、屏幕绘制、音频播放等。

LLM 决策 → 生成 Lua 代码 → lua_write_script 保存 → lua_run_script 执行 → 硬件响应

claw_event_router 支持 run_script 动作,允许事件规则在不经过 LLM 的情况下直接触发 Lua 脚本:

{
  // 省略了部分字段,如 id
  "match": { "event_type": "button_pressed" },
  "actions": [{ "type": "run_script", "input": { "path": "on_press.lua" } }]
}

这让设备具备完全本地、低延迟的自动化响应能力,不依赖网络和 LLM 推理。

对于不熟悉 C 开发的用户,Lua 脚本提供了一种无需重新编译固件即可扩展设备行为的方式。通过 lua_module_* 机制注册的原生模块,可以将任意外设能力以简洁的 Lua API 暴露出来。

ESP-Claw 使用 Lua 解释器,通过 cap_lua_runtime_* 系列函数初始化和执行脚本。运行时在 cap_lua_group_init 中初始化,锁定模块注册后启动。

cap_lua 的运行时文件需位于 Script 基目录(Script Base Dir)下。basic_demo 中 Script Base Dir 默认为 /fatfs/scripts/(应用可通过 cap_lua_set_base_dir 修改)。

  • path 必须是 .lua 文件,且位于 Script Base Dir 下。
  • path 可用两种形式:
  • 绝对路径:如 /fatfs/scripts/hello.lua(必须在 Base Dir 下,且不能包含 ..)。
  • 简短相对名:如 hello.lua(会自动拼成 ${base_dir}/hello.lua,不允许包含 /)。
  • lua_list_scripts 当前扫描 Base Dir 顶层并返回绝对路径列表(非递归)。
  • lua_list_scripts.prefix 也使用绝对路径前缀,并且必须位于 Base Dir 下。

cap_lua_register_module 只能在 cap_lua_group_init 调用之前执行。初始化完成后,s_module_registration_locked = true,之后的注册调用返回 ESP_ERR_INVALID_STATE。这确保运行时启动时模块集合已确定。

// 应在应用初始化阶段(cap_lua_register_group 之前)注册模块
lua_module_display_register();   // 注册 display 模块
lua_module_gpio_register();      // 注册 gpio 模块
// ...
cap_lua_register_group();        // 之后锁定

cap_lua 当前注册了八个 Callable:

工具 ID功能
lua_list_scripts列举受管目录下的 .lua 脚本
lua_write_script写入(创建或覆盖)Lua 脚本
lua_run_script同步执行脚本,等待结果返回
lua_run_script_async异步提交脚本,立即返回 job_id
lua_list_async_jobs列举异步任务(按状态过滤)
lua_get_async_job查询特定异步任务的状态和输出(支持按 job_idname
lua_stop_async_job停止单个异步任务(按 job_idname
lua_stop_all_async_jobs批量停止异步任务(可按 exclusive 分组过滤)
方式适用场景超时返回值
lua_run_script快速计算、状态读取可设 timeout_ms脚本输出字符串
lua_run_script_async长耗时操作(动画、等待传感器)timeout_ms=0 表示持续运行直到被停止(默认)job_id

lua_run_script_async 支持 nameexclusivereplace 三个控制字段:

  • name:给任务一个逻辑名,便于后续 lua_get_async_job / lua_stop_async_job 按名称操作。
  • exclusive:互斥分组(例如 "display"),同组常用于“单槽位”资源。
  • replace: true:当同名或同 exclusive 组已有活动任务时,尝试抢占并替换旧任务。

典型流程:

// LLM 调用(启动长期任务):
{"path": "animation.lua", "name": "clock_anim", "exclusive": "display", "timeout_ms": 0}

// 立即返回:
"Started Lua job 8a72f3c1 (name=clock_anim, exclusive=display, timeout_ms=0 [until cancelled], status=running) for animation.lua"

// 稍后查询:
{"name": "clock_anim"}
// 返回:包含 job_id / status / summary 等

// 停止任务:
{"name":"clock_anim"}

脚本执行时可传入 JSON 对象或数组作为参数(args 字段),在脚本内通过 args 全局变量访问:

-- 脚本内读取参数
local input = args  -- 对应调用时传入的 args 对象
local speed = input.speed or 100

lua_run_script / lua_run_script_async 由 Agent 在 IM 会话中触发时,如果工具调用里没有显式提供 args.channelargs.chat_idargs.session_id,运行时会自动把当前会话上下文合并进 args。 这让脚本可以直接读取会话信息(例如回消息)而不必每次均指定。

{"path": "hello.lua", "content": "print('hello')", "overwrite": false}

overwrite: false 时若文件已存在会报错,防止意外覆盖。默认为 true(覆盖写入)。

  • Lua 超时检测使用指令级 Hook(固定步数触发)+ 墙钟超时,超时后抛出 execution timed out
  • Hook 回调内会主动 taskYIELD(),避免紧密循环长期占用 CPU 导致任务看门狗误触发。
  • JSON args 里的“整数值”会尽量以 Lua 整数类型进入脚本,减少 GPIO/像素坐标等参数的类型歧义。

除工具调用外,cap_lua 也提供 C 函数接口,供应用代码直接调用:

// 写入脚本
cap_lua_write_script("startup.lua", lua_code, true, output, sizeof(output));

// 同步执行
cap_lua_run_script("startup.lua", NULL, 3000, output, sizeof(output));

// 异步执行(可选 name / exclusive / replace)
cap_lua_run_script_async("startup.lua",
                         NULL,
                         0,
                         "startup_loop",
                         "display",
                         false,
                         output,
                         sizeof(output));

// 停止单个异步任务(job_id 或 name)
cap_lua_stop_job("startup_loop", 2000, output, sizeof(output));

// 按分组停止全部异步任务
cap_lua_stop_all_jobs("display", 2000, output, sizeof(output));

// 注册模块(必须在 cap_lua_register_group 之前调用)
cap_lua_register_module("my_module", luaopen_my_module);

异步任务有以下状态:

Diagram

lua_list_async_jobs 支持按状态过滤:"all" / "queued" / "running" / "done" / "failed" / "timeout" / "stopped"

basic_demo 中,cap_lua_run Skill 已注册停用守卫:当仍有 Lua 异步任务在运行时,deactivate_skill 会被拒绝,并返回结构化原因(提示先调用 lua_stop_async_joblua_stop_all_async_jobs)。

该守卫在 main.capp_main 中、app_claw_start() 返回之后注册(claw_skill_register_deactivate_guard("cap_lua_run", cap_lua_run_deactivate_guard)),确保在 Skill 子系统完全就绪后再挂载守卫逻辑。