糖球系列③:ESP 圆屏不跑模型,它只是后台的语音客户端

掘金投稿用稿。系列上一篇:审核完成后我来改地址 标签建议:ESP32 WebSocket 本地大模型 语音助手 嵌入式 Ollama


先把结论放在标题里:这块 466×466 的 AMOLED 圆屏,开机之后一个 token 都不会算。

它算的是别的东西:什么时候该开口、什么时候该闭嘴、TLS 握手怎么在「刚拿到 DHCP」的那几百毫秒里活下来、掉线时麦克风要不要继续转。模型、检索、航班号、腹黑人格------全部在家里那台已经听 127.0.0.1:8000 的后台里。

系列①把公网接到这扇门。系列②把门后面的能力摊开。这篇写我真正花时间最多的那一层:

把一块会说话的屏,收成后台的一种客户端。

不是「ESP32 调用 ChatGPT」。是一套能出门、能打断、能听写、能重连的设备协议。固件瘦,协议胖,大脑在服务端------这才是能同时养活圆屏、方屏、以后再挂别的板的原因。


一句话职责表

圆屏固件必须做 圆屏固件禁止做
连网、鉴权、会话、录音、播放、UI、休眠 直连 Ollama / ASR / TTS
Opus 编解码、VAD、打断、重连 自己发明「要不要搜」
把偏好上报(含腹黑开关) 把人格、模型名烧进 if 里
把「现在听不清 / 现在没网」画给人看 假装自己在思考

方屏(4.3C)UI 完全另一套,协议同一套。换皮肤,不换大脑。寻呼机更瘦,那是系列④的戏。


一块屏,怎么「进门」

外出场景下,设备连的是公网域名背后的云机,再经 FRP 打到家里 backend。板上看到的是:

text 复制代码
STA 拿到 IPv4
    │
    ▼
WSS 握手(URI 用 IP,证书校验用域名)
    │  Header: X-API-Key
    ▼
hello { device_id, session_id?, fuhhei? }
    │
    ▼
session_ack { protocol: 2, resumed: true|false }
    │
    ▼
听写 listen start  /  按住说话  /  下行 Opus

这里有三个我愿意拿出来讲的细节。它们看起来像边角料,掉线、Guru Meditation、鉴权刷屏,都从这儿长出来。

1. URI 走 IP,证书走域名

Wi‑Fi 刚关联时 DNS 经常还是空的。如果你用域名去连 wss://your.example.com/api/device/ws,第一跳就会卡在解析。糖球的做法是:

  • 连接目标写成 wss://<公网IP>/api/device/ws
  • esp_websocket_clienthost / cert_common_name 仍填证书上的域名
  • CRT bundle 校验 SNI,不关 TLS

于是:路由按 IP 走,身份按域名验。 证书不会因为你「抄了个 IP 图省事」而变成 skip_cert_common_name=true 这种自杀选项。

配套还有一个 lwIP 钩子:当解析请求正好是那张证书域名时,同步填入已知 IP。这里踩过一次真崩------钩子里如果去调 found() 回调,会在 netconn 栈帧已经返回之后再解引用,表现为「拿到 IP 立刻 LoadProhibited 循环重启」。正确做法是只填 addr、返回 ERR_OK,让协议栈自己往下走。

这种 bug 不会出现在「用 Arduino 调一个 HTTPS GET」的教程里。它只出现在:你真的要让一块屏,在咖啡馆的 2.4G 上,对着家里的后台建立一个可重连的 WSS。

2. Key 写在握手里,不要写在 CONNECTED 之后

X-API-Key 放进 esp_websocket_client_initheaders。自动重连会沿用同一份 config。

有人会在 WEBSOCKET_EVENT_BEFORE_CONNECT 里再 set_headers。IDF 这个 API 要求已经 CONNECTED------在握手前调用必然 ESP_ERR_INVALID_ARG,日志刷屏,鉴权还是旧的。糖球选择:一次写对,重连别碰。

公网只信任进过糖球门的请求。Ollama 继续只听家里的回环。这和系列①、②是同一条纪律。

3. hello 要能 resume

断线重连不是新开一个人生。板上带着 session_id 再 hello,服务端能把 WebSocket 登记回同一会话:上下文、腹黑标志、进行中的 turn 语义还在。

做不到这一点,用户体验就是:说一半,Wi‑Fi 抖一下,助手失忆。嵌入式语音产品里,会话比连接长,这是后台当基石时才能做的事。


一轮话,究竟经过谁的手

按住说,或者听写模式开口,板上只产生一件事:Opus 帧。

