mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-01-14 01:07:30 +08:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b48506171b | ||
|
|
7240ea99f1 | ||
|
|
1e8fefbede | ||
|
|
906d819454 | ||
|
|
be88719932 | ||
|
|
213117ded2 | ||
|
|
76ff1cf0dc | ||
|
|
5d44633687 | ||
|
|
5113a5f4bb | ||
|
|
f501a5f440 | ||
|
|
e9649cfc58 | ||
|
|
ee5587019b | ||
|
|
ebdd58748a | ||
|
|
99c32d9331 | ||
|
|
cccaf71c3e | ||
|
|
564018c762 | ||
|
|
0ccdc082b5 | ||
|
|
4b582f8074 | ||
|
|
1f0d2e993b | ||
|
|
b7db68457c | ||
|
|
11c79a7003 | ||
|
|
f9de29519b | ||
|
|
d7c1aef77a | ||
|
|
33c2fe90a8 | ||
|
|
e8f68a331f | ||
|
|
2d15bef298 | ||
|
|
28db4bd60a | ||
|
|
01a12b325f | ||
|
|
c87b1eabf4 | ||
|
|
908c9d5708 | ||
|
|
860d12a12c | ||
|
|
511349a7bd | ||
|
|
764f6e3349 | ||
|
|
59d08c7612 | ||
|
|
ebbb2fa319 | ||
|
|
d984cda486 | ||
|
|
52f3134f30 | ||
|
|
7f332f120c | ||
|
|
ba10b2a2d2 | ||
|
|
92de37e182 | ||
|
|
30970abd1f | ||
|
|
06da25fd26 | ||
|
|
cdb025dd90 | ||
|
|
ce72f196b7 | ||
|
|
4aa47cc591 | ||
|
|
1664fda6e4 | ||
|
|
ccee790c0d | ||
|
|
c6815c7ad5 | ||
|
|
0e51c4c94c | ||
|
|
221d4e7f69 | ||
|
|
bcddbbde26 | ||
|
|
e39a46c1a0 | ||
|
|
1f602fa3a0 | ||
|
|
a9413e2d45 | ||
|
|
6fbc60fa9f |
@ -4,7 +4,7 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(PROJECT_VER "2.0.4")
|
||||
set(PROJECT_VER "2.1.0")
|
||||
|
||||
# Add this line to disable the specific warning
|
||||
add_compile_options(-Wno-missing-field-initializers)
|
||||
|
||||
164
README.md
164
README.md
@ -1,73 +1,73 @@
|
||||
# An MCP-based Chatbot
|
||||
|
||||
(中文 | [English](README_en.md) | [日本語](README_ja.md))
|
||||
(English | [中文](README_zh.md) | [日本語](README_ja.md))
|
||||
|
||||
## 介绍
|
||||
## Introduction
|
||||
|
||||
👉 [人类:给 AI 装摄像头 vs AI:当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
👉 [Human: Give AI a camera vs AI: Instantly finds out the owner hasn't washed hair for three days【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
|
||||
👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
👉 [Handcraft your AI girlfriend, beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
|
||||
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
|
||||
As a voice interaction entry, the XiaoZhi AI chatbot leverages the AI capabilities of large models like Qwen / DeepSeek, and achieves multi-terminal control via the MCP protocol.
|
||||
|
||||
<img src="docs/mcp-based-graph.jpg" alt="通过MCP控制万物" width="320">
|
||||
<img src="docs/mcp-based-graph.jpg" alt="Control everything via MCP" width="320">
|
||||
|
||||
### 版本说明
|
||||
## Version Notes
|
||||
|
||||
当前 v2 版本与 v1 版本分区表不兼容,所以无法从 v1 版本通过 OTA 升级到 v2 版本。分区表说明参见 [partitions/v2/README.md](partitions/v2/README.md)。
|
||||
The current v2 version is incompatible with the v1 partition table, so it is not possible to upgrade from v1 to v2 via OTA. For partition table details, see [partitions/v2/README.md](partitions/v2/README.md).
|
||||
|
||||
使用 v1 版本的所有硬件,可以通过手动烧录固件来升级到 v2 版本。
|
||||
All hardware running v1 can be upgraded to v2 by manually flashing the firmware.
|
||||
|
||||
v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版本,该分支会持续维护到 2026 年 2 月。
|
||||
The stable version of v1 is 1.9.2. You can switch to v1 by running `git checkout v1`. The v1 branch will be maintained until February 2026.
|
||||
|
||||
### 已实现功能
|
||||
### Features Implemented
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP)
|
||||
- 采用 OPUS 音频编解码
|
||||
- 基于流式 ASR + LLM + TTS 架构的语音交互
|
||||
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD 显示屏,支持表情显示
|
||||
- 电量显示与电源管理
|
||||
- 支持多语言(中文、英文、日文)
|
||||
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
|
||||
- 通过设备端 MCP 实现设备控制(音量、灯光、电机、GPIO 等)
|
||||
- 通过云端 MCP 扩展大模型能力(智能家居控制、PC桌面操作、知识搜索、邮件收发等)
|
||||
- 自定义唤醒词、字体、表情与聊天背景,支持网页端在线修改 ([自定义Assets生成器](https://github.com/78/xiaozhi-assets-generator))
|
||||
- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP)
|
||||
- Uses OPUS audio codec
|
||||
- Voice interaction based on streaming ASR + LLM + TTS architecture
|
||||
- Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD display, supports emoji display
|
||||
- Battery display and power management
|
||||
- Multi-language support (Chinese, English, Japanese)
|
||||
- Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms
|
||||
- Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
|
||||
- Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
|
||||
- Customizable wake words, fonts, emojis, and chat backgrounds with online web-based editing ([Custom Assets Generator](https://github.com/78/xiaozhi-assets-generator))
|
||||
|
||||
## 硬件
|
||||
## Hardware
|
||||
|
||||
### 面包板手工制作实践
|
||||
### Breadboard DIY Practice
|
||||
|
||||
详见飞书文档教程:
|
||||
See the Feishu document tutorial:
|
||||
|
||||
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
面包板效果图如下:
|
||||
Breadboard demo:
|
||||
|
||||

|
||||

|
||||
|
||||
### 支持 70 多个开源硬件(仅展示部分)
|
||||
### Supports 70+ Open Source Hardware (Partial List)
|
||||
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
|
||||
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
|
||||
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·AI 吊坠</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-1.54">无名科技 Nologo-星智-1.54TFT</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI Pendant</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a>
|
||||
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI 超低成本机器狗">ESP-HI 超低成本机器狗</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI Low Cost Robot Dog">ESP-HI Low Cost Robot Dog</a>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
|
||||
<img src="docs/v1/lichuang-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3">
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
|
||||
<img src="docs/v1/espbox3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
|
||||
@ -76,86 +76,90 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
|
||||
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
|
||||
<img src="docs/v1/atoms3r.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4">
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4">
|
||||
<img src="docs/v1/magiclick.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
|
||||
<img src="docs/v1/waveshare.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3">
|
||||
<img src="docs/v1/xmini-c3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
|
||||
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54">
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54">
|
||||
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
|
||||
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低成本机器狗">
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI Low Cost Robot Dog">
|
||||
<img src="docs/v1/esp-hi.jpg" width="240" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 软件
|
||||
## Software
|
||||
|
||||
### 固件烧录
|
||||
### Firmware Flashing
|
||||
|
||||
新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。
|
||||
For beginners, it is recommended to use the firmware that can be flashed without setting up a development environment.
|
||||
|
||||
固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。
|
||||
The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Personal users can register an account to use the Qwen real-time model for free.
|
||||
|
||||
👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
👉 [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
|
||||
### 开发环境
|
||||
### Development Environment
|
||||
|
||||
- Cursor 或 VSCode
|
||||
- 安装 ESP-IDF 插件,选择 SDK 版本 5.4 或以上
|
||||
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
|
||||
- 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范
|
||||
- Cursor or VSCode
|
||||
- Install ESP-IDF plugin, select SDK version 5.4 or above
|
||||
- Linux is better than Windows for faster compilation and fewer driver issues
|
||||
- This project uses Google C++ code style, please ensure compliance when submitting code
|
||||
|
||||
### 开发者文档
|
||||
### Developer Documentation
|
||||
|
||||
- [自定义开发板指南](docs/custom-board.md) - 学习如何为小智 AI 创建自定义开发板
|
||||
- [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备
|
||||
- [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式
|
||||
- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md)
|
||||
- [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
|
||||
- [Custom Board Guide](docs/custom-board.md) - Learn how to create custom boards for XiaoZhi AI
|
||||
- [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol
|
||||
- [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation
|
||||
- [MQTT + UDP Hybrid Communication Protocol Document](docs/mqtt-udp.md)
|
||||
- [A detailed WebSocket communication protocol document](docs/websocket.md)
|
||||
|
||||
## 大模型配置
|
||||
## Large Model Configuration
|
||||
|
||||
如果你已经拥有一个小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
|
||||
If you already have a XiaoZhi AI chatbot device and have connected to the official server, you can log in to the [xiaozhi.me](https://xiaozhi.me) console for configuration.
|
||||
|
||||
👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
👉 [Backend Operation Video Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
|
||||
## 相关开源项目
|
||||
## Related Open Source Projects
|
||||
|
||||
在个人电脑上部署服务器,可以参考以下第三方开源的项目:
|
||||
For server deployment on personal computers, refer to the following open-source projects:
|
||||
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python server
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java server
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang server
|
||||
|
||||
使用小智通信协议的第三方客户端项目:
|
||||
Other client projects using the XiaoZhi communication protocol:
|
||||
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客户端
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python client
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android client
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) Linux client by 100ask
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) Bluetooth chip firmware by Sichuan
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) QuecPython firmware by Quectel
|
||||
|
||||
## 关于项目
|
||||
Custom Assets Tools:
|
||||
|
||||
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,修改或用于商业用途。
|
||||
- [78/xiaozhi-assets-generator](https://github.com/78/xiaozhi-assets-generator) Custom Assets Generator (Wake words, fonts, emojis, backgrounds)
|
||||
|
||||
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
|
||||
## About the Project
|
||||
|
||||
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:1011329060
|
||||
This is an open-source ESP32 project, released under the MIT license, allowing anyone to use it for free, including for commercial purposes.
|
||||
|
||||
We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices.
|
||||
|
||||
If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 1011329060
|
||||
|
||||
## Star History
|
||||
|
||||
@ -165,4 +169,4 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
172
README_en.md
172
README_en.md
@ -1,172 +0,0 @@
|
||||
# An MCP-based Chatbot
|
||||
|
||||
(English | [中文](README.md) | [日本語](README_ja.md))
|
||||
|
||||
## Introduction
|
||||
|
||||
👉 [Human: Give AI a camera vs AI: Instantly finds out the owner hasn't washed hair for three days【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
|
||||
👉 [Handcraft your AI girlfriend, beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
|
||||
As a voice interaction entry, the XiaoZhi AI chatbot leverages the AI capabilities of large models like Qwen / DeepSeek, and achieves multi-terminal control via the MCP protocol.
|
||||
|
||||
<img src="docs/mcp-based-graph.jpg" alt="Control everything via MCP" width="320">
|
||||
|
||||
## Version Notes
|
||||
|
||||
The current v2 version is incompatible with the v1 partition table, so it is not possible to upgrade from v1 to v2 via OTA. For partition table details, see [partitions/v2/README.md](partitions/v2/README.md).
|
||||
|
||||
All hardware running v1 can be upgraded to v2 by manually flashing the firmware.
|
||||
|
||||
The stable version of v1 is 1.9.2. You can switch to v1 by running `git checkout v1`. The v1 branch will be maintained until February 2026.
|
||||
|
||||
### Features Implemented
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP)
|
||||
- Uses OPUS audio codec
|
||||
- Voice interaction based on streaming ASR + LLM + TTS architecture
|
||||
- Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD display, supports emoji display
|
||||
- Battery display and power management
|
||||
- Multi-language support (Chinese, English, Japanese)
|
||||
- Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms
|
||||
- Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
|
||||
- Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
|
||||
- Customizable wake words, fonts, emojis, and chat backgrounds with online web-based editing ([Custom Assets Generator](https://github.com/78/xiaozhi-assets-generator))
|
||||
|
||||
## Hardware
|
||||
|
||||
### Breadboard DIY Practice
|
||||
|
||||
See the Feishu document tutorial:
|
||||
|
||||
👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
Breadboard demo:
|
||||
|
||||

|
||||
|
||||
### Supports 70+ Open Source Hardware (Partial List)
|
||||
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
|
||||
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
|
||||
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI Pendant</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a>
|
||||
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI Low Cost Robot Dog">ESP-HI Low Cost Robot Dog</a>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
|
||||
<img src="docs/v1/lichuang-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
|
||||
<img src="docs/v1/espbox3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
|
||||
<img src="docs/v1/m5cores3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
|
||||
<img src="docs/v1/atoms3r.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4">
|
||||
<img src="docs/v1/magiclick.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
|
||||
<img src="docs/v1/waveshare.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3">
|
||||
<img src="docs/v1/xmini-c3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
|
||||
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54">
|
||||
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
|
||||
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI Low Cost Robot Dog">
|
||||
<img src="docs/v1/esp-hi.jpg" width="240" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Software
|
||||
|
||||
### Firmware Flashing
|
||||
|
||||
For beginners, it is recommended to use the firmware that can be flashed without setting up a development environment.
|
||||
|
||||
The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Personal users can register an account to use the Qwen real-time model for free.
|
||||
|
||||
👉 [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
|
||||
### Development Environment
|
||||
|
||||
- Cursor or VSCode
|
||||
- Install ESP-IDF plugin, select SDK version 5.4 or above
|
||||
- Linux is better than Windows for faster compilation and fewer driver issues
|
||||
- This project uses Google C++ code style, please ensure compliance when submitting code
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
- [Custom Board Guide](docs/custom-board.md) - Learn how to create custom boards for XiaoZhi AI
|
||||
- [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol
|
||||
- [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation
|
||||
- [MQTT + UDP Hybrid Communication Protocol Document](docs/mqtt-udp.md)
|
||||
- [A detailed WebSocket communication protocol document](docs/websocket.md)
|
||||
|
||||
## Large Model Configuration
|
||||
|
||||
If you already have a XiaoZhi AI chatbot device and have connected to the official server, you can log in to the [xiaozhi.me](https://xiaozhi.me) console for configuration.
|
||||
|
||||
👉 [Backend Operation Video Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
|
||||
## Related Open Source Projects
|
||||
|
||||
For server deployment on personal computers, refer to the following open-source projects:
|
||||
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python server
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java server
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang server
|
||||
|
||||
Other client projects using the XiaoZhi communication protocol:
|
||||
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python client
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android client
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) Linux client by 100ask
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) Bluetooth chip firmware by Sichuan
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) QuecPython firmware by Quectel
|
||||
|
||||
Custom Assets Tools:
|
||||
|
||||
- [78/xiaozhi-assets-generator](https://github.com/78/xiaozhi-assets-generator) Custom Assets Generator (Wake words, fonts, emojis, backgrounds)
|
||||
|
||||
## About the Project
|
||||
|
||||
This is an open-source ESP32 project, released under the MIT license, allowing anyone to use it for free, including for commercial purposes.
|
||||
|
||||
We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices.
|
||||
|
||||
If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 1011329060
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
@ -1,6 +1,6 @@
|
||||
# MCP ベースのチャットボット
|
||||
|
||||
(日本語 | [中文](README.md) | [English](README_en.md))
|
||||
(日本語 | [中文](README_zh.md) | [English](README.md))
|
||||
|
||||
## はじめに
|
||||
|
||||
|
||||
168
README_zh.md
Normal file
168
README_zh.md
Normal file
@ -0,0 +1,168 @@
|
||||
# An MCP-based Chatbot
|
||||
|
||||
(中文 | [English](README.md) | [日本語](README_ja.md))
|
||||
|
||||
## 介绍
|
||||
|
||||
👉 [人类:给 AI 装摄像头 vs AI:当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
|
||||
👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
|
||||
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
|
||||
|
||||
<img src="docs/mcp-based-graph.jpg" alt="通过MCP控制万物" width="320">
|
||||
|
||||
### 版本说明
|
||||
|
||||
当前 v2 版本与 v1 版本分区表不兼容,所以无法从 v1 版本通过 OTA 升级到 v2 版本。分区表说明参见 [partitions/v2/README.md](partitions/v2/README.md)。
|
||||
|
||||
使用 v1 版本的所有硬件,可以通过手动烧录固件来升级到 v2 版本。
|
||||
|
||||
v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版本,该分支会持续维护到 2026 年 2 月。
|
||||
|
||||
### 已实现功能
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP)
|
||||
- 采用 OPUS 音频编解码
|
||||
- 基于流式 ASR + LLM + TTS 架构的语音交互
|
||||
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD 显示屏,支持表情显示
|
||||
- 电量显示与电源管理
|
||||
- 支持多语言(中文、英文、日文)
|
||||
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
|
||||
- 通过设备端 MCP 实现设备控制(音量、灯光、电机、GPIO 等)
|
||||
- 通过云端 MCP 扩展大模型能力(智能家居控制、PC桌面操作、知识搜索、邮件收发等)
|
||||
- 自定义唤醒词、字体、表情与聊天背景,支持网页端在线修改 ([自定义Assets生成器](https://github.com/78/xiaozhi-assets-generator))
|
||||
|
||||
## 硬件
|
||||
|
||||
### 面包板手工制作实践
|
||||
|
||||
详见飞书文档教程:
|
||||
|
||||
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
面包板效果图如下:
|
||||
|
||||

|
||||
|
||||
### 支持 70 多个开源硬件(仅展示部分)
|
||||
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
|
||||
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
|
||||
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·AI 吊坠</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-1.54">无名科技 Nologo-星智-1.54TFT</a>
|
||||
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI 超低成本机器狗">ESP-HI 超低成本机器狗</a>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
|
||||
<img src="docs/v1/lichuang-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3">
|
||||
<img src="docs/v1/espbox3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
|
||||
<img src="docs/v1/m5cores3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
|
||||
<img src="docs/v1/atoms3r.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4">
|
||||
<img src="docs/v1/magiclick.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">
|
||||
<img src="docs/v1/waveshare.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
|
||||
<img src="docs/v1/xmini-c3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
|
||||
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54">
|
||||
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
|
||||
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低成本机器狗">
|
||||
<img src="docs/v1/esp-hi.jpg" width="240" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 软件
|
||||
|
||||
### 固件烧录
|
||||
|
||||
新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。
|
||||
|
||||
固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。
|
||||
|
||||
👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
|
||||
### 开发环境
|
||||
|
||||
- Cursor 或 VSCode
|
||||
- 安装 ESP-IDF 插件,选择 SDK 版本 5.4 或以上
|
||||
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
|
||||
- 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范
|
||||
|
||||
### 开发者文档
|
||||
|
||||
- [自定义开发板指南](docs/custom-board.md) - 学习如何为小智 AI 创建自定义开发板
|
||||
- [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备
|
||||
- [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式
|
||||
- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md)
|
||||
- [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
|
||||
|
||||
## 大模型配置
|
||||
|
||||
如果你已经拥有一个小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
|
||||
|
||||
👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
|
||||
## 相关开源项目
|
||||
|
||||
在个人电脑上部署服务器,可以参考以下第三方开源的项目:
|
||||
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
|
||||
|
||||
使用小智通信协议的第三方客户端项目:
|
||||
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客户端
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件
|
||||
|
||||
## 关于项目
|
||||
|
||||
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,修改或用于商业用途。
|
||||
|
||||
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
|
||||
|
||||
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:1011329060
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
36
docs/blufi.md
Normal file
36
docs/blufi.md
Normal file
@ -0,0 +1,36 @@
|
||||
# BluFi 配网(集成 esp-wifi-connect)
|
||||
|
||||
本文档说明如何在小智固件中启用和使用 BluFi(BLE Wi‑Fi 配网),并结合项目内置的 `esp-wifi-connect` 组件完成 Wi‑Fi 连接与存储。官方
|
||||
BluFi
|
||||
协议说明请参考 [Espressif 文档](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-guides/ble/blufi.html)。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 需要支持 BLE 的芯片与固件配置。
|
||||
- 在 `idf.py menuconfig` 中启用 `WiFi Configuration Method -> Esp Blufi`(`CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y`
|
||||
)。如果只想用 BluFi,可关闭同一菜单下的 Hotspot/Acoustic 选项。
|
||||
|
||||
- 保持默认的 NVS 与事件循环初始化(项目的 `app_main` 已处理)。
|
||||
- CONFIG_BT_BLUEDROID_ENABLED、CONFIG_BT_NIMBLE_ENABLED这两个宏应二选一,不能同时启用。
|
||||
## 工作流程
|
||||
|
||||
1) 手机端通过 BluFi(如官方 EspBlufi App 或自研客户端)连接设备,发送 Wi‑Fi SSID/密码。
|
||||
2) 设备侧在 `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP` 中将凭据写入 `SsidManager`(存储到 NVS,属于 `esp-wifi-connect` 组件)。
|
||||
3) 随后启动 `WifiStation` 扫描并连接;状态通过 BluFi 返回。
|
||||
4) 配网成功后设备会自动连接新 Wi‑Fi;失败则返回失败状态。
|
||||
|
||||
## 使用步骤
|
||||
|
||||
1. 配置:在 menuconfig 开启 `Esp Blufi`。编译并烧录固件。
|
||||
2. 触发配网:设备首次启动且没有已保存的 Wi‑Fi 时会自动进入配网。
|
||||
3. 手机端操作:打开 EspBlufi App(或其他 BluFi 客户端),搜索并连接设备,可以选择是否加密,按提示输入 Wi‑Fi SSID/密码并发送。
|
||||
4. 观察结果:
|
||||
- 成功:BluFi 报告连接成功,设备自动连接 Wi‑Fi。
|
||||
- 失败:BluFi 返回失败状态,可重新发送或检查路由器。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- BluFi 与 Hotspot/声波配网可以同时编译,但会同时启动,增加内存占用。建议在 menuconfig 中只保留一种方式。
|
||||
- 若多次测试,建议清除或覆盖存储的 SSID(`wifi` 命名空间),避免旧配置干扰。
|
||||
- 如果使用自定义 BluFi 客户端,需遵循官方协议帧格式,参考上文官方文档链接。
|
||||
- 官方文档中已提供EspBlufi APP下载地址
|
||||
@ -197,8 +197,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -24,6 +24,7 @@ set(SOURCES "audio/audio_codec.cc"
|
||||
"display/lvgl_display/gif/lvgl_gif.cc"
|
||||
"display/lvgl_display/gif/gifdec.c"
|
||||
"display/lvgl_display/jpg/image_to_jpeg.cpp"
|
||||
"display/lvgl_display/jpg/jpeg_to_image.c"
|
||||
"protocols/protocol.cc"
|
||||
"protocols/mqtt_protocol.cc"
|
||||
"protocols/websocket_protocol.cc"
|
||||
@ -32,7 +33,7 @@ set(SOURCES "audio/audio_codec.cc"
|
||||
"application.cc"
|
||||
"ota.cc"
|
||||
"settings.cc"
|
||||
"device_state_event.cc"
|
||||
"device_state_machine.cc"
|
||||
"assets.cc"
|
||||
"main.cc"
|
||||
)
|
||||
@ -61,6 +62,8 @@ endfunction()
|
||||
set(BUILTIN_TEXT_FONT font_puhui_14_1)
|
||||
set(BUILTIN_ICON_FONT font_awesome_14_1)
|
||||
|
||||
set(EMOTE_RESOLUTION "320_240")
|
||||
|
||||
# Add board files according to BOARD_TYPE
|
||||
# Set default assets if the board uses partition table V2
|
||||
if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
|
||||
@ -89,11 +92,13 @@ elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(EMOTE_RESOLUTION "320_240")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_BOX)
|
||||
set(BOARD_TYPE "esp-box")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(EMOTE_RESOLUTION "320_240")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE)
|
||||
set(BOARD_TYPE "esp-box-lite")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
@ -199,7 +204,9 @@ elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3)
|
||||
set(BOARD_TYPE "esp-spot-s3")
|
||||
set(BOARD_TYPE "esp-spot")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_SPOT_C5)
|
||||
set(BOARD_TYPE "esp-spot")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_HI)
|
||||
set(BOARD_TYPE "esp-hi")
|
||||
# Set ESP_HI emoji directory for DEFAULT_ASSETS_EXTRA_FILES
|
||||
@ -209,6 +216,13 @@ elseif(CONFIG_BOARD_TYPE_ECHOEAR)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
set(EMOTE_RESOLUTION "360_360")
|
||||
# set(EMOTE_EXTERNAL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/boards/echoear/assets")
|
||||
elseif(CONFIG_BOARD_TYPE_ESP_SENSAIRSHUTTLE)
|
||||
set(BOARD_TYPE "esp-sensairshuttle")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_16_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_AUDIO_BOARD)
|
||||
set(BOARD_TYPE "waveshare-s3-audio-board")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
@ -224,6 +238,11 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_2_06)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_2_06)
|
||||
set(BOARD_TYPE "waveshare-c6-touch-amoled-2.06")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4B)
|
||||
set(BOARD_TYPE "waveshare-s3-touch-lcd-4b")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
@ -264,6 +283,10 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_5B)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_16_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_ePaper_1_54)
|
||||
set(BOARD_TYPE "waveshare-s3-epaper-1.54")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_49)
|
||||
set(BOARD_TYPE "waveshare-s3-touch-lcd-3.49")
|
||||
set(LVGL_TEXT_FONT font_puhui_basic_30_4)
|
||||
@ -274,11 +297,26 @@ elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_LCD_1_69)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_TOUCH_LCD_1_83)
|
||||
set(BOARD_TYPE "waveshare-c6-touch-lcd-1.83")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_16_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_43)
|
||||
set(BOARD_TYPE "waveshare-c6-touch-amoled-1.43")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_32)
|
||||
set(BOARD_TYPE "waveshare-c6-touch-amoled-1.32")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_32)
|
||||
set(BOARD_TYPE "waveshare-s3-touch-amoled-1.32")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_P4_NANO)
|
||||
set(BOARD_TYPE "waveshare-p4-nano")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
@ -345,6 +383,11 @@ elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI2_ESP32C5)
|
||||
set(BOARD_TYPE "movecall-moji2-esp32c5")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3)
|
||||
set(BOARD_TYPE "movecall-cuican-esp32s3")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
@ -423,6 +466,11 @@ elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_XINGZHI_METAL_1_54_WIFI)
|
||||
set(BOARD_TYPE "xingzhi-metal-1.54-wifi")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER)
|
||||
set(BOARD_TYPE "sensecap-watcher")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
@ -526,6 +574,15 @@ elseif(CONFIG_BOARD_TYPE_WIRELESS_TAG_WTP4C5MP07S)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_AIPI_LITE)
|
||||
set(BOARD_TYPE "aipi-lite")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
|
||||
set(BUILTIN_ICON_FONT font_awesome_14_1)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
|
||||
elseif(CONFIG_BOARD_TYPE_HU_087)
|
||||
set(BOARD_TYPE "hu-087")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_14_1)
|
||||
set(BUILTIN_ICON_FONT font_awesome_14_1)
|
||||
endif()
|
||||
|
||||
file(GLOB BOARD_SOURCES
|
||||
@ -547,6 +604,10 @@ else()
|
||||
list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc")
|
||||
endif()
|
||||
|
||||
# Auto Select Additional Sources
|
||||
if (CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING)
|
||||
list(APPEND SOURCES "boards/common/blufi.cpp")
|
||||
endif ()
|
||||
# Select language directory according to Kconfig
|
||||
if(CONFIG_LANGUAGE_ZH_CN)
|
||||
set(LANG_DIR "zh-CN")
|
||||
@ -663,7 +724,8 @@ if(CONFIG_IDF_TARGET_ESP32)
|
||||
"audio/codecs/es8389_audio_codec.cc"
|
||||
"led/gpio_led.cc"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_camera.cc"
|
||||
"display/lvgl_display/jpg/image_to_jpeg.cpp"
|
||||
"display/lvgl_display/jpg/image_to_jpeg.cpp"
|
||||
"display/lvgl_display/jpg/jpeg_to_image.c"
|
||||
)
|
||||
endif()
|
||||
|
||||
@ -872,6 +934,14 @@ if ("${size}" AND "${offset}")
|
||||
get_assets_local_file("${CONFIG_CUSTOM_ASSETS_FILE}" ASSETS_LOCAL_FILE)
|
||||
esptool_py_flash_to_partition(flash "assets" "${ASSETS_LOCAL_FILE}")
|
||||
message(STATUS "Custom assets flash configured: ${ASSETS_LOCAL_FILE} -> assets partition")
|
||||
elseif(CONFIG_FLASH_EXPRESSION_ASSETS)
|
||||
set(ASSETS_NAME "expression_assets")
|
||||
set(ASSETS_PARTITION "assets")
|
||||
set(ASSETS_FILE "${CMAKE_BINARY_DIR}/${ASSETS_NAME}.bin")
|
||||
|
||||
build_speaker_assets_bin("${ASSETS_PARTITION}" ${EMOTE_RESOLUTION} ${ASSETS_FILE} ${CONFIG_MMAP_FILE_NAME_LENGTH})
|
||||
message(STATUS "Generated emote assets: ${ASSETS_FILE} -> ${ASSETS_PARTITION} partition")
|
||||
esptool_py_flash_to_partition(flash "${ASSETS_PARTITION}" "${ASSETS_FILE}")
|
||||
elseif(CONFIG_FLASH_NONE_ASSETS)
|
||||
message(STATUS "Assets flashing disabled (FLASH_NONE_ASSETS)")
|
||||
endif()
|
||||
|
||||
@ -8,7 +8,8 @@ config OTA_URL
|
||||
|
||||
choice
|
||||
prompt "Flash Assets"
|
||||
default FLASH_DEFAULT_ASSETS
|
||||
default FLASH_DEFAULT_ASSETS if !USE_EMOTE_MESSAGE_STYLE
|
||||
default FLASH_EXPRESSION_ASSETS if USE_EMOTE_MESSAGE_STYLE
|
||||
help
|
||||
Select the assets to flash.
|
||||
|
||||
@ -16,8 +17,12 @@ choice
|
||||
bool "Do not flash assets"
|
||||
config FLASH_DEFAULT_ASSETS
|
||||
bool "Flash Default Assets"
|
||||
depends on !USE_EMOTE_MESSAGE_STYLE
|
||||
config FLASH_CUSTOM_ASSETS
|
||||
bool "Flash Custom Assets"
|
||||
config FLASH_EXPRESSION_ASSETS
|
||||
bool "Flash Emote Assets"
|
||||
depends on USE_EMOTE_MESSAGE_STYLE
|
||||
endchoice
|
||||
|
||||
config CUSTOM_ASSETS_FILE
|
||||
@ -150,9 +155,15 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_ESP_SPARKBOT
|
||||
bool "Espressif SparkBot"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_ESP_SENSAIRSHUTTLE
|
||||
bool "Espressif ESP-SensairShuttle"
|
||||
depends on IDF_TARGET_ESP32C5
|
||||
config BOARD_TYPE_ESP_SPOT_S3
|
||||
bool "Espressif Spot-S3"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_ESP_SPOT_C5
|
||||
bool "Espressif Spot-C5"
|
||||
depends on IDF_TARGET_ESP32C5
|
||||
config BOARD_TYPE_ESP_HI
|
||||
bool "Espressif ESP-HI"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
@ -246,6 +257,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_2_06
|
||||
bool "Waveshare ESP32-S3-Touch-AMOLED-2.06"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_2_06
|
||||
bool "Waveshare ESP32-C6-Touch-AMOLED-2.06"
|
||||
depends on IDF_TARGET_ESP32C6
|
||||
config BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_75
|
||||
bool "Waveshare ESP32-S3-Touch-AMOLED-1.75"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
@ -267,15 +281,27 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_WAVESHARE_C6_LCD_1_69
|
||||
bool "Waveshare ESP32-C6-LCD-1.69"
|
||||
depends on IDF_TARGET_ESP32C6
|
||||
config BOARD_TYPE_WAVESHARE_C6_TOUCH_LCD_1_83
|
||||
bool "Waveshare ESP32-C6-Touch-LCD-1.83"
|
||||
depends on IDF_TARGET_ESP32C6
|
||||
config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_43
|
||||
bool "Waveshare ESP32-C6-Touch-AMOLOED-1.43"
|
||||
depends on IDF_TARGET_ESP32C6
|
||||
config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_32
|
||||
bool "Waveshare ESP32-C6-Touch-AMOLOED-1.32"
|
||||
depends on IDF_TARGET_ESP32C6
|
||||
config BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_32
|
||||
bool "Waveshare ESP32-S3-Touch-AMOLOED-1.32"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_49
|
||||
bool "Waveshare ESP32-S3-Touch-LCD-3.49"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_5
|
||||
bool "Waveshare ESP32-S3-Touch-LCD-3.5"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_WAVESHARE_S3_ePaper_1_54
|
||||
bool "Waveshare ESP32-S3-ePaper-1.54"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_5B
|
||||
bool "Waveshare ESP32-S3-Touch-LCD-3.5B"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
@ -315,6 +341,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_MOVECALL_MOJI_ESP32S3
|
||||
bool "Movecall Moji 小智AI衍生版"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_MOVECALL_MOJI2_ESP32C5
|
||||
bool "Movecall Moji2.0 小智AI衍生版"
|
||||
depends on IDF_TARGET_ESP32C5
|
||||
config BOARD_TYPE_MOVECALL_CUICAN_ESP32S3
|
||||
bool "Movecall CuiCan 璀璨·AI吊坠"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
@ -363,6 +392,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307
|
||||
bool "无名科技星智1.54(ML307)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_XINGZHI_METAL_1_54_WIFI
|
||||
bool "无名科技星智1.54 METAL(wifi)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER
|
||||
bool "Seeed Studio SenseCAP Watcher"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
@ -399,13 +431,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_OTTO_ROBOT
|
||||
bool "ottoRobot"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
select LV_USE_GIF
|
||||
select LV_GIF_CACHE_DECODE_DATA
|
||||
config BOARD_TYPE_ELECTRON_BOT
|
||||
bool "electronBot"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
select LV_USE_GIF
|
||||
select LV_GIF_CACHE_DECODE_DATA
|
||||
config BOARD_TYPE_JIUCHUAN
|
||||
bool "九川智能"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
@ -424,6 +452,12 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_WIRELESS_TAG_WTP4C5MP07S
|
||||
bool "Wireless-Tag WTP4C5MP07S"
|
||||
depends on IDF_TARGET_ESP32P4
|
||||
config BOARD_TYPE_AIPI_LITE
|
||||
bool "AIPI-Lite"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_HU_087
|
||||
bool "HU-087"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
endchoice
|
||||
|
||||
choice
|
||||
@ -457,7 +491,7 @@ choice ESP_S3_LCD_EV_Board_Version_TYPE
|
||||
endchoice
|
||||
|
||||
choice DISPLAY_OLED_TYPE
|
||||
depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32
|
||||
depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32 || BOARD_TYPE_HU_087
|
||||
prompt "OLED Type"
|
||||
default OLED_SSD1306_128X32
|
||||
help
|
||||
@ -556,7 +590,9 @@ choice DISPLAY_STYLE
|
||||
|
||||
config USE_EMOTE_MESSAGE_STYLE
|
||||
bool "Emote animation style"
|
||||
depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3
|
||||
depends on BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_3 \
|
||||
|| BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3 \
|
||||
|| BOARD_TYPE_ESP_SENSAIRSHUTTLE
|
||||
endchoice
|
||||
|
||||
choice WAKE_WORD_TYPE
|
||||
@ -651,6 +687,29 @@ config USE_AUDIO_DEBUGGER
|
||||
help
|
||||
Enable audio debugger, send audio data through UDP to the host machine
|
||||
|
||||
menu "WiFi Configuration Method"
|
||||
help
|
||||
WiFi Configuration Method Selection
|
||||
config USE_HOTSPOT_WIFI_PROVISIONING
|
||||
bool "Hotspot"
|
||||
default y
|
||||
help
|
||||
Use WiFi Hotspot to transmit WiFi configuration data
|
||||
config USE_ACOUSTIC_WIFI_PROVISIONING
|
||||
bool "Acoustic"
|
||||
help
|
||||
Use audio signal to transmit WiFi configuration data
|
||||
|
||||
config USE_ESP_BLUFI_WIFI_PROVISIONING
|
||||
bool "Esp Blufi"
|
||||
help
|
||||
Use esp blufi protocol to transmit WiFi configuration data
|
||||
select BT_ENABLED
|
||||
select BT_BLE_42_FEATURES_SUPPORTED
|
||||
select BT_BLE_BLUFI_ENABLE
|
||||
select MBEDTLS_DHM_C
|
||||
endmenu
|
||||
|
||||
config AUDIO_DEBUG_UDP_SERVER
|
||||
string "Audio Debug UDP Server Address"
|
||||
default "192.168.2.100:8000"
|
||||
@ -658,12 +717,6 @@ config AUDIO_DEBUG_UDP_SERVER
|
||||
help
|
||||
UDP server address, format: IP:PORT, used to receive audio debugging data
|
||||
|
||||
config USE_ACOUSTIC_WIFI_PROVISIONING
|
||||
bool "Enable Acoustic WiFi Provisioning"
|
||||
default n
|
||||
help
|
||||
Enable acoustic WiFi provisioning, use audio signal to transmit WiFi configuration data
|
||||
|
||||
config RECEIVE_CUSTOM_MESSAGE
|
||||
bool "Enable Custom Message Reception"
|
||||
default n
|
||||
@ -673,6 +726,18 @@ config RECEIVE_CUSTOM_MESSAGE
|
||||
menu "Camera Configuration"
|
||||
depends on !IDF_TARGET_ESP32
|
||||
|
||||
comment "Warning: Please read the help text before modifying these settings."
|
||||
|
||||
config XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
bool "Allow JPEG Input"
|
||||
default n
|
||||
help
|
||||
Allow JPEG Input format for the camera.
|
||||
|
||||
This option may need to be enabled when using a USB camera.
|
||||
|
||||
Not currently supported when used simultaneously with XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE.
|
||||
|
||||
config XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
bool "Enable Hardware JPEG Encoder"
|
||||
default y
|
||||
@ -681,6 +746,14 @@ menu "Camera Configuration"
|
||||
Use hardware JPEG encoder on ESP32-P4 to encode image to JPEG.
|
||||
See https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/api-reference/peripherals/jpeg.html for more details.
|
||||
|
||||
config XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER
|
||||
bool "Enable Hardware JPEG Decoder"
|
||||
default n
|
||||
depends on SOC_JPEG_DECODE_SUPPORTED && XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
help
|
||||
Use hardware JPEG decoder on ESP32-P4 to decode JPEG to image.
|
||||
See https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/api-reference/peripherals/jpeg.html for more details.
|
||||
|
||||
config XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
|
||||
bool "Enable Camera Debug Mode"
|
||||
default n
|
||||
@ -689,17 +762,43 @@ menu "Camera Configuration"
|
||||
Only works on boards that support camera.
|
||||
|
||||
config XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP
|
||||
bool "Enable software camera buffer endianness swapping (USE WITH CAUTION)"
|
||||
bool "Enable software camera buffer endianness swapping"
|
||||
default n
|
||||
depends on !CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER
|
||||
help
|
||||
This option treats the camera buffer as a uint16_t[] array and performs byte-swapping (endianness conversion) on each element.
|
||||
|
||||
|
||||
Should only be modified by development board integration engineers.
|
||||
|
||||
|
||||
**Incorrect usage may result in incorrect image colors!**
|
||||
|
||||
|
||||
ATTENTION: If the option CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER is available for your sensor, please use that instead.
|
||||
|
||||
menuconfig XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
bool "Enable Camera Image Rotation"
|
||||
default n
|
||||
depends on !XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
help
|
||||
Enable camera image rotation, rotate the camera image to the correct orientation.
|
||||
- On ESP32-P4, rotation is handled by PPA hardware.
|
||||
- On other chips, rotation is done in software with performance cost.
|
||||
- For 180° rotation, use HFlip + VFlip instead of this option.
|
||||
|
||||
Not currently supported when used simultaneously with XIAOZHI_CAMERA_ALLOW_JPEG_INPUT.
|
||||
|
||||
if XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
choice XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE
|
||||
prompt "Camera Image Rotation Angle (clockwise)"
|
||||
default XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_90
|
||||
help
|
||||
Camera image rotation angle.
|
||||
config XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_90
|
||||
bool "90°"
|
||||
config XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_270
|
||||
bool "270°"
|
||||
comment "For 180° rotation, use HFlip + VFlip instead of this option"
|
||||
endchoice
|
||||
endif
|
||||
endmenu
|
||||
|
||||
menu "TAIJIPAI_S3_CONFIG"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -14,16 +14,23 @@
|
||||
#include "protocol.h"
|
||||
#include "ota.h"
|
||||
#include "audio_service.h"
|
||||
#include "device_state_event.h"
|
||||
#include "device_state.h"
|
||||
#include "device_state_machine.h"
|
||||
|
||||
|
||||
#define MAIN_EVENT_SCHEDULE (1 << 0)
|
||||
#define MAIN_EVENT_SEND_AUDIO (1 << 1)
|
||||
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
|
||||
#define MAIN_EVENT_VAD_CHANGE (1 << 3)
|
||||
#define MAIN_EVENT_ERROR (1 << 4)
|
||||
#define MAIN_EVENT_CHECK_NEW_VERSION_DONE (1 << 5)
|
||||
#define MAIN_EVENT_CLOCK_TICK (1 << 6)
|
||||
// Main event bits
|
||||
#define MAIN_EVENT_SCHEDULE (1 << 0)
|
||||
#define MAIN_EVENT_SEND_AUDIO (1 << 1)
|
||||
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
|
||||
#define MAIN_EVENT_VAD_CHANGE (1 << 3)
|
||||
#define MAIN_EVENT_ERROR (1 << 4)
|
||||
#define MAIN_EVENT_ACTIVATION_DONE (1 << 5)
|
||||
#define MAIN_EVENT_CLOCK_TICK (1 << 6)
|
||||
#define MAIN_EVENT_NETWORK_CONNECTED (1 << 7)
|
||||
#define MAIN_EVENT_NETWORK_DISCONNECTED (1 << 8)
|
||||
#define MAIN_EVENT_TOGGLE_CHAT (1 << 9)
|
||||
#define MAIN_EVENT_START_LISTENING (1 << 10)
|
||||
#define MAIN_EVENT_STOP_LISTENING (1 << 11)
|
||||
#define MAIN_EVENT_STATE_CHANGED (1 << 12)
|
||||
|
||||
|
||||
enum AecMode {
|
||||
@ -38,31 +45,80 @@ public:
|
||||
static Application instance;
|
||||
return instance;
|
||||
}
|
||||
// 删除拷贝构造函数和赋值运算符
|
||||
// Delete copy constructor and assignment operator
|
||||
Application(const Application&) = delete;
|
||||
Application& operator=(const Application&) = delete;
|
||||
|
||||
void Start();
|
||||
void MainEventLoop();
|
||||
DeviceState GetDeviceState() const { return device_state_; }
|
||||
/**
|
||||
* Initialize the application
|
||||
* This sets up display, audio, network callbacks, etc.
|
||||
* Network connection starts asynchronously.
|
||||
*/
|
||||
void Initialize();
|
||||
|
||||
/**
|
||||
* Run the main event loop
|
||||
* This function runs in the main task and never returns.
|
||||
* It handles all events including network, state changes, and user interactions.
|
||||
*/
|
||||
void Run();
|
||||
|
||||
DeviceState GetDeviceState() const { return state_machine_.GetState(); }
|
||||
bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); }
|
||||
void Schedule(std::function<void()> callback);
|
||||
void SetDeviceState(DeviceState state);
|
||||
|
||||
/**
|
||||
* Request state transition
|
||||
* Returns true if transition was successful
|
||||
*/
|
||||
bool SetDeviceState(DeviceState state);
|
||||
|
||||
/**
|
||||
* Schedule a callback to be executed in the main task
|
||||
*/
|
||||
void Schedule(std::function<void()>&& callback);
|
||||
|
||||
/**
|
||||
* Alert with status, message, emotion and optional sound
|
||||
*/
|
||||
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");
|
||||
void DismissAlert();
|
||||
|
||||
void AbortSpeaking(AbortReason reason);
|
||||
|
||||
/**
|
||||
* Toggle chat state (event-based, thread-safe)
|
||||
* Sends MAIN_EVENT_TOGGLE_CHAT to be handled in Run()
|
||||
*/
|
||||
void ToggleChatState();
|
||||
|
||||
/**
|
||||
* Start listening (event-based, thread-safe)
|
||||
* Sends MAIN_EVENT_START_LISTENING to be handled in Run()
|
||||
*/
|
||||
void StartListening();
|
||||
|
||||
/**
|
||||
* Stop listening (event-based, thread-safe)
|
||||
* Sends MAIN_EVENT_STOP_LISTENING to be handled in Run()
|
||||
*/
|
||||
void StopListening();
|
||||
|
||||
void Reboot();
|
||||
void WakeWordInvoke(const std::string& wake_word);
|
||||
bool UpgradeFirmware(Ota& ota, const std::string& url = "");
|
||||
bool UpgradeFirmware(const std::string& url, const std::string& version = "");
|
||||
bool CanEnterSleepMode();
|
||||
void SendMcpMessage(const std::string& payload);
|
||||
void SetAecMode(AecMode mode);
|
||||
AecMode GetAecMode() const { return aec_mode_; }
|
||||
void PlaySound(const std::string_view& sound);
|
||||
AudioService& GetAudioService() { return audio_service_; }
|
||||
|
||||
/**
|
||||
* Reset protocol resources (thread-safe)
|
||||
* Can be called from any task to release resources allocated after network connected
|
||||
* This includes closing audio channel, resetting protocol and ota objects
|
||||
*/
|
||||
void ResetProtocol();
|
||||
|
||||
private:
|
||||
Application();
|
||||
@ -73,23 +129,43 @@ private:
|
||||
std::unique_ptr<Protocol> protocol_;
|
||||
EventGroupHandle_t event_group_ = nullptr;
|
||||
esp_timer_handle_t clock_timer_handle_ = nullptr;
|
||||
volatile DeviceState device_state_ = kDeviceStateUnknown;
|
||||
DeviceStateMachine state_machine_;
|
||||
ListeningMode listening_mode_ = kListeningModeAutoStop;
|
||||
AecMode aec_mode_ = kAecOff;
|
||||
std::string last_error_message_;
|
||||
AudioService audio_service_;
|
||||
std::unique_ptr<Ota> ota_;
|
||||
|
||||
bool has_server_time_ = false;
|
||||
bool aborted_ = false;
|
||||
bool assets_version_checked_ = false;
|
||||
bool play_popup_on_listening_ = false; // Flag to play popup sound after state changes to listening
|
||||
int clock_ticks_ = 0;
|
||||
TaskHandle_t check_new_version_task_handle_ = nullptr;
|
||||
TaskHandle_t main_event_loop_task_handle_ = nullptr;
|
||||
TaskHandle_t activation_task_handle_ = nullptr;
|
||||
|
||||
void OnWakeWordDetected();
|
||||
void CheckNewVersion(Ota& ota);
|
||||
|
||||
// Event handlers
|
||||
void HandleStateChangedEvent();
|
||||
void HandleToggleChatEvent();
|
||||
void HandleStartListeningEvent();
|
||||
void HandleStopListeningEvent();
|
||||
void HandleNetworkConnectedEvent();
|
||||
void HandleNetworkDisconnectedEvent();
|
||||
void HandleActivationDoneEvent();
|
||||
void HandleWakeWordDetectedEvent();
|
||||
|
||||
// Activation task (runs in background)
|
||||
void ActivationTask();
|
||||
|
||||
// Helper methods
|
||||
void CheckAssetsVersion();
|
||||
void CheckNewVersion();
|
||||
void InitializeProtocol();
|
||||
void ShowActivationCode(const std::string& code, const std::string& message);
|
||||
void SetListeningMode(ListeningMode mode);
|
||||
|
||||
// State change handler called by state machine
|
||||
void OnStateChanged(DeviceState old_state, DeviceState new_state);
|
||||
};
|
||||
|
||||
|
||||
|
||||
395
main/assets.cc
395
main/assets.cc
@ -4,14 +4,19 @@
|
||||
#include "application.h"
|
||||
#include "lvgl_theme.h"
|
||||
#include "emote_display.h"
|
||||
#include "expression_emote.h"
|
||||
#if HAVE_LVGL
|
||||
#include "display/lcd_display.h"
|
||||
#include <spi_flash_mmap.h>
|
||||
#endif
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <spi_flash_mmap.h>
|
||||
#include <esp_timer.h>
|
||||
#include <cbin_font.h>
|
||||
|
||||
|
||||
#define TAG "Assets"
|
||||
#define PARTITION_LABEL "assets"
|
||||
|
||||
struct mmap_assets_table {
|
||||
char asset_name[32]; /*!< Name of the asset */
|
||||
@ -21,19 +26,99 @@ struct mmap_assets_table {
|
||||
uint16_t asset_height; /*!< Height of the asset */
|
||||
};
|
||||
|
||||
|
||||
Assets::Assets() {
|
||||
#if HAVE_LVGL
|
||||
strategy_ = std::make_unique<Assets::LvglStrategy>();
|
||||
#else
|
||||
strategy_ = std::make_unique<Assets::EmoteStrategy>();
|
||||
#endif
|
||||
// Initialize the partition
|
||||
InitializePartition();
|
||||
}
|
||||
|
||||
Assets::~Assets() {
|
||||
if (mmap_handle_ != 0) {
|
||||
esp_partition_munmap(mmap_handle_);
|
||||
UnApplyPartition();
|
||||
}
|
||||
|
||||
bool Assets::FindPartition(Assets* assets) {
|
||||
assets->partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, PARTITION_LABEL);
|
||||
if (assets->partition_ == nullptr) {
|
||||
ESP_LOGI(TAG, "No assets partition found");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Assets::Apply() {
|
||||
return strategy_ ? strategy_->Apply(this) : false;
|
||||
}
|
||||
|
||||
bool Assets::InitializePartition() {
|
||||
return strategy_ ? strategy_->InitializePartition(this) : false;
|
||||
}
|
||||
|
||||
void Assets::UnApplyPartition() {
|
||||
if (strategy_) {
|
||||
strategy_->UnApplyPartition(this);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) {
|
||||
bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) {
|
||||
return strategy_ ? strategy_->GetAssetData(this, name, ptr, size) : false;
|
||||
}
|
||||
|
||||
bool Assets::LoadSrmodelsFromIndex(Assets* assets, cJSON* root) {
|
||||
void* ptr = nullptr;
|
||||
size_t size = 0;
|
||||
bool need_delete_root = false;
|
||||
|
||||
// If root is not provided, parse index.json
|
||||
if (root == nullptr) {
|
||||
if (!assets->GetAssetData("index.json", ptr, size)) {
|
||||
ESP_LOGE(TAG, "The index.json file is not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
|
||||
if (root == nullptr) {
|
||||
ESP_LOGE(TAG, "The index.json file is not valid");
|
||||
return false;
|
||||
}
|
||||
need_delete_root = true;
|
||||
}
|
||||
|
||||
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
|
||||
if (cJSON_IsString(srmodels)) {
|
||||
std::string srmodels_file = srmodels->valuestring;
|
||||
if (assets->GetAssetData(srmodels_file, ptr, size)) {
|
||||
if (assets->models_list_ != nullptr) {
|
||||
esp_srmodel_deinit(assets->models_list_);
|
||||
assets->models_list_ = nullptr;
|
||||
}
|
||||
assets->models_list_ = srmodel_load(static_cast<uint8_t*>(ptr));
|
||||
if (assets->models_list_ != nullptr) {
|
||||
auto& app = Application::GetInstance();
|
||||
app.GetAudioService().SetModelsList(assets->models_list_);
|
||||
if (need_delete_root) {
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to load srmodels.bin");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (need_delete_root) {
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#if HAVE_LVGL
|
||||
uint32_t Assets::LvglStrategy::CalculateChecksum(const char* data, uint32_t length) {
|
||||
uint32_t checksum = 0;
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
checksum += data[i];
|
||||
@ -41,40 +126,37 @@ uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) {
|
||||
return checksum & 0xFFFF;
|
||||
}
|
||||
|
||||
bool Assets::InitializePartition() {
|
||||
partition_valid_ = false;
|
||||
checksum_valid_ = false;
|
||||
bool Assets::LvglStrategy::InitializePartition(Assets* assets) {
|
||||
assets->partition_valid_ = false;
|
||||
assets_.clear();
|
||||
|
||||
partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, "assets");
|
||||
if (partition_ == nullptr) {
|
||||
ESP_LOGI(TAG, "No assets partition found");
|
||||
if (!Assets::FindPartition(assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
|
||||
uint32_t storage_size = free_pages * 64 * 1024;
|
||||
ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024);
|
||||
ESP_LOGI(TAG, "The partition size is %ld KB", partition_->size / 1024);
|
||||
if (storage_size < partition_->size) {
|
||||
ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, partition_->size / 1024);
|
||||
ESP_LOGI(TAG, "The partition size is %ld KB", assets->partition_->size / 1024);
|
||||
if (storage_size < assets->partition_->size) {
|
||||
ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, assets->partition_->size / 1024);
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_partition_mmap(partition_, 0, partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_);
|
||||
esp_err_t err = esp_partition_mmap(assets->partition_, 0, assets->partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
partition_valid_ = true;
|
||||
assets->partition_valid_ = true;
|
||||
|
||||
uint32_t stored_files = *(uint32_t*)(mmap_root_ + 0);
|
||||
uint32_t stored_chksum = *(uint32_t*)(mmap_root_ + 4);
|
||||
uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8);
|
||||
|
||||
if (stored_len > partition_->size - 12) {
|
||||
ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, partition_->size);
|
||||
if (stored_len > assets->partition_->size - 12) {
|
||||
ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, assets->partition_->size);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -101,10 +183,37 @@ bool Assets::InitializePartition() {
|
||||
return checksum_valid_;
|
||||
}
|
||||
|
||||
bool Assets::Apply() {
|
||||
void Assets::LvglStrategy::UnApplyPartition(Assets* assets) {
|
||||
if (mmap_handle_ != 0) {
|
||||
esp_partition_munmap(mmap_handle_);
|
||||
mmap_handle_ = 0;
|
||||
mmap_root_ = nullptr;
|
||||
}
|
||||
checksum_valid_ = false;
|
||||
assets_.clear();
|
||||
(void)assets; // Unused parameter
|
||||
}
|
||||
|
||||
bool Assets::LvglStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) {
|
||||
auto asset = assets_.find(name);
|
||||
if (asset == assets_.end()) {
|
||||
return false;
|
||||
}
|
||||
auto data = (const char*)(mmap_root_ + asset->second.offset);
|
||||
if (data[0] != 'Z' || data[1] != 'Z') {
|
||||
ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]);
|
||||
return false;
|
||||
}
|
||||
|
||||
ptr = static_cast<void*>(const_cast<char*>(data + 2));
|
||||
size = asset->second.size;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Assets::LvglStrategy::Apply(Assets* assets) {
|
||||
void* ptr = nullptr;
|
||||
size_t size = 0;
|
||||
if (!GetAssetData("index.json", ptr, size)) {
|
||||
if (!assets->GetAssetData("index.json", ptr, size)) {
|
||||
ESP_LOGE(TAG, "The index.json file is not found");
|
||||
return false;
|
||||
}
|
||||
@ -122,28 +231,9 @@ bool Assets::Apply() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
|
||||
if (cJSON_IsString(srmodels)) {
|
||||
std::string srmodels_file = srmodels->valuestring;
|
||||
if (GetAssetData(srmodels_file, ptr, size)) {
|
||||
if (models_list_ != nullptr) {
|
||||
esp_srmodel_deinit(models_list_);
|
||||
models_list_ = nullptr;
|
||||
}
|
||||
models_list_ = srmodel_load(static_cast<uint8_t*>(ptr));
|
||||
if (models_list_ != nullptr) {
|
||||
auto& app = Application::GetInstance();
|
||||
app.GetAudioService().SetModelsList(models_list_);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to load srmodels.bin");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAVE_LVGL
|
||||
Assets::LoadSrmodelsFromIndex(assets, root);
|
||||
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto light_theme = theme_manager.GetTheme("light");
|
||||
auto dark_theme = theme_manager.GetTheme("dark");
|
||||
@ -151,7 +241,7 @@ bool Assets::Apply() {
|
||||
cJSON* font = cJSON_GetObjectItem(root, "text_font");
|
||||
if (cJSON_IsString(font)) {
|
||||
std::string fonts_text_file = font->valuestring;
|
||||
if (GetAssetData(fonts_text_file, ptr, size)) {
|
||||
if (assets->GetAssetData(fonts_text_file, ptr, size)) {
|
||||
auto text_font = std::make_shared<LvglCBinFont>(ptr);
|
||||
if (text_font->font() == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to load fonts.bin");
|
||||
@ -179,7 +269,7 @@ bool Assets::Apply() {
|
||||
cJSON* file = cJSON_GetObjectItem(emoji, "file");
|
||||
cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf");
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) {
|
||||
if (!GetAssetData(file->valuestring, ptr, size)) {
|
||||
if (!assets->GetAssetData(file->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
|
||||
continue;
|
||||
}
|
||||
@ -210,7 +300,7 @@ bool Assets::Apply() {
|
||||
light_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_image)) {
|
||||
if (!GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
if (!assets->GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
|
||||
return false;
|
||||
}
|
||||
@ -231,7 +321,7 @@ bool Assets::Apply() {
|
||||
dark_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_image)) {
|
||||
if (!GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
if (!assets->GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
|
||||
return false;
|
||||
}
|
||||
@ -248,136 +338,95 @@ bool Assets::Apply() {
|
||||
if (current_theme != nullptr) {
|
||||
display->SetTheme(current_theme);
|
||||
}
|
||||
#elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE)
|
||||
auto &board = Board::GetInstance();
|
||||
auto display = board.GetDisplay();
|
||||
auto emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
|
||||
cJSON* font = cJSON_GetObjectItem(root, "text_font");
|
||||
if (cJSON_IsString(font)) {
|
||||
std::string fonts_text_file = font->valuestring;
|
||||
if (GetAssetData(fonts_text_file, ptr, size)) {
|
||||
auto text_font = std::make_shared<LvglCBinFont>(ptr);
|
||||
if (text_font->font() == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to load fonts.bin");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (emote_display) {
|
||||
emote_display->AddTextFont(text_font);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
|
||||
// Parse hide_subtitle configuration
|
||||
cJSON* hide_subtitle = cJSON_GetObjectItem(root, "hide_subtitle");
|
||||
if (cJSON_IsBool(hide_subtitle)) {
|
||||
bool hide = cJSON_IsTrue(hide_subtitle);
|
||||
auto lcd_display = dynamic_cast<LcdDisplay*>(display);
|
||||
if (lcd_display != nullptr) {
|
||||
lcd_display->SetHideSubtitle(hide);
|
||||
ESP_LOGI(TAG, "Set hide_subtitle to %s", hide ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
|
||||
if (cJSON_IsArray(emoji_collection)) {
|
||||
int emoji_count = cJSON_GetArraySize(emoji_collection);
|
||||
if (emote_display) {
|
||||
for (int i = 0; i < emoji_count; i++) {
|
||||
cJSON* icon = cJSON_GetArrayItem(emoji_collection, i);
|
||||
if (cJSON_IsObject(icon)) {
|
||||
cJSON* name = cJSON_GetObjectItem(icon, "name");
|
||||
cJSON* file = cJSON_GetObjectItem(icon, "file");
|
||||
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file)) {
|
||||
if (GetAssetData(file->valuestring, ptr, size)) {
|
||||
cJSON* eaf = cJSON_GetObjectItem(icon, "eaf");
|
||||
bool lack_value = false;
|
||||
bool loop_value = false;
|
||||
int fps_value = 0;
|
||||
|
||||
if (cJSON_IsObject(eaf)) {
|
||||
cJSON* lack = cJSON_GetObjectItem(eaf, "lack");
|
||||
cJSON* loop = cJSON_GetObjectItem(eaf, "loop");
|
||||
cJSON* fps = cJSON_GetObjectItem(eaf, "fps");
|
||||
|
||||
lack_value = lack ? cJSON_IsTrue(lack) : false;
|
||||
loop_value = loop ? cJSON_IsTrue(loop) : false;
|
||||
fps_value = fps ? fps->valueint : 0;
|
||||
|
||||
emote_display->AddEmojiData(name->valuestring, ptr, size,
|
||||
static_cast<uint8_t>(fps_value),
|
||||
loop_value, lack_value);
|
||||
}
|
||||
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Emoji \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* icon_collection = cJSON_GetObjectItem(root, "icon_collection");
|
||||
if (cJSON_IsArray(icon_collection)) {
|
||||
if (emote_display) {
|
||||
int icon_count = cJSON_GetArraySize(icon_collection);
|
||||
for (int i = 0; i < icon_count; i++) {
|
||||
cJSON* icon = cJSON_GetArrayItem(icon_collection, i);
|
||||
if (cJSON_IsObject(icon)) {
|
||||
cJSON* name = cJSON_GetObjectItem(icon, "name");
|
||||
cJSON* file = cJSON_GetObjectItem(icon, "file");
|
||||
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file)) {
|
||||
if (GetAssetData(file->valuestring, ptr, size)) {
|
||||
emote_display->AddIconData(name->valuestring, ptr, size);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Icon \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* layout_json = cJSON_GetObjectItem(root, "layout");
|
||||
if (cJSON_IsArray(layout_json)) {
|
||||
int layout_count = cJSON_GetArraySize(layout_json);
|
||||
|
||||
for (int i = 0; i < layout_count; i++) {
|
||||
cJSON* layout_item = cJSON_GetArrayItem(layout_json, i);
|
||||
if (cJSON_IsObject(layout_item)) {
|
||||
cJSON* name = cJSON_GetObjectItem(layout_item, "name");
|
||||
cJSON* align = cJSON_GetObjectItem(layout_item, "align");
|
||||
cJSON* x = cJSON_GetObjectItem(layout_item, "x");
|
||||
cJSON* y = cJSON_GetObjectItem(layout_item, "y");
|
||||
cJSON* width = cJSON_GetObjectItem(layout_item, "width");
|
||||
cJSON* height = cJSON_GetObjectItem(layout_item, "height");
|
||||
|
||||
if (cJSON_IsString(name) && cJSON_IsString(align) && cJSON_IsNumber(x) && cJSON_IsNumber(y)) {
|
||||
int width_val = cJSON_IsNumber(width) ? width->valueint : 0;
|
||||
int height_val = cJSON_IsNumber(height) ? height->valueint : 0;
|
||||
|
||||
if (emote_display) {
|
||||
emote_display->AddLayoutData(name->valuestring, align->valuestring,
|
||||
x->valueint, y->valueint, width_val, height_val);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid layout item %d: missing required fields", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
cJSON_Delete(root);
|
||||
return true;
|
||||
}
|
||||
#endif // HAVE_LVGL
|
||||
|
||||
bool Assets::EmoteStrategy::InitializePartition(Assets* assets) {
|
||||
assets->partition_valid_ = false;
|
||||
|
||||
if (!Assets::FindPartition(assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t ret = ESP_ERR_INVALID_STATE;
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
const emote_data_t data = {
|
||||
.type = EMOTE_SOURCE_PARTITION,
|
||||
.source = {
|
||||
.partition_label = PARTITION_LABEL,
|
||||
},
|
||||
.flags = {
|
||||
.mmap_enable = true, //must be true here!!!
|
||||
},
|
||||
};
|
||||
ret = emote_mount_assets(emote_display->GetEmoteHandle(), &data);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Emote display is not initialized");
|
||||
}
|
||||
assets->partition_valid_ = ((ret == ESP_OK) ? true : false);
|
||||
return assets->partition_valid_;
|
||||
}
|
||||
|
||||
void Assets::EmoteStrategy::UnApplyPartition(Assets* assets) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
emote_unmount_assets(emote_display->GetEmoteHandle());
|
||||
}
|
||||
(void)assets; // Unused parameter
|
||||
}
|
||||
|
||||
bool Assets::EmoteStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
const uint8_t* data = nullptr;
|
||||
size_t data_size = 0;
|
||||
if (ESP_OK == emote_get_asset_data_by_name(emote_display->GetEmoteHandle(), name.c_str(), &data, &data_size)) {
|
||||
ptr = const_cast<void*>(static_cast<const void*>(data));
|
||||
size = data_size;
|
||||
return true;
|
||||
}
|
||||
ESP_LOGE(TAG, "Failed to get asset data by name: %s", name.c_str());
|
||||
return false;
|
||||
}
|
||||
(void)assets; // Unused parameter
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Assets::EmoteStrategy::Apply(Assets* assets) {
|
||||
Assets::LoadSrmodelsFromIndex(assets);
|
||||
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
emote_load_assets(emote_display->GetEmoteHandle());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Assets::Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback) {
|
||||
ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str());
|
||||
|
||||
|
||||
// 取消当前资源分区的内存映射
|
||||
if (mmap_handle_ != 0) {
|
||||
esp_partition_munmap(mmap_handle_);
|
||||
mmap_handle_ = 0;
|
||||
mmap_root_ = nullptr;
|
||||
}
|
||||
checksum_valid_ = false;
|
||||
assets_.clear();
|
||||
UnApplyPartition();
|
||||
|
||||
// 下载新的资源文件
|
||||
auto network = Board::GetInstance().GetNetwork();
|
||||
@ -499,19 +548,3 @@ bool Assets::Download(std::string url, std::function<void(int progress, size_t s
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) {
|
||||
auto asset = assets_.find(name);
|
||||
if (asset == assets_.end()) {
|
||||
return false;
|
||||
}
|
||||
auto data = (const char*)(mmap_root_ + asset->second.offset);
|
||||
if (data[0] != 'Z' || data[1] != 'Z') {
|
||||
ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]);
|
||||
return false;
|
||||
}
|
||||
|
||||
ptr = static_cast<void*>(const_cast<char*>(data + 2));
|
||||
size = asset->second.size;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
#ifndef ASSETS_H
|
||||
#define ASSETS_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <cJSON.h>
|
||||
#include <esp_partition.h>
|
||||
#include <model_path.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#if HAVE_LVGL
|
||||
#include <spi_flash_mmap.h>
|
||||
#endif
|
||||
|
||||
struct Asset {
|
||||
size_t size;
|
||||
@ -28,7 +33,6 @@ public:
|
||||
bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
|
||||
|
||||
inline bool partition_valid() const { return partition_valid_; }
|
||||
inline bool checksum_valid() const { return checksum_valid_; }
|
||||
inline std::string default_assets_url() const { return default_assets_url_; }
|
||||
|
||||
private:
|
||||
@ -37,16 +41,49 @@ private:
|
||||
Assets& operator=(const Assets&) = delete;
|
||||
|
||||
bool InitializePartition();
|
||||
uint32_t CalculateChecksum(const char* data, uint32_t length);
|
||||
void UnApplyPartition();
|
||||
static bool FindPartition(Assets* assets);
|
||||
static bool LoadSrmodelsFromIndex(Assets* assets, cJSON* root = nullptr);
|
||||
|
||||
class AssetStrategy {
|
||||
public:
|
||||
virtual ~AssetStrategy() = default;
|
||||
virtual bool Apply(Assets* assets) = 0;
|
||||
virtual bool InitializePartition(Assets* assets) = 0;
|
||||
virtual void UnApplyPartition(Assets* assets) = 0;
|
||||
virtual bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) = 0;
|
||||
};
|
||||
|
||||
class LvglStrategy : public AssetStrategy {
|
||||
public:
|
||||
bool Apply(Assets* assets) override;
|
||||
bool InitializePartition(Assets* assets) override;
|
||||
void UnApplyPartition(Assets* assets) override;
|
||||
bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override;
|
||||
private:
|
||||
static uint32_t CalculateChecksum(const char* data, uint32_t length);
|
||||
std::map<std::string, Asset> assets_;
|
||||
esp_partition_mmap_handle_t mmap_handle_ = 0;
|
||||
const char* mmap_root_ = nullptr;
|
||||
bool checksum_valid_ = false;
|
||||
};
|
||||
|
||||
class EmoteStrategy : public AssetStrategy {
|
||||
public:
|
||||
bool Apply(Assets* assets) override;
|
||||
bool InitializePartition(Assets* assets) override;
|
||||
void UnApplyPartition(Assets* assets) override;
|
||||
bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override;
|
||||
};
|
||||
|
||||
// Strategy instance
|
||||
std::unique_ptr<AssetStrategy> strategy_;
|
||||
|
||||
protected:
|
||||
const esp_partition_t* partition_ = nullptr;
|
||||
esp_partition_mmap_handle_t mmap_handle_ = 0;
|
||||
const char* mmap_root_ = nullptr;
|
||||
bool partition_valid_ = false;
|
||||
bool checksum_valid_ = false;
|
||||
std::string default_assets_url_;
|
||||
srmodel_list_t* models_list_ = nullptr;
|
||||
std::map<std::string, Asset> assets_;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,6 +2,26 @@
|
||||
#include <esp_log.h>
|
||||
#include <cstring>
|
||||
|
||||
#define RATE_CVT_CFG(_src_rate, _dest_rate, _channel) \
|
||||
(esp_ae_rate_cvt_cfg_t) \
|
||||
{ \
|
||||
.src_rate = (uint32_t)(_src_rate), \
|
||||
.dest_rate = (uint32_t)(_dest_rate), \
|
||||
.channel = (uint8_t)(_channel), \
|
||||
.bits_per_sample = ESP_AUDIO_BIT16, \
|
||||
.complexity = 2, \
|
||||
.perf_type = ESP_AE_RATE_CVT_PERF_TYPE_SPEED, \
|
||||
}
|
||||
|
||||
#define OPUS_DEC_CFG(_sample_rate, _frame_duration_ms) \
|
||||
(esp_opus_dec_cfg_t) \
|
||||
{ \
|
||||
.sample_rate = (uint32_t)(_sample_rate), \
|
||||
.channel = ESP_AUDIO_MONO, \
|
||||
.frame_duration = (esp_opus_dec_frame_duration_t)AS_OPUS_GET_FRAME_DRU_ENUM(_frame_duration_ms), \
|
||||
.self_delimited = false, \
|
||||
}
|
||||
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
#include "processors/afe_audio_processor.h"
|
||||
#else
|
||||
@ -17,7 +37,6 @@
|
||||
|
||||
#define TAG "AudioService"
|
||||
|
||||
|
||||
AudioService::AudioService() {
|
||||
event_group_ = xEventGroupCreate();
|
||||
}
|
||||
@ -26,21 +45,51 @@ AudioService::~AudioService() {
|
||||
if (event_group_ != nullptr) {
|
||||
vEventGroupDelete(event_group_);
|
||||
}
|
||||
if (opus_encoder_ != nullptr) {
|
||||
esp_opus_enc_close(opus_encoder_);
|
||||
}
|
||||
if (opus_decoder_ != nullptr) {
|
||||
esp_opus_dec_close(opus_decoder_);
|
||||
}
|
||||
if (input_resampler_ != nullptr) {
|
||||
esp_ae_rate_cvt_close(input_resampler_);
|
||||
}
|
||||
if (output_resampler_ != nullptr) {
|
||||
esp_ae_rate_cvt_close(output_resampler_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AudioService::Initialize(AudioCodec* codec) {
|
||||
codec_ = codec;
|
||||
codec_->Start();
|
||||
|
||||
/* Setup the audio codec */
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);
|
||||
opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
|
||||
opus_encoder_->SetComplexity(0);
|
||||
esp_opus_dec_cfg_t opus_dec_cfg = OPUS_DEC_CFG(codec->output_sample_rate(), OPUS_FRAME_DURATION_MS);
|
||||
auto ret = esp_opus_dec_open(&opus_dec_cfg, sizeof(esp_opus_dec_cfg_t), &opus_decoder_);
|
||||
if (opus_decoder_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", ret);
|
||||
} else {
|
||||
decoder_sample_rate_ = codec->output_sample_rate();
|
||||
decoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
|
||||
decoder_frame_size_ = decoder_sample_rate_ / 1000 * OPUS_FRAME_DURATION_MS;
|
||||
}
|
||||
esp_opus_enc_config_t opus_enc_cfg = AS_OPUS_ENC_CONFIG();
|
||||
ret = esp_opus_enc_open(&opus_enc_cfg, sizeof(esp_opus_enc_config_t), &opus_encoder_);
|
||||
if (opus_encoder_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", ret);
|
||||
} else {
|
||||
encoder_sample_rate_ = 16000;
|
||||
encoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
|
||||
esp_opus_enc_get_frame_size(opus_encoder_, &encoder_frame_size_, &encoder_outbuf_size_);
|
||||
encoder_frame_size_ = encoder_frame_size_ / sizeof(int16_t);
|
||||
}
|
||||
|
||||
if (codec->input_sample_rate() != 16000) {
|
||||
input_resampler_.Configure(codec->input_sample_rate(), 16000);
|
||||
reference_resampler_.Configure(codec->input_sample_rate(), 16000);
|
||||
esp_ae_rate_cvt_cfg_t input_resampler_cfg = RATE_CVT_CFG(
|
||||
codec->input_sample_rate(), ESP_AUDIO_SAMPLE_RATE_16K, codec->input_channels());
|
||||
auto resampler_ret = esp_ae_rate_cvt_open(&input_resampler_cfg, &input_resampler_);
|
||||
if (input_resampler_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create input resampler, error code: %d", resampler_ret);
|
||||
}
|
||||
}
|
||||
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
@ -114,7 +163,7 @@ void AudioService::Start() {
|
||||
AudioService* audio_service = (AudioService*)arg;
|
||||
audio_service->OpusCodecTask();
|
||||
vTaskDelete(NULL);
|
||||
}, "opus_codec", 2048 * 13, this, 2, &opus_codec_task_handle_);
|
||||
}, "opus_codec", 2048 * 12, this, 2, &opus_codec_task_handle_);
|
||||
}
|
||||
|
||||
void AudioService::Stop() {
|
||||
@ -144,25 +193,15 @@ bool AudioService::ReadAudioData(std::vector<int16_t>& data, int sample_rate, in
|
||||
if (!codec_->InputData(data)) {
|
||||
return false;
|
||||
}
|
||||
if (codec_->input_channels() == 2) {
|
||||
auto mic_channel = std::vector<int16_t>(data.size() / 2);
|
||||
auto reference_channel = std::vector<int16_t>(data.size() / 2);
|
||||
for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) {
|
||||
mic_channel[i] = data[j];
|
||||
reference_channel[i] = data[j + 1];
|
||||
}
|
||||
auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size()));
|
||||
auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size()));
|
||||
input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data());
|
||||
reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data());
|
||||
data.resize(resampled_mic.size() + resampled_reference.size());
|
||||
for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) {
|
||||
data[j] = resampled_mic[i];
|
||||
data[j + 1] = resampled_reference[i];
|
||||
}
|
||||
} else {
|
||||
auto resampled = std::vector<int16_t>(input_resampler_.GetOutputSamples(data.size()));
|
||||
input_resampler_.Process(data.data(), data.size(), resampled.data());
|
||||
if (input_resampler_ != nullptr) {
|
||||
uint32_t in_sample_num = data.size() / codec_->input_channels();
|
||||
uint32_t output_samples = 0;
|
||||
esp_ae_rate_cvt_get_max_out_sample_num(input_resampler_, in_sample_num, &output_samples);
|
||||
auto resampled = std::vector<int16_t>(output_samples * codec_->input_channels());
|
||||
uint32_t actual_output = output_samples;
|
||||
esp_ae_rate_cvt_process(input_resampler_, (esp_ae_sample_t)data.data(), in_sample_num,
|
||||
(esp_ae_sample_t)resampled.data(), &actual_output);
|
||||
resampled.resize(actual_output * codec_->input_channels());
|
||||
data = std::move(resampled);
|
||||
}
|
||||
} else {
|
||||
@ -316,25 +355,49 @@ void AudioService::OpusCodecTask() {
|
||||
task->timestamp = packet->timestamp;
|
||||
|
||||
SetDecodeSampleRate(packet->sample_rate, packet->frame_duration);
|
||||
if (opus_decoder_->Decode(std::move(packet->payload), task->pcm)) {
|
||||
// Resample if the sample rate is different
|
||||
if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
|
||||
int target_size = output_resampler_.GetOutputSamples(task->pcm.size());
|
||||
std::vector<int16_t> resampled(target_size);
|
||||
output_resampler_.Process(task->pcm.data(), task->pcm.size(), resampled.data());
|
||||
task->pcm = std::move(resampled);
|
||||
if (opus_decoder_ != nullptr) {
|
||||
task->pcm.resize(decoder_frame_size_);
|
||||
esp_audio_dec_in_raw_t raw = {
|
||||
.buffer = (uint8_t *)(packet->payload.data()),
|
||||
.len = (uint32_t)(packet->payload.size()),
|
||||
.consumed = 0,
|
||||
.frame_recover = ESP_AUDIO_DEC_RECOVERY_NONE,
|
||||
};
|
||||
esp_audio_dec_out_frame_t out_frame = {
|
||||
.buffer = (uint8_t *)(task->pcm.data()),
|
||||
.len = (uint32_t)(task->pcm.size() * sizeof(int16_t)),
|
||||
.decoded_size = 0,
|
||||
};
|
||||
esp_audio_dec_info_t dec_info = {};
|
||||
std::unique_lock<std::mutex> decoder_lock(decoder_mutex_);
|
||||
auto ret = esp_opus_dec_decode(opus_decoder_, &raw, &out_frame, &dec_info);
|
||||
decoder_lock.unlock();
|
||||
if (ret == ESP_AUDIO_ERR_OK) {
|
||||
task->pcm.resize(out_frame.decoded_size / sizeof(int16_t));
|
||||
if (decoder_sample_rate_ != codec_->output_sample_rate() && output_resampler_ != nullptr) {
|
||||
uint32_t target_size = 0;
|
||||
esp_ae_rate_cvt_get_max_out_sample_num(output_resampler_, task->pcm.size(), &target_size);
|
||||
std::vector<int16_t> resampled(target_size);
|
||||
uint32_t actual_output = target_size;
|
||||
esp_ae_rate_cvt_process(output_resampler_, (esp_ae_sample_t)task->pcm.data(), task->pcm.size(),
|
||||
(esp_ae_sample_t)resampled.data(), &actual_output);
|
||||
resampled.resize(actual_output);
|
||||
task->pcm = std::move(resampled);
|
||||
}
|
||||
lock.lock();
|
||||
audio_playback_queue_.push_back(std::move(task));
|
||||
audio_queue_cv_.notify_all();
|
||||
debug_statistics_.decode_count++;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to decode audio after resize, error code: %d", ret);
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
audio_playback_queue_.push_back(std::move(task));
|
||||
audio_queue_cv_.notify_all();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to decode audio");
|
||||
ESP_LOGE(TAG, "Audio decoder is not configured");
|
||||
lock.lock();
|
||||
}
|
||||
debug_statistics_.decode_count++;
|
||||
}
|
||||
|
||||
/* Encode the audio to send queue */
|
||||
if (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) {
|
||||
auto task = std::move(audio_encode_queue_.front());
|
||||
@ -346,24 +409,42 @@ void AudioService::OpusCodecTask() {
|
||||
packet->frame_duration = OPUS_FRAME_DURATION_MS;
|
||||
packet->sample_rate = 16000;
|
||||
packet->timestamp = task->timestamp;
|
||||
if (!opus_encoder_->Encode(std::move(task->pcm), packet->payload)) {
|
||||
ESP_LOGE(TAG, "Failed to encode audio");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (task->type == kAudioTaskTypeEncodeToSendQueue) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_send_queue_.push_back(std::move(packet));
|
||||
if (opus_encoder_ != nullptr && task->pcm.size() == encoder_frame_size_) {
|
||||
std::vector<uint8_t> buf(encoder_outbuf_size_);
|
||||
esp_audio_enc_in_frame_t in = {
|
||||
.buffer = (uint8_t *)(task->pcm.data()),
|
||||
.len = (uint32_t)(encoder_frame_size_ * sizeof(int16_t)),
|
||||
};
|
||||
esp_audio_enc_out_frame_t out = {
|
||||
.buffer = buf.data(),
|
||||
.len = (uint32_t)encoder_outbuf_size_,
|
||||
.encoded_bytes = 0,
|
||||
};
|
||||
auto ret = esp_opus_enc_process(opus_encoder_, &in, &out);
|
||||
if (ret == ESP_AUDIO_ERR_OK) {
|
||||
packet->payload.assign(buf.data(), buf.data() + out.encoded_bytes);
|
||||
|
||||
if (task->type == kAudioTaskTypeEncodeToSendQueue) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock2(audio_queue_mutex_);
|
||||
audio_send_queue_.push_back(std::move(packet));
|
||||
}
|
||||
if (callbacks_.on_send_queue_available) {
|
||||
callbacks_.on_send_queue_available();
|
||||
}
|
||||
} else if (task->type == kAudioTaskTypeEncodeToTestingQueue) {
|
||||
std::lock_guard<std::mutex> lock2(audio_queue_mutex_);
|
||||
audio_testing_queue_.push_back(std::move(packet));
|
||||
}
|
||||
debug_statistics_.encode_count++;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to encode audio, error code: %d", ret);
|
||||
}
|
||||
if (callbacks_.on_send_queue_available) {
|
||||
callbacks_.on_send_queue_available();
|
||||
}
|
||||
} else if (task->type == kAudioTaskTypeEncodeToTestingQueue) {
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_testing_queue_.push_back(std::move(packet));
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to encode audio: encoder not configured or invalid frame size (got %u, expected %u)",
|
||||
task->pcm.size(), encoder_frame_size_);
|
||||
}
|
||||
debug_statistics_.encode_count++;
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
@ -372,17 +453,38 @@ void AudioService::OpusCodecTask() {
|
||||
}
|
||||
|
||||
void AudioService::SetDecodeSampleRate(int sample_rate, int frame_duration) {
|
||||
if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) {
|
||||
if (decoder_sample_rate_ == sample_rate && decoder_duration_ms_ == frame_duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
opus_decoder_.reset();
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, frame_duration);
|
||||
std::unique_lock<std::mutex> decoder_lock(decoder_mutex_);
|
||||
if (opus_decoder_ != nullptr) {
|
||||
esp_opus_dec_close(opus_decoder_);
|
||||
opus_decoder_ = nullptr;
|
||||
}
|
||||
decoder_lock.unlock();
|
||||
esp_opus_dec_cfg_t opus_dec_cfg = OPUS_DEC_CFG(sample_rate, frame_duration);
|
||||
auto ret = esp_opus_dec_open(&opus_dec_cfg, sizeof(esp_opus_dec_cfg_t), &opus_decoder_);
|
||||
if (opus_decoder_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", ret);
|
||||
return;
|
||||
}
|
||||
decoder_sample_rate_ = sample_rate;
|
||||
decoder_duration_ms_ = frame_duration;
|
||||
decoder_frame_size_ = decoder_sample_rate_ / 1000 * frame_duration;
|
||||
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
|
||||
ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate());
|
||||
output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate());
|
||||
if (decoder_sample_rate_ != codec->output_sample_rate()) {
|
||||
ESP_LOGI(TAG, "Resampling audio from %d to %d", decoder_sample_rate_, codec->output_sample_rate());
|
||||
if (output_resampler_ != nullptr) {
|
||||
esp_ae_rate_cvt_close(output_resampler_);
|
||||
output_resampler_ = nullptr;
|
||||
}
|
||||
esp_ae_rate_cvt_cfg_t output_resampler_cfg = RATE_CVT_CFG(
|
||||
decoder_sample_rate_, codec->output_sample_rate(), ESP_AUDIO_MONO);
|
||||
auto resampler_ret = esp_ae_rate_cvt_open(&output_resampler_cfg, &output_resampler_);
|
||||
if (output_resampler_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create output resampler, error code: %d", resampler_ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +492,6 @@ void AudioService::PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t
|
||||
auto task = std::make_unique<AudioTask>();
|
||||
task->type = type;
|
||||
task->pcm = std::move(pcm);
|
||||
|
||||
/* Push the task to the encode queue */
|
||||
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
|
||||
|
||||
@ -580,18 +681,16 @@ void AudioService::PlaySound(const std::string_view& ogg) {
|
||||
// 解析OpusHead包
|
||||
if (pkt_len >= 19 && std::memcmp(pkt_ptr, "OpusHead", 8) == 0) {
|
||||
seen_head = true;
|
||||
|
||||
// OpusHead结构:[0-7] "OpusHead", [8] version, [9] channel_count, [10-11] pre_skip
|
||||
// [12-15] input_sample_rate, [16-17] output_gain, [18] mapping_family
|
||||
if (pkt_len >= 12) {
|
||||
uint8_t version = pkt_ptr[8];
|
||||
uint8_t channel_count = pkt_ptr[9];
|
||||
|
||||
if (pkt_len >= 16) {
|
||||
// 读取输入采样率 (little-endian)
|
||||
sample_rate = pkt_ptr[12] | (pkt_ptr[13] << 8) |
|
||||
sample_rate = pkt_ptr[12] | (pkt_ptr[13] << 8) |
|
||||
(pkt_ptr[14] << 16) | (pkt_ptr[15] << 24);
|
||||
ESP_LOGI(TAG, "OpusHead: version=%d, channels=%d, sample_rate=%d",
|
||||
ESP_LOGI(TAG, "OpusHead: version=%d, channels=%d, sample_rate=%d",
|
||||
version, channel_count, sample_rate);
|
||||
}
|
||||
}
|
||||
@ -626,7 +725,11 @@ bool AudioService::IsIdle() {
|
||||
|
||||
void AudioService::ResetDecoder() {
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
opus_decoder_->ResetState();
|
||||
std::unique_lock<std::mutex> decoder_lock(decoder_mutex_);
|
||||
if (opus_decoder_ != nullptr) {
|
||||
esp_opus_dec_reset(opus_decoder_);
|
||||
}
|
||||
decoder_lock.unlock();
|
||||
timestamp_queue_.clear();
|
||||
audio_decode_queue_.clear();
|
||||
audio_playback_queue_.clear();
|
||||
|
||||
@ -12,10 +12,11 @@
|
||||
#include <freertos/event_groups.h>
|
||||
#include <esp_timer.h>
|
||||
#include <model_path.h>
|
||||
|
||||
#include <opus_encoder.h>
|
||||
#include <opus_decoder.h>
|
||||
#include <opus_resampler.h>
|
||||
#include "esp_audio_enc.h"
|
||||
#include "esp_opus_enc.h"
|
||||
#include "esp_opus_dec.h"
|
||||
#include "esp_ae_rate_cvt.h"
|
||||
#include "esp_audio_types.h"
|
||||
|
||||
#include "audio_codec.h"
|
||||
#include "audio_processor.h"
|
||||
@ -46,12 +47,34 @@
|
||||
#define AUDIO_POWER_TIMEOUT_MS 15000
|
||||
#define AUDIO_POWER_CHECK_INTERVAL_MS 1000
|
||||
|
||||
|
||||
#define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0)
|
||||
#define AS_EVENT_WAKE_WORD_RUNNING (1 << 1)
|
||||
#define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2)
|
||||
#define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3)
|
||||
|
||||
#define AS_OPUS_GET_FRAME_DRU_ENUM(duration_ms) \
|
||||
((duration_ms) == 5 ? ESP_OPUS_ENC_FRAME_DURATION_5_MS : \
|
||||
(duration_ms) == 10 ? ESP_OPUS_ENC_FRAME_DURATION_10_MS : \
|
||||
(duration_ms) == 20 ? ESP_OPUS_ENC_FRAME_DURATION_20_MS : \
|
||||
(duration_ms) == 40 ? ESP_OPUS_ENC_FRAME_DURATION_40_MS : \
|
||||
(duration_ms) == 60 ? ESP_OPUS_ENC_FRAME_DURATION_60_MS : \
|
||||
(duration_ms) == 80 ? ESP_OPUS_ENC_FRAME_DURATION_80_MS : \
|
||||
(duration_ms) == 100 ? ESP_OPUS_ENC_FRAME_DURATION_100_MS : \
|
||||
(duration_ms) == 120 ? ESP_OPUS_ENC_FRAME_DURATION_120_MS : -1)
|
||||
|
||||
#define AS_OPUS_ENC_CONFIG() { \
|
||||
.sample_rate = ESP_AUDIO_SAMPLE_RATE_16K, \
|
||||
.channel = ESP_AUDIO_MONO, \
|
||||
.bits_per_sample = ESP_AUDIO_BIT16, \
|
||||
.bitrate = ESP_OPUS_BITRATE_AUTO, \
|
||||
.frame_duration = (esp_opus_enc_frame_duration_t)AS_OPUS_GET_FRAME_DRU_ENUM(OPUS_FRAME_DURATION_MS), \
|
||||
.application_mode = ESP_OPUS_ENC_APPLICATION_AUDIO, \
|
||||
.complexity = 0, \
|
||||
.enable_fec = false, \
|
||||
.enable_dtx = true, \
|
||||
.enable_vbr = true, \
|
||||
}
|
||||
|
||||
struct AudioServiceCallbacks {
|
||||
std::function<void(void)> on_send_queue_available;
|
||||
std::function<void(const std::string&)> on_wake_word_detected;
|
||||
@ -116,11 +139,20 @@ private:
|
||||
std::unique_ptr<AudioProcessor> audio_processor_;
|
||||
std::unique_ptr<WakeWord> wake_word_;
|
||||
std::unique_ptr<AudioDebugger> audio_debugger_;
|
||||
std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
|
||||
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
|
||||
OpusResampler input_resampler_;
|
||||
OpusResampler reference_resampler_;
|
||||
OpusResampler output_resampler_;
|
||||
void* opus_encoder_ = nullptr;
|
||||
void* opus_decoder_ = nullptr;
|
||||
std::mutex decoder_mutex_;
|
||||
esp_ae_rate_cvt_handle_t input_resampler_ = nullptr;
|
||||
esp_ae_rate_cvt_handle_t output_resampler_ = nullptr;
|
||||
|
||||
// Encoder/Decoder state
|
||||
int encoder_sample_rate_ = 16000;
|
||||
int encoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
|
||||
int encoder_frame_size_ = 0;
|
||||
int encoder_outbuf_size_ = 0;
|
||||
int decoder_sample_rate_ = 0;
|
||||
int decoder_duration_ms_ = OPUS_FRAME_DURATION_MS;
|
||||
int decoder_frame_size_ = 0;
|
||||
DebugStatistics debug_statistics_;
|
||||
srmodel_list_t* models_list_ = nullptr;
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ public:
|
||||
|
||||
private:
|
||||
EventGroupHandle_t event_group_ = nullptr;
|
||||
esp_afe_sr_iface_t* afe_iface_ = nullptr;
|
||||
const esp_afe_sr_iface_t* afe_iface_ = nullptr;
|
||||
esp_afe_sr_data_t* afe_data_ = nullptr;
|
||||
std::function<void(std::vector<int16_t>&& data)> output_callback_;
|
||||
std::function<void(bool speaking)> vad_state_change_callback_;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#include "afe_wake_word.h"
|
||||
#include "audio_service.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <sstream>
|
||||
|
||||
@ -157,7 +156,7 @@ void AfeWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) {
|
||||
}
|
||||
|
||||
void AfeWakeWord::EncodeWakeWordData() {
|
||||
const size_t stack_size = 4096 * 7;
|
||||
const size_t stack_size = 4096 * 6;
|
||||
wake_word_opus_.clear();
|
||||
if (wake_word_encode_task_stack_ == nullptr) {
|
||||
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
|
||||
@ -172,20 +171,62 @@ void AfeWakeWord::EncodeWakeWordData() {
|
||||
auto this_ = (AfeWakeWord*)arg;
|
||||
{
|
||||
auto start_time = esp_timer_get_time();
|
||||
auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
|
||||
encoder->SetComplexity(0); // 0 is the fastest
|
||||
|
||||
// Create encoder
|
||||
esp_opus_enc_config_t opus_enc_cfg = AS_OPUS_ENC_CONFIG();
|
||||
void* encoder_handle = nullptr;
|
||||
auto ret = esp_opus_enc_open(&opus_enc_cfg, sizeof(esp_opus_enc_config_t), &encoder_handle);
|
||||
if (encoder_handle == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", ret);
|
||||
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
|
||||
this_->wake_word_opus_.push_back(std::vector<uint8_t>());
|
||||
this_->wake_word_cv_.notify_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get frame size
|
||||
int frame_size = 0;
|
||||
int outbuf_size = 0;
|
||||
esp_opus_enc_get_frame_size(encoder_handle, &frame_size, &outbuf_size);
|
||||
frame_size = frame_size / sizeof(int16_t);
|
||||
|
||||
// Encode all PCM data
|
||||
int packets = 0;
|
||||
std::vector<int16_t> in_buffer;
|
||||
esp_audio_enc_in_frame_t in = {};
|
||||
esp_audio_enc_out_frame_t out = {};
|
||||
|
||||
for (auto& pcm: this_->wake_word_pcm_) {
|
||||
encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) {
|
||||
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
|
||||
this_->wake_word_opus_.emplace_back(std::move(opus));
|
||||
this_->wake_word_cv_.notify_all();
|
||||
});
|
||||
packets++;
|
||||
if (in_buffer.empty()) {
|
||||
in_buffer = std::move(pcm);
|
||||
} else {
|
||||
in_buffer.reserve(in_buffer.size() + pcm.size());
|
||||
in_buffer.insert(in_buffer.end(), pcm.begin(), pcm.end());
|
||||
}
|
||||
|
||||
while (in_buffer.size() >= frame_size) {
|
||||
std::vector<uint8_t> opus_buf(outbuf_size);
|
||||
in.buffer = (uint8_t *)(in_buffer.data());
|
||||
in.len = (uint32_t)(frame_size * sizeof(int16_t));
|
||||
out.buffer = opus_buf.data();
|
||||
out.len = outbuf_size;
|
||||
out.encoded_bytes = 0;
|
||||
|
||||
ret = esp_opus_enc_process(encoder_handle, &in, &out);
|
||||
if (ret == ESP_AUDIO_ERR_OK) {
|
||||
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
|
||||
this_->wake_word_opus_.emplace_back(opus_buf.data(), opus_buf.data() + out.encoded_bytes);
|
||||
this_->wake_word_cv_.notify_all();
|
||||
packets++;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to encode audio, error code: %d", ret);
|
||||
}
|
||||
|
||||
in_buffer.erase(in_buffer.begin(), in_buffer.begin() + frame_size);
|
||||
}
|
||||
}
|
||||
this_->wake_word_pcm_.clear();
|
||||
|
||||
// Close encoder
|
||||
esp_opus_enc_close(encoder_handle);
|
||||
auto end_time = esp_timer_get_time();
|
||||
ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000));
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ public:
|
||||
|
||||
private:
|
||||
srmodel_list_t *models_ = nullptr;
|
||||
esp_afe_sr_iface_t* afe_iface_ = nullptr;
|
||||
const esp_afe_sr_iface_t* afe_iface_ = nullptr;
|
||||
esp_afe_sr_data_t* afe_data_ = nullptr;
|
||||
char* wakenet_model_ = NULL;
|
||||
std::vector<std::string> wake_words_;
|
||||
|
||||
@ -9,10 +9,8 @@
|
||||
#include <esp_mn_speech_commands.h>
|
||||
#include <cJSON.h>
|
||||
|
||||
|
||||
#define TAG "CustomWakeWord"
|
||||
|
||||
|
||||
CustomWakeWord::CustomWakeWord()
|
||||
: wake_word_pcm_(), wake_word_opus_() {
|
||||
}
|
||||
@ -218,20 +216,56 @@ void CustomWakeWord::EncodeWakeWordData() {
|
||||
auto this_ = (CustomWakeWord*)arg;
|
||||
{
|
||||
auto start_time = esp_timer_get_time();
|
||||
auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
|
||||
encoder->SetComplexity(0); // 0 is the fastest
|
||||
|
||||
// Create encoder
|
||||
esp_opus_enc_config_t opus_enc_cfg = AS_OPUS_ENC_CONFIG();
|
||||
void* encoder_handle = nullptr;
|
||||
auto ret = esp_opus_enc_open(&opus_enc_cfg, sizeof(esp_opus_enc_config_t), &encoder_handle);
|
||||
if (encoder_handle == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", ret);
|
||||
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
|
||||
this_->wake_word_opus_.push_back(std::vector<uint8_t>());
|
||||
this_->wake_word_cv_.notify_all();
|
||||
return;
|
||||
}
|
||||
// Get frame size
|
||||
int frame_size = 0;
|
||||
int outbuf_size = 0;
|
||||
esp_opus_enc_get_frame_size(encoder_handle, &frame_size, &outbuf_size);
|
||||
frame_size = frame_size / sizeof(int16_t);
|
||||
// Encode all PCM data
|
||||
int packets = 0;
|
||||
std::vector<int16_t> in_buffer;
|
||||
esp_audio_enc_in_frame_t in = {};
|
||||
esp_audio_enc_out_frame_t out = {};
|
||||
for (auto& pcm: this_->wake_word_pcm_) {
|
||||
encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) {
|
||||
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
|
||||
this_->wake_word_opus_.emplace_back(std::move(opus));
|
||||
this_->wake_word_cv_.notify_all();
|
||||
});
|
||||
packets++;
|
||||
if (in_buffer.empty()) {
|
||||
in_buffer = std::move(pcm);
|
||||
} else {
|
||||
in_buffer.reserve(in_buffer.size() + pcm.size());
|
||||
in_buffer.insert(in_buffer.end(), pcm.begin(), pcm.end());
|
||||
}
|
||||
while (in_buffer.size() >= frame_size) {
|
||||
std::vector<uint8_t> opus_buf(outbuf_size);
|
||||
in.buffer = (uint8_t *)(in_buffer.data());
|
||||
in.len = (uint32_t)(frame_size * sizeof(int16_t));
|
||||
out.buffer = opus_buf.data();
|
||||
out.len = outbuf_size;
|
||||
out.encoded_bytes = 0;
|
||||
ret = esp_opus_enc_process(encoder_handle, &in, &out);
|
||||
if (ret == ESP_AUDIO_ERR_OK) {
|
||||
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
|
||||
this_->wake_word_opus_.emplace_back(opus_buf.data(), opus_buf.data() + out.encoded_bytes);
|
||||
this_->wake_word_cv_.notify_all();
|
||||
packets++;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to encode audio, error code: %d", ret);
|
||||
}
|
||||
in_buffer.erase(in_buffer.begin(), in_buffer.begin() + frame_size);
|
||||
}
|
||||
}
|
||||
this_->wake_word_pcm_.clear();
|
||||
|
||||
// Close encoder
|
||||
esp_opus_enc_close(encoder_handle);
|
||||
auto end_time = esp_timer_get_time();
|
||||
ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000));
|
||||
|
||||
|
||||
41
main/boards/aipi-lite/README.md
Normal file
41
main/boards/aipi-lite/README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# 编译命令
|
||||
|
||||
## 一键编译
|
||||
|
||||
```bash
|
||||
python scripts/release.py aipi-lite
|
||||
```
|
||||
|
||||
## 手动配置编译
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32s3
|
||||
```
|
||||
|
||||
**配置**
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
选择板子
|
||||
|
||||
```
|
||||
Xiaozhi Assistant -> Board Type -> AIPI-Lite
|
||||
```
|
||||
|
||||
## 编译烧入
|
||||
|
||||
```bash
|
||||
idf.py -DBOARD_NAME=aipi-lite build flash
|
||||
```
|
||||
|
||||
注意: 如果当前设备出货之前是AiPi-Lite 固件(非小智版本),请特别小心处理闪存固件分区地址,以避免错误擦除 AiPi-Lite 的自身设备信息(EUI 等),否则设备即使恢复成Xorigin固件也无法正确连接到 服务器!所以在刷写固件之前,请务必记录设备的相关必要信息,以确保有恢复的方法!
|
||||
|
||||
您可以使用以下命令备份生产信息
|
||||
|
||||
```bash
|
||||
# firstly backup the factory information partition which contains the credentials for connecting the SenseCraft server
|
||||
esptool.py --chip esp32s3 --baud 2000000 --before default_reset --after hard_reset --no-stub read_flash 0x9000 16384 nvsfactory.bin
|
||||
|
||||
```
|
||||
40
main/boards/aipi-lite/README_en.md
Normal file
40
main/boards/aipi-lite/README_en.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Build Instructions
|
||||
|
||||
## One-click Build
|
||||
|
||||
```bash
|
||||
python scripts/release.py aipi-lite -c config_en.json
|
||||
```
|
||||
|
||||
## Manual Configuration and Build
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32s3
|
||||
```
|
||||
|
||||
**Configuration**
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
Select the board:
|
||||
|
||||
```
|
||||
Xiaozhi Assistant -> Board Type -> AiPi-Lite
|
||||
```
|
||||
|
||||
## Build and Flash
|
||||
|
||||
```bash
|
||||
idf.py -DBOARD_NAME=aipi-lite build flash
|
||||
```
|
||||
|
||||
Note: If your device was previously shipped with the AiPi-Lite firmware (not the Xiaozhi version), please be very careful with the flash partition addresses to avoid accidentally erasing the device information (such as EUI) of the AiPi-Lite. Otherwise, even if you restore the AiPi-Lite firmware, the device may not be able to connect to the Xorigin server correctly! Therefore, before flashing the firmware, be sure to record the necessary device information to ensure you have a way to recover it!
|
||||
|
||||
You can use the following command to back up the factory information:
|
||||
|
||||
```bash
|
||||
# Firstly backup the factory information partition which contains the credentials for connecting the SenseCraft server
|
||||
esptool.py --chip esp32s3 --baud 2000000 --before default_reset --after hard_reset --no-stub read_flash 0x9000 16384 nvsfactory.bin
|
||||
```
|
||||
246
main/boards/aipi-lite/aipi-lite.cc
Normal file
246
main/boards/aipi-lite/aipi-lite.cc
Normal file
@ -0,0 +1,246 @@
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/rtc_io.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "config.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "led/single_led.h"
|
||||
#include "mcp_server.h"
|
||||
#include "power_manager.h"
|
||||
#include "power_save_timer.h"
|
||||
#include "system_reset.h"
|
||||
#include "wifi_board.h"
|
||||
|
||||
#define TAG "AIPI-Lite"
|
||||
|
||||
class AIPILite : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
Button boot_button_;
|
||||
Button power_button_;
|
||||
LcdDisplay* display_;
|
||||
PowerManager* power_manager_;
|
||||
PowerSaveTimer* power_save_timer_;
|
||||
esp_lcd_panel_handle_t panel_ = nullptr;
|
||||
|
||||
void InitializePowerManager() {
|
||||
power_manager_ = new PowerManager(POWER_CHARGE_DETECT_PIN);
|
||||
power_manager_->OnChargingStatusChanged([this](bool is_charging) {
|
||||
if (is_charging) {
|
||||
power_save_timer_->SetEnabled(false);
|
||||
} else {
|
||||
power_save_timer_->SetEnabled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void InitializePowerSaveTimer() {
|
||||
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
|
||||
power_save_timer_->OnEnterSleepMode([this]() {
|
||||
GetDisplay()->SetPowerSaveMode(true);
|
||||
GetBacklight()->SetBrightness(1);
|
||||
});
|
||||
power_save_timer_->OnExitSleepMode([this]() {
|
||||
GetDisplay()->SetPowerSaveMode(false);
|
||||
GetBacklight()->RestoreBrightness();
|
||||
});
|
||||
power_save_timer_->OnShutdownRequest([this]() {
|
||||
ESP_LOGI(TAG, "Shutting down");
|
||||
esp_lcd_panel_disp_on_off(panel_, false); // 关闭显示
|
||||
rtc_gpio_set_level(POWER_CONTROL_PIN, 0);
|
||||
rtc_gpio_hold_dis(POWER_CONTROL_PIN);
|
||||
esp_deep_sleep_start();
|
||||
});
|
||||
power_save_timer_->SetEnabled(true);
|
||||
}
|
||||
|
||||
void InitializeI2c() {
|
||||
// Initialize I2C peripheral
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = (i2c_port_t)1,
|
||||
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
|
||||
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.intr_priority = 0,
|
||||
.trans_queue_depth = 0,
|
||||
.flags =
|
||||
{
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
|
||||
}
|
||||
|
||||
void InitializeSpi() {
|
||||
spi_bus_config_t buscfg = {};
|
||||
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
|
||||
buscfg.miso_io_num = GPIO_NUM_NC;
|
||||
buscfg.sclk_io_num = DISPLAY_SPI_SCLK_PIN;
|
||||
buscfg.quadwp_io_num = GPIO_NUM_NC;
|
||||
buscfg.quadhd_io_num = GPIO_NUM_NC;
|
||||
buscfg.max_transfer_sz =
|
||||
DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
|
||||
ESP_ERROR_CHECK(
|
||||
spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
|
||||
}
|
||||
|
||||
void InitializeLcdDisplay() {
|
||||
esp_lcd_panel_io_handle_t panel_io = nullptr;
|
||||
// 液晶屏控制IO初始化
|
||||
ESP_LOGD(TAG, "Install panel IO");
|
||||
esp_lcd_panel_io_spi_config_t io_config = {};
|
||||
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
|
||||
io_config.dc_gpio_num = DISPLAY_SPI_DC_PIN;
|
||||
io_config.spi_mode = DISPLAY_SPI_MODE;
|
||||
io_config.pclk_hz = 40 * 1000 * 1000;
|
||||
io_config.trans_queue_depth = 10;
|
||||
io_config.lcd_cmd_bits = 8;
|
||||
io_config.lcd_param_bits = 8;
|
||||
ESP_ERROR_CHECK(
|
||||
esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));
|
||||
|
||||
// 初始化液晶屏驱动芯片
|
||||
ESP_LOGD(TAG, "Install LCD driver");
|
||||
esp_lcd_panel_dev_config_t panel_config = {};
|
||||
panel_config.reset_gpio_num = DISPLAY_SPI_RESET_PIN;
|
||||
panel_config.rgb_ele_order = DISPLAY_RGB_ORDER;
|
||||
panel_config.bits_per_pixel = 16;
|
||||
ESP_ERROR_CHECK(
|
||||
esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel_));
|
||||
|
||||
esp_lcd_panel_reset(panel_);
|
||||
|
||||
esp_lcd_panel_init(panel_);
|
||||
esp_lcd_panel_invert_color(panel_, DISPLAY_INVERT_COLOR);
|
||||
esp_lcd_panel_swap_xy(panel_, DISPLAY_SWAP_XY);
|
||||
esp_lcd_panel_mirror(panel_, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
|
||||
display_ = new SpiLcdDisplay(panel_io, panel_, DISPLAY_WIDTH,
|
||||
DISPLAY_HEIGHT, DISPLAY_OFFSET_X,
|
||||
DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X,
|
||||
DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
power_save_timer_->WakeUp();
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
// 设置开机按钮的长按事件(直接进入配网模式)
|
||||
boot_button_.OnLongPress([this]() {
|
||||
// 唤醒电源保存定时器
|
||||
power_save_timer_->WakeUp();
|
||||
// 获取应用程序实例
|
||||
auto& app = Application::GetInstance();
|
||||
|
||||
// 进入配网模式
|
||||
app.SetDeviceState(kDeviceStateWifiConfiguring);
|
||||
|
||||
// 重置WiFi配置以确保进入配网模式
|
||||
EnterWifiConfigMode();
|
||||
});
|
||||
|
||||
power_button_.OnClick([this]() { power_save_timer_->WakeUp(); });
|
||||
power_button_.OnLongPress([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() != kDeviceStateStarting &&
|
||||
!(power_manager_->IsCharging() &&
|
||||
power_manager_->GetBatteryLevel() < 100)) {
|
||||
ESP_LOGI(TAG, "Power button long pressed, shutting down");
|
||||
esp_lcd_panel_disp_on_off(panel_, false); // 关闭显示
|
||||
rtc_gpio_set_level(POWER_CONTROL_PIN, 0);
|
||||
rtc_gpio_hold_dis(POWER_CONTROL_PIN);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void InitializePowerCtl() {
|
||||
ESP_LOGI(TAG, "Initialize Power Control GPIO");
|
||||
rtc_gpio_init(POWER_CONTROL_PIN);
|
||||
rtc_gpio_set_direction(POWER_CONTROL_PIN, RTC_GPIO_MODE_OUTPUT_ONLY);
|
||||
rtc_gpio_set_level(POWER_CONTROL_PIN, 1);
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeTools() {}
|
||||
|
||||
public:
|
||||
AIPILite()
|
||||
: boot_button_(BOOT_BUTTON_GPIO), power_button_(POWER_BUTTON_GPIO) {
|
||||
InitializePowerCtl();
|
||||
InitializePowerManager();
|
||||
InitializePowerSaveTimer();
|
||||
InitializeI2c();
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializeTools();
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
static SingleLed led(BUILTIN_LED_GPIO);
|
||||
return &led;
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static Es8311AudioCodec audio_codec(
|
||||
i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE,
|
||||
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK,
|
||||
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN,
|
||||
AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR, false);
|
||||
return &audio_codec;
|
||||
}
|
||||
|
||||
virtual Display* GetDisplay() override { return display_; }
|
||||
|
||||
virtual Backlight* GetBacklight() override {
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN,
|
||||
DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
return &backlight;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
virtual bool GetBatteryLevel(int& level, bool& charging,
|
||||
bool& discharging) override {
|
||||
static bool last_discharging = false;
|
||||
charging = power_manager_->IsCharging();
|
||||
discharging = power_manager_->IsDischarging();
|
||||
if (discharging != last_discharging) {
|
||||
power_save_timer_->SetEnabled(discharging);
|
||||
last_discharging = discharging;
|
||||
}
|
||||
level = power_manager_->GetBatteryLevel();
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
|
||||
if (level != PowerSaveLevel::LOW_POWER) {
|
||||
power_save_timer_->WakeUp();
|
||||
}
|
||||
WifiBoard::SetPowerSaveLevel(level);
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(AIPILite);
|
||||
53
main/boards/aipi-lite/config.h
Normal file
53
main/boards/aipi-lite/config.h
Normal file
@ -0,0 +1,53 @@
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
// aipi-lite configuration
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define AUDIO_INPUT_SAMPLE_RATE 24000
|
||||
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
|
||||
|
||||
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_6 // MCLK
|
||||
#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 // LRCK
|
||||
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14 // SCLK
|
||||
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_13 // DIN
|
||||
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 // DOUT
|
||||
|
||||
#define AUDIO_CODEC_PA_PIN GPIO_NUM_9
|
||||
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_5
|
||||
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_4
|
||||
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
|
||||
|
||||
#define BUILTIN_LED_GPIO GPIO_NUM_46
|
||||
#define BOOT_BUTTON_GPIO GPIO_NUM_42
|
||||
|
||||
#define DISPLAY_WIDTH 128
|
||||
#define DISPLAY_HEIGHT 128
|
||||
#define DISPLAY_MIRROR_X true
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
#define DISPLAY_SWAP_XY true
|
||||
#define DISPLAY_INVERT_COLOR false
|
||||
#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR
|
||||
|
||||
#define DISPLAY_OFFSET_X 0
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
|
||||
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_3
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
|
||||
|
||||
#define DISPLAY_SPI_SCLK_PIN GPIO_NUM_16
|
||||
#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_17
|
||||
#define DISPLAY_SPI_CS_PIN GPIO_NUM_15
|
||||
#define DISPLAY_SPI_DC_PIN GPIO_NUM_7
|
||||
#define DISPLAY_SPI_RESET_PIN GPIO_NUM_18
|
||||
#define DISPLAY_SPI_MODE 0
|
||||
#define DISPLAY_SPI_SCLK_HZ (20 * 1000 * 1000)
|
||||
|
||||
#define POWER_BUTTON_GPIO GPIO_NUM_1
|
||||
#define POWER_CONTROL_PIN GPIO_NUM_10
|
||||
#define POWER_CHARGE_DETECT_PIN GPIO_NUM_8
|
||||
#define POWER_ADC_UNIT ADC_UNIT_1
|
||||
#define POWER_ADC_CHANNEL ADC_CHANNEL_1
|
||||
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
12
main/boards/aipi-lite/config.json
Normal file
12
main/boards/aipi-lite/config.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "aipi-lite",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
17
main/boards/aipi-lite/config_en.json
Normal file
17
main/boards/aipi-lite/config_en.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "aipi-lite_en",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"",
|
||||
"CONFIG_LANGUAGE_EN_US=y",
|
||||
"CONFIG_SR_WN_WN9_NIHAOXIAOZHI_TTS=n",
|
||||
"CONFIG_SR_WN_WN9_JARVIS_TTS=y",
|
||||
"CONFIG_SR_WN_WN9_SOPHIA_TTS=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
187
main/boards/aipi-lite/power_manager.h
Normal file
187
main/boards/aipi-lite/power_manager.h
Normal file
@ -0,0 +1,187 @@
|
||||
#pragma once
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_adc/adc_oneshot.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
class PowerManager {
|
||||
private:
|
||||
esp_timer_handle_t timer_handle_;
|
||||
std::function<void(bool)> on_charging_status_changed_;
|
||||
std::function<void(bool)> on_low_battery_status_changed_;
|
||||
|
||||
gpio_num_t charging_pin_ = POWER_CHARGE_DETECT_PIN;
|
||||
std::vector<uint16_t> adc_values_;
|
||||
uint32_t battery_level_ = 0;
|
||||
bool is_charging_ = false;
|
||||
bool is_low_battery_ = false;
|
||||
int ticks_ = 0;
|
||||
const int kBatteryAdcInterval = 60;
|
||||
const int kBatteryAdcDataCount = 3;
|
||||
const int kLowBatteryLevel = 20;
|
||||
|
||||
adc_oneshot_unit_handle_t adc_handle_;
|
||||
|
||||
void CheckBatteryStatus() {
|
||||
// Get charging status
|
||||
bool new_charging_status = gpio_get_level(charging_pin_) == 1;
|
||||
if (new_charging_status != is_charging_) {
|
||||
is_charging_ = new_charging_status;
|
||||
if (on_charging_status_changed_) {
|
||||
on_charging_status_changed_(is_charging_);
|
||||
}
|
||||
ReadBatteryAdcData();
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果电池电量数据不足,则读取电池电量数据
|
||||
if (adc_values_.size() < kBatteryAdcDataCount) {
|
||||
ReadBatteryAdcData();
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick
|
||||
// 读取一次电池电量数据
|
||||
ticks_++;
|
||||
if (ticks_ % kBatteryAdcInterval == 0) {
|
||||
ReadBatteryAdcData();
|
||||
}
|
||||
}
|
||||
|
||||
void ReadBatteryAdcData() {
|
||||
int adc_value;
|
||||
ESP_ERROR_CHECK(
|
||||
adc_oneshot_read(adc_handle_, POWER_ADC_CHANNEL, &adc_value));
|
||||
|
||||
// 将 ADC 值添加到队列中
|
||||
adc_values_.push_back(adc_value);
|
||||
if (adc_values_.size() > kBatteryAdcDataCount) {
|
||||
adc_values_.erase(adc_values_.begin());
|
||||
}
|
||||
uint32_t average_adc = 0;
|
||||
for (auto value : adc_values_) {
|
||||
average_adc += value;
|
||||
}
|
||||
average_adc /= adc_values_.size();
|
||||
|
||||
// 定义电池电量区间
|
||||
const struct {
|
||||
uint16_t adc;
|
||||
uint8_t level;
|
||||
} levels[] = {{1480, 0}, {1581, 20}, {1663, 40},
|
||||
{1750, 60}, {1840, 80}, {1980, 100}};
|
||||
|
||||
// 低于最低值时
|
||||
if (average_adc < levels[0].adc) {
|
||||
battery_level_ = 0;
|
||||
}
|
||||
// 高于最高值时
|
||||
else if (average_adc >= levels[5].adc) {
|
||||
battery_level_ = 100;
|
||||
} else {
|
||||
// 线性插值计算中间值
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (average_adc >= levels[i].adc &&
|
||||
average_adc < levels[i + 1].adc) {
|
||||
float ratio =
|
||||
static_cast<float>(average_adc - levels[i].adc) /
|
||||
(levels[i + 1].adc - levels[i].adc);
|
||||
battery_level_ =
|
||||
levels[i].level +
|
||||
ratio * (levels[i + 1].level - levels[i].level);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check low battery status
|
||||
if (adc_values_.size() >= kBatteryAdcDataCount) {
|
||||
bool new_low_battery_status = battery_level_ <= kLowBatteryLevel;
|
||||
if (new_low_battery_status != is_low_battery_) {
|
||||
is_low_battery_ = new_low_battery_status;
|
||||
if (on_low_battery_status_changed_) {
|
||||
on_low_battery_status_changed_(is_low_battery_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld",
|
||||
adc_value, average_adc, battery_level_);
|
||||
}
|
||||
|
||||
public:
|
||||
PowerManager(gpio_num_t pin) : charging_pin_(pin) {
|
||||
// 初始化充电引脚
|
||||
gpio_config_t io_conf = {};
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_INPUT;
|
||||
io_conf.pin_bit_mask = (1ULL << charging_pin_);
|
||||
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
// 创建电池电量检查定时器
|
||||
esp_timer_create_args_t timer_args = {
|
||||
.callback =
|
||||
[](void* arg) {
|
||||
PowerManager* self = static_cast<PowerManager*>(arg);
|
||||
self->CheckBatteryStatus();
|
||||
},
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "battery_check_timer",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 100000));
|
||||
|
||||
// 初始化 ADC
|
||||
adc_oneshot_unit_init_cfg_t init_config = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
.ulp_mode = ADC_ULP_MODE_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_));
|
||||
|
||||
adc_oneshot_chan_cfg_t chan_config = {
|
||||
.atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_12,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_oneshot_config_channel(
|
||||
adc_handle_, POWER_ADC_CHANNEL, &chan_config));
|
||||
}
|
||||
|
||||
~PowerManager() {
|
||||
if (timer_handle_) {
|
||||
esp_timer_stop(timer_handle_);
|
||||
esp_timer_delete(timer_handle_);
|
||||
}
|
||||
if (adc_handle_) {
|
||||
adc_oneshot_del_unit(adc_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsCharging() {
|
||||
// 如果电量已经满了,则不再显示充电中
|
||||
if (battery_level_ == 100) {
|
||||
return false;
|
||||
}
|
||||
return is_charging_;
|
||||
}
|
||||
|
||||
bool IsDischarging() {
|
||||
// 没有区分充电和放电,所以直接返回相反状态
|
||||
return !is_charging_;
|
||||
}
|
||||
|
||||
uint8_t GetBatteryLevel() { return battery_level_; }
|
||||
|
||||
void OnLowBatteryStatusChanged(std::function<void(bool)> callback) {
|
||||
on_low_battery_status_changed_ = callback;
|
||||
}
|
||||
|
||||
void OnChargingStatusChanged(std::function<void(bool)> callback) {
|
||||
on_charging_status_changed_ = callback;
|
||||
}
|
||||
};
|
||||
@ -8,7 +8,6 @@
|
||||
#include "led/single_led.h"
|
||||
#include "i2c_device.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
@ -244,8 +243,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
#include "i2c_device.h"
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#include <driver/rtc_io.h>
|
||||
#include <esp_sleep.h>
|
||||
@ -227,8 +226,9 @@ private:
|
||||
|
||||
middle_button_.OnLongPress([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (app.GetDeviceState() != kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
|
||||
@ -377,11 +377,11 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void SetPowerSaveMode(bool enabled) override {
|
||||
if (!enabled) {
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
|
||||
if (level != PowerSaveLevel::LOW_POWER) {
|
||||
power_save_timer_->WakeUp();
|
||||
}
|
||||
WifiBoard::SetPowerSaveMode(enabled);
|
||||
WifiBoard::SetPowerSaveLevel(level);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
#include "i2c_device.h"
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#include <driver/rtc_io.h>
|
||||
#include <esp_sleep.h>
|
||||
@ -280,9 +279,10 @@ private:
|
||||
|
||||
auto& app = Application::GetInstance();
|
||||
if (self->GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
auto& wifi_board = static_cast<WifiBoard&>(self->GetCurrentBoard());
|
||||
wifi_board.ResetWifiConfiguration();
|
||||
wifi_board.EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -463,11 +463,11 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void SetPowerSaveMode(bool enabled) override {
|
||||
if (!enabled) {
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
|
||||
if (level != PowerSaveLevel::LOW_POWER) {
|
||||
power_save_timer_->WakeUp();
|
||||
}
|
||||
DualNetworkBoard::SetPowerSaveMode(enabled);
|
||||
DualNetworkBoard::SetPowerSaveLevel(level);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
#include "i2c_device.h"
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#include <driver/rtc_io.h>
|
||||
#include <esp_sleep.h>
|
||||
@ -262,8 +261,9 @@ private:
|
||||
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
|
||||
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
self->ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
self->EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->power_status_ == kDeviceBatterySupply) {
|
||||
@ -442,11 +442,11 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void SetPowerSaveMode(bool enabled) override {
|
||||
if (!enabled) {
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
|
||||
if (level != PowerSaveLevel::LOW_POWER) {
|
||||
power_save_timer_->WakeUp();
|
||||
}
|
||||
WifiBoard::SetPowerSaveMode(enabled);
|
||||
WifiBoard::SetPowerSaveLevel(level);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#define TAG "atk_dnesp32s3"
|
||||
|
||||
@ -87,8 +86,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -60,8 +59,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#define TAG "AtomEchoS3R"
|
||||
|
||||
@ -57,8 +56,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <wifi_station.h>
|
||||
#include "led/circular_strip.h"
|
||||
|
||||
#define TAG "XX+EchoBase"
|
||||
@ -91,8 +90,9 @@ private:
|
||||
|
||||
ESP_LOGI(TAG, " ===>>> face_button_.OnClick ");
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <wifi_station.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_gc9a01.h>
|
||||
@ -180,8 +179,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <wifi_station.h>
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#define TAG "AtomS3R CAM/M12 + EchoBase"
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <wifi_station.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_gc9a01.h>
|
||||
@ -258,8 +257,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
#include "config.h"
|
||||
#include "led/single_led.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -138,10 +137,11 @@ private:
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
// cast to WifiBoard
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.ResetWifiConfiguration();
|
||||
wifi_board.EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
gpio_set_level(BUILTIN_LED_GPIO, 1);
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
#include "led/single_led.h"
|
||||
#include "display/oled_display.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
@ -106,10 +105,11 @@ private:
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
// cast to WifiBoard
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.ResetWifiConfiguration();
|
||||
wifi_board.EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
gpio_set_level(BUILTIN_LED_GPIO, 1);
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#define TAG "CompactMl307Board"
|
||||
|
||||
@ -96,10 +95,11 @@ private:
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
// cast to WifiBoard
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.ResetWifiConfiguration();
|
||||
wifi_board.EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
app.ToggleChatState();
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
#include "lamp_controller.h"
|
||||
#include "led/single_led.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -126,8 +125,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
#include "led/single_led.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -173,8 +172,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
#include "led/single_led.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
@ -104,8 +103,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include "esp_log.h"
|
||||
#include "display.h"
|
||||
#include "ssid_manager.h"
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
@ -13,7 +14,7 @@ namespace audio_wifi_config
|
||||
static const char *kLogTag = "AUDIO_WIFI_CONFIG";
|
||||
|
||||
void ReceiveWifiCredentialsFromAudio(Application *app,
|
||||
WifiConfigurationAp *wifi_ap,
|
||||
WifiManager *wifi_manager,
|
||||
Display *display,
|
||||
size_t input_channels
|
||||
)
|
||||
@ -90,13 +91,16 @@ namespace audio_wifi_config
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wifi_ap->ConnectToWifi(wifi_ssid, wifi_password)) {
|
||||
wifi_ap->Save(wifi_ssid, wifi_password); // Save WiFi credentials
|
||||
esp_restart(); // Restart device to apply new WiFi configuration
|
||||
} else {
|
||||
ESP_LOGE(kLogTag, "Failed to connect to WiFi with received credentials");
|
||||
}
|
||||
// Save WiFi credentials using SsidManager
|
||||
auto& ssid_manager = SsidManager::GetInstance();
|
||||
ssid_manager.AddSsid(wifi_ssid, wifi_password);
|
||||
ESP_LOGI(kLogTag, "WiFi credentials saved successfully");
|
||||
|
||||
// Exit config mode (triggers ConfigModeExit event)
|
||||
wifi_manager->StopConfigAp();
|
||||
|
||||
data_buffer.decoded_text.reset(); // Clear processed data
|
||||
return; // Exit the function
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms delay
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include "wifi_configuration_ap.h"
|
||||
#include "wifi_manager.h"
|
||||
#include "application.h"
|
||||
|
||||
// Audio signal processing constants for WiFi configuration via audio
|
||||
@ -19,7 +19,7 @@ const size_t kWindowSize = 64;
|
||||
namespace audio_wifi_config
|
||||
{
|
||||
// Main function to receive WiFi credentials through audio signal
|
||||
void ReceiveWifiCredentialsFromAudio(Application *app, WifiConfigurationAp *wifi_ap, Display *display,
|
||||
void ReceiveWifiCredentialsFromAudio(Application *app, WifiManager *wifi_manager, Display *display,
|
||||
size_t input_channels = 1);
|
||||
|
||||
/**
|
||||
|
||||
777
main/boards/common/blufi.cpp
Normal file
777
main/boards/common/blufi.cpp
Normal file
@ -0,0 +1,777 @@
|
||||
#include "blufi.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "application.h"
|
||||
#include "esp_bt.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "wifi_manager.h"
|
||||
|
||||
// Bluedroid specific
|
||||
#ifdef CONFIG_BT_BLUEDROID_ENABLED
|
||||
#include "esp_bt_device.h"
|
||||
#include "esp_bt_main.h"
|
||||
#include "esp_gap_ble_api.h"
|
||||
#endif
|
||||
|
||||
// NimBLE specific
|
||||
#ifdef CONFIG_BT_NIMBLE_ENABLED
|
||||
#include "console/console.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
extern void esp_blufi_gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
|
||||
extern int esp_blufi_gatt_svr_init(void);
|
||||
extern void esp_blufi_gatt_svr_deinit(void);
|
||||
extern void esp_blufi_btc_init(void);
|
||||
extern void esp_blufi_btc_deinit(void);
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
// Blufi Advertising & Connection
|
||||
void esp_blufi_adv_start(void);
|
||||
|
||||
void esp_blufi_adv_stop(void);
|
||||
|
||||
void esp_blufi_disconnect(void);
|
||||
|
||||
// Internal BTC layer functions needed for error reporting
|
||||
void btc_blufi_report_error(esp_blufi_error_state_t state);
|
||||
|
||||
// Bluedroid specific GAP event handler
|
||||
#ifdef CONFIG_BT_BLUEDROID_ENABLED
|
||||
void esp_blufi_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||||
#endif
|
||||
|
||||
// NimBLE specific internal functions
|
||||
#ifdef CONFIG_BT_NIMBLE_ENABLED
|
||||
void esp_blufi_gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
|
||||
int esp_blufi_gatt_svr_init(void);
|
||||
void esp_blufi_gatt_svr_deinit(void);
|
||||
void esp_blufi_btc_init(void);
|
||||
void esp_blufi_btc_deinit(void);
|
||||
#endif
|
||||
}
|
||||
|
||||
// mbedTLS for security
|
||||
#include <wifi_station.h>
|
||||
#include "esp_crc.h"
|
||||
#include "esp_random.h"
|
||||
#include "mbedtls/md5.h"
|
||||
#include "ssid_manager.h"
|
||||
|
||||
// Logging Tag
|
||||
static const char *BLUFI_TAG = "BLUFI_CLASS";
|
||||
|
||||
static wifi_mode_t GetWifiModeWithFallback(const WifiManager &wifi) {
|
||||
if (wifi.IsConfigMode()) {
|
||||
return WIFI_MODE_AP;
|
||||
}
|
||||
if (wifi.IsInitialized() && wifi.IsConnected()) {
|
||||
return WIFI_MODE_STA;
|
||||
}
|
||||
|
||||
wifi_mode_t mode = WIFI_MODE_STA;
|
||||
esp_wifi_get_mode(&mode);
|
||||
return mode;
|
||||
}
|
||||
|
||||
Blufi &Blufi::GetInstance() {
|
||||
static Blufi instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Blufi::Blufi()
|
||||
: m_sec(nullptr),
|
||||
m_ble_is_connected(false),
|
||||
m_sta_connected(false),
|
||||
m_sta_got_ip(false),
|
||||
m_provisioned(false),
|
||||
m_deinited(false),
|
||||
m_sta_ssid_len(0),
|
||||
m_sta_is_connecting(false) {
|
||||
// Initialize member variables
|
||||
memset(&m_sta_config, 0, sizeof(m_sta_config));
|
||||
memset(&m_ap_config, 0, sizeof(m_ap_config));
|
||||
memset(m_sta_bssid, 0, sizeof(m_sta_bssid));
|
||||
memset(m_sta_ssid, 0, sizeof(m_sta_ssid));
|
||||
memset(&m_sta_conn_info, 0, sizeof(m_sta_conn_info));
|
||||
}
|
||||
|
||||
Blufi::~Blufi() {
|
||||
if (m_sec) {
|
||||
_security_deinit();
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t Blufi::init() {
|
||||
esp_err_t ret;
|
||||
inited_ = true;
|
||||
m_provisioned = false;
|
||||
m_deinited = false;
|
||||
|
||||
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
|
||||
ret = _controller_init();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "BLUFI controller init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
ret = _host_and_cb_init();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "BLUFI host and cb init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI VERSION %04x", esp_blufi_get_version());
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t Blufi::deinit() {
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
if (inited_) {
|
||||
if (m_deinited) {
|
||||
return ESP_OK;
|
||||
}
|
||||
m_deinited = true;
|
||||
ret = _host_deinit();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "Host deinit failed: %s", esp_err_to_name(ret));
|
||||
}
|
||||
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
|
||||
ret = _controller_deinit();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "Controller deinit failed: %s", esp_err_to_name(ret));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_BT_BLUEDROID_ENABLED
|
||||
esp_err_t Blufi::_host_init() {
|
||||
esp_err_t ret = esp_bluedroid_init();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s init bluedroid failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ret = esp_bluedroid_enable();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s enable bluedroid failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ESP_LOGI(BLUFI_TAG, "BD ADDR: " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(esp_bt_dev_get_address()));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_host_deinit() {
|
||||
esp_err_t ret = esp_blufi_profile_deinit();
|
||||
if (ret != ESP_OK)
|
||||
return ret;
|
||||
|
||||
ret = esp_bluedroid_disable();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s disable bluedroid failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ret = esp_bluedroid_deinit();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s deinit bluedroid failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_gap_register_callback() {
|
||||
esp_err_t rc = esp_ble_gap_register_callback(esp_blufi_gap_event_handler);
|
||||
if (rc) {
|
||||
return rc;
|
||||
}
|
||||
return esp_blufi_profile_init();
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_host_and_cb_init() {
|
||||
static esp_blufi_callbacks_t blufi_callbacks = {
|
||||
.event_cb = &_event_callback_trampoline,
|
||||
.negotiate_data_handler = &_negotiate_data_handler_trampoline,
|
||||
.encrypt_func = &_encrypt_func_trampoline,
|
||||
.decrypt_func = &_decrypt_func_trampoline,
|
||||
.checksum_func = &_checksum_func_trampoline,
|
||||
};
|
||||
|
||||
esp_err_t ret = _host_init();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s initialise host failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
ret = esp_blufi_register_callbacks(&blufi_callbacks);
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s blufi register failed, error code = %x", __func__, ret);
|
||||
return ret;
|
||||
}
|
||||
ret = _gap_register_callback();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s gap register failed, error code = %x", __func__, ret);
|
||||
return ret;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif /* CONFIG_BT_BLUEDROID_ENABLED */
|
||||
|
||||
#ifdef CONFIG_BT_NIMBLE_ENABLED
|
||||
// Stubs for NimBLE specific store functionality
|
||||
void ble_store_config_init();
|
||||
|
||||
void Blufi::_nimble_on_reset(int reason) {
|
||||
ESP_LOGE(BLUFI_TAG, "NimBLE Resetting state; reason=%d", reason);
|
||||
}
|
||||
|
||||
void Blufi::_nimble_on_sync() {
|
||||
// This is called when the host and controller are synced.
|
||||
// It's a good place to initialize the Blufi profile.
|
||||
esp_blufi_profile_init();
|
||||
}
|
||||
|
||||
void Blufi::_nimble_host_task(void *param) {
|
||||
ESP_LOGI(BLUFI_TAG, "BLE Host Task Started");
|
||||
nimble_port_run(); // This function will return only when nimble_port_stop() is executed
|
||||
nimble_port_freertos_deinit();
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_host_init() {
|
||||
// esp_nimble_init() is called by controller_init for NimBLE
|
||||
ble_hs_cfg.reset_cb = _nimble_on_reset;
|
||||
ble_hs_cfg.sync_cb = _nimble_on_sync;
|
||||
ble_hs_cfg.gatts_register_cb = esp_blufi_gatt_svr_register_cb;
|
||||
|
||||
// Security Manager settings (can be customized)
|
||||
ble_hs_cfg.sm_io_cap = 4; // IO capability: No Input, No Output
|
||||
#ifdef CONFIG_EXAMPLE_BONDING
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
#endif
|
||||
|
||||
int rc = esp_blufi_gatt_svr_init();
|
||||
assert(rc == 0);
|
||||
|
||||
ble_store_config_init(); // Configure the BLE storage
|
||||
esp_blufi_btc_init();
|
||||
|
||||
esp_err_t err = esp_nimble_enable(_nimble_host_task);
|
||||
if (err) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s failed: %s", __func__, esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_host_deinit(void) {
|
||||
esp_err_t ret = nimble_port_stop();
|
||||
if (ret == ESP_OK) {
|
||||
esp_nimble_deinit();
|
||||
}
|
||||
esp_blufi_gatt_svr_deinit();
|
||||
ret = esp_blufi_profile_deinit();
|
||||
esp_blufi_btc_deinit();
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_gap_register_callback(void) {
|
||||
return ESP_OK; // For NimBLE, GAP callbacks are handled differently
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_host_and_cb_init() {
|
||||
static esp_blufi_callbacks_t blufi_callbacks = {
|
||||
.event_cb = &_event_callback_trampoline,
|
||||
.negotiate_data_handler = &_negotiate_data_handler_trampoline,
|
||||
.encrypt_func = &_encrypt_func_trampoline,
|
||||
.decrypt_func = &_decrypt_func_trampoline,
|
||||
.checksum_func = &_checksum_func_trampoline,
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_blufi_register_callbacks(&blufi_callbacks);
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s blufi register failed, error code = %x", __func__, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Host init must be called after registering callbacks for NimBLE
|
||||
ret = _host_init();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s initialise host failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif /* CONFIG_BT_NIMBLE_ENABLED */
|
||||
|
||||
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
|
||||
esp_err_t Blufi::_controller_init() {
|
||||
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
||||
esp_err_t ret = esp_bt_controller_init(&bt_cfg);
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_BT_NIMBLE_ENABLED
|
||||
// For NimBLE, host init needs to be done after controller init
|
||||
ret = esp_nimble_init();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "esp_nimble_init() failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t Blufi::_controller_deinit() {
|
||||
esp_err_t ret = esp_bt_controller_disable();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s disable controller failed: %s", __func__, esp_err_to_name(ret));
|
||||
}
|
||||
ret = esp_bt_controller_deinit();
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "%s deinit controller failed: %s", __func__, esp_err_to_name(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
#endif // Generic controller init
|
||||
|
||||
static int myrand(void *rng_state, unsigned char *output, size_t len) {
|
||||
esp_fill_random(output, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Blufi::_security_init() {
|
||||
m_sec = new BlufiSecurity();
|
||||
if (m_sec == nullptr) {
|
||||
ESP_LOGE(BLUFI_TAG, "Failed to allocate security context");
|
||||
return;
|
||||
}
|
||||
memset(m_sec, 0, sizeof(BlufiSecurity));
|
||||
m_sec->dhm = new mbedtls_dhm_context();
|
||||
m_sec->aes = new mbedtls_aes_context();
|
||||
|
||||
mbedtls_dhm_init(m_sec->dhm);
|
||||
mbedtls_aes_init(m_sec->aes);
|
||||
|
||||
memset(m_sec->iv, 0x0, sizeof(m_sec->iv));
|
||||
}
|
||||
|
||||
void Blufi::_security_deinit() {
|
||||
if (m_sec == nullptr)
|
||||
return;
|
||||
|
||||
if (m_sec->dh_param) {
|
||||
free(m_sec->dh_param);
|
||||
}
|
||||
mbedtls_dhm_free(m_sec->dhm);
|
||||
mbedtls_aes_free(m_sec->aes);
|
||||
delete m_sec->dhm;
|
||||
delete m_sec->aes;
|
||||
delete m_sec;
|
||||
m_sec = nullptr;
|
||||
}
|
||||
|
||||
void Blufi::_dh_negotiate_data_handler(uint8_t *data, int len, uint8_t **output_data,
|
||||
int *output_len, bool *need_free) {
|
||||
if (m_sec == nullptr) {
|
||||
ESP_LOGE(BLUFI_TAG, "Security not initialized in DH handler");
|
||||
btc_blufi_report_error(ESP_BLUFI_INIT_SECURITY_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (len < 1) {
|
||||
ESP_LOGE(BLUFI_TAG, "DH handler: data too short");
|
||||
btc_blufi_report_error(ESP_BLUFI_DATA_FORMAT_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t type = data[0];
|
||||
switch (type) {
|
||||
case 0x00: /* DH_PARAM_LEN */
|
||||
if (len < 3) {
|
||||
ESP_LOGE(BLUFI_TAG, "DH_PARAM_LEN packet too short");
|
||||
btc_blufi_report_error(ESP_BLUFI_DATA_FORMAT_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
m_sec->dh_param_len = (data[1] << 8) | data[2];
|
||||
if (m_sec->dh_param) {
|
||||
free(m_sec->dh_param);
|
||||
m_sec->dh_param = nullptr;
|
||||
}
|
||||
m_sec->dh_param = (uint8_t *)malloc(m_sec->dh_param_len);
|
||||
if (m_sec->dh_param == nullptr) {
|
||||
ESP_LOGE(BLUFI_TAG, "DH malloc failed");
|
||||
btc_blufi_report_error(ESP_BLUFI_DH_MALLOC_ERROR);
|
||||
}
|
||||
break;
|
||||
case 0x01: /* DH_PARAM_DATA */ {
|
||||
if (m_sec->dh_param == nullptr) {
|
||||
ESP_LOGE(BLUFI_TAG, "DH param not allocated");
|
||||
btc_blufi_report_error(ESP_BLUFI_DH_PARAM_ERROR);
|
||||
return;
|
||||
}
|
||||
uint8_t *param = m_sec->dh_param;
|
||||
memcpy(m_sec->dh_param, &data[1], m_sec->dh_param_len);
|
||||
int ret = mbedtls_dhm_read_params(m_sec->dhm, ¶m, ¶m[m_sec->dh_param_len]);
|
||||
if (ret) {
|
||||
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_read_params failed %d", ret);
|
||||
btc_blufi_report_error(ESP_BLUFI_READ_PARAM_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
const int dhm_len = mbedtls_dhm_get_len(m_sec->dhm);
|
||||
|
||||
ret = mbedtls_dhm_make_public(m_sec->dhm, dhm_len, m_sec->self_public_key, dhm_len,
|
||||
myrand, NULL);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_make_public failed: %d", ret);
|
||||
btc_blufi_report_error(ESP_BLUFI_MAKE_PUBLIC_ERROR);
|
||||
return;
|
||||
}
|
||||
ret = mbedtls_dhm_calc_secret(m_sec->dhm, m_sec->share_key, SHARE_KEY_LEN,
|
||||
&m_sec->share_len, myrand, NULL);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_calc_secret failed: %d", ret);
|
||||
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = mbedtls_md5(m_sec->share_key, m_sec->share_len, m_sec->psk);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(BLUFI_TAG, "mbedtls_md5 failed: %d", ret);
|
||||
btc_blufi_report_error(ESP_BLUFI_CALC_MD5_ERROR);
|
||||
return;
|
||||
}
|
||||
ret = mbedtls_aes_setkey_enc(m_sec->aes, m_sec->psk, PSK_LEN * 8);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(BLUFI_TAG, "mbedtls_aes_setkey_enc failed: -0x%04X", -ret);
|
||||
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
|
||||
return;
|
||||
}
|
||||
*output_data = m_sec->self_public_key;
|
||||
*output_len = dhm_len;
|
||||
*need_free = false;
|
||||
ESP_LOGI(BLUFI_TAG, "DH negotiation completed successfully");
|
||||
|
||||
free(m_sec->dh_param);
|
||||
m_sec->dh_param = nullptr;
|
||||
m_sec->dh_param_len = 0;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGE(BLUFI_TAG, "DH handler unknown type: %d", type);
|
||||
}
|
||||
}
|
||||
|
||||
int Blufi::_aes_encrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
|
||||
if (!m_sec || !m_sec->aes || !crypt_data || crypt_len <= 0) {
|
||||
ESP_LOGE(BLUFI_TAG, "Invalid parameters for AES encryption");
|
||||
return -ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
size_t iv_offset = 0;
|
||||
uint8_t iv0[16];
|
||||
memcpy(iv0, m_sec->iv, 16);
|
||||
iv0[0] = iv8;
|
||||
int ret = mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_ENCRYPT, crypt_len, &iv_offset, iv0,
|
||||
crypt_data, crypt_data);
|
||||
|
||||
if (ret == 0) {
|
||||
return crypt_len;
|
||||
} else {
|
||||
ESP_LOGE(BLUFI_TAG, "AES encrypt failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
int Blufi::_aes_decrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
|
||||
if (!m_sec || !m_sec->aes || !crypt_data || crypt_len < 0) {
|
||||
ESP_LOGE(BLUFI_TAG, "Invalid parameters for AES decryption %p %p %d", m_sec->aes,
|
||||
crypt_data, crypt_len);
|
||||
return -ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
size_t iv_offset = 0;
|
||||
uint8_t iv0[16];
|
||||
memcpy(iv0, m_sec->iv, 16);
|
||||
iv0[0] = iv8;
|
||||
int ret = mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_DECRYPT, crypt_len, &iv_offset, iv0,
|
||||
crypt_data, crypt_data);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(BLUFI_TAG, "AES decrypt failed: %d", ret);
|
||||
return ret;
|
||||
} else {
|
||||
return crypt_len;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t Blufi::_crc_checksum(uint8_t iv8, uint8_t *data, int len) {
|
||||
return esp_crc16_be(0, data, len);
|
||||
}
|
||||
|
||||
int Blufi::_get_softap_conn_num() {
|
||||
auto &wifi = WifiManager::GetInstance();
|
||||
if (!wifi.IsInitialized() || !wifi.IsConfigMode()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
wifi_sta_list_t sta_list{};
|
||||
if (esp_wifi_ap_get_sta_list(&sta_list) == ESP_OK) {
|
||||
return sta_list.num;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Blufi::_handle_event(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_BLUFI_EVENT_INIT_FINISH:
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI init finish");
|
||||
esp_blufi_adv_start();
|
||||
break;
|
||||
case ESP_BLUFI_EVENT_DEINIT_FINISH:
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI deinit finish");
|
||||
break;
|
||||
case ESP_BLUFI_EVENT_BLE_CONNECT:
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI ble connect");
|
||||
m_ble_is_connected = true;
|
||||
esp_blufi_adv_stop();
|
||||
_security_init();
|
||||
break;
|
||||
case ESP_BLUFI_EVENT_BLE_DISCONNECT:
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI ble disconnect");
|
||||
m_ble_is_connected = false;
|
||||
_security_deinit();
|
||||
if (!m_provisioned) {
|
||||
esp_blufi_adv_start();
|
||||
} else {
|
||||
esp_blufi_adv_stop();
|
||||
if (!m_deinited) {
|
||||
// Deinit BLE stack after provisioning completes to free resources.
|
||||
xTaskCreate(
|
||||
[](void *ctx) {
|
||||
static_cast<Blufi *>(ctx)->deinit();
|
||||
vTaskDelete(nullptr);
|
||||
},
|
||||
"blufi_deinit", 4096, this, 5, nullptr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ESP_BLUFI_EVENT_SET_WIFI_OPMODE: {
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI Set WIFI opmode %d", param->wifi_mode.op_mode);
|
||||
auto &wifi_manager = WifiManager::GetInstance();
|
||||
if (!wifi_manager.IsInitialized() && !wifi_manager.Initialize()) {
|
||||
ESP_LOGE(BLUFI_TAG, "Failed to initialize WifiManager for opmode change");
|
||||
break;
|
||||
}
|
||||
switch (param->wifi_mode.op_mode) {
|
||||
case WIFI_MODE_STA:
|
||||
wifi_manager.StartStation();
|
||||
break;
|
||||
case WIFI_MODE_AP:
|
||||
wifi_manager.StartConfigAp();
|
||||
break;
|
||||
case WIFI_MODE_APSTA:
|
||||
ESP_LOGW(BLUFI_TAG, "APSTA mode not supported, starting station only");
|
||||
wifi_manager.StartStation();
|
||||
break;
|
||||
default:
|
||||
wifi_manager.StopStation();
|
||||
wifi_manager.StopConfigAp();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP: {
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI request wifi connect to AP via esp-wifi-connect");
|
||||
std::string ssid(reinterpret_cast<const char *>(m_sta_config.sta.ssid));
|
||||
std::string password(reinterpret_cast<const char *>(m_sta_config.sta.password));
|
||||
|
||||
// Save credentials through SsidManager
|
||||
SsidManager::GetInstance().AddSsid(ssid, password);
|
||||
auto &wifi_manager = WifiManager::GetInstance();
|
||||
if (!wifi_manager.IsInitialized() && !wifi_manager.Initialize()) {
|
||||
ESP_LOGE(BLUFI_TAG, "Failed to initialize WifiManager");
|
||||
break;
|
||||
}
|
||||
|
||||
// Track SSID for BLUFI status reporting.
|
||||
m_sta_ssid_len = static_cast<int>(std::min(ssid.size(), sizeof(m_sta_ssid)));
|
||||
memcpy(m_sta_ssid, ssid.c_str(), m_sta_ssid_len);
|
||||
memset(m_sta_bssid, 0, sizeof(m_sta_bssid));
|
||||
m_sta_connected = false;
|
||||
m_sta_got_ip = false;
|
||||
m_sta_is_connecting = true;
|
||||
m_sta_conn_info = {}; // Reset connection info
|
||||
m_sta_conn_info.sta_ssid = m_sta_ssid;
|
||||
m_sta_conn_info.sta_ssid_len = m_sta_ssid_len;
|
||||
|
||||
wifi_manager.StartStation();
|
||||
|
||||
// Wait for connection in a separate task to avoid blocking the BLUFI handler.
|
||||
xTaskCreate(
|
||||
[](void *ctx) {
|
||||
auto *self = static_cast<Blufi *>(ctx);
|
||||
auto &wifi = WifiManager::GetInstance();
|
||||
constexpr int kConnectTimeoutMs = 10000; // 10s
|
||||
constexpr TickType_t kDelayTick = pdMS_TO_TICKS(200);
|
||||
int waited_ms = 0;
|
||||
|
||||
while (waited_ms < kConnectTimeoutMs && !wifi.IsConnected()) {
|
||||
vTaskDelay(kDelayTick);
|
||||
waited_ms += 200;
|
||||
}
|
||||
|
||||
wifi_mode_t mode = GetWifiModeWithFallback(wifi);
|
||||
const int softap_conn_num = _get_softap_conn_num();
|
||||
|
||||
if (wifi.IsConnected()) {
|
||||
self->m_sta_is_connecting = false;
|
||||
self->m_sta_connected = true;
|
||||
self->m_sta_got_ip = true;
|
||||
self->m_provisioned = true;
|
||||
|
||||
auto current_ssid = wifi.GetSsid();
|
||||
if (!current_ssid.empty()) {
|
||||
self->m_sta_ssid_len = static_cast<int>(
|
||||
std::min(current_ssid.size(), sizeof(self->m_sta_ssid)));
|
||||
memcpy(self->m_sta_ssid, current_ssid.c_str(), self->m_sta_ssid_len);
|
||||
}
|
||||
|
||||
wifi_ap_record_t ap_info{};
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
memcpy(self->m_sta_bssid, ap_info.bssid, sizeof(self->m_sta_bssid));
|
||||
}
|
||||
|
||||
esp_blufi_extra_info_t info = {};
|
||||
memcpy(info.sta_bssid, self->m_sta_bssid, sizeof(self->m_sta_bssid));
|
||||
info.sta_bssid_set = true;
|
||||
info.sta_ssid = self->m_sta_ssid;
|
||||
info.sta_ssid_len = self->m_sta_ssid_len;
|
||||
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_SUCCESS,
|
||||
softap_conn_num, &info);
|
||||
ESP_LOGI(BLUFI_TAG, "connected to WiFi");
|
||||
|
||||
// Close BluFi session after successful provisioning to free resources.
|
||||
if (self->m_ble_is_connected) {
|
||||
esp_blufi_disconnect();
|
||||
}
|
||||
} else {
|
||||
self->m_sta_is_connecting = false;
|
||||
self->m_sta_connected = false;
|
||||
self->m_sta_got_ip = false;
|
||||
|
||||
esp_blufi_extra_info_t info = {};
|
||||
info.sta_ssid = self->m_sta_ssid;
|
||||
info.sta_ssid_len = self->m_sta_ssid_len;
|
||||
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_FAIL,
|
||||
softap_conn_num, &info);
|
||||
ESP_LOGE(BLUFI_TAG, "Failed to connect to WiFi via esp-wifi-connect");
|
||||
}
|
||||
vTaskDelete(nullptr);
|
||||
},
|
||||
"blufi_wifi_conn", 4096, this, 5, nullptr);
|
||||
break;
|
||||
}
|
||||
case ESP_BLUFI_EVENT_REQ_DISCONNECT_FROM_AP:
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI request wifi disconnect from AP");
|
||||
if (WifiManager::GetInstance().IsInitialized()) {
|
||||
WifiManager::GetInstance().StopStation();
|
||||
}
|
||||
m_sta_is_connecting = false;
|
||||
m_sta_connected = false;
|
||||
m_sta_got_ip = false;
|
||||
break;
|
||||
case ESP_BLUFI_EVENT_GET_WIFI_STATUS: {
|
||||
auto &wifi = WifiManager::GetInstance();
|
||||
wifi_mode_t mode = GetWifiModeWithFallback(wifi);
|
||||
const int softap_conn_num = _get_softap_conn_num();
|
||||
|
||||
if (wifi.IsInitialized() && wifi.IsConnected()) {
|
||||
m_sta_connected = true;
|
||||
m_sta_got_ip = true;
|
||||
|
||||
auto current_ssid = wifi.GetSsid();
|
||||
if (!current_ssid.empty()) {
|
||||
m_sta_ssid_len =
|
||||
static_cast<int>(std::min(current_ssid.size(), sizeof(m_sta_ssid)));
|
||||
memcpy(m_sta_ssid, current_ssid.c_str(), m_sta_ssid_len);
|
||||
}
|
||||
|
||||
esp_blufi_extra_info_t info;
|
||||
memset(&info, 0, sizeof(esp_blufi_extra_info_t));
|
||||
memcpy(info.sta_bssid, m_sta_bssid, 6);
|
||||
info.sta_ssid = m_sta_ssid;
|
||||
info.sta_ssid_len = m_sta_ssid_len;
|
||||
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_SUCCESS, softap_conn_num,
|
||||
&info);
|
||||
} else if (m_sta_is_connecting) {
|
||||
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONNECTING, softap_conn_num,
|
||||
&m_sta_conn_info);
|
||||
} else {
|
||||
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_FAIL, softap_conn_num,
|
||||
&m_sta_conn_info);
|
||||
}
|
||||
ESP_LOGI(BLUFI_TAG, "BLUFI get wifi status");
|
||||
break;
|
||||
}
|
||||
case ESP_BLUFI_EVENT_RECV_STA_BSSID:
|
||||
memcpy(m_sta_config.sta.bssid, param->sta_bssid.bssid, 6);
|
||||
m_sta_config.sta.bssid_set = true;
|
||||
ESP_LOGI(BLUFI_TAG, "Recv STA BSSID");
|
||||
break;
|
||||
case ESP_BLUFI_EVENT_RECV_STA_SSID:
|
||||
strncpy((char *)m_sta_config.sta.ssid, (char *)param->sta_ssid.ssid,
|
||||
param->sta_ssid.ssid_len);
|
||||
m_sta_config.sta.ssid[param->sta_ssid.ssid_len] = '\0';
|
||||
ESP_LOGI(BLUFI_TAG, "Recv STA SSID: %s", m_sta_config.sta.ssid);
|
||||
break;
|
||||
case ESP_BLUFI_EVENT_RECV_STA_PASSWD:
|
||||
strncpy((char *)m_sta_config.sta.password, (char *)param->sta_passwd.passwd,
|
||||
param->sta_passwd.passwd_len);
|
||||
m_sta_config.sta.password[param->sta_passwd.passwd_len] = '\0';
|
||||
ESP_LOGI(BLUFI_TAG, "Recv STA PASSWORD : %s", m_sta_config.sta.password);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(BLUFI_TAG, "Unhandled event: %d", event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Blufi::_event_callback_trampoline(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param) {
|
||||
GetInstance()._handle_event(event, param);
|
||||
}
|
||||
|
||||
void Blufi::_negotiate_data_handler_trampoline(uint8_t *data, int len, uint8_t **output_data,
|
||||
int *output_len, bool *need_free) {
|
||||
GetInstance()._dh_negotiate_data_handler(data, len, output_data, output_len, need_free);
|
||||
}
|
||||
|
||||
int Blufi::_encrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
|
||||
return GetInstance()._aes_encrypt(iv8, crypt_data, crypt_len);
|
||||
}
|
||||
|
||||
int Blufi::_decrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
|
||||
return GetInstance()._aes_decrypt(iv8, crypt_data, crypt_len);
|
||||
}
|
||||
|
||||
uint16_t Blufi::_checksum_func_trampoline(uint8_t iv8, uint8_t *data, int len) {
|
||||
return _crc_checksum(iv8, data, len);
|
||||
}
|
||||
123
main/boards/common/blufi.h
Normal file
123
main/boards/common/blufi.h
Normal file
@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <aes/esp_aes.h>
|
||||
#include "mbedtls/dhm.h"
|
||||
#include "mbedtls/aes.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_blufi_api.h"
|
||||
#include "esp_wifi_types.h"
|
||||
|
||||
|
||||
class Blufi {
|
||||
public:
|
||||
/**
|
||||
* @brief Get the singleton instance of the Blufi class.
|
||||
*/
|
||||
static Blufi &GetInstance();
|
||||
|
||||
/**
|
||||
* @brief Initializes the Bluetooth controller, host, and Blufi profile.
|
||||
* This is the main entry point to start the Blufi process.
|
||||
* @return ESP_OK on success, otherwise an error code.
|
||||
*/
|
||||
esp_err_t init();
|
||||
|
||||
/**
|
||||
* @brief Deinitializes Blufi and the Bluetooth stack.
|
||||
* @return ESP_OK on success, otherwise an error code.
|
||||
*/
|
||||
esp_err_t deinit();
|
||||
|
||||
// Delete copy constructor and assignment operator for singleton
|
||||
Blufi(const Blufi &) = delete;
|
||||
|
||||
Blufi &operator=(const Blufi &) = delete;
|
||||
|
||||
private:
|
||||
bool inited_ = false;
|
||||
|
||||
Blufi();
|
||||
|
||||
~Blufi();
|
||||
|
||||
|
||||
// Initialization logic
|
||||
static esp_err_t _controller_init();
|
||||
|
||||
static esp_err_t _controller_deinit();
|
||||
|
||||
static esp_err_t _host_init();
|
||||
|
||||
static esp_err_t _host_deinit();
|
||||
|
||||
static esp_err_t _gap_register_callback();
|
||||
|
||||
static esp_err_t _host_and_cb_init();
|
||||
|
||||
void _security_init();
|
||||
|
||||
void _security_deinit();
|
||||
|
||||
void _dh_negotiate_data_handler(uint8_t *data, int len, uint8_t **output_data, int *output_len, bool *need_free);
|
||||
|
||||
int _aes_encrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
|
||||
|
||||
int _aes_decrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
|
||||
|
||||
static uint16_t _crc_checksum(uint8_t iv8, uint8_t *data, int len);
|
||||
|
||||
void _handle_event(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param);
|
||||
|
||||
static int _get_softap_conn_num();
|
||||
|
||||
// These C-style functions are registered with ESP-IDF and call the corresponding instance methods.
|
||||
|
||||
static void _event_callback_trampoline(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param);
|
||||
|
||||
static void _negotiate_data_handler_trampoline(uint8_t *data, int len, uint8_t **output_data, int *output_len,
|
||||
bool *need_free);
|
||||
|
||||
static int _encrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
|
||||
|
||||
static int _decrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
|
||||
|
||||
static uint16_t _checksum_func_trampoline(uint8_t iv8, uint8_t *data, int len);
|
||||
|
||||
#ifdef CONFIG_BT_NIMBLE_ENABLED
|
||||
static void _nimble_on_reset(int reason);
|
||||
static void _nimble_on_sync();
|
||||
static void _nimble_host_task(void *param);
|
||||
#endif
|
||||
|
||||
// Security context, formerly blufi_sec struct
|
||||
struct BlufiSecurity {
|
||||
#define DH_SELF_PUB_KEY_LEN 128
|
||||
uint8_t self_public_key[DH_SELF_PUB_KEY_LEN];
|
||||
#define SHARE_KEY_LEN 128
|
||||
uint8_t share_key[SHARE_KEY_LEN];
|
||||
size_t share_len;
|
||||
#define PSK_LEN 16
|
||||
uint8_t psk[PSK_LEN];
|
||||
uint8_t *dh_param;
|
||||
int dh_param_len;
|
||||
uint8_t iv[16];
|
||||
mbedtls_dhm_context *dhm;
|
||||
esp_aes_context *aes;
|
||||
};
|
||||
|
||||
BlufiSecurity *m_sec;
|
||||
|
||||
// State variables
|
||||
wifi_config_t m_sta_config{};
|
||||
wifi_config_t m_ap_config{};
|
||||
bool m_ble_is_connected;
|
||||
bool m_sta_connected;
|
||||
bool m_sta_got_ip;
|
||||
bool m_provisioned;
|
||||
bool m_deinited;
|
||||
uint8_t m_sta_bssid[6]{};
|
||||
uint8_t m_sta_ssid[32]{};
|
||||
int m_sta_ssid_len;
|
||||
bool m_sta_is_connecting;
|
||||
esp_blufi_extra_info_t m_sta_conn_info{};
|
||||
};
|
||||
@ -6,6 +6,7 @@
|
||||
#include <mqtt.h>
|
||||
#include <udp.h>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <network_interface.h>
|
||||
|
||||
#include "led/led.h"
|
||||
@ -13,6 +14,34 @@
|
||||
#include "camera.h"
|
||||
#include "assets.h"
|
||||
|
||||
/**
|
||||
* Network events for unified callback
|
||||
*/
|
||||
enum class NetworkEvent {
|
||||
Scanning, // Network is scanning (WiFi scanning, etc.)
|
||||
Connecting, // Network is connecting (data: SSID/network name)
|
||||
Connected, // Network connected successfully (data: SSID/network name)
|
||||
Disconnected, // Network disconnected
|
||||
WifiConfigModeEnter, // Entered WiFi configuration mode
|
||||
WifiConfigModeExit, // Exited WiFi configuration mode
|
||||
// Cellular modem specific events
|
||||
ModemDetecting, // Detecting modem (baud rate, module type)
|
||||
ModemErrorNoSim, // No SIM card detected
|
||||
ModemErrorRegDenied, // Network registration denied
|
||||
ModemErrorInitFailed, // Modem initialization failed
|
||||
ModemErrorTimeout // Operation timeout
|
||||
};
|
||||
|
||||
// Power save level enumeration
|
||||
enum class PowerSaveLevel {
|
||||
LOW_POWER, // Maximum power saving (lowest power consumption)
|
||||
BALANCED, // Medium power saving (balanced)
|
||||
PERFORMANCE, // No power saving (maximum power consumption / full performance)
|
||||
};
|
||||
|
||||
// Network event callback type (event, data)
|
||||
// data contains additional info like SSID for Connecting/Connected events
|
||||
using NetworkEventCallback = std::function<void(NetworkEvent event, const std::string& data)>;
|
||||
|
||||
void* create_board();
|
||||
class AudioCodec;
|
||||
@ -46,10 +75,11 @@ public:
|
||||
virtual Camera* GetCamera();
|
||||
virtual NetworkInterface* GetNetwork() = 0;
|
||||
virtual void StartNetwork() = 0;
|
||||
virtual void SetNetworkEventCallback(NetworkEventCallback callback) { (void)callback; }
|
||||
virtual const char* GetNetworkStateIcon() = 0;
|
||||
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging);
|
||||
virtual std::string GetSystemInfoJson();
|
||||
virtual void SetPowerSaveMode(bool enabled) = 0;
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) = 0;
|
||||
virtual std::string GetBoardJson() = 0;
|
||||
virtual std::string GetDeviceStatusJson() = 0;
|
||||
};
|
||||
|
||||
@ -72,6 +72,11 @@ void DualNetworkBoard::StartNetwork() {
|
||||
current_board_->StartNetwork();
|
||||
}
|
||||
|
||||
void DualNetworkBoard::SetNetworkEventCallback(NetworkEventCallback callback) {
|
||||
// Forward the callback to the current board
|
||||
current_board_->SetNetworkEventCallback(std::move(callback));
|
||||
}
|
||||
|
||||
NetworkInterface* DualNetworkBoard::GetNetwork() {
|
||||
return current_board_->GetNetwork();
|
||||
}
|
||||
@ -80,8 +85,8 @@ const char* DualNetworkBoard::GetNetworkStateIcon() {
|
||||
return current_board_->GetNetworkStateIcon();
|
||||
}
|
||||
|
||||
void DualNetworkBoard::SetPowerSaveMode(bool enabled) {
|
||||
current_board_->SetPowerSaveMode(enabled);
|
||||
void DualNetworkBoard::SetPowerSaveLevel(PowerSaveLevel level) {
|
||||
current_board_->SetPowerSaveLevel(level);
|
||||
}
|
||||
|
||||
std::string DualNetworkBoard::GetBoardJson() {
|
||||
|
||||
@ -49,9 +49,10 @@ public:
|
||||
// 重写Board接口
|
||||
virtual std::string GetBoardType() override;
|
||||
virtual void StartNetwork() override;
|
||||
virtual void SetNetworkEventCallback(NetworkEventCallback callback) override;
|
||||
virtual NetworkInterface* GetNetwork() override;
|
||||
virtual const char* GetNetworkStateIcon() override;
|
||||
virtual void SetPowerSaveMode(bool enabled) override;
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
|
||||
virtual std::string GetBoardJson() override;
|
||||
virtual std::string GetDeviceStatusJson() override;
|
||||
};
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
#include "esp32_camera.h"
|
||||
#include <fcntl.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/param.h>
|
||||
#include <unistd.h>
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
#include <errno.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "esp_imgfx_color_convert.h"
|
||||
#include "esp_video_device.h"
|
||||
#include "esp_video_init.h"
|
||||
#include "jpg/image_to_jpeg.h"
|
||||
#include "linux/videodev2.h"
|
||||
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
#include "esp32_camera.h"
|
||||
#include "esp_jpeg_common.h"
|
||||
#include "jpg/image_to_jpeg.h"
|
||||
#include "jpg/jpeg_to_image.h"
|
||||
#include "lvgl_display.h"
|
||||
#include "mcp_server.h"
|
||||
#include "system_info.h"
|
||||
@ -18,17 +27,36 @@
|
||||
#undef LOG_LOCAL_LEVEL
|
||||
#define LOG_LOCAL_LEVEL MAX(CONFIG_LOG_DEFAULT_LEVEL, ESP_LOG_DEBUG)
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
|
||||
#include <esp_log.h> // should be after LOCAL_LOG_LEVEL definition
|
||||
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32P4
|
||||
#include "driver/ppa.h"
|
||||
#if defined(CONFIG_XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_90)
|
||||
#define IMAGE_ROTATION_ANGLE (PPA_SRM_ROTATION_ANGLE_270)
|
||||
#elif defined(CONFIG_XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_270)
|
||||
#define IMAGE_ROTATION_ANGLE (PPA_SRM_ROTATION_ANGLE_90)
|
||||
#else
|
||||
#error "CONFIG_XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE is not set"
|
||||
#endif // angle
|
||||
#else // target
|
||||
#include "esp_imgfx_rotate.h"
|
||||
#if defined(CONFIG_XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_90)
|
||||
#define IMAGE_ROTATION_ANGLE (90)
|
||||
#elif defined(CONFIG_XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE_270)
|
||||
#define IMAGE_ROTATION_ANGLE (270)
|
||||
#else
|
||||
#error "CONFIG_XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE is not set"
|
||||
#endif // angle
|
||||
#endif // target
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
|
||||
#include <errno.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_log.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "Esp32Camera"
|
||||
|
||||
#if defined(CONFIG_CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER) || defined(CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP)
|
||||
#warning "CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER or CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP is enabled, which may cause image corruption in YUV422 format!"
|
||||
#warning \
|
||||
"CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER or CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP is enabled, which may cause image corruption in YUV422 format!"
|
||||
#endif
|
||||
|
||||
#if CONFIG_XIAOZHI_ENABLE_CAMERA_DEBUG_MODE
|
||||
@ -41,6 +69,16 @@
|
||||
fourcc[4] = '\0'; \
|
||||
ESP_LOGD(TAG, "FOURCC: '%c%c%c%c'", fourcc[0], fourcc[1], fourcc[2], fourcc[3]);
|
||||
|
||||
// for compatibility with old esp_video version
|
||||
#ifndef MAP_FAILED
|
||||
#define MAP_FAILED nullptr
|
||||
#endif
|
||||
|
||||
__attribute__((weak)) esp_err_t esp_video_deinit(void) {
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
// end of for compatibility with old esp_video version
|
||||
|
||||
static void log_available_video_devices() {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
char path[16];
|
||||
@ -92,7 +130,7 @@ Esp32Camera::Esp32Camera(const esp_video_init_config_t& config) {
|
||||
#endif
|
||||
#if CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
else if (config.usb_uvc != nullptr) {
|
||||
video_device_name = ESP_VIDEO_USB_UVC_DEVICE_NAME(config.usb_uvc->uvc.uvc_dev_num);
|
||||
video_device_name = ESP_VIDEO_USB_UVC_DEVICE_NAME(0);
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -138,6 +176,10 @@ Esp32Camera::Esp32Camera(const esp_video_init_config_t& config) {
|
||||
|
||||
struct v4l2_format setformat = {};
|
||||
setformat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
sensor_width_ = format.fmt.pix.width;
|
||||
sensor_height_ = format.fmt.pix.height;
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
setformat.fmt.pix.width = format.fmt.pix.width;
|
||||
setformat.fmt.pix.height = format.fmt.pix.height;
|
||||
|
||||
@ -146,22 +188,49 @@ Esp32Camera::Esp32Camera(const esp_video_init_config_t& config) {
|
||||
fmtdesc.index = 0;
|
||||
uint32_t best_fmt = 0;
|
||||
int best_rank = 1 << 30; // large number
|
||||
// 优先级: YUV422P > RGB565 > RGB24 > GREY
|
||||
// 注: 当前版本中 YUV422P 实际输出为 YUYV。YUYV 色彩格式在后续的处理中更节省内存空间。
|
||||
|
||||
// 注: 当前版本 esp_video 中 YUV422P 实际输出为 YUYV。
|
||||
#if defined(CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE) && defined(CONFIG_SOC_PPA_SUPPORTED)
|
||||
auto get_rank = [](uint32_t fmt) -> int {
|
||||
switch (fmt) {
|
||||
case V4L2_PIX_FMT_YUV422P:
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
return 0;
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
return 1;
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
case V4L2_PIX_FMT_YUV420: // 软件 JPEG 编码器不支持 YUV420 格式
|
||||
return 2;
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
case V4L2_PIX_FMT_GREY:
|
||||
return 3;
|
||||
case V4L2_PIX_FMT_YUV422P:
|
||||
default:
|
||||
return 1 << 29; // unsupported
|
||||
}
|
||||
};
|
||||
#else
|
||||
auto get_rank = [](uint32_t fmt) -> int {
|
||||
switch (fmt) {
|
||||
case V4L2_PIX_FMT_YUV422P:
|
||||
return 10;
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
return 11;
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
return 12;
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
case V4L2_PIX_FMT_YUV420:
|
||||
return 13;
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER
|
||||
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
case V4L2_PIX_FMT_JPEG:
|
||||
return 5;
|
||||
#endif // CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
case V4L2_PIX_FMT_GREY:
|
||||
return 20;
|
||||
default:
|
||||
return 1 << 29; // unsupported
|
||||
}
|
||||
};
|
||||
#endif
|
||||
while (ioctl(video_fd_, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
|
||||
ESP_LOGD(TAG, "VIDIOC_ENUM_FMT: pixelformat=0x%08lx, description=%s", fmtdesc.pixelformat, fmtdesc.description);
|
||||
CAM_PRINT_FOURCC(fmtdesc.pixelformat);
|
||||
@ -195,8 +264,13 @@ Esp32Camera::Esp32Camera(const esp_video_init_config_t& config) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
frame_.width = setformat.fmt.pix.height;
|
||||
frame_.height = setformat.fmt.pix.width;
|
||||
#else
|
||||
frame_.width = setformat.fmt.pix.width;
|
||||
frame_.height = setformat.fmt.pix.height;
|
||||
#endif
|
||||
|
||||
// 申请缓冲并mmap
|
||||
struct v4l2_requestbuffers req = {};
|
||||
@ -274,8 +348,8 @@ Esp32Camera::Esp32Camera(const esp_video_init_config_t& config) {
|
||||
}
|
||||
capture_count++;
|
||||
}
|
||||
ESP_LOGI(TAG, "Camera init success, captured %d frames in %dms", capture_count,
|
||||
(xTaskGetTickCount() - start) * portTICK_PERIOD_MS);
|
||||
ESP_LOGI(TAG, "Camera init success, captured %d frames in %lums", capture_count,
|
||||
(unsigned long)((xTaskGetTickCount() - start) * portTICK_PERIOD_MS));
|
||||
self->streaming_on_ = true;
|
||||
vTaskDelete(NULL);
|
||||
},
|
||||
@ -336,29 +410,44 @@ bool Esp32Camera::Capture() {
|
||||
frame_.len = buf.bytesused;
|
||||
frame_.data = (uint8_t*)heap_caps_malloc(frame_.len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!frame_.data) {
|
||||
ESP_LOGE(TAG, "alloc frame copy failed");
|
||||
ESP_LOGE(TAG, "alloc frame copy failed: need allocate %lu bytes", buf.bytesused);
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "frame.len = %d, frame.width = %d, frame.height = %d", frame_.len, frame_.width,
|
||||
frame_.height);
|
||||
ESP_LOG_BUFFER_HEXDUMP(TAG, frame_.data, MIN(frame_.len, 256), ESP_LOG_DEBUG);
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
ESP_LOGW(TAG, "mmap_buffers_[buf.index].length = %d, sensor_width = %d, sensor_height = %d",
|
||||
mmap_buffers_[buf.index].length, sensor_width_, sensor_height_);
|
||||
#else
|
||||
ESP_LOGW(TAG, "mmap_buffers_[buf.index].length = %d, frame.width = %d, frame.height = %d",
|
||||
mmap_buffers_[buf.index].length, frame_.width, frame_.height);
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
ESP_LOG_BUFFER_HEXDUMP(TAG, mmap_buffers_[buf.index].start, MIN(mmap_buffers_[buf.index].length, 256),
|
||||
ESP_LOG_DEBUG);
|
||||
|
||||
switch (sensor_format_) {
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
case V4L2_PIX_FMT_YUYV:
|
||||
case V4L2_PIX_FMT_YUV420:
|
||||
case V4L2_PIX_FMT_GREY:
|
||||
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
case V4L2_PIX_FMT_JPEG:
|
||||
#endif // CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP
|
||||
{
|
||||
auto src16 = (uint16_t*)mmap_buffers_[buf.index].start;
|
||||
auto dst16 = (uint16_t*)frame_.data;
|
||||
size_t count = (size_t)mmap_buffers_[buf.index].length / 2;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
dst16[i] = __builtin_bswap16(src16[i]);
|
||||
}
|
||||
{
|
||||
auto src16 = (uint16_t*)mmap_buffers_[buf.index].start;
|
||||
auto dst16 = (uint16_t*)frame_.data;
|
||||
size_t count = (size_t)mmap_buffers_[buf.index].length / 2;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
dst16[i] = __builtin_bswap16(src16[i]);
|
||||
}
|
||||
}
|
||||
#else
|
||||
memcpy(frame_.data, mmap_buffers_[buf.index].start, frame_.len);
|
||||
memcpy(frame_.data, mmap_buffers_[buf.index].start,
|
||||
MIN(mmap_buffers_[buf.index].length, frame_.len));
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP
|
||||
frame_.format = sensor_format_;
|
||||
break;
|
||||
@ -375,7 +464,8 @@ bool Esp32Camera::Capture() {
|
||||
}
|
||||
}
|
||||
#else
|
||||
memcpy(frame_.data, mmap_buffers_[buf.index].start, frame_.len);
|
||||
memcpy(frame_.data, mmap_buffers_[buf.index].start,
|
||||
MIN(mmap_buffers_[buf.index].length, frame_.len));
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP
|
||||
break;
|
||||
}
|
||||
@ -393,9 +483,245 @@ bool Esp32Camera::Capture() {
|
||||
}
|
||||
default:
|
||||
ESP_LOGE(TAG, "unsupported sensor format: 0x%08lx", sensor_format_);
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
#ifndef CONFIG_SOC_PPA_SUPPORTED
|
||||
uint8_t* rotate_dst =
|
||||
(uint8_t*)heap_caps_aligned_alloc(64, frame_.len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (rotate_dst == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for rotate image");
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
uint8_t* rotate_src = (uint8_t*)frame_.data;
|
||||
|
||||
esp_imgfx_rotate_cfg_t rotate_cfg = {
|
||||
.in_res =
|
||||
{
|
||||
.width = static_cast<int16_t>(sensor_width_),
|
||||
.height = static_cast<int16_t>(sensor_height_),
|
||||
},
|
||||
.degree = IMAGE_ROTATION_ANGLE,
|
||||
};
|
||||
switch (frame_.format) {
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
rotate_cfg.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB565_LE;
|
||||
break;
|
||||
case V4L2_PIX_FMT_YUYV:
|
||||
rotate_cfg.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB565_LE;
|
||||
break;
|
||||
case V4L2_PIX_FMT_GREY:
|
||||
rotate_cfg.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_Y;
|
||||
break;
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
rotate_cfg.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB888;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "unsupported sensor format: 0x%08lx", sensor_format_);
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
esp_imgfx_rotate_handle_t rotate_handle = nullptr;
|
||||
esp_imgfx_err_t imgfx_err = esp_imgfx_rotate_open(&rotate_cfg, &rotate_handle);
|
||||
if (imgfx_err != ESP_IMGFX_ERR_OK || rotate_handle == nullptr) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_rotate_create failed");
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_imgfx_data_t rotate_input_data = {
|
||||
.data = rotate_src,
|
||||
.data_len = frame_.len,
|
||||
};
|
||||
esp_imgfx_data_t rotate_output_data = {
|
||||
.data = rotate_dst,
|
||||
.data_len = frame_.len,
|
||||
};
|
||||
|
||||
imgfx_err = esp_imgfx_rotate_process(rotate_handle, &rotate_input_data, &rotate_output_data);
|
||||
if (imgfx_err != ESP_IMGFX_ERR_OK) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_rotate_process failed");
|
||||
heap_caps_free(rotate_dst);
|
||||
rotate_dst = nullptr;
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
esp_imgfx_rotate_close(rotate_handle);
|
||||
rotate_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
frame_.data = rotate_dst;
|
||||
|
||||
heap_caps_free(rotate_src);
|
||||
rotate_src = nullptr;
|
||||
|
||||
esp_imgfx_rotate_close(rotate_handle);
|
||||
rotate_handle = nullptr;
|
||||
#else // CONFIG_SOC_PPA_SUPPORTED
|
||||
uint8_t* rotate_src = nullptr;
|
||||
|
||||
ppa_srm_color_mode_t ppa_color_mode;
|
||||
switch (frame_.format) {
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
rotate_src = (uint8_t*)frame_.data;
|
||||
ppa_color_mode = PPA_SRM_COLOR_MODE_RGB565;
|
||||
break;
|
||||
case V4L2_PIX_FMT_RGB24:
|
||||
rotate_src = (uint8_t*)frame_.data;
|
||||
ppa_color_mode = PPA_SRM_COLOR_MODE_RGB888;
|
||||
break;
|
||||
case V4L2_PIX_FMT_YUYV: {
|
||||
ESP_LOGW(TAG, "YUYV format is not supported for PPA rotation, using software conversion to RGB888");
|
||||
rotate_src = (uint8_t*)heap_caps_malloc(frame_.width * frame_.height * 3,
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (rotate_src == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for rotate image");
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
esp_imgfx_color_convert_cfg_t convert_cfg = {
|
||||
.in_res = {.width = static_cast<int16_t>(frame_.width),
|
||||
.height = static_cast<int16_t>(frame_.height)},
|
||||
.in_pixel_fmt = ESP_IMGFX_PIXEL_FMT_YUYV,
|
||||
.out_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB888,
|
||||
};
|
||||
esp_imgfx_color_convert_handle_t convert_handle = nullptr;
|
||||
esp_imgfx_err_t err = esp_imgfx_color_convert_open(&convert_cfg, &convert_handle);
|
||||
if (err != ESP_IMGFX_ERR_OK || convert_handle == nullptr) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_color_convert_open failed");
|
||||
heap_caps_free(rotate_src);
|
||||
rotate_src = nullptr;
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
esp_imgfx_data_t convert_input_data = {
|
||||
.data = frame_.data,
|
||||
.data_len = frame_.len,
|
||||
};
|
||||
esp_imgfx_data_t convert_output_data = {
|
||||
.data = rotate_src,
|
||||
.data_len = static_cast<uint32_t>(frame_.width * frame_.height * 3),
|
||||
};
|
||||
err = esp_imgfx_color_convert_process(convert_handle, &convert_input_data, &convert_output_data);
|
||||
if (err != ESP_IMGFX_ERR_OK) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_color_convert_process failed");
|
||||
heap_caps_free(rotate_src);
|
||||
rotate_src = nullptr;
|
||||
esp_imgfx_color_convert_close(convert_handle);
|
||||
convert_handle = nullptr;
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
esp_imgfx_color_convert_close(convert_handle);
|
||||
convert_handle = nullptr;
|
||||
ppa_color_mode = PPA_SRM_COLOR_MODE_RGB888;
|
||||
heap_caps_free(frame_.data);
|
||||
frame_.data = rotate_src;
|
||||
frame_.len = frame_.width * frame_.height * 3;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGE(TAG, "unsupported sensor format for PPA rotation: 0x%08lx", sensor_format_);
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* rotate_dst = (uint8_t*)heap_caps_malloc(
|
||||
frame_.width * frame_.height * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT | MALLOC_CAP_CACHE_ALIGNED);
|
||||
if (rotate_dst == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for rotate image");
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ppa_client_handle_t ppa_client = nullptr;
|
||||
ppa_client_config_t client_cfg = {
|
||||
.oper_type = PPA_OPERATION_SRM,
|
||||
.max_pending_trans_num = 1,
|
||||
};
|
||||
esp_err_t err = ppa_register_client(&client_cfg, &ppa_client);
|
||||
if (err != ESP_OK || ppa_client == nullptr) {
|
||||
ESP_LOGE(TAG, "ppa_register_client failed: %d", (int)err);
|
||||
heap_caps_free(rotate_dst);
|
||||
rotate_dst = nullptr;
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ppa_srm_rotation_angle_t ppa_angle = IMAGE_ROTATION_ANGLE;
|
||||
|
||||
ppa_srm_oper_config_t srm_cfg = {};
|
||||
srm_cfg.in.buffer = (void*)rotate_src;
|
||||
srm_cfg.in.pic_w = sensor_width_;
|
||||
srm_cfg.in.pic_h = sensor_height_;
|
||||
srm_cfg.in.block_w = sensor_width_;
|
||||
srm_cfg.in.block_h = sensor_height_;
|
||||
srm_cfg.in.block_offset_x = 0;
|
||||
srm_cfg.in.block_offset_y = 0;
|
||||
srm_cfg.in.srm_cm = ppa_color_mode;
|
||||
|
||||
srm_cfg.out.buffer = (void*)rotate_dst;
|
||||
srm_cfg.out.buffer_size = frame_.len;
|
||||
srm_cfg.out.pic_w = frame_.width;
|
||||
srm_cfg.out.pic_h = frame_.height;
|
||||
srm_cfg.out.block_offset_x = 0;
|
||||
srm_cfg.out.block_offset_y = 0;
|
||||
srm_cfg.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
|
||||
|
||||
// 等比例缩放 1.0
|
||||
srm_cfg.scale_x = 1.0f;
|
||||
srm_cfg.scale_y = 1.0f;
|
||||
srm_cfg.rotation_angle = ppa_angle;
|
||||
srm_cfg.mode = PPA_TRANS_MODE_BLOCKING;
|
||||
srm_cfg.user_data = nullptr;
|
||||
|
||||
err = ppa_do_scale_rotate_mirror(ppa_client, &srm_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ppa_do_scale_rotate_mirror failed: %d", (int)err);
|
||||
heap_caps_free(rotate_dst);
|
||||
rotate_dst = nullptr;
|
||||
(void)ppa_unregister_client(ppa_client);
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "Cleanup: VIDIOC_QBUF failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
(void)ppa_unregister_client(ppa_client);
|
||||
|
||||
frame_.data = rotate_dst;
|
||||
frame_.len = frame_.width * frame_.height * 2;
|
||||
frame_.format = V4L2_PIX_FMT_RGB565;
|
||||
heap_caps_free(rotate_src);
|
||||
rotate_src = nullptr;
|
||||
#endif // CONFIG_SOC_PPA_SUPPORTED
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
}
|
||||
|
||||
if (ioctl(video_fd_, VIDIOC_QBUF, &buf) != 0) {
|
||||
ESP_LOGE(TAG, "VIDIOC_QBUF failed");
|
||||
}
|
||||
@ -405,6 +731,7 @@ bool Esp32Camera::Capture() {
|
||||
auto display = dynamic_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
|
||||
if (display != nullptr) {
|
||||
if (!frame_.data) {
|
||||
ESP_LOGE(TAG, "frame.data is null");
|
||||
return false;
|
||||
}
|
||||
uint16_t w = frame_.width;
|
||||
@ -415,72 +742,55 @@ bool Esp32Camera::Capture() {
|
||||
uint8_t* data = nullptr;
|
||||
|
||||
switch (frame_.format) {
|
||||
// LVGL 显示 YUV 系的图像似乎都有问题,暂时转换为 RGB565 显示
|
||||
case V4L2_PIX_FMT_YUYV:
|
||||
// color_format = LV_COLOR_FORMAT_YUY2;
|
||||
// [[fallthrough]];
|
||||
// LV_COLOR_FORMAT_YUY2 的显示似乎有问题,暂时转换为 RGB565 显示
|
||||
{
|
||||
color_format = LV_COLOR_FORMAT_RGB565;
|
||||
data = (uint8_t*)heap_caps_malloc(w * h * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
lvgl_image_size = w * h * 2;
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
|
||||
return false;
|
||||
}
|
||||
const uint8_t* src = (const uint8_t*)frame_.data;
|
||||
size_t src_len = frame_.len;
|
||||
size_t dst_off = 0;
|
||||
|
||||
auto clamp = [](int v) -> uint8_t {
|
||||
if (v < 0) return 0;
|
||||
if (v > 255) return 255;
|
||||
return (uint8_t)v;
|
||||
};
|
||||
|
||||
// 每 4 字节处理两个像素: Y0 U Y1 V
|
||||
for (size_t i = 0; i + 3 < src_len; i += 4) {
|
||||
int y0 = (int)src[i + 0];
|
||||
int u = (int)src[i + 1];
|
||||
int y1 = (int)src[i + 2];
|
||||
int v = (int)src[i + 3];
|
||||
|
||||
int c0 = y0 - 16;
|
||||
int c1 = y1 - 16;
|
||||
int d = u - 128;
|
||||
int e = v - 128;
|
||||
|
||||
// 常用整数近似转换
|
||||
int r0 = (298 * c0 + 409 * e + 128) >> 8;
|
||||
int g0 = (298 * c0 - 100 * d - 208 * e + 128) >> 8;
|
||||
int b0 = (298 * c0 + 516 * d + 128) >> 8;
|
||||
|
||||
int r1 = (298 * c1 + 409 * e + 128) >> 8;
|
||||
int g1 = (298 * c1 - 100 * d - 208 * e + 128) >> 8;
|
||||
int b1 = (298 * c1 + 516 * d + 128) >> 8;
|
||||
|
||||
uint8_t cr0 = clamp(r0);
|
||||
uint8_t cg0 = clamp(g0);
|
||||
uint8_t cb0 = clamp(b0);
|
||||
|
||||
uint8_t cr1 = clamp(r1);
|
||||
uint8_t cg1 = clamp(g1);
|
||||
uint8_t cb1 = clamp(b1);
|
||||
|
||||
// RGB565 打包
|
||||
uint16_t pix0 = (uint16_t)(((cr0 >> 3) << 11) | ((cg0 >> 2) << 5) | (cb0 >> 3));
|
||||
uint16_t pix1 = (uint16_t)(((cr1 >> 3) << 11) | ((cg1 >> 2) << 5) | (cb1 >> 3));
|
||||
|
||||
// 小端序:低字节先写入
|
||||
data[dst_off++] = (uint8_t)(pix0 & 0xFF);
|
||||
data[dst_off++] = (uint8_t)((pix0 >> 8) & 0xFF);
|
||||
|
||||
data[dst_off++] = (uint8_t)(pix1 & 0xFF);
|
||||
data[dst_off++] = (uint8_t)((pix1 >> 8) & 0xFF);
|
||||
}
|
||||
break;
|
||||
case V4L2_PIX_FMT_YUV420:
|
||||
case V4L2_PIX_FMT_RGB24: {
|
||||
color_format = LV_COLOR_FORMAT_RGB565;
|
||||
data = (uint8_t*)heap_caps_malloc(w * h * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
|
||||
return false;
|
||||
}
|
||||
esp_imgfx_color_convert_cfg_t convert_cfg = {
|
||||
.in_res = {.width = static_cast<int16_t>(frame_.width),
|
||||
.height = static_cast<int16_t>(frame_.height)},
|
||||
.in_pixel_fmt = static_cast<esp_imgfx_pixel_fmt_t>(frame_.format),
|
||||
.out_pixel_fmt = ESP_IMGFX_PIXEL_FMT_RGB565_LE,
|
||||
.color_space_std = ESP_IMGFX_COLOR_SPACE_STD_BT601,
|
||||
};
|
||||
esp_imgfx_color_convert_handle_t convert_handle = nullptr;
|
||||
esp_imgfx_err_t err = esp_imgfx_color_convert_open(&convert_cfg, &convert_handle);
|
||||
if (err != ESP_IMGFX_ERR_OK || convert_handle == nullptr) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_color_convert_open failed");
|
||||
heap_caps_free(data);
|
||||
data = nullptr;
|
||||
return false;
|
||||
}
|
||||
esp_imgfx_data_t convert_input_data = {
|
||||
.data = frame_.data,
|
||||
.data_len = frame_.len,
|
||||
};
|
||||
esp_imgfx_data_t convert_output_data = {
|
||||
.data = data,
|
||||
.data_len = static_cast<uint32_t>(w * h * 2),
|
||||
};
|
||||
err = esp_imgfx_color_convert_process(convert_handle, &convert_input_data, &convert_output_data);
|
||||
if (err != ESP_IMGFX_ERR_OK) {
|
||||
ESP_LOGE(TAG, "esp_imgfx_color_convert_process failed");
|
||||
heap_caps_free(data);
|
||||
data = nullptr;
|
||||
esp_imgfx_color_convert_close(convert_handle);
|
||||
convert_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
esp_imgfx_color_convert_close(convert_handle);
|
||||
convert_handle = nullptr;
|
||||
lvgl_image_size = w * h * 2;
|
||||
break;
|
||||
}
|
||||
|
||||
case V4L2_PIX_FMT_RGB565:
|
||||
// 默认的 color_format 就是 LV_COLOR_FORMAT_RGB565
|
||||
data = (uint8_t*)heap_caps_malloc(w * h * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
|
||||
@ -490,26 +800,33 @@ bool Esp32Camera::Capture() {
|
||||
lvgl_image_size = frame_.len; // fallthrough 时兼顾 YUYV 与 RGB565
|
||||
break;
|
||||
|
||||
case V4L2_PIX_FMT_RGB24: {
|
||||
// RGB888 需要转换为 RGB565
|
||||
color_format = LV_COLOR_FORMAT_RGB565;
|
||||
data = (uint8_t*)heap_caps_malloc(w * h * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
uint16_t* dst16 = (uint16_t*)data;
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for preview image");
|
||||
#ifdef CONFIG_XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
|
||||
case V4L2_PIX_FMT_JPEG: {
|
||||
uint8_t* out_data = nullptr; // out data is allocated by jpeg_to_image
|
||||
size_t out_len = 0;
|
||||
size_t out_width = 0;
|
||||
size_t out_height = 0;
|
||||
size_t out_stride = 0;
|
||||
|
||||
esp_err_t ret =
|
||||
jpeg_to_image(frame_.data, frame_.len, &out_data, &out_len, &out_width, &out_height, &out_stride);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to decode JPEG image: %d (%s)", (int)ret, esp_err_to_name(ret));
|
||||
if (out_data) {
|
||||
heap_caps_free(out_data);
|
||||
out_data = nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const uint8_t* src = frame_.data;
|
||||
size_t pixel_count = (size_t)w * (size_t)h;
|
||||
for (size_t i = 0; i < pixel_count; i++) {
|
||||
uint8_t r = src[i * 3 + 0];
|
||||
uint8_t g = src[i * 3 + 1];
|
||||
uint8_t b = src[i * 3 + 2];
|
||||
dst16[i] = (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
|
||||
}
|
||||
lvgl_image_size = w * h * 2;
|
||||
|
||||
data = out_data;
|
||||
w = out_width;
|
||||
h = out_height;
|
||||
lvgl_image_size = out_len;
|
||||
stride = out_stride;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
ESP_LOGE(TAG, "unsupported frame format: 0x%08lx", frame_.format);
|
||||
return false;
|
||||
@ -595,16 +912,31 @@ std::string Esp32Camera::Explain(const std::string& question) {
|
||||
uint16_t w = frame_.width ? frame_.width : 320;
|
||||
uint16_t h = frame_.height ? frame_.height : 240;
|
||||
v4l2_pix_fmt_t enc_fmt = frame_.format;
|
||||
image_to_jpeg_cb(
|
||||
bool ok = image_to_jpeg_cb(
|
||||
frame_.data, frame_.len, w, h, enc_fmt, 80,
|
||||
[](void* arg, size_t index, const void* data, size_t len) -> size_t {
|
||||
auto jpeg_queue = (QueueHandle_t)arg;
|
||||
JpegChunk chunk = {.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM), .len = len};
|
||||
memcpy(chunk.data, data, len);
|
||||
auto jpeg_queue = static_cast<QueueHandle_t>(arg);
|
||||
JpegChunk chunk = {.data = nullptr, .len = len};
|
||||
if (index == 0 && data != nullptr && len > 0) {
|
||||
chunk.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (chunk.data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %zu bytes for JPEG chunk", len);
|
||||
chunk.len = 0;
|
||||
} else {
|
||||
memcpy(chunk.data, data, len);
|
||||
}
|
||||
} else {
|
||||
chunk.len = 0; // Sentinel or error
|
||||
}
|
||||
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
|
||||
return len;
|
||||
},
|
||||
jpeg_queue);
|
||||
|
||||
if (!ok) {
|
||||
JpegChunk chunk = {.data = nullptr, .len = 0};
|
||||
xQueueSend(jpeg_queue, &chunk, portMAX_DELAY);
|
||||
}
|
||||
});
|
||||
|
||||
auto network = Board::GetInstance().GetNetwork();
|
||||
@ -657,6 +989,7 @@ std::string Esp32Camera::Explain(const std::string& question) {
|
||||
|
||||
// 第三块:JPEG数据
|
||||
size_t total_sent = 0;
|
||||
bool saw_terminator = false;
|
||||
while (true) {
|
||||
JpegChunk chunk;
|
||||
if (xQueueReceive(jpeg_queue, &chunk, portMAX_DELAY) != pdPASS) {
|
||||
@ -664,6 +997,7 @@ std::string Esp32Camera::Explain(const std::string& question) {
|
||||
break;
|
||||
}
|
||||
if (chunk.data == nullptr) {
|
||||
saw_terminator = true;
|
||||
break; // The last chunk
|
||||
}
|
||||
http->Write((const char*)chunk.data, chunk.len);
|
||||
@ -675,6 +1009,11 @@ std::string Esp32Camera::Explain(const std::string& question) {
|
||||
// 清理队列
|
||||
vQueueDelete(jpeg_queue);
|
||||
|
||||
if (!saw_terminator || total_sent == 0) {
|
||||
ESP_LOGE(TAG, "JPEG encoder failed or produced empty output");
|
||||
throw std::runtime_error("Failed to encode image to JPEG");
|
||||
}
|
||||
|
||||
{
|
||||
// 第四块:multipart尾部
|
||||
std::string multipart_footer;
|
||||
|
||||
@ -29,6 +29,10 @@ private:
|
||||
v4l2_pix_fmt_t format = 0;
|
||||
} frame_;
|
||||
v4l2_pix_fmt_t sensor_format_ = 0;
|
||||
#ifdef CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
uint16_t sensor_width_ = 0;
|
||||
uint16_t sensor_height_ = 0;
|
||||
#endif // CONFIG_XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE
|
||||
int video_fd_ = -1;
|
||||
bool streaming_on_ = false;
|
||||
struct MmapBuffer { void *start = nullptr; size_t length = 0; };
|
||||
|
||||
@ -6,11 +6,18 @@
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_timer.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <font_awesome.h>
|
||||
#include <opus_encoder.h>
|
||||
#include <utility>
|
||||
|
||||
static const char *TAG = "Ml307Board";
|
||||
|
||||
// Maximum retry count for modem detection
|
||||
static constexpr int MODEM_DETECT_MAX_RETRIES = 30;
|
||||
// Maximum retry count for network registration
|
||||
static constexpr int NETWORK_REG_MAX_RETRIES = 6;
|
||||
|
||||
Ml307Board::Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin) : tx_pin_(tx_pin), rx_pin_(rx_pin), dtr_pin_(dtr_pin) {
|
||||
}
|
||||
|
||||
@ -18,47 +25,106 @@ std::string Ml307Board::GetBoardType() {
|
||||
return "ml307";
|
||||
}
|
||||
|
||||
void Ml307Board::StartNetwork() {
|
||||
auto& application = Application::GetInstance();
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
display->SetStatus(Lang::Strings::DETECTING_MODULE);
|
||||
void Ml307Board::SetNetworkEventCallback(NetworkEventCallback callback) {
|
||||
network_event_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
void Ml307Board::OnNetworkEvent(NetworkEvent event, const std::string& data) {
|
||||
switch (event) {
|
||||
case NetworkEvent::ModemDetecting:
|
||||
ESP_LOGI(TAG, "Detecting modem...");
|
||||
break;
|
||||
case NetworkEvent::Connecting:
|
||||
ESP_LOGI(TAG, "Registering network...");
|
||||
break;
|
||||
case NetworkEvent::Connected:
|
||||
ESP_LOGI(TAG, "Network connected");
|
||||
break;
|
||||
case NetworkEvent::Disconnected:
|
||||
ESP_LOGW(TAG, "Network disconnected");
|
||||
break;
|
||||
case NetworkEvent::ModemErrorNoSim:
|
||||
ESP_LOGE(TAG, "No SIM card detected");
|
||||
break;
|
||||
case NetworkEvent::ModemErrorRegDenied:
|
||||
ESP_LOGE(TAG, "Network registration denied");
|
||||
break;
|
||||
case NetworkEvent::ModemErrorInitFailed:
|
||||
ESP_LOGE(TAG, "Modem initialization failed");
|
||||
break;
|
||||
case NetworkEvent::ModemErrorTimeout:
|
||||
ESP_LOGE(TAG, "Operation timeout");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Notify external callback if set
|
||||
if (network_event_callback_) {
|
||||
network_event_callback_(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
void Ml307Board::NetworkTask() {
|
||||
auto& application = Application::GetInstance();
|
||||
|
||||
// Notify modem detection started
|
||||
OnNetworkEvent(NetworkEvent::ModemDetecting);
|
||||
|
||||
// Try to detect modem with retry limit
|
||||
int detect_retries = 0;
|
||||
while (detect_retries < MODEM_DETECT_MAX_RETRIES) {
|
||||
modem_ = AtModem::Detect(tx_pin_, rx_pin_, dtr_pin_, 921600);
|
||||
if (modem_ != nullptr) {
|
||||
break;
|
||||
}
|
||||
detect_retries++;
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
if (modem_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to detect modem after %d retries", MODEM_DETECT_MAX_RETRIES);
|
||||
OnNetworkEvent(NetworkEvent::ModemErrorInitFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Modem detected successfully");
|
||||
|
||||
// Set up network state change callback
|
||||
// Note: Don't call GetCarrierName() here as it sends AT command and will block ReceiveTask
|
||||
modem_->OnNetworkStateChanged([this, &application](bool network_ready) {
|
||||
if (network_ready) {
|
||||
ESP_LOGI(TAG, "Network is ready");
|
||||
OnNetworkEvent(NetworkEvent::Connected);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Network is down");
|
||||
auto device_state = application.GetDeviceState();
|
||||
if (device_state == kDeviceStateListening || device_state == kDeviceStateSpeaking) {
|
||||
application.Schedule([this, &application]() {
|
||||
application.SetDeviceState(kDeviceStateIdle);
|
||||
});
|
||||
}
|
||||
OnNetworkEvent(NetworkEvent::Disconnected);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for network ready
|
||||
display->SetStatus(Lang::Strings::REGISTERING_NETWORK);
|
||||
while (true) {
|
||||
// Notify network registration started
|
||||
OnNetworkEvent(NetworkEvent::Connecting);
|
||||
|
||||
// Wait for network ready with retry limit
|
||||
int reg_retries = 0;
|
||||
while (reg_retries < NETWORK_REG_MAX_RETRIES) {
|
||||
auto result = modem_->WaitForNetworkReady();
|
||||
if (result == NetworkStatus::ErrorInsertPin) {
|
||||
application.Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_PIN);
|
||||
} else if (result == NetworkStatus::ErrorRegistrationDenied) {
|
||||
application.Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_REG);
|
||||
} else {
|
||||
if (result == NetworkStatus::Ready) {
|
||||
break;
|
||||
} else if (result == NetworkStatus::ErrorInsertPin) {
|
||||
OnNetworkEvent(NetworkEvent::ModemErrorNoSim);
|
||||
} else if (result == NetworkStatus::ErrorRegistrationDenied) {
|
||||
OnNetworkEvent(NetworkEvent::ModemErrorRegDenied);
|
||||
} else if (result == NetworkStatus::ErrorTimeout) {
|
||||
OnNetworkEvent(NetworkEvent::ModemErrorTimeout);
|
||||
}
|
||||
reg_retries++;
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
}
|
||||
|
||||
if (!modem_->network_ready()) {
|
||||
ESP_LOGE(TAG, "Failed to register network after %d retries", NETWORK_REG_MAX_RETRIES);
|
||||
return;
|
||||
}
|
||||
|
||||
// Print the ML307 modem information
|
||||
std::string module_revision = modem_->GetModuleRevision();
|
||||
std::string imei = modem_->GetImei();
|
||||
@ -68,6 +134,15 @@ void Ml307Board::StartNetwork() {
|
||||
ESP_LOGI(TAG, "ML307 ICCID: %s", iccid.c_str());
|
||||
}
|
||||
|
||||
void Ml307Board::StartNetwork() {
|
||||
// Create network initialization task and return immediately
|
||||
xTaskCreate([](void* arg) {
|
||||
Ml307Board* board = static_cast<Ml307Board*>(arg);
|
||||
board->NetworkTask();
|
||||
vTaskDelete(NULL);
|
||||
}, "ml307_net", 4096, this, 5, NULL);
|
||||
}
|
||||
|
||||
NetworkInterface* Ml307Board::GetNetwork() {
|
||||
return modem_.get();
|
||||
}
|
||||
@ -79,13 +154,13 @@ const char* Ml307Board::GetNetworkStateIcon() {
|
||||
int csq = modem_->GetCsq();
|
||||
if (csq == -1) {
|
||||
return FONT_AWESOME_SIGNAL_OFF;
|
||||
} else if (csq >= 0 && csq <= 14) {
|
||||
} else if (csq >= 0 && csq <= 9) {
|
||||
return FONT_AWESOME_SIGNAL_WEAK;
|
||||
} else if (csq >= 15 && csq <= 19) {
|
||||
} else if (csq >= 10 && csq <= 14) {
|
||||
return FONT_AWESOME_SIGNAL_FAIR;
|
||||
} else if (csq >= 20 && csq <= 24) {
|
||||
} else if (csq >= 15 && csq <= 19) {
|
||||
return FONT_AWESOME_SIGNAL_GOOD;
|
||||
} else if (csq >= 25 && csq <= 31) {
|
||||
} else if (csq >= 20 && csq <= 31) {
|
||||
return FONT_AWESOME_SIGNAL_STRONG;
|
||||
}
|
||||
|
||||
@ -106,8 +181,9 @@ std::string Ml307Board::GetBoardJson() {
|
||||
return board_json;
|
||||
}
|
||||
|
||||
void Ml307Board::SetPowerSaveMode(bool enabled) {
|
||||
// TODO: Implement power save mode for ML307
|
||||
void Ml307Board::SetPowerSaveLevel(PowerSaveLevel level) {
|
||||
// TODO: Implement power save level for ML307
|
||||
(void)level;
|
||||
}
|
||||
|
||||
std::string Ml307Board::GetDeviceStatusJson() {
|
||||
|
||||
@ -12,16 +12,25 @@ protected:
|
||||
gpio_num_t tx_pin_;
|
||||
gpio_num_t rx_pin_;
|
||||
gpio_num_t dtr_pin_;
|
||||
NetworkEventCallback network_event_callback_;
|
||||
|
||||
virtual std::string GetBoardJson() override;
|
||||
|
||||
// Internal helper to trigger network event callback
|
||||
void OnNetworkEvent(NetworkEvent event, const std::string& data = "");
|
||||
|
||||
// Network initialization task (runs in FreeRTOS task)
|
||||
static void NetworkTaskEntry(void* arg);
|
||||
void NetworkTask();
|
||||
|
||||
public:
|
||||
Ml307Board(gpio_num_t tx_pin, gpio_num_t rx_pin, gpio_num_t dtr_pin = GPIO_NUM_NC);
|
||||
virtual std::string GetBoardType() override;
|
||||
virtual void StartNetwork() override;
|
||||
virtual void SetNetworkEventCallback(NetworkEventCallback callback) override;
|
||||
virtual NetworkInterface* GetNetwork() override;
|
||||
virtual const char* GetNetworkStateIcon() override;
|
||||
virtual void SetPowerSaveMode(bool enabled) override;
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
|
||||
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
|
||||
virtual std::string GetDeviceStatusJson() override;
|
||||
};
|
||||
|
||||
@ -10,21 +10,38 @@
|
||||
#include <freertos/task.h>
|
||||
#include <esp_network.h>
|
||||
#include <esp_log.h>
|
||||
#include <utility>
|
||||
|
||||
#include <font_awesome.h>
|
||||
#include <wifi_manager.h>
|
||||
#include <wifi_station.h>
|
||||
#include <wifi_configuration_ap.h>
|
||||
#include <ssid_manager.h>
|
||||
#include "afsk_demod.h"
|
||||
#ifdef CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
|
||||
#include "blufi.h"
|
||||
#endif
|
||||
|
||||
static const char *TAG = "WifiBoard";
|
||||
|
||||
// Connection timeout in seconds
|
||||
static constexpr int CONNECT_TIMEOUT_SEC = 60;
|
||||
|
||||
WifiBoard::WifiBoard() {
|
||||
Settings settings("wifi", true);
|
||||
wifi_config_mode_ = settings.GetInt("force_ap") == 1;
|
||||
if (wifi_config_mode_) {
|
||||
ESP_LOGI(TAG, "force_ap is set to 1, reset to 0");
|
||||
settings.SetInt("force_ap", 0);
|
||||
// Create connection timeout timer
|
||||
esp_timer_create_args_t timer_args = {
|
||||
.callback = OnWifiConnectTimeout,
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "wifi_connect_timer",
|
||||
.skip_unhandled_events = true
|
||||
};
|
||||
esp_timer_create(&timer_args, &connect_timer_);
|
||||
}
|
||||
|
||||
WifiBoard::~WifiBoard() {
|
||||
if (connect_timer_) {
|
||||
esp_timer_stop(connect_timer_);
|
||||
esp_timer_delete(connect_timer_);
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,88 +49,198 @@ std::string WifiBoard::GetBoardType() {
|
||||
return "wifi";
|
||||
}
|
||||
|
||||
void WifiBoard::EnterWifiConfigMode() {
|
||||
auto& application = Application::GetInstance();
|
||||
application.SetDeviceState(kDeviceStateWifiConfiguring);
|
||||
void WifiBoard::StartNetwork() {
|
||||
auto& wifi_manager = WifiManager::GetInstance();
|
||||
|
||||
auto& wifi_ap = WifiConfigurationAp::GetInstance();
|
||||
wifi_ap.SetLanguage(Lang::CODE);
|
||||
wifi_ap.SetSsidPrefix("Xiaozhi");
|
||||
wifi_ap.Start();
|
||||
// Initialize WiFi manager
|
||||
WifiManagerConfig config;
|
||||
config.ssid_prefix = "Xiaozhi";
|
||||
config.language = Lang::CODE;
|
||||
wifi_manager.Initialize(config);
|
||||
|
||||
// 等待 1.5 秒显示开发板信息
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
// Set unified event callback - forward to NetworkEvent with SSID data
|
||||
wifi_manager.SetEventCallback([this, &wifi_manager](WifiEvent event) {
|
||||
std::string ssid = wifi_manager.GetSsid();
|
||||
switch (event) {
|
||||
case WifiEvent::Scanning:
|
||||
OnNetworkEvent(NetworkEvent::Scanning);
|
||||
break;
|
||||
case WifiEvent::Connecting:
|
||||
OnNetworkEvent(NetworkEvent::Connecting, ssid);
|
||||
break;
|
||||
case WifiEvent::Connected:
|
||||
OnNetworkEvent(NetworkEvent::Connected, ssid);
|
||||
break;
|
||||
case WifiEvent::Disconnected:
|
||||
OnNetworkEvent(NetworkEvent::Disconnected);
|
||||
break;
|
||||
case WifiEvent::ConfigModeEnter:
|
||||
OnNetworkEvent(NetworkEvent::WifiConfigModeEnter);
|
||||
break;
|
||||
case WifiEvent::ConfigModeExit:
|
||||
OnNetworkEvent(NetworkEvent::WifiConfigModeExit);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL
|
||||
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
|
||||
hint += wifi_ap.GetSsid();
|
||||
hint += Lang::Strings::ACCESS_VIA_BROWSER;
|
||||
hint += wifi_ap.GetWebServerUrl();
|
||||
hint += "\n\n";
|
||||
|
||||
// 播报配置 WiFi 的提示
|
||||
application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG);
|
||||
// Try to connect or enter config mode
|
||||
TryWifiConnect();
|
||||
}
|
||||
|
||||
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
int channel = 1;
|
||||
if (codec) {
|
||||
channel = codec->input_channels();
|
||||
}
|
||||
ESP_LOGI(TAG, "Start receiving WiFi credentials from audio, input channels: %d", channel);
|
||||
audio_wifi_config::ReceiveWifiCredentialsFromAudio(&application, &wifi_ap, display, channel);
|
||||
#endif
|
||||
|
||||
// Wait forever until reset after configuration
|
||||
while (true) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
void WifiBoard::TryWifiConnect() {
|
||||
auto& ssid_manager = SsidManager::GetInstance();
|
||||
bool have_ssid = !ssid_manager.GetSsidList().empty();
|
||||
|
||||
if (have_ssid) {
|
||||
// Start connection attempt with timeout
|
||||
ESP_LOGI(TAG, "Starting WiFi connection attempt");
|
||||
esp_timer_start_once(connect_timer_, CONNECT_TIMEOUT_SEC * 1000000ULL);
|
||||
WifiManager::GetInstance().StartStation();
|
||||
} else {
|
||||
// No SSID configured, enter config mode
|
||||
// Wait for the board version to be shown
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
StartWifiConfigMode();
|
||||
}
|
||||
}
|
||||
|
||||
void WifiBoard::StartNetwork() {
|
||||
// User can press BOOT button while starting to enter WiFi configuration mode
|
||||
if (wifi_config_mode_) {
|
||||
EnterWifiConfigMode();
|
||||
void WifiBoard::OnNetworkEvent(NetworkEvent event, const std::string& data) {
|
||||
switch (event) {
|
||||
case NetworkEvent::Connected:
|
||||
// Stop timeout timer
|
||||
esp_timer_stop(connect_timer_);
|
||||
#ifdef CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
|
||||
// make sure blufi resources has been released
|
||||
Blufi::GetInstance().deinit();
|
||||
#endif
|
||||
in_config_mode_ = false;
|
||||
ESP_LOGI(TAG, "Connected to WiFi: %s", data.c_str());
|
||||
break;
|
||||
case NetworkEvent::Scanning:
|
||||
ESP_LOGI(TAG, "WiFi scanning");
|
||||
break;
|
||||
case NetworkEvent::Connecting:
|
||||
ESP_LOGI(TAG, "WiFi connecting to %s", data.c_str());
|
||||
break;
|
||||
case NetworkEvent::Disconnected:
|
||||
ESP_LOGW(TAG, "WiFi disconnected");
|
||||
break;
|
||||
case NetworkEvent::WifiConfigModeEnter:
|
||||
ESP_LOGI(TAG, "WiFi config mode entered");
|
||||
in_config_mode_ = true;
|
||||
break;
|
||||
case NetworkEvent::WifiConfigModeExit:
|
||||
ESP_LOGI(TAG, "WiFi config mode exited");
|
||||
in_config_mode_ = false;
|
||||
// Try to connect with the new credentials
|
||||
TryWifiConnect();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Notify external callback if set
|
||||
if (network_event_callback_) {
|
||||
network_event_callback_(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
void WifiBoard::SetNetworkEventCallback(NetworkEventCallback callback) {
|
||||
network_event_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void WifiBoard::OnWifiConnectTimeout(void* arg) {
|
||||
auto* board = static_cast<WifiBoard*>(arg);
|
||||
ESP_LOGW(TAG, "WiFi connection timeout, entering config mode");
|
||||
|
||||
WifiManager::GetInstance().StopStation();
|
||||
board->StartWifiConfigMode();
|
||||
}
|
||||
|
||||
void WifiBoard::StartWifiConfigMode() {
|
||||
in_config_mode_ = true;
|
||||
// Transition to wifi configuring state
|
||||
Application::GetInstance().SetDeviceState(kDeviceStateWifiConfiguring);
|
||||
#ifdef CONFIG_USE_HOTSPOT_WIFI_PROVISIONING
|
||||
auto& wifi_manager = WifiManager::GetInstance();
|
||||
|
||||
wifi_manager.StartConfigAp();
|
||||
|
||||
// Show config prompt after a short delay
|
||||
Application::GetInstance().Schedule([&wifi_manager]() {
|
||||
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
|
||||
hint += wifi_manager.GetApSsid();
|
||||
hint += Lang::Strings::ACCESS_VIA_BROWSER;
|
||||
hint += wifi_manager.GetApWebUrl();
|
||||
|
||||
Application::GetInstance().Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG);
|
||||
});
|
||||
#endif
|
||||
#if CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
|
||||
auto &blufi = Blufi::GetInstance();
|
||||
// initialize esp-blufi protocol
|
||||
blufi.init();
|
||||
#endif
|
||||
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
|
||||
// Start acoustic provisioning task
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
int channel = codec ? codec->input_channels() : 1;
|
||||
ESP_LOGI(TAG, "Starting acoustic WiFi provisioning, channels: %d", channel);
|
||||
|
||||
xTaskCreate([](void* arg) {
|
||||
auto ch = reinterpret_cast<intptr_t>(arg);
|
||||
auto& app = Application::GetInstance();
|
||||
auto& wifi = WifiManager::GetInstance();
|
||||
auto disp = Board::GetInstance().GetDisplay();
|
||||
audio_wifi_config::ReceiveWifiCredentialsFromAudio(&app, &wifi, disp, ch);
|
||||
vTaskDelete(NULL);
|
||||
}, "acoustic_wifi", 4096, reinterpret_cast<void*>(channel), 2, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
void WifiBoard::EnterWifiConfigMode() {
|
||||
ESP_LOGI(TAG, "EnterWifiConfigMode called");
|
||||
GetDisplay()->ShowNotification(Lang::Strings::ENTERING_WIFI_CONFIG_MODE);
|
||||
|
||||
auto& app = Application::GetInstance();
|
||||
auto state = app.GetDeviceState();
|
||||
|
||||
if (state == kDeviceStateSpeaking || state == kDeviceStateListening || state == kDeviceStateIdle) {
|
||||
// Reset protocol (close audio channel, reset protocol)
|
||||
Application::GetInstance().ResetProtocol();
|
||||
|
||||
xTaskCreate([](void* arg) {
|
||||
auto* board = static_cast<WifiBoard*>(arg);
|
||||
|
||||
// Wait for 1 second to allow speaking to finish gracefully
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
// Stop any ongoing connection attempt
|
||||
esp_timer_stop(board->connect_timer_);
|
||||
WifiManager::GetInstance().StopStation();
|
||||
|
||||
// Enter config mode
|
||||
board->StartWifiConfigMode();
|
||||
|
||||
vTaskDelete(NULL);
|
||||
}, "wifi_cfg_delay", 4096, this, 2, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no WiFi SSID is configured, enter WiFi configuration mode
|
||||
auto& ssid_manager = SsidManager::GetInstance();
|
||||
auto ssid_list = ssid_manager.GetSsidList();
|
||||
if (ssid_list.empty()) {
|
||||
wifi_config_mode_ = true;
|
||||
EnterWifiConfigMode();
|
||||
if (state != kDeviceStateStarting) {
|
||||
ESP_LOGE(TAG, "EnterWifiConfigMode called but device state is not starting or speaking, device state: %d", state);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
wifi_station.OnScanBegin([this]() {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
display->ShowNotification(Lang::Strings::SCANNING_WIFI, 30000);
|
||||
});
|
||||
wifi_station.OnConnect([this](const std::string& ssid) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
std::string notification = Lang::Strings::CONNECT_TO;
|
||||
notification += ssid;
|
||||
notification += "...";
|
||||
display->ShowNotification(notification.c_str(), 30000);
|
||||
});
|
||||
wifi_station.OnConnected([this](const std::string& ssid) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
std::string notification = Lang::Strings::CONNECTED_TO;
|
||||
notification += ssid;
|
||||
display->ShowNotification(notification.c_str(), 30000);
|
||||
});
|
||||
wifi_station.Start();
|
||||
// Stop any ongoing connection attempt
|
||||
esp_timer_stop(connect_timer_);
|
||||
WifiManager::GetInstance().StopStation();
|
||||
|
||||
// Try to connect to WiFi, if failed, launch the WiFi configuration AP
|
||||
if (!wifi_station.WaitForConnected(60 * 1000)) {
|
||||
wifi_station.Stop();
|
||||
wifi_config_mode_ = true;
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
StartWifiConfigMode();
|
||||
}
|
||||
|
||||
bool WifiBoard::IsInWifiConfigMode() const {
|
||||
return WifiManager::GetInstance().IsConfigMode();
|
||||
}
|
||||
|
||||
NetworkInterface* WifiBoard::GetNetwork() {
|
||||
@ -122,147 +249,111 @@ NetworkInterface* WifiBoard::GetNetwork() {
|
||||
}
|
||||
|
||||
const char* WifiBoard::GetNetworkStateIcon() {
|
||||
if (wifi_config_mode_) {
|
||||
auto& wifi = WifiManager::GetInstance();
|
||||
|
||||
if (wifi.IsConfigMode()) {
|
||||
return FONT_AWESOME_WIFI;
|
||||
}
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
if (!wifi_station.IsConnected()) {
|
||||
if (!wifi.IsConnected()) {
|
||||
return FONT_AWESOME_WIFI_SLASH;
|
||||
}
|
||||
int8_t rssi = wifi_station.GetRssi();
|
||||
if (rssi >= -60) {
|
||||
|
||||
int rssi = wifi.GetRssi();
|
||||
if (rssi >= -65) {
|
||||
return FONT_AWESOME_WIFI;
|
||||
} else if (rssi >= -70) {
|
||||
} else if (rssi >= -75) {
|
||||
return FONT_AWESOME_WIFI_FAIR;
|
||||
} else {
|
||||
return FONT_AWESOME_WIFI_WEAK;
|
||||
}
|
||||
return FONT_AWESOME_WIFI_WEAK;
|
||||
}
|
||||
|
||||
std::string WifiBoard::GetBoardJson() {
|
||||
// Set the board type for OTA
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
std::string board_json = R"({)";
|
||||
board_json += R"("type":")" + std::string(BOARD_TYPE) + R"(",)";
|
||||
board_json += R"("name":")" + std::string(BOARD_NAME) + R"(",)";
|
||||
if (!wifi_config_mode_) {
|
||||
board_json += R"("ssid":")" + wifi_station.GetSsid() + R"(",)";
|
||||
board_json += R"("rssi":)" + std::to_string(wifi_station.GetRssi()) + R"(,)";
|
||||
board_json += R"("channel":)" + std::to_string(wifi_station.GetChannel()) + R"(,)";
|
||||
board_json += R"("ip":")" + wifi_station.GetIpAddress() + R"(",)";
|
||||
auto& wifi = WifiManager::GetInstance();
|
||||
std::string json = R"({"type":")" + std::string(BOARD_TYPE) + R"(",)";
|
||||
json += R"("name":")" + std::string(BOARD_NAME) + R"(",)";
|
||||
|
||||
if (!wifi.IsConfigMode()) {
|
||||
json += R"("ssid":")" + wifi.GetSsid() + R"(",)";
|
||||
json += R"("rssi":)" + std::to_string(wifi.GetRssi()) + R"(,)";
|
||||
json += R"("channel":)" + std::to_string(wifi.GetChannel()) + R"(,)";
|
||||
json += R"("ip":")" + wifi.GetIpAddress() + R"(",)";
|
||||
}
|
||||
board_json += R"("mac":")" + SystemInfo::GetMacAddress() + R"(")";
|
||||
board_json += R"(})";
|
||||
return board_json;
|
||||
|
||||
json += R"("mac":")" + SystemInfo::GetMacAddress() + R"("})";
|
||||
return json;
|
||||
}
|
||||
|
||||
void WifiBoard::SetPowerSaveMode(bool enabled) {
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
wifi_station.SetPowerSaveMode(enabled);
|
||||
}
|
||||
|
||||
void WifiBoard::ResetWifiConfiguration() {
|
||||
// Set a flag and reboot the device to enter the network configuration mode
|
||||
{
|
||||
Settings settings("wifi", true);
|
||||
settings.SetInt("force_ap", 1);
|
||||
void WifiBoard::SetPowerSaveLevel(PowerSaveLevel level) {
|
||||
WifiPowerSaveLevel wifi_level;
|
||||
switch (level) {
|
||||
case PowerSaveLevel::LOW_POWER:
|
||||
wifi_level = WifiPowerSaveLevel::LOW_POWER;
|
||||
break;
|
||||
case PowerSaveLevel::BALANCED:
|
||||
wifi_level = WifiPowerSaveLevel::BALANCED;
|
||||
break;
|
||||
case PowerSaveLevel::PERFORMANCE:
|
||||
default:
|
||||
wifi_level = WifiPowerSaveLevel::PERFORMANCE;
|
||||
break;
|
||||
}
|
||||
GetDisplay()->ShowNotification(Lang::Strings::ENTERING_WIFI_CONFIG_MODE);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
// Reboot the device
|
||||
esp_restart();
|
||||
WifiManager::GetInstance().SetPowerSaveLevel(wifi_level);
|
||||
}
|
||||
|
||||
std::string WifiBoard::GetDeviceStatusJson() {
|
||||
/*
|
||||
* 返回设备状态JSON
|
||||
*
|
||||
* 返回的JSON结构如下:
|
||||
* {
|
||||
* "audio_speaker": {
|
||||
* "volume": 70
|
||||
* },
|
||||
* "screen": {
|
||||
* "brightness": 100,
|
||||
* "theme": "light"
|
||||
* },
|
||||
* "battery": {
|
||||
* "level": 50,
|
||||
* "charging": true
|
||||
* },
|
||||
* "network": {
|
||||
* "type": "wifi",
|
||||
* "ssid": "Xiaozhi",
|
||||
* "rssi": -60
|
||||
* },
|
||||
* "chip": {
|
||||
* "temperature": 25
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
auto& board = Board::GetInstance();
|
||||
auto root = cJSON_CreateObject();
|
||||
|
||||
// Audio speaker
|
||||
auto audio_speaker = cJSON_CreateObject();
|
||||
auto audio_codec = board.GetAudioCodec();
|
||||
if (audio_codec) {
|
||||
cJSON_AddNumberToObject(audio_speaker, "volume", audio_codec->output_volume());
|
||||
if (auto codec = board.GetAudioCodec()) {
|
||||
cJSON_AddNumberToObject(audio_speaker, "volume", codec->output_volume());
|
||||
}
|
||||
cJSON_AddItemToObject(root, "audio_speaker", audio_speaker);
|
||||
|
||||
// Screen brightness
|
||||
auto backlight = board.GetBacklight();
|
||||
// Screen
|
||||
auto screen = cJSON_CreateObject();
|
||||
if (backlight) {
|
||||
if (auto backlight = board.GetBacklight()) {
|
||||
cJSON_AddNumberToObject(screen, "brightness", backlight->brightness());
|
||||
}
|
||||
auto display = board.GetDisplay();
|
||||
if (display && display->height() > 64) { // For LCD display only
|
||||
auto theme = display->GetTheme();
|
||||
if (theme != nullptr) {
|
||||
if (auto display = board.GetDisplay(); display && display->height() > 64) {
|
||||
if (auto theme = display->GetTheme()) {
|
||||
cJSON_AddStringToObject(screen, "theme", theme->name().c_str());
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(root, "screen", screen);
|
||||
|
||||
// Battery
|
||||
int battery_level = 0;
|
||||
bool charging = false;
|
||||
bool discharging = false;
|
||||
if (board.GetBatteryLevel(battery_level, charging, discharging)) {
|
||||
cJSON* battery = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(battery, "level", battery_level);
|
||||
int level = 0;
|
||||
bool charging = false, discharging = false;
|
||||
if (board.GetBatteryLevel(level, charging, discharging)) {
|
||||
auto battery = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(battery, "level", level);
|
||||
cJSON_AddBoolToObject(battery, "charging", charging);
|
||||
cJSON_AddItemToObject(root, "battery", battery);
|
||||
}
|
||||
|
||||
// Network
|
||||
auto& wifi = WifiManager::GetInstance();
|
||||
auto network = cJSON_CreateObject();
|
||||
auto& wifi_station = WifiStation::GetInstance();
|
||||
cJSON_AddStringToObject(network, "type", "wifi");
|
||||
cJSON_AddStringToObject(network, "ssid", wifi_station.GetSsid().c_str());
|
||||
int rssi = wifi_station.GetRssi();
|
||||
if (rssi >= -60) {
|
||||
cJSON_AddStringToObject(network, "signal", "strong");
|
||||
} else if (rssi >= -70) {
|
||||
cJSON_AddStringToObject(network, "signal", "medium");
|
||||
} else {
|
||||
cJSON_AddStringToObject(network, "signal", "weak");
|
||||
}
|
||||
cJSON_AddStringToObject(network, "ssid", wifi.GetSsid().c_str());
|
||||
int rssi = wifi.GetRssi();
|
||||
const char* signal = rssi >= -60 ? "strong" : (rssi >= -70 ? "medium" : "weak");
|
||||
cJSON_AddStringToObject(network, "signal", signal);
|
||||
cJSON_AddItemToObject(root, "network", network);
|
||||
|
||||
// Chip
|
||||
float esp32temp = 0.0f;
|
||||
if (board.GetTemperature(esp32temp)) {
|
||||
// Chip temperature
|
||||
float temp = 0.0f;
|
||||
if (board.GetTemperature(temp)) {
|
||||
auto chip = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(chip, "temperature", esp32temp);
|
||||
cJSON_AddNumberToObject(chip, "temperature", temp);
|
||||
cJSON_AddItemToObject(root, "chip", chip);
|
||||
}
|
||||
|
||||
auto json_str = cJSON_PrintUnformatted(root);
|
||||
std::string json(json_str);
|
||||
cJSON_free(json_str);
|
||||
auto str = cJSON_PrintUnformatted(root);
|
||||
std::string result(str);
|
||||
cJSON_free(str);
|
||||
cJSON_Delete(root);
|
||||
return json;
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -2,23 +2,68 @@
|
||||
#define WIFI_BOARD_H
|
||||
|
||||
#include "board.h"
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
class WifiBoard : public Board {
|
||||
protected:
|
||||
bool wifi_config_mode_ = false;
|
||||
void EnterWifiConfigMode();
|
||||
esp_timer_handle_t connect_timer_ = nullptr;
|
||||
bool in_config_mode_ = false;
|
||||
NetworkEventCallback network_event_callback_ = nullptr;
|
||||
|
||||
virtual std::string GetBoardJson() override;
|
||||
|
||||
/**
|
||||
* Handle network event (called from WiFi manager callbacks)
|
||||
* @param event The network event type
|
||||
* @param data Additional data (e.g., SSID for Connecting/Connected events)
|
||||
*/
|
||||
void OnNetworkEvent(NetworkEvent event, const std::string& data = "");
|
||||
|
||||
/**
|
||||
* Start WiFi connection attempt
|
||||
*/
|
||||
void TryWifiConnect();
|
||||
|
||||
/**
|
||||
* Enter WiFi configuration mode
|
||||
*/
|
||||
void StartWifiConfigMode();
|
||||
|
||||
/**
|
||||
* WiFi connection timeout callback
|
||||
*/
|
||||
static void OnWifiConnectTimeout(void* arg);
|
||||
|
||||
public:
|
||||
WifiBoard();
|
||||
virtual ~WifiBoard();
|
||||
|
||||
virtual std::string GetBoardType() override;
|
||||
|
||||
/**
|
||||
* Start network connection asynchronously
|
||||
* This function returns immediately. Network events are notified through the callback set by SetNetworkEventCallback().
|
||||
*/
|
||||
virtual void StartNetwork() override;
|
||||
|
||||
virtual NetworkInterface* GetNetwork() override;
|
||||
virtual void SetNetworkEventCallback(NetworkEventCallback callback) override;
|
||||
virtual const char* GetNetworkStateIcon() override;
|
||||
virtual void SetPowerSaveMode(bool enabled) override;
|
||||
virtual void ResetWifiConfiguration();
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override;
|
||||
virtual AudioCodec* GetAudioCodec() override { return nullptr; }
|
||||
virtual std::string GetDeviceStatusJson() override;
|
||||
|
||||
/**
|
||||
* Enter WiFi configuration mode (thread-safe, can be called from any task)
|
||||
*/
|
||||
void EnterWifiConfigMode();
|
||||
|
||||
/**
|
||||
* Check if in WiFi config mode
|
||||
*/
|
||||
bool IsInWifiConfigMode() const;
|
||||
};
|
||||
|
||||
#endif // WIFI_BOARD_H
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#include "esp_io_expander_tca95xx_16bit.h"
|
||||
|
||||
@ -119,8 +118,9 @@ private:
|
||||
iot_button_register_cb(btn_a, BUTTON_SINGLE_CLICK, nullptr, [](void* button_handle, void* usr_data) {
|
||||
auto self = static_cast<Df_K10Board*>(usr_data);
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
self->ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
self->EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
}, this);
|
||||
@ -149,8 +149,9 @@ private:
|
||||
iot_button_register_cb(btn_b, BUTTON_SINGLE_CLICK, nullptr, [](void* button_handle, void* usr_data) {
|
||||
auto self = static_cast<Df_K10Board*>(usr_data);
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
self->ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
self->EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
}, this);
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include "led/gpio_led.h"
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/gpio.h>
|
||||
@ -22,8 +21,9 @@ class DfrobotEsp32S3AiCam : public WifiBoard {
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "led/gpio_led.h"
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/gpio.h>
|
||||
@ -36,8 +35,9 @@ private:
|
||||
check_time = 0;
|
||||
}
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
@ -46,13 +46,13 @@ private:
|
||||
ESP_LOGI(TAG, "DoubleClick times %d", click_times);
|
||||
if(click_times==3) {
|
||||
click_times = 0;
|
||||
ResetWifiConfiguration();
|
||||
EnterWifiConfigMode();
|
||||
}
|
||||
});
|
||||
|
||||
boot_button_.OnLongPress([this]() {
|
||||
if(click_times>=3) {
|
||||
ResetWifiConfiguration();
|
||||
EnterWifiConfigMode();
|
||||
} else {
|
||||
click_times = 0;
|
||||
check_time = 0;
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
#include "power_manager.h"
|
||||
#include "power_save_timer.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -105,8 +104,9 @@ private:
|
||||
boot_button_.OnClick([this]() {
|
||||
power_save_timer_->WakeUp();
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
@ -153,11 +153,11 @@ public:
|
||||
level = power_manager_->GetBatteryLevel();
|
||||
return true;
|
||||
}
|
||||
virtual void SetPowerSaveMode(bool enabled) override {
|
||||
if (!enabled) {
|
||||
virtual void SetPowerSaveLevel(PowerSaveLevel level) override {
|
||||
if (level != PowerSaveLevel::LOW_POWER) {
|
||||
power_save_timer_->WakeUp();
|
||||
}
|
||||
WifiBoard::SetPowerSaveMode(enabled);
|
||||
WifiBoard::SetPowerSaveLevel(level);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "backlight.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <driver/i2c_master.h>
|
||||
@ -380,7 +380,7 @@ private:
|
||||
SemaphoreHandle_t touch_isr_mux_;
|
||||
};
|
||||
|
||||
class EspS3Cat : public WifiBoard {
|
||||
class EchoEar : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
Cst816s* cst816s_;
|
||||
@ -390,6 +390,7 @@ private:
|
||||
PwmBacklight* backlight_ = nullptr;
|
||||
esp_timer_handle_t touchpad_timer_;
|
||||
esp_lcd_touch_handle_t tp; // LCD touch handle
|
||||
Esp32Camera* camera_ = nullptr;
|
||||
|
||||
void InitializeI2c()
|
||||
{
|
||||
@ -468,16 +469,15 @@ private:
|
||||
while (true) {
|
||||
if (touchpad->WaitForTouchEvent()) {
|
||||
auto &app = Application::GetInstance();
|
||||
auto &board = (EspS3Cat &)Board::GetInstance();
|
||||
auto &board = (EchoEar &)Board::GetInstance();
|
||||
|
||||
ESP_LOGI(TAG, "Touch event, TP_PIN_NUM_INT: %d", gpio_get_level(TP_PIN_NUM_INT));
|
||||
ESP_LOGD(TAG, "Touch event, TP_PIN_NUM_INT: %d", gpio_get_level(TP_PIN_NUM_INT));
|
||||
touchpad->UpdateTouchPoint();
|
||||
auto touch_event = touchpad->CheckTouchEvent();
|
||||
|
||||
if (touch_event == Cst816s::TOUCH_RELEASE) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting &&
|
||||
!WifiStation::GetInstance().IsConnected()) {
|
||||
board.ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
board.EnterWifiConfigMode();
|
||||
} else {
|
||||
app.ToggleChatState();
|
||||
}
|
||||
@ -507,7 +507,7 @@ private:
|
||||
gpio_config(&int_gpio_config);
|
||||
gpio_install_isr_service(0);
|
||||
gpio_intr_enable(TP_PIN_NUM_INT);
|
||||
gpio_isr_handler_add(TP_PIN_NUM_INT, EspS3Cat::touch_isr_callback, cst816s_);
|
||||
gpio_isr_handler_add(TP_PIN_NUM_INT, EchoEar::touch_isr_callback, cst816s_);
|
||||
}
|
||||
|
||||
void InitializeSpi()
|
||||
@ -567,9 +567,10 @@ private:
|
||||
{
|
||||
boot_button_.OnClick([this]() {
|
||||
auto &app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
ESP_LOGI(TAG, "Boot button pressed, enter WiFi configuration mode");
|
||||
ResetWifiConfiguration();
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
@ -583,8 +584,33 @@ private:
|
||||
gpio_set_level(POWER_CTRL, 0);
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
void InitializeCamera() {
|
||||
esp_video_init_usb_uvc_config_t usb_uvc_config = {
|
||||
.uvc = {
|
||||
.uvc_dev_num = 1,
|
||||
.task_stack = 4096,
|
||||
.task_priority = 5,
|
||||
.task_affinity = -1,
|
||||
},
|
||||
.usb = {
|
||||
.init_usb_host_lib = true,
|
||||
.task_stack = 4096,
|
||||
.task_priority = 5,
|
||||
.task_affinity = -1,
|
||||
},
|
||||
};
|
||||
|
||||
esp_video_init_config_t video_config = {
|
||||
.usb_uvc = &usb_uvc_config,
|
||||
};
|
||||
|
||||
camera_ = new Esp32Camera(video_config);
|
||||
}
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
|
||||
public:
|
||||
EspS3Cat() : boot_button_(BOOT_BUTTON_GPIO)
|
||||
EchoEar() : boot_button_(BOOT_BUTTON_GPIO)
|
||||
{
|
||||
InitializeI2c();
|
||||
uint8_t pcb_verison = DetectPcbVersion();
|
||||
@ -594,6 +620,9 @@ public:
|
||||
InitializeSpi();
|
||||
Initializest77916Display(pcb_verison);
|
||||
InitializeButtons();
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
InitializeCamera();
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override
|
||||
@ -628,6 +657,10 @@ public:
|
||||
{
|
||||
return backlight_;
|
||||
}
|
||||
|
||||
virtual Camera* GetCamera() override {
|
||||
return camera_;
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(EspS3Cat);
|
||||
DECLARE_BOARD(EchoEar);
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
"name": "echoear",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_USE_EMOTE_MESSAGE_STYLE=y",
|
||||
"CONFIG_FLASH_CUSTOM_ASSETS=y",
|
||||
"CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-echoear.bin\""
|
||||
"CONFIG_MMAP_FILE_NAME_LENGTH=32",
|
||||
"CONFIG_FLASH_EXPRESSION_ASSETS=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
[
|
||||
{"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20},
|
||||
{"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20}
|
||||
]
|
||||
@ -1,37 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "eye_anim",
|
||||
"align": "GFX_ALIGN_LEFT_MID",
|
||||
"x": 10,
|
||||
"y": 10
|
||||
},
|
||||
{
|
||||
"name": "status_icon",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": -100,
|
||||
"y": 38
|
||||
},
|
||||
{
|
||||
"name": "toast_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 40,
|
||||
"width": 160,
|
||||
"height": 40
|
||||
},
|
||||
{
|
||||
"name": "clock_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 40,
|
||||
"width": 60,
|
||||
"height": 50
|
||||
},
|
||||
{
|
||||
"name": "listen_anim",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 25
|
||||
}
|
||||
]
|
||||
|
||||
@ -47,5 +47,5 @@
|
||||
|
||||
#define BOOT_BUTTON_GPIO GPIO_NUM_0
|
||||
|
||||
#define ELECTRON_BOT_VERSION "1.1.3"
|
||||
#define ELECTRON_BOT_VERSION "2.0.4"
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
|
||||
@ -3,9 +3,7 @@
|
||||
"builds": [
|
||||
{
|
||||
"name": "electron-bot",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/16m.csv\""
|
||||
]
|
||||
"sdkconfig_append": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -5,7 +5,6 @@
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <esp_log.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#include "application.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
@ -76,9 +75,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting &&
|
||||
!WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -1,150 +1,139 @@
|
||||
#include "electron_emoji_display.h"
|
||||
#include "lvgl_theme.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <font_awesome.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "assets/lang_config.h"
|
||||
#include "display/lvgl_display/emoji_collection.h"
|
||||
#include "display/lvgl_display/lvgl_image.h"
|
||||
#include "display/lvgl_display/lvgl_theme.h"
|
||||
#include "otto_emoji_gif.h"
|
||||
|
||||
#define TAG "ElectronEmojiDisplay"
|
||||
|
||||
// 表情映射表 - 将多种表情映射到现有6个GIF
|
||||
const ElectronEmojiDisplay::EmotionMap ElectronEmojiDisplay::emotion_maps_[] = {
|
||||
// 中性/平静类表情 -> staticstate
|
||||
{"neutral", &staticstate},
|
||||
{"relaxed", &staticstate},
|
||||
{"sleepy", &staticstate},
|
||||
|
||||
// 积极/开心类表情 -> happy
|
||||
{"happy", &happy},
|
||||
{"laughing", &happy},
|
||||
{"funny", &happy},
|
||||
{"loving", &happy},
|
||||
{"confident", &happy},
|
||||
{"winking", &happy},
|
||||
{"cool", &happy},
|
||||
{"delicious", &happy},
|
||||
{"kissy", &happy},
|
||||
{"silly", &happy},
|
||||
|
||||
// 悲伤类表情 -> sad
|
||||
{"sad", &sad},
|
||||
{"crying", &sad},
|
||||
|
||||
// 愤怒类表情 -> anger
|
||||
{"angry", &anger},
|
||||
|
||||
// 惊讶类表情 -> scare
|
||||
{"surprised", &scare},
|
||||
{"shocked", &scare},
|
||||
|
||||
// 思考/困惑类表情 -> buxue
|
||||
{"thinking", &buxue},
|
||||
{"confused", &buxue},
|
||||
{"embarrassed", &buxue},
|
||||
|
||||
{nullptr, nullptr} // 结束标记
|
||||
};
|
||||
|
||||
ElectronEmojiDisplay::ElectronEmojiDisplay(esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_handle_t panel, int width, int height,
|
||||
int offset_x, int offset_y, bool mirror_x, bool mirror_y,
|
||||
ElectronEmojiDisplay::ElectronEmojiDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y,
|
||||
bool swap_xy)
|
||||
: SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy),
|
||||
emotion_gif_(nullptr) {
|
||||
SetupGifContainer();
|
||||
: SpiLcdDisplay(panel_io, panel, width, height, offset_x, offset_y, mirror_x, mirror_y, swap_xy) {
|
||||
InitializeElectronEmojis();
|
||||
SetupChatLabel();
|
||||
}
|
||||
|
||||
void ElectronEmojiDisplay::SetupGifContainer() {
|
||||
void ElectronEmojiDisplay::InitializeElectronEmojis() {
|
||||
ESP_LOGI(TAG, "初始化Electron GIF表情");
|
||||
|
||||
auto otto_emoji_collection = std::make_shared<EmojiCollection>();
|
||||
|
||||
// 中性/平静类表情 -> staticstate
|
||||
otto_emoji_collection->AddEmoji("staticstate", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("neutral", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("relaxed", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("sleepy", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
otto_emoji_collection->AddEmoji("idle", new LvglRawImage((void*)staticstate.data, staticstate.data_size));
|
||||
|
||||
// 积极/开心类表情 -> happy
|
||||
otto_emoji_collection->AddEmoji("happy", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("laughing", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("funny", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("loving", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("confident", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("winking", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("cool", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("delicious", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("kissy", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
otto_emoji_collection->AddEmoji("silly", new LvglRawImage((void*)happy.data, happy.data_size));
|
||||
|
||||
// 悲伤类表情 -> sad
|
||||
otto_emoji_collection->AddEmoji("sad", new LvglRawImage((void*)sad.data, sad.data_size));
|
||||
otto_emoji_collection->AddEmoji("crying", new LvglRawImage((void*)sad.data, sad.data_size));
|
||||
|
||||
// 愤怒类表情 -> anger
|
||||
otto_emoji_collection->AddEmoji("anger", new LvglRawImage((void*)anger.data, anger.data_size));
|
||||
otto_emoji_collection->AddEmoji("angry", new LvglRawImage((void*)anger.data, anger.data_size));
|
||||
|
||||
// 惊讶类表情 -> scare
|
||||
otto_emoji_collection->AddEmoji("scare", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
otto_emoji_collection->AddEmoji("surprised", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
otto_emoji_collection->AddEmoji("shocked", new LvglRawImage((void*)scare.data, scare.data_size));
|
||||
|
||||
// 思考/困惑类表情 -> buxue
|
||||
otto_emoji_collection->AddEmoji("buxue", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("thinking", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("confused", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
otto_emoji_collection->AddEmoji("embarrassed", new LvglRawImage((void*)buxue.data, buxue.data_size));
|
||||
|
||||
// 将表情集合添加到主题中
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto light_theme = theme_manager.GetTheme("light");
|
||||
auto dark_theme = theme_manager.GetTheme("dark");
|
||||
|
||||
if (light_theme != nullptr) {
|
||||
light_theme->set_emoji_collection(otto_emoji_collection);
|
||||
}
|
||||
if (dark_theme != nullptr) {
|
||||
dark_theme->set_emoji_collection(otto_emoji_collection);
|
||||
}
|
||||
|
||||
// 设置默认表情为staticstate
|
||||
SetEmotion("staticstate");
|
||||
|
||||
ESP_LOGI(TAG, "Electron GIF表情初始化完成");
|
||||
}
|
||||
|
||||
void ElectronEmojiDisplay::SetupChatLabel() {
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
if (emoji_label_) {
|
||||
lv_obj_del(emoji_label_);
|
||||
}
|
||||
if (chat_message_label_) {
|
||||
lv_obj_del(chat_message_label_);
|
||||
}
|
||||
if (content_) {
|
||||
lv_obj_del(content_);
|
||||
}
|
||||
|
||||
content_ = lv_obj_create(container_);
|
||||
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
|
||||
lv_obj_set_size(content_, LV_HOR_RES, LV_HOR_RES);
|
||||
lv_obj_set_style_bg_opa(content_, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(content_, 0, 0);
|
||||
lv_obj_set_flex_grow(content_, 1);
|
||||
lv_obj_center(content_);
|
||||
|
||||
emoji_label_ = lv_label_create(content_);
|
||||
lv_label_set_text(emoji_label_, "");
|
||||
lv_obj_set_width(emoji_label_, 0);
|
||||
lv_obj_set_style_border_width(emoji_label_, 0, 0);
|
||||
lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
emotion_gif_ = lv_gif_create(content_);
|
||||
int gif_size = LV_HOR_RES;
|
||||
lv_obj_set_size(emotion_gif_, gif_size, gif_size);
|
||||
lv_obj_set_style_border_width(emotion_gif_, 0, 0);
|
||||
lv_obj_set_style_bg_opa(emotion_gif_, LV_OPA_TRANSP, 0);
|
||||
lv_obj_center(emotion_gif_);
|
||||
lv_gif_set_src(emotion_gif_, &staticstate);
|
||||
|
||||
chat_message_label_ = lv_label_create(content_);
|
||||
chat_message_label_ = lv_label_create(container_);
|
||||
lv_label_set_text(chat_message_label_, "");
|
||||
lv_obj_set_width(chat_message_label_, LV_HOR_RES * 0.9);
|
||||
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_width(chat_message_label_, width_ * 0.9); // 限制宽度为屏幕宽度的 90%
|
||||
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); // 设置为自动换行模式
|
||||
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐
|
||||
lv_obj_set_style_text_color(chat_message_label_, lv_color_white(), 0);
|
||||
lv_obj_set_style_border_width(chat_message_label_, 0, 0);
|
||||
|
||||
lv_obj_set_style_bg_opa(chat_message_label_, LV_OPA_70, 0);
|
||||
lv_obj_set_style_bg_color(chat_message_label_, lv_color_black(), 0);
|
||||
lv_obj_set_style_pad_ver(chat_message_label_, 5, 0);
|
||||
|
||||
lv_obj_align(chat_message_label_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto theme = theme_manager.GetTheme("dark");
|
||||
if (theme != nullptr) {
|
||||
LcdDisplay::SetTheme(theme);
|
||||
}
|
||||
SetTheme(LvglThemeManager::GetInstance().GetTheme("dark"));
|
||||
}
|
||||
|
||||
void ElectronEmojiDisplay::SetEmotion(const char* emotion) {
|
||||
if (!emotion || !emotion_gif_) {
|
||||
return;
|
||||
}
|
||||
|
||||
LV_FONT_DECLARE(OTTO_ICON_FONT);
|
||||
void ElectronEmojiDisplay::SetStatus(const char* status) {
|
||||
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
||||
auto text_font = lvgl_theme->text_font()->font();
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
for (const auto& map : emotion_maps_) {
|
||||
if (map.name && strcmp(map.name, emotion) == 0) {
|
||||
lv_gif_set_src(emotion_gif_, map.gif);
|
||||
ESP_LOGI(TAG, "设置表情: %s", emotion);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lv_gif_set_src(emotion_gif_, &staticstate);
|
||||
ESP_LOGI(TAG, "未知表情'%s',使用默认", emotion);
|
||||
}
|
||||
|
||||
void ElectronEmojiDisplay::SetChatMessage(const char* role, const char* content) {
|
||||
DisplayLockGuard lock(this);
|
||||
if (chat_message_label_ == nullptr) {
|
||||
if (!status) {
|
||||
ESP_LOGE(TAG, "SetStatus: status is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
if (content == nullptr || strlen(content) == 0) {
|
||||
lv_obj_add_flag(chat_message_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
if (strcmp(status, Lang::Strings::LISTENING) == 0) {
|
||||
lv_obj_set_style_text_font(status_label_, &OTTO_ICON_FONT, 0);
|
||||
lv_label_set_text(status_label_, "\xEF\x84\xB0"); // U+F130 麦克风图标
|
||||
lv_obj_clear_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(network_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(battery_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
} else if (strcmp(status, Lang::Strings::SPEAKING) == 0) {
|
||||
lv_obj_set_style_text_font(status_label_, &OTTO_ICON_FONT, 0);
|
||||
lv_label_set_text(status_label_, "\xEF\x80\xA8"); // U+F028 说话图标
|
||||
lv_obj_clear_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(network_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(battery_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
} else if (strcmp(status, Lang::Strings::CONNECTING) == 0) {
|
||||
lv_obj_set_style_text_font(status_label_, &OTTO_ICON_FONT, 0);
|
||||
lv_label_set_text(status_label_, "\xEF\x83\x81"); // U+F0c1 连接图标
|
||||
lv_obj_clear_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
} else if (strcmp(status, Lang::Strings::STANDBY) == 0) {
|
||||
lv_obj_set_style_text_font(status_label_, text_font, 0);
|
||||
lv_label_set_text(status_label_, "");
|
||||
lv_obj_clear_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
lv_label_set_text(chat_message_label_, content);
|
||||
lv_obj_remove_flag(chat_message_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
ESP_LOGI(TAG, "设置聊天消息 [%s]: %s", role, content);
|
||||
lv_obj_set_style_text_font(status_label_, text_font, 0);
|
||||
lv_label_set_text(status_label_, status);
|
||||
lv_obj_clear_flag(status_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(network_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(battery_label_, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
@ -1,48 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <libs/gif/lv_gif.h>
|
||||
|
||||
#include "display/lcd_display.h"
|
||||
|
||||
// Electron Bot表情GIF声明 - 使用与Otto相同的6个表情
|
||||
LV_IMAGE_DECLARE(staticstate); // 静态状态/中性表情
|
||||
LV_IMAGE_DECLARE(sad); // 悲伤
|
||||
LV_IMAGE_DECLARE(happy); // 开心
|
||||
LV_IMAGE_DECLARE(scare); // 惊吓/惊讶
|
||||
LV_IMAGE_DECLARE(buxue); // 不学/困惑
|
||||
LV_IMAGE_DECLARE(anger); // 愤怒
|
||||
|
||||
/**
|
||||
* @brief Electron Bot GIF表情显示类
|
||||
* 继承LcdDisplay,添加GIF表情支持
|
||||
* @brief Electron机器人GIF表情显示类
|
||||
* 继承SpiLcdDisplay,通过EmojiCollection添加GIF表情支持
|
||||
*/
|
||||
class ElectronEmojiDisplay : public SpiLcdDisplay {
|
||||
public:
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数,参数与SpiLcdDisplay相同
|
||||
*/
|
||||
ElectronEmojiDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
||||
int width, int height, int offset_x, int offset_y, bool mirror_x,
|
||||
bool mirror_y, bool swap_xy);
|
||||
ElectronEmojiDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy);
|
||||
|
||||
virtual ~ElectronEmojiDisplay() = default;
|
||||
virtual void SetStatus(const char* status) override;
|
||||
|
||||
// 重写表情设置方法
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
|
||||
// 重写聊天消息设置方法
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
|
||||
private:
|
||||
void SetupGifContainer();
|
||||
|
||||
lv_obj_t* emotion_gif_; ///< GIF表情组件
|
||||
|
||||
// 表情映射
|
||||
struct EmotionMap {
|
||||
const char* name;
|
||||
const lv_image_dsc_t* gif;
|
||||
};
|
||||
|
||||
static const EmotionMap emotion_maps_[];
|
||||
private:
|
||||
void InitializeElectronEmojis();
|
||||
void SetupChatLabel();
|
||||
};
|
||||
121
main/boards/electron-bot/otto_icon_font.c
Normal file
121
main/boards/electron-bot/otto_icon_font.c
Normal file
@ -0,0 +1,121 @@
|
||||
/*******************************************************************************
|
||||
* Size: 20 px
|
||||
* Bpp: 1
|
||||
* Opts: --bpp 1 --size 20 --no-compress --stride 1 --align 1 --font fontawesome-webfont.ttf --range 61744,61633,61480 --format lvgl -o OTTO_ICON_FONT.c
|
||||
******************************************************************************/
|
||||
|
||||
#ifdef __has_include
|
||||
#if __has_include("lvgl.h")
|
||||
#ifndef LV_LVGL_H_INCLUDE_SIMPLE
|
||||
#define LV_LVGL_H_INCLUDE_SIMPLE
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
|
||||
#include "lvgl.h"
|
||||
#else
|
||||
#include "lvgl/lvgl.h"
|
||||
#endif
|
||||
|
||||
#ifndef ENABLE_OTTO_ICON_FONT
|
||||
#define ENABLE_OTTO_ICON_FONT 1
|
||||
#endif
|
||||
|
||||
#if ENABLE_OTTO_ICON_FONT
|
||||
|
||||
/*-----------------
|
||||
* BITMAPS
|
||||
*----------------*/
|
||||
|
||||
/*Store the image of the glyphs*/
|
||||
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
|
||||
/* U+F028 "" */
|
||||
0x0, 0x6, 0x0, 0x10, 0xe0, 0x6, 0x6, 0x3, 0xc6, 0x60, 0xf8, 0x65, 0xff, 0x24, 0xff, 0xe6, 0x4f, 0xfc, 0x49, 0xff, 0x89, 0x3f, 0xf3, 0x27, 0xfe, 0x49, 0x87, 0xc3, 0x20, 0x78, 0xcc, 0x3, 0x3, 0x0,
|
||||
0x21, 0xc0, 0x0, 0x30,
|
||||
|
||||
/* U+F0C1 "" */
|
||||
0x1e, 0x0, 0xf, 0xc0, 0x7, 0x38, 0x3, 0x87, 0x0, 0xc0, 0xc0, 0x30, 0xb0, 0x7, 0x3c, 0x0, 0xef, 0x0, 0x1f, 0xfe, 0x3, 0xff, 0xc0, 0x7, 0x38, 0x3, 0xe7, 0x0, 0xd0, 0xc0, 0x30, 0x30, 0x6, 0x1c, 0x0,
|
||||
0xce, 0x0, 0x1f, 0x0, 0x3, 0x80,
|
||||
|
||||
/* U+F130 "" */
|
||||
0x7, 0x0, 0xfe, 0x7, 0xf0, 0x3f, 0x81, 0xfc, 0xf, 0xe0, 0x7f, 0x13, 0xf9, 0x9f, 0xcc, 0xfe, 0x67, 0xf3, 0xbf, 0xb4, 0x73, 0x18, 0x30, 0x7f, 0x0, 0x60, 0x2, 0x1, 0xff, 0x0};
|
||||
|
||||
/*---------------------
|
||||
* GLYPH DESCRIPTION
|
||||
*--------------------*/
|
||||
|
||||
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {{.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
|
||||
{.bitmap_index = 0, .adv_w = 297, .box_w = 19, .box_h = 16, .ofs_x = 0, .ofs_y = -1},
|
||||
{.bitmap_index = 38, .adv_w = 297, .box_w = 18, .box_h = 18, .ofs_x = 0, .ofs_y = -1},
|
||||
{.bitmap_index = 79, .adv_w = 206, .box_w = 13, .box_h = 18, .ofs_x = 0, .ofs_y = -1}};
|
||||
|
||||
/*---------------------
|
||||
* CHARACTER MAPPING
|
||||
*--------------------*/
|
||||
|
||||
static const uint16_t unicode_list_0[] = {0x0, 0x99, 0x108};
|
||||
|
||||
/*Collect the unicode lists and glyph_id offsets*/
|
||||
static const lv_font_fmt_txt_cmap_t cmaps[] = {
|
||||
{.range_start = 61480, .range_length = 265, .glyph_id_start = 1, .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 3, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY}};
|
||||
|
||||
/*--------------------
|
||||
* ALL CUSTOM DATA
|
||||
*--------------------*/
|
||||
|
||||
#if LVGL_VERSION_MAJOR == 8
|
||||
/*Store all the custom data of the font*/
|
||||
static lv_font_fmt_txt_glyph_cache_t cache;
|
||||
#endif
|
||||
|
||||
#if LVGL_VERSION_MAJOR >= 8
|
||||
static const lv_font_fmt_txt_dsc_t otto_icon_font_dsc = {
|
||||
#else
|
||||
static lv_font_fmt_txt_dsc_t otto_icon_font_dsc = {
|
||||
#endif
|
||||
.glyph_bitmap = glyph_bitmap,
|
||||
.glyph_dsc = glyph_dsc,
|
||||
.cmaps = cmaps,
|
||||
.kern_dsc = NULL,
|
||||
.kern_scale = 0,
|
||||
.cmap_num = 1,
|
||||
.bpp = 1,
|
||||
.kern_classes = 0,
|
||||
.bitmap_format = 0,
|
||||
#if LVGL_VERSION_MAJOR == 8
|
||||
.cache = &cache
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
/*-----------------
|
||||
* PUBLIC FONT
|
||||
*----------------*/
|
||||
|
||||
/*Initialize a public general font descriptor*/
|
||||
#if LVGL_VERSION_MAJOR >= 8
|
||||
const lv_font_t OTTO_ICON_FONT = {
|
||||
#else
|
||||
lv_font_t OTTO_ICON_FONT = {
|
||||
#endif
|
||||
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
|
||||
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
|
||||
.line_height = 18, /*The maximum line height required by the font*/
|
||||
.base_line = 1, /*Baseline measured from the bottom of the line*/
|
||||
#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
|
||||
.subpx = LV_FONT_SUBPX_NONE,
|
||||
#endif
|
||||
#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
|
||||
.underline_position = 0,
|
||||
.underline_thickness = 0,
|
||||
#endif
|
||||
.static_bitmap = 0,
|
||||
.dsc = &otto_icon_font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
|
||||
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
|
||||
.fallback = NULL,
|
||||
#endif
|
||||
.user_data = NULL,
|
||||
};
|
||||
|
||||
#endif /*#if ENABLE_OTTO_ICON_FONT*/
|
||||
@ -1,22 +0,0 @@
|
||||
[
|
||||
{"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20},
|
||||
{"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20}
|
||||
]
|
||||
@ -12,7 +12,6 @@
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#define TAG "EspBox3Board"
|
||||
|
||||
@ -74,8 +73,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "eye_anim",
|
||||
"align": "GFX_ALIGN_LEFT_MID",
|
||||
"x": 10,
|
||||
"y": 30
|
||||
},
|
||||
{
|
||||
"name": "status_icon",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": -120,
|
||||
"y": 18
|
||||
},
|
||||
{
|
||||
"name": "toast_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
"width": 200,
|
||||
"height": 40
|
||||
},
|
||||
{
|
||||
"name": "clock_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
"width": 200,
|
||||
"height": 50
|
||||
},
|
||||
{
|
||||
"name": "listen_anim",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 5
|
||||
}
|
||||
]
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#define TAG "EspBoxBoardLite"
|
||||
|
||||
@ -98,9 +97,10 @@ private:
|
||||
|
||||
void TogleState() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
}
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "display/emote_display.h"
|
||||
#include "esp_lcd_ili9341.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
@ -10,7 +11,6 @@
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <wifi_station.h>
|
||||
|
||||
#define TAG "EspBoxBoard"
|
||||
|
||||
@ -39,7 +39,7 @@ class EspBox3Board : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
Button boot_button_;
|
||||
LcdDisplay* display_;
|
||||
Display* display_;
|
||||
|
||||
void InitializeI2c() {
|
||||
// Initialize I2C peripheral
|
||||
@ -72,8 +72,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
@ -125,8 +126,13 @@ private:
|
||||
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
|
||||
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
|
||||
esp_lcd_panel_disp_on_off(panel, true);
|
||||
|
||||
#if CONFIG_USE_EMOTE_MESSAGE_STYLE
|
||||
display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||
#else
|
||||
display_ = new SpiLcdDisplay(panel_io, panel,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
@ -23,7 +22,7 @@
|
||||
#include "servo_dog_ctrl.h"
|
||||
#include "led_strip.h"
|
||||
#include "driver/rmt_tx.h"
|
||||
#include "device_state_event.h"
|
||||
#include "device_state.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
@ -171,8 +170,10 @@ private:
|
||||
|
||||
boot_button_.OnClick([this]() {
|
||||
auto &app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
// During startup (before connected), pressing BOOT button enters Wi-Fi config mode without reboot
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
@ -223,6 +224,7 @@ private:
|
||||
SetLedColor(0x00, 0x00, 0x00);
|
||||
|
||||
#ifdef CONFIG_ESP_HI_WEB_CONTROL_ENABLED
|
||||
esp_event_loop_create_default();
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED,
|
||||
&wifi_event_handler, this));
|
||||
#endif //CONFIG_ESP_HI_WEB_CONTROL_ENABLED
|
||||
|
||||
@ -5,7 +5,13 @@ Board support for ESP-P4-Function-EV-Board. Wi‑Fi uses ESP‑Hosted via the on
|
||||
## Features
|
||||
- Wi‑Fi: `esp_wifi_remote` + `esp_hosted` (SDIO) with ESP32‑C6 co‑processor
|
||||
- Display: 7" MIPI‑DSI LCD (1024×600) via adapter; can also run headless
|
||||
- Audio: Can run with dummy codec; board includes ES8311 + PA if needed
|
||||
- Audio: ES8311 codec with speaker and microphone support
|
||||
- Touch: GT911 capacitive touch controller
|
||||
- SD Card: MicroSD card support (MMC mode)
|
||||
- Camera: MIPI-CSI camera interface with fallback DVP configuration (OV5647, SC2336 sensors supported)
|
||||
- USB: USB host/device support
|
||||
- SPIFFS: Built-in flash filesystem support
|
||||
- Fonts: Custom font support with Unicode characters (Vietnamese, Chinese, etc.)
|
||||
|
||||
## Configure
|
||||
In `menuconfig`: Xiaozhi Assistant -> Board Type -> ESP-P4-Function-EV-Board
|
||||
|
||||
@ -3,18 +3,20 @@
|
||||
// Display
|
||||
#include "display/display.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "lvgl_theme.h"
|
||||
// Backlight
|
||||
// PwmBacklight is declared in backlight headers pulled by display/lcd_display includes via lvgl stack
|
||||
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <inttypes.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
#include <soc/clk_tree_defs.h>
|
||||
// SD card
|
||||
#include <esp_vfs_fat.h>
|
||||
#include <sdmmc_cmd.h>
|
||||
@ -43,6 +45,7 @@ private:
|
||||
Button boot_button_;
|
||||
LcdDisplay *display_ = nullptr;
|
||||
esp_lcd_touch_handle_t tp_ = nullptr;
|
||||
Esp32Camera* camera_ = nullptr;
|
||||
|
||||
void InitializeI2cBuses()
|
||||
{
|
||||
@ -61,7 +64,7 @@ private:
|
||||
bsp_display_config_t config = {
|
||||
.hdmi_resolution = BSP_HDMI_RES_NONE,
|
||||
.dsi_bus = {
|
||||
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT,
|
||||
.phy_clk_src = (mipi_dsi_phy_clock_source_t)SOC_MOD_CLK_PLL_F20M,
|
||||
.lane_bit_rate_mbps = 1000,
|
||||
},
|
||||
};
|
||||
@ -77,10 +80,12 @@ private:
|
||||
boot_button_.OnClick([this]()
|
||||
{
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState(); });
|
||||
app.ToggleChatState();
|
||||
});
|
||||
}
|
||||
|
||||
void InitializeTouch()
|
||||
@ -88,6 +93,92 @@ private:
|
||||
ESP_ERROR_CHECK(bsp_touch_new(NULL, &tp_));
|
||||
}
|
||||
|
||||
void InitializeSdCard()
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing SD card");
|
||||
esp_err_t ret = bsp_sdcard_mount();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to mount SD card: %s", esp_err_to_name(ret));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "SD card mounted successfully");
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeCamera()
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing camera");
|
||||
|
||||
// Use BSP camera initialization for ESP-P4
|
||||
bsp_camera_cfg_t camera_cfg = {0};
|
||||
esp_err_t ret = bsp_camera_start(&camera_cfg);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BSP camera: %s", esp_err_to_name(ret));
|
||||
ESP_LOGI(TAG, "Attempting alternative camera initialization");
|
||||
|
||||
// Alternative: Direct Esp32Camera initialization if BSP fails
|
||||
// This provides more control over camera configuration
|
||||
static esp_cam_ctlr_dvp_pin_config_t dvp_pin_config = {
|
||||
.data_width = CAM_CTLR_DATA_WIDTH_8,
|
||||
.data_io = {
|
||||
[0] = BSP_I2C_SDA, // Reuse I2C pins if camera pins not defined
|
||||
[1] = BSP_I2C_SCL,
|
||||
[2] = GPIO_NUM_NC,
|
||||
[3] = GPIO_NUM_NC,
|
||||
[4] = GPIO_NUM_NC,
|
||||
[5] = GPIO_NUM_NC,
|
||||
[6] = GPIO_NUM_NC,
|
||||
[7] = GPIO_NUM_NC,
|
||||
},
|
||||
.vsync_io = GPIO_NUM_NC,
|
||||
.de_io = GPIO_NUM_NC,
|
||||
.pclk_io = GPIO_NUM_NC,
|
||||
.xclk_io = GPIO_NUM_NC,
|
||||
};
|
||||
|
||||
esp_video_init_sccb_config_t sccb_config = {
|
||||
.init_sccb = false, // Use existing I2C bus
|
||||
.i2c_handle = codec_i2c_bus_, // Reuse the existing I2C bus
|
||||
.freq = 100000,
|
||||
};
|
||||
|
||||
esp_video_init_dvp_config_t dvp_config = {
|
||||
.sccb_config = sccb_config,
|
||||
.reset_pin = GPIO_NUM_NC,
|
||||
.pwdn_pin = GPIO_NUM_NC,
|
||||
.dvp_pin = dvp_pin_config,
|
||||
.xclk_freq = 20000000, // 20MHz typical for cameras
|
||||
};
|
||||
|
||||
esp_video_init_config_t video_config = {
|
||||
.dvp = &dvp_config,
|
||||
};
|
||||
|
||||
// Try to create camera with direct configuration
|
||||
camera_ = new Esp32Camera(video_config);
|
||||
ESP_LOGI(TAG, "Camera initialized with direct configuration");
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Camera initialized successfully via BSP");
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeFonts()
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing font support");
|
||||
// Font initialization is handled by the Assets system
|
||||
// The board supports loading fonts from assets partition
|
||||
// Verify that fonts are properly loaded by checking theme
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto current_theme = theme_manager.GetTheme("light");
|
||||
if (current_theme != nullptr) {
|
||||
auto text_font = current_theme->text_font();
|
||||
if (text_font != nullptr && text_font->font() != nullptr) {
|
||||
ESP_LOGI(TAG, "Custom font loaded successfully: line_height=%d", text_font->font()->line_height);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Custom font not loaded, using built-in font");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
ESP32P4FunctionEvBoard() : boot_button_(0)
|
||||
@ -97,6 +188,9 @@ public:
|
||||
InitializeLCD();
|
||||
InitializeButtons();
|
||||
InitializeTouch();
|
||||
InitializeSdCard();
|
||||
InitializeCamera();
|
||||
InitializeFonts();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
@ -105,6 +199,11 @@ public:
|
||||
// Clean up display pointer
|
||||
delete display_;
|
||||
display_ = nullptr;
|
||||
// Unmount SD card
|
||||
esp_err_t ret = bsp_sdcard_unmount();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(ret));
|
||||
}
|
||||
// If other resources need cleanup, add here
|
||||
}
|
||||
|
||||
@ -124,6 +223,11 @@ public:
|
||||
static PwmBacklight backlight(BSP_LCD_BACKLIGHT, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
return &backlight;
|
||||
}
|
||||
|
||||
virtual Camera *GetCamera() override
|
||||
{
|
||||
return camera_;
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(ESP32P4FunctionEvBoard);
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
#include "button.h"
|
||||
#include "led/single_led.h"
|
||||
#include "pin_config.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include "esp_lcd_gc9503.h"
|
||||
@ -27,6 +27,8 @@ private:
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
Button boot_button_;
|
||||
LcdDisplay* display_;
|
||||
Esp32Camera* camera_;
|
||||
|
||||
//add support ev board lcd
|
||||
esp_io_expander_handle_t expander = NULL;
|
||||
|
||||
@ -153,8 +155,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
});
|
||||
boot_button_.OnPressDown([this]() {
|
||||
@ -195,12 +198,39 @@ private:
|
||||
lvgl_port_add_touch(&touch_cfg);
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
void InitializeCamera() {
|
||||
esp_video_init_usb_uvc_config_t usb_uvc_config = {
|
||||
.uvc = {
|
||||
.uvc_dev_num = 1,
|
||||
.task_stack = 4096,
|
||||
.task_priority = 5,
|
||||
.task_affinity = -1,
|
||||
},
|
||||
.usb = {
|
||||
.init_usb_host_lib = true,
|
||||
.task_stack = 4096,
|
||||
.task_priority = 5,
|
||||
.task_affinity = -1,
|
||||
},
|
||||
};
|
||||
}
|
||||
esp_video_init_config_t video_config = {
|
||||
.usb_uvc = &usb_uvc_config,
|
||||
};
|
||||
camera_ = new Esp32Camera(video_config);
|
||||
}
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
|
||||
public:
|
||||
ESP_S3_LCD_EV_Board_2() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeCodecI2c();
|
||||
InitializeButtons();
|
||||
InitializeRGB_GC9503V_Display();
|
||||
InitializeTouch();
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
InitializeCamera();
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
@ -230,6 +260,12 @@ public:
|
||||
return &led;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
virtual Camera* GetCamera() override {
|
||||
return camera_;
|
||||
}
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
|
||||
};
|
||||
|
||||
DECLARE_BOARD(ESP_S3_LCD_EV_Board_2);
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
#include "button.h"
|
||||
#include "led/single_led.h"
|
||||
#include "pin_config.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include "esp_lcd_gc9503.h"
|
||||
@ -25,6 +25,7 @@ private:
|
||||
i2c_master_bus_handle_t codec_i2c_bus_;
|
||||
Button boot_button_;
|
||||
LcdDisplay* display_;
|
||||
Esp32Camera* camera_;
|
||||
|
||||
//add support ev board lcd
|
||||
esp_io_expander_handle_t expander = NULL;
|
||||
@ -153,8 +154,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
});
|
||||
boot_button_.OnPressDown([this]() {
|
||||
@ -165,11 +167,39 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
void InitializeCamera() {
|
||||
esp_video_init_usb_uvc_config_t usb_uvc_config = {
|
||||
.uvc = {
|
||||
.uvc_dev_num = 1,
|
||||
.task_stack = 4096,
|
||||
.task_priority = 5,
|
||||
.task_affinity = -1,
|
||||
},
|
||||
.usb = {
|
||||
.init_usb_host_lib = true,
|
||||
.task_stack = 4096,
|
||||
.task_priority = 5,
|
||||
.task_affinity = -1,
|
||||
},
|
||||
};
|
||||
|
||||
esp_video_init_config_t video_config = {
|
||||
.usb_uvc = &usb_uvc_config,
|
||||
};
|
||||
|
||||
camera_ = new Esp32Camera(video_config);
|
||||
}
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
|
||||
public:
|
||||
ESP_S3_LCD_EV_Board() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeCodecI2c();
|
||||
InitializeButtons();
|
||||
InitializeRGB_GC9503V_Display();
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
InitializeCamera();
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
@ -199,6 +229,12 @@ public:
|
||||
return &led;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
virtual Camera* GetCamera() override {
|
||||
return camera_;
|
||||
}
|
||||
#endif // CONFIG_ESP_VIDEO_ENABLE_USB_UVC_VIDEO_DEVICE
|
||||
|
||||
};
|
||||
|
||||
DECLARE_BOARD(ESP_S3_LCD_EV_Board);
|
||||
|
||||
39
main/boards/esp-sensairshuttle/README.md
Normal file
39
main/boards/esp-sensairshuttle/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# ESP-SensairShuttle
|
||||
|
||||
## 简介
|
||||
|
||||
<div align="center">
|
||||
<a href="https://docs.espressif.com/projects/esp-dev-kits/zh_CN/latest/esp32c5/esp-sensairshuttle/index.html">
|
||||
<b> 开发版文档 </b>
|
||||
</a>
|
||||
|
|
||||
<a href="#传感器--shuttleboard-子板支持">
|
||||
<b> 传感器 & <i>ShuttleBoard</i> 文档 </b>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
ESP-SensairShuttle 是乐鑫携手 Bosch Sensortec 面向**动作感知**与**大模型人机交互**场景联合推出的开发板。
|
||||
|
||||
ESP-SensairShuttle 主控采用乐鑫 ESP32-C5-WROOM-1-N16R8 模组,具有 2.4 & 5 GHz 双频 Wi-Fi 6 (802.11ax)、Bluetooth® 5 (LE)、Zigbee 及 Thread (802.15.4) 无线通信能力。
|
||||
|
||||
## 传感器 & _ShuttleBoard_ 子板支持
|
||||
|
||||
即将推出,敬请期待。
|
||||
|
||||
## 配置、编译命令
|
||||
|
||||
由于 ESP-SensairShuttle 需要配置较多的 sdkconfig 选项,推荐使用编译脚本编译。
|
||||
|
||||
**编译**
|
||||
|
||||
```bash
|
||||
python ./scripts/release.py esp-sensairshuttle
|
||||
```
|
||||
|
||||
如需手动编译,请参考 `main/boards/esp-sensairshuttle/config.json` 修改 menuconfig 对应选项。
|
||||
|
||||
**烧录**
|
||||
|
||||
```bash
|
||||
idf.py flash
|
||||
```
|
||||
249
main/boards/esp-sensairshuttle/adc_pdm_audio_codec.cc
Normal file
249
main/boards/esp-sensairshuttle/adc_pdm_audio_codec.cc
Normal file
@ -0,0 +1,249 @@
|
||||
#include "adc_pdm_audio_codec.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_timer.h>
|
||||
#include <driver/i2c.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/i2s_tdm.h>
|
||||
#include "adc_mic.h"
|
||||
#include "driver/i2s_pdm.h"
|
||||
#include "soc/gpio_sig_map.h"
|
||||
#include "soc/io_mux_reg.h"
|
||||
#include "hal/rtc_io_hal.h"
|
||||
#include "hal/gpio_ll.h"
|
||||
#include "settings.h"
|
||||
#include "config.h"
|
||||
|
||||
static const char TAG[] = "AdcPdmAudioCodec";
|
||||
|
||||
#define BSP_I2S_GPIO_CFG(_dout) \
|
||||
{ \
|
||||
.clk = GPIO_NUM_NC, \
|
||||
.dout = _dout, \
|
||||
.invert_flags = { \
|
||||
.clk_inv = false, \
|
||||
}, \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Mono Duplex I2S configuration structure
|
||||
*
|
||||
* This configuration is used by default in bsp_audio_init()
|
||||
*/
|
||||
#define BSP_I2S_DUPLEX_MONO_CFG(_sample_rate, _dout) \
|
||||
{ \
|
||||
.clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(_sample_rate), \
|
||||
.slot_cfg = I2S_PDM_TX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), \
|
||||
.gpio_cfg = BSP_I2S_GPIO_CFG(_dout), \
|
||||
}
|
||||
|
||||
AdcPdmAudioCodec::AdcPdmAudioCodec(int input_sample_rate, int output_sample_rate,
|
||||
uint32_t adc_mic_channel, gpio_num_t pdm_speak_p,gpio_num_t pdm_speak_n, gpio_num_t pa_ctl) {
|
||||
|
||||
input_reference_ = false;
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
output_sample_rate_ = output_sample_rate;
|
||||
|
||||
uint8_t adc_channel[1] = {0};
|
||||
adc_channel[0] = adc_mic_channel;
|
||||
|
||||
audio_codec_adc_cfg_t cfg = {
|
||||
.handle = NULL,
|
||||
.max_store_buf_size = 1024 * 2,
|
||||
.conv_frame_size = 1024,
|
||||
.unit_id = ADC_UNIT_1,
|
||||
.adc_channel_list = adc_channel,
|
||||
.adc_channel_num = sizeof(adc_channel) / sizeof(adc_channel[0]),
|
||||
.sample_rate_hz = (uint32_t)input_sample_rate,
|
||||
};
|
||||
const audio_codec_data_if_t *adc_if = audio_codec_new_adc_data(&cfg);
|
||||
|
||||
esp_codec_dev_cfg_t codec_dev_cfg = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_IN,
|
||||
.data_if = adc_if,
|
||||
};
|
||||
input_dev_ = esp_codec_dev_new(&codec_dev_cfg);
|
||||
if (!input_dev_) {
|
||||
ESP_LOGE(TAG, "Failed to create codec device");
|
||||
return;
|
||||
}
|
||||
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
|
||||
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, NULL));
|
||||
|
||||
i2s_pdm_tx_config_t pdm_cfg_default = BSP_I2S_DUPLEX_MONO_CFG((uint32_t)output_sample_rate, pdm_speak_p);
|
||||
pdm_cfg_default.clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
|
||||
pdm_cfg_default.slot_cfg.sd_scale = I2S_PDM_SIG_SCALING_MUL_4;
|
||||
pdm_cfg_default.slot_cfg.hp_scale = I2S_PDM_SIG_SCALING_MUL_4;
|
||||
pdm_cfg_default.slot_cfg.lp_scale = I2S_PDM_SIG_SCALING_MUL_4;
|
||||
pdm_cfg_default.slot_cfg.sinc_scale = I2S_PDM_SIG_SCALING_MUL_4;
|
||||
const i2s_pdm_tx_config_t *p_i2s_cfg = &pdm_cfg_default;
|
||||
|
||||
ESP_ERROR_CHECK(i2s_channel_init_pdm_tx_mode(tx_handle_, p_i2s_cfg));
|
||||
|
||||
audio_codec_i2s_cfg_t i2s_cfg = {
|
||||
.port = I2S_NUM_0,
|
||||
.rx_handle = NULL,
|
||||
.tx_handle = tx_handle_,
|
||||
};
|
||||
|
||||
const audio_codec_data_if_t *i2s_data_if = audio_codec_new_i2s_data(&i2s_cfg);
|
||||
|
||||
codec_dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_OUT;
|
||||
codec_dev_cfg.codec_if = NULL;
|
||||
codec_dev_cfg.data_if = i2s_data_if;
|
||||
output_dev_ = esp_codec_dev_new(&codec_dev_cfg);
|
||||
|
||||
output_volume_ = 100;
|
||||
if(pa_ctl != GPIO_NUM_NC) {
|
||||
pa_ctrl_pin_ = pa_ctl;
|
||||
gpio_config_t io_conf = {};
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||
io_conf.pin_bit_mask = (1ULL << pa_ctrl_pin_);
|
||||
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
gpio_config(&io_conf);
|
||||
}
|
||||
gpio_set_drive_capability(pdm_speak_p, GPIO_DRIVE_CAP_0);
|
||||
|
||||
if(pdm_speak_n != GPIO_NUM_NC){
|
||||
PIN_FUNC_SELECT(IO_MUX_GPIO10_REG, PIN_FUNC_GPIO);
|
||||
gpio_set_direction(pdm_speak_n, GPIO_MODE_OUTPUT);
|
||||
esp_rom_gpio_connect_out_signal(pdm_speak_n, I2SO_SD_OUT_IDX, 1, 0); //反转输出 SD OUT 信号
|
||||
gpio_set_drive_capability(pdm_speak_n, GPIO_DRIVE_CAP_0);
|
||||
}
|
||||
|
||||
// 初始化输出定时器
|
||||
esp_timer_create_args_t output_timer_args = {
|
||||
.callback = &AdcPdmAudioCodec::OutputTimerCallback,
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "output_timer"
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&output_timer_args, &output_timer_));
|
||||
|
||||
ESP_LOGI(TAG, "AdcPdmAudioCodec initialized");
|
||||
}
|
||||
|
||||
AdcPdmAudioCodec::~AdcPdmAudioCodec() {
|
||||
// 删除定时器
|
||||
if (output_timer_) {
|
||||
esp_timer_stop(output_timer_);
|
||||
esp_timer_delete(output_timer_);
|
||||
output_timer_ = nullptr;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
|
||||
esp_codec_dev_delete(output_dev_);
|
||||
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
|
||||
esp_codec_dev_delete(input_dev_);
|
||||
}
|
||||
|
||||
void AdcPdmAudioCodec::SetOutputVolume(int volume) {
|
||||
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
|
||||
AudioCodec::SetOutputVolume(volume);
|
||||
}
|
||||
|
||||
void AdcPdmAudioCodec::EnableInput(bool enable) {
|
||||
if (enable == input_enabled_) {
|
||||
return;
|
||||
}
|
||||
if (enable) {
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.bits_per_sample = 16,
|
||||
.channel = 1,
|
||||
.channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
|
||||
.sample_rate = (uint32_t)input_sample_rate_,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
|
||||
} else {
|
||||
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
|
||||
}
|
||||
AudioCodec::EnableInput(enable);
|
||||
}
|
||||
|
||||
void AdcPdmAudioCodec::EnableOutput(bool enable) {
|
||||
if (enable == output_enabled_) {
|
||||
return;
|
||||
}
|
||||
if (enable) {
|
||||
// Play 16bit 1 channel
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.bits_per_sample = 16,
|
||||
.channel = 1,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = (uint32_t)output_sample_rate_,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
|
||||
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
|
||||
|
||||
// 强制按板卡配置重配PDM TX时钟,覆盖第三方库在set_fmt中的默认up_sample_fs
|
||||
// 若通道已启用,先禁用再重配,最后再启用
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_channel_disable(tx_handle_));
|
||||
i2s_pdm_tx_clk_config_t clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG((uint32_t)output_sample_rate_);
|
||||
clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
|
||||
ESP_ERROR_CHECK(i2s_channel_reconfig_pdm_tx_clock(tx_handle_, &clk_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
|
||||
if(pa_ctrl_pin_ != GPIO_NUM_NC){
|
||||
gpio_set_level(pa_ctrl_pin_, 1);
|
||||
}
|
||||
// 启用输出时启动定时器
|
||||
if (output_timer_) {
|
||||
esp_timer_start_once(output_timer_, TIMER_TIMEOUT_US);
|
||||
}
|
||||
|
||||
} else {
|
||||
// 禁用输出时停止定时器
|
||||
if (output_timer_) {
|
||||
esp_timer_stop(output_timer_);
|
||||
}
|
||||
if(pa_ctrl_pin_ != GPIO_NUM_NC){
|
||||
gpio_set_level(pa_ctrl_pin_, 0);
|
||||
}
|
||||
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
|
||||
}
|
||||
AudioCodec::EnableOutput(enable);
|
||||
}
|
||||
|
||||
int AdcPdmAudioCodec::Read(int16_t* dest, int samples) {
|
||||
if (input_enabled_) {
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
int AdcPdmAudioCodec::Write(const int16_t* data, int samples) {
|
||||
if (output_enabled_) {
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
|
||||
// 重置输出定时器
|
||||
if (output_timer_) {
|
||||
esp_timer_stop(output_timer_);
|
||||
esp_timer_start_once(output_timer_, TIMER_TIMEOUT_US);
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
void AdcPdmAudioCodec::Start() {
|
||||
Settings settings("audio", false);
|
||||
output_volume_ = settings.GetInt("output_volume", output_volume_);
|
||||
if (output_volume_ <= 0) {
|
||||
ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_);
|
||||
output_volume_ = 10;
|
||||
}
|
||||
|
||||
EnableInput(true);
|
||||
EnableOutput(true);
|
||||
ESP_LOGI(TAG, "Audio codec started");
|
||||
}
|
||||
|
||||
// 定时器回调函数实现
|
||||
void AdcPdmAudioCodec::OutputTimerCallback(void* arg) {
|
||||
AdcPdmAudioCodec* codec = static_cast<AdcPdmAudioCodec*>(arg);
|
||||
if (codec && codec->output_enabled_) {
|
||||
codec->EnableOutput(false);
|
||||
}
|
||||
}
|
||||
37
main/boards/esp-sensairshuttle/adc_pdm_audio_codec.h
Normal file
37
main/boards/esp-sensairshuttle/adc_pdm_audio_codec.h
Normal file
@ -0,0 +1,37 @@
|
||||
#ifndef _BOX_AUDIO_CODEC_H
|
||||
#define _BOX_AUDIO_CODEC_H
|
||||
|
||||
#include "audio_codec.h"
|
||||
|
||||
#include <esp_codec_dev.h>
|
||||
#include <esp_codec_dev_defaults.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
class AdcPdmAudioCodec : public AudioCodec {
|
||||
private:
|
||||
esp_codec_dev_handle_t output_dev_ = nullptr;
|
||||
esp_codec_dev_handle_t input_dev_ = nullptr;
|
||||
gpio_num_t pa_ctrl_pin_ = GPIO_NUM_NC;
|
||||
|
||||
// 定时器相关成员变量
|
||||
esp_timer_handle_t output_timer_ = nullptr;
|
||||
static constexpr uint64_t TIMER_TIMEOUT_US = 120000; // 120ms = 120000us
|
||||
|
||||
// 定时器回调函数
|
||||
static void OutputTimerCallback(void* arg);
|
||||
|
||||
virtual int Read(int16_t* dest, int samples) override;
|
||||
virtual int Write(const int16_t* data, int samples) override;
|
||||
|
||||
public:
|
||||
AdcPdmAudioCodec(int input_sample_rate, int output_sample_rate,
|
||||
uint32_t adc_mic_channel, gpio_num_t pdm_speak_p, gpio_num_t pdm_speak_n, gpio_num_t pa_ctl);
|
||||
virtual ~AdcPdmAudioCodec();
|
||||
|
||||
virtual void SetOutputVolume(int volume) override;
|
||||
virtual void EnableInput(bool enable) override;
|
||||
virtual void EnableOutput(bool enable) override;
|
||||
void Start();
|
||||
};
|
||||
|
||||
#endif // _BOX_AUDIO_CODEC_H
|
||||
40
main/boards/esp-sensairshuttle/config.h
Normal file
40
main/boards/esp-sensairshuttle/config.h
Normal file
@ -0,0 +1,40 @@
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define AUDIO_INPUT_SAMPLE_RATE 16000
|
||||
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
|
||||
|
||||
#define AUDIO_PDM_UPSAMPLE_FS 480
|
||||
|
||||
#define AUDIO_ADC_MIC_CHANNEL 5
|
||||
#define AUDIO_PDM_SPEAK_P_GPIO GPIO_NUM_7
|
||||
#define AUDIO_PDM_SPEAK_N_GPIO GPIO_NUM_8
|
||||
#define AUDIO_PA_CTL_GPIO GPIO_NUM_1
|
||||
|
||||
#define BOOT_BUTTON_GPIO GPIO_NUM_28
|
||||
#define DISPLAY_MOSI_PIN GPIO_NUM_23
|
||||
#define DISPLAY_CLK_PIN GPIO_NUM_24
|
||||
#define DISPLAY_DC_PIN GPIO_NUM_26
|
||||
#define DISPLAY_RST_PIN GPIO_NUM_NC
|
||||
#define DISPLAY_CS_PIN GPIO_NUM_25
|
||||
|
||||
#define LCD_TP_SCL GPIO_NUM_3
|
||||
#define LCD_TP_SDA GPIO_NUM_2
|
||||
|
||||
#define LCD_TYPE_ST7789_SERIAL
|
||||
#define DISPLAY_WIDTH 284
|
||||
#define DISPLAY_HEIGHT 240
|
||||
#define DISPLAY_MIRROR_X false
|
||||
#define DISPLAY_MIRROR_Y true
|
||||
#define DISPLAY_SWAP_XY true
|
||||
|
||||
#define DISPLAY_INVERT_COLOR true
|
||||
#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB
|
||||
#define DISPLAY_OFFSET_X 36
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
|
||||
#define DISPLAY_SPI_MODE 0
|
||||
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user