跳转到内容

Lua 扩展模块(lua_module_*)

lua_module_* 是 ESP-Claw 中将硬件外设能力暴露给 Lua 脚本的扩展机制。每个模块本质上是一个标准 Lua C 模块(lua_CFunction 格式),通过 cap_lua_register_module 注册后,在 Lua 脚本中可以用 require("module_name") 加载。

模块名组件目录说明
displaylua_module_displayLCD 屏幕绘图(文字、图形、JPEG/PNG)
gpiolua_module_gpioGPIO 读写、方向配置
buttonlua_module_button按键事件注册与回调
led_striplua_module_led_stripWS2812 等可寻址 LED 灯带控制
audiolua_module_audio音频播放/录制(PCM/WAV)与频谱分析
cameralua_module_camera摄像头拍照与流式采集
lcd_touchlua_module_lcd_touch触摸屏坐标读取
delaylua_module_delaydelay.delay_ms(n) 毫秒级延时
storagelua_module_storage文件系统操作
esp_heaplua_module_esp_heap堆内存查询
systemlua_module_system时间、运行时长、IP、内存与 Wi-Fi 状态查询
mcpwmlua_module_mcpwm通用 PWM 输出(频率/占空比控制)
event_publisherlua_module_event_publisher从 Lua 脚本向 Event Router 发布事件
board_managerlua_module_board_manager板级初始化与外设句柄获取

每个 lua_module_* 组件对外只暴露一个注册函数,命名规则为 lua_module_<name>_register()

// lua_module_display/include/lua_module_display.h
esp_err_t lua_module_display_register(void);

注册函数内部调用 cap_lua_register_module,将模块名与 luaopen_* 函数关联:

// lua_module_display/src/lua_module_display.c
esp_err_t lua_module_display_register(void)
{
    return cap_lua_register_module("display", luaopen_display);
}

注意:所有模块必须在 cap_lua_register_group() 之前注册,之后注册会被拒绝(运行时锁定)。

// app_claw.c 或类似初始化文件
#include "lua_module_display.h"
#include "lua_module_gpio.h"
#include "lua_module_delay.h"
#include "cap_lua.h"

void app_lua_modules_init(void)
{
    lua_module_display_register();
    lua_module_gpio_register();
    lua_module_delay_register();
    // ... 其他模块 ...

    cap_lua_register_group();  // 锁定,之后不能再注册模块
}

以下展示实现一个简单的 myled 模块(控制单个 LED)的完整流程:

components/lua_modules/lua_module_myled/
├── CMakeLists.txt
├── include/
│   └── lua_module_myled.h
└── lua_module_myled.c
# CMakeLists.txt
idf_component_register(
    SRCS "lua_module_myled.c"
    INCLUDE_DIRS "include"
    REQUIRES cap_lua driver
)

每个 Lua C 函数遵循统一签名 int func(lua_State *L)

  • 通过 luaL_check* 系列函数从 Lua 栈获取参数
  • 执行实际操作(调用 ESP-IDF 驱动等)
  • 将返回值 push 到栈,return 返回值数量
// lua_module_myled.c
#include "lua.h"
#include "lauxlib.h"
#include "cap_lua.h"
#include "driver/gpio.h"

#define MYLED_GPIO GPIO_NUM_2

// Lua: myled.set(on)
static int myled_set(lua_State *L)
{
    // 1. 从栈获取参数(bool)
    int on = lua_toboolean(L, 1);

    // 2. 执行操作
    gpio_set_level(MYLED_GPIO, on ? 1 : 0);

    // 3. 无返回值,push 0 个值
    return 0;
}

// Lua: myled.get() -> bool
static int myled_get(lua_State *L)
{
    int level = gpio_get_level(MYLED_GPIO);
    lua_pushboolean(L, level);
    return 1;  // 返回 1 个值
}