text 复制代码
圆屏 ──WSS JSON──► listen start / audio_start
      ──WSS binary / HTTPS audio_turn──► Opus 帧
                    │
                    ▼
              糖球 backend
         ┌────┴────┬─────────┬──────────┐
         ▼         ▼         ▼          ▼
       STT      前置路由    作答 LLM     TTS
     (本机ASR)  need_search  need_large  CosyVoice
                need_flight  腹黑覆盖
                    │
                    ▼
圆屏 ◄── JSON:stt_partial / processing / 流式字
     ◄── binary:Opus 包(可先 burst 再按帧节拍)
     ◄── JPEG 分片:需要「一张图说完」的显示模式

上行:为什么不一条 WebSocket 打天下

控制面走 WSS 文本帧:hellolistenabort、偏好。音频有两条路:

  • 听写 / 短突发 :WS binary,服务端按 audio_start 门闩收包;没开门的 bin 直接丢,防止重连后旧包炸协议。
  • 一整轮上传POST /api/device/audio_turn(HTTPS JSON 里带 Opus)。注释里写得很直白------大包走 HTTP 比 WS 更稳;结果仍然从已经登记的那条 WebSocket 下行。

这是典型的「炫」完要解释的设计:不是协议洁癖,是现场。TLS 写队列一堵,IDF 客户端 transport_poll_write 超时就断。于是 network_timeout_ms 给到 20s,ping 拉到 45s,还关掉 pingpong 断线。该厚的地方要厚。

HTTP 那条还要求 session 上已经有活 WS,否则 409。上传和会话绑死,避免「文件传到一个没人听的房间」。

听写:板上 VAD + 云端 stt_partial

听写不是「录完再转」。板上用能量 VAD(最短说话、静音收尾、回声抑制窗口),服务端大约每 1.2s、至少十来帧,把已缓冲 Opus 批转写一次,下发 stt_partial

断句因此可以看两件事:本地能量,以及「文案已经稳定了多久」。字数不够不算说完;STT 稳定后再等一小段,才 listen stop 开一轮。

这是设备协议最不像 Demo 的地方:说话结束是产品定义,不是 silence < threshold

路由:固件一个 bit 都没有

转写出来的文本进前置模型,只准吐 JSON:

  • 要不要搜、搜什么(指代要还原)
  • 要不要按航班号查动态(有号才查,没有就不要拿网页搜冒充)
  • 这题要不要走后置更大档

腹黑开着,前后置被会话标志拧到同一套宽松小模型------开关在 NVS 里,经 hello/prefs 上报。烧两份固件来换人格,是业余的。

地理 IP 可以给天气/附近用,但系统提示写死:用户没问位置,禁止拿来寒暄。这种约束只能放服务端。板子不知道自己在哪个咖啡馆,也不该知道。

下行:先灌满,再按帧走,字和声音不是同一份稿

TTS 按句(或短首段)合成,Opus 包先 burst 几十包 填设备预取,再按略快于实时的节拍(约 12ms/包,帧长 20ms)往下灌。欠载比过冲更难听。

还有两件「只有真做过屏」才会管的事:

  1. 屏上可以有 emoji,嘴里不能念「sparkles」。 显示走 imgfont / JPEG;朗读前剥表情区码点。空语音包曾经就是表情包搞出来的。
  2. 图走 base64 分片 JSON,语音走 binary。 部分代理会吞 WS 二进制;图片宁可拆 text 帧。音频要对齐小智那种逐包 binary,才播得顺。

回合进行中服务端还打 processing keepalive。家里 GPU 一忙就是数秒,FRP 中间盒如果当连接死了,你会在用户最不想掉线的时候掉线。

打断(barge-in)是一等公民:板上停播、抑制旧轮次尾包、发 abort,服务端停 TTS。麦克风长期预热会把扬声器音量吃掉,还会引出隐私争议------所以 codec 惰性初始化 ,第一次播音才 codec_new


掉线时,屏应该像个产品,而不是像个栈回溯

WSS 断了,IDF 会按 reconnect_timeout_ms 自己连。固件要决定的是状态机,不是再 start() 一次造成竞态。

糖球现在的策略:

现场 做法
听写中途掉线 麦克风继续转,重连后 resume listen
上传 / 回合中掉线 保留 session,清空 TX 队列(新连接 in_audio=0,残留 bin 会触发 missing audio_start)
故意断开 不计入失败、不累计退避
连续下行失败 超过阈值中止 turn,避免半包音频仍 turn_done

