大多数桌面小玩意只是展示信息。我想要一个能对人做出反应的:一个在你说话时会看着你、会回答你、在没人时还会做点有趣事情的机器人。乐鑫 ESP-VoCat v1.2 具备实现这一切的硬件。它将 ESP32-S3 SoC、一块 360×360 的圆形触摸屏、两个麦克风、一个扬声器和一块电池集成在带有猫耳的机壳中。
VoCat Eyes 是我为它编写的脸部程序,也是 Cute Desktop Robot 系列的第一个机器人。它的主要特性包括:
- 56 种面部表情和 47 段短动画
- 一个小型状态机,决定机器人的行为:玩耍、反应、无聊、入睡
- 眼睛会转向正在说话的人
- 通过 Gemini Live API 进行语音对话,用唤醒词“Hi ESP”或轻触启动
- 在机器人自己的屏幕上设置 Wi-Fi、音量、亮度和语音
- 一个模拟器,可在 PC 和本页面中运行同一套代码
固件、模拟器和工具均在 cute-desktop-robot 仓库中开源。

上图是模拟器的 3D 视图:乐鑫发布的机壳模型,内部嵌入着被模拟机器人的实时屏幕画面。
下面是一台放在桌上的真实机器人,运行着同一套固件:
真实硬件上的机器人:在 YouTube 上观看。
在浏览器中体验机器人
下面的模拟器运行的是编译为 WebAssembly 的机器人 C++ 代码:与 ESP32-S3 上运行的是同一套脸部引擎、状态机和界面。只有硬件层被替换为模拟部件。无需账号,无需安装。只有语音对话需要真实机器人,因为模拟器没有网络。
如果画面太小,可以在全窗口中打开模拟器。
窗口分为三部分。左侧是 3D 机器人,下方是状态机,右侧是带有情绪图和传感器的面板。试用方法:
- 轻触机器人的屏幕。 在 3D 机器人的圆形屏幕上单击一次。机器人会播放下一个动画。
- 打开设置。 按住屏幕直到时钟和电池出现,然后单击时钟一次。设置菜单会打开。单击某一行的左半部分降低数值,单击右半部分提高数值。按住可关闭菜单。
- 从一侧说话。 在右侧面板的 SENSORS 下,将 from 旁边的滑块拖到某个角度,然后单击 talk [V]。地板上的紫色圆点显示模拟声音的来源,机器人的眼睛会转向它。
- 观察状态机。 每当机器人改变活动时,状态机中对应的方框会亮起,底部的条形图显示接下来可能改变状态的计时器。
- 转动机器人。 在屏幕外的任意位置拖动可旋转 3D 视图,使用鼠标滚轮缩放。按
M切换到屏幕的平面视图。
单击模拟器后键盘也可使用:1 到 9 显示表情,Y 点头,U 摇头,H 显示对话气泡。轻触和设置在真实机器人上的操作方式相同。
下面的截图展示了桌面 PC 上整个模拟器窗口的样子:

硬件
ESP-VoCat 出厂即已组装好,机壳、电池和屏幕均已就位。ESP-VoCat 用户指南详细介绍了该开发板。一个 ESP32-S3 模组承担所有工作。它拥有 16 MB 闪存和 16 MB PSRAM(外部 RAM)。PSRAM 使 360×360 的脸部成为可能:一帧 16 位色的完整画面需要 253 KB,超过了 SoC 内部 RAM 的容量。
下图展示了固件使用的部件以及每个部件连接到 ESP32-S3 的总线:
有两个部件需要额外处理:
- 屏幕点亮了背光但保持黑屏。我从厂商驱动改编的初始化序列缺少最后两条命令:像素格式(
0x3A)和开启显示(0x29)。 - 麦克风最初只返回零。麦克风 ADC(ES7210)和扬声器 DAC(ES8311)共享相同的 I2S 时钟线。因此 I2S 总线在两个方向上必须使用相同的帧格式:具有四个 16 位时隙的 TDM(时分复用)。做出这一更改后,两个麦克风出现在时隙 0 和 2 中,时隙 1 携带扬声器信号的副本用于回声参考。
机器人如何绘制它的脸
机器人每 33 ms 重绘一次脸部,大约每秒 30 次。下图展示了其中一帧从传感器到屏幕的过程:
图中的每个方块都是固件的一个模块:
- Sensors 将触摸、电量、时间和声音事件收集到一个结构中。
- Brain 是一个状态机,包含 awake、show、reacting、dozing、sleeping、info 和 manual 等状态。每个状态在特定事件下退出,例如触摸、大声响、一天中的时间或长时间独处。Brain 还跟踪缓慢的情绪:经常被抚摸的机器人会倾向于开心的表情,而被戳太多次的机器人会变得烦躁。
- FaceEngine 沿着一张图在表情之间移动。56 种表情是它的节点,换脸遵循最短路径,因此机器人会通过中间表情从开心过渡到悲伤。每只眼睛由几个参数描述,例如大小、圆角半径、倾斜度、眼睑和颜色,引擎会对它们进行插值。
- Canvas 在 PSRAM 中绘制帧,并记录哪些 24×24 图块发生了变化。一次眨眼只改变少数图块,因此显示总线发送的是几 KB 而不是完整的 253 KB。
- VocatLcd 通过 QSPI(四线 SPI 总线)将变化的图块发送到 ST77916 显示屏。
所有表情和动画
模拟器包含一个命令行工具 facetool,它无需打开窗口即可渲染机器人的屏幕。下面的视频均由它生成。每个视频会同时播放同一组中的所有表情:先展示切换到每个表情的过程,再展示表情本身。
表情
平静与思考(16 种):普通、好奇、专注、怀疑、无聊、困惑、倾听等。
开心(15 种):从微微一笑到开怀大笑、爱心眼和星星眼。
难过(8 种):担忧、恳求、哭泣、孤独、内疚等。
生气(6 种):从恼怒到暴怒,以及厌恶。
惊讶与警觉(7 种):惊讶、害怕、眩晕、昏迷、被催眠等。
困倦(4 种):困倦、打哈欠、睡着和疲惫。
动画
动画在代码中称为 act,是一些短场景,其中许多会在眼睛旁边绘制一个物体:雨云、咖啡杯、雷达屏幕。Brain 会在机器人空闲时播放它们,轻触屏幕则会播放下一个。
肢体语言(9 种):点头、摇头、翻白眼、打喷嚏。
屏幕特效(5 种):故障、重启、扫描线、代码雨。
带物体(18 种):爱心、下雨、烟花、泡泡、球、跳舞。
日常事物(9 种):书、耳机、咖啡、火箭、雪。
游戏与圆屏特效(6 种):吃豆人、乒乓、彩虹、雷达、水族箱。
同样的网格图(以动画 GIF 形式)位于仓库 README 中。
机器人如何说话
对话使用 Gemini Live API,它通过一条 WebSocket 连接接收音频流,并以音频流作答。在撰写本文时,机器人使用的是 gemini-2.5-flash-native-audio 模型。下图展示了音频的传输路径:
机器人将一只麦克风的声音以 16 kHz PCM 音频发送,每块 100 毫秒。Gemini 以 24 kHz 语音作答,机器人将其转换为 16 kHz 并播放。
对话是半双工的:机器人要么在听,要么在说,绝不会同时进行。扬声器距离麦克风只有几厘米,因此机器人在播放回答时会静音麦克风流。否则 Gemini 会听到自己的声音。由 Gemini 在其一侧判断对方是否已经说完。
Gemini 还会收到四个工具,即它可以在对话中调用的函数:set_face、play_act、look 和 show_text。当对话变得伤感时,Gemini 可以调用 set_face("sad")。当你让机器人表示同意时,它可以调用 play_act("yes"),机器人便会点头。每次会话开始时都会发送一条包含本地时间和日期的简短文本消息,这样 Gemini 就能回答相关问题。
有三个问题占用了大部分开发时间:
- 安全连接。 机器人通过 TLS 连接 Gemini。ArduinoWebsockets 库的
setInsecure()函数在 ESP32-S3 上不起作用,因此 TLS 握手失败。固件现在内置了 Google 的两张根证书(GTS Root R1 和 R4,有效期至 2036 年)。它还会等待时钟通过 NTP 同步,因为验证证书需要当前日期。 - 内部 RAM。 “Hi ESP”唤醒词运行在 ESP-SR(乐鑫的语音识别库)上,它占用约 100 KB 内部 RAM。TLS 库在握手时需要内部 RAM,最初的尝试因
SSL - Memory allocation failed而失败。机器人现在会在对话开始时停止 ESP-SR,从而释放 107 KB,并在对话结束时重新加载它。加载 ESP-SR 大约需要 1.2 秒。 - 缺失的麦克风流。 有一天,机器人向用户问好后便忽略了之后的一切。它的眼睛仍然跟随我的声音,所以麦克风显然工作正常。原因是固件从未启用发往 Gemini 的音频流。串口控制台现在会打印 Gemini 对它所听到内容的转写,这样同样的问题就能立刻被发现。
使用 Gemini 需要来自 Google AI Studio 的 API 密钥,并且该 API 会根据 Google 的 Gemini API 定价 按使用量计费。密钥保存在本地的 secrets.h 文件中,绝不会提交到仓库。
机器人屏幕上的设置
机器人设置完成后就不再需要电脑。长按屏幕会显示时钟、电池电量和 Wi-Fi 状态。点击时钟会打开设置,你可以在其中更改:
- 音量和屏幕亮度
- Gemini 的语音,共八种声音
- “Hi ESP”唤醒词的开启或关闭
- 伴随表情的音效
- 机器人是否自行播放和休眠
- 多少秒的静音会结束一次对话
Wi-Fi 一行会打开范围内网络列表,以及一个包含大写字母、数字和符号的屏幕键盘。ESP32-S3 在连接某个网络时无法扫描网络,因此固件会在扫描期间暂停连接尝试,并在扫描结束后恢复。如果密码错误,时钟屏幕会将其显示为原因。ESP32-S3 仅支持 2.4 GHz 网络。
机器人、PC 和浏览器共用一套代码
机器人的代码并不直接调用显示、触摸或音频驱动。表情、Brain 和各屏幕都通过 src/hw 和 src/audio 中的一层薄硬件层。模拟器替换了该层,因此其余代码无需改动即可为三个目标构建:
在模拟器中,Arduino.h、Wire.h 和 SPI.h 的小型替代版本会写入一个代表硬件的结构体。该结构体保存显示内存、触摸控制器寄存器和电池读数。使用 raylib 图形库编写的模拟器窗口会绘制该结构体,并允许你更改传感器数值。
浏览器版本是用 Emscripten 编译的同一个模拟器程序,Emscripten 将 C++ 转换为 WebAssembly。三项改动使之成为可能:
- 面向 Web 的 raylib。 raylib 使用 Emscripten 构建为一个通过 WebGL 绘制的静态库。
- 每次调用绘制一帧。 桌面版本运行在一个无限循环中。该循环变成了一个绘制单帧的函数。桌面版本在循环中调用它,浏览器则在每次屏幕刷新时调用一次。
- 固定的窗口尺寸。 在浏览器中,模拟器绘制到一个 1380×900 的画布上,页面会缩放画布以适应显示。可调整大小的画布会破坏鼠标与画布坐标之间的映射。
3D 外壳来自乐鑫。ESP-College 在 OSHWHub 上以 GPL-3.0 许可证发布了 ESP-VoCat(EchoEar)外壳的 STL 文件。一个简短的 Python 脚本将 STL 文件转换为一个 3D 模型文件,并降低其细节,以便浏览器快速加载。模拟器将实时屏幕绘制到外壳圆形窗口中的圆盘上。当你点击 3D 屏幕时,模拟器会在该圆盘上找到点击点,并将其转换为对应像素处的触摸。
自己动手构建
要构建 VoCat Eyes,你需要一块 ESP-VoCat v1.2 开发板,以及在撰写本文时版本为 3.3 的 ESP32 Arduino 核心。VoCat Eyes README 列出了所需的库和确切的命令。简而言之:
- 将
secrets.example.h复制为secrets.h,并添加你的 Gemini API 密钥。你也可以在那里输入 Wi-Fi 网络,或稍后在机器人上设置。 - 使用
esp_sr_16分区方案和 sketch 文件夹中的partitions.csv文件编译 sketch。此布局为应用程序提供 6 MB 闪存。 - 将固件与
srmodels.bin(“Hi ESP”唤醒词的模型文件)一起上传。 - 在机器人上,按住屏幕,点击时钟,选择 Wi-Fi,选择你的网络并输入密码。
- 说“Hi ESP”,或点击屏幕底部的小麦克风,然后开始说话。
如果你正在为 ESP32-S3 或其他开发板设计固件,Velxio 可以在你烧录任何硬件之前,在浏览器中对其进行模拟。