// 模块入口:注册所有函数到 Lua table
int luaopen_myled(lua_State *L)
{
    // 初始化 GPIO(实际项目中通常由 board_manager 管理)
    gpio_config_t cfg = {
        .pin_bit_mask = (1ULL << MYLED_GPIO),
        .mode = GPIO_MODE_OUTPUT,
    };
    gpio_config(&cfg);

    // 创建模块 table
    lua_newtable(L);

    lua_pushcfunction(L, myled_set);
    lua_setfield(L, -2, "set");

    lua_pushcfunction(L, myled_get);
    lua_setfield(L, -2, "get");

    return 1;  // 返回 table
}

esp_err_t lua_module_myled_register(void)
{
    return cap_lua_register_module("myled", luaopen_myled);
}
// include/lua_module_myled.h
#pragma once
#include "esp_err.h"

esp_err_t lua_module_myled_register(void);
local myled = require("myled")

myled.set(true)   -- 点亮
delay.delay_ms(500)     -- 需要 lua_module_delay
myled.set(false)  -- 熄灭

lua_module_*cap_* 一样,强烈建议为每个模块提供配套的 Skill 文档,告诉 LLM 如何在 Lua 脚本中正确使用该模块。

lua_module_myled/
└── skills/
    ├── lua_module_myled.md   # Skill 使用文档
    └── skills_list.json      # Skill 元信息目录

cap_* 的 Skill 不同,lua_module_* 的 Skill 绑定的 cap_groups 不是自己的 Group(lua_module 没有独立的 Capability Group),而是绑定到 cap_lua

{
  "skills": [
    {
      "id": "lua_module_myled",
      "file": "lua_module_myled.md",
      "summary": "How to control a single LED from Lua using the myled module.",
      "cap_groups": ["cap_lua"]
    }
  ]
}

激活该 Skill 时,cap_lua Group 的工具(lua_run_scriptlua_write_script 等)会对当前 session 可见,同时 Skill 文档注入上下文,LLM 即可编写并执行使用 myled 模块的脚本。

关于 Lua 模块 Skill 文档的撰写规范(API 参考格式、要素说明等),请参阅 Skills 指南

典型外设模块深度分析:lua_module_display

Section titled “典型外设模块深度分析:lua_module_display”

源码:lua_module_display.c

lua_module_display 是最复杂的 Lua 模块,提供完整的 LCD 绘图能力,是理解复杂模块设计的最佳参考。

lua_module_display 不直接操作 LCD 驱动,而是通过 display_hal.h 定义的 HAL(硬件抽象层)接口:

// include/display_hal.h (板级实现提供)
esp_err_t display_hal_create(esp_lcd_panel_handle_t, esp_lcd_panel_io_handle_t, int w, int h);
esp_err_t display_hal_draw_text(int x, int y, const char *text, uint8_t font_size, ...);
esp_err_t display_hal_draw_jpeg(int x, int y, const uint8_t *data, size_t len, ...);
// ... 等

Lua 模块层只负责参数解析和 HAL 调用,板级代码实现 HAL 接口,这样同一套 Lua API 可以适配不同型号的 LCD 控制器。

模块定义了辅助函数统一处理参数校验:

static int lua_display_check_integer_arg(lua_State *L, int index, const char *name)
{
    if (!lua_isinteger(L, index)) {
        return luaL_error(L, "display %s must be an integer", name);
    }
    return (int)lua_tointeger(L, index);
}

// 颜色参数:三个连续整数 → RGB565
static uint16_t lua_display_color(lua_State *L, int start_index)
{
    int r = lua_display_check_integer_arg(L, start_index,     "color component");
    int g = lua_display_check_integer_arg(L, start_index + 1, "color component");
    int b = lua_display_check_integer_arg(L, start_index + 2, "color component");
    return (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3));
}

文字绘制函数支持可选的 options table,通过逐字段读取 Lua table 实现:

