【政安晨】嵌入式AI开发实践 —— ESP-Claw 深度拆解:把 AI Agent 塞进 $5 芯片,它是怎么“看见“并“握住“硬件的

政安晨的个人主页:************政安晨****************

欢迎 👍点赞✍评论⭐收藏

希望政安晨的博客能够对您有所裨益,如有不足之处,欢迎在评论区提出指正!

目录

[01 | 一个"Claw"宇宙:先搞清楚 ESP-Claw 的血缘](#01 | 一个"Claw"宇宙:先搞清楚 ESP-Claw 的血缘)

[02 | 硬件底座:ESP32-S31 与 ESP-Mosaico 板级解剖](#02 | 硬件底座:ESP32-S31 与 ESP-Mosaico 板级解剖)

[02.1 ESP32-S31:乐鑫第一次让 S 系列说 RISC-V](#02.1 ESP32-S31:乐鑫第一次让 S 系列说 RISC-V)

[02.2 ESP-Mosaico 板:一块"AI 创作瑞士军刀"](#02.2 ESP-Mosaico 板:一块"AI 创作瑞士军刀")

[03 | 控制平面:Event 结构体 + Event Router 六动作](#03 | 控制平面:Event 结构体 + Event Router 六动作)

[03.1 claw_event_t:一个 300 字节的消息信封](#03.1 claw_event_t:一个 300 字节的消息信封)

[03.2 Event Router:一张 JSON 规则表决定"谁干活"](#03.2 Event Router:一张 JSON 规则表决定"谁干活")

[03.3 为什么这个分层是"大师级"的?](#03.3 为什么这个分层是"大师级"的?)

[04 | Agent Loop 主循环:514 行 C 代码里的六阶段与插话收件箱](#04 | Agent Loop 主循环:514 行 C 代码里的六阶段与插话收件箱)

[04.1 主循环的六个阶段](#04.1 主循环的六个阶段)

[04.2 循环主体(我按源码顺序伪码化,保留了全部关键分支)](#04.2 循环主体(我按源码顺序伪码化,保留了全部关键分支))

[04.3 上下文拼装:五个 provider,顺序是精心算过的](#04.3 上下文拼装:五个 provider,顺序是精心算过的)

[05 | 能力注册表 claw_cap:工具可见性的"渐进式披露"](#05 | 能力注册表 claw_cap:工具可见性的"渐进式披露")

[06 | 硬件交互控制核心机制:三层 claim 体系(本篇主线)](#06 | 硬件交互控制核心机制:三层 claim 体系(本篇主线))

[06.1 第一层:模块槽位的三态模型(mosaico_module_mgr)](#06.1 第一层:模块槽位的三态模型(mosaico_module_mgr))

[06.2 第二层:claw_hw_registry------Agent 世界的"引脚锁"](#06.2 第二层:claw_hw_registry——Agent 世界的"引脚锁")

[06.3 第三层:从"一句话"到"一个引脚"的完整数据流](#06.3 第三层:从"一句话"到"一个引脚"的完整数据流)

[06.4 为什么不是"GPIO 直接映射成 MCP tool"?](#06.4 为什么不是"GPIO 直接映射成 MCP tool"?)

[07 | 记忆、技能与 MCP:设备如何"长记性"、"学招式"、"连别人"](#07 | 记忆、技能与 MCP:设备如何"长记性"、"学招式"、"连别人")

[07.1 记忆:两个时间尺度,三种模式](#07.1 记忆:两个时间尺度,三种模式)

[07.2 技能:SKILL.md + metadata.cap_groups 的"招式包"](#07.2 技能:SKILL.md + metadata.cap_groups 的"招式包")

[07.3 MCP:设备既是 Client 也是 Server](#07.3 MCP:设备既是 Client 也是 Server)

[07.4 多设备:磁吸拼接 + ESP-NOW 的 16 节点网格](#07.4 多设备:磁吸拼接 + ESP-NOW 的 16 节点网格)

[08 | 科研视角、实操手册与关键技术总结](#08 | 科研视角、实操手册与关键技术总结)

[08.1 科研视角:ESP-Claw 踩中了哪三条研究线](#08.1 科研视角:ESP-Claw 踩中了哪三条研究线)

[08.2 实操手册:从 0 到"说一句、亮一个灯"](#08.2 实操手册:从 0 到"说一句、亮一个灯")

[08.3 关键技术总结](#08.3 关键技术总结)

[08.4 我的判断(踩坑叙事之外的几句实话)](#08.4 我的判断(踩坑叙事之外的几句实话))

参考资源


最近一个月,我几乎把 Espressif 新推的 ESP-Mosaico 开发板(2026-10-20 发布,基于全新 RISC-V 双核 ESP32-S31)和它背后的 ESP-Claw 智能体框架翻了三遍------官网、GitHub 全部 6 个仓库、ESP-Claw 官方文档,外加 arXiv 上端侧 Agent 的几篇论文。

先说结论:

ESP-Claw 不是"在 ESP32 上跑大模型",而是把大模型当成一个会下棋但棋力不稳的对手------能算的地方绝不让它算,不能算的地方只许它走一个格。

这句话是全文的题眼。

下面用 8 个部分拆开它:

01 一个"Claw"宇宙:OpenClaw、MimiClaw、ESP-Claw 的血缘关系

02 硬件底座:ESP32-S31 和 ESP-Mosaico 板级解剖

03 控制平面:Event 结构体 + Event Router 六动作

04 Agent Loop 主循环:514 行 C 代码里的六阶段与插话收件箱

05 能力注册表 claw_cap:工具可见性的"渐进式披露"

06 硬件交互控制核心机制:三层 claim 体系(本篇主线)

07 记忆、技能与 MCP:设备如何"长记性"、"学招式"、"连别人"

08 科研视角 + 实操手册 + 关键技术总结

01 | 一个"Claw"宇宙:先搞清楚 ESP-Claw 的血缘

2026 年 1 月,Peter Steinberger 的个人 Agent 项目 OpenClaw 爆红------72 小时从 9000 涨到 60000 star,一周破 10 万(期间还经历了 Clawdbot → Moltbot → OpenClaw 的两次改名,第二次是为了躲 Anthropic 的商标)。它的架构是一个常驻的 Gateway 进程:所有消息渠道(WhatsApp/Telegram/Discord......25+ 个)都归它管,会话状态、工具调用、记忆持久化全在一个 Node.js 进程里跑 ReAct 循环。

OpenClaw 火了三个月后,乐鑫干了件事:把这套"个人 Agent 操作系统"的精髓,用 C 重写,塞进 5 美元的 ESP32 芯片。

这就是 ESP-Claw (github.com/espressif/esp-claw,2.2k stars,Apache-2.0,759 个提交,仓库 2026-05-18 首提交)。

官方 README 写得很坦诚:

Inspired by the OpenClaw concept and reimplemented in C.

还特别致谢了 MimiClaw------一个更早的尝试:在 $5 的 ESP32-S3 上,不装 Linux、不起 Node、不要树莓派,纯 C 编译出一个完整 ReAct Agent Loop,数据全部存在片内 flash。ESP-Claw 在 ESP32 上跑 Agent Loop、IM 通信的实现,都参考了它。

三者关系一张表:

|--------------|----------------|------------------------------|------------------------------|
| 项目 | 语言/运行时 | 载体 | 定位 |
| OpenClaw | Node.js | 你的 Mac mini / 服务器 | 个人 Agent 操作系统(25+ 渠道、心跳、工作区) |
| MimiClaw | C(裸机 FreeRTOS) | $5 ESP32-S3 | 证明"Agent 可以没有 OS"的最小系统 |
| ESP-Claw | C(FreeRTOS 任务) | ESP32-S3 / C5 / P4 / S31 | 量产级 IoT 设备端 Agent 框架 |

为什么这个方向值得写? 因为 2026 年的 Agent 框架几乎全活在 Python/Node 里,"智能"和"硬件"之间隔着至少一层 HTTP。ESP-Claw 把这条链压到了固件层:感知(I2C 传感器/麦克风/触控)→ 决策(本地规则 + 云端 LLM 混合)→ 执行(GPIO/I2S/LVGL 屏幕)在同一颗芯片 上闭环。Elektor 杂志对它的评价是:"这把讨论从**『微控制器能不能调 API』** 推进到了**『一个小设备能不能自己保持上下文、响应事件、干活,而不需要每个动作都请示云』。**"

但注意一个关键设计决策:ESP-Claw 本体不跑 LLM。

LLM 在云端(OpenAI/Qwen/Claude/DeepSeek 的 API 都支持),ESP32 跑的是 Agent 运行时------上下文拼装、事件路由、工具执行、记忆管理。芯片负责"手脚和短期记忆",云端负责"大脑",两者之间走的是结构化的 tool call,而不是一整段对话。

这就是后面所有机制的出发点:怎么让一颗 320MHz、512KB SRAM 的芯片,可靠地驱动一个不可靠的、按 token 计费的、住在云端的"大脑"去碰真实的 GPIO?


02 | 硬件底座:ESP32-S31 与 ESP-Mosaico 板级解剖

02.1 ESP32-S31:乐鑫第一次让 S 系列说 RISC-V

S31 是 2026 年 4 月发布的(Hackaday 4 月 8 日就有报道),它是 S 系列第一次脱离 Tensilica Xtensa LX7,改用双核 32-bit RISC-V,最高 320MHz ,官方 datasheet 标称 CoreMark 2195.20 (6.86 CoreMark/MHz),比 S3 的 1329.92(5.54/MHz)高约 65%,每兆效率也更高。其他要点:

MMU + Sv32 这个组合对 Agent 框架很友好:内存隔离不再全靠"小心别越界"。

02.2 ESP-Mosaico 板:一块"AI 创作瑞士军刀"

2026-10-20 乐鑫在浦东发布 ESP-Mosaico,参考价 40(首发抢手时炒到 100)。它拆成核心板 + 底板两层,核心板里是:

两个热插拔扩展槽是理解整个平台的关键。源码(boards/esp_mosaico/README.md)里写得极其具体:

支持的模块族(esp-mosaico-bsp 里各自有驱动组件):相机、摇杆、交互板、TOF 距离(VL53L4CD)、热成像、8×8 RGB 灯阵、四路继电器。

另外两台 Mosaico 可以磁吸拼接 :双 BMM150 感应接触/分离/朝向,ESP-NOW 建链,mosaico_network 组件把最多 16 台设备组织成方格网格拓扑,带最短路径路由------两屏 Pong、跨屏游戏都是官方示例。

测试方法:

拿到板子先跑 BSP 自带的 examples/module_slot_scan,它能扫描左右槽 EEPROM 并打印模块描述符;

再跑 examples/relay_control,四路继电器触屏开关,验证"触控 → GPIO"最短路径。这两个示例后面 06 章会当"对照组"用。

03 | 控制平面:Event 结构体 + Event Router 六动作

ESP-Claw 是事件驱动的。理解它之前先扔掉一个念头:没有"设备主动轮询",也没有"用户输入 → 函数调用"的线性流。框架里流动的唯一货币是 Event 。

03.1 claw_event_t:一个 300 字节的消息信封

源码 claw_event_router/include/claw_event.h 里的结构体长这样:

python 复制代码
typedef struct {
    char event_id[48];        /* 事件唯一 ID,可被单独取消 */
    char source_cap[32];      /* 来源 Capability(谁发的) */
    char event_type[32];      /* message / trigger / schedule / out_message / agent_stage ... */
    char source_channel[16];  /* 来源通道(qq / telegram / wechat / web / local) */
    char target_channel[16];
    char source_endpoint[64];
    char target_endpoint[96];
    char chat_id[96];         /* 会话 ID(群号/话题 ID) */
    char sender_id[96];
    char message_id[96];
    char correlation_id[96];  /* 关联 ID:跨请求追踪 */
    char content_type[24];
    int64_t timestamp_ms;
    claw_session_policy_t session_policy;  /* 会话策略:复用/新建 */
    char *text;               /* 文本负载 */
    char *payload_json;       /* JSON 负载(传感器数据等) */
} claw_event_t;

几个字段的设计意图值得停一下:

  • source_cap 让"来源"成为一等公民------IM 消息、调度器定时、传感器触发、Agent 自己的输出(out_message),在路由层是同一类东西。
  • correlation_id 是分布式追踪思想在 MCU 上的投影:一个用户请求触发了 5 轮工具调用、跨了 3 个 cap,靠它串起来。
  • session_policy 直接进事件,而不是事后查表------"这条消息该进老会话还是开新会话"在路由匹配阶段就能决定。

03.2 Event Router:一张 JSON 规则表决定"谁干活"

事件进了 claw_event_router(FreeRTOS 队列),逐条匹配 /fatfs/router_rules/router_rules.json 里的规则,顺序即优先级,命中即执行动作列表,consume_on_match: true 时停止继续匹配。全部动作类型就 6 个:

|--------------|---------------------------------------|------------|
| 动作 | 干什么 | 是否碰 LLM |
| call_cap | 直接执行某个 Capability | 否(毫秒级) |
| run_agent | 异步提交给 claw_core,响应以 out_message 事件回来 | 是 |
| run_script | 跑 Lua 脚本(可带 {{match.remainder}} 模板参数) | 否 |
| send_message | 发到出站 IM 通道 | 否 |
| emit_event | 派生新事件回注路由(可组合出状态机) | 否 |
| drop | 丢弃 | 否 |

6 个动作里 4 个不碰 LLM------这是整个框架"省钱、省延迟、保可靠"的第一道闸门。官方 edge_agent 自带的规则集就是范例:/run <脚本> 命令式前缀匹配 → run_script;out_message → send_message 回原渠道({{event.source_channel}}/{{event.chat_id}} 模板渲染)。

测试方法(设备上 Console):

bash 复制代码
# 1. 看当前规则集
router --rules
# 2. 手动打一条消息事件,模拟 QQ 用户说话
router --emit-message --source-cap qq_gateway --channel qq --chat-id 123456 --text hello
# 3. 看最近一次匹配结果(matched/route/ack/failed_actions)
router --last

route=1 表示事件被某条规则消费了;route=0 表示穿过去走了 fallback(默认进 Agent)。

03.3 为什么这个分层是"大师级"的?

我读过太多 Agent 框架把"路由"和"推理"揉在一个大 if-else 里。

ESP-Claw 的分工是:

Event Router = 确定性控制平面(规则表、JSON、毫秒级、可审计、可热重载)

claw_core = 概率性决策平面(LLM、秒级、贵、不可靠但灵活)

run_agent 是唯一的跨平面桥,而且是异步的:提交后立刻返回 request_id,Agent 想好了再把结果以 out_message 事件"扔回"控制平面。控制平面永远不阻塞在 LLM 上------这就是官方敢写"响应可以快至毫秒"的底气:大部分事件根本不需要 LLM。

还有一组运维接口我特别欣赏(都在 dataflow-and-automation 文档里):

bash 复制代码
claw_core_cancel_request(request_id);        /* 协作式取消 in-flight LLM 请求,0=任意 */
claw_event_router_cancel_event(event_id);    /* 跳过队列里单个未处理事件 */
claw_event_router_purge_queue(type, cap, &n);/* 按类型/来源批量清空队列 */

LLM 请求是"协作式中断"(主要砍掉进行中的 HTTP),事件取消是"跳过"(已开跑的不会回滚)。边界划得很干净,MCU 上没有回滚可言,不如老实承认。


04 | Agent Loop 主循环:514 行 C 代码里的六阶段与插话收件箱

claw_core 是 Agent 本体。它的实现核心 claw_core_agent_loop.c 一共 514 行(我逐行读过),跑在一个独立 FreeRTOS 任务里。

先给请求/响应的最小骨架:

cpp 复制代码
/* 启动一次 run(独立于消息生命周期) */
claw_core_start_run(&request);
/* 或对话型入口:原子判断"进当前 run 的收件箱"还是"排队开新 run" */
claw_core_post_message(&msg, &receipt);
/* 取回结果(文本 + 错误信息) */
claw_core_receive_for(&response, timeout_ms);

Message 和 Run 是两个生命周期 ,这是源码里反复强调的:用户消息随时可能插进来,Agent 的 run 也可能正在收尾。post_message 用一次原子操作决定归属,避免"你先查一下运行阶段再决定 submit 还是 append"这种必然产生竞态的写法。

04.1 主循环的六个阶段

每个 run 是一个 while 循环,阶段机显式写在代码里:

IDLE → BEFORE_LLM_HTTP → IN_LLM_HTTP → AFTER_LLM_BEFORE_TOOL → RUNNING_TOOL → FINALIZING

而每个阶段边界都有一个 drain_run_inbox 检查点(before_llm_http / in_llm_http_abort / after_llm_before_tool 三处显式出现)。收件箱(run inbox)是个有界队列:

LLM 正在跑 → 用户插话 → 文本进 run 的有界收件箱

→ 协作式中止当前 LLM HTTP 调用

→ 下一个安全检查点取出

→ 先把每条消息持久化为 user 上下文,再重建推理输入

→ continue(这一轮重新过 LLM)

换句话说:插话 = 打断 + 重算,插进来的话和原请求平级地进入下一轮 prompt。收件箱容量就是背压边界------UI 不再用"busy 判断"糊弄运行时决策。

04.2 循环主体(我按源码顺序伪码化,保留了全部关键分支)

cpp 复制代码
iteration = 0;
while (1) {
    /* 检查点 1:发 LLM 请求前,取插话 */
    if (drain_run_inbox(..., "before_llm_http", ...)) { persist_user_msgs(); continue; }

    /* 调 LLM(HTTP 流式,tools_json 随请求发送) */
    err = claw_core_llm_chat_messages(core, system_prompt, messages, tools_json,
                                      &llm_response, &error);
    if (err != ESP_OK) {
        /* 检查点 2:HTTP 失败时,检查是不是被插话中止的 */
        if (take_run_inbox_http_abort(...)) { drain_run_inbox(...); if (drained) continue; }
        goto finish;                       /* 真失败 */
    }

    if (llm_response.tool_call_count == 0) {
        /* 模型不再调工具 → 本轮最终文本,收尾 */
        publish_stage_note(); finish_from_plain_text(); break;
    }

    /* 检查点 3:执行工具前再取一次插话 */
    if (drain_run_inbox(..., "after_llm_before_tool", ...)) continue;

    /* 执行模型要求的每个工具(顺序执行,结果 JSON 化) */
    append_assistant_tool_calls(messages, &llm_response);
    tool_results_json = append_tool_results_messages(...);   /* 内部走 call_cap 回调 */
    persist_context_tool_round(...);   /* 工具轮也持久化,可回放 */

    iteration++;
    if (iteration >= core->max_tool_iterations) {  /* 默认 32 */
        error = "cap tool iteration limit reached"; goto finish;
    }
}

三个值得抄进你自己框架的设计:

工具迭代上限 32(max_tool_iterations,Kconfig 可调)------LLM 有时会陷入"调工具→不满意→再调"的死循环,这是硬保险丝。

每个工具轮都持久化 (persist_context_tool_round),普通轮、最终轮、连失败说明都持久化(build_context_failure_trace 会把错误轨迹存进会话历史)。会话历史是 append-only 数据文件 + 同名 .idx 索引,文件超尺寸才压缩旧的工具细节。

完成观察器(claw_core_add_completion_observer):run 结束时回调拿到 {request_id, session_id, final_text, context_providers_csv, tool_calls_csv}。官方说它适合"审计、统计、结果一致性检查(比如检测模型声明是否真的伴随了对应工具调用)"------这就是给 Agent 上"言行核对"的钩子。

04.3 上下文拼装:五个 provider,顺序是精心算过的

每轮发 LLM 前,claw_core 按固定顺序问五个 context provider 要内容:

  1. claw_memory_profile_provider 人设与用户画像(user.md / soul.md / identity.md)

  2. claw_memory_long_term_provider 长期记忆(完整模式=摘要标签目录;轻量模式=MEMORY.md 全文)

  3. claw_memory_session_history_provider 当前会话近期轮次

  4. claw_skill_skills_list_provider 技能目录清单(name + description,不是正文!)

  5. claw_cap_tools_provider 当前对 LLM 可见的工具 JSON

顺序即缓存策略:LLM API 的 prompt cache 按前缀匹配 计费。所以最稳定的内容(人设)永远在最前,最易变的内容(会话尾部)在最后。官方文档里还专门记录了 2026-05-11 的一次重构:"用更稳定的 system prompt 提高缓存命中率"------早期把激活技能的全文注入 system prompt,每次激活/停用都让缓存全量失效;现在改成 activate_skill 工具的返回值把技能文档带进会话历史,system prompt 从此冻结。这一改,直接省的是真金白银的 token 费,也是"上下文工程"在嵌入式上最务实的形态。


05 | 能力注册表 claw_cap:工具可见性的"渐进式披露"

claw_cap 是所有"可被调用的能力"的注册表。每个能力是一个描述符:

cpp 复制代码
typedef struct {
    const char *id;
    const char *name;
    const char *family;              /* 逻辑分组(IM/搜索/MCP/Lua...) */
    const char *description;         /* 给模型看的说明,UTF-8 安全截断到 256 字节 */
    const char *input_schema_json;   /* JSON Schema:给模型看的参数契约 */
    claw_cap_kind_t kind;            /* CALLABLE / EVENT_SOURCE / HYBRID */
    uint32_t cap_flags;              /* CLAW_CAP_FLAG_CALLABLE_BY_LLM 等 */
    claw_cap_execute_fn execute;     /* (input_json, call_context, out_buf, len) → esp_err_t */
    /* init / start / stop 生命周期钩子(可选) */
} claw_cap_descriptor_t;

三个调用方共用同一个入口 claw_cap_call,call_context.caller 区分 SYSTEM / AGENT / CONSOLE:

Agent → claw_core → call_cap 回调 → claw_cap_call_from_core

Console → cap call <name> <json>

Router → 规则动作 call_cap

关键机制: claw_cap_set_llm_visible_groups 白名单。 注册了 20+ 个 cap 组(IM 四平台、搜索、MCP 双端、Lua、技能、文件、调度、路由器管理......),但出厂默认只有 8 个组对 LLM 可见:

cap_files / cap_scheduler / cap_lua / cap_skill /

cap_llm_inspect / cap_http_request / cap_web_search / cap_router_mgr

(完整结构化记忆模式再加 claw_memory)

其余能力依然存在------Console 和自动化规则随时能调------只是模型看不见。为什么?官方文档的原话:"避免上下文过长导致大语言模型能力退化。"

这不是孤立的工程直觉,arXiv 上有论文背书:Less-is-More (arXiv:2411.15399,SIU/UT Austin)系统性证明了给端侧 LLM 限制可见工具数量 能同时降低执行时间和功耗(论文报告最高 70% 执行时间、40% 功耗下降);更早的 TinyAgent(arXiv:2409.00608)就专做"边缘上的 function calling"。ESP-Claw 等于把论文结论直接写进了默认配置。

那模型想用看不见的工具怎么办?→ 用 Skill 激活 (07 章细讲):Skill 的 metadata.cap_groups 字段声明"我激活时,把这些组打开"。于是"工具用法文档"和"工具本身"绑在同一个生命周期里------模型不可能在不知道用法的状态下裸用工具。这个设计比"把所有工具全丢给模型 + 祈祷"优雅一个量级。


06 | 硬件交互控制核心机制:三层 claim 体系(本篇主线)

前面四章讲的是"大脑怎么想",这一章讲"手怎么伸"------也是你点这篇博客的初衷:ESP-Claw 到底怎么控制硬件和外设的?

答案是三层嵌套的资源治理体系,从下到上:

第一层 mosaico_module_mgr 物理槽位层:谁插在哪、占没占

第二层 claw_hw_registry 总线/引脚层:GPIO/I2C/I2S/ADC/LED-C... 谁在用

第三层 app_claw_hw_bridge 板级目录层:把板卡上每个资源登记成可 claim 的键

06.1 第一层:模块槽位的三态模型(mosaico_module_mgr)

ESP-Mosaico 的扩展模块是热插拔的,且两槽共享 I2C------这意味着"模块在不在"和"模块是什么"和"谁在用"是三个互相独立的问题。mosaico_module_mgr(BSP 组件)把它们拆成三个互不覆盖的状态:

presence: UNKNOWN / ABSENT / PRESENT /* EEPROM 地址探测结果(I2C 0x50/0x51 ping) */

descriptor_state: UNKNOWN / VALID / INVALID /* AT24C02 里的模块描述符校验结果 */

owner_state: FREE / CLAIMED / RESTORING /* 驱动占用关系 */

每次快照变化,generation 计数器递增;订阅者收到的是最终状态通知(不是无损审计日志)------官方文档专门提醒"需要完整初始视图时,订阅后调一次 get_info()"。这是嵌入式状态机的正确姿势:不承诺你错过的历史,只保证当前视图一致。

驱动要"等一个匹配的模块并独占槽位",用一次原子 claim:

cpp 复制代码
mosaico_module_mgr_claim_config_t claim = {
    .expected_type = MOSAICO_BOARD_TYPE_INTERACT,  /* 想要交互板 */
    .slot          = MOSAICO_MODULE_MGR_SLOT_AUTO, /* 左右都行 */
    .timeout_ms    = 3000,
};
mosaico_module_lease_t lease = {0};
mosaico_module_mgr_claim(&claim, &lease);   /* 阻塞至匹配+独占,或超时 */
/* ... 用槽位 ... */
mosaico_module_mgr_release(&lease);         /* 必须带非零 lease ID */

只有 PRESENT + VALID + 类型匹配 + FREE 才允许正常 claim。模块拔出时保留最后一份有效描述符(事件处理能认出"拔走的是谁"),状态改回 UNKNOWN。

相机是教科书级的边界案例 :相机模块 EEPROM 地址内部固定 0x50(没有 A0 地址线),只支持左槽;而且相机工作时 GPIO14 要改作 DVP D4 用------槽位控制引脚被相机征用了。驱动在 claim 时声明 USE_SLOT_CONTROL_GPIO | PROBE_WHILE_CLAIMED 两个标志,管理器因此不把"相机"硬编码进扫描逻辑,且允许在占用期间继续探测固定地址 0x50。一句话:用能力声明代替类型特判,扫描器永远不用改。

并发模型 (README 里逐条写的,我挑关键的):一个 scan task 跑全部 EEPROM 探测(I2C 操作不持状态锁);一个 event task 串行派发回调(回调不在状态锁内跑);最多 4 个并发 claim waiter + 4 个订阅者;unsubscribe 会等正在跑的回调结束;回调里不许调 deinit。

这套东西看着平淡,其实是热插拔系统的全部痛点清单:共享总线仲裁、拔出瞬间驱动还在读写、GPIO 复用冲突、订阅-快照竞态。乐鑫把它做成了 400 行左右的组件而不是散在驱动里------这是 BSP 层能给 Agent 框架的最大礼物。

06.2 第二层:claw_hw_registry------Agent 世界的"引脚锁"

上面管的是"模块槽",但 Agent 写 Lua 时直接碰的是 GPIO、I2C 总线、ADC 通道。两个 Lua 脚本同时 gpio.write(14) 谁说了算?claw_hw_registry(components/common/claw_hw_registry)就是一把命名资源锁:

cpp 复制代码
/* 资源键的类型化构造器(从源码摘的) */
claw_hw_key_gpio(key, sizeof(key), pin);          /* "gpio:14" */
claw_hw_key_i2c_controller(key, sizeof(key), port);
claw_hw_key_i2c_addr(key, sizeof(key), port, addr);
claw_hw_key_i2s_bus(key, sizeof(key), port);
claw_hw_key_spi_bus / _uart_bus / _adc_unit / _adc_channel /
claw_hw_key_ledc_timer / _ledc_channel / _sdmmc_slot / _dsi_bus / _ldo_channel;
claw_hw_key_device(key, sizeof(key), name);        /* 整设备级键 */

/* 策略只有两种 */
CLAW_HW_EXCLUSIVE  /* 独占:默认,冲突返回 CLAW_HW_ERR_CONFLICT (0x7c01) */
CLAW_HW_SHARED     /* 共享:如只读类资源 */

claw_hw_registry_claim(registry, cfg, &claim_id);
claw_hw_registry_release(registry, claim_id);

出厂固件 main.c 第 906 行就是它的启动位置:

cpp 复制代码
ESP_ERROR_CHECK(claw_hw_registry_create(&s_hw_registry));
ESP_ERROR_CHECK(app_claw_hw_bridge_init(s_hw_registry));   /* 登记板级目录 */

app_claw_hw_bridge 是板级目录生成器(代码由 sync_lua_module_resources.py 同步生成):它把 Mosaico 板卡上所有 Lua 可触达的资源------GPIO 引脚、I2C 总线与地址、I2S、SPI、UART、ADC 单元/通道、LEDC 定时器/通道、SDMMC、DSI、LDO------按 app_claw_hw_group_t 批量登记。注意这个结构体里有个细节:

cpp 复制代码
typedef struct {
    const char *name;
    bool device;
    size_t group;      /* Codec/I2S 别名共享同一个物理 claim! */
    const app_claw_hw_resource_t *resources;
    size_t resource_count;
    bool board_owned;  /* 板载资源:Agent 的 Lua 不能抢走 */
} app_claw_hw_group_t;

group 字段解决"ES8311 的 I2C 句柄和 I2S 句柄是同一个物理设备"的别名问题------claim 其中一个,另一个自动占用,不会出现"音频被自己人打断"。board_owned 则划了红线:板载核心资源(屏幕、触控、主 Codec)属于系统,Agent 生成的脚本只能 claim 扩展槽资源。Agent 的权限边界,在资源注册那一刻就固化了,而不是靠运行时 if。

06.3 第三层:从"一句话"到"一个引脚"的完整数据流

把 03/04/05/06 章全部串起来,"把左槽继电器 1 号通道打开"的完整链路:

cpp 复制代码
微信消息 "打开继电器1"
  → cap_im_wechat 发布 Event{source_cap=cap_im_wechat, type=message, text=...}
  → Event Router 匹配规则(无 call_cap 命中)→ fallback run_agent
  → claw_core 独立任务:拼装 5-provider 上下文(含当前可见工具)
  → LLM 返回 tool call: {"path": "relay.lua", "name": "relay1", "exclusive": "slot_relay"}
      (cap_lua 的 lua_run_script_async:立即返回 job_id,脚本常驻直到被 stop)
  → call_cap 回调 → claw_cap_call_from_core → cap_lua execute → Lua VM
  → 脚本内 relay 模块初始化时
      → ① mosaico_module_mgr_claim(继电器模块, 槽位)  ← 槽位层
      → ② claw_hw_registry_claim("gpio:46", EXCLUSIVE) ← 引脚层
      → ③ gpio.set_direction(46, "output") / gpio.set_level(46, 1)
  → 工具结果 JSON 回注 messages → 下一轮 LLM 输出最终文本
  → out_message 事件 → Router 规则 send_message → 微信回复"已打开"

整条链上,LLM 只出现了一次 (04 章的循环里就是 IN_LLM_HTTP 那一拍),其余全是确定性组件。而"硬件被占用/冲突"这种错误,会以工具结果 JSON 的形式回到 LLM 手里------模型能看到 CLAW_HW_ERR_CONFLICT,从而学会"先查状态再操作"。硬件状态错误没有走日志黑洞,而是走回了 Agent 的感知回路------这是"把硬件变成 Agent 的一等工具"在错误处理层的完整闭环。

06.4 为什么不是"GPIO 直接映射成 MCP tool"?

你可能会想:社区里(Xiaozhi、SenseCAP Watcher 等)流行把每个 GPIO/传感器直接注册成 MCP tool,让 LLM 直接调 gpio_read(14)。ESP-Claw 也能做 MCP(07 章),但它的默认路径是 Lua 脚本,区别很大:

MCP tool 直连 GPIO → 每次碰硬件都是一次 tool call(一次 LLM 往返,秒级,贵)

Lua 脚本 → LLM 写一次脚本(毫秒级执行,可循环、可 sleep、可 while)

→ 确定性逻辑固化在脚本里,LLM 只负责"生成脚本"

一个"检测人体靠近 3 秒后开灯"的行为,前者要 LLM 参与 30 次轮询,后者是一段 5 行 Lua 跑在芯片本地。LLM 是编译器,不是解释器------这是我对 ESP-Claw 交互控制哲学最凝练的概括。官方文档里"Chat Coding"的示例("在屏幕上绘制一朵旋转的小花,持续 30 秒")走的正是这条路:对话生成脚本,脚本接管硬件。


07 | 记忆、技能与 MCP:设备如何"长记性"、"学招式"、"连别人"

07.1 记忆:两个时间尺度,三种模式

claw_memory 把记忆切成会话历史 (短期)和长期记忆(跨会话):

完整模式下,上下文里只注入摘要标签目录 (不是记忆全文)------LLM 先看到"有这么些记忆",要细节再走 claw_memory 的工具调用去检索。文档专门加了警告框:"MEMORY.md 主要便于人工查看,不作为记忆检索来源"。这个防呆设计很成熟:多少框架的 Agent 是把"给人看的 README"当"给机器查的数据库"用的。

单条消息上限默认 4096 字符(edge_agent 配置项可调),会话文件超尺寸才压缩旧的工具调用细节------写入永远 append,压缩是异步的事。

07.2 技能:SKILL.md + metadata.cap_groups 的"招式包"

ESP-Claw 的 Skill 和 PC 端的"提示词片段"不同,一个 Skill 是目录,可以带 Lua 脚本、静态资源、API 参考文档:

出厂固件的 fatfs_image 里预置了一批(我在源码里数过):current_weather、plan_mode、ai_create_quick、light_switch、scheduled_task、cap_scheduler、cap_router_mgr、cap_http_request、cap_web_search、cap_llm_inspect_image、app_creator、skills_lab_search/downloader、memory_ops、profile_memory_ops......

激活的完整效果(一次 activate_skill 调用):

① 技能文档(SKILL.md 正文)→ 作为工具返回值注入会话历史(不进 system prompt,保缓存)

② metadata.cap_groups 声明的 cap 组 → 加入 LLM 可见白名单

③ 该会话的技能激活状态 → 持久化到 session state 目录

官方对"写好一个 Skill"给了张五要素表:场景描述(何时用)、调用规则(参数约束/频率限制)、完整可复制的 JSON 示例(不要自然语言)、错误对照表、与其他工具的依赖关系。对 Lua 驱动类 Skill 则要求完整 API 参考(每个函数的参数类型、返回值、Lua 示例、raise 条件)。

这是"渐进式披露"的完整闭环 :05 章的白名单决定"模型此刻知道哪些工具",Skill 决定"模型如何按场景扩大知识面",文档质量决定"模型会不会用对"。三层都是数据(Markdown + JSON),全部可以运行时增删------cap_skill_mgr 甚至自带 Skills Lab 技能:模型能联网搜索并安装新技能包到设备。Agent 给自己升级招式,不需要刷机。

07.3 MCP:设备既是 Client 也是 Server

cap_mcp_client 注册三个工具:mcp_list_tools / mcp_call_tool / mcp_discover(后者做局域网发现)。cap_mcp_server 让 Mosaico 自己暴露工具给别人的 Agent。也就是说:

Mosaico 作为 Client → 调家里其他设备的 MCP server(比如 Matter 灯)

Mosaico 作为 Server → 被手机上的 Agent 调(读它的传感器、控它的继电器)

学术界的印证:IoT-MCP(arXiv:2510.01260)提出把 MCP 部署到边缘 MCU 上桥接 LLM 与 IoT,支持 6 个 MCU 家族 + 22 种传感器,并建了首个 MCP-IoT 基准集。ESP-Claw 的双向 MCP 是这条研究线里已经量产进固件的实现------比论文早落地,比论文多了"设备自己就是 Agent"这一层。

常见问题 & 解决方案(MCP 集成时实测会遇到的高频问题):

|----------------------------|---------------------------------------------------------------------------------------------|
| 问题 | 解决方案 |
| mcp_discover 找不到局域网 server | 确认同一子网;MCP server 端确认已做 mDNS 注册;设备端 Wi-Fi 必须是 STA 模式且已连上(SoftAP 模式下无上行网络) |
| 工具调用超时 | 默认超时在 cap 配置里,云端/远端 MCP 目标要放宽;本地 MCP(同 LAN)一般 <1s |
| 模型"看见"了 MCP 工具但参数总错 | MCP tool 的 description 同样受 256 字节截断保护------把关键参数说明放 description 前 200 字节内,或写一个对应 Skill 补全用法 |

07.4 多设备:磁吸拼接 + ESP-NOW 的 16 节点网格

mosaico_network(BSP 组件)把"两台板子拼一起"做成了协议:物理层是双 BMM150 的磁感边分类(接近/接触/释放/朝向事件,含滤波),传输层是 ESP-NOW(带重试、会话 ID、拓扑传播),网络层是方格网格拓扑 + 最短路径路由,上限 16 台。

有效拓扑的定义很有意思:"连通、不重叠、每条物理边最多一个邻居、只允许同轴拼合(所以相邻设备相对旋转只能是 0° 或 180°)"。出现姿态矛盾环或坐标重叠 → 判 topology conflict,直接 block 应用层发送------宁可不传,不传错方向。这个"冲突即静默"的策略在双屏游戏里就是:拼歪了游戏不同步,而不是画面错位。


08 | 科研视角、实操手册与关键技术总结

08.1 科研视角:ESP-Claw 踩中了哪三条研究线

把源码读完后,ESP-Claw 的架构决策几乎都能和 2024-2026 的端侧 Agent 论文对上:

|-------------------------|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------|
| 研究线 | 代表工作 | ESP-Claw 的对应实现 |
| 边缘 function calling 的效率 | TinyAgent (arXiv:2409.00608,"Function calling at the edge");Less-is-More(arXiv:2411.15399,限工具数 → 最高 -70% 执行时间 / -40% 功耗) | claw_cap 的 LLM 可见组白名单(默认仅 8/20+ 组)+ Skill 按需扩大可见性 |
| MCP 作为 LLM↔物理设备的标准接口 | IoT-MCP(arXiv:2510.01260,边缘部署 MCP server,6 族 MCU + 22 传感器) | cap_mcp_client/cap_mcp_server 双向 MCP,局域网 mDNS 发现 |
| 无 OS 裸机 Agent | MimiClaw(ESP32-S3,纯 C ReAct,无 Linux/Node) | ESP-Claw 整体:FreeRTOS 任务 + C,官方致谢其 Agent Loop/IM 实现 |

还有一个论文里少见、但产品里致命的维度:成本结构。LLM 按 token 计费,ESP-Claw 把"确定性逻辑下沉到 Lua 规则"直接改变了调用频次------06 章算过,"检测 3 秒开灯"从 30 次 LLM 往返变成 1 次脚本生成。端侧 Agent 的经济学本质不是"模型多小",而是"多少次动作必须过模型"。ESP-Claw 把这个问题做成了配置问题(router 规则 + 技能 + 可见组),而不是架构问题。

08.2 实操手册:从 0 到"说一句、亮一个灯"

适用场景 :手里有一块 ESP-Mosaico(或任意 ESP32-S3 板),想体验设备端 Agent 控制硬件的完整链路。 硬件要求:ESP-Mosaico(ESP32-S31)×1;扩展模块(继电器/灯阵)×1(可选,纯屏幕交互也能跑);Wi-Fi;一个 OpenAI/Qwen/Claude/DeepSeek API Key(出厂固件内置体验额度,用完自带控制面板配自己的)。

1.刷出厂固件(浏览器在线刷,不用装环境)

打开 Flash Online --- ESP-Claw(ESP-Claw 官方在线刷写)

选板子(ESP-Mosaico 在 Board Manager 支持列表内)

浏览器直连串口,一键刷写完成

2.配网 + 绑定 IM

短按 Power 开机,进 Vibe Mode,安装/打开 Mosaico-Claw Idea

Settings > AI Model 会显示设备 IP,同局域网用手机打开 → 控制面板

配 Wi-Fi;绑定微信(或 Telegram/QQ/飞书)

确认 ASR 与 LLM 服务可用(出厂内置额度可先用)

3. 第一句对话

在绑定的微信里发:

请简单介绍一下自己。

每 30 分钟提醒我喝水。

第二句走的是 cap_scheduler(cron/interval 调度 → 到时发 Event → Router 规则 → IM 提醒)。

4. 让 Agent 碰硬件(Chat Coding 核心体验)

在屏幕上绘制一朵旋转的小花,持续 30 秒。

帮我写个脚本:TOF 模块检测到 20cm 内有东西 3 秒,就打开继电器 1 通道。

设备上的 Claw 应用会生成 Lua 脚本并跑起来。生成的技能可以存进 Work 区复用/继续对话改进。

5. Console 验证(USB 串口,或控制面板的在线聊天)

cpp 复制代码
# 看当前对 LLM 可见的工具组
cap --list-groups
# 手动执行一段 Lua(跳过 LLM,验证纯硬件路径)
cap call lua_run_script '{"path": "test.lua"}'   # test.lua 内容:local gpio=require("gpio") gpio.set_direction(46,"output") gpio.set_level(46,1)
# 模拟一条事件看路由全过程
router --emit-message --source-cap tester --channel local --chat-id 1 --text 测试
router --last

6. 无板验证:PC 模拟器

esp-mosaico-vibe 仓库 + ESP-GSP 模拟器:不刷板子就能在 PC 上验证 UI 原型和交互逻辑,官方 Vibe Coding 流程(把 Codex/Claude Code/OpenCode 接进开发闭环)就是"模拟器 → 构建 → 真机"三段。

常见问题 & 解决方案:

|-------------------|------------------------------------------------------------------------------------------------------------|
| 问题 | 解决方案 |
| 刷完机 Console 没反应 | 确认串口号(USB HS 控制台走 USB OTG,不是原生 UART 引脚);idf.py -p PORT flash monitor 重连一次 |
| LLM 配置后 Agent 不响应 | 三要素缺一即跳过 core 初始化:llm_api_key/llm_model/llm_backend_type 全非空才行(日志会打印跳过原因);改完设置必须手动重启设备 |
| 模型老重复调同一个工具 | 查 max_tool_iterations(默认 32,Kconfig 调低可更快熔断);确认该工具的错误信息可读------模型要靠工具结果 JSON 学会改道 |
| 技能激活了但模型不用对应工具 | 检查 SKILL.md 的 metadata.cap_groups 是否填了正确的 group ID(claw_cap_group_info_t.id,如 cap_lua);目录名/name/构建路径必须三者一致 |
| 左右槽模块抢 I2C 总线 | 正常现象:module_mgr 的 claim 机制会仲裁;若某模块长期 INVALID,用 examples/module_eeprom_programmer 重写模块 EEPROM 描述符 |
| 相机插右槽没反应 | 硬件限制:相机 EEPROM 固定 0x50 无地址线,只支持左槽 |

08.3 关键技术总结

|-----------------------------|--------------------------------------------------|------------------------------------------------------------|
| 机制 | 作用 | 源码/文档位置 |
| claw_event_t + Event Router | 确定性控制平面:6 动作、JSON 规则、热重载、可审计 | claw_event_router/ + dataflow-and-automation.mdx |
| claw_core Agent Loop | 概率性决策平面:5-provider 上下文、32 轮工具熔断、收件箱插话、协作式取消 | claw_core_agent_loop.c(514 行) |
| claw_cap 注册表 + 可见组白名单 | 工具治理:默认 8 组可见,Skill 按需扩大(Less-is-More 工程化) | claw_cap/ + claw-cap.mdx |
| 三层 claim 体系 | 硬件交互控制核心:槽位三态 → 资源键独占 → 板级目录(board_owned 权限边界) | mosaico_module_mgr / claw_hw_registry / app_claw_hw_bridge |
| claw_memory 双尺度记忆 | 会话 append-only + 长期结构化(jsonl + 索引,摘要目录注入保缓存) | claw_memory/ + memory.mdx |
| Skill 目录 + Skills Lab | 运行时学招式:文档+脚本+资源,cap_groups 绑生命周期 | claw_skill/ + skills.mdx |
| 双向 MCP | 设备作为 Client/Server 接入 agent 生态(IoT-MCP 论文线的量产实现) | cap_mcp_client/server |
| mosaico_network 16 节点网格 | 磁吸多设备:双 BMM150 边分类 + ESP-NOW + 方格拓扑最短路径 | BSP mosaico_network/ |
| LLM 后端抽象 | OpenAI 风格 + Anthropic 风格双协议,4 后端实现 | claw_core/src/llm/backends/ |

08.4 我的判断(踩坑叙事之外的几句实话)

它不是"MCU 上跑 LLM",别被各种标题党带偏。 LLM 在云端,ESP32 跑的是 Agent 运行时 + 确定性执行体。512KB SRAM 跑 27B 模型?做梦。但跑一个"会自己看状态、写脚本、管引脚锁"的运行时,绰绰有余。

最值得抄的不是 Lua,是"三层 claim"。 任何做热插拔 + Agent 的平台都会撞上"谁在用这个 GPIO"的问题,ESP-Claw 给了一个可以整段搬走的参考实现(claw_hw_registry 核心不到 500 行)。

渐进式披露是端侧 Agent 的上下文工程第一性原理。 system prompt 冻结保缓存、技能按会话激活、工具白名单默认收窄------这三招合起来,是 24K token 输出预算下还能跟 200K 上下文云端模型周旋的全部秘密。

MCP 双向是它和"语音盒子固件"的分水岭。 Xiaozhi 们做的是"终端 + 工具宿主",ESP-Claw 做的是"设备自己也是 Agent"------后者才有资格谈"多设备网格 + 跨屏协作"。

风险也直说:框架还在快迭代(README 自述 under active development,不在 Bug Bounty 范围);LLM 可见组/技能/路由规则的组合空间爆炸,新手调试靠 router --last 和 Console 的 cap 直调;对"强工具调用能力"的模型有依赖(官方点名 gpt-5.4 / qwen3.6-plus / claude4.6-sonnet / deepseek-v4-pro 级别)。

参考资源

  • Less-is-More(arXiv:2411.15399):边缘 function calling 的工具数限制
  • TinyAgent(arXiv:2409.00608):边缘 function calling
  • IoT-MCP(arXiv:2510.01260):MCP 桥接 LLM 与 IoT 的边缘部署

全文完。


相关推荐
卷无止境22 分钟前
以 Obsidian 为核心的知识库体系:工具生态、AI 协同与最佳实践方案
人工智能
ShuangNaoPai32 分钟前
饱和品类的心智词生成:从“新米”认知到“五新米”的判定路由
人工智能·程序人生·产品·思维·品牌营销
星云_byto36 分钟前
CSS布局
人工智能·机器学习·目标跟踪·1024程序员节
天天代码码天天1 小时前
照片和视频之外,微信里的声音也值得好好保存
人工智能
IT_陈寒1 小时前
Java开发实战:从入门到精通
前端·人工智能·后端
lucas_AI1 小时前
560B 只激活 25B:蚂蚁百灵 Ling-3.2-flash 把原生多模态 API 卷到 2 块钱一百万 token
人工智能
lucas_AI1 小时前
OpenAI 融资缩水、Anthropic IPO 被压价:AI 的钱,突然不好融了
人工智能
miofly1 小时前
GitHub 周榜趋势速报 | 2026-11-08
开源·github
分布式存储与RustFS1 小时前
RustFS 自建对象存储实战目录:110 篇按场景分类(持续更新)
运维·云原生·开源·对象存储·分布式存储·s3·性能基准