1. 为什么要在 AIWatch 上改 endpoint
AIWatch 是一套跑在 ESP32-S3 上的开源 AI 语音可穿戴终端固件,核心能力是把一块带 AMOLED 屏、双麦克风、扬声器和 SD 卡槽的开发板,变成一个能离线唤醒、自然语音对话、还能播放 MP3 的随身 AI 伴侣。它默认通过 WebSocket 连接自托管的 OpenClaw 网关,再由网关去调用 STT(语音转文字)和 TTS(文字转语音)服务。问题就出在这里:对大多数嵌入式开发者来说,自己搭一套 OpenClaw 网关、再单独申请小米 MiMo 的 ASR/TTS 密钥,前期成本太高,而且一旦网关挂了,整台设备就变成一块只会亮屏的砖。
我试过在本地用 Docker 起 OpenClaw,光是 ED25519 设备凭据的生成和网关侧的白名单配置就折腾了一下午,更别说还要维护 STT 和 TTS 两条独立的 HTTP 链路。对于只想验证「ESP32-S3 能不能跑通端到端语音对话」的人来说,这套架构太重了。真正想要的是一个统一的 Key/API 通道:设备端只认一个 Base URL、一个 Key,剩下的模型路由、鉴权、流式回包都由服务端处理。
TaoToken 在这里扮演的就是这个统一通道的角色。它对外暴露 OpenAI 兼容的 HTTP 接口,STT、TTS、LLM 都可以走同一套鉴权和 endpoint 规范。你要做的,是把 AIWatch 固件里原本指向 OpenClaw 网关和 MiMo 平台的地址,改成 TaoToken 的 API 地址,把分散的密钥收敛成一个 Key。改完之后,语音上行、模型回包、TTS 流式播放这三段链路依然完整,但配置复杂度从「网关 + 两个平台密钥」降到「一个 Base URL + 一个 Key」。
这篇文章面向已经拿到 Waveshare ESP32-S3-Touch-AMOLED-2.06 或类似板卡、能编译 ESP-IDF 工程的嵌入式开发者。我会从工程结构讲起,给出可以直接复制的 endpoint 和鉴权配置片段,然后带你用串口 CLI 验证语音上行和模型回包是否跑通,最后给一份失败排查清单。全程不涉及任何网络加速工具,所有请求都走正常的 HTTPS 出站。
需要提前说明的是,AIWatch 的语音链路分三段:唤醒词在本地由 ESP-SR 的 WakeNet 引擎处理,不联网;录音结束后音频通过 HTTP 上传到 STT 服务;STT 返回文本后,再由 LLM 生成回复,最后 TTS 把回复转成音频流回传播放。我们要改的是后三段的 endpoint 和鉴权字段,唤醒词部分保持原样。
2. TaoToken 前置准备与工程定位
在动固件代码之前,先把服务端这边的事情理清楚。TaoToken 的 API 入口是https://taotoken.net/api,它兼容 OpenAI 的接口规范,也就是说你原来调/v1/chat/completions、/v1/audio/transcriptions、/v1/audio/speech这些路径的代码,只需要把 Base URL 换掉、Key 换掉就能跑。对于 AIWatch 这种固件项目,这一点很关键,因为它的 STT 和 TTS 组件本来就是按 OpenAI 格式写的 HTTP 请求,改造成本极低。
你需要先拿到一个 API Key。登录 TaoToken 控制台,在 API Keys 页面创建一个新 Key,复制下来。这个 Key 会同时用于 STT、TTS 和 LLM 三条链路,不需要为每个服务单独申请。如果你还没注册,可以先访问官网了解服务范围,再进控制台创建 Key。整个流程不涉及任何特殊网络配置,正常浏览器访问即可。
接下来是工程定位。AIWatch 的仓库结构里,和网络请求相关的组件集中在components/目录下:
components/stt/:语音转文字,默认走 MiMo ASR 的 HTTP REST 接口components/tts/:文字转语音,默认走 MiMo TTS 的 HTTP SSE 流式接口components/openclaw/:WebSocket 客户端,负责和 OpenClaw 网关通信,带 ED25519 认证main/include/secrets.h:存放 WiFi 凭据和各服务 API Key 的配置文件
我们的改造策略是:保留openclaw组件的 WebSocket 框架不动(因为状态机和资源仲裁都依赖它),但把它连接的 host 和 port 指向 TaoToken 的兼容端点;同时把stt和tts组件里的 Base URL 和鉴权头改成 TaoToken 的规范。这样改动量最小,也不会破坏原有的 15 态状态机。
在开始改代码前,先确认你的开发环境就绪:
# 确认 ESP-IDF 版本,需要 v5.5 及以上 idf.py --version # 确认目标芯片 idf.py set-target esp32s3 # 确认工程能正常编译(改代码前先跑一次基线) idf.py build如果基线编译就报错,先解决工具链问题,不要急着改 endpoint。常见的基线错误是 ESP-IDF 版本低于 v5.5,导致esp_lcd_sh8601驱动不兼容。这种情况下先按官方文档升级 IDF。
另外,secrets.h是从secrets.h.example复制出来的,默认不在 git 跟踪范围内。你需要先执行:
cp main/include/secrets.h.example main/include/secrets.h然后在这个文件里填入 WiFi SSID、密码,以及我们接下来要配的 TaoToken Key。这个文件是固件里所有敏感信息的唯一入口,改它比散落在各个组件里改宏定义要清晰得多。
3. 可复制的 endpoint 与鉴权配置
这一节是全文的核心,所有片段都可以直接复制到你的工程里。我会按「配置文件 → STT 组件 → TTS 组件 → OpenClaw 客户端」的顺序给出改动点,每个片段都标注了文件路径。
3.1 secrets.h 里的统一 Key 配置
打开main/include/secrets.h,把原来分散的 MiMo Key 和 OpenClaw 凭据替换成 TaoToken 的统一配置:
// main/include/secrets.h #pragma once // WiFi 配置 #define CONFIG_WIFI_SSID "你的WiFi名称" #define CONFIG_WIFI_PASSWORD "你的WiFi密码" // TaoToken 统一 API 配置 #define TAOTOKEN_API_BASE "https://taotoken.net/api" #define TAOTOKEN_API_KEY "sk-你的TaoToken密钥" // 模型 ID 配置(按需替换为你账号下可用的模型) #define TAOTOKEN_STT_MODEL "whisper-1" #define TAOTOKEN_TTS_MODEL "tts-1" #define TAOTOKEN_LLM_MODEL "gpt-4o-mini" // OpenClaw 兼容端点(指向 TaoToken 的 WebSocket 兼容入口) #define OPENCLAW_GATEWAY_HOST "taotoken.net" #define OPENCLAW_GATEWAY_PORT 443 #define OPENCLAW_GATEWAY_PATH "/api/ws"这里有几个点要注意。第一,TAOTOKEN_API_BASE不带尾部斜杠,组件里拼接路径时统一用%s/v1/...的格式。第二,OPENCLAW_GATEWAY_HOST只填域名,不带https://前缀,因为 WebSocket 客户端会自己处理 TLS。第三,模型 ID 不是固定的,你要根据自己账号下实际可用的模型来填,STT 和 TTS 的模型名如果和示例不同,以控制台里显示的为准。
3.2 STT 组件的 endpoint 改造
找到components/stt/目录下的请求构造代码。通常是一个stt_request.c或类似文件,里面会有硬编码的 MiMo API 地址。把它改成从secrets.h读取:
// components/stt/stt_request.c(片段) #include "secrets.h" static const char *STT_ENDPOINT_FMT = "%s/v1/audio/transcriptions"; esp_err_t stt_build_request(char *url_buf, size_t url_len, char *auth_buf, size_t auth_len) { // 拼接完整 endpoint snprintf(url_buf, url_len, STT_ENDPOINT_FMT, TAOTOKEN_API_BASE); // 构造 Bearer 鉴权头 snprintf(auth_buf, auth_len, "Authorization: Bearer %s", TAOTOKEN_API_KEY); return ESP_OK; }对应的 HTTP 请求头里,除了Authorization,还要确保Content-Type是multipart/form-data,因为音频上传走的是表单格式。如果你原来的代码用的是 MiMo 特有的鉴权字段(比如X-Api-Key之类),全部替换成标准的Authorization: Bearer。
3.3 TTS 组件的 endpoint 改造
TTS 走的是 SSE 流式输出,改动点在components/tts/下。核心是把请求 URL 和鉴权头换成 TaoToken 规范:
// components/tts/tts_stream.c(片段) #include "secrets.h" static const char *TTS_ENDPOINT_FMT = "%s/v1/audio/speech"; esp_err_t tts_build_stream_request(char *url_buf, size_t url_len, char *auth_buf, size_t auth_len, const char *text) { snprintf(url_buf, url_len, TTS_ENDPOINT_FMT, TAOTOKEN_API_BASE); snprintf(auth_buf, auth_len, "Authorization: Bearer %s", TAOTOKEN_API_KEY); // 请求体 JSON,注意 model 和 voice 字段 // {"model":"tts-1","input":"...","voice":"alloy","stream":true} return ESP_OK; }TTS 的请求体是 JSON,stream字段设为true才能拿到 SSE 流。如果你不需要流式播放,可以设为false,但 AIWatch 的 TTS 播放器是按流式设计的,建议保持true。
3.4 OpenClaw 客户端的 host 与 port 改造
components/openclaw/里的 WebSocket 客户端默认连自托管网关。找到初始化连接的地方,把 host 和 port 改成从secrets.h读取:
// components/openclaw/openclaw_client.c(片段) #include "secrets.h" static esp_websocket_client_config_t ws_cfg = { .uri = "wss://" OPENCLAW_GATEWAY_HOST OPENCLAW_GATEWAY_PATH, .port = OPENCLAW_GATEWAY_PORT, .transport = WEBSOCKET_TRANSPORT_OVER_SSL, .reconnect_timeout_ms = 5000, .network_timeout_ms = 10000, };注意 URI 用的是wss://而不是ws://,因为 TaoToken 的入口是 HTTPS,WebSocket 必须走 TLS。ED25519 设备凭据那部分如果 TaoToken 侧不需要,可以在配置里关掉,或者保留但填一个占位值。具体以你账号下的接入文档为准。
3.5 配置对照表
为了让你一眼看清改了哪些字段,我整理了一张对照表:
| 配置项 | 改造前 | 改造后 |
|---|---|---|
| STT Base URL | MiMo 平台地址 | https://taotoken.net/api |
| TTS Base URL | MiMo 平台地址 | https://taotoken.net/api |
| 鉴权字段 | 平台专有字段 | Authorization: Bearer <Key> |
| OpenClaw Host | 自托管网关 IP | taotoken.net |
| OpenClaw Port | 自定义端口 | 443 |
| Key 数量 | 多个平台密钥 | 一个统一 Key |
改完这些,执行idf.py build确认编译通过。如果报错说找不到secrets.h,检查一下main/include/是否在 include 路径里,以及 CMakeLists 有没有把main目录加进去。
4. 串口验证语音上行与模型回包
编译通过后,把固件烧录到板子上,用串口 CLI 验证整条链路。烧录命令:
idf.py -p /dev/ttyACM0 flash monitorWindows 下端口可能是COM3之类,按设备管理器里显示的实际端口填。烧录完成后,串口会输出启动日志,你会看到状态机从BOOT走到CONNECTING,再走到IDLE。如果卡在CONNECTING,说明 WebSocket 没连上,先跳到第 5 节排查。
4.1 用 CLI 验证文本链路
在串口 CLI 里输入status,确认设备状态:
AIWatch> status State: IDLE WiFi: connected (192.168.1.100) Gateway: connected STT endpoint: https://taotoken.net/api/v1/audio/transcriptions TTS endpoint: https://taotoken.net/api/v1/audio/speech如果Gateway显示disconnected,说明 WebSocket 没连上。如果 endpoint 显示的还是旧地址,说明secrets.h没生效,检查一下是不是改错了文件。
接下来用say命令直接发文本给 AI,跳过录音和 STT,先验证 LLM 和 TTS 链路:
AIWatch> say 你好,请用一句话介绍你自己正常的话,你会看到状态机依次经过THINKING→STREAMING→RESPONSE→TTS_LOADING→TTS_PLAYING,然后扬声器里传出 AI 的语音回复。串口日志里会打印出模型返回的文本,类似:
[LLM] response: 你好,我是运行在 ESP32-S3 上的 AI 语音助手... [TTS] streaming 12800 bytes [TTS] playback done这一步跑通,说明 LLM 和 TTS 的 endpoint 和鉴权都对了。
4.2 验证语音上行链路
文本链路通了之后,再验证录音和 STT。输入talk命令进入语音对话模式:
AIWatch> talk State: LISTENING 请说话...对着板子的麦克风说一句话,比如「今天天气怎么样」。说完后,VAD 会检测到静音并结束录音,状态机进入SENDING,音频上传到 STT。串口会打印转写结果:
[STT] transcript: 今天天气怎么样 [LLM] response: 我无法获取实时天气,但可以帮你...如果[STT] transcript是空的或者乱码,说明音频上传或转写有问题,检查 STT 的 endpoint 和Content-Type是否正确。
4.3 用 curl 做旁路验证
如果你怀疑是固件问题,可以先用 curl 在电脑上验证 TaoToken 的接口是否正常:
# 验证 LLM 接口 curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' # 验证 TTS 接口 curl -s https://taotoken.net/api/v1/audio/speech \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{"model":"tts-1","input":"test","voice":"alloy"}' \ --output test.mp3如果 curl 能通而固件不通,问题就在固件侧;如果 curl 也不通,问题在 Key 或网络侧。这种二分法能帮你快速定位。
4.4 观察内存与状态
AIWatch 有内存监控组件,串口会周期性打印 DRAM/PSRAM 使用情况。语音链路跑通后,观察一下内存是否稳定:
[MEM] DRAM free: 182KB, PSRAM free: 6.2MB如果 DRAM 持续下降,可能是 HTTP 响应没释放,检查一下esp_http_client的 cleanup 逻辑。PSRAM 主要给 LVGL 和音频缓冲用,正常应该在 6MB 以上。
5. 常见报错排查清单
这一节按真实报错来组织,每条都给出原因和修复方法。
5.1 401 Unauthorized
串口日志里出现:
[STT] HTTP status: 401 [STT] response: {"error":{"message":"Invalid API key"}}原因通常是 Key 填错、Key 过期,或者鉴权头格式不对。检查secrets.h里的TAOTOKEN_API_KEY是否完整复制,有没有多余空格。鉴权头必须是Authorization: Bearer sk-xxx,注意Bearer和 Key 之间有一个空格。如果你用的是环境变量注入,确认编译时变量确实传进去了。
5.2 local proxy failed / connection refused
[OpenClaw] websocket connect failed: local proxy failed这个报错说明 WebSocket 客户端尝试走本地代理但失败了。检查ws_cfg里有没有误设.proxy字段,把它删掉或设为 NULL。另外确认transport是WEBSOCKET_TRANSPORT_OVER_SSL,端口是 443。如果你的开发环境有全局代理,先关掉再试。
5.3 reading choices 解析失败
[LLM] parse error: reading choices: unexpected end of JSON这是 LLM 返回的 JSON 不完整导致的。常见原因是 HTTP 响应体太大,接收缓冲区不够。检查esp_http_client的buffer_size配置,建议设为 4096 以上。另外确认你请求的模型 ID 是有效的,如果模型名写错,服务端可能返回一个非标准 JSON,导致解析失败。
5.4 OAuth / token 过期
[Gateway] auth failed: OAuth token expired如果你保留了 OpenClaw 的 ED25519 认证逻辑,而 TaoToken 侧不需要这个认证,就会出现这个报错。解决方法是在openclaw_client.c里把认证回调设为 NULL,或者填一个占位凭据。具体以接入文档为准。
5.5 状态机卡在 CONNECTING
如果串口一直显示State: CONNECTING,先确认 WiFi 是否连上(status命令会显示)。WiFi 正常但 WebSocket 连不上,检查OPENCLAW_GATEWAY_PATH是否正确,以及 TaoToken 侧是否支持 WebSocket 接入。如果不支持,可以改用 HTTP 轮询模式,或者只保留 STT/TTS 的 HTTP 链路,LLM 部分用say命令触发。
5.6 排查速查表
| 报错关键词 | 最可能原因 | 修复动作 |
|---|---|---|
| 401 Unauthorized | Key 错误或格式不对 | 检查 Bearer 头 |
| local proxy failed | 误设代理字段 | 删除 proxy 配置 |
| reading choices | 缓冲区太小或模型名错 | 增大 buffer,核对模型 ID |
| OAuth expired | 多余认证逻辑 | 关闭 ED25519 回调 |
| 卡在 CONNECTING | WebSocket 路径错 | 核对 path 和端口 |
排查时建议打开详细日志,在 menuconfig 里把 log level 设为 Debug,这样能看到完整的 HTTP 请求和响应头,定位问题会快很多。
6. 把统一通道用起来
改完 endpoint 之后,AIWatch 的语音链路就收敛到一条通道上了。你不再需要维护 OpenClaw 网关和 MiMo 平台两套密钥,STT、TTS、LLM 共用同一个 Key,换模型只需要改secrets.h里的模型 ID。对于嵌入式开发者来说,这意味着你可以把精力放在硬件调试和交互优化上,而不是服务端的运维。
如果你后续要做长期编码或 Agent 类的实验,可以了解一下 Coding Plan,它适合需要持续调用模型的场景。如果只是想验证某个模型的效果,可以直接在模型对话页面里试。接入过程中遇到鉴权或 endpoint 问题,API Keys 页面和接入文档是最直接的参考。
最后给一个实用建议:把secrets.h加入.gitignore,避免 Key 泄露。如果你要分享工程给别人,用secrets.h.example做模板,把 Key 留空。另外,TTS 的流式播放对网络抖动比较敏感,如果发现播放卡顿,可以在tts_stream.c里把接收缓冲区调大,或者把stream设为false改用整段下载再播放。这些细节调优,比改 endpoint 更能影响最终体验。