news 2026/9/26 10:59:09

用 ESP32 给 Claude Code CLI 做个电子宠物:程序员的实体监工代码搭子

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 ESP32 给 Claude Code CLI 做个电子宠物:程序员的实体监工代码搭子

1. 凌晨两点,我盯着滚动的终端,决定给 Claude Code 配个实体监工

Claude Code CLI 是个好东西,工具自动执行、Bash 命令一条接一条,效率确实高。但问题也在这儿:它跑起来之后,终端就是个黑盒。你盯着飞速滚动的日志,心里直打鼓——它现在是在忙,还是卡住了?高危操作的审批提示会不会被日志刷过去漏看?我到底还要等多久?

这种失控感,用过 Claude Code 的人应该都懂。尤其是让它跑长任务的时候,你不可能一直盯着屏幕,但离开又怕它偷偷改了配置文件,或者某个需要审批的操作被忽略掉。

所以我决定给它做个实体监工:一块 ESP32 开发板,通过 BLE 和 PC 上的守护进程通信,把 Claude Code 的运行状态映射到桌面上的小屏幕。空闲时它闭眼打哈欠,忙碌时皱眉干活,等待审批时瞪大眼睛歪头等你点 YES,任务完成跳个爱心舞。你不用再刷日志找进度,瞟一眼小屏幕就知道 Claude 现在是在摸鱼还是在干活。

这篇文章我会把整套方案拆开讲:从 TaoToken 的 Key 配置,到 Claude Code 的 settings.json 骨架,再到 ESP32 端的 BLE 连接参数和 asyncio 状态轮询脚本。你可以跟着一步步搭起来,也可以只挑其中一部分用到自己的项目里。

2. 为什么用 TaoToken 统一 Key 和 API 通道

在动手写代码之前,先把接入层理清楚。Claude Code CLI 本身支持自定义 API 端点,但如果你同时用多个模型或者多个工具,每个都单独配 Key 和地址会很乱。TaoToken 在这里的作用就是统一入口:一个 Key 走通所有请求,API 地址固定,不用在每个工具里重复填不同的配置。

具体来说,TaoToken 提供的是兼容 Anthropic 协议的 API 通道。你只需要在 Claude Code 的配置里把 base_url 指向https://taotoken.net/api,然后把 API Key 填进去,剩下的请求格式、鉴权方式都和原生一致。这样做的直接好处是:你的 ESP32 守护进程、Claude Code CLI、以及后续可能加的其他工具,都走同一个 Key 和同一个通道,排查问题的时候不用来回切换配置。

如果你还没有 Key,可以去 TaoToken 控制台创建一个。创建完之后,在 API Keys 页面能看到完整的 Key 字符串,复制下来备用。注意这个 Key 只在创建时显示一次,记得存好。

注意:TaoToken 的 API 地址是https://taotoken.net/api,不要加多余的路径后缀。Claude Code 的配置里填这个地址就行。

3. 可复制配置:settings.json 与 config.toml 骨架

Claude Code CLI 的配置分两部分:一部分是 Claude Code 自己的 settings.json,另一部分是守护进程用的 config.toml。先看 Claude Code 这边。

3.1 Claude Code settings.json

在项目根目录或者用户目录下创建.claude/settings.json,内容如下:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-your-taotoken-key-here" }, "permissions": { "allow": [ "Bash", "Write", "Edit" ] }, "hooks": { "PreToolUse": [ { "matcher": "Bash|Write|Edit", "hooks": [ { "type": "command", "command": "python3 ~/claude-buddy/hook_bridge.py" } ] } ] } }

这里有几个关键点。ANTHROPIC_BASE_URL指向 TaoToken 的 API 地址,ANTHROPIC_API_KEY填你刚才创建的 Key。hooks里的PreToolUse是 Claude Code 在执行工具之前触发的钩子,我们用它把审批请求转发给 ESP32。hook_bridge.py是 PC 端的桥接脚本,负责和守护进程通信。