UI 上只需要诚实:没网、在重连、在听、在想。不要转圈假装推理。推理在家里,圈转在板上,用户会被骗两次。

会话 resume 加上「听写保麦」,才能把咖啡馆里那次 DHCP 续租,从事故变成一次闪烁。


板端工程:炫的是约束,不是组件名

圆屏是 Waveshare ESP32-S3 AMOLED 1.75C:无 SD、32MB Flash、CO5300、AXP2101。从旧圆屏 LCD 迁过来时,不是改个分辨率就完。

几件我认的硬约束(不写引脚表):

DMA 不能按整屏一次性吐。 max_transfer_sz 按满屏申请,启动即 OOM。改成按行块(二十行一级),显示栈才活。

CJK 字库进 Flash,不进 PSRAM 整库加载。 启动要先有字,再抢 Wi‑Fi 堆。字库和 Wi‑Fi 并行,是另一类「能跑 Demo、不能量产」的死法。

LVGL worker 默认栈太小。 加大并放到 PSRAM;这不是调参游戏,是第一帧就栈溢出还是能进表盘的差别。

ES8311 要先被 PMU 喂电。 音频「有驱动没声音」,优先查 LDO,而不是查 I2S 配置贴。

这些加在一起,才配叫语音客户端:它要在 160MHz 级 MCU 上,同时扛 UI、麦、喇叭、TLS、Opus。它仍然不配叫大脑。

方屏分辨率、触摸、横竖完全不同,只要继续打同一套 /api/device。新板是新皮肤。新业务(寻呼)是新路由,不是新 Ollama。


和网页客户端比,设备端到底特殊在哪

网页可以把 SSE、DOM、浏览器 TTS 当玩具。圆屏不行。

  • 网页掉线,人会刷新。圆屏掉线,人会拍桌子。
  • 网页可以「先出字再出声」。圆屏必须双缓冲接段,否则像电话卡字。
  • 网页可以塞 RAG、生图扫描、观察者双模型。设备第一版故意 不上 Comfy、不把整条网页 intent 搬过来。能出门的,先保证:听清、答完、播完、能打断。

特殊的是物理和链路,不是另搞一套智能。智能继续住在系列②那层编排里。


仓库里怎么看这一层

text 复制代码
backend/device_ws.py           ← WSS + HTTP audio_turn + 会话登记
backend/device_voice_turn.py   ← Opus → STT → 路由 → 作答 → TTS/图
backend/device_route.py        ← need_search / flight / large / 腹黑
hardware/.../1.75C/firmware     ← 圆屏:协议状态机 + UI + 音频
hardware/.../4.3C/firmware      ← 方屏:同一协议,另一套皮

密钥、Wi‑Fi、云机 IP 在 gitignore 的本地头文件里。公开仓只留 example。克隆之后你填自己的门牌,连的是你自己的后台------这才叫客户端。


系列定位不变:入口、基石、客户端、垂直业务。

我自己同时写固件和后台。求职向一句话仍然是 本地 AI 应用工程 + 多端设备协议------屏是作品集的封面,不是架构的中心。

仓库:github.com/snf2046/tan...

相关推荐
小宋102112 分钟前
MCP 是什么:从零编写一个可供 AI 调用的工具服务
人工智能·github
桃西西呀15 分钟前
AI 为什么一本正经地胡说八道?3 个底层原因 + 2 个防坑法
人工智能·llm·ai编程
天空11016 分钟前
Claude Code 怎么换用 Claude Fable 5.1:配置步骤、端点验证方法,以及缓存降价 75% 到底省了多少(2026 年 9 月)
人工智能·ai编程
用户7686874404918 分钟前
我为什么不用 OpenAI 的 function calling,自己用正则解析工具调用
人工智能
正在走向自律18 分钟前
从 Python 基础到大模型落地:读《深入浅出 Python 人工智能》,吃透 AI 工程化 CRUD 实战
开发语言·人工智能·python·机器学习·知识脉络
长沙京卓19 分钟前
MagenticLite 设备需求:能不能本地跑,先分清 App 端和模型端
人工智能·ai
啊阿狸不会拉杆20 分钟前
《计算机网络-自顶向下方法》1.4 分组交换网中的时延、丢包和吞吐量 读书笔记
人工智能·计算机网络
宋哥转AI22 分钟前
深入理解 AI Agent:从黑盒到全链路——生产级 Agent 的可观测性体系怎么建
人工智能·agent·ai编程
Mr.朱鹏22 分钟前
科技周报(2026-09-01期):版权战与机器人潮
人工智能·科技·机器人