static void lua_display_parse_text_options(lua_State *L, int index,
                                            uint8_t *text_r, uint8_t *text_g, uint8_t *text_b,
                                            uint8_t *font_size, ...)
{
    if (lua_isnoneornil(L, index)) return;  // 参数可缺省
    luaL_checktype(L, index, LUA_TTABLE);

    lua_getfield(L, index, "r");   // 读取 options.r
    if (!lua_isnil(L, -1) && text_r) {
        *text_r = (uint8_t)lua_display_check_integer_arg(L, -1, "text color component");
    }
    lua_pop(L, 1);
    // ... 类似读取 g, b, font_size, align, valign ...
}

Lua 脚本中的用法:

local d = require("display")
d.begin_frame({clear=true, r=0, g=0, b=0})
d.draw_text(10, 20, "Hello!", {
    r=255, g=255, b=0,    -- 黄色文字
    font_size=24,
    align="center"
})
d.present()

lua_module_display 内置了以下图像处理能力:

函数格式说明
draw_jpeg_fileJPEG从文件路径解码并显示,委托给 HAL
draw_png_filePNG用 libpng 解码 RGBA → RGB565,再调用 HAL
draw_jpeg_file_scaledJPEG硬件缩放(需 scale_w/h 为 8 的倍数)
draw_jpeg_file_fitJPEG按比例缩放适配目标区域
draw_jpeg_file_cropJPEG从 JPEG 中裁剪指定区域显示

PNG 解码在 C 层完成(避免 Lua 层内存管理复杂度),RGBA 转 RGB565 时以黑色为背景做 alpha 预乘:

uint8_t alpha = src[3];
uint8_t r = (uint8_t)((src[0] * alpha + 127) / 255);
// ...

除了文件路径的图像绘制外,lua_module_display 还支持直接从内存缓冲区绘制 RGB565 数据,适用于摄像头预览等实时场景:

函数说明
draw_rgb565_crop从 RGB565 缓冲区裁剪指定区域显示
draw_rgb565_scaled将 RGB565 缓冲区缩放到指定尺寸显示
draw_rgb565_fit将 RGB565 缓冲区按比例适配目标区域显示

data 参数可以是 Lua stringlightuserdata(如 camera.frame:ptr()),字节数需为 src_width * src_height * 2。典型用法见摄像头预览示例 camera_preview_demo.lua

支持双缓冲模式(begin_frame / present / end_frame),避免绘制过程中的屏幕撕裂:

d.begin_frame({clear=true})  -- 清空后台缓冲
-- ... 所有绘制操作 ...
d.present()                  -- 一次性刷新到屏幕
d.end_frame()

图像文件路径同样有安全校验,只允许以 / 开头的绝对路径,且必须以 .jpg.jpeg.png 结尾,禁止 .. 路径跳转。

显示模块有特殊的所有权管理逻辑(「仲裁」机制),确保 Lua 脚本可以独占使用显示资源,避免与其他任务冲突。

lua_module_display 模块在 display.init(...) 成功后,Lua 会自动获取前台显示所有权;display.deinit() 或脚本结束清理时释放所有权,避免与其他显示任务冲突。

另外,在显示 HAL 重新创建场景中,运行时会清理历史残留的 swap buffer / display callback 状态,减少跨脚本切换时的显示资源泄漏风险(特别是配合 lua_run_script_asyncexclusive:"display" + replace:true 切换时)。

event_publisher 向 Event Router 发布事件

Section titled “event_publisher 向 Event Router 发布事件”

publish_message 支持两种形式:

  1. 字符串形式:更加简洁,但可携带的信息较少
local ep = require("event_publisher")
ep.publish_message("Button pressed!")
  1. 消息对象形式:可以携带更多信息,但需要手动构造消息对象
local ep = require("event_publisher")
ep.publish_message({
  source_cap = "lua_script", -- source_cap 必填
  channel = args.channel, -- channel 可选,若未提供,运行时会尝试从全局 args.channel 回填
  chat_id = args.chat_id, -- chat_id 可选,若未提供,运行时会尝试从全局 args.chat_id 回填
  text = "Button pressed!", -- text 必填
})