3.2 守护进程 config.toml

守护进程跑在 PC 上,负责 BLE 连接管理和状态轮询。配置文件config.toml如下:

[ble] device_name = "ClaudeBuddy" service_uuid = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" tx_char_uuid = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" rx_char_uuid = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" mtu = 20 reconnect_interval = 5 [daemon] poll_interval = 0.5 approval_timeout = 30 log_level = "INFO" [taotoken] base_url = "https://taotoken.net/api" api_key = "sk-your-taotoken-key-here"

BLE 部分用的是 Nordic UART Service 的 UUID,这是 ESP32 上最常用的 BLE 串口服务。mtu = 20是 BLE 单次传输的字节限制,后面会在驱动层做透明分片。approval_timeout = 30表示审批请求发出后 30 秒内没有响应就自动放行,避免卡住任务。

4. ESP32 端 BLE 连接与 asyncio 状态轮询脚本

ESP32 这边我用的是 MicroPython,因为 asyncio 支持比较完整,写并发任务方便。核心逻辑分三块:BLE 连接管理、状态机、渲染循环。

4.1 BLE 透明分片

ESP32 的 BLE NUS 服务单次只能发 20 字节,JSON 消息很容易被截断。我在驱动层做了透明拼接:发送时自动按 20 字节分片,加帧头标记;接收时自动拼接完整消息,遇到换行符再抛给上层。这样上层代码收发 JSON 就和普通字符串一样简单。

import bluetooth import struct import asyncio class BLETransport: def __init__(self, ble, conn_handle): self.ble = ble self.conn_handle = conn_handle self.rx_buffer = b"" async def send_json(self, data: str): payload = data.encode("utf-8") + b"\n" for i in range(0, len(payload), 20): chunk = payload[i:i+20] self.ble.gatts_notify(self.conn_handle, TX_CHAR, chunk) await asyncio.sleep_ms(10) def on_rx(self, data: bytes): self.rx_buffer += data while b"\n" in self.rx_buffer: line, self.rx_buffer = self.rx_buffer.split(b"\n", 1) asyncio.create_task(self.handle_message(line.decode("utf-8")))

4.2 双层状态机

状态机分两层:base 状态和 Claude Code 同步,active 状态是临时动画。比如审批通过后跳爱心舞,持续 2-3 秒后自动回落到 base 状态。这样就算 Claude 还在忙碌,跳完庆祝舞也会自动回到忙碌状态,不用写复杂的恢复逻辑。

class StateMachine: def __init__(self): self.base_state = "idle" self.active_state = None self.active_until = 0 def set_base(self, state: str): self.base_state = state def trigger_active(self, state: str, duration_ms: int = 2500): self.active_state = state self.active_until = ticks_ms() + duration_ms def current(self) -> str: if self.active_state and ticks_ms() < self.active_until: return self.active_state self.active_state = None return self.base_state

4.3 三任务异步并发

用 asyncio 开三个独立任务:ble_task 处理蓝牙连接和消息收发,touch_task 监听触控,render_task 固定 20FPS 渲染动画。就算 Claude 疯狂发消息,小猫的眨眼动画也不会掉帧。

async def ble_task(transport): while True: await transport.process() await asyncio.sleep_ms(50) async def touch_task(state_machine): while True: if touch_pressed(): state_machine.trigger_active("approve") await asyncio.sleep_ms(100) async def render_task(state_machine, display): while True: state = state_machine.current() display.render(state) await asyncio.sleep_ms(50) async def main(): state_machine = StateMachine() transport = BLETransport(ble, conn_handle) await asyncio.gather( ble_task(transport), touch_task(state_machine), render_task(state_machine, display) )

5. 验证请求:从终端到 ESP32 的完整链路

配置写完之后,先验证 TaoToken 的 API 通道是否通。在终端里直接跑一条 curl:

curl -X POST https://taotoken.net/api/v1/messages \ -H "x-api-key: sk-your-taotoken-key-here" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 100, "messages": [{"role": "user", "content": "ping"}] }'

如果返回正常的 JSON 响应,说明 Key 和通道都没问题。然后启动守护进程:

python3 ~/claude-buddy/daemon.py --config ~/claude-buddy/config.toml

守护进程会开始扫描 BLE 设备,找到名为ClaudeBuddy的 ESP32 后自动连接。连接成功后,ESP32 屏幕上会显示CONNECTED,然后进入空闲状态。

接下来在另一个终端里跑 Claude Code:

claude --dangerously-skip-permissions

注意这里加了--dangerously-skip-permissions是为了让 Claude Code 不弹终端审批,全部走我们的 Hook 转发。当你让 Claude 执行一个 Bash 命令时,ESP32 屏幕上会弹出审批界面,显示命令内容和一个倒计时。点一下 YES,命令才会真正执行。

实测下来,从 Hook 触发到 ESP32 显示审批请求,延迟在 200ms 左右,基本感觉不到。审批通过后,小猫会跳个爱心舞,然后回到忙碌状态继续同步进度。

6. 本篇常见错排查

6.1 BLE 连接不上

最常见的原因是设备名不对。ESP32 广播的名称必须和 config.toml 里的device_name完全一致,大小写敏感。另外确认一下 PC 的蓝牙适配器支持 BLE 4.0 以上,有些老适配器只支持经典蓝牙,扫不到 NUS 服务。

如果连接后频繁断开,把reconnect_interval调大一点,比如 10 秒。BLE 连接本身就不太稳定,加个自动重连逻辑能省很多事。

6.2 Hook 不触发

检查.claude/settings.json里的hooks配置是否正确。matcher字段用的是正则表达式,Bash|Write|Edit表示匹配这三种工具。如果你想让所有工具都触发审批,可以改成.*。

另外确认hook_bridge.py有可执行权限,并且路径是绝对路径。Claude Code 执行 Hook 的时候工作目录可能和你想象的不一样,用绝对路径最保险。

6.3 TaoToken 返回 401

先检查 Key 有没有复制完整,前后有没有多余空格。然后确认ANTHROPIC_BASE_URL填的是https://taotoken.net/api,不要加/v1或者别的后缀。如果还是 401,去 TaoToken 控制台确认一下 Key 的状态,看看是不是被禁用了或者额度用完了。

6.4 ESP32 屏幕不刷新

大概率是 render_task 被阻塞了。检查一下有没有在渲染循环里做耗时操作,比如同步的文件读写或者网络请求。所有耗时操作都应该放到独立的 asyncio 任务里,用await asyncio.sleep_ms()让出控制权。

如果屏幕完全没反应,先确认电源是否正常。ESP32 的屏幕背光耗电不小,USB 供电不足的时候可能会闪屏或者不亮。

7. 接入文档与后续调试

整套方案跑通之后,你可以根据自己的需求调整。比如换一个 ASCII 角色,或者加一个蜂鸣器,审批请求来的时候响一声。硬件抽象层都放在config.py里,换板子只需要改引脚定义,业务代码不用动。

如果你在接入 TaoToken 的过程中遇到问题,或者想看看完整的 API 参数说明,可以直接翻接入文档。需要管理多个 Key 或者查看用量的话,控制台里有详细的调用记录。长期跑编码任务的话,Coding Plan 的额度更划算,适合把 Claude Code 当成日常开发搭子的人。

调试的时候有个小技巧:先把守护进程的日志级别调到 DEBUG,这样能看到每一条 BLE 消息的收发内容。确认链路通了之后再调回 INFO,不然日志刷得太快反而看不清关键信息。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 10:57:33

前端入门必装:VS Code 实用插件 + TaoToken 统一 Key 配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 10:56:03

vi/vim 中空格与 tab 互转:expandtab、tabstop 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华