欢迎光临
我们一直在努力

ESP32-S3 实战教程:本地语音识别控制 Web 塔防游戏,从固件到前端完整跑通

前言

这篇文章记录一个完整的 ESP32-S3 实战项目:用 ESP32-S3 在本地识别语音命令,然后通过 WebSocket 把命令发送给电脑端的 FastAPI 服务,最后在 Web 页面上实时显示塔防游戏状态。

本文不是单纯介绍某一个库,而是把一个可以运行的闭环做出来:

  • ESP32-S3 采集麦克风音频。

  • ESP-SR / MultiNet 在 ESP32-S3 本地识别命令词。

  • ESP32-S3 通过 Wi-Fi 连接 FastAPI。

  • 设备端用 WebSocket 上报“开始游戏、放塔、释放技能”等事件。

  • FastAPI 维护游戏状态。

  • Web 前端通过 WebSocket 实时显示战局变化。

  • 所以这篇更偏“教程”,目标是让别人照着步骤也能搭出同样的系统。

    最终效果:

    玩家说话

    ESP32-S3 本地语音识别

    WebSocket 上报命令

    FastAPI 更新游戏状态

    Web 前端实时显示塔防效果

    一、项目效果

    目前已经实现的功能:

    • ESP32-S3 可以本地识别中文语音命令

    • 支持“开始游戏”“暂停游戏”“放塔”“释放技能”“重置游戏”

    • 识别到“放塔”后,Web 页面上会增加一个防御塔

    • 识别到“释放技能”后,Web 页面触发技能特效

    • FastAPI 统一维护游戏状态,包括血量、金币、波次、敌人和塔

    • Web 前端通过 WebSocket 实时接收状态变化

    • ESP32-S3 会周期性发送心跳,前端可以显示设备在线状态

    这个项目适合作为 ESP32-S3 综合实战练习,涉及嵌入式、语音识别、网络通信、后端服务和前端可视化。本文只讲已经完成并跑通的部分。

    二、系统架构

    整体架构如下:

    +——————-+
    |     玩家语音     |
    +———+———+
            |
            v
    +——————-+
    | ESP32-S3 + ESP-SR |
    | 本地语音识别       |
    +———+———+
            |
            | WebSocket JSON
            v
    +——————-+
    | FastAPI 游戏服务   |
    | 权威状态机         |
    +———+———+
            |
            | WebSocket game_state
            v
    +——————-+
    | Web 塔防前端       |
    | 实时战局显示       |
    +——————-+

    各模块分工:

    模块作用
    ESP32-S3 本地语音识别、发送控制事件、设备心跳
    FastAPI 游戏状态维护、命令处理、广播状态
    Web 前端 显示游戏画面、显示设备在线状态、手动控制
    WebSocket 设备、服务端、前端之间的实时通信

    三、技术选型说明

    这个项目没有使用很复杂的框架,主要是为了方便调试和讲清楚完整流程。

    1. 为什么用 ESP32-S3

    ESP32-S3 相比普通 ESP32 更适合做 AIoT 小项目:

    • 有 Wi-Fi,适合连接后端服务

    • 性能够用,可以跑本地语音识别

    • 支持 ESP-IDF,方便做 FreeRTOS 多任务

    • 可以接麦克风、屏幕、蜂鸣器等外设

    2. 为什么用 ESP-SR

    ESP-SR 是乐鑫官方语音识别方案,适合在 ESP32-S3 上做离线命令词识别。

    这里使用的是 MultiNet 命令词识别,不需要把音频传到云端,也不依赖电脑端语音识别。这样做的好处是:

    • 延迟低

    • 不需要联网语音 API

    • 没有语音上传隐私问题

    • 适合“开始游戏、放塔、释放技能”这种固定命令

    3. 为什么用 FastAPI

    FastAPI 适合快速搭建一个 WebSocket 后端服务。这个项目里,FastAPI 的定位是“权威游戏服务器”。

    也就是说,游戏的血量、金币、波次、塔的位置都由后端统一维护,ESP32-S3 和 Web 前端都只是发送事件或者显示状态。

    这样设计有一个好处:前端刷新、设备重连、网络短暂断开后,只要重新拿到后端状态,就能恢复到正确画面。

    4. 为什么用 WebSocket

    这个项目需要实时通信:

    • ESP32-S3 要实时上报语音识别结果

    • 前端要实时看到游戏状态变化

    • 后端要主动广播 game_state

    普通 HTTP 请求不太适合这种双向实时场景,所以这里使用 WebSocket。

    四、硬件和软件环境

    硬件:

    • ESP32-S3 开发板

    • 板载或外接麦克风

    • 可选:蜂鸣器 / 扬声器

    • 可选:LCD 屏幕

    • 一台电脑,运行 FastAPI 和 Web 前端

    软件:

    • ESP-IDF

    • ESP-SR

    • Python 3

    • FastAPI

    • Uvicorn

    • HTML / CSS / JavaScript

    • WebSocket

    项目目录大致如下:

    ESP32S3
    ├── main
    │   ├── main.c
    │   ├── voice_local.c
    │   ├── net_client.c
    │   ├── audio.c
    │   ├── game.c
    │   └── Kconfig.projbuild
    ├── server
    │   └── fastapi
    │       ├── app.py
    │       └── requirements.txt
    ├── web
    │   ├── index.html
    │   ├── styles.css
    │   └── app.js
    └── tools
      └── scripts
          └── fake_esp32s3_device.py

    五、教程实现路线

    建议按下面顺序完成,不要一开始就把所有模块同时打开,否则不好排查问题。

    第一步:先跑通 FastAPI

    目标:确认后端服务可以启动,并且浏览器能访问。

    成功标志:

    http://127.0.0.1:8000/
    http://127.0.0.1:8000/state

    这两个地址能正常打开。

    第二步:跑通 Web 前端

    目标:确认 Web 页面可以打开,并且可以连接 FastAPI 的 WebSocket。

    成功标志:

    • 页面不是空白

    • 控制台没有明显报错

    • 页面显示 WebSocket 在线

    第三步:用假设备测试通信

    如果还没有 ESP32-S3,或者固件还没有烧录,可以先用 Python 脚本模拟设备。

    这样可以先验证:

    • 后端能接收设备消息

    • 前端能收到状态广播

    • 放塔和技能效果能显示

    第四步:烧录 ESP32-S3 网络客户端

    目标:让 ESP32-S3 连上 Wi-Fi,并连接 FastAPI 的 /ws/device。

    成功标志:

    NET: websocket connected
    NET: heartbeat sent

    第五步:加入本地语音识别

    目标:ESP32-S3 本地识别语音命令,并把识别结果转换成 WebSocket 消息。

    成功标志:

    [VOICE] cmd_id=2 phrase_id=2 text=fangta prob=0.681
    [VOICE] action=tower prob=0.681

    同时 Web 页面出现对应变化。

    六、ESP32-S3 本地语音识别

    ESP32-S3 端使用的是 Espressif 的 ESP-SR 语音识别方案,核心是 MultiNet 命令词识别。

    我这里配置了几个中文命令:

    kai shi you xi     -> 开始游戏
    zan ting you xi     -> 暂停游戏
    fang ta             -> 放塔
    shi fang ji neng   -> 释放技能
    chong zhi you xi   -> 重置游戏

    在代码里,识别结果会映射成游戏动作:

    if (command_id == 0) {
       net_client_send_control("start");
    } else if (command_id == 1) {
       net_client_send_control("pause");
    } else if (command_id == 2) {
       net_client_send_piece_update(cell, "tower", prob);
    } else if (command_id == 3) {
       net_client_send_gesture("open_palm", prob);
    } else if (command_id == 4) {
       net_client_send_control("reset");
    }

    实际调试时要注意一个坑:MultiNet 返回的文本不一定和配置字符串完全一致。

    例如命令词配置的是:

    fang ta

    但串口里可能输出:

    text=fangta

    所以匹配时最好同时兼容 fang ta 和 fangta。

    七、语音识别稳定性优化

    刚开始语音识别不是很稳定,主要遇到了几个问题。

    1. 麦克风增益过高

    串口里经常看到:

    [MIC] level peak=32768

    这说明音频已经削顶了,语音波形严重失真,模型反而识别不准。

    解决方法是把麦克风采集增益做成可配置项,例如默认设置为 2:

    Microphone capture gain = 2

    如果 peak 经常接近 32767/32768,继续降低到 1。

    如果声音很小,比如 peak 长期低于 500,可以适当提高到 3 或 4。

    2. 置信度阈值

    识别结果里会有概率值:

    [VOICE] cmd_id=2 text=fangta prob=0.681

    可以增加一个应用层最低置信度:

    Voice command minimum probability = 0.05

    低于这个值的结果直接忽略:

    [VOICE] ignore low prob cmd_id=0 text=kaishiyouxi prob=0.032 threshold=0.05

    调试阶段可以先设成 0.00 或 0.05,先保证命令能触发。等确认没有明显误触发后,再逐步提高到 0.10 左右。不要一开始就把阈值设太高,否则很多低概率但正确的命令会被过滤掉。

    3. 命令重复触发

    语音模型可能会在一句话里连续返回多次相同命令,比如“开始游戏”连续触发几次。

    解决方法是加冷却时间:

    Voice command cooldown in ms = 1200

    也就是 1.2 秒内同一个命令只执行一次。这个值不建议太大,否则连续下达两次同类命令时会感觉“不灵敏”。

    4. 日志刷屏

    如果一直打印麦克风电平:

    [MIC] level peak=…

    会导致真正的 [VOICE] 日志看不清。

    所以我加了日志间隔:

    Microphone level log interval = 100

    表示每 100 个音频块打印一次麦克风电平。

    八、ESP32-S3 WebSocket 客户端

    ESP32-S3 识别到语音命令后,通过 WebSocket 发送 JSON 到 FastAPI。

    设备上报心跳:

    {
     "type": "heartbeat",
     "seq": 38,
     "ts": 39344,
     "device_id": "s3-node-01",
     "free_heap": 123456
    }

    设备上报放塔:

    {
     "type": "piece_update",
     "seq": 51,
     "ts": 50549,
     "device_id": "s3-node-01",
     "cell": 5,
     "piece_type": "tower",
     "confidence": 0.68
    }

    设备上报技能:

    {
     "type": "gesture",
     "seq": 39,
     "ts": 39344,
     "device_id": "s3-node-01",
     "gesture": "open_palm",
     "score": 0.73
    }

    这里虽然字段叫 gesture,但也可以把语音触发的技能映射成同一个事件,这样前端和后端不需要关心技能来自语音还是手势。

    这一部分实现时可以分成三个函数:

    net_client_send_control("start");
    net_client_send_piece_update(cell, "tower", prob);
    net_client_send_gesture("open_palm", prob);

    对应关系如下:

    • control:用于开始、暂停、重置这种游戏控制命令

    • piece_update:用于放塔,告诉后端第几个格子出现了塔

    • gesture:用于释放技能,这里把语音命令映射成技能事件

    调试 WebSocket 时建议先只发心跳。心跳能稳定发送后,再加 piece_update 和 gesture。这样如果连接失败,可以先排除语音识别和游戏逻辑,只检查网络。

    串口里看到类似下面的日志,说明设备端已经成功把 JSON 发出去了:

    NET: ws send: {"type":"piece_update","seq":51,"device_id":"s3-node-01","cell":5,"piece_type":"tower","confidence":0.68}

    九、FastAPI 后端

    FastAPI 负责维护游戏的权威状态。

    它主要做三件事:

  • 接收 ESP32-S3 设备消息

  • 根据消息更新游戏状态

  • 把最新状态广播给 Web 前端

  • 后端的核心思路是:不要让 ESP32-S3 和前端各自维护一套游戏逻辑。ESP32-S3 只负责“我识别到了什么”,Web 前端只负责“我显示什么”,真正的游戏状态统一放在 FastAPI。

    例如:

    • ESP32-S3 说“放塔”

    • 设备端发送 piece_update

    • FastAPI 判断金币是否足够、塔是否能放

    • FastAPI 更新 towers、gold、version

    • FastAPI 广播新的 game_state

    • Web 前端收到后重新渲染棋盘

    这样即使前端刷新页面,也不会丢失当前战局,因为状态在后端。

    启动方式:

    cd E:\\ESP32S3\\server\\fastapi
    python -m venv .venv
    .\\.venv\\Scripts\\Activate.ps1
    pip install -r requirements.txt
    uvicorn app:app –host 0.0.0.0 –port 8000 –reload

    WebSocket 路由:

    前端连接:ws://电脑IP:8000/ws/frontend
    设备连接:ws://电脑IP:8000/ws/device

    注意:如果是真实 ESP32-S3 连接,不要填 127.0.0.1。

    因为 127.0.0.1 对 ESP32-S3 来说是它自己,不是电脑。

    应该填电脑在局域网里的 IP,例如:

    ws://192.168.1.23:8000/ws/device

    建议先在浏览器里访问:

    http://127.0.0.1:8000/state

    如果能看到当前游戏状态,说明 FastAPI 服务本身是正常的。然后再去排查 WebSocket 或 ESP32-S3 连接。

    十、Web 前端

    Web 前端用于展示塔防游戏状态,包括:

    • 当前波次

    • 基地血量

    • 金币

    • 敌人数量

    • 已放置的塔

    • 技能特效

    • 设备在线状态

    • 事件日志

    启动前端:

    cd E:\\ESP32S3
    python -m http.server 5173 -d web

    浏览器访问:

    http://127.0.0.1:5173

    如果用手机访问,把 127.0.0.1 换成电脑局域网 IP:

    http://192.168.1.23:5173

    前端默认连接:

    ws://当前页面主机:8000/ws/frontend

    服务端广播的游戏状态大致如下:

    {
     "type": "game_state",
     "version": 42,
     "wave": 3,
     "base_hp": 16,
     "gold": 85,
     "enemies_alive": 4,
     "towers": [
      { "cell": 0, "type": "tower" },
      { "cell": 5, "type": "tower" }
    ],
     "paused": false,
     "game_over": false
    }

    前端拿到这个状态后,只负责渲染,不自己判断游戏胜负。

    这样可以保证游戏逻辑统一在后端,前端只是显示层。

    前端实现时建议分成几个小函数:

    • connectWs():连接 FastAPI 的 WebSocket

    • handleMessage():处理后端发来的消息

    • renderBoard():根据 game_state 渲染棋盘

    • triggerSkillEffect():播放技能动画

    • logEvent():把关键事件写到页面日志里

    这样写的好处是调试很方便。比如 WebSocket 已经连接,但棋盘没有变化,就重点看 handleMessage() 和 renderBoard();如果技能命令到了但没有动画,就只看 triggerSkillEffect()。

    前端收到状态后,不建议用 innerHTML 直接拼接外部消息。更安全的做法是使用 createElement() 和 textContent 创建节点,避免 WebSocket 消息里带入恶意 HTML。

    十一、ESP32-S3 配置

    在 ESP-IDF 里进入配置:

    cd E:\\ESP32S3
    idf.py menuconfig

    找到:

    Tower Defense Network

    配置 Wi-Fi 和 WebSocket:

    Wi-Fi SSID
    Wi-Fi password
    Device WebSocket URL
    Device ID

    例如:

    Wi-Fi SSID             = iPhone
    Wi-Fi password         = 12345678
    Device WebSocket URL   = ws://172.20.10.3:8000/ws/device
    Device ID               = s3-node-01

    语音识别调试参数建议:

    Voice command minimum probability = 0.05
    MultiNet detection threshold     = 0.01
    Microphone capture gain           = 2
    Microphone level log interval     = 100
    Voice command cooldown in ms     = 1200

    如果想先尽量提高识别率,可以临时把最低概率设成:

    Voice command minimum probability = 0.00

    等确认能识别后,再改回 0.05。如果误触发很多,再慢慢提高到 0.10。

    十二、完整联调流程

    1. 启动 FastAPI

    cd E:\\ESP32S3\\server\\fastapi
    .\\.venv\\Scripts\\Activate.ps1
    uvicorn app:app –host 0.0.0.0 –port 8000 –reload

    2. 启动 Web 前端

    cd E:\\ESP32S3
    python -m http.server 5173 -d web

    浏览器访问:

    http://127.0.0.1:5173

    3. 没有硬件时先用假设备验证

    这一步不是必须的,但很适合排查问题。假设备会模拟 ESP32-S3 连接 /ws/device,并定时发送心跳和测试事件。

    cd E:\\ESP32S3
    python tools\\scripts\\fake_esp32s3_device.py

    如果假设备可以让 Web 页面变化,说明 FastAPI 和前端是好的。后面真实 ESP32-S3 出问题时,就可以重点检查 Wi-Fi、WebSocket URL、固件配置和语音识别。

    4. 配置并烧录 ESP32-S3

    cd E:\\ESP32S3
    idf.py menuconfig
    idf.py build
    idf.py flash monitor

    5. 测试语音命令

    可以依次说:

    开始游戏
    放塔
    释放技能
    暂停游戏
    重置游戏

    串口中应该能看到:

    [VOICE] cmd_id=2 phrase_id=2 text=fangta prob=0.681
    [VOICE] action=tower prob=0.681

    同时 FastAPI 终端应该收到设备消息,Web 页面也应该出现对应变化。

    十三、常见问题

    1. Web 前端一直显示离线

    检查:

    • FastAPI 是否启动

    • 前端连接的 WebSocket 地址是否正确

    • 电脑防火墙是否拦截 8000 端口

    • ESP32-S3 和电脑是否在同一个局域网

    • 手机热点是否开启“最大兼容性”

    2. ESP32-S3 连不上 WebSocket

    重点检查 Device WebSocket URL。

    错误写法:

    ws://127.0.0.1:8000/ws/device

    正确写法:

    ws://电脑局域网IP:8000/ws/device

    例如:

    ws://172.20.10.3:8000/ws/device

    3. 串口日志乱码或者卡住

    不要直接打印 WebSocket 原始数据,尤其是可能收到 ping/pong 或二进制帧。

    建议只打印长度和 opcode:

    ESP_LOGI(TAG, "ws recv len=%d op=%d", data->data_len, data->op_code);

    4. 语音识别不到

    检查:

    • 是否加载了 MultiNet 模型

    • 是否打印了 [VOICE] detect start

    • 麦克风是否有 [MIC] level

    • 命令词是否配置正确

    • 概率阈值是否太高

    可以先把:

    Voice command minimum probability = 0.05

    调低做测试。

    5. 识别不稳定

    如果日志里经常出现:

    [MIC] level peak=32768

    说明音频削顶了,要降低麦克风增益。

    如果命令重复触发,可以调大:

    Voice command cooldown in ms

    比如从 1200 调到 1800。如果感觉识别后反应太慢,可以反过来调到 800~1000。

    6. 短命令识别差

    “放塔”只有两个字,对语音识别模型来说比较短,稳定性会差一些。

    可以改成长一点的命令,例如:

    建造防御塔
    释放全屏技能
    重新开始游戏

    通常更长、更明确的命令词识别效果会更好。

    十四、总结

    这个项目虽然是一个塔防小游戏,但它把很多常见的嵌入式联网项目能力串起来了:

    • ESP32-S3 本地 AI 语音识别

    • ESP-IDF FreeRTOS 任务开发

    • WebSocket 实时通信

    • FastAPI 后端状态机

    • Web 前端实时渲染

    • 设备心跳、断线重连、日志调试

    我觉得这个项目最有价值的地方不是游戏本身,而是完整跑通了一个“端侧智能设备 + 后端服务 + Web 可视化”的闭环。

    到这里,一个基于 ESP32-S3 的本地语音控制 Web 塔防系统就完整跑通了。读者照着本文完成后,应该可以实现:说出语音命令,ESP32-S3 本地识别,FastAPI 接收事件,Web 页面实时显示游戏变化。

    赞(0)
    未经允许不得转载:171主机测评 » ESP32-S3 实战教程:本地语音识别控制 Web 塔防游戏,从固件到前端完整跑通
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址