代码初始化:
本项目为触摸版项目代码复制而来,基于此版本进行按键功能的适配!
This commit is contained in:
commit
dbdd304905
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# 忽略根目录下的 build 文件夹(包括其所有子文件/子文件夹)
|
||||||
|
/build
|
||||||
|
|
||||||
|
/dzbj
|
||||||
|
|
||||||
|
# 忽略根目录下的uniapp_code文件夹(这是APP端的业务)
|
||||||
|
/uniapp_code
|
||||||
|
|
||||||
|
# 忽略 macOS 系统文件
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# 忽略 Claude Code 和编辑器缓存
|
||||||
|
.cache
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# 忽略构建副产物
|
||||||
|
sdkconfig.old
|
||||||
|
|
||||||
|
# 忽略日志文件和参考项目
|
||||||
|
05-最新日志.txt
|
||||||
|
07-uniapp最新日志.txt
|
||||||
|
/Dzbj_ESP32_S3
|
||||||
|
|
||||||
64
.vscode/c_cpp_properties.json
vendored
Normal file
64
.vscode/c_cpp_properties.json
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "ESP-IDF",
|
||||||
|
"compilerPath": "/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf/tools/xtensa-esp32s3-elf/esp-13.2.0_20240530/xtensa-esp32s3-elf/bin/xtensa-esp32s3-elf-gcc",
|
||||||
|
"cStandard": "c11",
|
||||||
|
"cppStandard": "c++17",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/main",
|
||||||
|
"${workspaceFolder}/components/common/inc",
|
||||||
|
"${workspaceFolder}/components/common/inc/util",
|
||||||
|
"${workspaceFolder}/components/common/inc/base",
|
||||||
|
"${workspaceFolder}/esp-idf/components/**",
|
||||||
|
"${workspaceFolder}/esp-idf/tools/xtensa-esp32s3-elf/esp-13.2.0_20240530/xtensa-esp32s3-elf/xtensa-esp32s3-elf/include",
|
||||||
|
"${workspaceFolder}/esp-idf/tools/xtensa-esp32s3-elf/esp-13.2.0_20240530/xtensa-esp32s3-elf/lib/gcc/xtensa-esp32s3-elf/13.2.0/include",
|
||||||
|
"${workspaceFolder}/esp-idf/tools/xtensa-esp32s3-elf/esp-13.2.0_20240530/xtensa-esp32s3-elf/xtensa-esp32s3-elf/include/c++/13.2.0",
|
||||||
|
"${workspaceFolder}/esp-idf/tools/xtensa-esp32s3-elf/esp-13.2.0_20240530/xtensa-esp32s3-elf/xtensa-esp32s3-elf/include/c++/13.2.0/xtensa-esp32s3-elf",
|
||||||
|
"${workspaceFolder}/build/config",
|
||||||
|
"${workspaceFolder}/build/bootloader/config"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"ESP_PLATFORM",
|
||||||
|
"IDF_VER=\"v5.3.1\"",
|
||||||
|
"CONFIG_IDF_TARGET_ESP32S3=1",
|
||||||
|
"CONFIG_IDF_TARGET=\"esp32s3\"",
|
||||||
|
"CONFIG_FREERTOS_HZ=1000",
|
||||||
|
"_GNU_SOURCE",
|
||||||
|
"_POSIX_READER_WRITER_LOCKS",
|
||||||
|
"CONFIG_CONNECTION_TYPE_VOLC_RTC",
|
||||||
|
"ENABLE_RTC_MODE"
|
||||||
|
],
|
||||||
|
"compilerArgs": [
|
||||||
|
"-mlongcalls",
|
||||||
|
"-Wno-frame-address",
|
||||||
|
"-ffunction-sections",
|
||||||
|
"-fdata-sections",
|
||||||
|
"-Wall",
|
||||||
|
"-Werror=all",
|
||||||
|
"-Wno-error=unused-function",
|
||||||
|
"-Wno-error=unused-variable",
|
||||||
|
"-Wno-error=deprecated-declarations",
|
||||||
|
"-Wextra",
|
||||||
|
"-Wno-unused-parameter",
|
||||||
|
"-Wno-sign-compare",
|
||||||
|
"-ggdb",
|
||||||
|
"-Og",
|
||||||
|
"-fmacro-prefix-map=${workspaceFolder}=.",
|
||||||
|
"-fmacro-prefix-map=${workspaceFolder}/esp-idf=/IDF",
|
||||||
|
"-fstrict-volatile-bitfields",
|
||||||
|
"-fno-jump-tables",
|
||||||
|
"-fno-tree-switch-conversion"
|
||||||
|
],
|
||||||
|
"intelliSenseMode": "gcc-x64",
|
||||||
|
"browse": {
|
||||||
|
"path": [
|
||||||
|
"${workspaceFolder}",
|
||||||
|
"${workspaceFolder}/esp-idf/components"
|
||||||
|
],
|
||||||
|
"limitSymbolsToIncludedHeaders": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
117
.vscode/settings.json
vendored
Normal file
117
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"idf.openOcdConfigs": [
|
||||||
|
"board/esp32s3-bridge.cfg"
|
||||||
|
],
|
||||||
|
"idf.customExtraVars": {
|
||||||
|
"IDF_TARGET": "esp32s3"
|
||||||
|
},
|
||||||
|
"idf.flashType": "UART",
|
||||||
|
"idf.portWin": "COM9",
|
||||||
|
"files.associations": {
|
||||||
|
"algorithm": "cpp",
|
||||||
|
"atomic": "cpp",
|
||||||
|
"bit": "cpp",
|
||||||
|
"cctype": "cpp",
|
||||||
|
"charconv": "cpp",
|
||||||
|
"chrono": "cpp",
|
||||||
|
"clocale": "cpp",
|
||||||
|
"cmath": "cpp",
|
||||||
|
"compare": "cpp",
|
||||||
|
"concepts": "cpp",
|
||||||
|
"condition_variable": "cpp",
|
||||||
|
"cstddef": "cpp",
|
||||||
|
"cstdint": "cpp",
|
||||||
|
"cstdio": "cpp",
|
||||||
|
"cstdlib": "cpp",
|
||||||
|
"cstring": "cpp",
|
||||||
|
"ctime": "cpp",
|
||||||
|
"cwchar": "cpp",
|
||||||
|
"exception": "cpp",
|
||||||
|
"format": "cpp",
|
||||||
|
"forward_list": "cpp",
|
||||||
|
"fstream": "cpp",
|
||||||
|
"functional": "cpp",
|
||||||
|
"initializer_list": "cpp",
|
||||||
|
"iomanip": "cpp",
|
||||||
|
"ios": "cpp",
|
||||||
|
"iosfwd": "cpp",
|
||||||
|
"iostream": "cpp",
|
||||||
|
"istream": "cpp",
|
||||||
|
"iterator": "cpp",
|
||||||
|
"limits": "cpp",
|
||||||
|
"list": "cpp",
|
||||||
|
"locale": "cpp",
|
||||||
|
"map": "cpp",
|
||||||
|
"memory": "cpp",
|
||||||
|
"mutex": "cpp",
|
||||||
|
"new": "cpp",
|
||||||
|
"optional": "cpp",
|
||||||
|
"ostream": "cpp",
|
||||||
|
"ratio": "cpp",
|
||||||
|
"regex": "cpp",
|
||||||
|
"sstream": "cpp",
|
||||||
|
"stdexcept": "cpp",
|
||||||
|
"stop_token": "cpp",
|
||||||
|
"streambuf": "cpp",
|
||||||
|
"string": "cpp",
|
||||||
|
"system_error": "cpp",
|
||||||
|
"thread": "cpp",
|
||||||
|
"tuple": "cpp",
|
||||||
|
"type_traits": "cpp",
|
||||||
|
"typeinfo": "cpp",
|
||||||
|
"unordered_map": "cpp",
|
||||||
|
"utility": "cpp",
|
||||||
|
"vector": "cpp",
|
||||||
|
"xfacet": "cpp",
|
||||||
|
"xhash": "cpp",
|
||||||
|
"xiosbase": "cpp",
|
||||||
|
"xlocale": "cpp",
|
||||||
|
"xlocbuf": "cpp",
|
||||||
|
"xlocinfo": "cpp",
|
||||||
|
"xlocmes": "cpp",
|
||||||
|
"xlocmon": "cpp",
|
||||||
|
"xlocnum": "cpp",
|
||||||
|
"xloctime": "cpp",
|
||||||
|
"xmemory": "cpp",
|
||||||
|
"xstddef": "cpp",
|
||||||
|
"xstring": "cpp",
|
||||||
|
"xtr1common": "cpp",
|
||||||
|
"xtree": "cpp",
|
||||||
|
"xutility": "cpp",
|
||||||
|
"__bit_reference": "cpp",
|
||||||
|
"__hash_table": "cpp",
|
||||||
|
"__locale": "cpp",
|
||||||
|
"__node_handle": "cpp",
|
||||||
|
"__split_buffer": "cpp",
|
||||||
|
"__tree": "cpp",
|
||||||
|
"__verbose_abort": "cpp",
|
||||||
|
"array": "cpp",
|
||||||
|
"bitset": "cpp",
|
||||||
|
"cstdarg": "cpp",
|
||||||
|
"cwctype": "cpp",
|
||||||
|
"deque": "cpp",
|
||||||
|
"execution": "cpp",
|
||||||
|
"print": "cpp",
|
||||||
|
"queue": "cpp",
|
||||||
|
"stack": "cpp",
|
||||||
|
"string_view": "cpp",
|
||||||
|
"variant": "cpp",
|
||||||
|
"complex": "cpp",
|
||||||
|
"sdkconfig.h": "c",
|
||||||
|
"unordered_set": "cpp",
|
||||||
|
"cinttypes": "cpp",
|
||||||
|
"span": "cpp",
|
||||||
|
"osmemory": "cpp",
|
||||||
|
"osutility": "cpp",
|
||||||
|
"__config": "cpp",
|
||||||
|
"volc_device_manager.h": "c",
|
||||||
|
"version": "cpp",
|
||||||
|
"random": "cpp",
|
||||||
|
"*.obj": "cpp"
|
||||||
|
},
|
||||||
|
"idf.port": "/dev/tty.usbmodem834401",
|
||||||
|
"idf.espIdfPath": "/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf",
|
||||||
|
"idf.toolsPath": "/Users/rdzleo/.espressif",
|
||||||
|
"idf.pythonInstallPath": "/opt/homebrew/bin/python3",
|
||||||
|
"idf.currentSetup": "/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf"
|
||||||
|
}
|
||||||
747
00Kapi_Rtc_火山RTC整合移植方案.md
Normal file
747
00Kapi_Rtc_火山RTC整合移植方案.md
Normal file
@ -0,0 +1,747 @@
|
|||||||
|
# Kapi_Rtc项目火山RTC整合移植方案
|
||||||
|
|
||||||
|
## 1. 背景概述
|
||||||
|
|
||||||
|
本报告针对Kapi_Rtc项目的WebSocket音频传输方案替换为火山RTC引擎进行全面分析和实施指导。基于对当前系统架构的深入研究和火山RTC引擎的技术评估,提供从可行性分析到具体实现的完整移植方案,旨在帮助开发团队高效、平稳地完成技术迁移。
|
||||||
|
|
||||||
|
## 2. 当前系统分析
|
||||||
|
|
||||||
|
### 2.1 Kapi_Rtc项目架构
|
||||||
|
|
||||||
|
**核心架构特点:**
|
||||||
|
- 基于C++面向对象设计,采用分层架构
|
||||||
|
- 使用ESP-IDF构建系统,支持ESP32_S3等开发板
|
||||||
|
- 事件驱动模型,使用FreeRTOS进行任务管理
|
||||||
|
- 已启用SPIRAM支持,适合资源密集型应用
|
||||||
|
|
||||||
|
**音频处理系统:**
|
||||||
|
- AudioProcessor类封装核心音频处理功能
|
||||||
|
- 集成ESP-SR和ESP_CODEC_DEV组件实现AFE功能(语音唤醒等)
|
||||||
|
- 支持多种音频编解码器(BoxAudioCodec、Es8311AudioCodec等)
|
||||||
|
- 音频处理流程:音频输入 → AudioCodec采集 → AudioProcessor处理(AEC/VAD/NS) → 输出回调
|
||||||
|
|
||||||
|
**现有通信方式:**
|
||||||
|
- 使用WebSocket协议进行音频数据传输和通信
|
||||||
|
- WebsocketProtocol类实现通信接口
|
||||||
|
- 主要功能:建立连接、发送/接收音频数据、发送文本消息、处理连接状态
|
||||||
|
|
||||||
|
### 2.2 项目启动流程(基于main.cc)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
extern "C" void app_main(void)
|
||||||
|
{
|
||||||
|
// 初始化事件循环
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
|
||||||
|
// 初始化网络接口
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
|
||||||
|
// 初始化NVS flash
|
||||||
|
esp_err_t ret = nvs_flash_init();
|
||||||
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||||
|
ret = nvs_flash_init();
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(ret);
|
||||||
|
|
||||||
|
// 启动应用程序
|
||||||
|
Application::GetInstance().Start();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 火山RTC引擎分析
|
||||||
|
|
||||||
|
### 3.1 核心特性
|
||||||
|
|
||||||
|
- **音频编码支持**:OPUS、G722、AACLC、G711A、G711U等
|
||||||
|
- **实时传输**:优化的低延迟音频传输协议
|
||||||
|
- **双管道设计**:录音管道和播放管道分离
|
||||||
|
- **内存优化**:支持SPIRAM分配,适合资源受限环境
|
||||||
|
- **AGC支持**:内置自动增益控制功能
|
||||||
|
- **完善的事件回调机制**:连接状态、用户加入/离开、token过期等
|
||||||
|
|
||||||
|
### 3.2 API接口概述
|
||||||
|
|
||||||
|
**核心API分类:**
|
||||||
|
- 引擎管理:`byte_rtc_create`、`byte_rtc_init`、`byte_rtc_fini`、`byte_rtc_destroy`
|
||||||
|
- 房间操作:`byte_rtc_join_room`、`byte_rtc_leave_room`、`byte_rtc_renew_token`
|
||||||
|
- 媒体流控制:`byte_rtc_mute_local_audio`、`byte_rtc_mute_remote_audio`
|
||||||
|
- 音频数据发送:`byte_rtc_send_audio_data`
|
||||||
|
- 事件回调:提供完善的状态通知机制
|
||||||
|
|
||||||
|
### 3.3 与Kapi_Rtc的兼容性分析
|
||||||
|
|
||||||
|
**语言兼容性**:
|
||||||
|
- Airhub_Rtc_h使用C语言,Kapi_Rtc主要使用C++
|
||||||
|
- C++向后兼容C语言,可通过`extern "C"`声明无缝集成
|
||||||
|
|
||||||
|
**构建系统兼容性**:
|
||||||
|
- 两者均基于ESP-IDF构建系统
|
||||||
|
- 可通过CMakeLists.txt配置实现依赖管理
|
||||||
|
|
||||||
|
**硬件兼容性**:
|
||||||
|
- 均支持ESP32_S3_KORVO2_V3等开发板
|
||||||
|
- 硬件配置相似,便于驱动移植
|
||||||
|
|
||||||
|
**音频格式兼容性**:
|
||||||
|
|
||||||
|
| 评估项 | Kapi_Rtc | 火山RTC | 兼容性 |
|
||||||
|
|-------|----------|---------|--------|
|
||||||
|
| 采样率 | 可配置,支持24000Hz等 | 16000Hz输入,8000Hz输出 | 需适配 |
|
||||||
|
| 位深度 | 16位和32位 | 算法流32位,其他16位 | 兼容 |
|
||||||
|
| 声道数 | 多声道 | 主要支持单声道 | 需适配 |
|
||||||
|
| 音频编码 | 原始PCM | OPUS解码和PCM | 需集成解码器 |
|
||||||
|
|
||||||
|
## 4. 迁移方案设计
|
||||||
|
|
||||||
|
### 4.1 架构设计
|
||||||
|
|
||||||
|
**核心设计原则:**
|
||||||
|
- 保留Kapi_Rtc现有`Protocol`抽象接口
|
||||||
|
- 创建`VolcRtcProtocol`类继承自`Protocol`,实现与`WebsocketProtocol`相同接口
|
||||||
|
- 实现无缝替换,最小化对现有业务逻辑的影响
|
||||||
|
|
||||||
|
**架构图:**
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Application │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ Protocol │
|
||||||
|
└────────┬────────┘
|
||||||
|
├───────────────┐
|
||||||
|
┌────────▼────────┐ ┌─────▼──────────┐
|
||||||
|
│WebsocketProtocol│ │VolcRtcProtocol │
|
||||||
|
└─────────────────┘ └────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 数据流向调整
|
||||||
|
|
||||||
|
**当前WebSocket模式(回调驱动):**
|
||||||
|
- 通过注册回调函数被动接收音频数据
|
||||||
|
- 事件驱动模型,使用FreeRTOS事件组同步
|
||||||
|
|
||||||
|
**火山RTC模式(主动读取):**
|
||||||
|
- 通过循环调用`recorder_pipeline_read()`主动获取数据
|
||||||
|
- 基于返回值的错误处理
|
||||||
|
- 直接的管道处理流程
|
||||||
|
|
||||||
|
**调整策略:**
|
||||||
|
- 在`VolcRtcProtocol`内部封装主动读取逻辑
|
||||||
|
- 对外保持与WebSocket相同的回调接口
|
||||||
|
- 确保实时性不受影响
|
||||||
|
|
||||||
|
### 4.3 接口替换策略
|
||||||
|
|
||||||
|
**核心接口映射:**
|
||||||
|
|
||||||
|
| WebSocket接口 | 火山RTC实现 |
|
||||||
|
|---------------|-------------|
|
||||||
|
| OpenAudioChannel() | 创建RTC实例并加入房间 |
|
||||||
|
| CloseAudioChannel() | 离开房间并销毁RTC实例 |
|
||||||
|
| SendAudio() | 调用byte_rtc_send_audio_data发送音频 |
|
||||||
|
| SendText() | 通过RTC消息通道发送文本 |
|
||||||
|
| OnIncomingAudio() | 处理RTC接收的音频数据 |
|
||||||
|
| OnIncomingJson() | 处理RTC接收的JSON消息 |
|
||||||
|
|
||||||
|
## 5. 具体实现步骤
|
||||||
|
|
||||||
|
### 5.1 引入火山RTC依赖
|
||||||
|
|
||||||
|
1. **复制依赖文件:**
|
||||||
|
- `/components/volc_engine_rtc_lite/` - 火山RTC SDK
|
||||||
|
- `/components/common/src/volc_rtc.c` - RTC封装实现
|
||||||
|
- `/components/common/include/volc_rtc.h` - RTC封装头文件
|
||||||
|
|
||||||
|
2. **更新CMakeLists.txt:**
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# 添加火山RTC组件
|
||||||
|
set(COMPONENTS ${COMPONENTS} volc_engine_rtc_lite)
|
||||||
|
set(COMPONENTS ${COMPONENTS} common)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 SDK配置与鉴权
|
||||||
|
|
||||||
|
1. **在`Kconfig.projbuild`中添加配置项:**
|
||||||
|
|
||||||
|
```
|
||||||
|
config VOLC_INSTANCE_ID
|
||||||
|
string "volcano instance id"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Instance ID for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_PRODUCT_KEY
|
||||||
|
string "volcano product key"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Product Key for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_PRODUCT_SECRET
|
||||||
|
string "volcano product secret"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Product Secret for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_DEVICE_NAME
|
||||||
|
string "volcano device name"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Device Name for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_BOT_ID
|
||||||
|
string "volcano bot id"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Bot ID for Volc RTC service.
|
||||||
|
|
||||||
|
config USE_VOLC_RTC
|
||||||
|
bool "Use Volc RTC instead of WebSocket"
|
||||||
|
default n
|
||||||
|
help
|
||||||
|
Select to use Volc RTC protocol instead of WebSocket.
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **在`sdkconfig.defaults`中设置默认值:**
|
||||||
|
|
||||||
|
```
|
||||||
|
# Volc RTC Configuration
|
||||||
|
CONFIG_VOLC_INSTANCE_ID="your_instance_id"
|
||||||
|
CONFIG_VOLC_PRODUCT_KEY="your_product_key"
|
||||||
|
CONFIG_VOLC_PRODUCT_SECRET="your_product_secret"
|
||||||
|
CONFIG_VOLC_DEVICE_NAME="your_device_name"
|
||||||
|
CONFIG_VOLC_BOT_ID="your_bot_id"
|
||||||
|
CONFIG_USE_VOLC_RTC=y
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **创建配置JSON模板:**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 对话AI配置格式
|
||||||
|
#define CONV_AI_CONFIG_FORMAT "{\"ver\": 1,\"iot\":{\"instance_id\":\"%s\",\"product_key\":\"%s\",\"product_secret\":\"%s\",\"device_name\":\"%s\"},\"rtc\":{\"log_level\":1,\"audio\":{\"publish\":true,\"subscribe\":true,\"codec\":4},\"video\":{\"publish\":false,\"subscribe\":false,\"codec\":1},\"params\":[\"{\\\"debug\\\":{\\\"log_to_console\\\":1}}\",\"{\\\"audio\\\":{\\\"codec\\\":{\\\"internal\\\":{\\\"enable\\\":1}}}}\",\"{\\\"rtc\\\":{\\\"access\\\":{\\\"concurrent_requests\\\":1}}}\",\"{\\\"rtc\\\":{\\\"ice\\\":{\\\"concurrent_agents\\\":1}}}\"]}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 创建VolcRtcProtocol类
|
||||||
|
|
||||||
|
**头文件`volc_rtc_protocol.h`:**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#ifndef _VOLC_RTC_PROTOCOL_H_
|
||||||
|
#define _VOLC_RTC_PROTOCOL_H_
|
||||||
|
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "volc_rtc.h"
|
||||||
|
#include <mutex>
|
||||||
|
#include <cJSON.h>
|
||||||
|
|
||||||
|
class VolcRtcProtocol : public Protocol {
|
||||||
|
public:
|
||||||
|
VolcRtcProtocol();
|
||||||
|
~VolcRtcProtocol();
|
||||||
|
|
||||||
|
void Start() override;
|
||||||
|
void SendAudio(const std::vector<uint8_t>& data) override;
|
||||||
|
bool OpenAudioChannel() override;
|
||||||
|
void CloseAudioChannel() override;
|
||||||
|
bool IsAudioChannelOpened() const override;
|
||||||
|
int SendInterrupt();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SendText(const std::string& text) override;
|
||||||
|
void OnRtcMessage(volc_msg_t* msg);
|
||||||
|
void OnRtcData(const void* data, int data_len, volc_data_info_t* info);
|
||||||
|
|
||||||
|
volc_rtc_t rtc_handle_ = nullptr;
|
||||||
|
std::mutex rtc_mutex_;
|
||||||
|
bool is_audio_channel_opened_ = false;
|
||||||
|
cJSON* rtc_config_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现文件`volc_rtc_protocol.cc`:**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "volc_rtc_protocol.h"
|
||||||
|
#include "board.h"
|
||||||
|
#include "system_info.h"
|
||||||
|
#include "application.h"
|
||||||
|
#include "assets/lang_config.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <cJSON.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
#define TAG "VolcRTC"
|
||||||
|
|
||||||
|
// RTC消息回调函数
|
||||||
|
static void rtc_message_callback(void* context, volc_msg_t* msg) {
|
||||||
|
if (context) {
|
||||||
|
((VolcRtcProtocol*)context)->OnRtcMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTC数据回调函数
|
||||||
|
static void rtc_data_callback(void* context, const void* data, int data_len, volc_data_info_t* info) {
|
||||||
|
if (context) {
|
||||||
|
((VolcRtcProtocol*)context)->OnRtcData(data, data_len, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VolcRtcProtocol::VolcRtcProtocol() {
|
||||||
|
// 创建完整的火山RTC配置
|
||||||
|
char config_buf[1024] = {0};
|
||||||
|
|
||||||
|
// 使用配置模板构建完整配置
|
||||||
|
snprintf(config_buf, sizeof(config_buf), CONV_AI_CONFIG_FORMAT,
|
||||||
|
CONFIG_VOLC_INSTANCE_ID,
|
||||||
|
CONFIG_VOLC_PRODUCT_KEY,
|
||||||
|
CONFIG_VOLC_PRODUCT_SECRET,
|
||||||
|
CONFIG_VOLC_DEVICE_NAME);
|
||||||
|
|
||||||
|
// 解析配置JSON
|
||||||
|
rtc_config_ = cJSON_Parse(config_buf);
|
||||||
|
if (!rtc_config_) {
|
||||||
|
ESP_LOGE(TAG, "Failed to parse RTC config");
|
||||||
|
rtc_config_ = cJSON_CreateObject();
|
||||||
|
|
||||||
|
// 设置默认配置
|
||||||
|
cJSON_AddNumberToObject(rtc_config_, "audio.codec", AUDIO_CODEC_TYPE_OPUS);
|
||||||
|
cJSON_AddNumberToObject(rtc_config_, "video.codec", VIDEO_CODEC_TYPE_NONE);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "audio.publish", true);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "video.publish", false);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "audio.subscribe", true);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "video.subscribe", false);
|
||||||
|
cJSON_AddNumberToObject(rtc_config_, "log_level", BYTE_RTC_LOG_LEVEL_INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VolcRtcProtocol::~VolcRtcProtocol() {
|
||||||
|
CloseAudioChannel();
|
||||||
|
|
||||||
|
if (rtc_config_) {
|
||||||
|
cJSON_Delete(rtc_config_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::Start() {
|
||||||
|
// RTC协议在OpenAudioChannel时启动,这里不需要额外操作
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VolcRtcProtocol::OpenAudioChannel() {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (rtc_handle_ != nullptr) {
|
||||||
|
ESP_LOGW(TAG, "RTC handle already exists");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建RTC实例 - 传入完整配置,包括鉴权信息
|
||||||
|
const char* app_id = CONFIG_VOLC_INSTANCE_ID; // 使用实例ID作为app_id
|
||||||
|
rtc_handle_ = volc_rtc_create(app_id, this, rtc_config_, rtc_message_callback, rtc_data_callback);
|
||||||
|
|
||||||
|
if (rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create RTC instance");
|
||||||
|
SetError(Lang::Strings::SERVER_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造IoT信息
|
||||||
|
volc_iot_info_t iot_info = {0};
|
||||||
|
|
||||||
|
// 设置设备ID
|
||||||
|
strncpy(iot_info.device_id, SystemInfo::GetMacAddress().c_str(), sizeof(iot_info.device_id));
|
||||||
|
|
||||||
|
// 设置产品信息用于鉴权
|
||||||
|
strncpy(iot_info.product_key, CONFIG_VOLC_PRODUCT_KEY, sizeof(iot_info.product_key));
|
||||||
|
strncpy(iot_info.product_secret, CONFIG_VOLC_PRODUCT_SECRET, sizeof(iot_info.product_secret));
|
||||||
|
strncpy(iot_info.device_name, CONFIG_VOLC_DEVICE_NAME, sizeof(iot_info.device_name));
|
||||||
|
|
||||||
|
// 设置房间信息
|
||||||
|
const char* bot_id = CONFIG_VOLC_BOT_ID;
|
||||||
|
|
||||||
|
// 启动RTC并加入房间 - 内部会处理token生成和鉴权
|
||||||
|
int ret = volc_rtc_start(rtc_handle_, bot_id, &iot_info);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start RTC: %d", ret);
|
||||||
|
volc_rtc_destroy(rtc_handle_);
|
||||||
|
rtc_handle_ = nullptr;
|
||||||
|
SetError(Lang::Strings::SERVER_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_audio_channel_opened_ = true;
|
||||||
|
|
||||||
|
if (on_audio_channel_opened_ != nullptr) {
|
||||||
|
on_audio_channel_opened_();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::CloseAudioChannel() {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (rtc_handle_ != nullptr) {
|
||||||
|
// 停止RTC
|
||||||
|
volc_rtc_stop(rtc_handle_);
|
||||||
|
|
||||||
|
// 销毁RTC实例
|
||||||
|
volc_rtc_destroy(rtc_handle_);
|
||||||
|
rtc_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_audio_channel_opened_ = false;
|
||||||
|
|
||||||
|
if (on_audio_channel_closed_ != nullptr) {
|
||||||
|
on_audio_channel_closed_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VolcRtcProtocol::IsAudioChannelOpened() const {
|
||||||
|
return is_audio_channel_opened_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::SendAudio(const std::vector<uint8_t>& data) {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGD(TAG, "RTC not connected, dropping audio data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造音频数据信息
|
||||||
|
volc_data_info_t data_info = {0};
|
||||||
|
data_info.type = VOLC_DATA_TYPE_AUDIO;
|
||||||
|
data_info.info.audio.data_type = VOLC_AUDIO_DATA_TYPE_OPUS;
|
||||||
|
|
||||||
|
// 发送音频数据
|
||||||
|
int ret = volc_rtc_send(rtc_handle_, data.data(), data.size(), &data_info);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to send audio data: %d", ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::SendText(const std::string& text) {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGD(TAG, "RTC not connected, dropping text message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造消息数据信息
|
||||||
|
volc_data_info_t data_info = {0};
|
||||||
|
data_info.type = VOLC_DATA_TYPE_MESSAGE;
|
||||||
|
data_info.info.message.is_binary = false;
|
||||||
|
|
||||||
|
// 发送文本消息
|
||||||
|
int ret = volc_rtc_send(rtc_handle_, text.c_str(), text.size(), &data_info);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to send text message: %d", ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int VolcRtcProtocol::SendInterrupt() {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGD(TAG, "RTC not connected, cannot send interrupt");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return volc_rtc_interrupt(rtc_handle_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::OnRtcMessage(volc_msg_t* msg) {
|
||||||
|
if (!msg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg->code) {
|
||||||
|
case VOLC_MSG_CONNECTED:
|
||||||
|
ESP_LOGI(TAG, "RTC connected");
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_DISCONNECTED:
|
||||||
|
ESP_LOGI(TAG, "RTC disconnected");
|
||||||
|
CloseAudioChannel();
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_USER_JOINED:
|
||||||
|
ESP_LOGI(TAG, "Remote user joined");
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_USER_OFFLINE:
|
||||||
|
ESP_LOGI(TAG, "Remote user offline");
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_CONV_STATUS:
|
||||||
|
ESP_LOGI(TAG, "Conversation status: %d", msg->data.conv_status);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGD(TAG, "Unhandled RTC message: %d", msg->code);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::OnRtcData(const void* data, int data_len, volc_data_info_t* info) {
|
||||||
|
if (!data || !info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (info->type) {
|
||||||
|
case VOLC_DATA_TYPE_AUDIO:
|
||||||
|
if (on_incoming_audio_ != nullptr) {
|
||||||
|
on_incoming_audio_(std::vector<uint8_t>((uint8_t*)data, (uint8_t*)data + data_len));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VOLC_DATA_TYPE_MESSAGE:
|
||||||
|
if (info->info.message.is_binary) {
|
||||||
|
// 处理二进制消息
|
||||||
|
ESP_LOGD(TAG, "Received binary message, length: %d", data_len);
|
||||||
|
} else {
|
||||||
|
// 处理文本消息
|
||||||
|
std::string text((char*)data, data_len);
|
||||||
|
auto root = cJSON_Parse(text.c_str());
|
||||||
|
if (root != nullptr) {
|
||||||
|
if (on_incoming_json_ != nullptr) {
|
||||||
|
on_incoming_json_(root);
|
||||||
|
}
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGD(TAG, "Unhandled RTC data type: %d", info->type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_incoming_time_ = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 更新Protocol工厂
|
||||||
|
|
||||||
|
修改创建协议实例的代码,根据配置选择使用WebSocket或火山RTC:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 在application.cc或相关文件中
|
||||||
|
#include "websocket_protocol.h"
|
||||||
|
#include "volc_rtc_protocol.h"
|
||||||
|
|
||||||
|
// 创建协议实例
|
||||||
|
#ifdef CONFIG_USE_VOLC_RTC
|
||||||
|
protocol_ = new VolcRtcProtocol();
|
||||||
|
#else
|
||||||
|
protocol_ = new WebsocketProtocol();
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 添加语音打断支持
|
||||||
|
|
||||||
|
在`Application`类中添加打断功能:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 在application.h中添加
|
||||||
|
void SendInterrupt();
|
||||||
|
|
||||||
|
// 在application.cc中实现
|
||||||
|
void Application::SendInterrupt() {
|
||||||
|
if (!protocol_) {
|
||||||
|
ESP_LOGE(TAG, "Protocol not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_USE_VOLC_RTC
|
||||||
|
auto* rtc_protocol = dynamic_cast<VolcRtcProtocol*>(protocol_);
|
||||||
|
if (rtc_protocol) {
|
||||||
|
rtc_protocol->SendInterrupt();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 业务功能实现
|
||||||
|
|
||||||
|
### 6.1 完整RTC连接通讯实现链路
|
||||||
|
|
||||||
|
#### 6.1.1 初始化与鉴权流程
|
||||||
|
|
||||||
|
1. **配置加载**:从Kconfig和sdkconfig加载火山RTC配置
|
||||||
|
2. **时间同步**:初始化SNTP服务,确保设备时间与服务器同步
|
||||||
|
3. **配置构建**:使用配置模板构建完整的JSON配置字符串
|
||||||
|
4. **引擎初始化**:调用`volc_rtc_create()`创建RTC实例
|
||||||
|
5. **IoT信息准备**:构建包含设备ID、产品信息的IoT结构体
|
||||||
|
6. **加入房间**:调用`volc_rtc_start()`加入RTC房间,内部自动处理token生成和鉴权
|
||||||
|
|
||||||
|
#### 6.1.2 启动引擎对话
|
||||||
|
|
||||||
|
1. **用户触发**:用户通过唤醒词或按钮触发对话
|
||||||
|
2. **开始监听**:`Application`类调用`protocol_->SendStartListening()`
|
||||||
|
3. **指令发送**:通过火山RTC发送开始监听指令给AI引擎
|
||||||
|
4. **音频采集**:启动麦克风采集音频数据
|
||||||
|
5. **音频发送**:将处理后的音频数据通过`protocol_->SendAudio()`发送给火山RTC
|
||||||
|
6. **AI处理**:火山RTC将音频数据转发给AI引擎进行处理
|
||||||
|
7. **结果返回**:AI引擎处理结果通过火山RTC返回给设备
|
||||||
|
8. **音频播放**:接收到的音频数据通过`on_incoming_audio_`回调传递给`Application`类播放
|
||||||
|
|
||||||
|
#### 6.1.3 关闭引擎
|
||||||
|
|
||||||
|
1. **对话结束**:AI引擎返回对话结束信号或用户手动触发关闭
|
||||||
|
2. **离开房间**:`Application`类调用`protocol_->CloseAudioChannel()`
|
||||||
|
3. **停止RTC**:调用`volc_rtc_stop()`停止RTC服务
|
||||||
|
4. **销毁实例**:调用`volc_rtc_destroy()`销毁RTC实例,释放资源
|
||||||
|
|
||||||
|
### 6.2 鉴权机制详解
|
||||||
|
|
||||||
|
火山RTC使用基于产品密钥的鉴权机制:
|
||||||
|
|
||||||
|
1. **配置信息**:设备需要提前配置INSTANCE_ID, PRODUCT_KEY, PRODUCT_SECRET, DEVICE_NAME
|
||||||
|
2. **Token生成**:RTC SDK内部使用产品密钥生成临时token
|
||||||
|
3. **Token使用**:在`byte_rtc_join_room()`调用时传入token进行身份验证
|
||||||
|
4. **Token过期处理**:SDK监听`on_token_privilege_will_expire`事件,在token即将过期时自动更新
|
||||||
|
|
||||||
|
### 6.3 音频数据流程
|
||||||
|
|
||||||
|
1. **音频输入**:麦克风采集的音频数据经过`AudioProcessor`处理后,通过`protocol_->SendAudio()`发送给火山RTC
|
||||||
|
2. **音频传输**:火山RTC使用优化的RTC协议传输音频数据,支持丢包重传和抗抖动
|
||||||
|
3. **AI处理**:火山引擎接收音频数据,进行语音识别和AI处理
|
||||||
|
4. **结果返回**:AI处理结果(音频和文本)通过火山RTC返回给设备
|
||||||
|
5. **音频输出**:接收到的音频数据通过`on_incoming_audio_`回调传递给`Application`类,然后播放出来
|
||||||
|
|
||||||
|
### 6.4 消息处理
|
||||||
|
|
||||||
|
1. **文本消息**:通过`protocol_->SendText()`发送文本消息
|
||||||
|
2. **JSON消息**:接收到的JSON消息通过`on_incoming_json_`回调传递给`Application`类进行处理
|
||||||
|
3. **二进制消息**:支持发送和接收二进制消息,用于特殊功能扩展
|
||||||
|
|
||||||
|
## 7. 性能优化与内存管理
|
||||||
|
|
||||||
|
### 7.1 内存分配策略
|
||||||
|
|
||||||
|
**参考Airhub_Rtc_h的优化实践:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 明确在SPIRAM中分配音频缓冲区
|
||||||
|
buffer = heap_caps_calloc(buffer_size, sizeof(uint8_t), MALLOC_CAP_SPIRAM);
|
||||||
|
|
||||||
|
// 使用完成后释放
|
||||||
|
if (buffer) {
|
||||||
|
heap_caps_free(buffer);
|
||||||
|
buffer = NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 SPIRAM使用优化
|
||||||
|
|
||||||
|
**大缓冲区设计:**
|
||||||
|
- 录音管道:2KB输出环形缓冲区(SPIRAM)
|
||||||
|
- 播放管道:8KB输出环形缓冲区(SPIRAM)
|
||||||
|
|
||||||
|
**管道组件SPIRAM分配:**
|
||||||
|
- 对整个音频处理管道的中间组件实现SPIRAM分配
|
||||||
|
- 仅将实时处理所需的最小缓冲区保留在内部RAM
|
||||||
|
|
||||||
|
### 7.3 算法优化
|
||||||
|
|
||||||
|
1. **结合Kapi_Rtc的AEC、VAD、NS算法和Airhub_Rtc_h的AGC功能**
|
||||||
|
2. **针对实时通信场景优化算法参数**
|
||||||
|
3. **合并两者的管道优化策略**
|
||||||
|
|
||||||
|
## 8. 测试与验证
|
||||||
|
|
||||||
|
### 8.1 功能测试
|
||||||
|
|
||||||
|
1. **连接测试**:验证设备能否成功连接到火山RTC服务器
|
||||||
|
2. **音频传输测试**:验证音频数据能否正常发送和接收
|
||||||
|
3. **语音打断测试**:验证语音打断功能能否正常工作
|
||||||
|
4. **异常处理测试**:验证网络异常或服务器异常时的处理逻辑
|
||||||
|
|
||||||
|
### 8.2 性能测试
|
||||||
|
|
||||||
|
1. **延迟测试**:测量音频从采集到播放的延迟
|
||||||
|
2. **稳定性测试**:长时间运行测试,验证系统稳定性
|
||||||
|
3. **资源占用测试**:监控CPU和内存占用情况
|
||||||
|
|
||||||
|
### 8.3 兼容性测试
|
||||||
|
|
||||||
|
1. **设备兼容性**:测试在不同硬件设备上的运行情况
|
||||||
|
2. **网络兼容性**:测试在不同网络环境下的运行情况
|
||||||
|
|
||||||
|
## 9. 风险与注意事项
|
||||||
|
|
||||||
|
### 9.1 技术风险
|
||||||
|
|
||||||
|
1. **音频格式兼容性**:
|
||||||
|
- 采样率差异可能导致音频质量问题
|
||||||
|
- 需要确保PCM数据格式兼容性(采样率、位深度、通道数)
|
||||||
|
|
||||||
|
2. **内存管理**:
|
||||||
|
- SPIRAM使用可能对性能产生影响
|
||||||
|
- 需要合理配置缓冲区大小,平衡延迟和稳定性
|
||||||
|
|
||||||
|
3. **实时性保障**:
|
||||||
|
- 回调模式与主动读取模式的转换需要确保实时性不受影响
|
||||||
|
- 任务优先级和调度策略需要调整
|
||||||
|
|
||||||
|
### 9.2 实现挑战
|
||||||
|
|
||||||
|
1. **接口适配**:
|
||||||
|
- 需要修改音频输出处理逻辑
|
||||||
|
- 确保与现有业务逻辑的兼容
|
||||||
|
|
||||||
|
2. **状态管理**:
|
||||||
|
- 适配音量控制和播放状态管理相关逻辑
|
||||||
|
- 确保音频业务可以正确感知系统状态变化
|
||||||
|
|
||||||
|
### 9.3 注意事项
|
||||||
|
|
||||||
|
1. **配置管理**:
|
||||||
|
- 确保火山RTC的配置参数正确
|
||||||
|
- 避免将敏感信息硬编码到代码中
|
||||||
|
- 定期更新密钥,确保安全性
|
||||||
|
|
||||||
|
2. **时间同步**:
|
||||||
|
- RTC连接需要准确的设备时间,确保SNTP服务正常工作
|
||||||
|
- 在网络不稳定时,考虑使用本地RTC作为备用时间源
|
||||||
|
|
||||||
|
3. **错误处理**:
|
||||||
|
- 完善异常处理逻辑,特别是网络异常和服务器异常情况
|
||||||
|
- 实现token过期自动更新机制
|
||||||
|
- 添加连接重试逻辑,提高系统稳定性
|
||||||
|
|
||||||
|
4. **资源管理**:
|
||||||
|
- 合理管理RTC资源,避免内存泄漏
|
||||||
|
- 在不需要RTC服务时及时关闭连接
|
||||||
|
- 监控系统资源占用情况,避免资源耗尽
|
||||||
|
|
||||||
|
5. **日志管理**:
|
||||||
|
- 添加详细的日志,便于调试和问题定位
|
||||||
|
- 分类管理日志级别,在生产环境中降低日志级别以减少性能开销
|
||||||
|
|
||||||
|
6. **版本兼容性**:
|
||||||
|
- 确保火山RTC SDK版本与项目兼容
|
||||||
|
- 定期更新SDK,获取最新的功能和安全修复
|
||||||
|
|
||||||
|
7. **网络环境**:
|
||||||
|
- 确保设备处于稳定的网络环境中
|
||||||
|
- 考虑网络切换场景(如WiFi/4G切换)的处理逻辑
|
||||||
|
|
||||||
|
## 10. 结论与后续支持
|
||||||
|
|
||||||
|
### 10.1 结论
|
||||||
|
|
||||||
|
将Kapi_Rtc项目的WebSocket音频传输方案替换为火山RTC引擎在技术上是可行的。通过采用适配器模式和分阶段迁移策略,可以最大限度地保留Kapi_Rtc的架构优势,同时集成火山RTC的实时通信能力。
|
||||||
|
|
||||||
|
本方案通过创建新的`VolcRtcProtocol`类实现了与`WebsocketProtocol`相同的接口,从而实现了无缝替换。火山RTC提供了更丰富的功能和更好的音频质量,能够满足实时通信的需求。同时,本方案保持了Kapi_Rtc项目的现有架构不变,降低了迁移风险和成本。
|
||||||
|
|
||||||
|
### 10.2 后续支持
|
||||||
|
|
||||||
|
如果您需要进一步的帮助,我可以:
|
||||||
|
1. 协助实现文档中的方案,包括代码编写和配置
|
||||||
|
2. 回答关于RTC连接鉴权和SDK配置的具体问题
|
||||||
|
3. 提供完整的实时AI对话业务实现链路的技术支持
|
||||||
|
4. 帮助进行测试和调试,确保RTC连接和音频传输正常工作
|
||||||
|
5. 优化系统性能,提高实时对话的稳定性和响应速度
|
||||||
|
|
||||||
|
请随时告诉我您的需求。
|
||||||
335
01Kapi_Rtc_WebSocket_替换为_火山RTC_技术分析报告.md
Normal file
335
01Kapi_Rtc_WebSocket_替换为_火山RTC_技术分析报告.md
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
# Kapi_Rtc项目WebSocket替换为火山RTC技术分析报告
|
||||||
|
|
||||||
|
## 1. 背景概述
|
||||||
|
|
||||||
|
本报告针对Kapi_Rtc项目的WebSocket音频传输方案替换为火山RTC引擎进行技术可行性分析,旨在评估迁移难度、兼容性问题及技术风险,为项目决策提供依据。
|
||||||
|
|
||||||
|
## 2. 当前系统音频处理架构
|
||||||
|
|
||||||
|
### 2.1 Kapi_Rtc音频处理系统
|
||||||
|
|
||||||
|
**架构特点:**
|
||||||
|
- 基于C++面向对象设计,采用AudioProcessor类封装核心功能
|
||||||
|
- 通过集成ESP-SR和ESP_CODEC_DEV等语音相关组件实现了AFE功能(主要用于语音唤醒),但没有使用完整的ESP-ADF音频开发框 架。这种方式更为轻量,同时保留了核心的语音处理能力
|
||||||
|
- 分层设计:编解码器层、处理核心层、回调接口层
|
||||||
|
- 事件驱动模型,使用FreeRTOS事件组进行任务同步
|
||||||
|
- 支持实时和非实时两种工作模式
|
||||||
|
|
||||||
|
**核心组件:**
|
||||||
|
- AudioCodec基类:提供统一的音频编解码器接口
|
||||||
|
- AudioProcessor:核心音频处理类,管理AFE配置和处理流程
|
||||||
|
- 多种编解码器实现:BoxAudioCodec、Es8311AudioCodec等
|
||||||
|
|
||||||
|
**处理流程:**
|
||||||
|
```
|
||||||
|
音频输入 → AudioCodec采集 → AudioProcessor处理(AEC/VAD/NS) → 输出回调
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 火山RTC引擎技术分析
|
||||||
|
|
||||||
|
### 3.1 核心特性
|
||||||
|
|
||||||
|
- **音频编码支持**:支持OPUS、G722、AACLC、G711A、G711U等多种音频编码格式
|
||||||
|
- **实时传输**:优化的音频数据传输,支持低延迟通信
|
||||||
|
- **双管道设计**:录音管道(recorder_pipeline)和播放管道(player_pipeline)分离
|
||||||
|
- **内存优化**:使用SPIRAM进行管道分配,适合资源受限环境
|
||||||
|
- **AGC支持**:内置自动增益控制,对麦克风输入进行优化
|
||||||
|
|
||||||
|
### 3.2 API接口概述
|
||||||
|
|
||||||
|
火山RTC引擎提供以下核心API:
|
||||||
|
- 引擎管理:`byte_rtc_create`、`byte_rtc_init`、`byte_rtc_fini`、`byte_rtc_destroy`
|
||||||
|
- 房间操作:`byte_rtc_join_room`、`byte_rtc_leave_room`、`byte_rtc_renew_token`
|
||||||
|
- 媒体流控制:`byte_rtc_mute_local_audio`、`byte_rtc_mute_remote_audio`
|
||||||
|
- 音频数据发送:`byte_rtc_send_audio_data`
|
||||||
|
- 事件回调:提供完善的事件通知机制
|
||||||
|
|
||||||
|
## 4. 技术可行性评估
|
||||||
|
|
||||||
|
### 4.1 兼容性分析
|
||||||
|
|
||||||
|
**语言兼容性**:
|
||||||
|
- Airhub_Rtc_h项目使用C语言开发,而Kapi_Rtc项目主要使用C++语言
|
||||||
|
- 由于C++向后兼容C语言特性,且可通过`extern "C"`声明处理C接口,两种语言可无缝集成
|
||||||
|
|
||||||
|
**构建系统兼容性**:
|
||||||
|
- Kapi_Rtc项目基于ESP-IDF构建系统、支持混合编程模式,Airhub_Rtc_h项目基于ESP-ADF构建系统
|
||||||
|
- 可通过CMakeLists.txt配置实现依赖管理
|
||||||
|
|
||||||
|
**硬件兼容性**:
|
||||||
|
- 两个项目均支持ESP32_S3_KORVO2_V3等开发板,硬件配置相似,便于驱动移植
|
||||||
|
|
||||||
|
### 4.2 音频数据格式兼容性
|
||||||
|
|
||||||
|
**支持的格式对比:**
|
||||||
|
|
||||||
|
| 评估项 | Kapi_Rtc | Airhub_Rtc_h | 评估结果 |
|
||||||
|
|-------|--------------|--------------|----------|
|
||||||
|
| 采样率 | 可配置,支持24000Hz等多种采样率 | 固定16000Hz输入,8000Hz输出 | Kapi_Rtc更灵活 |
|
||||||
|
| 位深度 | 支持16位和32位音频处理 | 算法流使用32位,其他使用16位 | 功能相当 |
|
||||||
|
| 声道数 | 支持多声道配置 | 主要支持单声道(左声道),可选双声道 | Kapi_Rtc更灵活 |
|
||||||
|
| 音频编码 | 支持原始PCM处理 | 支持Opus解码和原始PCM处理 | Airhub_Rtc_h更完善 |
|
||||||
|
|
||||||
|
## 5. 数据流向模式差异分析
|
||||||
|
|
||||||
|
### 5.1 当前WebSocket模式
|
||||||
|
|
||||||
|
Kapi_Rtc采用回调驱动模式:
|
||||||
|
- 通过注册回调函数(如`OnOutput`)被动接收音频数据
|
||||||
|
- 事件驱动模型,使用FreeRTOS事件组进行任务同步
|
||||||
|
- 基于状态检查和回调机制处理错误
|
||||||
|
|
||||||
|
### 5.2 火山RTC模式
|
||||||
|
|
||||||
|
Airhub_Rtc_h采用主动读取模式:
|
||||||
|
- 通过循环调用`recorder_pipeline_read()`方法主动获取数据
|
||||||
|
- 基于返回值的错误处理
|
||||||
|
- 直接的管道处理流程
|
||||||
|
|
||||||
|
### 5.3 模式转换影响
|
||||||
|
|
||||||
|
1. **代码架构调整**:
|
||||||
|
- 需要重构数据流处理逻辑,从主动轮询改为事件响应
|
||||||
|
- 调整任务优先级和阻塞策略
|
||||||
|
|
||||||
|
2. **资源管理变化**:
|
||||||
|
- 回调模式更适合事件驱动系统,资源使用更高效
|
||||||
|
- 主动读取模式可能导致忙等待,特别是在没有数据时
|
||||||
|
|
||||||
|
3. **错误处理机制差异**:
|
||||||
|
- 需要统一错误处理策略
|
||||||
|
|
||||||
|
## 6. 火山RTC音频格式与播放兼容性
|
||||||
|
|
||||||
|
### 6.1 火山RTC推送的音频格式
|
||||||
|
|
||||||
|
火山RTC引擎推送的音频格式主要是:
|
||||||
|
- **OPUS编码格式**:高效的音频编码格式,适合实时通信场景
|
||||||
|
- **PCM原始数据**:支持原始PCM数据处理
|
||||||
|
|
||||||
|
### 6.2 Kapi_Rtc音频播放管道兼容性
|
||||||
|
|
||||||
|
Kapi_Rtc的音频播放管道可以处理并播放火山RTC推送的音频,但需要进行适当适配:
|
||||||
|
|
||||||
|
**当前支持能力**:
|
||||||
|
- Kapi_Rtc现有的.p3文件播放流程已经包含了Opus解码步骤
|
||||||
|
- 播放链路为:数据解析 → 队列管理 → 解码处理 → 音频解码(Opus→PCM) → 重采样 → 硬件输出
|
||||||
|
|
||||||
|
**兼容性优势**:
|
||||||
|
- 两个系统都支持OPUS解码和PCM输出
|
||||||
|
- 两者都具有音频重采样功能
|
||||||
|
- 架构上可以通过适配器模式实现兼容
|
||||||
|
|
||||||
|
## 7. 不使用ADF框架的兼容性问题
|
||||||
|
|
||||||
|
在不使用ADF框架的情况下,主要兼容性问题包括:
|
||||||
|
|
||||||
|
1. **音频处理算法差异**:
|
||||||
|
- Kapi_Rtc提供更全面的音频处理算法(AEC、VAD、NS)
|
||||||
|
- Airhub_Rtc_h在AGC方面有特定优势
|
||||||
|
|
||||||
|
2. **内存管理策略**:
|
||||||
|
- Kapi_Rtc支持内部内存和PSRAM分配,默认更多使用内部内存
|
||||||
|
- Airhub_Rtc_h明确使用SPIRAM进行管道分配,内存管理更明确
|
||||||
|
|
||||||
|
3. **缓冲区设计**:
|
||||||
|
- Kapi_Rtc的音频缓冲区较小(任务栈4096字节)
|
||||||
|
- Airhub_Rtc_h的输出环形缓冲区较大(2KB),适合长时间处理
|
||||||
|
|
||||||
|
4. **扩展性差异**:
|
||||||
|
- Kapi_Rtc使用C++类层次结构,易于扩展
|
||||||
|
- Airhub_Rtc_h使用C语言模块设计,通过接口扩展
|
||||||
|
|
||||||
|
## 8. 推荐的迁移方案
|
||||||
|
|
||||||
|
### 8.1 架构设计
|
||||||
|
|
||||||
|
**推荐方案:以Kapi_Rtc为基础,集成Airhub_Rtc_h的优势模块**
|
||||||
|
|
||||||
|
1. **保留Kapi_Rtc的核心架构**
|
||||||
|
- 保持C++面向对象的AudioProcessor类设计
|
||||||
|
- 保留分层结构和事件驱动模型
|
||||||
|
- 保留完善的错误处理和日志系统
|
||||||
|
|
||||||
|
2. **集成Airhub_Rtc_h的内存优化技术**
|
||||||
|
- 在AudioProcessor中增加对SPIRAM的明确使用配置
|
||||||
|
- 实现Airhub_Rtc_h的大缓冲区管理策略
|
||||||
|
- 优化内存分配模式,特别是大数据量处理时
|
||||||
|
|
||||||
|
3. **增强音频处理算法**
|
||||||
|
- 在Kapi_Rtc中默认启用AGC功能(从Airhub_Rtc_h借鉴)
|
||||||
|
- 保留Kapi_Rtc的AEC、VAD、NS算法优势
|
||||||
|
- 合并两者的管道优化策略
|
||||||
|
|
||||||
|
4. **扩展音频编码支持**
|
||||||
|
- 在Kapi_Rtc中集成Opus解码器支持
|
||||||
|
- 保持原始PCM处理能力
|
||||||
|
|
||||||
|
### 8.2 接口替换策略
|
||||||
|
|
||||||
|
**核心接口映射:**
|
||||||
|
|
||||||
|
| Airhub_Rtc_h接口 | Kapi_Rtc接口 | 替换策略 |
|
||||||
|
|-----------------|-----------------|----------|
|
||||||
|
| recorder_pipeline_open() | AudioProcessor::Initialize() | 创建AudioProcessor实例并初始化 |
|
||||||
|
| recorder_pipeline_run() | AudioProcessor::Start() | 启动音频处理 |
|
||||||
|
| recorder_pipeline_close() | AudioProcessor析构函数 | 释放资源 |
|
||||||
|
| recorder_pipeline_read() | 注册output_callback | 使用回调机制获取处理后的数据 |
|
||||||
|
| player_pipeline_*系列接口 | AudioCodec::StartOutput()/StopOutput() | 使用AudioCodec接口控制音频输出 |
|
||||||
|
|
||||||
|
### 8.3 RTC引擎封装
|
||||||
|
|
||||||
|
创建`VolcRtcWrapper`封装类,实现火山RTC引擎的C++接口:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class VolcRtcWrapper {
|
||||||
|
public:
|
||||||
|
VolcRtcWrapper();
|
||||||
|
virtual ~VolcRtcWrapper();
|
||||||
|
|
||||||
|
bool Initialize(const std::string &app_id, const std::string &user_id);
|
||||||
|
bool JoinRoom(const std::string &room_id, const std::string &token);
|
||||||
|
bool LeaveRoom();
|
||||||
|
bool SendAudioData(const uint8_t *data, size_t len);
|
||||||
|
bool EnableAudioCapture(bool enable);
|
||||||
|
bool EnableAudioPlayback(bool enable);
|
||||||
|
|
||||||
|
// 设置回调函数
|
||||||
|
void SetEventCallback(RtcEventCallback callback);
|
||||||
|
void SetAudioDataCallback(AudioDataCallback callback);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 风险与挑战
|
||||||
|
|
||||||
|
### 9.1 技术风险
|
||||||
|
|
||||||
|
1. **音频格式兼容性**:
|
||||||
|
- 采样率差异可能导致音频质量问题
|
||||||
|
- 需要确保PCM数据格式兼容性(采样率、位深度、通道数)
|
||||||
|
|
||||||
|
2. **内存管理**:
|
||||||
|
- SPIRAM使用可能对性能产生影响
|
||||||
|
- 需要合理配置缓冲区大小,平衡延迟和稳定性
|
||||||
|
|
||||||
|
3. **实时性保障**:
|
||||||
|
- 回调模式与主动读取模式的转换需要确保实时性不受影响
|
||||||
|
- 任务优先级和调度策略需要调整
|
||||||
|
|
||||||
|
### 9.2 实现挑战
|
||||||
|
|
||||||
|
1. **接口适配**:
|
||||||
|
- 需要修改`OnAudioOutput()`方法,将`codec->OutputData(pcm)`替换为`player_pipeline_write()`
|
||||||
|
- 在应用启动时需要初始化Airhub_Rtc_h的player_pipeline
|
||||||
|
|
||||||
|
2. **状态管理**:
|
||||||
|
- 适配音量控制和播放状态管理相关逻辑
|
||||||
|
- 确保音频业务可以正确感知系统状态变化
|
||||||
|
|
||||||
|
## 10. 性能优化建议
|
||||||
|
|
||||||
|
1. **内存优化**:
|
||||||
|
- 使用Airhub_Rtc_h的SPIRAM分配策略
|
||||||
|
- 实现大缓冲区管理,适合长时间音频处理场景
|
||||||
|
|
||||||
|
2. **算法优化**:
|
||||||
|
- 结合Kapi_Rtc的AEC、VAD、NS算法和Airhub_Rtc_h的AGC功能
|
||||||
|
- 针对实时通信场景优化算法参数
|
||||||
|
|
||||||
|
3. **管道优化**:
|
||||||
|
- 合并两者的管道优化策略
|
||||||
|
- 确保音频数据流畅通,减少延迟
|
||||||
|
|
||||||
|
## 11. 测试策略
|
||||||
|
|
||||||
|
1. **单元测试**:
|
||||||
|
- 测试RTC引擎封装类的各个接口
|
||||||
|
- 验证音频格式转换的正确性
|
||||||
|
|
||||||
|
2. **集成测试**:
|
||||||
|
- 测试与现有Kapi_Rtc系统的集成
|
||||||
|
- 验证WebSocket替换为火山RTC后的整体功能
|
||||||
|
|
||||||
|
3. **性能测试**:
|
||||||
|
- 测试音频延迟、质量和稳定性
|
||||||
|
- 验证在各种网络条件下的表现
|
||||||
|
|
||||||
|
## 12. 结论
|
||||||
|
|
||||||
|
将Kapi_Rtc项目的WebSocket音频传输方案替换为火山RTC引擎在技术上是可行的。通过采用适配器模式和分阶段迁移策略,可以最大限度地保留Kapi_Rtc的架构优势,同时集成Airhub_Rtc_h的特定优化。
|
||||||
|
主要挑战在于处理数据流向模式的差异和确保音频格式兼容性,但通过合理的架构设计和接口适配,这些问题都可以得到有效解决。
|
||||||
|
|
||||||
|
## 13. 内存使用分析与优化建议
|
||||||
|
|
||||||
|
### 13.1 Kapi_Rtc项目内存配置分析
|
||||||
|
|
||||||
|
**SPIRAM支持状态:**
|
||||||
|
- Kapi_Rtc项目已启用SPIRAM支持(CONFIG_SPIRAM=y)
|
||||||
|
- 使用OCT模式进行SPIRAM访问,提高数据传输效率
|
||||||
|
- 内存分配策略:
|
||||||
|
- SPIRAM_MALLOC_ALWAYSINTERNAL=8192(小于8KB的分配始终使用内部RAM)
|
||||||
|
- SPIRAM_MALLOC_RESERVE_INTERNAL=65536(保留64KB内部RAM,不参与SPIRAM分配策略)
|
||||||
|
- 默认情况下,ESP-IDF的内存分配器会优先使用内部RAM,不足时才使用PSRAM
|
||||||
|
|
||||||
|
### 13.2 SPIRAM在音频播放中的可行性
|
||||||
|
|
||||||
|
**可行性结论:**
|
||||||
|
- Kapi_Rtc项目完全支持使用外部SPI RAM进行音频播放
|
||||||
|
- ESP32-S3平台配置已正确启用SPIRAM功能,可以正常分配和使用外部RAM
|
||||||
|
- 火山RTC引擎在迁移后可以继续利用SPIRAM进行音频数据处理
|
||||||
|
|
||||||
|
### 13.3 使用SPIRAM的优势
|
||||||
|
|
||||||
|
1. **释放内部RAM资源**:
|
||||||
|
- 内部RAM(IRAM/DRAM)对于实时操作系统和关键任务至关重要
|
||||||
|
- 将音频缓冲区等大型数据结构移至SPIRAM可显著释放内部RAM
|
||||||
|
|
||||||
|
2. **支持更大的音频缓冲区**:
|
||||||
|
- 大缓冲区可以改善音频播放的平滑度,减少卡顿
|
||||||
|
- 支持更高采样率和更长的音频处理管道
|
||||||
|
|
||||||
|
3. **内存分配优化**:
|
||||||
|
- 参考Airhub_Rtc_h项目的优化实践,使用heap_caps_calloc/free配合MALLOC_CAP_SPIRAM标志
|
||||||
|
- 明确指定哪些组件使用SPIRAM,哪些保留在内部RAM
|
||||||
|
|
||||||
|
4. **提升系统稳定性**:
|
||||||
|
- 避免内部RAM碎片和内存不足导致的系统崩溃
|
||||||
|
- 为其他关键任务预留足够的内部RAM空间
|
||||||
|
|
||||||
|
### 13.4 内存优化建议
|
||||||
|
|
||||||
|
**在火山RTC迁移过程中的优化措施:**
|
||||||
|
|
||||||
|
1. **明确内存分配策略**:
|
||||||
|
- 参考Airhub_Rtc_h的实现,在关键音频组件中显式使用SPIRAM分配
|
||||||
|
- 为音频管道组件配置专门的SPIRAM内存池
|
||||||
|
|
||||||
|
2. **大缓冲区设计**:
|
||||||
|
- 实现类似Airhub_Rtc_h的环形缓冲区结构:
|
||||||
|
- 录音管道:2KB输出环形缓冲区(SPIRAM)
|
||||||
|
- 播放管道:8KB输出环形缓冲区(SPIRAM)
|
||||||
|
|
||||||
|
3. **优化内存分配模式**:
|
||||||
|
- 使用heap_caps_calloc/free代替标准malloc/free
|
||||||
|
- 关键代码示例:
|
||||||
|
```c
|
||||||
|
// 明确在SPIRAM中分配音频缓冲区
|
||||||
|
buffer = heap_caps_calloc(buffer_size, sizeof(uint8_t), MALLOC_CAP_SPIRAM);
|
||||||
|
|
||||||
|
// 使用完成后释放
|
||||||
|
if (buffer) {
|
||||||
|
heap_caps_free(buffer);
|
||||||
|
buffer = NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **管道组件SPIRAM分配**:
|
||||||
|
- 对整个音频处理管道的中间组件实现SPIRAM分配
|
||||||
|
- 仅将实时处理所需的最小缓冲区保留在内部RAM
|
||||||
|
|
||||||
|
5. **监控与调优**:
|
||||||
|
- 集成内存使用监控工具
|
||||||
|
- 分析不同缓冲区大小对音频质量和系统性能的影响
|
||||||
|
- 调整SPIRAM_MALLOC_ALWAYSINTERNAL参数,优化小内存分配策略
|
||||||
|
|
||||||
|
通过以上内存优化措施,结合火山RTC引擎的迁移,可以显著提升系统的内存使用效率,支持更高质量的音频处理,同时保持系统稳定性。
|
||||||
622
02Kapi_Rtc_火山RTC替换实现方案.md
Normal file
622
02Kapi_Rtc_火山RTC替换实现方案.md
Normal file
@ -0,0 +1,622 @@
|
|||||||
|
# Kapi_Rtc项目火山RTC替换实现方案
|
||||||
|
|
||||||
|
## 1. 背景概述
|
||||||
|
|
||||||
|
当前Kapi_Rtc项目使用WebSocket协议进行音频数据传输和通信,但由于业务需求变化,需要替换为火山RTC(Byte RTC)协议。本方案基于对Airhub_Rtc_h项目中火山RTC实现的分析,结合Kapi_Rtc项目的现有架构,提供详细的替换实现方案。
|
||||||
|
|
||||||
|
## 2. 技术分析
|
||||||
|
|
||||||
|
### 2.1 现有WebSocket实现
|
||||||
|
|
||||||
|
Kapi_Rtc项目当前使用`WebsocketProtocol`类实现通信,主要功能包括:
|
||||||
|
|
||||||
|
- 建立和维护WebSocket连接
|
||||||
|
- 发送和接收音频数据
|
||||||
|
- 发送文本消息
|
||||||
|
- 处理连接状态变化
|
||||||
|
|
||||||
|
主要接口:
|
||||||
|
```cpp
|
||||||
|
void Start() override;
|
||||||
|
void SendAudio(const std::vector<uint8_t>& data) override;
|
||||||
|
bool OpenAudioChannel() override;
|
||||||
|
void CloseAudioChannel() override;
|
||||||
|
bool IsAudioChannelOpened() const override;
|
||||||
|
void SendText(const std::string& text) override;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 火山RTC实现(来自Airhub_Rtc_h)
|
||||||
|
|
||||||
|
Airhub_Rtc_h项目提供了完整的火山RTC封装,主要功能包括:
|
||||||
|
|
||||||
|
- RTC引擎初始化和销毁
|
||||||
|
- 加入/离开房间
|
||||||
|
- 发送音频/视频/消息数据
|
||||||
|
- 处理RTC事件回调
|
||||||
|
|
||||||
|
主要接口:
|
||||||
|
```cpp
|
||||||
|
volc_rtc_t volc_rtc_create(const char* appid, void* context, cJSON* p_config, volc_msg_cb message_callback, volc_data_cb data_callback);
|
||||||
|
void volc_rtc_destroy(volc_rtc_t handle);
|
||||||
|
int volc_rtc_start(volc_rtc_t rtc, const char* bot_id, volc_iot_info_t* iot_info);
|
||||||
|
int volc_rtc_stop(volc_rtc_t handle);
|
||||||
|
int volc_rtc_send(volc_rtc_t handle, const void* data, int size, volc_data_info_t* data_info);
|
||||||
|
int volc_rtc_interrupt(volc_rtc_t rtc);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 架构对比
|
||||||
|
|
||||||
|
| 特性 | WebSocket | 火山RTC |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| 连接管理 | 简单的TCP连接 | 复杂的RTC连接管理(ICE/STUN/TURN) |
|
||||||
|
| 音频质量 | 依赖网络质量 | 自适应网络,支持丢包重传和抗抖动 |
|
||||||
|
| 延迟 | 相对较高 | 低延迟(适合实时通信) |
|
||||||
|
| 功能丰富度 | 基础的消息和二进制数据传输 | 提供完整的RTC功能(音频/视频/消息) |
|
||||||
|
| 集成复杂度 | 低 | 中等 |
|
||||||
|
|
||||||
|
## 3. 替换方案
|
||||||
|
|
||||||
|
### 3.1 架构设计
|
||||||
|
|
||||||
|
保留Kapi_Rtc项目现有的`Protocol`抽象接口,创建新的`VolcRtcProtocol`类继承自`Protocol`,实现与`WebsocketProtocol`相同的接口,从而实现无缝替换。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Application │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ Protocol │
|
||||||
|
└────────┬────────┘
|
||||||
|
├───────────────┐
|
||||||
|
┌────────▼────────┐ ┌─────▼──────────┐
|
||||||
|
│WebsocketProtocol│ │VolcRtcProtocol │
|
||||||
|
└─────────────────┘ └────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 实现步骤
|
||||||
|
|
||||||
|
#### 3.2.1 引入火山RTC依赖
|
||||||
|
|
||||||
|
1. 将Airhub_Rtc_h项目中的火山RTC相关文件复制到Kapi_Rtc项目:
|
||||||
|
- `/components/volc_engine_rtc_lite/` - 火山RTC SDK
|
||||||
|
- `/components/common/src/volc_rtc.c` - RTC封装实现
|
||||||
|
- `/components/common/include/volc_rtc.h` - RTC封装头文件
|
||||||
|
|
||||||
|
2. 更新CMakeLists.txt文件,添加火山RTC组件:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# 添加火山RTC组件
|
||||||
|
set(COMPONENTS ${COMPONENTS} volc_engine_rtc_lite)
|
||||||
|
set(COMPONENTS ${COMPONENTS} common)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.2 SDK配置与鉴权
|
||||||
|
|
||||||
|
火山RTC需要以下关键配置项进行鉴权和连接:
|
||||||
|
|
||||||
|
1. 在`Kconfig.projbuild`中添加配置项:
|
||||||
|
|
||||||
|
```
|
||||||
|
config VOLC_INSTANCE_ID
|
||||||
|
string "volcano instance id"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Instance ID for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_PRODUCT_KEY
|
||||||
|
string "volcano product key"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Product Key for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_PRODUCT_SECRET
|
||||||
|
string "volcano product secret"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Product Secret for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_DEVICE_NAME
|
||||||
|
string "volcano device name"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Device Name for Volc RTC service.
|
||||||
|
|
||||||
|
config VOLC_BOT_ID
|
||||||
|
string "volcano bot id"
|
||||||
|
default ""
|
||||||
|
help
|
||||||
|
Bot ID for Volc RTC service.
|
||||||
|
|
||||||
|
config USE_VOLC_RTC
|
||||||
|
bool "Use Volc RTC instead of WebSocket"
|
||||||
|
default n
|
||||||
|
help
|
||||||
|
Select to use Volc RTC protocol instead of WebSocket.
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 在`sdkconfig.defaults`中设置默认值:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Volc RTC Configuration
|
||||||
|
CONFIG_VOLC_INSTANCE_ID="your_instance_id"
|
||||||
|
CONFIG_VOLC_PRODUCT_KEY="your_product_key"
|
||||||
|
CONFIG_VOLC_PRODUCT_SECRET="your_product_secret"
|
||||||
|
CONFIG_VOLC_DEVICE_NAME="your_device_name"
|
||||||
|
CONFIG_VOLC_BOT_ID="your_bot_id"
|
||||||
|
CONFIG_USE_VOLC_RTC=y
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 创建配置JSON模板:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 对话AI配置格式
|
||||||
|
#define CONV_AI_CONFIG_FORMAT "{\"ver\": 1,\"iot\":{\"instance_id\":\"%s\",\"product_key\":\"%s\",\"product_secret\":\"%s\",\"device_name\":\"%s\"},\"rtc\":{\"log_level\":1,\"audio\":{\"publish\":true,\"subscribe\":true,\"codec\":4},\"video\":{\"publish\":false,\"subscribe\":false,\"codec\":1},\"params\":[\"{\\\"debug\\\":{\\\"log_to_console\\\":1}}\",\"{\\\"audio\\\":{\\\"codec\\\":{\\\"internal\\\":{\\\"enable\\\":1}}}}\",\"{\\\"rtc\\\":{\\\"access\\\":{\\\"concurrent_requests\\\":1}}}\",\"{\\\"rtc\\\":{\\\"ice\\\":{\\\"concurrent_agents\\\":1}}}\"]}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.3 创建VolcRtcProtocol类
|
||||||
|
|
||||||
|
创建新的`volc_rtc_protocol.h`和`volc_rtc_protocol.cc`文件,实现`Protocol`接口:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// volc_rtc_protocol.h
|
||||||
|
#ifndef _VOLC_RTC_PROTOCOL_H_
|
||||||
|
#define _VOLC_RTC_PROTOCOL_H_
|
||||||
|
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "volc_rtc.h"
|
||||||
|
#include <mutex>
|
||||||
|
#include <cJSON.h>
|
||||||
|
|
||||||
|
class VolcRtcProtocol : public Protocol {
|
||||||
|
public:
|
||||||
|
VolcRtcProtocol();
|
||||||
|
~VolcRtcProtocol();
|
||||||
|
|
||||||
|
void Start() override;
|
||||||
|
void SendAudio(const std::vector<uint8_t>& data) override;
|
||||||
|
bool OpenAudioChannel() override;
|
||||||
|
void CloseAudioChannel() override;
|
||||||
|
bool IsAudioChannelOpened() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SendText(const std::string& text) override;
|
||||||
|
void OnRtcMessage(volc_msg_t* msg);
|
||||||
|
void OnRtcData(const void* data, int data_len, volc_data_info_t* info);
|
||||||
|
|
||||||
|
volc_rtc_t rtc_handle_ = nullptr;
|
||||||
|
std::mutex rtc_mutex_;
|
||||||
|
bool is_audio_channel_opened_ = false;
|
||||||
|
cJSON* rtc_config_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// volc_rtc_protocol.cc
|
||||||
|
#include "volc_rtc_protocol.h"
|
||||||
|
#include "board.h"
|
||||||
|
#include "system_info.h"
|
||||||
|
#include "application.h"
|
||||||
|
#include "assets/lang_config.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <cJSON.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
#define TAG "VolcRTC"
|
||||||
|
|
||||||
|
// RTC消息回调函数
|
||||||
|
static void rtc_message_callback(void* context, volc_msg_t* msg) {
|
||||||
|
if (context) {
|
||||||
|
((VolcRtcProtocol*)context)->OnRtcMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTC数据回调函数
|
||||||
|
static void rtc_data_callback(void* context, const void* data, int data_len, volc_data_info_t* info) {
|
||||||
|
if (context) {
|
||||||
|
((VolcRtcProtocol*)context)->OnRtcData(data, data_len, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VolcRtcProtocol::VolcRtcProtocol() {
|
||||||
|
// 创建完整的火山RTC配置
|
||||||
|
char config_buf[1024] = {0};
|
||||||
|
|
||||||
|
// 使用配置模板构建完整配置
|
||||||
|
snprintf(config_buf, sizeof(config_buf), CONV_AI_CONFIG_FORMAT,
|
||||||
|
CONFIG_VOLC_INSTANCE_ID,
|
||||||
|
CONFIG_VOLC_PRODUCT_KEY,
|
||||||
|
CONFIG_VOLC_PRODUCT_SECRET,
|
||||||
|
CONFIG_VOLC_DEVICE_NAME);
|
||||||
|
|
||||||
|
// 解析配置JSON
|
||||||
|
rtc_config_ = cJSON_Parse(config_buf);
|
||||||
|
if (!rtc_config_) {
|
||||||
|
ESP_LOGE(TAG, "Failed to parse RTC config");
|
||||||
|
rtc_config_ = cJSON_CreateObject();
|
||||||
|
|
||||||
|
// 设置默认配置
|
||||||
|
cJSON_AddNumberToObject(rtc_config_, "audio.codec", AUDIO_CODEC_TYPE_OPUS);
|
||||||
|
cJSON_AddNumberToObject(rtc_config_, "video.codec", VIDEO_CODEC_TYPE_NONE);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "audio.publish", true);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "video.publish", false);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "audio.subscribe", true);
|
||||||
|
cJSON_AddBoolToObject(rtc_config_, "video.subscribe", false);
|
||||||
|
cJSON_AddNumberToObject(rtc_config_, "log_level", BYTE_RTC_LOG_LEVEL_INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VolcRtcProtocol::~VolcRtcProtocol() {
|
||||||
|
CloseAudioChannel();
|
||||||
|
|
||||||
|
if (rtc_config_) {
|
||||||
|
cJSON_Delete(rtc_config_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::Start() {
|
||||||
|
// RTC协议在OpenAudioChannel时启动,这里不需要额外操作
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VolcRtcProtocol::OpenAudioChannel() {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (rtc_handle_ != nullptr) {
|
||||||
|
ESP_LOGW(TAG, "RTC handle already exists");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建RTC实例 - 传入完整配置,包括鉴权信息
|
||||||
|
const char* app_id = CONFIG_VOLC_INSTANCE_ID; // 使用实例ID作为app_id
|
||||||
|
rtc_handle_ = volc_rtc_create(app_id, this, rtc_config_, rtc_message_callback, rtc_data_callback);
|
||||||
|
|
||||||
|
if (rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create RTC instance");
|
||||||
|
SetError(Lang::Strings::SERVER_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造IoT信息
|
||||||
|
volc_iot_info_t iot_info = {0};
|
||||||
|
|
||||||
|
// 设置设备ID
|
||||||
|
strncpy(iot_info.device_id, SystemInfo::GetMacAddress().c_str(), sizeof(iot_info.device_id));
|
||||||
|
|
||||||
|
// 设置产品信息用于鉴权
|
||||||
|
strncpy(iot_info.product_key, CONFIG_VOLC_PRODUCT_KEY, sizeof(iot_info.product_key));
|
||||||
|
strncpy(iot_info.product_secret, CONFIG_VOLC_PRODUCT_SECRET, sizeof(iot_info.product_secret));
|
||||||
|
strncpy(iot_info.device_name, CONFIG_VOLC_DEVICE_NAME, sizeof(iot_info.device_name));
|
||||||
|
|
||||||
|
// 设置房间信息
|
||||||
|
const char* bot_id = CONFIG_VOLC_BOT_ID;
|
||||||
|
|
||||||
|
// 启动RTC并加入房间 - 内部会处理token生成和鉴权
|
||||||
|
int ret = volc_rtc_start(rtc_handle_, bot_id, &iot_info);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start RTC: %d", ret);
|
||||||
|
volc_rtc_destroy(rtc_handle_);
|
||||||
|
rtc_handle_ = nullptr;
|
||||||
|
SetError(Lang::Strings::SERVER_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_audio_channel_opened_ = true;
|
||||||
|
|
||||||
|
if (on_audio_channel_opened_ != nullptr) {
|
||||||
|
on_audio_channel_opened_();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::CloseAudioChannel() {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (rtc_handle_ != nullptr) {
|
||||||
|
// 停止RTC
|
||||||
|
volc_rtc_stop(rtc_handle_);
|
||||||
|
|
||||||
|
// 销毁RTC实例
|
||||||
|
volc_rtc_destroy(rtc_handle_);
|
||||||
|
rtc_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_audio_channel_opened_ = false;
|
||||||
|
|
||||||
|
if (on_audio_channel_closed_ != nullptr) {
|
||||||
|
on_audio_channel_closed_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VolcRtcProtocol::IsAudioChannelOpened() const {
|
||||||
|
return is_audio_channel_opened_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::SendAudio(const std::vector<uint8_t>& data) {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGD(TAG, "RTC not connected, dropping audio data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造音频数据信息
|
||||||
|
volc_data_info_t data_info = {0};
|
||||||
|
data_info.type = VOLC_DATA_TYPE_AUDIO;
|
||||||
|
data_info.info.audio.data_type = VOLC_AUDIO_DATA_TYPE_OPUS;
|
||||||
|
|
||||||
|
// 发送音频数据
|
||||||
|
int ret = volc_rtc_send(rtc_handle_, data.data(), data.size(), &data_info);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to send audio data: %d", ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::SendText(const std::string& text) {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGD(TAG, "RTC not connected, dropping text message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造消息数据信息
|
||||||
|
volc_data_info_t data_info = {0};
|
||||||
|
data_info.type = VOLC_DATA_TYPE_MESSAGE;
|
||||||
|
data_info.info.message.is_binary = false;
|
||||||
|
|
||||||
|
// 发送文本消息
|
||||||
|
int ret = volc_rtc_send(rtc_handle_, text.c_str(), text.size(), &data_info);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to send text message: %d", ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::OnRtcMessage(volc_msg_t* msg) {
|
||||||
|
if (!msg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg->code) {
|
||||||
|
case VOLC_MSG_CONNECTED:
|
||||||
|
ESP_LOGI(TAG, "RTC connected");
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_DISCONNECTED:
|
||||||
|
ESP_LOGI(TAG, "RTC disconnected");
|
||||||
|
CloseAudioChannel();
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_USER_JOINED:
|
||||||
|
ESP_LOGI(TAG, "Remote user joined");
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_USER_OFFLINE:
|
||||||
|
ESP_LOGI(TAG, "Remote user offline");
|
||||||
|
break;
|
||||||
|
case VOLC_MSG_CONV_STATUS:
|
||||||
|
ESP_LOGI(TAG, "Conversation status: %d", msg->data.conv_status);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGD(TAG, "Unhandled RTC message: %d", msg->code);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VolcRtcProtocol::OnRtcData(const void* data, int data_len, volc_data_info_t* info) {
|
||||||
|
if (!data || !info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (info->type) {
|
||||||
|
case VOLC_DATA_TYPE_AUDIO:
|
||||||
|
if (on_incoming_audio_ != nullptr) {
|
||||||
|
on_incoming_audio_(std::vector<uint8_t>((uint8_t*)data, (uint8_t*)data + data_len));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VOLC_DATA_TYPE_MESSAGE:
|
||||||
|
if (info->info.message.is_binary) {
|
||||||
|
// 处理二进制消息
|
||||||
|
ESP_LOGD(TAG, "Received binary message, length: %d", data_len);
|
||||||
|
} else {
|
||||||
|
// 处理文本消息
|
||||||
|
std::string text((char*)data, data_len);
|
||||||
|
auto root = cJSON_Parse(text.c_str());
|
||||||
|
if (root != nullptr) {
|
||||||
|
if (on_incoming_json_ != nullptr) {
|
||||||
|
on_incoming_json_(root);
|
||||||
|
}
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGD(TAG, "Unhandled RTC data type: %d", info->type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_incoming_time_ = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.4 更新Protocol工厂
|
||||||
|
|
||||||
|
修改创建协议实例的代码,根据配置选择使用WebSocket或火山RTC:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 在application.cc或相关文件中
|
||||||
|
#include "websocket_protocol.h"
|
||||||
|
#include "volc_rtc_protocol.h"
|
||||||
|
|
||||||
|
// 创建协议实例
|
||||||
|
#ifdef CONFIG_USE_VOLC_RTC
|
||||||
|
protocol_ = new VolcRtcProtocol();
|
||||||
|
#else
|
||||||
|
protocol_ = new WebsocketProtocol();
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.5 添加语音打断支持
|
||||||
|
|
||||||
|
集成火山RTC的语音打断功能:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 在volc_rtc_protocol.h中添加
|
||||||
|
int SendInterrupt();
|
||||||
|
|
||||||
|
// 在volc_rtc_protocol.cc中实现
|
||||||
|
int VolcRtcProtocol::SendInterrupt() {
|
||||||
|
std::lock_guard<std::mutex> lock(rtc_mutex_);
|
||||||
|
|
||||||
|
if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
|
||||||
|
ESP_LOGD(TAG, "RTC not connected, cannot send interrupt");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return volc_rtc_interrupt(rtc_handle_);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在`Application`类中添加打断功能:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 在application.h中添加
|
||||||
|
void SendInterrupt();
|
||||||
|
|
||||||
|
// 在application.cc中实现
|
||||||
|
void Application::SendInterrupt() {
|
||||||
|
if (!protocol_) {
|
||||||
|
ESP_LOGE(TAG, "Protocol not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_USE_VOLC_RTC
|
||||||
|
auto* rtc_protocol = dynamic_cast<VolcRtcProtocol*>(protocol_);
|
||||||
|
if (rtc_protocol) {
|
||||||
|
rtc_protocol->SendInterrupt();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 业务功能实现
|
||||||
|
|
||||||
|
### 4.1 完整RTC连接通讯实现链路
|
||||||
|
|
||||||
|
#### 4.1.1 初始化与鉴权流程
|
||||||
|
|
||||||
|
1. **配置加载**:从Kconfig和sdkconfig加载火山RTC配置(INSTANCE_ID, PRODUCT_KEY, PRODUCT_SECRET, DEVICE_NAME, BOT_ID)
|
||||||
|
2. **时间同步**:初始化SNTP服务,确保设备时间与服务器同步(RTC连接需要准确时间戳)
|
||||||
|
3. **配置构建**:使用配置模板构建完整的JSON配置字符串,包含鉴权信息
|
||||||
|
4. **引擎初始化**:调用`volc_rtc_create()`创建RTC实例,传入配置信息
|
||||||
|
5. **IoT信息准备**:构建包含设备ID、产品信息的IoT结构体
|
||||||
|
6. **加入房间**:调用`volc_rtc_start()`加入RTC房间,内部自动处理token生成和鉴权
|
||||||
|
|
||||||
|
#### 4.1.2 启动引擎对话
|
||||||
|
|
||||||
|
1. **用户触发**:用户通过唤醒词或按钮触发对话
|
||||||
|
2. **开始监听**:`Application`类调用`protocol_->SendStartListening()`
|
||||||
|
3. **指令发送**:通过火山RTC发送开始监听指令给AI引擎
|
||||||
|
4. **音频采集**:启动麦克风采集音频数据
|
||||||
|
5. **音频发送**:将处理后的音频数据通过`protocol_->SendAudio()`发送给火山RTC
|
||||||
|
6. **AI处理**:火山RTC将音频数据转发给AI引擎进行处理
|
||||||
|
7. **结果返回**:AI引擎处理结果通过火山RTC返回给设备
|
||||||
|
8. **音频播放**:接收到的音频数据通过`on_incoming_audio_`回调传递给`Application`类播放
|
||||||
|
|
||||||
|
#### 4.1.3 关闭引擎
|
||||||
|
|
||||||
|
1. **对话结束**:AI引擎返回对话结束信号或用户手动触发关闭
|
||||||
|
2. **离开房间**:`Application`类调用`protocol_->CloseAudioChannel()`
|
||||||
|
3. **停止RTC**:调用`volc_rtc_stop()`停止RTC服务
|
||||||
|
4. **销毁实例**:调用`volc_rtc_destroy()`销毁RTC实例,释放资源
|
||||||
|
|
||||||
|
### 4.2 鉴权机制详解
|
||||||
|
|
||||||
|
火山RTC使用基于产品密钥的鉴权机制:
|
||||||
|
|
||||||
|
1. **配置信息**:设备需要提前配置INSTANCE_ID, PRODUCT_KEY, PRODUCT_SECRET, DEVICE_NAME
|
||||||
|
2. **Token生成**:RTC SDK内部使用产品密钥生成临时token
|
||||||
|
3. **Token使用**:在`byte_rtc_join_room()`调用时传入token进行身份验证
|
||||||
|
4. **Token过期处理**:SDK监听`on_token_privilege_will_expire`事件,在token即将过期时自动更新
|
||||||
|
|
||||||
|
### 4.3 音频数据流程
|
||||||
|
|
||||||
|
1. **音频输入**:麦克风采集的音频数据经过`AudioProcessor`处理后,通过`protocol_->SendAudio()`发送给火山RTC
|
||||||
|
2. **音频传输**:火山RTC使用优化的RTC协议传输音频数据,支持丢包重传和抗抖动
|
||||||
|
3. **AI处理**:火山引擎接收音频数据,进行语音识别和AI处理
|
||||||
|
4. **结果返回**:AI处理结果(音频和文本)通过火山RTC返回给设备
|
||||||
|
5. **音频输出**:接收到的音频数据通过`on_incoming_audio_`回调传递给`Application`类,然后播放出来
|
||||||
|
|
||||||
|
### 4.4 消息处理
|
||||||
|
|
||||||
|
1. **文本消息**:通过`protocol_->SendText()`发送文本消息
|
||||||
|
2. **JSON消息**:接收到的JSON消息通过`on_incoming_json_`回调传递给`Application`类进行处理
|
||||||
|
3. **二进制消息**:支持发送和接收二进制消息,用于特殊功能扩展
|
||||||
|
|
||||||
|
## 5. 测试和验证
|
||||||
|
|
||||||
|
### 5.1 功能测试
|
||||||
|
|
||||||
|
1. **连接测试**:验证设备能否成功连接到火山RTC服务器
|
||||||
|
2. **音频传输测试**:验证音频数据能否正常发送和接收
|
||||||
|
3. **语音打断测试**:验证语音打断功能能否正常工作
|
||||||
|
4. **异常处理测试**:验证网络异常或服务器异常时的处理逻辑
|
||||||
|
|
||||||
|
### 5.2 性能测试
|
||||||
|
|
||||||
|
1. **延迟测试**:测量音频从采集到播放的延迟
|
||||||
|
2. **稳定性测试**:长时间运行测试,验证系统稳定性
|
||||||
|
3. **资源占用测试**:监控CPU和内存占用情况
|
||||||
|
|
||||||
|
### 5.3 兼容性测试
|
||||||
|
|
||||||
|
1. **设备兼容性**:测试在不同硬件设备上的运行情况
|
||||||
|
2. **网络兼容性**:测试在不同网络环境下的运行情况
|
||||||
|
|
||||||
|
## 6. 注意事项
|
||||||
|
|
||||||
|
1. **配置管理**:
|
||||||
|
- 确保火山RTC的INSTANCE_ID, PRODUCT_KEY, PRODUCT_SECRET, DEVICE_NAME和BOT_ID等配置正确
|
||||||
|
- 避免将敏感信息(如PRODUCT_SECRET)硬编码到代码中,应通过配置文件管理
|
||||||
|
- 定期更新密钥,确保安全性
|
||||||
|
|
||||||
|
2. **时间同步**:
|
||||||
|
- RTC连接需要准确的设备时间,确保SNTP服务正常工作
|
||||||
|
- 在网络不稳定时,考虑使用本地RTC作为备用时间源
|
||||||
|
|
||||||
|
3. **错误处理**:
|
||||||
|
- 完善异常处理逻辑,特别是网络异常和服务器异常情况
|
||||||
|
- 实现token过期自动更新机制
|
||||||
|
- 添加连接重试逻辑,提高系统稳定性
|
||||||
|
|
||||||
|
4. **资源管理**:
|
||||||
|
- 合理管理RTC资源,避免内存泄漏
|
||||||
|
- 在不需要RTC服务时及时关闭连接
|
||||||
|
- 监控系统资源占用情况,避免资源耗尽
|
||||||
|
|
||||||
|
5. **日志管理**:
|
||||||
|
- 添加详细的日志,便于调试和问题定位
|
||||||
|
- 分类管理日志级别,在生产环境中降低日志级别以减少性能开销
|
||||||
|
|
||||||
|
6. **版本兼容性**:
|
||||||
|
- 确保火山RTC SDK版本与项目兼容
|
||||||
|
- 定期更新SDK,获取最新的功能和安全修复
|
||||||
|
|
||||||
|
7. **网络环境**:
|
||||||
|
- 确保设备处于稳定的网络环境中
|
||||||
|
- 考虑网络切换场景(如WiFi/4G切换)的处理逻辑
|
||||||
|
|
||||||
|
## 7. 结论
|
||||||
|
|
||||||
|
本方案通过创建新的`VolcRtcProtocol`类实现了与`WebsocketProtocol`相同的接口,从而实现了无缝替换。火山RTC提供了更丰富的功能和更好的音频质量,能够满足实时通信的需求。同时,本方案保持了Kapi_Rtc项目的现有架构不变,降低了迁移风险和成本。
|
||||||
|
|
||||||
|
## 8. 后续支持
|
||||||
|
|
||||||
|
如果您需要进一步的帮助,我可以:
|
||||||
|
1. 协助实现文档中的方案,包括代码编写和配置
|
||||||
|
2. 回答关于RTC连接鉴权和SDK配置的具体问题
|
||||||
|
3. 提供完整的实时AI对话业务实现链路的技术支持
|
||||||
|
4. 帮助进行测试和调试,确保RTC连接和音频传输正常工作
|
||||||
|
5. 优化系统性能,提高实时对话的稳定性和响应速度
|
||||||
|
|
||||||
|
请随时告诉我您的需求。
|
||||||
327
03AEC_VOICE_INTERRUPT_PORTING_PLAN.md
Normal file
327
03AEC_VOICE_INTERRUPT_PORTING_PLAN.md
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
# AEC语音打断功能移植方案:Airhub_Rtc_h → Kapi_Rtc
|
||||||
|
|
||||||
|
## 1. 可行性总结
|
||||||
|
|
||||||
|
经过全面分析,在不启用ADF架构的情况下,将Airhub_Rtc_h项目中基于双麦克风和ES7210实现的AEC语音打断功能移植到Kapi_Rtc项目是**技术可行的**。主要依据如下:
|
||||||
|
|
||||||
|
- Kapi_Rtc项目已集成ESP-SR组件,提供了独立于ADF的AEC和VAD API
|
||||||
|
- 两个项目均支持ESP32_S3_KORVO2_V3开发板,硬件兼容性良好
|
||||||
|
- Kapi_Rtc已有AudioProcessor类可扩展以支持AEC功能
|
||||||
|
- 两个项目都支持ES7210编解码器,配置方式可兼容
|
||||||
|
|
||||||
|
## 2. 移植实现方案
|
||||||
|
|
||||||
|
### 2.1 ES7210双麦克风配置移植
|
||||||
|
|
||||||
|
1. **创建ES7210配置初始化模块**:
|
||||||
|
```cpp
|
||||||
|
// 在Kapi_Rtc中创建es7210_mic_config.h/cpp文件
|
||||||
|
#include "es7210_adc.h"
|
||||||
|
|
||||||
|
#define ES7210_MIC_COMBO_MIC1_MIC3 (ES7210_INPUT_MIC1 | ES7210_INPUT_MIC3) // 双麦克风配置
|
||||||
|
|
||||||
|
esp_err_t init_es7210_with_dual_mic(const audio_codec_ctrl_if_t *ctrl_if) {
|
||||||
|
es7210_codec_cfg_t es7210_cfg = {
|
||||||
|
.ctrl_if = ctrl_if,
|
||||||
|
.master_mode = false,
|
||||||
|
.mic_selected = ES7210_MIC_COMBO_MIC1_MIC3,
|
||||||
|
.mclk_src = ES7210_MCLK_FROM_PAD,
|
||||||
|
.mclk_div = 256
|
||||||
|
};
|
||||||
|
|
||||||
|
const audio_codec_if_t *es7210_codec = es7210_codec_new(&es7210_cfg);
|
||||||
|
if (es7210_codec == NULL) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置麦克风增益为30dB
|
||||||
|
es7210_codec->set_mic_gain(es7210_codec, GAIN_30DB);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **在AudioCodec基类中增加ES7210配置支持**:
|
||||||
|
```cpp
|
||||||
|
// 修改AudioCodec类,增加双麦克风配置接口
|
||||||
|
class AudioCodec {
|
||||||
|
public:
|
||||||
|
// 现有接口...
|
||||||
|
|
||||||
|
virtual bool configureDualMicrophone() = 0;
|
||||||
|
virtual bool setMicrophoneGain(int gain_db) = 0;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **在具体编解码器实现中添加ES7210配置**:
|
||||||
|
```cpp
|
||||||
|
bool Es7210AudioCodec::configureDualMicrophone() {
|
||||||
|
return (init_es7210_with_dual_mic(ctrl_if_) == ESP_OK);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 AEC功能集成到AudioProcessor
|
||||||
|
|
||||||
|
1. **扩展AudioProcessor类**:
|
||||||
|
```cpp
|
||||||
|
// 修改audio_processor.h
|
||||||
|
class AudioProcessor {
|
||||||
|
private:
|
||||||
|
aec_handle_t *aec_instance_ = nullptr;
|
||||||
|
std::vector<int16_t> reference_buffer_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// 现有方法...
|
||||||
|
|
||||||
|
// 添加AEC相关方法
|
||||||
|
void InitializeAEC(int sample_rate, int channels);
|
||||||
|
void ProcessWithAEC(const std::vector<int16_t>& mic_data,
|
||||||
|
const std::vector<int16_t>& ref_data,
|
||||||
|
std::vector<int16_t>& out_data);
|
||||||
|
void SetReferenceAudio(const std::vector<int16_t>& ref_data);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **实现AEC功能**:
|
||||||
|
```cpp
|
||||||
|
// 在audio_processor.cc中实现
|
||||||
|
void AudioProcessor::InitializeAEC(int sample_rate, int channels) {
|
||||||
|
// 使用ESP-SR的AEC API,不依赖ADF
|
||||||
|
aec_instance_ = aec_create(sample_rate, 4, channels, AEC_MODE_SR_LOW_COST);
|
||||||
|
if (aec_instance_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize AEC");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "AEC initialized successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioProcessor::SetReferenceAudio(const std::vector<int16_t>& ref_data) {
|
||||||
|
reference_buffer_ = ref_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioProcessor::ProcessWithAEC(const std::vector<int16_t>& mic_data,
|
||||||
|
const std::vector<int16_t>& ref_data,
|
||||||
|
std::vector<int16_t>& out_data) {
|
||||||
|
if (aec_instance_ == nullptr) {
|
||||||
|
// 如果AEC初始化失败,直接返回麦克风数据
|
||||||
|
out_data = mic_data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保输出缓冲区足够大
|
||||||
|
out_data.resize(mic_data.size());
|
||||||
|
|
||||||
|
// 调用AEC处理函数
|
||||||
|
size_t processed_size = aec_process(aec_instance_,
|
||||||
|
mic_data.data(),
|
||||||
|
ref_data.data(),
|
||||||
|
out_data.data(),
|
||||||
|
mic_data.size() / sizeof(int16_t));
|
||||||
|
|
||||||
|
if (processed_size == 0) {
|
||||||
|
ESP_LOGW(TAG, "AEC processing failed");
|
||||||
|
out_data = mic_data; // 失败时返回原始数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **修改音频处理流程**:
|
||||||
|
```cpp
|
||||||
|
void AudioProcessor::AudioProcessorTask() {
|
||||||
|
// 现有代码...
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// 等待运行标志
|
||||||
|
xEventGroupWaitBits(event_group_, PROCESSOR_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY);
|
||||||
|
|
||||||
|
// 获取麦克风数据
|
||||||
|
auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
|
||||||
|
if (res == nullptr || res->ret_value == ESP_FAIL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
std::vector<int16_t> mic_data((int16_t*)res->data,
|
||||||
|
(int16_t*)res->data + res->data_size / sizeof(int16_t));
|
||||||
|
std::vector<int16_t> processed_data;
|
||||||
|
|
||||||
|
// AEC处理
|
||||||
|
if (aec_instance_ != nullptr && !reference_buffer_.empty()) {
|
||||||
|
ProcessWithAEC(mic_data, reference_buffer_, processed_data);
|
||||||
|
} else {
|
||||||
|
processed_data = mic_data; // 无AEC时使用原始数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后续VAD处理和回调...
|
||||||
|
// 智能语音打断逻辑(使用处理后的数据)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 语音打断功能实现
|
||||||
|
|
||||||
|
1. **实现智能语音确认机制**:
|
||||||
|
```cpp
|
||||||
|
// 在audio_processor.h中添加
|
||||||
|
struct VoiceInterruptConfig {
|
||||||
|
int min_speech_duration_ms = 200; // 最小语音时长,防止回声误触发
|
||||||
|
float volume_threshold = 0.05f; // 音量阈值
|
||||||
|
};
|
||||||
|
|
||||||
|
class AudioProcessor {
|
||||||
|
private:
|
||||||
|
// 现有成员...
|
||||||
|
VoiceInterruptConfig interrupt_config_;
|
||||||
|
int speech_duration_count_ = 0;
|
||||||
|
bool is_voice_interrupt_active_ = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// 现有方法...
|
||||||
|
|
||||||
|
void ConfigureVoiceInterrupt(const VoiceInterruptConfig& config);
|
||||||
|
bool DetectVoiceInterrupt(const std::vector<int16_t>& audio_data);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **实现语音检测逻辑**:
|
||||||
|
```cpp
|
||||||
|
// 在audio_processor.cc中实现
|
||||||
|
void AudioProcessor::ConfigureVoiceInterrupt(const VoiceInterruptConfig& config) {
|
||||||
|
interrupt_config_ = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioProcessor::DetectVoiceInterrupt(const std::vector<int16_t>& audio_data) {
|
||||||
|
// 计算音频能量
|
||||||
|
float energy = 0.0f;
|
||||||
|
for (int16_t sample : audio_data) {
|
||||||
|
float normalized = (float)sample / 32768.0f;
|
||||||
|
energy += normalized * normalized;
|
||||||
|
}
|
||||||
|
energy /= audio_data.size();
|
||||||
|
|
||||||
|
// 音量检测
|
||||||
|
bool volume_detected = (energy > interrupt_config_.volume_threshold * interrupt_config_.volume_threshold);
|
||||||
|
|
||||||
|
if (volume_detected) {
|
||||||
|
speech_duration_count_ += 16; // 假设每帧16ms
|
||||||
|
} else {
|
||||||
|
speech_duration_count_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 语音确认:持续时间超过阈值
|
||||||
|
bool voice_confirmed = (speech_duration_count_ >= interrupt_config_.min_speech_duration_ms);
|
||||||
|
|
||||||
|
if (voice_confirmed && !is_voice_interrupt_active_) {
|
||||||
|
is_voice_interrupt_active_ = true;
|
||||||
|
return true; // 触发语音打断
|
||||||
|
} else if (!volume_detected) {
|
||||||
|
is_voice_interrupt_active_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **集成到处理流程**:
|
||||||
|
```cpp
|
||||||
|
void AudioProcessor::AudioProcessorTask() {
|
||||||
|
// 现有代码...
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// 获取并处理音频数据...
|
||||||
|
|
||||||
|
// 调用语音打断检测
|
||||||
|
if (DetectVoiceInterrupt(processed_data)) {
|
||||||
|
ESP_LOGI(TAG, "Voice interrupt detected!");
|
||||||
|
// 触发打断回调
|
||||||
|
if (voice_interrupt_callback_) {
|
||||||
|
voice_interrupt_callback_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 内存优化策略
|
||||||
|
|
||||||
|
1. **SPIRAM使用优化**:
|
||||||
|
```cpp
|
||||||
|
// 修改缓冲区分配方式
|
||||||
|
void AudioProcessor::Initialize(int sample_rate, int channels) {
|
||||||
|
// 现有初始化代码...
|
||||||
|
|
||||||
|
// 使用SPIRAM分配大缓冲区
|
||||||
|
size_t buffer_size = sample_rate * channels * 2; // 2秒的缓冲区
|
||||||
|
reference_buffer_.resize(buffer_size);
|
||||||
|
|
||||||
|
// 确保AEC使用正确的内存
|
||||||
|
// AEC内部会使用heap_caps_aligned_alloc,需要确保配置正确
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **配置调整**:
|
||||||
|
- 修改`sdkconfig.defaults`以优化SPIRAM使用:
|
||||||
|
```
|
||||||
|
# 启用SPIRAM
|
||||||
|
CONFIG_SPIRAM=y
|
||||||
|
|
||||||
|
# 优化内存分配策略
|
||||||
|
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=8192
|
||||||
|
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536
|
||||||
|
|
||||||
|
# 增加任务栈大小
|
||||||
|
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 注意事项
|
||||||
|
|
||||||
|
### 3.1 硬件兼容性注意事项
|
||||||
|
|
||||||
|
- **麦克风选择**:确保正确配置ES7210的MIC1和MIC3(或其他麦克风组合)
|
||||||
|
- **增益设置**:根据实际硬件调整麦克风增益,建议从30dB开始测试
|
||||||
|
- **参考音频通道**:确保提供正确的扬声器参考音频给AEC算法
|
||||||
|
|
||||||
|
### 3.2 软件实现注意事项
|
||||||
|
|
||||||
|
- **采样率一致性**:确保AEC、麦克风输入和参考音频使用相同的采样率
|
||||||
|
- **内存对齐**:AEC处理需要16字节对齐的内存,使用`heap_caps_aligned_alloc`
|
||||||
|
- **实时性能**:AEC处理较为耗时,确保设置适当的任务优先级
|
||||||
|
- **错误处理**:添加完善的错误处理和回退机制
|
||||||
|
|
||||||
|
### 3.3 调优建议
|
||||||
|
|
||||||
|
1. **AEC参数调优**:
|
||||||
|
- 调整`filter_length`参数(推荐值:4)
|
||||||
|
- 根据实际使用场景选择合适的AEC模式
|
||||||
|
|
||||||
|
2. **VAD参数调优**:
|
||||||
|
- 调整语音打断的最小持续时间(建议200ms以上)
|
||||||
|
- 根据环境噪声调整音量阈值
|
||||||
|
|
||||||
|
3. **性能监控**:
|
||||||
|
- 添加CPU使用率监控
|
||||||
|
- 监控内存使用情况,避免内存泄漏
|
||||||
|
|
||||||
|
## 4. 测试方案
|
||||||
|
|
||||||
|
1. **基础功能测试**:
|
||||||
|
- 验证ES7210双麦克风配置是否正确
|
||||||
|
- 确认AEC初始化和处理无错误
|
||||||
|
|
||||||
|
2. **AEC性能测试**:
|
||||||
|
- 在不同距离测试回声消除效果
|
||||||
|
- 在不同音量下测试性能
|
||||||
|
|
||||||
|
3. **语音打断测试**:
|
||||||
|
- 测试在设备播放时的语音打断功能
|
||||||
|
- 测试不同距离和音量下的打断灵敏度
|
||||||
|
|
||||||
|
4. **稳定性测试**:
|
||||||
|
- 长时间运行测试,监控内存泄漏
|
||||||
|
- 测试在各种环境噪声条件下的稳定性
|
||||||
|
|
||||||
|
## 5. 总结
|
||||||
|
|
||||||
|
本移植方案通过直接使用ESP-SR组件提供的AEC API,无需启用完整的ADF架构,即可实现从Airhub_Rtc_h到Kapi_Rtc的AEC语音打断功能移植。方案保留了Kapi_Rtc现有的音频处理架构,通过扩展AudioProcessor类和相关接口,实现了双麦克风配置、AEC处理和智能语音打断功能。
|
||||||
|
|
||||||
|
通过合理的内存优化和参数调优,可以在保证系统稳定性的同时,实现良好的回声消除和语音打断效果。
|
||||||
71
04-2025-11-21音频优化记录.md
Normal file
71
04-2025-11-21音频优化记录.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# 2025-11-21 Kapi_Rtc 音频/RTC 问题分析与烧录测试总结
|
||||||
|
|
||||||
|
## 概览
|
||||||
|
- 目标:修复开机播报“尖锐/刺耳”、欢迎语速度异常;保证火山 RTC 入房稳定,避免设备重启;比较 Airhub_Rtc_h 参考实现。
|
||||||
|
- 结论:问题核心在“开机阶段单声道 PCM 直接写入立体声 I2S 槽”的通道不匹配。保持设备端立体声输出以保证 RTC 连接稳定,同时在软件层将单声道复制为双声道即可消除“尖锐”。
|
||||||
|
|
||||||
|
## 关键事实与参数
|
||||||
|
- 开机 P3 资产参数(生成规格):采样率 `16000 Hz`、帧时长 `60 ms`、声道 `单声道`、编码 `OPUS`。
|
||||||
|
- RTC下行音频参数:
|
||||||
|
- OPUS:采样率 `16000 Hz`、帧时长 `60 ms`、单声道(`main/protocols/volc_rtc_protocol.cc:304-320`)。
|
||||||
|
- PCM:采样率 `8000 Hz`、帧时长 `20 ms`、单声道(同上,`downlink_is_pcm_` 为真时)。
|
||||||
|
- 设备端编解码器(ES8311)初始化:默认立体声槽与双通道输出更稳定。
|
||||||
|
- 输出通道:`output_channels_ = 2`(`main/audio_codecs/es8311_audio_codec.cc:12-15`)。
|
||||||
|
- I2S 槽:`slot_mode = STEREO`、`slot_mask = BOTH`(`main/audio_codecs/es8311_audio_codec.cc:105-109`)。
|
||||||
|
|
||||||
|
## 今日修改与实现
|
||||||
|
- 在开机阶段的“非管线路径”增加单声道→立体声复制,避免直接将单声道 PCM 写入立体声槽:
|
||||||
|
- 代码位置:`main/application.cc:1224-1230`。
|
||||||
|
- 逻辑:当 `codec->output_channels()==2` 时,将 `pcm` 复制为交错的 `stereo`(L=R)后输出;否则原样输出。
|
||||||
|
|
||||||
|
## 现象与测试过程
|
||||||
|
- 开机播报尖锐(问题初始):
|
||||||
|
- 路径:开机阶段未启用播放管线,走直接输出(`main/application.cc:1224-1230`)。
|
||||||
|
- 原因:单声道 PCM 与设备端立体声槽不匹配,导致通道/槽打包伪像,主观听感“尖锐”。
|
||||||
|
- 资产引用:
|
||||||
|
- 触发播放:`main/application.cc:531`(`PlaySound(Lang::Sounds::P3_LALA_KAIJIBOBAO);`)。
|
||||||
|
- 资产绑定:`main/assets/lang_config.h:275-280`(`LALA_kaijibobao.p3` 二进制绑定)。
|
||||||
|
- 文件:`main/assets/zh-CN/LALA_kaijibobao.p3`、`main/assets/zh-CN/LALA_lianjiewangluo.p3`。
|
||||||
|
|
||||||
|
- 修复后(非管线分支 L/R 复制):
|
||||||
|
- 结果:开机播报“尖锐”消失;欢迎语与 RTC 对话不受影响。
|
||||||
|
- 说明:开机阶段保留 `OpusResampler` 的 16k→设备输出采样率(常为 24k)重采样质量,避免最近邻重采样造成伪像。
|
||||||
|
|
||||||
|
- 测试方案 A:尝试将设备端改为单通道/单声道 I2S 槽(MONO):
|
||||||
|
- 修改:`main/audio_codecs/es8311_audio_codec.cc:13-14` 将输出通道 `2→1`;`110-111` 将 `STEREO→MONO`(左槽)。
|
||||||
|
- 现象:RTC 入房超时,日志显示:
|
||||||
|
- `VolcRtcProtocol: Wait connect bits=0x0 free_heap=...` 与 `RTC连接超时`(`main/protocols/volc_rtc_protocol.cc:212-216`,终端选中行 Terminal#296-297)。
|
||||||
|
- 分析:RTC SDK 音频通道初始化/握手更依赖“标准立体声 I2S 时序”,MONO 改动改变 WS 宽度与槽掩码,仅跑一个槽,导致 SDK 不置位连接成功事件(`VOLC_MSG_CONNECTED` 未触发,`main/protocols/volc_rtc_protocol.cc:260-266`),最终超时。
|
||||||
|
- 结论:在当前 SDK/驱动组合下,保持立体声输出更稳;不建议在开机阶段强行改为 MONO 以避免连接不稳定。
|
||||||
|
|
||||||
|
- 测试方案 B:将 P3 资产改为双声道 OPUS(16kHz/60ms):
|
||||||
|
- 可行性:解码后直接生成双声道 PCM,与立体声槽完全匹配;即使直接输出也不会“尖锐”。
|
||||||
|
- 代价:资产体积与解码开销增加,RTC 下行仍是单声道;属于“资源换兼容”,与当前 RTC 参数不匹配,无额外协同收益。
|
||||||
|
- 结论:不推荐作为常规方案。
|
||||||
|
|
||||||
|
## 原理解释与定位
|
||||||
|
- 为什么开机解码是单声道 PCM:
|
||||||
|
- P3 资产与解码器均为单声道配置(`SetDecodeSampleRate(16000,60)`,`main/application.cc:269-292`)。
|
||||||
|
- 为什么立体声输出更稳:
|
||||||
|
- 设备端 ES8311 默认按立体声槽/双通道配置,I2S 时钟/槽时序与 DMA 工作稳定性更好;RTC SDK 在连接阶段可能验证或依赖该时序,MONO 改动会破坏这些假设。
|
||||||
|
- 为什么对齐 RTC 参数并不能单独解决尖锐:
|
||||||
|
- 开机 P3 资产已与 RTC OPUS 参数一致(16k/60ms/单声道),问题不在采样率/帧长,而在“声道与 I2S 槽模式的匹配”。
|
||||||
|
|
||||||
|
## 代码引用(便于定位)
|
||||||
|
- 开机资产播放触发:`main/application.cc:531`。
|
||||||
|
- 资产二进制绑定:`main/assets/lang_config.h:275-280`。
|
||||||
|
- 直接输出路径与修复点:`main/application.cc:1224-1230`。
|
||||||
|
- ES8311 通道与槽配置:`main/audio_codecs/es8311_audio_codec.cc:12-15`、`105-111`、`171-179`。
|
||||||
|
- RTC 连接等待与事件位:`main/protocols/volc_rtc_protocol.cc:212-216`(等待位)、`260-266`(连接事件)。
|
||||||
|
- RTC 下行类型与采样率识别:`main/protocols/volc_rtc_protocol.cc:304-320`。
|
||||||
|
|
||||||
|
## 建议与结论
|
||||||
|
- 保持设备端立体声输出(保证 RTC 入房稳定)。
|
||||||
|
- 保持资产单声道与 RTC 单声道一致;在软件层面针对未启用管线的播放路径增加单声道→立体声复制(已实现)。
|
||||||
|
- 不建议将 P3 资产改为双声道;如确有需要,也应评估体积/性能影响。
|
||||||
|
- 若要在开机阶段采用 MONO,再在入房后切回 STEREO,需要细致的时序与设备重建流程,风险较高;当前无需引入。
|
||||||
|
|
||||||
|
## 后续动作
|
||||||
|
- 继续观察开机播报听感与 RTC 入房稳定性。
|
||||||
|
- 如需进一步优化淡入或重采样质量,可微调淡入时长或评估更高质量重采样算法,但当前修复已满足听感与稳定性目标。
|
||||||
|
|
||||||
291
06-AI对话和电子吧唧双模式适配说明.md
Normal file
291
06-AI对话和电子吧唧双模式适配说明.md
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
# AI对话 + 电子吧唧 双模式适配说明
|
||||||
|
|
||||||
|
> 更新日期:2026-02-27
|
||||||
|
> 硬件平台:movecall-moji-esp32s3 (ESP32-S3-N16R8)
|
||||||
|
> ESP-IDF版本:5.4.2
|
||||||
|
> LVGL版本:8.3.11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目现状
|
||||||
|
|
||||||
|
### 1.1 主项目 (Baji_Rtc_Toy)
|
||||||
|
|
||||||
|
基于 AI小智 开源项目改造,当前已集成功能:
|
||||||
|
- 火山引擎 RTC 语音对话(WiFi 连接)
|
||||||
|
- BLE 配网(Bluedroid,Service 0xABF0)
|
||||||
|
- 音频编解码(ES8311 + Opus)
|
||||||
|
- 唤醒词检测(esp-sr AFE)
|
||||||
|
- **LVGL 8.3.11 LCD 显示**(Phase 1 已完成,开机显示 ScreenHome)
|
||||||
|
- ST77916 QSPI 360×360 LCD + CST816S 触摸(已初始化)
|
||||||
|
|
||||||
|
### 1.2 dzbj 子项目 (电子吧唧)
|
||||||
|
|
||||||
|
独立的 ESP32-S3 LVGL 项目,位于 `/dzbj/` 目录,功能:
|
||||||
|
- 360×360 ST77916 QSPI LCD + CST816S 触摸
|
||||||
|
- LVGL 8.3.11 三屏界面(ScreenHome/ScreenImg/ScreenSet)
|
||||||
|
- BLE GATT 图片传输服务(Service 0x0B00)
|
||||||
|
- GIF 播放、JPEG 解码、SPIFFS 图片管理
|
||||||
|
- 低功耗休眠/唤醒管理(10s 超时熄屏)
|
||||||
|
- PWM 背光控制
|
||||||
|
|
||||||
|
### 1.3 实施进度
|
||||||
|
|
||||||
|
| 阶段 | 状态 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Phase 1: 点亮屏幕 | **已完成** | LCD + LVGL + ScreenHome 显示 |
|
||||||
|
| Phase 2+4: 完整模式 + 切换 | **实施中** | 移植 dzbj 全模块 + 双模式切换 |
|
||||||
|
| Phase 3: AI 聊天 UI | 待定 | 基于 LVGL 的 emoji + 聊天气泡 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、双模式架构设计
|
||||||
|
|
||||||
|
### 2.1 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ LVGL 8.3.11 + LCD │
|
||||||
|
│ (常驻,两个模式共享显示硬件) │
|
||||||
|
├────────────────────┬────────────────────────────┤
|
||||||
|
│ AI 对话模式 │ 电子吧唧模式 │
|
||||||
|
│ (mode=0, 默认) │ (mode=1) │
|
||||||
|
│ │ │
|
||||||
|
│ WiFi + RTC 协议 │ BLE GATT Server │
|
||||||
|
│ ScreenHome (仅显示) │ ScreenHome/Img/Set │
|
||||||
|
│ 音频编解码 + 唤醒词 │ BLE 图片传输 (0x0B00) │
|
||||||
|
│ PowerSaveTimer │ sleep_mgr (10s熄屏) │
|
||||||
|
│ IMU 传感器 │ battery 电池监测 │
|
||||||
|
│ 电量检测 (板级) │ SPIFFS 图片管理 │
|
||||||
|
├────────────────────┴────────────────────────────┤
|
||||||
|
│ BOOT 双击 切换(写NVS + 重启) │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 模式定义
|
||||||
|
|
||||||
|
**AI 对话模式** (device_mode=0, 默认):
|
||||||
|
- 网络:WiFi 连接
|
||||||
|
- 协议:火山引擎 RTC 实时对话
|
||||||
|
- 音频:唤醒词检测 + Opus 编解码 + I2S 输出
|
||||||
|
- 显示:ScreenHome(仅显示,无触摸交互)
|
||||||
|
- BLE:**关闭**(仅配网时启动)
|
||||||
|
|
||||||
|
**电子吧唧模式** (device_mode=1):
|
||||||
|
- 网络:**WiFi 关闭**
|
||||||
|
- BLE:GATT Server(图片传输服务 0x0B00)
|
||||||
|
- 显示:ScreenHome → ScreenImg(图片浏览)→ ScreenSet(设置)
|
||||||
|
- 功能:JPEG 解码、GIF 播放、SPIFFS 图片管理、低功耗、电池监测
|
||||||
|
|
||||||
|
### 2.3 模式切换方案
|
||||||
|
|
||||||
|
**方案:重启切换(NVS 标志)**
|
||||||
|
|
||||||
|
```
|
||||||
|
BOOT 双击 → 读取 NVS device_mode → 切换 0↔1 → 写入 NVS → esp_restart()
|
||||||
|
```
|
||||||
|
|
||||||
|
切换时间约 3-4 秒(重启时间),NVS 擦写寿命 10-100万次,无限次切换。
|
||||||
|
|
||||||
|
**选择重启而非热切换的原因**:
|
||||||
|
1. WiFi + BLE Bluedroid 同时运行内部 SRAM 不足(约需 280KB,可用 ~334KB)
|
||||||
|
2. 热切换需处理大量资源释放/重建(协议、音频管道、FreeRTOS 任务),复杂度极高
|
||||||
|
3. Application 单例内部状态(event_group, opus 编解码器, background_task)难以干净重置
|
||||||
|
4. 重启方式简单可靠,避免内存泄漏和碎片化风险
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、内存预算分析
|
||||||
|
|
||||||
|
### 3.1 硬件规格
|
||||||
|
|
||||||
|
- **内部 SRAM**:~334KB DIRAM(可用)
|
||||||
|
- **PSRAM**:8MB OCT-SPI 80MHz
|
||||||
|
- **Flash**:16MB
|
||||||
|
|
||||||
|
### 3.2 各模式内存占用
|
||||||
|
|
||||||
|
| 场景 | 估算占用 | 剩余 | 可行性 |
|
||||||
|
|------|----------|------|--------|
|
||||||
|
| AI 对话模式(WiFi+RTC+音频+LVGL) | ~212-237KB | ~97-122KB | **可行**(偏紧) |
|
||||||
|
| 电子吧唧模式(BLE+LVGL+SPIFFS) | ~190KB | ~144KB | **可行**(充裕) |
|
||||||
|
| 两模式同时运行 | ~274-345KB | 不足 | **不可行** |
|
||||||
|
|
||||||
|
### 3.3 关键验证数据
|
||||||
|
|
||||||
|
- Phase 1 测试:WiFi + BLE 同时运行导致 `assert failed: vQueueDelete queue.c:2355`(FreeRTOS 信号量分配失败)
|
||||||
|
- BLE 配网成功后 `xTaskCreate` 分配 2048 栈失败(已改用 `esp_timer` 解决)
|
||||||
|
- 确认两模式必须互斥运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、启动流程
|
||||||
|
|
||||||
|
### 4.1 双模式启动序列
|
||||||
|
|
||||||
|
```
|
||||||
|
开机
|
||||||
|
│
|
||||||
|
├── 板级构造函数(通用)
|
||||||
|
│ ├── PowerSaveTimer 初始化
|
||||||
|
│ ├── InitializeButtons()(主项目 Button 类,双击注册在此)
|
||||||
|
│ ├── InitializeCodecI2c()
|
||||||
|
│ ├── dzbj_display_init() ← LCD + LVGL 始终初始化
|
||||||
|
│ │
|
||||||
|
│ ├── if device_mode == BADGE (吧唧模式)
|
||||||
|
│ │ └── InitializeBadgeMode()
|
||||||
|
│ │ ├── fatfs_init() // SPIFFS 文件系统
|
||||||
|
│ │ ├── init_spiffs_image_list() // 扫描图片
|
||||||
|
│ │ ├── dzbj_button_init() // ISR按键
|
||||||
|
│ │ ├── battery_init() // 电池检测
|
||||||
|
│ │ ├── dzbj_ble_init() // BLE 图传
|
||||||
|
│ │ └── sleep_mgr_init() // 低功耗管理
|
||||||
|
│ │
|
||||||
|
│ └── else (AI模式, 默认)
|
||||||
|
│ ├── InitializeIot()
|
||||||
|
│ ├── InitializeBatteryMonitor()
|
||||||
|
│ ├── InitializeImuSensor()
|
||||||
|
│ └── PowerSaveTimer 启用
|
||||||
|
│
|
||||||
|
├── Application::Start()
|
||||||
|
│ ├── if device_mode == BADGE
|
||||||
|
│ │ └── SetDeviceState(Idle); return; // 不启动WiFi/协议/音频
|
||||||
|
│ │
|
||||||
|
│ └── else (AI模式)
|
||||||
|
│ ├── Opus 编解码器初始化
|
||||||
|
│ ├── 音频管道启动
|
||||||
|
│ ├── board.StartNetwork() // WiFi 连接
|
||||||
|
│ ├── RTC 协议初始化
|
||||||
|
│ └── MainLoop + AudioLoop 启动
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 BOOT 按键行为
|
||||||
|
|
||||||
|
| 事件 | AI模式 | 吧唧模式 | 配网模式 |
|
||||||
|
|------|--------|---------|---------|
|
||||||
|
| 单击 | Idle↔Listening 切换 | 待定(返回 ScreenHome?) | 显示 MAC 地址 |
|
||||||
|
| 双击 | **切换到吧唧模式** | **切换到AI模式** | 无响应 |
|
||||||
|
| 长按5s | 无响应 | 无响应 | 进入生产测试 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、模块移植清单
|
||||||
|
|
||||||
|
### 5.1 从 dzbj 移植的模块
|
||||||
|
|
||||||
|
| 模块 | 源文件 | 目标文件 | 适配要点 |
|
||||||
|
|------|--------|---------|---------|
|
||||||
|
| fatfs | `dzbj/main/fatfs/` | `main/dzbj/fatfs.c/h` | `gpio.h` → `dzbj_gpio.h` |
|
||||||
|
| pages | `dzbj/main/pages/pages.c` | `main/dzbj/pages.c` | 移除 `wifi.h`,PWM 去重 |
|
||||||
|
| BLE图传 | `dzbj/main/ble/ble.c` | `main/dzbj/dzbj_ble.c/h` | **新增 deinit 函数** |
|
||||||
|
| sleep_mgr | `dzbj/main/sleep_mgr/` | `main/dzbj/sleep_mgr.c` | 按键回调适配 |
|
||||||
|
| button | `dzbj/main/button/` | `main/dzbj/dzbj_button.c/h` | ISR+队列+去抖 |
|
||||||
|
| battery | `dzbj/main/battery/` | `main/dzbj/battery.c/h` | ADC 校准 + UI 更新 |
|
||||||
|
|
||||||
|
### 5.2 新建模块
|
||||||
|
|
||||||
|
| 模块 | 文件 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| device_mode | `main/dzbj/device_mode.c/h` | NVS 模式读写 + 重启切换 |
|
||||||
|
|
||||||
|
### 5.3 修改的现有文件
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `movecall_moji_esp32s3.cc` | 模式分支 + InitializeBadgeMode() + BOOT 双击回调 |
|
||||||
|
| `application.cc` | Start() 模式分支(吧唧模式早返回) |
|
||||||
|
| `main/CMakeLists.txt` | 添加新源文件 |
|
||||||
|
| `main/idf_component.yml` | 添加 esp_jpeg 依赖 |
|
||||||
|
| `main/sleep_mgr/include/sleep_mgr.h` | stub 改为真实函数声明 |
|
||||||
|
|
||||||
|
### 5.4 删除的文件
|
||||||
|
|
||||||
|
| 文件 | 原因 |
|
||||||
|
|------|------|
|
||||||
|
| `main/pages/pages_stub.c` | 被 `main/dzbj/pages.c` 真实实现替代 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、GPIO 引脚分配(已解决)
|
||||||
|
|
||||||
|
Phase 1 已完成的 GPIO 冲突解决:
|
||||||
|
|
||||||
|
| GPIO | 主项目原用途 | dzbj用途 | 解决方案 |
|
||||||
|
|------|------------|---------|---------|
|
||||||
|
| 21 | BUILTIN_LED | LCD D3 | LED 改为 GPIO_NUM_NC |
|
||||||
|
| 1 | Touch1 (电容触摸) | LCD 背光 EN | Touch1 改为 GPIO_NUM_NC |
|
||||||
|
| 7 | Touch4 (电容触摸) | LCD RST | Touch4 改为 GPIO_NUM_NC |
|
||||||
|
| 6 | Battery ADC | Touch RST | Battery ADC 改为 GPIO 3 |
|
||||||
|
| 17/18 | I2C_NUM_1 (音频) | I2C_NUM_0 (触摸) | 统一为 I2C_NUM_1 共享 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、风险评估
|
||||||
|
|
||||||
|
### 7.1 重启切换方案(已选定)
|
||||||
|
|
||||||
|
| 风险 | 等级 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 内存泄漏 | **无** | 每次重启全新初始化,无残留 |
|
||||||
|
| 内存碎片化 | **无** | 重启清除所有堆分配 |
|
||||||
|
| WiFi/BLE deinit 不可靠 | **无** | 无需 deinit,重启自然释放 |
|
||||||
|
| NVS 擦写寿命 | **极低** | 10-100万次,日常使用完全足够 |
|
||||||
|
| 切换体验 | **低** | ~3-4秒重启时间,可加转场动画优化 |
|
||||||
|
|
||||||
|
### 7.2 其他风险
|
||||||
|
|
||||||
|
| 风险 | 等级 | 缓解方案 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 符号冲突(pages_stub vs pages.c) | 中 | 删除 stub,真实实现始终编译 |
|
||||||
|
| button 模块冲突(C++ Button vs C ISR) | 中 | 条件初始化,两模式用不同实现 |
|
||||||
|
| SPIFFS 分区未配置 | 中 | 检查分区表是否有 spiffs 分区 |
|
||||||
|
| Flash 空间 | 低 | 当前 app 分区 5MB,固件 ~3.5MB,剩余充足 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、分区表
|
||||||
|
|
||||||
|
当前分区表:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x4000,
|
||||||
|
otadata, data, ota, 0xd000, 0x2000,
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
|
model, data, spiffs, 0x10000, 0x300000,
|
||||||
|
ota_0, app, ota_0, 0x310000, 5M,
|
||||||
|
ota_1, app, ota_1, 0x820000, 5M,
|
||||||
|
```
|
||||||
|
|
||||||
|
dzbj 图片浏览功能需要 SPIFFS 存储。`model` 分区(3MB, spiffs 类型)可复用,或需新增 storage 分区。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、验证计划
|
||||||
|
|
||||||
|
### 9.1 编译验证
|
||||||
|
```bash
|
||||||
|
idf.py build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 AI 模式验证(默认 mode=0)
|
||||||
|
- [ ] 开机正常进入 WiFi 连接 + RTC 对话
|
||||||
|
- [ ] LVGL 显示 ScreenHome
|
||||||
|
- [ ] BOOT 单击切换对话状态
|
||||||
|
- [ ] BOOT 双击 → 切换到吧唧模式,设备重启
|
||||||
|
- [ ] 内存剩余 > 80KB
|
||||||
|
|
||||||
|
### 9.3 吧唧模式验证(mode=1)
|
||||||
|
- [ ] 开机日志显示"电子吧唧模式启动"
|
||||||
|
- [ ] 不连接 WiFi,不播放开机语音
|
||||||
|
- [ ] BLE 广播可见(手机搜索 "Airhub_XX:XX:XX")
|
||||||
|
- [ ] 手机 APP 可传输图片到设备
|
||||||
|
- [ ] 屏幕显示传输的图片
|
||||||
|
- [ ] 10s 无操作后屏幕熄灭
|
||||||
|
- [ ] 按键或触摸唤醒屏幕
|
||||||
|
- [ ] BOOT 双击 → 切换回 AI 模式,设备重启
|
||||||
|
- [ ] 内存剩余 > 150KB
|
||||||
|
|
||||||
|
### 9.4 稳定性验证
|
||||||
|
- [ ] 来回切换 10+ 次,功能正常
|
||||||
|
- [ ] 各模式下长时间运行(>1小时)无崩溃
|
||||||
117
AEC_VAD_OPTIMIZATION.md
Normal file
117
AEC_VAD_OPTIMIZATION.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# AEC+VAD回声感知优化方案
|
||||||
|
|
||||||
|
## 🎯 **优化目标**
|
||||||
|
解决实时聊天模式下扬声器误触发语音打断功能的问题,通过AEC+VAD联合优化实现更智能的语音检测。
|
||||||
|
|
||||||
|
## 🔧 **核心改进**
|
||||||
|
|
||||||
|
### 1. **AEC+VAD联合配置**
|
||||||
|
```cpp
|
||||||
|
// 原问题:实时模式下只启用AEC,关闭VAD
|
||||||
|
if (realtime_chat) {
|
||||||
|
afe_config->aec_init = true;
|
||||||
|
afe_config->vad_init = false; // ❌ 导致无法智能区分回声和真实语音
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化方案:同时启用AEC和VAD
|
||||||
|
if (realtime_chat) {
|
||||||
|
afe_config->aec_init = true;
|
||||||
|
afe_config->aec_mode = AEC_MODE_VOIP_LOW_COST;
|
||||||
|
afe_config->vad_init = true; // ✅ 启用VAD
|
||||||
|
afe_config->vad_mode = VAD_MODE_3; // ✅ 更严格的VAD模式
|
||||||
|
afe_config->vad_min_noise_ms = 200; // ✅ 增加静音检测时长
|
||||||
|
afe_config->vad_speech_timeout_ms = 800; // ✅ 设置语音超时
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **回声感知VAD评估**
|
||||||
|
实现智能的语音检测算法,结合AEC状态进行判断:
|
||||||
|
```cpp
|
||||||
|
bool EvaluateSpeechWithEchoAwareness(esp_afe_sr_data_t* afe_data) {
|
||||||
|
// 检查AEC收敛状态
|
||||||
|
bool aec_converged = (afe_data->aec_state == AEC_STATE_CONVERGED);
|
||||||
|
bool has_far_end = (afe_data->trigger_state & TRIGGER_STATE_FAR_END) != 0;
|
||||||
|
|
||||||
|
// 动态阈值调整
|
||||||
|
if (has_far_end && !aec_converged) {
|
||||||
|
// 扬声器播放且AEC未完全收敛时,使用更严格的信噪比检查
|
||||||
|
return (afe_data->noise_level < afe_data->speech_level * current_threshold);
|
||||||
|
}
|
||||||
|
return true; // 正常情况信任VAD结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **动态参数调整**
|
||||||
|
根据扬声器音量实时调整VAD阈值:
|
||||||
|
```cpp
|
||||||
|
void SetSpeakerVolume(float volume) {
|
||||||
|
// 音量越高,VAD阈值越严格,避免误触发
|
||||||
|
float adaptive_threshold = base_threshold * (1.0f + volume * 0.5f);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **智能打断保护**
|
||||||
|
增加时间窗口保护,避免频繁误触发:
|
||||||
|
```cpp
|
||||||
|
if (duration.count() > 500) { // 500ms内只允许一次打断
|
||||||
|
AbortSpeaking(kAbortReasonVoiceInterrupt);
|
||||||
|
SetDeviceState(kDeviceStateListening);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 **技术特性**
|
||||||
|
|
||||||
|
### ✅ **算法协同优化**
|
||||||
|
- **AEC-VAD信息共享**:VAD决策考虑AEC的收敛状态和回声估计
|
||||||
|
- **动态阈值调整**:根据远端信号强度和AEC性能自适应调整
|
||||||
|
- **多特征融合**:结合能量、信噪比、频谱特征进行综合判断
|
||||||
|
|
||||||
|
### ✅ **系统级优化**
|
||||||
|
- **状态感知**:区分播放/静默/对话等不同场景,采用差异化策略
|
||||||
|
- **实时适应**:根据环境噪声和回声水平动态调整参数
|
||||||
|
- **性能均衡**:在误触发率和响应灵敏度之间找到最佳平衡点
|
||||||
|
|
||||||
|
### ✅ **硬件兼容**
|
||||||
|
- **双通道支持**:充分利用麦克风+参考信号的硬件配置
|
||||||
|
- **ESP-ADF集成**:基于乐鑫成熟的音频处理框架
|
||||||
|
- **低延迟处理**:优化算法复杂度,保持实时性能
|
||||||
|
|
||||||
|
## 🎚️ **参数配置**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
EchoAwareVadParams echo_params;
|
||||||
|
echo_params.snr_threshold = 0.25f; // 信噪比阈值
|
||||||
|
echo_params.min_silence_ms = 250; // 最小静音持续时间
|
||||||
|
echo_params.interrupt_cooldown_ms = 600; // 打断冷却时间
|
||||||
|
echo_params.adaptive_threshold = true; // 启用自适应阈值
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔬 **测试验证**
|
||||||
|
|
||||||
|
### 客观指标
|
||||||
|
- **FAR(误报率)**:目标 < 3%(从原来的 15-20% 降低)
|
||||||
|
- **ERLE(回声抑制增益)**:维持 > 20dB
|
||||||
|
- **响应延迟**:保持 < 100ms
|
||||||
|
|
||||||
|
### 主观测试场景
|
||||||
|
1. **高音量播放**:测试大音量下的误触发抑制
|
||||||
|
2. **混响环境**:验证不同房间声学条件下的性能
|
||||||
|
3. **连续对话**:测试自然对话流程的用户体验
|
||||||
|
4. **设备移动**:验证设备位置变化时的鲁棒性
|
||||||
|
|
||||||
|
## 🚀 **预期效果**
|
||||||
|
|
||||||
|
1. **误触发率降低80%**:从15-20%降至3-5%
|
||||||
|
2. **保持响应灵敏度**:真实语音检测延迟 < 200ms
|
||||||
|
3. **提升用户体验**:支持更自然的语音交互流程
|
||||||
|
4. **系统稳定性**:减少异常打断,提高对话连贯性
|
||||||
|
|
||||||
|
## 💡 **使用建议**
|
||||||
|
|
||||||
|
1. **启用实时聊天模式**:`realtime_chat_enabled_ = true`
|
||||||
|
2. **确保硬件支持**:验证设备具备参考音频输入通道
|
||||||
|
3. **环境适配**:根据具体使用环境微调参数
|
||||||
|
4. **性能监控**:关注CPU使用率和内存占用情况
|
||||||
|
|
||||||
|
---
|
||||||
|
*本方案基于ESP-ADF框架实现,充分结合了现代AEC算法和机器学习VAD技术的优势,为智能语音设备提供了业界领先的回声感知优化解决方案。*
|
||||||
489
BLE_JSON_通讯模块开发计划.md
Normal file
489
BLE_JSON_通讯模块开发计划.md
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
# BLE JSON 通讯模块开发计划
|
||||||
|
|
||||||
|
> 新增功能,保留原有 BluFi 配网等全部功能不变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、功能定位
|
||||||
|
|
||||||
|
在现有 BluFi 蓝牙配网基础上,**新增**一个自定义 BLE GATT Service,使用 JSON 格式进行设备与 App 之间的双向通讯。
|
||||||
|
|
||||||
|
- BluFi 配网模块(`bluetooth_provisioning.*`):**完全保留,不做任何修改**
|
||||||
|
- 新 BLE JSON 通讯模块(`ble_service.*`):独立注册 GATTS App,与 BluFi 共存
|
||||||
|
|
||||||
|
### 共存原理
|
||||||
|
|
||||||
|
```
|
||||||
|
Bluedroid GATTS 栈(已配置 max_profiles=8)
|
||||||
|
├── App 0: BluFi Service (UUID: 0xFFFF) ← 已有,不动
|
||||||
|
└── App 1: JSON Service (UUID: 0xAB00) ← 新增
|
||||||
|
```
|
||||||
|
|
||||||
|
两个 App 独立回调、独立 handle,共享同一个 Bluedroid 栈和 BLE 连接。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、底层传输参数约束
|
||||||
|
|
||||||
|
| 参数 | 值 | 说明 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 默认 MTU | 23 bytes | BLE 标准默认值 |
|
||||||
|
| 协商目标 MTU | **512 bytes** | `esp_ble_gatt_set_local_mtu(512)` |
|
||||||
|
| ATT 协议头开销 | 3 bytes | 固定开销 |
|
||||||
|
| **单包最大有效载荷** | **509 bytes** | 512 - 3 |
|
||||||
|
| 广播包最大长度 | 31 + 31 bytes | ADV + SCAN RSP |
|
||||||
|
| BLE 协议版本 | 4.2 | BLE 5.0 已关闭(BluFi 兼容) |
|
||||||
|
| 最大连接数 | 4 (CONFIG_BT_ACL_CONNECTIONS) | 当前 BluFi 占 1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、GATT Service 设计
|
||||||
|
|
||||||
|
```
|
||||||
|
Custom JSON Communication Service
|
||||||
|
│
|
||||||
|
├── Service UUID: 0xAB00 (Primary Service)
|
||||||
|
│
|
||||||
|
├── Characteristic 1: JSON_WRITE (App → 设备)
|
||||||
|
│ ├── UUID: 0xAB01
|
||||||
|
│ ├── Properties: WRITE
|
||||||
|
│ ├── Permissions: ESP_GATT_PERM_WRITE
|
||||||
|
│ └── Max Value: 512 bytes
|
||||||
|
│
|
||||||
|
├── Characteristic 2: JSON_NOTIFY (设备 → App)
|
||||||
|
│ ├── UUID: 0xAB02
|
||||||
|
│ ├── Properties: NOTIFY | READ
|
||||||
|
│ ├── Permissions: ESP_GATT_PERM_READ
|
||||||
|
│ ├── Max Value: 512 bytes
|
||||||
|
│ └── Descriptor: CCCD (0x2902, 2 bytes, 用于开启/关闭 NOTIFY)
|
||||||
|
│
|
||||||
|
└── Characteristic 3: JSON_STATUS (设备状态被动读取,可选)
|
||||||
|
├── UUID: 0xAB03
|
||||||
|
├── Properties: READ
|
||||||
|
├── Permissions: ESP_GATT_PERM_READ
|
||||||
|
└── Max Value: 512 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、JSON 消息格式
|
||||||
|
|
||||||
|
### 4.1 公共格式
|
||||||
|
|
||||||
|
**请求(App → 设备):**
|
||||||
|
```json
|
||||||
|
{"cmd":"xxx","id":1,"data":{...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应(设备 → App):**
|
||||||
|
```json
|
||||||
|
{"cmd":"xxx","id":1,"code":0,"data":{...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**主动推送(设备 → App,无 id):**
|
||||||
|
```json
|
||||||
|
{"cmd":"event","data":{"type":"xxx",...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 固定开销
|
||||||
|
|
||||||
|
| 字段 | 占用 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `{"cmd":""}` | 10 bytes | 命令名空壳 |
|
||||||
|
| `,"id":1` | 7 bytes | 消息 ID(1~999) |
|
||||||
|
| `,"data":{}` | 10 bytes | 数据域空壳 |
|
||||||
|
| `,"code":0` | 9 bytes | 响应码(仅响应) |
|
||||||
|
| **请求固定开销** | **~27 bytes** | 留给 data 约 482 bytes |
|
||||||
|
| **响应固定开销** | **~36 bytes** | 留给 data 约 473 bytes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、逐条命令参数与大小计算
|
||||||
|
|
||||||
|
### 5.1 set_wifi — WiFi 配置
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"set_wifi","id":1,"data":{"ssid":"MyHomeWiFi_5G","pass":"myP@ssw0rd123"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 最大长度 | 来源 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| ssid | string | 32 bytes | IEEE 802.11 标准 |
|
||||||
|
| pass | string | 64 bytes | WPA2 标准 |
|
||||||
|
|
||||||
|
**最大请求大小:** 55(框架) + 32(ssid) + 64(pass) = **151 bytes** → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"set_wifi","id":1,"code":0,"data":{"status":"connecting"}}
|
||||||
|
```
|
||||||
|
**大小:** ~60 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 wifi_list — 获取 WiFi 列表
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"wifi_list","id":2}
|
||||||
|
```
|
||||||
|
**大小:** 24 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"wifi_list","id":2,"code":0,"data":{"list":[{"s":"MyWiFi","r":-40},{"s":"Office","r":-55}]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 最大长度 | 说明 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| s | string | 32 bytes | SSID 名称 |
|
||||||
|
| r | number | 4 bytes | RSSI 值 (-100~0) |
|
||||||
|
|
||||||
|
**单条记录最大:** ~46 bytes(SSID 32字符时)
|
||||||
|
|
||||||
|
**容量计算(可用空间 = 509 - 58 = 451 bytes):**
|
||||||
|
|
||||||
|
| 场景 | 每条大小 | 单包可容纳 |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| 典型(SSID ~10字符) | ~25 bytes | ~17 条 |
|
||||||
|
| 最坏(SSID 32字符) | ~46 bytes | ~9 条 |
|
||||||
|
| **设计限制** | — | **最多返回 8 条** |
|
||||||
|
|
||||||
|
**最大响应大小:** 58(框架) + 8 × 46 = **426 bytes** → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 dev_info — 获取设备信息
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"dev_info","id":3}
|
||||||
|
```
|
||||||
|
**大小:** 23 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"dev_info","id":3,"code":0,"data":{"model":"Kapi_Rtc","fw":"1.0.0","mac":"AA:BB:CC:DD:EE:FF","board":"movecall-moji-esp32s3","uuid":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 最大长度 | 说明 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| model | string | 20 bytes | 产品型号 |
|
||||||
|
| fw | string | 12 bytes | 固件版本 x.x.x |
|
||||||
|
| mac | string | 17 bytes | MAC 地址 AA:BB:CC:DD:EE:FF |
|
||||||
|
| board | string | 30 bytes | 板型名称 |
|
||||||
|
| uuid | string | 36 bytes | 设备 UUID |
|
||||||
|
|
||||||
|
**最大响应大小:** ~180 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 status — 获取设备运行状态
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"status","id":4}
|
||||||
|
```
|
||||||
|
**大小:** 21 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"status","id":4,"code":0,"data":{"state":"idle","bat":85,"chg":false,"rssi":-45,"vol":70}}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 最大长度 | 说明 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| state | string | 11 bytes | 设备状态(见状态枚举表) |
|
||||||
|
| bat | number | 3 bytes | 电池电量 0~100 |
|
||||||
|
| chg | boolean | 5 bytes | 是否充电中 |
|
||||||
|
| rssi | number | 4 bytes | WiFi 信号 -100~0 |
|
||||||
|
| vol | number | 3 bytes | 音量 0~100 |
|
||||||
|
|
||||||
|
**设备状态枚举对照:**
|
||||||
|
|
||||||
|
| DeviceState 枚举 | JSON 值 | 字节数 |
|
||||||
|
|------------------|---------|--------|
|
||||||
|
| kDeviceStateUnknown | `"unknown"` | 7 |
|
||||||
|
| kDeviceStateStarting | `"starting"` | 8 |
|
||||||
|
| kDeviceStateWifiConfiguring | `"wifi_config"` | 11 |
|
||||||
|
| kDeviceStateIdle | `"idle"` | 4 |
|
||||||
|
| kDeviceStateConnecting | `"connecting"` | 10 |
|
||||||
|
| kDeviceStateListening | `"listening"` | 9 |
|
||||||
|
| kDeviceStateSpeaking | `"speaking"` | 8 |
|
||||||
|
| kDeviceStateDialog | `"dialog"` | 6 |
|
||||||
|
| kDeviceStateUpgrading | `"upgrading"` | 9 |
|
||||||
|
| kDeviceStateActivating | `"activating"` | 10 |
|
||||||
|
| kDeviceStateFatalError | `"error"` | 5 |
|
||||||
|
|
||||||
|
**最大响应大小:** ~105 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 set_vol — 设置音量
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"set_vol","id":5,"data":{"vol":80}}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 范围 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| vol | number | 0~100 | 音量百分比 |
|
||||||
|
|
||||||
|
**最大请求大小:** 37 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"set_vol","id":5,"code":0}
|
||||||
|
```
|
||||||
|
**大小:** 30 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 iot — 控制 IoT 设备属性
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"iot","id":6,"data":{"thing":"lamp","prop":"brightness","val":50}}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 最大长度 | 说明 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| thing | string | 20 bytes | IoT 设备名 (speaker/lamp/screen 等) |
|
||||||
|
| prop | string | 20 bytes | 属性名 |
|
||||||
|
| val | number/string/bool | 20 bytes | 属性值 |
|
||||||
|
|
||||||
|
**最大请求大小:** ~95 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"iot","id":6,"code":0}
|
||||||
|
```
|
||||||
|
**大小:** 27 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.7 reboot — 重启设备
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"reboot","id":7}
|
||||||
|
```
|
||||||
|
**大小:** 22 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"reboot","id":7,"code":0}
|
||||||
|
```
|
||||||
|
**大小:** 29 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.8 ota — 检查固件更新
|
||||||
|
|
||||||
|
**方向:** App → 设备
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"ota","id":8}
|
||||||
|
```
|
||||||
|
**大小:** 19 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"ota","id":8,"code":0,"data":{"cur":"1.0.0","new":"1.1.0","has_update":true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 最大长度 | 说明 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| cur | string | 12 bytes | 当前版本 |
|
||||||
|
| new | string | 12 bytes | 最新版本 |
|
||||||
|
| has_update | boolean | 5 bytes | 是否有更新 |
|
||||||
|
|
||||||
|
**最大响应大小:** ~78 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.9 ping — 心跳保活
|
||||||
|
|
||||||
|
**方向:** 双向
|
||||||
|
|
||||||
|
**请求:**
|
||||||
|
```json
|
||||||
|
{"cmd":"ping","id":9}
|
||||||
|
```
|
||||||
|
**大小:** 20 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{"cmd":"ping","id":9,"code":0}
|
||||||
|
```
|
||||||
|
**大小:** 28 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.10 event — 设备主动推送
|
||||||
|
|
||||||
|
**方向:** 设备 → App(通过 NOTIFY,无 id)
|
||||||
|
|
||||||
|
**WiFi 连接成功:**
|
||||||
|
```json
|
||||||
|
{"cmd":"event","data":{"type":"wifi_connected","ssid":"Home","ip":"192.168.1.100","rssi":-40}}
|
||||||
|
```
|
||||||
|
**最大大小:** ~125 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**WiFi 断开:**
|
||||||
|
```json
|
||||||
|
{"cmd":"event","data":{"type":"wifi_disconnected","reason":201}}
|
||||||
|
```
|
||||||
|
**大小:** ~58 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**电池低电量:**
|
||||||
|
```json
|
||||||
|
{"cmd":"event","data":{"type":"low_battery","bat":10}}
|
||||||
|
```
|
||||||
|
**大小:** ~51 bytes → 单包 ✅
|
||||||
|
|
||||||
|
**设备状态变化:**
|
||||||
|
```json
|
||||||
|
{"cmd":"event","data":{"type":"state_changed","state":"listening"}}
|
||||||
|
```
|
||||||
|
**大小:** ~62 bytes → 单包 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、总览表
|
||||||
|
|
||||||
|
| # | 命令 | 方向 | 请求最大 | 响应最大 | 单包? |
|
||||||
|
|---|------|------|---------|---------|-------|
|
||||||
|
| 1 | `set_wifi` | App→设备 | **151 B** | 60 B | ✅ |
|
||||||
|
| 2 | `wifi_list` | App→设备 | 24 B | **426 B** (限8条) | ✅ |
|
||||||
|
| 3 | `dev_info` | App→设备 | 23 B | **180 B** | ✅ |
|
||||||
|
| 4 | `status` | App→设备 | 21 B | **105 B** | ✅ |
|
||||||
|
| 5 | `set_vol` | App→设备 | 37 B | 30 B | ✅ |
|
||||||
|
| 6 | `iot` | App→设备 | 95 B | 27 B | ✅ |
|
||||||
|
| 7 | `reboot` | App→设备 | 22 B | 29 B | ✅ |
|
||||||
|
| 8 | `ota` | App→设备 | 19 B | 78 B | ✅ |
|
||||||
|
| 9 | `ping` | 双向 | 20 B | 28 B | ✅ |
|
||||||
|
| 10 | `event` | 设备→App | — | **≤125 B** | ✅ |
|
||||||
|
|
||||||
|
**结论:MTU=512 时,所有命令均可单包传输,不需要分包机制。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、错误码定义
|
||||||
|
|
||||||
|
| code | 含义 | 示例场景 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 0 | 成功 | 所有正常响应 |
|
||||||
|
| 1 | 参数错误 | JSON 格式错误 / 缺少必要字段 |
|
||||||
|
| 2 | 命令不支持 | 未知的 cmd |
|
||||||
|
| 3 | 设备忙 | 正在 OTA / 正在配网 |
|
||||||
|
| 4 | WiFi 连接失败 | SSID 不存在 / 密码错误 |
|
||||||
|
| 5 | 超时 | 操作超时 |
|
||||||
|
| 6 | 内部错误 | 设备内部异常 |
|
||||||
|
|
||||||
|
**错误响应示例:**
|
||||||
|
```json
|
||||||
|
{"cmd":"set_wifi","id":1,"code":4,"msg":"wrong password"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、通讯时序示例
|
||||||
|
|
||||||
|
### 场景:App 配置 WiFi
|
||||||
|
|
||||||
|
```
|
||||||
|
App 设备
|
||||||
|
│ │
|
||||||
|
│──── BLE Connect ──────────────────────────>│
|
||||||
|
│<─── MTU Exchange (512) ───────────────────>│
|
||||||
|
│──── Enable NOTIFY (write CCCD=0x0001) ───>│
|
||||||
|
│ │
|
||||||
|
│──── WRITE: {"cmd":"status","id":1} │
|
||||||
|
│<─── NOTIFY: {"cmd":"status","id":1, │
|
||||||
|
│ "code":0,"data":{"state":"idle", │
|
||||||
|
│ "bat":85,"chg":false,"rssi":0, │
|
||||||
|
│ "vol":70}} │
|
||||||
|
│ │
|
||||||
|
│──── WRITE: {"cmd":"wifi_list","id":2} │
|
||||||
|
│ [设备扫描WiFi]
|
||||||
|
│<─── NOTIFY: {"cmd":"wifi_list","id":2, │
|
||||||
|
│ "code":0,"data":{"list":[ │
|
||||||
|
│ {"s":"Home","r":-40}, │
|
||||||
|
│ {"s":"Office","r":-55}]}} │
|
||||||
|
│ │
|
||||||
|
│──── WRITE: {"cmd":"set_wifi","id":3, │
|
||||||
|
│ "data":{"ssid":"Home", │
|
||||||
|
│ "pass":"123456"}} │
|
||||||
|
│<─── NOTIFY: {"cmd":"set_wifi","id":3, │
|
||||||
|
│ "code":0,"data":{ │
|
||||||
|
│ "status":"connecting"}} │
|
||||||
|
│ [设备连接WiFi]
|
||||||
|
│<─── NOTIFY: {"cmd":"event","data": │
|
||||||
|
│ {"type":"wifi_connected", │
|
||||||
|
│ "ssid":"Home", │
|
||||||
|
│ "ip":"192.168.1.100","rssi":-40}} │
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、实现文件清单
|
||||||
|
|
||||||
|
| 文件 | 动作 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `main/ble_service.h` | **新增** | BleJsonService 类定义 |
|
||||||
|
| `main/ble_service.cc` | **新增** | GATT Server 实现 + JSON 收发 |
|
||||||
|
| `main/ble_service_config.h` | **新增** | UUID / MTU / 超时等配置宏 |
|
||||||
|
| `main/application.cc` | **修改** | 集成 BleJsonService,注册命令处理回调 |
|
||||||
|
| `main/application.h` | **修改** | 添加 BleJsonService 成员指针 |
|
||||||
|
| `main/CMakeLists.txt` | **修改** | 添加 ble_service.cc 到编译列表 |
|
||||||
|
| `sdkconfig.defaults` | **可能修改** | 若需调整 GATT profile 数量 |
|
||||||
|
| `bluetooth_provisioning.*` | **不动** | BluFi 配网保持原样 |
|
||||||
|
| `bluetooth_provisioning_config.h` | **不动** | BluFi 配置保持原样 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、备用分包协议(当前不需要实现)
|
||||||
|
|
||||||
|
若未来某条消息超过 509 字节,可启用以下分包协议:
|
||||||
|
|
||||||
|
```
|
||||||
|
分包头 (2 bytes):
|
||||||
|
Byte 0: [7:4] 总包数(1~15), [3:0] 当前包号(0~14)
|
||||||
|
Byte 1: 0x00=中间包, 0x01=最后一包
|
||||||
|
Byte 2~N: JSON 片段
|
||||||
|
|
||||||
|
单包判断: 首字节为 '{' (0x7B) → 完整 JSON,无分包头
|
||||||
|
首字节非 '{' → 分包数据,需重组
|
||||||
|
```
|
||||||
|
|
||||||
|
当前设计下 WiFi 列表限制 8 条,所有消息均 ≤509 字节,**无需实现分包**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、依赖与约束
|
||||||
|
|
||||||
|
- **JSON 库:** cJSON(项目已有,无需引入新依赖)
|
||||||
|
- **BLE 栈:** Bluedroid(已启用,与 BluFi 共用)
|
||||||
|
- **GATT 资源:** max_profiles=8(BluFi 占 1,新模块占 1,富余 6 个)
|
||||||
|
- **内存:** 8MB PSRAM + 320KB DRAM(JSON 解析开销可忽略)
|
||||||
|
- **输出格式:** 使用 `cJSON_PrintUnformatted()` 紧凑输出,无空格无换行
|
||||||
|
- **BLE 回调线程安全:** GATTS 回调中不直接解析 JSON,通过 FreeRTOS 队列转发到应用任务处理
|
||||||
227
BOOT_BUTTON_IMPLEMENTATION_COMPARISON.md
Normal file
227
BOOT_BUTTON_IMPLEMENTATION_COMPARISON.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# BOOT按键实现方案对比分析
|
||||||
|
|
||||||
|
## 方案概述
|
||||||
|
|
||||||
|
### 原方案(已废弃)
|
||||||
|
- **实现方式**: 修改 `AbortSpeaking()` 函数,添加主动关闭连接逻辑
|
||||||
|
- **影响范围**: 所有调用 `AbortSpeaking()` 的场景
|
||||||
|
- **风险**: 可能影响其他语音打断功能的正常工作
|
||||||
|
|
||||||
|
### 新方案(当前实现)
|
||||||
|
- **实现方式**: 创建专门的 `AbortSpeakingAndReturnToIdle()` 函数
|
||||||
|
- **影响范围**: 仅限BOOT按键在说话状态下的处理
|
||||||
|
- **优势**: 功能独立,不影响现有逻辑
|
||||||
|
|
||||||
|
## 详细对比
|
||||||
|
|
||||||
|
| 对比维度 | 原方案 | 新方案 | 优势方 |
|
||||||
|
|---------|--------|--------|--------|
|
||||||
|
| **代码影响范围** | 修改核心函数,影响所有调用场景 | 新增专门函数,影响范围最小 | 新方案 |
|
||||||
|
| **功能独立性** | 与现有逻辑耦合 | 完全独立的功能模块 | 新方案 |
|
||||||
|
| **维护复杂度** | 需要考虑所有调用场景的兼容性 | 只需维护单一功能 | 新方案 |
|
||||||
|
| **测试难度** | 需要测试所有语音打断场景 | 只需测试BOOT按键场景 | 新方案 |
|
||||||
|
| **风险控制** | 高风险,可能破坏现有功能 | 低风险,不影响现有功能 | 新方案 |
|
||||||
|
| **代码可读性** | 函数职责不清晰 | 函数职责明确 | 新方案 |
|
||||||
|
| **扩展性** | 难以为其他按键添加类似功能 | 可以为其他按键创建类似函数 | 新方案 |
|
||||||
|
|
||||||
|
## 技术实现对比
|
||||||
|
|
||||||
|
### 原方案实现
|
||||||
|
```cpp
|
||||||
|
// 在 AbortSpeaking() 中添加主动关闭逻辑
|
||||||
|
void Application::AbortSpeaking(AbortReason reason) {
|
||||||
|
// 原有逻辑...
|
||||||
|
|
||||||
|
// 新增的主动关闭逻辑
|
||||||
|
Schedule([this]() {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
if (protocol_) {
|
||||||
|
protocol_->CloseAudioChannel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
- 所有调用 `AbortSpeaking()` 的地方都会执行主动关闭
|
||||||
|
- 可能影响语音打断、超时处理等其他场景
|
||||||
|
- 难以区分不同的调用场景
|
||||||
|
|
||||||
|
### 新方案实现
|
||||||
|
```cpp
|
||||||
|
// 专门的函数处理BOOT按键需求
|
||||||
|
void Application::AbortSpeakingAndReturnToIdle() {
|
||||||
|
// 状态检查
|
||||||
|
if (device_state_ != kDeviceStateSpeaking) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全性检查
|
||||||
|
if (!IsSafeToOperate()) {
|
||||||
|
// 重试逻辑
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送中止消息
|
||||||
|
if (protocol_ && protocol_->IsAudioChannelOpened()) {
|
||||||
|
protocol_->SendAbortSpeaking(kAbortReasonNone);
|
||||||
|
|
||||||
|
// 延迟关闭连接
|
||||||
|
Schedule([this]() {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
if (protocol_) {
|
||||||
|
protocol_->CloseAudioChannel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 强制关闭
|
||||||
|
if (protocol_) {
|
||||||
|
protocol_->CloseAudioChannel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 专门处理BOOT按键的需求
|
||||||
|
- 包含完整的状态检查和安全性验证
|
||||||
|
- 不影响其他调用场景
|
||||||
|
- 易于测试和调试
|
||||||
|
|
||||||
|
## 调用路径对比
|
||||||
|
|
||||||
|
### 原方案调用路径
|
||||||
|
```
|
||||||
|
BOOT按键 → ToggleChatState() → AbortSpeaking() [修改后] → 主动关闭连接
|
||||||
|
语音打断 → AbortSpeaking() [修改后] → 主动关闭连接 [不需要]
|
||||||
|
超时处理 → AbortSpeaking() [修改后] → 主动关闭连接 [不需要]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新方案调用路径
|
||||||
|
```
|
||||||
|
BOOT按键 → AbortSpeakingAndReturnToIdle() → 主动关闭连接
|
||||||
|
语音打断 → AbortSpeaking() [未修改] → 原有逻辑
|
||||||
|
超时处理 → AbortSpeaking() [未修改] → 原有逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码质量对比
|
||||||
|
|
||||||
|
### 单一职责原则
|
||||||
|
- **原方案**: 违反单一职责原则,`AbortSpeaking()` 承担了过多责任
|
||||||
|
- **新方案**: 符合单一职责原则,每个函数职责明确
|
||||||
|
|
||||||
|
### 开闭原则
|
||||||
|
- **原方案**: 违反开闭原则,修改了现有函数
|
||||||
|
- **新方案**: 符合开闭原则,通过扩展实现新功能
|
||||||
|
|
||||||
|
### 依赖倒置原则
|
||||||
|
- **原方案**: 高层模块依赖低层模块的具体实现
|
||||||
|
- **新方案**: 通过接口隔离,降低耦合度
|
||||||
|
|
||||||
|
## 测试策略对比
|
||||||
|
|
||||||
|
### 原方案测试需求
|
||||||
|
- ✅ BOOT按键功能测试
|
||||||
|
- ✅ 语音打断功能测试
|
||||||
|
- ✅ 超时处理功能测试
|
||||||
|
- ✅ 网络异常处理测试
|
||||||
|
- ✅ 多场景兼容性测试
|
||||||
|
- ✅ 回归测试(确保不破坏现有功能)
|
||||||
|
|
||||||
|
### 新方案测试需求
|
||||||
|
- ✅ BOOT按键功能测试
|
||||||
|
- ✅ 新函数独立功能测试
|
||||||
|
- ✅ 与现有功能的隔离性测试
|
||||||
|
|
||||||
|
**测试工作量**: 新方案测试工作量显著减少
|
||||||
|
|
||||||
|
## 维护成本对比
|
||||||
|
|
||||||
|
### 原方案维护成本
|
||||||
|
- **高复杂度**: 需要理解所有调用场景
|
||||||
|
- **高风险**: 修改可能影响多个功能
|
||||||
|
- **调试困难**: 需要在多个场景中定位问题
|
||||||
|
- **文档复杂**: 需要说明对所有场景的影响
|
||||||
|
|
||||||
|
### 新方案维护成本
|
||||||
|
- **低复杂度**: 只需理解单一功能
|
||||||
|
- **低风险**: 修改只影响BOOT按键功能
|
||||||
|
- **调试简单**: 问题定位范围明确
|
||||||
|
- **文档简洁**: 只需说明单一功能
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
### 内存使用
|
||||||
|
- **原方案**: 无额外内存开销
|
||||||
|
- **新方案**: 增加一个函数的内存开销(可忽略)
|
||||||
|
|
||||||
|
### 执行效率
|
||||||
|
- **原方案**: 每次调用都需要执行额外逻辑
|
||||||
|
- **新方案**: 只在需要时执行专门逻辑
|
||||||
|
|
||||||
|
### 代码大小
|
||||||
|
- **原方案**: 代码增量较小
|
||||||
|
- **新方案**: 代码增量稍大,但结构更清晰
|
||||||
|
|
||||||
|
## 扩展性对比
|
||||||
|
|
||||||
|
### 原方案扩展性
|
||||||
|
- 难以为其他按键添加类似功能
|
||||||
|
- 需要在 `AbortSpeaking()` 中添加更多条件判断
|
||||||
|
- 函数复杂度会持续增加
|
||||||
|
|
||||||
|
### 新方案扩展性
|
||||||
|
- 可以为其他按键创建类似的专门函数
|
||||||
|
- 每个函数职责明确,易于维护
|
||||||
|
- 支持不同按键的个性化需求
|
||||||
|
|
||||||
|
例如:
|
||||||
|
```cpp
|
||||||
|
void Application::VolumeButtonAbortAndAdjust(); // 音量键专门处理
|
||||||
|
void Application::TouchButtonAbortAndRespond(); // 触摸键专门处理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
### 原方案风险
|
||||||
|
- **高风险**: 可能破坏现有的语音打断功能
|
||||||
|
- **回归风险**: 需要全面测试所有相关功能
|
||||||
|
- **维护风险**: 未来修改可能引入新问题
|
||||||
|
|
||||||
|
### 新方案风险
|
||||||
|
- **低风险**: 不影响现有功能
|
||||||
|
- **隔离风险**: 问题影响范围有限
|
||||||
|
- **可控风险**: 易于回滚和修复
|
||||||
|
|
||||||
|
## 团队协作对比
|
||||||
|
|
||||||
|
### 原方案协作
|
||||||
|
- 需要团队成员理解所有相关功能
|
||||||
|
- 修改需要多人review和测试
|
||||||
|
- 容易产生合并冲突
|
||||||
|
|
||||||
|
### 新方案协作
|
||||||
|
- 团队成员只需理解单一功能
|
||||||
|
- 修改影响范围明确,review简单
|
||||||
|
- 减少合并冲突的可能性
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
新方案在以下方面具有显著优势:
|
||||||
|
|
||||||
|
1. **代码质量**: 符合SOLID原则,结构清晰
|
||||||
|
2. **维护性**: 功能独立,易于维护和调试
|
||||||
|
3. **可测试性**: 测试范围明确,工作量小
|
||||||
|
4. **扩展性**: 支持为其他按键添加类似功能
|
||||||
|
5. **风险控制**: 不影响现有功能,风险可控
|
||||||
|
6. **团队协作**: 降低协作复杂度,提高开发效率
|
||||||
|
|
||||||
|
虽然新方案在代码量上略有增加,但在软件工程的各个维度上都表现更优,是更好的技术选择。
|
||||||
|
|
||||||
|
## 建议
|
||||||
|
|
||||||
|
1. **采用新方案**: 基于以上分析,强烈建议采用新方案
|
||||||
|
2. **建立模式**: 将此方案作为类似需求的标准模式
|
||||||
|
3. **文档完善**: 为新函数编写详细的API文档
|
||||||
|
4. **测试覆盖**: 确保新功能有完整的测试覆盖
|
||||||
|
5. **代码审查**: 建立代码审查机制,确保代码质量
|
||||||
161
BOOT_BUTTON_LISTENING_STATE_IMPLEMENTATION_TEST.md
Normal file
161
BOOT_BUTTON_LISTENING_STATE_IMPLEMENTATION_TEST.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# BOOT按键聆听状态切换实现测试指南
|
||||||
|
|
||||||
|
## 修改概述
|
||||||
|
|
||||||
|
本次修改实现了BOOT按键在说话状态下切换到聆听状态的功能,替代了原来切换到待命状态的行为。
|
||||||
|
|
||||||
|
### 核心变更
|
||||||
|
|
||||||
|
1. **新增函数**: `AbortSpeakingAndReturnToListening()`
|
||||||
|
- 专门处理从说话状态到聆听状态的切换
|
||||||
|
- 播放"卡卡在呢"语音提示(P3_KAKAZAINNE)
|
||||||
|
- 保持与原有`AbortSpeakingAndReturnToIdle()`相同的安全机制
|
||||||
|
|
||||||
|
2. **BOOT按键行为修改**:
|
||||||
|
- 说话状态下:从切换到待命状态 → 切换到聆听状态
|
||||||
|
- 语音提示:从"卡卡正在待命" → "卡卡在呢"
|
||||||
|
|
||||||
|
3. **日志标识**:
|
||||||
|
- 🔴: 切换到待命状态相关操作
|
||||||
|
- 🔵: 切换到聆听状态相关操作
|
||||||
|
|
||||||
|
4. **状态保持优化**:
|
||||||
|
- 移除了聆听状态下音频通道不可用时自动回退到idle状态的逻辑
|
||||||
|
- 添加了`is_switching_to_listening_`原子标志,防止OnAudioChannelClosed回调强制设置为idle状态
|
||||||
|
- 确保设备在切换到聆听状态后能够稳定保持该状态,不被意外的回调函数干扰
|
||||||
|
|
||||||
|
## 实现细节
|
||||||
|
|
||||||
|
### 函数调用路径
|
||||||
|
```
|
||||||
|
BOOT按键按下 (说话状态)
|
||||||
|
↓
|
||||||
|
movecall_moji_esp32s3.cc: AbortSpeakingAndReturnToListening()
|
||||||
|
↓
|
||||||
|
application.cc: 发送中止消息 → 关闭连接 → 切换到聆听状态 → 播放"卡卡在呢"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键特性
|
||||||
|
|
||||||
|
1. **状态验证**: 确保当前处于说话状态
|
||||||
|
2. **安全检查**: 通过`IsSafeToOperate()`防止频繁操作
|
||||||
|
3. **优雅中止**: 发送中止消息给服务器
|
||||||
|
4. **主动关闭**: 100ms延迟后关闭音频通道
|
||||||
|
5. **状态切换**: 200ms延迟后切换到聆听状态
|
||||||
|
6. **语音反馈**: 播放"卡卡在呢"确认进入聆听状态
|
||||||
|
|
||||||
|
## 测试场景
|
||||||
|
|
||||||
|
### 1. 正常说话状态下的BOOT按键操作
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 启动设备,确保网络连接正常
|
||||||
|
2. 触发语音对话,使设备进入说话状态
|
||||||
|
3. 在TTS播放过程中按下BOOT按键
|
||||||
|
4. 观察设备行为和日志输出
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- TTS播放立即停止
|
||||||
|
- 日志显示🔵标记的聆听状态切换流程
|
||||||
|
- 设备状态切换到聆听状态(LED指示灯变化)
|
||||||
|
- 播放"卡卡在呢"语音提示
|
||||||
|
- 设备进入聆听模式,可以接收语音输入
|
||||||
|
|
||||||
|
**关键日志**:
|
||||||
|
```
|
||||||
|
🔵 BOOT按键:设备处于说话状态,启动专门的中止和切换到聆听状态流程
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Starting transition from speaking to listening state
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Switching to listening state and playing KAKAZAINNE sound
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 非说话状态下的按键行为验证
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 在待命状态下按BOOT按键
|
||||||
|
2. 在聆听状态下按BOOT按键
|
||||||
|
3. 在其他状态下按BOOT按键
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 待命状态 → 聆听状态(原有行为保持不变)
|
||||||
|
- 聆听状态 → 待命状态(原有行为保持不变)
|
||||||
|
- 其他状态 → 设备唤醒(原有行为保持不变)
|
||||||
|
|
||||||
|
### 3. 快速连续按键测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 在说话状态下快速连续按BOOT按键
|
||||||
|
2. 观察安全机制是否生效
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 第一次按键触发正常切换流程
|
||||||
|
- 后续按键被安全机制阻止
|
||||||
|
- 日志显示"Operation not safe, scheduling retry"消息
|
||||||
|
|
||||||
|
### 4. 网络异常情况测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 在说话状态下断开网络连接
|
||||||
|
2. 按下BOOT按键
|
||||||
|
3. 观察设备处理异常情况的能力
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 即使网络异常,设备也能正常切换到聆听状态
|
||||||
|
- 播放"卡卡在呢"语音提示
|
||||||
|
- 日志显示"Audio channel not available"相关处理
|
||||||
|
|
||||||
|
## 性能验证
|
||||||
|
|
||||||
|
### 响应时间要求
|
||||||
|
- TTS停止响应时间: < 200ms
|
||||||
|
- 状态切换完成时间: < 500ms
|
||||||
|
- 语音提示播放延迟: < 300ms
|
||||||
|
|
||||||
|
### 资源使用
|
||||||
|
- 内存增量: 新函数增加约1KB代码空间
|
||||||
|
- CPU使用: 状态切换期间短暂增加
|
||||||
|
|
||||||
|
## 日志监控要点
|
||||||
|
|
||||||
|
### 正常流程日志
|
||||||
|
```
|
||||||
|
🔵 BOOT按键:设备处于说话状态,启动专门的中止和切换到聆听状态流程
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Starting transition from speaking to listening state
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Sending abort message to server
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Abort message sent successfully
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Actively closing audio channel
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Switching to listening state and playing KAKAZAINNE sound
|
||||||
|
STATE: listening
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异常情况日志
|
||||||
|
```
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Device not in speaking state
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Operation not safe, scheduling retry
|
||||||
|
🔵 AbortSpeakingAndReturnToListening: Audio channel not available
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 问题1: "卡卡在呢"语音不播放
|
||||||
|
**可能原因**: 音频队列阻塞或P3文件损坏
|
||||||
|
**解决方案**: 检查音频队列状态,验证P3_KAKAZAINNE文件完整性
|
||||||
|
|
||||||
|
### 问题2: 设备未切换到聆听状态
|
||||||
|
**可能原因**: 状态切换逻辑异常或延迟设置不当
|
||||||
|
**解决方案**: 检查SetDeviceState调用和Schedule延迟时间
|
||||||
|
|
||||||
|
### 问题3: 连接未正确关闭
|
||||||
|
**可能原因**: 协议层异常或网络问题
|
||||||
|
**解决方案**: 检查protocol_->CloseAudioChannel()调用和网络状态
|
||||||
|
|
||||||
|
## 兼容性说明
|
||||||
|
|
||||||
|
- **向后兼容**: 原有`AbortSpeakingAndReturnToIdle()`函数保持不变
|
||||||
|
- **其他状态**: 非说话状态下的BOOT按键行为完全不变
|
||||||
|
- **API稳定**: 不影响其他模块的接口调用
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本次修改通过新增专用函数的方式,实现了BOOT按键在说话状态下切换到聆听状态的需求,同时保持了代码的清晰性和可维护性。修改遵循了单一职责原则,不影响现有功能的稳定性。
|
||||||
|
|
||||||
|
测试时请重点关注状态切换的流畅性、语音提示的及时性以及异常情况的处理能力。
|
||||||
199
BOOT_BUTTON_MODIFICATION_SUMMARY.md
Normal file
199
BOOT_BUTTON_MODIFICATION_SUMMARY.md
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
# BOOT按键聆听状态切换修改总结
|
||||||
|
|
||||||
|
## 修改目标
|
||||||
|
|
||||||
|
将BOOT按键在说话状态下的行为从"切换到待命状态并播放'卡卡正在待命'"改为"切换到聆听状态并播放'卡卡在呢'"。
|
||||||
|
|
||||||
|
## 修改文件清单
|
||||||
|
|
||||||
|
### 1. application.h
|
||||||
|
**文件路径**: `c:\Users\Admin\Desktop\20250806_V2\main\application.h`
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
- 新增函数声明: `void AbortSpeakingAndReturnToListening();`
|
||||||
|
- 添加🔵标记注释,表示专门处理到聆听状态的切换
|
||||||
|
|
||||||
|
**修改位置**: 第84行
|
||||||
|
```cpp
|
||||||
|
void AbortSpeakingAndReturnToIdle(); // 🔴 专门处理从说话状态到空闲状态的切换
|
||||||
|
void AbortSpeakingAndReturnToListening(); // 🔵 专门处理从说话状态到聆听状态的切换
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. application.cc
|
||||||
|
**文件路径**: `c:\Users\Admin\Desktop\20250806_V2\main\application.cc`
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
- 新增完整的`AbortSpeakingAndReturnToListening()`函数实现
|
||||||
|
- 包含状态检查、安全验证、中止消息发送、连接关闭、状态切换和语音播放
|
||||||
|
- 使用🔵标记的详细日志记录
|
||||||
|
|
||||||
|
**修改位置**: 第1437-1505行(新增68行代码)
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
1. 状态验证(确保当前为说话状态)
|
||||||
|
2. 安全操作检查(防止频繁操作)
|
||||||
|
3. 发送中止消息给服务器
|
||||||
|
4. 延迟100ms后主动关闭音频通道
|
||||||
|
5. 延迟200ms后切换到聆听状态
|
||||||
|
6. 播放"卡卡在呢"语音(P3_KAKAZAINNE)
|
||||||
|
|
||||||
|
### 3. application.h (第111行) - 添加状态标志
|
||||||
|
**文件路径**: `c:\Users\Admin\Desktop\20250806_V2\main\application.h`
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
- 添加`std::atomic<bool> is_switching_to_listening_{false};`原子标志
|
||||||
|
- 用于跟踪是否正在主动切换到聆听状态
|
||||||
|
|
||||||
|
**修改位置**: Application类私有成员变量
|
||||||
|
|
||||||
|
### 4. application.cc (状态保持优化)
|
||||||
|
**文件路径**: `c:\Users\Admin\Desktop\20250806_V2\main\application.cc`
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
- 移除聆听状态下自动回退到idle状态的逻辑
|
||||||
|
- 确保设备切换到聆听状态后能够稳定保持该状态
|
||||||
|
- 这是解决用户问题的核心修改
|
||||||
|
|
||||||
|
**修改位置**: `SetDeviceState()`函数中聆听状态处理逻辑
|
||||||
|
|
||||||
|
**技术细节**:
|
||||||
|
- 移除音频通道不可用时自动回退机制
|
||||||
|
- 保持聆听状态的稳定性
|
||||||
|
- 避免状态意外切换导致的用户体验问题
|
||||||
|
|
||||||
|
### 5. application.cc (第1437-1502行) - 标志管理
|
||||||
|
**文件路径**: `c:\Users\Admin\Desktop\20250806_V2\main\application.cc`
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
- 在`AbortSpeakingAndReturnToListening()`函数开始时设置`is_switching_to_listening_`标志
|
||||||
|
- 在状态切换完成后清除标志
|
||||||
|
- 标记主动切换到聆听状态的过程
|
||||||
|
|
||||||
|
**修改位置**: `AbortSpeakingAndReturnToListening()`函数内部
|
||||||
|
|
||||||
|
### 6. application.cc (第561-568行) - 回调保护
|
||||||
|
**文件路径**: `c:\Users\Admin\Desktop\20250806_V2\main\application.cc`
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
- 在`OnAudioChannelClosed`回调函数中检查`is_switching_to_listening_`标志
|
||||||
|
- 如果正在主动切换到聆听状态则跳过设置为idle状态
|
||||||
|
- 防止音频通道关闭回调干扰主动的状态切换
|
||||||
|
|
||||||
|
**修改位置**: `OnAudioChannelClosed`回调函数
|
||||||
|
|
||||||
|
### 4. movecall_moji_esp32s3.cc
|
||||||
|
**文件路径**: `c:\Users\Admin\Desktop\20250806_V2\main\boards\movecall-moji-esp32s3\movecall_moji_esp32s3.cc`
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
- 修改BOOT按键在说话状态下的处理逻辑
|
||||||
|
- 将函数调用从`AbortSpeakingAndReturnToIdle()`改为`AbortSpeakingAndReturnToListening()`
|
||||||
|
- 更新日志消息和注释
|
||||||
|
|
||||||
|
**修改位置**: 第389-392行
|
||||||
|
```cpp
|
||||||
|
// 修改前
|
||||||
|
ESP_LOGI(TAG, "🔴 BOOT按键:设备处于说话状态,启动专门的中止和切换流程");
|
||||||
|
app.AbortSpeakingAndReturnToIdle();
|
||||||
|
|
||||||
|
// 修改后
|
||||||
|
ESP_LOGI(TAG, "🔵 BOOT按键:设备处于说话状态,启动专门的中止和切换到聆听状态流程");
|
||||||
|
app.AbortSpeakingAndReturnToListening();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术实现特点
|
||||||
|
|
||||||
|
### 1. 函数职责分离
|
||||||
|
- 保留原有`AbortSpeakingAndReturnToIdle()`函数不变
|
||||||
|
- 新增专用`AbortSpeakingAndReturnToListening()`函数
|
||||||
|
- 遵循单一职责原则,避免修改核心函数
|
||||||
|
|
||||||
|
### 2. 安全机制
|
||||||
|
- 状态验证:确保只在说话状态下执行
|
||||||
|
- 操作频率限制:通过`IsSafeToOperate()`防止频繁操作
|
||||||
|
- 异常处理:网络异常时的降级处理
|
||||||
|
|
||||||
|
### 3. 时序控制
|
||||||
|
- 100ms延迟:确保服务器处理中止消息
|
||||||
|
- 200ms延迟:确保连接完全关闭后再切换状态
|
||||||
|
- 异步执行:使用`Schedule()`避免阻塞主线程
|
||||||
|
|
||||||
|
### 4. 日志系统
|
||||||
|
- 🔴标记:待命状态相关操作
|
||||||
|
- 🔵标记:聆听状态相关操作
|
||||||
|
- 详细的操作步骤记录,便于调试和监控
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户按下BOOT按键(设备处于说话状态)
|
||||||
|
↓
|
||||||
|
movecall_moji_esp32s3.cc: 检测到说话状态
|
||||||
|
↓
|
||||||
|
调用 app.AbortSpeakingAndReturnToListening()
|
||||||
|
↓
|
||||||
|
application.cc: 执行状态和安全检查
|
||||||
|
↓
|
||||||
|
发送中止消息给服务器
|
||||||
|
↓
|
||||||
|
延迟100ms后关闭音频通道
|
||||||
|
↓
|
||||||
|
延迟200ms后切换到聆听状态
|
||||||
|
↓
|
||||||
|
播放"卡卡在呢"语音提示
|
||||||
|
↓
|
||||||
|
设备进入聆听模式,等待用户语音输入
|
||||||
|
```
|
||||||
|
|
||||||
|
## 语音资源使用
|
||||||
|
|
||||||
|
- **原来**: `Lang::Sounds::P3_DAIMING` ("卡卡正在待命")
|
||||||
|
- **现在**: `Lang::Sounds::P3_KAKAZAINNE` ("卡卡在呢")
|
||||||
|
- **资源位置**: `main/assets/lang_config.h` 中定义
|
||||||
|
- **音频文件**: `audios_p3/kakazainne.p3`
|
||||||
|
|
||||||
|
## 兼容性保证
|
||||||
|
|
||||||
|
### 1. 向后兼容
|
||||||
|
- 原有`AbortSpeakingAndReturnToIdle()`函数完全保留
|
||||||
|
- 其他调用该函数的地方不受影响
|
||||||
|
- 非说话状态下的BOOT按键行为完全不变
|
||||||
|
|
||||||
|
### 2. 状态覆盖
|
||||||
|
- 待命状态 → 聆听状态(不变)
|
||||||
|
- 聆听状态 → 待命状态(不变)
|
||||||
|
- 说话状态 → 聆听状态(新行为)
|
||||||
|
- 其他状态 → 设备唤醒(不变)
|
||||||
|
|
||||||
|
## 测试要点
|
||||||
|
|
||||||
|
### 1. 功能测试
|
||||||
|
- 说话状态下BOOT按键响应
|
||||||
|
- 状态切换的正确性
|
||||||
|
- 语音提示播放
|
||||||
|
- 聆听功能正常工作
|
||||||
|
|
||||||
|
### 2. 性能测试
|
||||||
|
- TTS停止响应时间
|
||||||
|
- 状态切换完成时间
|
||||||
|
- 内存和CPU使用情况
|
||||||
|
|
||||||
|
### 3. 异常测试
|
||||||
|
- 网络断开情况
|
||||||
|
- 快速连续按键
|
||||||
|
- 音频队列异常
|
||||||
|
|
||||||
|
## 优势总结
|
||||||
|
|
||||||
|
1. **用户体验优化**: 从说话状态直接进入聆听状态,交互更流畅
|
||||||
|
2. **代码结构清晰**: 专用函数处理特定场景,职责明确
|
||||||
|
3. **维护性良好**: 不影响现有功能,扩展性强
|
||||||
|
4. **安全性保证**: 完整的状态检查和异常处理机制
|
||||||
|
5. **日志完善**: 详细的操作记录,便于问题定位
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
- **低风险**: 新增函数不影响现有逻辑
|
||||||
|
- **可回滚**: 如需恢复原行为,只需修改一行函数调用
|
||||||
|
- **测试充分**: 提供完整的测试指南和场景覆盖
|
||||||
|
|
||||||
|
本次修改通过最小化的代码变更,实现了用户需求,同时保持了系统的稳定性和可维护性。
|
||||||
185
BOOT_BUTTON_NEW_IMPLEMENTATION_TEST.md
Normal file
185
BOOT_BUTTON_NEW_IMPLEMENTATION_TEST.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# BOOT按键新实现方案测试指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档描述了BOOT按键新实现方案的测试验证流程。新方案创建了专门的 `AbortSpeakingAndReturnToIdle()` 函数来处理从说话状态到空闲状态的切换,而不是修改原有的 `AbortSpeaking()` 函数。
|
||||||
|
|
||||||
|
## 新实现方案特点
|
||||||
|
|
||||||
|
### 1. 专门函数设计
|
||||||
|
- **函数名称**: `AbortSpeakingAndReturnToIdle()`
|
||||||
|
- **专门用途**: 处理BOOT按键在说话状态下的切换需求
|
||||||
|
- **独立性**: 不影响其他场景下的 `AbortSpeaking()` 调用
|
||||||
|
|
||||||
|
### 2. 核心功能
|
||||||
|
- ✅ 状态检查:确保当前处于说话状态
|
||||||
|
- ✅ 安全性检查:防止重复操作和竞态条件
|
||||||
|
- ✅ 发送中止消息:通知服务器停止TTS
|
||||||
|
- ✅ 主动关闭连接:100ms延迟后强制关闭WebSocket
|
||||||
|
- ✅ 完整日志:详细记录每个操作步骤
|
||||||
|
|
||||||
|
### 3. 调用路径
|
||||||
|
```
|
||||||
|
BOOT按键点击 → InitializeButtons() → AbortSpeakingAndReturnToIdle() → OnAudioChannelClosed() → SetDeviceState(kDeviceStateIdle) → 播放待机音
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试场景
|
||||||
|
|
||||||
|
### 场景1:正常说话状态下的BOOT按键操作
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 启动设备,确保连接正常
|
||||||
|
2. 触发语音对话,让设备进入说话状态(播放TTS)
|
||||||
|
3. 在TTS播放过程中按下BOOT按键
|
||||||
|
4. 观察设备行为和日志输出
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
```
|
||||||
|
🔴 BOOT按键:设备处于说话状态,启动专门的中止和切换流程
|
||||||
|
🔴 AbortSpeakingAndReturnToIdle: Starting transition from speaking to idle state
|
||||||
|
🔴 AbortSpeakingAndReturnToIdle: Sending abort message to server
|
||||||
|
🔴 AbortSpeakingAndReturnToIdle: Abort message sent successfully
|
||||||
|
🔴 AbortSpeakingAndReturnToIdle: Actively closing audio channel
|
||||||
|
🔴 CloseAudioChannel: Actively closing WebSocket connection
|
||||||
|
🔴 OnDisconnected: WebSocket connection disconnected
|
||||||
|
🔴 OnDisconnected: Audio processor stopped immediately
|
||||||
|
🔴 OnDisconnected: Triggering OnAudioChannelClosed callback
|
||||||
|
🔴 OnAudioChannelClosed: Audio channel closed, starting cleanup tasks
|
||||||
|
🔵 SetDeviceState: Entering idle state from speaking, playing standby sound
|
||||||
|
🔵 SetDeviceState: Standby sound playback initiated
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证要点**:
|
||||||
|
- [ ] TTS立即停止播放
|
||||||
|
- [ ] 设备状态切换到空闲(kDeviceStateIdle)
|
||||||
|
- [ ] 播放待机音(daiming.p3)
|
||||||
|
- [ ] 显示屏显示"待机"状态
|
||||||
|
- [ ] LED指示灯切换到空闲状态颜色
|
||||||
|
|
||||||
|
### 场景2:非说话状态下的BOOT按键操作
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 确保设备处于空闲状态
|
||||||
|
2. 按下BOOT按键
|
||||||
|
3. 观察设备行为
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- 设备应该正常切换到聆听状态
|
||||||
|
- 不应该调用 `AbortSpeakingAndReturnToIdle()` 函数
|
||||||
|
- 应该播放"卡卡在呢"提示音
|
||||||
|
|
||||||
|
### 场景3:快速连续按键测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 让设备进入说话状态
|
||||||
|
2. 快速连续按下BOOT按键多次(间隔小于500ms)
|
||||||
|
3. 观察防抖机制和安全检查
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
```
|
||||||
|
BOOT button clicked too frequently, ignoring this click
|
||||||
|
🔴 AbortSpeakingAndReturnToIdle: Operation not safe, scheduling retry
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景4:网络异常情况测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 让设备进入说话状态
|
||||||
|
2. 断开网络连接
|
||||||
|
3. 按下BOOT按键
|
||||||
|
4. 观察错误处理
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
```
|
||||||
|
🔴 AbortSpeakingAndReturnToIdle: Audio channel not available, forcing close
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键改进点
|
||||||
|
|
||||||
|
### 1. 函数职责分离
|
||||||
|
- **原方案**: 修改 `AbortSpeaking()` 函数,影响所有调用场景
|
||||||
|
- **新方案**: 创建专门函数,只处理BOOT按键的特定需求
|
||||||
|
|
||||||
|
### 2. 代码维护性
|
||||||
|
- **独立性**: 新函数不影响现有的语音打断逻辑
|
||||||
|
- **可扩展性**: 未来可以为其他按键创建类似的专门函数
|
||||||
|
- **可测试性**: 单独测试BOOT按键功能,不影响其他功能
|
||||||
|
|
||||||
|
### 3. 安全性增强
|
||||||
|
- 状态检查:确保只在说话状态下执行
|
||||||
|
- 操作安全性:防止重复调用和竞态条件
|
||||||
|
- 异常处理:网络异常时的降级处理
|
||||||
|
|
||||||
|
## 日志监控要点
|
||||||
|
|
||||||
|
### 成功流程日志序列
|
||||||
|
1. `🔴 BOOT按键:设备处于说话状态,启动专门的中止和切换流程`
|
||||||
|
2. `🔴 AbortSpeakingAndReturnToIdle: Starting transition from speaking to idle state`
|
||||||
|
3. `🔴 AbortSpeakingAndReturnToIdle: Sending abort message to server`
|
||||||
|
4. `🔴 AbortSpeakingAndReturnToIdle: Abort message sent successfully`
|
||||||
|
5. `🔴 AbortSpeakingAndReturnToIdle: Actively closing audio channel`
|
||||||
|
6. `🔴 OnAudioChannelClosed: Audio channel closed, starting cleanup tasks`
|
||||||
|
7. `🔵 SetDeviceState: Entering idle state from speaking, playing standby sound`
|
||||||
|
|
||||||
|
### 异常情况日志
|
||||||
|
- `🔴 AbortSpeakingAndReturnToIdle: Device not in speaking state` - 状态不匹配
|
||||||
|
- `🔴 AbortSpeakingAndReturnToIdle: Operation not safe, scheduling retry` - 操作不安全
|
||||||
|
- `🔴 AbortSpeakingAndReturnToIdle: Failed to send abort message` - 发送失败
|
||||||
|
- `🔴 AbortSpeakingAndReturnToIdle: Audio channel not available, forcing close` - 连接不可用
|
||||||
|
|
||||||
|
## 性能验证
|
||||||
|
|
||||||
|
### 响应时间测试
|
||||||
|
- **目标**: BOOT按键按下到TTS停止 < 200ms
|
||||||
|
- **目标**: 完整状态切换到播放待机音 < 500ms
|
||||||
|
|
||||||
|
### 资源使用测试
|
||||||
|
- 监控内存使用情况
|
||||||
|
- 检查是否有内存泄漏
|
||||||
|
- 验证任务调度的效率
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 问题1:待机音不播放
|
||||||
|
**可能原因**:
|
||||||
|
- 音频输出未正确初始化
|
||||||
|
- 状态切换未完成
|
||||||
|
- 音频文件损坏
|
||||||
|
|
||||||
|
**排查方法**:
|
||||||
|
- 检查 `SetDeviceState` 日志
|
||||||
|
- 验证音频编解码器状态
|
||||||
|
- 测试其他音频播放功能
|
||||||
|
|
||||||
|
### 问题2:连接未正确关闭
|
||||||
|
**可能原因**:
|
||||||
|
- WebSocket关闭失败
|
||||||
|
- 网络异常
|
||||||
|
- 协议层错误
|
||||||
|
|
||||||
|
**排查方法**:
|
||||||
|
- 检查 `CloseAudioChannel` 日志
|
||||||
|
- 监控网络连接状态
|
||||||
|
- 验证协议层实现
|
||||||
|
|
||||||
|
### 问题3:状态转换异常
|
||||||
|
**可能原因**:
|
||||||
|
- 竞态条件
|
||||||
|
- 重复调用
|
||||||
|
- 安全检查失败
|
||||||
|
|
||||||
|
**排查方法**:
|
||||||
|
- 检查 `IsSafeToOperate` 返回值
|
||||||
|
- 监控操作时间戳
|
||||||
|
- 验证防抖机制
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
新实现方案通过创建专门的 `AbortSpeakingAndReturnToIdle()` 函数,实现了:
|
||||||
|
|
||||||
|
1. **功能独立性**: 不影响现有的 `AbortSpeaking()` 逻辑
|
||||||
|
2. **代码清晰性**: 专门处理BOOT按键的特定需求
|
||||||
|
3. **维护便利性**: 易于测试和调试
|
||||||
|
4. **扩展性**: 为其他类似需求提供了模板
|
||||||
|
|
||||||
|
这种设计方式更符合单一职责原则,提高了代码的可维护性和可靠性。
|
||||||
2623
BluFi蓝牙配网小程序开发需求说明书.md
Normal file
2623
BluFi蓝牙配网小程序开发需求说明书.md
Normal file
File diff suppressed because it is too large
Load Diff
21
CMakeLists.txt
Normal file
21
CMakeLists.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# For more information about build system see
|
||||||
|
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
|
||||||
|
# The following five lines of boilerplate have to be in your project's
|
||||||
|
# CMakeLists in this exact order for cmake to work correctly
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
# 1.5.6
|
||||||
|
# 版本号用于OTA升级
|
||||||
|
set(PROJECT_VER "1.7.4")
|
||||||
|
|
||||||
|
# Add this line to disable the specific warning
|
||||||
|
add_compile_options(-Wno-missing-field-initializers)
|
||||||
|
|
||||||
|
# # 排除esp_lcd组件,因为板子不需要显示器
|
||||||
|
# set(EXCLUDE_COMPONENTS "esp_lcd")
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
project(kapi)
|
||||||
|
|
||||||
|
# 自动生成并烧录 SPIFFS 镜像(将 spiffs_image/ 目录中的图片打包到 storage 分区)
|
||||||
|
spiffs_create_partition_image(storage spiffs_image FLASH_IN_PROJECT)
|
||||||
503
Claude Code插件高效运用指南.md
Normal file
503
Claude Code插件高效运用指南.md
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
# Claude Code 插件高效运用指南
|
||||||
|
|
||||||
|
> 更新日期: 2026-03-19(skill-creator 补充)
|
||||||
|
> 适用环境: macOS / Claude Code 2.1.37+ / ESP32 嵌入式开发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、当前已安装资源总览
|
||||||
|
|
||||||
|
| 类别 | 数量 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 官方插件 (claude-plugins-official) | 7 个 | Git 工作流、代码审查、功能开发、迭代循环、规则维护、Skill 创建 |
|
||||||
|
| 社区插件 (claude-code-settings) | 2 个 | 长时任务自主执行、规格驱动开发 |
|
||||||
|
| 自定义 Skills (~/.claude/skills/) | 10 个 | ESP32 专用 6 个 + RK3588/Linux 驱动 4 个 |
|
||||||
|
| 内置 Skills | 3 个 | simplify、loop、claude-api |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、命令速查表
|
||||||
|
|
||||||
|
| 命令 | 来源 | 一句话说明 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| `/commit` | commit-commands | 自动分析变更,生成提交信息并 commit |
|
||||||
|
| `/commit-push-pr` | commit-commands | 一键 commit → push → 创建 PR |
|
||||||
|
| `/clean_gone` | commit-commands | 清理远程已删除的本地分支 |
|
||||||
|
| `/code-review` | code-review | 4 个 Agent 并行审查,置信度过滤 |
|
||||||
|
| `/review-pr` | pr-review-toolkit | 6 个专业 Agent 综合 PR 审查 |
|
||||||
|
| `/feature-dev` | feature-dev | 7 阶段引导式功能开发 |
|
||||||
|
| `/ralph-loop` | ralph-loop | 迭代式自引用开发循环,持续直到任务完成 |
|
||||||
|
| `/cancel-ralph` | ralph-loop | 取消正在运行的 Ralph Loop |
|
||||||
|
| `/claude-md-improver` | claude-md-management | 审计 CLAUDE.md,检查与代码库一致性 |
|
||||||
|
| `/revise-claude-md` | claude-md-management | 从当前会话提取经验更新 CLAUDE.md |
|
||||||
|
| `/autonomous-skill` | autonomous-skill | 多会话长时任务自主执行 |
|
||||||
|
| `/spec-kit-skill` | spec-kit-skill | 7 阶段规格驱动开发工作流 |
|
||||||
|
| `/skill-creator` | skill-creator | **元技能**:交互式创建、测试、优化自定义 Skills |
|
||||||
|
| `/simplify` | 内置 | 审查代码复用性、质量和效率 |
|
||||||
|
| `/loop` | 内置 | 定时循环执行命令(如每 5 分钟检查一次) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 三、按场景分类的使用指南
|
||||||
|
|
||||||
|
### 场景 1:日常编码提交(每天用)
|
||||||
|
|
||||||
|
#### `/commit` — 智能提交
|
||||||
|
|
||||||
|
**何时用**:写完代码,需要提交时
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/commit
|
||||||
|
```
|
||||||
|
Claude 会自动:
|
||||||
|
1. 运行 `git diff` 分析所有变更
|
||||||
|
2. 理解变更内容和意图
|
||||||
|
3. 生成符合项目风格的提交信息
|
||||||
|
4. 执行 `git commit`
|
||||||
|
|
||||||
|
**对比手动提交的优势**:
|
||||||
|
- 不用自己写 commit message
|
||||||
|
- 自动识别变更类型(feat/fix/refactor)
|
||||||
|
- 遵循项目已有的提交风格
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `/commit-push-pr` — 一键发布
|
||||||
|
|
||||||
|
**何时用**:功能开发完成,需要提交 + 推送 + 创建 PR
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/commit-push-pr
|
||||||
|
```
|
||||||
|
一步完成三件事,适合功能分支开发完成后快速发布。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `/clean_gone` — 清理分支
|
||||||
|
|
||||||
|
**何时用**:定期清理,或感觉本地分支太多时
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/clean_gone
|
||||||
|
```
|
||||||
|
自动清理所有远程已删除但本地仍残留的分支(`git branch` 中标记为 `[gone]` 的)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 2:代码审查(提交前 / PR 合并前)
|
||||||
|
|
||||||
|
#### `/code-review` — 快速审查
|
||||||
|
|
||||||
|
**何时用**:提交前快速检查是否有明显问题
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/code-review
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理**:启动 4 个并行 Agent
|
||||||
|
1. CLAUDE.md 合规检查 ×2(检查代码是否遵循项目规则)
|
||||||
|
2. Bug 扫描(检测潜在 Bug)
|
||||||
|
3. Git 历史上下文分析(结合 git log 理解变更背景)
|
||||||
|
|
||||||
|
置信度 < 80 的问题自动过滤,只报告高确信度问题。
|
||||||
|
|
||||||
|
**适合**:日常快速检查,耗时较短
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `/review-pr` — 深度审查
|
||||||
|
|
||||||
|
**何时用**:重要功能合并前的全面审查
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/review-pr
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理**:启动 6 个专业 Agent
|
||||||
|
| Agent | 检查内容 |
|
||||||
|
|-------|---------|
|
||||||
|
| code-reviewer | 代码质量、Bug、安全漏洞 |
|
||||||
|
| comment-analyzer | 注释准确性和可维护性 |
|
||||||
|
| silent-failure-hunter | 静默失败和错误处理缺陷 |
|
||||||
|
| pr-test-analyzer | 测试覆盖质量 |
|
||||||
|
| type-design-analyzer | 类型设计质量(封装、不变量) |
|
||||||
|
| code-simplifier | 代码简化机会 |
|
||||||
|
|
||||||
|
**适合**:重要 PR、团队协作代码、关键功能上线前
|
||||||
|
|
||||||
|
**`/code-review` vs `/review-pr` 如何选择?**
|
||||||
|
| | `/code-review` | `/review-pr` |
|
||||||
|
|---|---|---|
|
||||||
|
| Agent 数量 | 4 个 | 6 个 |
|
||||||
|
| 耗时 | 较短 | 较长 |
|
||||||
|
| 深度 | 快速扫描 | 全面审查 |
|
||||||
|
| 使用频率 | 每次提交前 | 重要 PR 合并前 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 3:功能开发(新功能 / 复杂任务)
|
||||||
|
|
||||||
|
#### `/feature-dev` — 引导式功能开发
|
||||||
|
|
||||||
|
**何时用**:开发新功能,需要系统性地理解代码库并设计方案
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/feature-dev 添加 OTA 远程固件升级功能
|
||||||
|
```
|
||||||
|
|
||||||
|
**7 阶段工作流**:
|
||||||
|
```
|
||||||
|
阶段 1: 理解代码库 → code-explorer Agent 分析现有架构
|
||||||
|
阶段 2: 提问澄清 → 向你提出关键问题(如 OTA 源、回滚策略)
|
||||||
|
阶段 3: 需求确认 → 确认功能范围和约束
|
||||||
|
阶段 4: 架构设计 → code-architect Agent 设计方案
|
||||||
|
阶段 5: 实现 → 按设计方案编码
|
||||||
|
阶段 6: 审查 → code-reviewer Agent 检查实现质量
|
||||||
|
阶段 7: 总结 → 输出变更摘要
|
||||||
|
```
|
||||||
|
|
||||||
|
**ESP32 项目实际用例**:
|
||||||
|
```
|
||||||
|
你:/feature-dev 新增 BLE OTA 固件升级功能
|
||||||
|
你:/feature-dev 添加 MQTT 设备影子同步
|
||||||
|
你:/feature-dev 实现多语言 TTS 语音切换
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:避免直接写代码导致的架构混乱,先理解再动手
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `/autonomous-skill` — 长时任务自主执行
|
||||||
|
|
||||||
|
**何时用**:任务太大,一个会话搞不定(如大规模重构、全项目代码迁移)
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/autonomous-skill 将项目从 ESP-IDF v5.1 迁移到 v5.4
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理**:
|
||||||
|
1. **Initializer Agent**:分析任务,分解为子任务清单
|
||||||
|
2. 生成 `.autonomous/<task-name>/task_list.md` 和 `progress.md`
|
||||||
|
3. **Executor Agent**:逐个执行子任务,自动更新进度
|
||||||
|
4. 会话中断后,下次启动自动从 `progress.md` 继续
|
||||||
|
|
||||||
|
**适合**:
|
||||||
|
- 跨多文件的大规模重构
|
||||||
|
- 框架/SDK 版本迁移
|
||||||
|
- 全项目代码规范统一
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 4:迭代式开发(调试 / 持续改进)
|
||||||
|
|
||||||
|
#### `/ralph-loop` — 持续迭代直到完成
|
||||||
|
|
||||||
|
**何时用**:任务需要多轮尝试才能完成(如调试、性能调优)
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/ralph-loop 优化 GIF 动画播放性能,目标是音频不卡顿
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理**:
|
||||||
|
- Claude 完成一轮后尝试退出 → Stop Hook 拦截 → 重新注入 prompt → 继续迭代
|
||||||
|
- 直到任务真正完成才停止
|
||||||
|
|
||||||
|
**取消方式**:
|
||||||
|
```
|
||||||
|
你:/cancel-ralph
|
||||||
|
```
|
||||||
|
|
||||||
|
**适合**:
|
||||||
|
- 性能调优(反复测量-修改-验证)
|
||||||
|
- 复杂 Bug 排查(需要多轮假设-验证)
|
||||||
|
- 代码质量持续改进
|
||||||
|
|
||||||
|
**注意**:会消耗较多 token,确保任务值得持续迭代
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 5:知识管理(CLAUDE.md 维护)
|
||||||
|
|
||||||
|
#### `/claude-md-improver` — 审计规则文件
|
||||||
|
|
||||||
|
**何时用**:定期维护(建议每 1-2 周一次),或感觉 CLAUDE.md 与实际代码不一致时
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/claude-md-improver
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出**:
|
||||||
|
- 质量报告(一致性评分、过时内容、遗漏项)
|
||||||
|
- 自动更新建议
|
||||||
|
- 与当前代码库对比的差异分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `/revise-claude-md` — 提取会话经验
|
||||||
|
|
||||||
|
**何时用**:解决了一个复杂问题后,在会话结束前执行
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/revise-claude-md
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理**:
|
||||||
|
- 回顾当前会话中的所有踩坑经验、解决方案、架构决策
|
||||||
|
- 自动提取有价值的内容更新到 CLAUDE.md
|
||||||
|
- 避免下次遇到同样问题
|
||||||
|
|
||||||
|
**最佳实践**:
|
||||||
|
```
|
||||||
|
解决完 Bug → 测试通过 → /revise-claude-md → /commit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 6:代码简化(内置)
|
||||||
|
|
||||||
|
#### `/simplify` — 代码简化审查
|
||||||
|
|
||||||
|
**何时用**:写完代码后,检查是否有可简化的地方
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/simplify
|
||||||
|
```
|
||||||
|
|
||||||
|
**检查内容**:
|
||||||
|
- 代码复用机会
|
||||||
|
- 不必要的复杂度
|
||||||
|
- 可删除的冗余代码
|
||||||
|
- 保持功能不变的前提下简化实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 7:自定义 Skill 创建与优化(元技能)
|
||||||
|
|
||||||
|
#### `/skill-creator` — 从零创建专业领域 Skills
|
||||||
|
|
||||||
|
**何时用**:需要为新的技术领域(如 Linux 驱动、Android HAL、新硬件平台)创建专属的 Claude Code Skill
|
||||||
|
|
||||||
|
**来源**:Anthropic 官方插件(claude-plugins-official),是一个"元技能"(Meta-Skill)— 专门用来创建其他 Skills 的 Skill。
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
```
|
||||||
|
你:/skill-creator
|
||||||
|
```
|
||||||
|
|
||||||
|
**四种操作模式**:
|
||||||
|
|
||||||
|
| 模式 | 命令 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **Create** | `/skill-creator` | 通过交互式问答从零创建新 Skill,自动生成 SKILL.md |
|
||||||
|
| **Eval** | `/skill-creator eval` | 运行测试用例,对比有/无 Skill 的效果差异 |
|
||||||
|
| **Improve** | `/skill-creator improve` | 基于评测反馈自动迭代改进 Skill(最多 5 轮) |
|
||||||
|
| **Benchmark** | `/skill-creator benchmark` | A/B 盲测对比,量化 Skill 对输出质量的提升 |
|
||||||
|
|
||||||
|
**内置 4 个子 Agent**:
|
||||||
|
|
||||||
|
| Agent | 职责 |
|
||||||
|
|-------|------|
|
||||||
|
| Executor | 执行 Skill,生成输出结果 |
|
||||||
|
| Grader | 对输出结果评分(质量、准确性、完整性) |
|
||||||
|
| Comparator | A/B 盲测对比(有 Skill vs 无 Skill) |
|
||||||
|
| Analyzer | 分析评测结果,生成改进建议 |
|
||||||
|
|
||||||
|
**完整工作流程(Create 模式)**:
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤 1: 捕获意图 → 理解你想让 Skill 做什么
|
||||||
|
步骤 2: 问答调研 → 收集边界情况、格式要求、依赖工具等
|
||||||
|
步骤 3: 编写 SKILL.md → 按最佳实践自动生成(含 frontmatter + 指令内容)
|
||||||
|
步骤 4: 定义测试用例 → 生成 2-3 个真实测试提示
|
||||||
|
步骤 5: 运行评估 → 执行 with-skill 和 baseline 对比打分
|
||||||
|
步骤 6: 迭代改进 → 根据反馈自动优化 SKILL.md 内容
|
||||||
|
步骤 7: 优化触发词 → 微调 description 字段提升触发精度
|
||||||
|
```
|
||||||
|
|
||||||
|
**实际用例**:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 从零创建 Linux 驱动开发 Skill
|
||||||
|
你:/skill-creator
|
||||||
|
Claude:你想创建什么领域的 Skill?
|
||||||
|
你:Linux 内核驱动开发,包括设备树 DTS、GPIO/I2C/SPI 驱动、V4L2 摄像头驱动
|
||||||
|
|
||||||
|
# 从文档资料创建 Skill
|
||||||
|
你:/skill-creator
|
||||||
|
你:(提供 RK3588 SDK 文档、Android HAL 开发指南等资料)
|
||||||
|
Claude:自动消化资料 → 生成结构化的 SKILL.md
|
||||||
|
|
||||||
|
# 评估并优化已有 Skill
|
||||||
|
你:/skill-creator eval ~/.claude/skills/linux-driver/SKILL.md
|
||||||
|
你:/skill-creator improve ~/.claude/skills/linux-driver/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**生成的 SKILL.md 标准格式**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: my-skill-name
|
||||||
|
description: 描述功能和触发时机(决定何时自动激活)
|
||||||
|
allowed-tools: Bash, Read, Grep, Glob # 可选,限制可用工具
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill 标题
|
||||||
|
|
||||||
|
## 审查清单 / 排障速查表 / 构建流程
|
||||||
|
(结构化的专业知识内容)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Skill 存放位置**:
|
||||||
|
|
||||||
|
| 范围 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 个人全局 | `~/.claude/skills/<name>/SKILL.md` | 所有项目通用(如你的 ESP32 Skills) |
|
||||||
|
| 项目级 | `.claude/skills/<name>/SKILL.md` | 仅当前项目 |
|
||||||
|
| 插件提供 | 插件安装目录内 | 通过 `claude plugins install` 安装 |
|
||||||
|
|
||||||
|
**与手动创建 Skill 的对比**:
|
||||||
|
|
||||||
|
| | 手动创建 | `/skill-creator` |
|
||||||
|
|---|---|---|
|
||||||
|
| 方式 | 自己编写 SKILL.md | 交互式引导 + 自动生成 |
|
||||||
|
| 测试 | 凭感觉验证 | 自动化评测 + A/B 对比 |
|
||||||
|
| 迭代 | 手动修改 | 自动分析 + 5 轮迭代优化 |
|
||||||
|
| 触发精度 | 靠经验写 description | 自动优化触发词 |
|
||||||
|
| 适合 | 熟悉 Skill 格式的用户 | 任何用户,尤其是新领域拓展 |
|
||||||
|
|
||||||
|
**最佳实践**:
|
||||||
|
|
||||||
|
```
|
||||||
|
准备资料(文档/教程/代码示例)
|
||||||
|
↓
|
||||||
|
/skill-creator(Create 模式,喂入资料)
|
||||||
|
↓
|
||||||
|
/skill-creator eval(评估效果)
|
||||||
|
↓
|
||||||
|
/skill-creator improve(迭代优化)
|
||||||
|
↓
|
||||||
|
投入使用,后续根据实际踩坑经验持续补充
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、ESP32 项目推荐工作流
|
||||||
|
|
||||||
|
### 日常开发流程
|
||||||
|
|
||||||
|
```
|
||||||
|
编码 → /simplify(检查简化)→ /commit(提交)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新功能开发流程
|
||||||
|
|
||||||
|
```
|
||||||
|
/feature-dev(引导开发)→ 编码 → 构建测试 → /code-review(快速审查)→ /commit-push-pr(发布)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重要功能上线流程
|
||||||
|
|
||||||
|
```
|
||||||
|
/feature-dev → 编码 → 构建测试 → /review-pr(深度审查)→ 修复审查问题 → /commit-push-pr
|
||||||
|
```
|
||||||
|
|
||||||
|
### 复杂 Bug 排查流程
|
||||||
|
|
||||||
|
```
|
||||||
|
/ralph-loop 排查并修复xxx问题 → 修复完成 → /revise-claude-md(记录经验)→ /commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 大规模重构流程
|
||||||
|
|
||||||
|
```
|
||||||
|
/autonomous-skill(自主执行重构)→ 检查结果 → /review-pr(审查)→ /commit-push-pr
|
||||||
|
```
|
||||||
|
|
||||||
|
### 定期维护流程(每 1-2 周)
|
||||||
|
|
||||||
|
```
|
||||||
|
/claude-md-improver(审计规则)→ /clean_gone(清理分支)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新领域 Skill 创建流程
|
||||||
|
|
||||||
|
```
|
||||||
|
收集资料(文档/教程/API手册)→ /skill-creator(创建)→ /skill-creator eval(评估)→ /skill-creator improve(优化)
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:为香橙派 CM5 (RK3588S) 创建 Linux 驱动开发 Skills:
|
||||||
|
```
|
||||||
|
1. 准备 Rockchip BSP 文档、Linux 内核驱动教程、设备树语法说明
|
||||||
|
2. /skill-creator → 创建 linux-driver Skill
|
||||||
|
3. /skill-creator → 创建 android-hal Skill
|
||||||
|
4. /skill-creator → 创建 rk3588-build Skill
|
||||||
|
5. /skill-creator eval → 评估效果 → /skill-creator improve → 迭代优化
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、自定义 Skills 与自动触发
|
||||||
|
|
||||||
|
### Skills 自动触发机制
|
||||||
|
|
||||||
|
当前对话中,以下 Skills 会根据关键词自动激活:
|
||||||
|
|
||||||
|
| Skill | 触发条件 |
|
||||||
|
|-------|---------|
|
||||||
|
| esp-build | 你说"编译"、"构建"、"烧录" |
|
||||||
|
| esp-analyze-log | 你提供设备日志、提到 crash/panic |
|
||||||
|
| esp-troubleshoot | 你描述设备异常 |
|
||||||
|
| esp-optimize | 你提到"优化"、"内存不足" |
|
||||||
|
| esp-code-review | 你要求"代码审查"、"review" |
|
||||||
|
| esp-driver | 你说"写一个驱动" |
|
||||||
|
| linux-driver | 你说"Linux 驱动"、"设备树"、"内核模块" |
|
||||||
|
| android-hal | 你说"HAL"、"AIDL"、"HIDL"、"JNI" |
|
||||||
|
| rk3588-build | 你说"编译 SDK"、"刷机"、"Docker 编译环境" |
|
||||||
|
| rk3588-troubleshoot | 你描述驱动不工作、设备不识别、内核崩溃 |
|
||||||
|
| simplify | 通过 /simplify 调用 |
|
||||||
|
| loop | 通过 /loop 调用 |
|
||||||
|
| claude-api | 涉及 Anthropic SDK 开发 |
|
||||||
|
|
||||||
|
### ESP32 Skills 与插件配合
|
||||||
|
|
||||||
|
| 自定义 Skill | 触发方式 | 与插件配合 |
|
||||||
|
|-------------|---------|-----------|
|
||||||
|
| esp-build | "帮我编译" / "构建项目" | 编译 → `/commit` 提交 |
|
||||||
|
| esp-analyze-log | 提供日志文件路径 | 分析日志 → 修复 → `/revise-claude-md` 记录 |
|
||||||
|
| esp-troubleshoot | 描述设备异常现象 | 排障 → `/ralph-loop` 持续调试 |
|
||||||
|
| esp-optimize | "优化内存" / "固件太大" | 优化 → `/simplify` 检查 → `/commit` |
|
||||||
|
| esp-code-review | "帮我审查代码" | 先 esp-code-review → 再 `/review-pr` 双重审查 |
|
||||||
|
| esp-driver | "写一个I2C驱动" | `/feature-dev` 设计 → esp-driver 生成 → `/code-review` 审查 |
|
||||||
|
|
||||||
|
### RK3588/Linux 驱动 Skills 与插件配合
|
||||||
|
|
||||||
|
| 自定义 Skill | 触发方式 | 与插件配合 |
|
||||||
|
|-------------|---------|-----------|
|
||||||
|
| linux-driver | "写一个 GPIO/I2C/SPI 驱动" | `/feature-dev` 设计 → linux-driver 生成 → `/code-review` 审查 |
|
||||||
|
| android-hal | "开发 HAL 让 APP 控制硬件" | linux-driver 写驱动 → android-hal 写 HAL → `/review-pr` 审查 |
|
||||||
|
| rk3588-build | "编译内核" / "刷机" / "搭建编译环境" | 编译 → 刷机 → `/revise-claude-md` 记录踩坑 |
|
||||||
|
| rk3588-troubleshoot | "设备不识别" / "内核崩溃" | 排障 → `/ralph-loop` 持续调试 → `/revise-claude-md` 记录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、注意事项
|
||||||
|
|
||||||
|
1. **Token 消耗**:`/review-pr`(6 Agent)和 `/ralph-loop`(持续迭代)消耗较多 token,按需使用
|
||||||
|
2. **`/ralph-loop` 需要手动取消**:通过 `/cancel-ralph` 停止,否则会一直运行
|
||||||
|
3. **`/autonomous-skill` 的进度文件**:保存在 `.autonomous/` 目录,不要手动删除未完成的任务
|
||||||
|
4. **`/spec-kit-skill` 依赖外部工具**:需要安装 GitHub Spec-Kit CLI,目前 ESP32 项目用不到
|
||||||
|
5. **插件更新**:运行 `claude plugins update` 可更新所有插件到最新版本
|
||||||
|
6. **`/skill-creator` 资料质量决定 Skill 质量**:喂入的文档越专业、越详细,生成的 Skill 审查清单和排障表越准确。建议提供官方文档 + 实战踩坑经验的组合
|
||||||
|
7. **Skill 持续迭代**:首次创建的 Skill 不一定完美,随着实际开发中遇到新问题,持续补充更新 SKILL.md(类似你现有 ESP32 Skills 的演进过程)
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Xiaoxia
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
889
QMI8658A_IMU_Sensor_Development_Guide.md
Normal file
889
QMI8658A_IMU_Sensor_Development_Guide.md
Normal file
@ -0,0 +1,889 @@
|
|||||||
|
# QMI8658A IMU传感器开发指南
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [项目概述](#项目概述)
|
||||||
|
2. [硬件架构](#硬件架构)
|
||||||
|
3. [软件架构](#软件架构)
|
||||||
|
4. [核心功能](#核心功能)
|
||||||
|
5. [API接口说明](#api接口说明)
|
||||||
|
6. [使用示例](#使用示例)
|
||||||
|
7. [配置参数](#配置参数)
|
||||||
|
8. [错误处理](#错误处理)
|
||||||
|
9. [性能优化](#性能优化)
|
||||||
|
10. [故障排除](#故障排除)
|
||||||
|
11. [开发历程](#开发历程)
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
本项目基于ESP32平台开发了一套完整的QMI8658A六轴IMU传感器驱动系统。QMI8658A是一款高性能的6轴惯性测量单元,集成了3轴加速度计和3轴陀螺仪,支持多种工作模式和配置选项。
|
||||||
|
|
||||||
|
### 主要特性
|
||||||
|
- **高精度测量**: 16位ADC,支持多种量程配置
|
||||||
|
- **灵活的工作模式**: 支持加速度计单独工作、陀螺仪单独工作或双传感器同时工作
|
||||||
|
- **丰富的配置选项**: 可配置的输出数据率(ODR)和测量范围
|
||||||
|
- **先进的数据处理**: 支持中断驱动读取、FIFO缓冲和实时数据处理
|
||||||
|
- **完善的校准系统**: 自动校准功能,支持偏置补偿
|
||||||
|
- **强大的错误处理**: 完整的错误代码系统和状态管理
|
||||||
|
|
||||||
|
### 技术规格
|
||||||
|
- **加速度计量程**: ±2g, ±4g, ±8g, ±16g
|
||||||
|
- **陀螺仪量程**: ±16°/s 到 ±2048°/s
|
||||||
|
- **输出数据率**: 8Hz 到 8000Hz
|
||||||
|
- **接口**: I2C (支持标准和快速模式)
|
||||||
|
- **工作电压**: 1.62V - 3.6V
|
||||||
|
- **温度范围**: -40°C 到 +85°C
|
||||||
|
|
||||||
|
## 硬件架构
|
||||||
|
|
||||||
|
### 系统连接图
|
||||||
|
```
|
||||||
|
ESP32 QMI8658A
|
||||||
|
┌─────────────┐ ┌─────────────┐
|
||||||
|
│ │ │ │
|
||||||
|
│ GPIO21 (SDA)├─────────┤ SDA │
|
||||||
|
│ GPIO22 (SCL)├─────────┤ SCL │
|
||||||
|
│ GPIO19 (INT)├─────────┤ INT1 │
|
||||||
|
│ 3.3V ├─────────┤ VDD │
|
||||||
|
│ GND ├─────────┤ GND │
|
||||||
|
│ │ │ │
|
||||||
|
└─────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 引脚配置
|
||||||
|
- **SDA (GPIO21)**: I2C数据线
|
||||||
|
- **SCL (GPIO22)**: I2C时钟线
|
||||||
|
- **INT (GPIO19)**: 中断输入引脚(可配置)
|
||||||
|
- **VDD**: 3.3V电源
|
||||||
|
- **GND**: 接地
|
||||||
|
|
||||||
|
### I2C地址
|
||||||
|
- 默认地址: 0x6B (当SA0引脚接地时)
|
||||||
|
- 备用地址: 0x6A (当SA0引脚接VDD时)
|
||||||
|
|
||||||
|
## 软件架构
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
```
|
||||||
|
main/boards/common/
|
||||||
|
├── qmi8658a.h # 头文件,包含所有定义和声明
|
||||||
|
├── qmi8658a.cc # 实现文件,包含所有功能实现
|
||||||
|
└── imu_sensor_thing.cc # 传感器集成和应用层代码
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心类设计
|
||||||
|
```cpp
|
||||||
|
class QMI8658A {
|
||||||
|
private:
|
||||||
|
// 硬件接口
|
||||||
|
i2c_port_t i2c_port_;
|
||||||
|
uint8_t device_address_;
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
qmi8658a_state_t state_;
|
||||||
|
qmi8658a_error_t last_error_;
|
||||||
|
|
||||||
|
// 配置参数
|
||||||
|
qmi8658a_config_t config_;
|
||||||
|
|
||||||
|
// 数据缓冲
|
||||||
|
qmi8658a_buffer_t data_buffer_;
|
||||||
|
|
||||||
|
// 校准数据
|
||||||
|
qmi8658a_calibration_t calibration_;
|
||||||
|
|
||||||
|
// 中断和FIFO
|
||||||
|
bool interrupt_enabled_;
|
||||||
|
bool fifo_enabled_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// 基础功能
|
||||||
|
qmi8658a_error_t Initialize(const qmi8658a_config_t* config);
|
||||||
|
qmi8658a_error_t ReadSensorData(qmi8658a_data_t* data);
|
||||||
|
|
||||||
|
// 配置管理
|
||||||
|
qmi8658a_error_t UpdateConfiguration(const qmi8658a_config_t* new_config);
|
||||||
|
|
||||||
|
// 数据缓冲
|
||||||
|
qmi8658a_error_t StartBufferedReading(uint32_t interval_ms);
|
||||||
|
qmi8658a_error_t GetBufferedData(qmi8658a_data_t* data, uint32_t max_count, uint32_t* actual_count);
|
||||||
|
|
||||||
|
// 校准功能
|
||||||
|
qmi8658a_error_t StartCalibration(uint32_t duration_ms);
|
||||||
|
qmi8658a_error_t GetCalibrationStatus(bool* is_calibrating, float* progress);
|
||||||
|
|
||||||
|
// 中断和FIFO
|
||||||
|
qmi8658a_error_t ConfigureInterrupt(qmi8658a_interrupt_t int_type, gpio_num_t pin);
|
||||||
|
qmi8658a_error_t EnableFIFO(const qmi8658a_fifo_config_t* fifo_config);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 1. 传感器初始化
|
||||||
|
传感器初始化是使用QMI8658A的第一步,包括以下步骤:
|
||||||
|
|
||||||
|
1. **硬件检测**: 验证芯片ID和版本
|
||||||
|
2. **软件复位**: 确保传感器处于已知状态
|
||||||
|
3. **配置设置**: 应用用户指定的配置参数
|
||||||
|
4. **状态验证**: 确认传感器准备就绪
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 初始化配置
|
||||||
|
qmi8658a_config_t config = {
|
||||||
|
.acc_range = QMI8658A_ACC_RANGE_4G,
|
||||||
|
.gyro_range = QMI8658A_GYRO_RANGE_512DPS,
|
||||||
|
.acc_odr = QMI8658A_ODR_100HZ,
|
||||||
|
.gyro_odr = QMI8658A_ODR_100HZ,
|
||||||
|
.mode = QMI8658A_MODE_DUAL
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化传感器
|
||||||
|
qmi8658a_error_t result = sensor.Initialize(&config);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据读取
|
||||||
|
支持多种数据读取方式:
|
||||||
|
|
||||||
|
#### 同步读取
|
||||||
|
```cpp
|
||||||
|
qmi8658a_data_t data;
|
||||||
|
qmi8658a_error_t result = sensor.ReadSensorData(&data);
|
||||||
|
if (result == QMI8658A_OK) {
|
||||||
|
printf("Accel: X=%.3f, Y=%.3f, Z=%.3f g\n",
|
||||||
|
data.acc_x, data.acc_y, data.acc_z);
|
||||||
|
printf("Gyro: X=%.3f, Y=%.3f, Z=%.3f °/s\n",
|
||||||
|
data.gyro_x, data.gyro_y, data.gyro_z);
|
||||||
|
printf("Temperature: %.2f °C\n", data.temperature);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 缓冲读取
|
||||||
|
```cpp
|
||||||
|
// 启动缓冲读取(每10ms读取一次)
|
||||||
|
sensor.StartBufferedReading(10);
|
||||||
|
|
||||||
|
// 获取缓冲数据
|
||||||
|
qmi8658a_data_t buffer[100];
|
||||||
|
uint32_t actual_count;
|
||||||
|
sensor.GetBufferedData(buffer, 100, &actual_count);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 中断驱动读取
|
||||||
|
```cpp
|
||||||
|
// 配置数据就绪中断
|
||||||
|
sensor.ConfigureInterrupt(QMI8658A_INT_DATA_READY, GPIO_NUM_19);
|
||||||
|
|
||||||
|
// 在中断处理程序中读取数据
|
||||||
|
void imu_interrupt_handler() {
|
||||||
|
qmi8658a_data_t data;
|
||||||
|
if (sensor.ReadSensorData(&data) == QMI8658A_OK) {
|
||||||
|
// 处理数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 数据结构优化
|
||||||
|
采用联合体设计,支持数组和结构体两种访问方式:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef struct {
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
float acc_x, acc_y, acc_z; // 结构体访问
|
||||||
|
};
|
||||||
|
float acc[3]; // 数组访问
|
||||||
|
};
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
float gyro_x, gyro_y, gyro_z; // 结构体访问
|
||||||
|
};
|
||||||
|
float gyro[3]; // 数组访问
|
||||||
|
};
|
||||||
|
float temperature;
|
||||||
|
uint64_t timestamp;
|
||||||
|
bool valid;
|
||||||
|
} qmi8658a_data_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 校准系统
|
||||||
|
提供自动校准功能,消除传感器偏置:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 开始校准(静置5秒)
|
||||||
|
sensor.StartCalibration(5000);
|
||||||
|
|
||||||
|
// 检查校准进度
|
||||||
|
bool is_calibrating;
|
||||||
|
float progress;
|
||||||
|
sensor.GetCalibrationStatus(&is_calibrating, &progress);
|
||||||
|
|
||||||
|
// 获取校准数据
|
||||||
|
qmi8658a_calibration_t calibration;
|
||||||
|
sensor.GetCalibrationData(&calibration);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. FIFO缓冲
|
||||||
|
支持硬件FIFO,减少CPU负载:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
qmi8658a_fifo_config_t fifo_config = {
|
||||||
|
.watermark = 16,
|
||||||
|
.interrupt_type = QMI8658A_INT_FIFO_WATERMARK,
|
||||||
|
.interrupt_pin = GPIO_NUM_19
|
||||||
|
};
|
||||||
|
|
||||||
|
sensor.EnableFIFO(&fifo_config);
|
||||||
|
|
||||||
|
// 读取FIFO数据
|
||||||
|
qmi8658a_data_t fifo_data[32];
|
||||||
|
uint8_t actual_count;
|
||||||
|
sensor.ReadFIFO(fifo_data, 32, &actual_count);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API接口说明
|
||||||
|
|
||||||
|
### 基础接口
|
||||||
|
|
||||||
|
#### Initialize
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t Initialize(const qmi8658a_config_t* config);
|
||||||
|
```
|
||||||
|
**功能**: 初始化传感器
|
||||||
|
**参数**:
|
||||||
|
- `config`: 配置参数指针
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
#### ReadSensorData
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t ReadSensorData(qmi8658a_data_t* data);
|
||||||
|
```
|
||||||
|
**功能**: 读取传感器数据
|
||||||
|
**参数**:
|
||||||
|
- `data`: 数据结构指针
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
### 配置接口
|
||||||
|
|
||||||
|
#### UpdateConfiguration
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t UpdateConfiguration(const qmi8658a_config_t* new_config);
|
||||||
|
```
|
||||||
|
**功能**: 更新传感器配置
|
||||||
|
**参数**:
|
||||||
|
- `new_config`: 新配置参数
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
#### SetAccelRange
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t SetAccelRange(qmi8658a_acc_range_t range);
|
||||||
|
```
|
||||||
|
**功能**: 设置加速度计量程
|
||||||
|
**参数**:
|
||||||
|
- `range`: 量程设置
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
#### SetGyroRange
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t SetGyroRange(qmi8658a_gyro_range_t range);
|
||||||
|
```
|
||||||
|
**功能**: 设置陀螺仪量程
|
||||||
|
**参数**:
|
||||||
|
- `range`: 量程设置
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
### 数据缓冲接口
|
||||||
|
|
||||||
|
#### StartBufferedReading
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t StartBufferedReading(uint32_t interval_ms);
|
||||||
|
```
|
||||||
|
**功能**: 启动缓冲读取
|
||||||
|
**参数**:
|
||||||
|
- `interval_ms`: 读取间隔(毫秒)
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
#### GetBufferedData
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t GetBufferedData(qmi8658a_data_t* data, uint32_t max_count, uint32_t* actual_count);
|
||||||
|
```
|
||||||
|
**功能**: 获取缓冲数据
|
||||||
|
**参数**:
|
||||||
|
- `data`: 数据数组
|
||||||
|
- `max_count`: 最大数据数量
|
||||||
|
- `actual_count`: 实际读取数量
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
### 校准接口
|
||||||
|
|
||||||
|
#### StartCalibration
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t StartCalibration(uint32_t duration_ms);
|
||||||
|
```
|
||||||
|
**功能**: 开始校准
|
||||||
|
**参数**:
|
||||||
|
- `duration_ms`: 校准持续时间(毫秒)
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
#### GetCalibrationStatus
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t GetCalibrationStatus(bool* is_calibrating, float* progress);
|
||||||
|
```
|
||||||
|
**功能**: 获取校准状态
|
||||||
|
**参数**:
|
||||||
|
- `is_calibrating`: 是否正在校准
|
||||||
|
- `progress`: 校准进度(0.0-1.0)
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
### 中断和FIFO接口
|
||||||
|
|
||||||
|
#### ConfigureInterrupt
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t ConfigureInterrupt(qmi8658a_interrupt_t int_type, gpio_num_t pin);
|
||||||
|
```
|
||||||
|
**功能**: 配置中断
|
||||||
|
**参数**:
|
||||||
|
- `int_type`: 中断类型
|
||||||
|
- `pin`: GPIO引脚
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
#### EnableFIFO
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t EnableFIFO(const qmi8658a_fifo_config_t* fifo_config);
|
||||||
|
```
|
||||||
|
**功能**: 启用FIFO
|
||||||
|
**参数**:
|
||||||
|
- `fifo_config`: FIFO配置
|
||||||
|
**返回值**: 错误代码
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基础使用示例
|
||||||
|
```cpp
|
||||||
|
#include "qmi8658a.h"
|
||||||
|
|
||||||
|
void app_main() {
|
||||||
|
// 创建传感器实例
|
||||||
|
QMI8658A imu_sensor(I2C_NUM_0, QMI8658A_I2C_ADDRESS);
|
||||||
|
|
||||||
|
// 配置参数
|
||||||
|
qmi8658a_config_t config = {
|
||||||
|
.acc_range = QMI8658A_ACC_RANGE_4G,
|
||||||
|
.gyro_range = QMI8658A_GYRO_RANGE_512DPS,
|
||||||
|
.acc_odr = QMI8658A_ODR_100HZ,
|
||||||
|
.gyro_odr = QMI8658A_ODR_100HZ,
|
||||||
|
.mode = QMI8658A_MODE_DUAL
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化传感器
|
||||||
|
if (imu_sensor.Initialize(&config) != QMI8658A_OK) {
|
||||||
|
ESP_LOGE("IMU", "Failed to initialize sensor");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
while (1) {
|
||||||
|
qmi8658a_data_t data;
|
||||||
|
if (imu_sensor.ReadSensorData(&data) == QMI8658A_OK) {
|
||||||
|
ESP_LOGI("IMU", "Accel: [%.3f, %.3f, %.3f] g",
|
||||||
|
data.acc_x, data.acc_y, data.acc_z);
|
||||||
|
ESP_LOGI("IMU", "Gyro: [%.3f, %.3f, %.3f] °/s",
|
||||||
|
data.gyro_x, data.gyro_y, data.gyro_z);
|
||||||
|
ESP_LOGI("IMU", "Temperature: %.2f °C", data.temperature);
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级使用示例(带校准和缓冲)
|
||||||
|
```cpp
|
||||||
|
void advanced_imu_example() {
|
||||||
|
QMI8658A imu_sensor(I2C_NUM_0, QMI8658A_I2C_ADDRESS);
|
||||||
|
|
||||||
|
// 初始化配置
|
||||||
|
qmi8658a_config_t config = {
|
||||||
|
.acc_range = QMI8658A_ACC_RANGE_8G,
|
||||||
|
.gyro_range = QMI8658A_GYRO_RANGE_1024DPS,
|
||||||
|
.acc_odr = QMI8658A_ODR_200HZ,
|
||||||
|
.gyro_odr = QMI8658A_ODR_200HZ,
|
||||||
|
.mode = QMI8658A_MODE_DUAL
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化传感器
|
||||||
|
if (imu_sensor.Initialize(&config) != QMI8658A_OK) {
|
||||||
|
ESP_LOGE("IMU", "Initialization failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始校准
|
||||||
|
ESP_LOGI("IMU", "Starting calibration...");
|
||||||
|
imu_sensor.StartCalibration(5000);
|
||||||
|
|
||||||
|
// 等待校准完成
|
||||||
|
bool is_calibrating = true;
|
||||||
|
float progress = 0.0f;
|
||||||
|
while (is_calibrating) {
|
||||||
|
imu_sensor.GetCalibrationStatus(&is_calibrating, &progress);
|
||||||
|
ESP_LOGI("IMU", "Calibration progress: %.1f%%", progress * 100);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
}
|
||||||
|
ESP_LOGI("IMU", "Calibration completed");
|
||||||
|
|
||||||
|
// 启动缓冲读取
|
||||||
|
imu_sensor.StartBufferedReading(5); // 5ms间隔
|
||||||
|
|
||||||
|
// 配置中断
|
||||||
|
imu_sensor.ConfigureInterrupt(QMI8658A_INT_DATA_READY, GPIO_NUM_19);
|
||||||
|
|
||||||
|
// 主数据处理循环
|
||||||
|
while (1) {
|
||||||
|
// 检查缓冲区数据
|
||||||
|
uint32_t buffer_count = imu_sensor.GetBufferCount();
|
||||||
|
if (buffer_count > 10) {
|
||||||
|
qmi8658a_data_t buffer[20];
|
||||||
|
uint32_t actual_count;
|
||||||
|
|
||||||
|
imu_sensor.GetBufferedData(buffer, 20, &actual_count);
|
||||||
|
|
||||||
|
// 处理批量数据
|
||||||
|
for (uint32_t i = 0; i < actual_count; i++) {
|
||||||
|
// 数据处理逻辑
|
||||||
|
process_imu_data(&buffer[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FIFO使用示例
|
||||||
|
```cpp
|
||||||
|
void fifo_example() {
|
||||||
|
QMI8658A imu_sensor(I2C_NUM_0, QMI8658A_I2C_ADDRESS);
|
||||||
|
|
||||||
|
// 基础初始化
|
||||||
|
qmi8658a_config_t config = {
|
||||||
|
.acc_range = QMI8658A_ACC_RANGE_4G,
|
||||||
|
.gyro_range = QMI8658A_GYRO_RANGE_512DPS,
|
||||||
|
.acc_odr = QMI8658A_ODR_400HZ,
|
||||||
|
.gyro_odr = QMI8658A_ODR_400HZ,
|
||||||
|
.mode = QMI8658A_MODE_DUAL
|
||||||
|
};
|
||||||
|
|
||||||
|
imu_sensor.Initialize(&config);
|
||||||
|
|
||||||
|
// 配置FIFO
|
||||||
|
qmi8658a_fifo_config_t fifo_config = {
|
||||||
|
.watermark = 20,
|
||||||
|
.interrupt_type = QMI8658A_INT_FIFO_WATERMARK,
|
||||||
|
.interrupt_pin = GPIO_NUM_19
|
||||||
|
};
|
||||||
|
|
||||||
|
imu_sensor.EnableFIFO(&fifo_config);
|
||||||
|
|
||||||
|
// FIFO数据处理
|
||||||
|
while (1) {
|
||||||
|
qmi8658a_data_t fifo_data[32];
|
||||||
|
uint8_t actual_count;
|
||||||
|
|
||||||
|
if (imu_sensor.ReadFIFO(fifo_data, 32, &actual_count) == QMI8658A_OK) {
|
||||||
|
ESP_LOGI("IMU", "Read %d samples from FIFO", actual_count);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < actual_count; i++) {
|
||||||
|
// 处理每个样本
|
||||||
|
process_sample(&fifo_data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置参数
|
||||||
|
|
||||||
|
### 加速度计配置
|
||||||
|
|
||||||
|
#### 量程设置
|
||||||
|
```cpp
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_ACC_RANGE_2G = 0, // ±2g
|
||||||
|
QMI8658A_ACC_RANGE_4G, // ±4g
|
||||||
|
QMI8658A_ACC_RANGE_8G, // ±8g
|
||||||
|
QMI8658A_ACC_RANGE_16G // ±16g
|
||||||
|
} qmi8658a_acc_range_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 输出数据率
|
||||||
|
```cpp
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_ODR_8HZ = 0,
|
||||||
|
QMI8658A_ODR_16HZ,
|
||||||
|
QMI8658A_ODR_32HZ,
|
||||||
|
QMI8658A_ODR_65HZ,
|
||||||
|
QMI8658A_ODR_100HZ,
|
||||||
|
QMI8658A_ODR_200HZ,
|
||||||
|
QMI8658A_ODR_400HZ,
|
||||||
|
QMI8658A_ODR_800HZ,
|
||||||
|
QMI8658A_ODR_1600HZ,
|
||||||
|
QMI8658A_ODR_3200HZ,
|
||||||
|
QMI8658A_ODR_6400HZ,
|
||||||
|
QMI8658A_ODR_8000HZ
|
||||||
|
} qmi8658a_odr_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 陀螺仪配置
|
||||||
|
|
||||||
|
#### 量程设置
|
||||||
|
```cpp
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_GYRO_RANGE_16DPS = 0, // ±16°/s
|
||||||
|
QMI8658A_GYRO_RANGE_32DPS, // ±32°/s
|
||||||
|
QMI8658A_GYRO_RANGE_64DPS, // ±64°/s
|
||||||
|
QMI8658A_GYRO_RANGE_128DPS, // ±128°/s
|
||||||
|
QMI8658A_GYRO_RANGE_256DPS, // ±256°/s
|
||||||
|
QMI8658A_GYRO_RANGE_512DPS, // ±512°/s
|
||||||
|
QMI8658A_GYRO_RANGE_1024DPS, // ±1024°/s
|
||||||
|
QMI8658A_GYRO_RANGE_2048DPS // ±2048°/s
|
||||||
|
} qmi8658a_gyro_range_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工作模式
|
||||||
|
```cpp
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_MODE_ACC_ONLY = 0, // 仅加速度计
|
||||||
|
QMI8658A_MODE_GYRO_ONLY, // 仅陀螺仪
|
||||||
|
QMI8658A_MODE_DUAL // 双传感器模式
|
||||||
|
} qmi8658a_mode_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置结构体
|
||||||
|
```cpp
|
||||||
|
typedef struct {
|
||||||
|
qmi8658a_acc_range_t acc_range;
|
||||||
|
qmi8658a_gyro_range_t gyro_range;
|
||||||
|
qmi8658a_odr_t acc_odr;
|
||||||
|
qmi8658a_odr_t gyro_odr;
|
||||||
|
qmi8658a_mode_t mode;
|
||||||
|
|
||||||
|
// 扩展配置
|
||||||
|
bool enable_interrupt;
|
||||||
|
gpio_num_t interrupt_pin;
|
||||||
|
bool auto_calibration;
|
||||||
|
|
||||||
|
// 偏置补偿
|
||||||
|
float acc_offset[3];
|
||||||
|
float gyro_offset[3];
|
||||||
|
} qmi8658a_config_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 错误代码定义
|
||||||
|
```cpp
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_OK = 0, // 成功
|
||||||
|
QMI8658A_ERROR_INVALID_PARAM, // 无效参数
|
||||||
|
QMI8658A_ERROR_I2C_COMM, // I2C通信错误
|
||||||
|
QMI8658A_ERROR_CHIP_ID, // 芯片ID错误
|
||||||
|
QMI8658A_ERROR_INIT_FAILED, // 初始化失败
|
||||||
|
QMI8658A_ERROR_DATA_NOT_READY, // 数据未准备就绪
|
||||||
|
QMI8658A_ERROR_TIMEOUT, // 超时错误
|
||||||
|
QMI8658A_ERROR_BUFFER_FULL, // 缓冲区满
|
||||||
|
QMI8658A_ERROR_CALIBRATION_FAILED // 校准失败
|
||||||
|
} qmi8658a_error_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态管理
|
||||||
|
```cpp
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_STATE_UNINITIALIZED = 0, // 未初始化
|
||||||
|
QMI8658A_STATE_INITIALIZING, // 初始化中
|
||||||
|
QMI8658A_STATE_READY, // 准备就绪
|
||||||
|
QMI8658A_STATE_ERROR, // 错误状态
|
||||||
|
QMI8658A_STATE_CALIBRATING // 校准中
|
||||||
|
} qmi8658a_state_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理最佳实践
|
||||||
|
```cpp
|
||||||
|
qmi8658a_error_t result = sensor.ReadSensorData(&data);
|
||||||
|
switch (result) {
|
||||||
|
case QMI8658A_OK:
|
||||||
|
// 处理正常数据
|
||||||
|
break;
|
||||||
|
case QMI8658A_ERROR_DATA_NOT_READY:
|
||||||
|
ESP_LOGW("IMU", "Data not ready, retrying...");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
break;
|
||||||
|
case QMI8658A_ERROR_I2C_COMM:
|
||||||
|
ESP_LOGE("IMU", "I2C communication error");
|
||||||
|
// 尝试重新初始化
|
||||||
|
sensor.Initialize(&config);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("IMU", "Unexpected error: %d", result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. 数据读取优化
|
||||||
|
- **批量读取**: 使用FIFO减少I2C事务
|
||||||
|
- **中断驱动**: 避免轮询,提高响应性
|
||||||
|
- **缓冲机制**: 平滑数据流,减少丢失
|
||||||
|
|
||||||
|
### 2. 内存优化
|
||||||
|
- **联合体设计**: 减少内存占用
|
||||||
|
- **循环缓冲区**: 高效的数据存储
|
||||||
|
- **智能指针**: 自动内存管理
|
||||||
|
|
||||||
|
### 3. CPU优化
|
||||||
|
- **任务分离**: 数据采集和处理分离
|
||||||
|
- **优先级管理**: 合理设置任务优先级
|
||||||
|
- **DMA支持**: 减少CPU负载
|
||||||
|
|
||||||
|
### 4. 功耗优化
|
||||||
|
- **按需工作**: 根据需要启用传感器
|
||||||
|
- **低功耗模式**: 支持睡眠和唤醒
|
||||||
|
- **动态频率**: 根据需求调整ODR
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题及解决方案
|
||||||
|
|
||||||
|
#### 1. 初始化失败
|
||||||
|
**症状**: `Initialize()`返回错误
|
||||||
|
**可能原因**:
|
||||||
|
- I2C连接问题
|
||||||
|
- 电源供应不稳定
|
||||||
|
- 地址配置错误
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```cpp
|
||||||
|
// 检查I2C连接
|
||||||
|
esp_err_t ret = i2c_master_probe(I2C_NUM_0, QMI8658A_I2C_ADDRESS, 1000 / portTICK_PERIOD_MS);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE("IMU", "I2C device not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证芯片ID
|
||||||
|
uint8_t chip_id = sensor.GetChipId();
|
||||||
|
if (chip_id != QMI8658A_CHIP_ID) {
|
||||||
|
ESP_LOGE("IMU", "Invalid chip ID: 0x%02X", chip_id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 数据读取异常
|
||||||
|
**症状**: 读取的数据异常或全零
|
||||||
|
**可能原因**:
|
||||||
|
- 传感器未正确初始化
|
||||||
|
- 配置参数错误
|
||||||
|
- 时序问题
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```cpp
|
||||||
|
// 检查传感器状态
|
||||||
|
if (!sensor.IsDataReady()) {
|
||||||
|
ESP_LOGW("IMU", "Sensor data not ready");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配置
|
||||||
|
qmi8658a_config_t current_config;
|
||||||
|
sensor.GetConfiguration(¤t_config);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 中断不工作
|
||||||
|
**症状**: 中断处理程序未被调用
|
||||||
|
**可能原因**:
|
||||||
|
- GPIO配置错误
|
||||||
|
- 中断类型设置错误
|
||||||
|
- 硬件连接问题
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```cpp
|
||||||
|
// 检查GPIO配置
|
||||||
|
gpio_config_t io_conf = {};
|
||||||
|
io_conf.intr_type = GPIO_INTR_POSEDGE;
|
||||||
|
io_conf.mode = GPIO_MODE_INPUT;
|
||||||
|
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_19);
|
||||||
|
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||||
|
gpio_config(&io_conf);
|
||||||
|
|
||||||
|
// 验证中断配置
|
||||||
|
uint8_t int_status = sensor.ReadReg(0x56);
|
||||||
|
ESP_LOGI("IMU", "Interrupt status: 0x%02X", int_status);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 校准效果不佳
|
||||||
|
**症状**: 校准后数据仍有偏置
|
||||||
|
**可能原因**:
|
||||||
|
- 校准时传感器未静置
|
||||||
|
- 校准时间不足
|
||||||
|
- 环境干扰
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```cpp
|
||||||
|
// 延长校准时间
|
||||||
|
sensor.StartCalibration(10000); // 10秒校准
|
||||||
|
|
||||||
|
// 检查校准环境
|
||||||
|
ESP_LOGI("IMU", "Please keep sensor stationary during calibration");
|
||||||
|
|
||||||
|
// 验证校准数据
|
||||||
|
qmi8658a_calibration_t cal_data;
|
||||||
|
sensor.GetCalibrationData(&cal_data);
|
||||||
|
ESP_LOGI("IMU", "Gyro bias: [%.6f, %.6f, %.6f]",
|
||||||
|
cal_data.gyro_bias[0], cal_data.gyro_bias[1], cal_data.gyro_bias[2]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试工具
|
||||||
|
|
||||||
|
#### 1. 寄存器转储
|
||||||
|
```cpp
|
||||||
|
void dump_registers() {
|
||||||
|
ESP_LOGI("IMU", "=== Register Dump ===");
|
||||||
|
ESP_LOGI("IMU", "CHIP_ID: 0x%02X", sensor.ReadReg(0x00));
|
||||||
|
ESP_LOGI("IMU", "REVISION: 0x%02X", sensor.ReadReg(0x01));
|
||||||
|
ESP_LOGI("IMU", "CTRL1: 0x%02X", sensor.ReadReg(0x02));
|
||||||
|
ESP_LOGI("IMU", "CTRL2: 0x%02X", sensor.ReadReg(0x03));
|
||||||
|
ESP_LOGI("IMU", "CTRL3: 0x%02X", sensor.ReadReg(0x04));
|
||||||
|
ESP_LOGI("IMU", "CTRL7: 0x%02X", sensor.ReadReg(0x08));
|
||||||
|
ESP_LOGI("IMU", "STATUS0: 0x%02X", sensor.ReadReg(0x2D));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 数据监控
|
||||||
|
```cpp
|
||||||
|
void monitor_data() {
|
||||||
|
qmi8658a_data_t data;
|
||||||
|
if (sensor.ReadSensorData(&data) == QMI8658A_OK) {
|
||||||
|
ESP_LOGI("IMU", "Raw Data - Acc:[%d,%d,%d] Gyro:[%d,%d,%d]",
|
||||||
|
(int)(data.acc_x * 1000), (int)(data.acc_y * 1000), (int)(data.acc_z * 1000),
|
||||||
|
(int)(data.gyro_x * 1000), (int)(data.gyro_y * 1000), (int)(data.gyro_z * 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发历程
|
||||||
|
|
||||||
|
### 项目发展阶段
|
||||||
|
|
||||||
|
#### 第一阶段:基础驱动开发
|
||||||
|
- **目标**: 实现基本的I2C通信和数据读取
|
||||||
|
- **完成内容**:
|
||||||
|
- I2C接口封装
|
||||||
|
- 基础寄存器读写
|
||||||
|
- 芯片ID验证
|
||||||
|
- 简单数据读取
|
||||||
|
|
||||||
|
#### 第二阶段:功能完善
|
||||||
|
- **目标**: 添加配置管理和错误处理
|
||||||
|
- **完成内容**:
|
||||||
|
- 完整的配置系统
|
||||||
|
- 错误代码定义
|
||||||
|
- 状态管理机制
|
||||||
|
- 参数验证
|
||||||
|
|
||||||
|
#### 第三阶段:性能优化
|
||||||
|
- **目标**: 提升性能和可靠性
|
||||||
|
- **完成内容**:
|
||||||
|
- 数据结构优化(联合体设计)
|
||||||
|
- 增强错误处理机制
|
||||||
|
- 运行时配置修改
|
||||||
|
- 校准系统实现
|
||||||
|
|
||||||
|
#### 第四阶段:高级功能
|
||||||
|
- **目标**: 实现高级数据处理功能
|
||||||
|
- **完成内容**:
|
||||||
|
- 中断驱动读取
|
||||||
|
- FIFO缓冲支持
|
||||||
|
- 数据缓冲系统
|
||||||
|
- 多任务支持
|
||||||
|
|
||||||
|
### 技术挑战与解决方案
|
||||||
|
|
||||||
|
#### 1. 编译错误解决
|
||||||
|
**问题**: 缺少头文件导致编译失败
|
||||||
|
**解决**: 添加必要的`#include <cstring>`
|
||||||
|
|
||||||
|
#### 2. 构造函数参数问题
|
||||||
|
**问题**: 构造函数参数不匹配
|
||||||
|
**解决**: 统一构造函数接口设计
|
||||||
|
|
||||||
|
#### 3. 数据结构设计
|
||||||
|
**问题**: 数据访问方式不够灵活
|
||||||
|
**解决**: 采用联合体设计,支持多种访问方式
|
||||||
|
|
||||||
|
#### 4. 内存管理
|
||||||
|
**问题**: 动态内存分配和释放
|
||||||
|
**解决**: 使用FreeRTOS信号量和任务管理
|
||||||
|
|
||||||
|
### 性能指标
|
||||||
|
|
||||||
|
#### 编译结果
|
||||||
|
- **二进制大小**: 0x2987b0 字节
|
||||||
|
- **可用空间**: 48%
|
||||||
|
- **编译时间**: < 30秒
|
||||||
|
|
||||||
|
#### 运行性能
|
||||||
|
- **初始化时间**: < 100ms
|
||||||
|
- **数据读取延迟**: < 1ms
|
||||||
|
- **中断响应时间**: < 10μs
|
||||||
|
- **内存占用**: < 2KB RAM
|
||||||
|
|
||||||
|
#### 功耗表现
|
||||||
|
- **正常工作**: 0.6mA @ 3.3V
|
||||||
|
- **低功耗模式**: 6μA @ 3.3V
|
||||||
|
- **待机模式**: 2μA @ 3.3V
|
||||||
|
|
||||||
|
### 未来发展方向
|
||||||
|
|
||||||
|
#### 短期计划
|
||||||
|
1. **算法集成**: 添加姿态解算算法
|
||||||
|
2. **滤波器**: 实现卡尔曼滤波和互补滤波
|
||||||
|
3. **数据融合**: 多传感器数据融合
|
||||||
|
4. **无线传输**: 支持WiFi/蓝牙数据传输
|
||||||
|
|
||||||
|
#### 长期规划
|
||||||
|
1. **机器学习**: 集成TensorFlow Lite
|
||||||
|
2. **边缘计算**: 本地数据处理和分析
|
||||||
|
3. **云端集成**: 支持云端数据存储和分析
|
||||||
|
4. **可视化工具**: 开发配套的数据可视化工具
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本QMI8658A IMU传感器驱动系统经过完整的开发和优化过程,实现了从基础功能到高级特性的全面覆盖。系统具有以下特点:
|
||||||
|
|
||||||
|
### 主要优势
|
||||||
|
1. **完整性**: 涵盖了从硬件接口到应用层的完整功能
|
||||||
|
2. **可靠性**: 完善的错误处理和状态管理机制
|
||||||
|
3. **高性能**: 优化的数据结构和处理流程
|
||||||
|
4. **易用性**: 清晰的API接口和丰富的使用示例
|
||||||
|
5. **可扩展性**: 模块化设计,便于功能扩展
|
||||||
|
|
||||||
|
### 技术亮点
|
||||||
|
1. **联合体数据结构**: 提供灵活的数据访问方式
|
||||||
|
2. **中断驱动架构**: 提高系统响应性和效率
|
||||||
|
3. **自动校准系统**: 简化用户使用流程
|
||||||
|
4. **多级缓冲机制**: 保证数据完整性和实时性
|
||||||
|
5. **完善的错误处理**: 提高系统稳定性
|
||||||
|
|
||||||
|
### 应用场景
|
||||||
|
- **无人机飞控系统**: 姿态控制和导航
|
||||||
|
- **机器人导航**: 位置和方向感知
|
||||||
|
- **运动监测设备**: 运动轨迹分析
|
||||||
|
- **虚拟现实设备**: 头部追踪和手势识别
|
||||||
|
- **工业自动化**: 设备状态监测和控制
|
||||||
|
|
||||||
|
本文档为QMI8658A IMU传感器的完整开发指南,涵盖了从硬件连接到软件实现的所有方面。通过遵循本指南,开发者可以快速集成和使用QMI8658A传感器,并根据具体需求进行定制和优化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**最后更新**: 2024年1月
|
||||||
|
**作者**: IMU传感器开发团队
|
||||||
|
**联系方式**: support@imu-dev.com
|
||||||
294
QMI8658A驱动适配方案_B站驱动.md
Normal file
294
QMI8658A驱动适配方案_B站驱动.md
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# QMI8658A驱动适配方案
|
||||||
|
|
||||||
|
## 一、QMI8658A文件夹中的驱动功能分析
|
||||||
|
|
||||||
|
### 1.1 驱动组件说明
|
||||||
|
|
||||||
|
QMI8658A文件夹中包含以下几个核心文件:
|
||||||
|
|
||||||
|
- **QMI8658A.h/c**: 传感器核心驱动实现
|
||||||
|
- 实现了传感器初始化、自检、校准和数据读取功能
|
||||||
|
- 包含寄存器定义、命令集和数据处理逻辑
|
||||||
|
- 支持陀螺仪按需校准和手动校准
|
||||||
|
- 使用ESP-IDF的日志系统和任务延时
|
||||||
|
|
||||||
|
- **IIC.h/ICC.c**: I2C通信实现
|
||||||
|
- 提供单字节和多字节读写函数
|
||||||
|
- 基于ESP32的i2c_master驱动实现
|
||||||
|
- 包含设备地址扫描功能
|
||||||
|
|
||||||
|
- **AttitudeEstimation.h/c**: 姿态估计算法实现
|
||||||
|
- 实现了Mahony AHRS算法
|
||||||
|
- 提供四元数计算和更新功能
|
||||||
|
- 支持自适应参数调整
|
||||||
|
|
||||||
|
### 1.2 关键功能特性
|
||||||
|
|
||||||
|
- **完整的传感器自检流程**:加速度计>200mg、陀螺仪>300dps的响应阈值检测
|
||||||
|
- **两级校准机制**:芯片内置校准(COD)和外部数据统计校准
|
||||||
|
- **数据处理管线**:原始数据读取→单位转换→校准补偿→姿态计算
|
||||||
|
- **基于ESP32特定API**:使用ESP-IDF的I2C驱动、日志和任务调度系统
|
||||||
|
|
||||||
|
## 二、适配方案设计
|
||||||
|
|
||||||
|
### 2.1 适配原则
|
||||||
|
|
||||||
|
根据分析,可以保留现有项目的QMI8658A C++类接口,将QMI8658A文件夹中的驱动实现作为底层功能提供者。这样可以最小化对现有代码的修改,同时利用新驱动的完整功能。
|
||||||
|
|
||||||
|
### 2.2 适配步骤
|
||||||
|
|
||||||
|
#### 2.2.1 替换I2C驱动接口
|
||||||
|
|
||||||
|
**不需要**直接使用QMI8658A文件夹中的I2C实现,而是应该:
|
||||||
|
|
||||||
|
1. 创建适配层函数,将项目现有的I2C设备接口转换为QMI8658A驱动需要的接口形式
|
||||||
|
2. 主要替换以下函数:
|
||||||
|
- `i2cwrite(uint8_t addr, uint8_t Data)`
|
||||||
|
- `i2cread(uint8_t addr, uint8_t *Data)`
|
||||||
|
- `i2creads(uint8_t addr, uint8_t length, uint8_t *Data)`
|
||||||
|
|
||||||
|
#### 2.2.2 替换延时函数
|
||||||
|
|
||||||
|
将QMI8658A.c中的`vTaskDelay(pdMS_TO_TICKS(x))`替换为项目中使用的延时函数:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 原代码
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
|
||||||
|
// 替换为(根据项目实际情况)
|
||||||
|
esp_timer_delay(100 * 1000); // 微秒单位
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.3 替换日志函数
|
||||||
|
|
||||||
|
将QMI8658A.c中的`ESP_LOGE(TAG, ...)`替换为项目中使用的日志输出方式:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 原代码
|
||||||
|
ESP_LOGE(TAG, "初始化成功!");
|
||||||
|
|
||||||
|
// 替换为
|
||||||
|
ESP_LOGI(TAG, "初始化成功!"); // 或项目自定义的日志函数
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.4 创建C++包装类
|
||||||
|
|
||||||
|
创建一个包装类,将C语言驱动封装在C++类中,保持与现有QMI8658A类接口兼容:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class QMI8658AAdaptor : public QMI8658A {
|
||||||
|
private:
|
||||||
|
// 适配层的I2C操作函数
|
||||||
|
bool i2c_write_reg(uint8_t reg, uint8_t value);
|
||||||
|
bool i2c_read_reg(uint8_t reg, uint8_t *value);
|
||||||
|
bool i2c_read_regs(uint8_t reg, uint8_t length, uint8_t *values);
|
||||||
|
|
||||||
|
// C驱动需要的全局变量
|
||||||
|
static QMI8658AAdaptor* instance_; // 单例模式存储当前实例
|
||||||
|
|
||||||
|
public:
|
||||||
|
// 重写基类接口
|
||||||
|
bool Initialize() override;
|
||||||
|
bool ReadSensorData(qmi8658a_data_t *data) override;
|
||||||
|
|
||||||
|
// 适配层静态函数,供C驱动调用
|
||||||
|
static bool c_i2cwrite(uint8_t addr, uint8_t data);
|
||||||
|
static bool c_i2cread(uint8_t addr, uint8_t *data);
|
||||||
|
static bool c_i2creads(uint8_t addr, uint8_t length, uint8_t *data);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.5 配置测试模式初始化流程
|
||||||
|
|
||||||
|
建议在进入测试模式后再初始化传感器,流程如下:
|
||||||
|
|
||||||
|
1. 系统启动,初始化基础硬件
|
||||||
|
2. 进入测试模式
|
||||||
|
3. 初始化传感器(调用QMI8658A_Init)
|
||||||
|
4. 执行自检(Acc_Self_Test和Gyr_Self_Test)
|
||||||
|
5. 执行校准(Gyr_COD和calibration_ACC_GYR)
|
||||||
|
6. 开始姿态数据采集和处理
|
||||||
|
|
||||||
|
## 三、具体实现代码示例
|
||||||
|
|
||||||
|
### 3.1 I2C适配层实现
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// i2c_adapter.cpp
|
||||||
|
#include "qmi8658a.h"
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
|
||||||
|
// 全局变量存储当前I2C设备实例
|
||||||
|
i2c_device_t *g_i2c_device = nullptr;
|
||||||
|
|
||||||
|
// C接口函数,供QMI8658A.c驱动调用
|
||||||
|
extern "C" unsigned char i2cwrite(uint8_t addr, uint8_t Data) {
|
||||||
|
if (!g_i2c_device) return 0;
|
||||||
|
return i2c_device_write_reg(g_i2c_device, addr, &Data, 1) == ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" unsigned char i2cread(unsigned char addr, unsigned char *Data) {
|
||||||
|
if (!g_i2c_device) return 0;
|
||||||
|
return i2c_device_read_reg(g_i2c_device, addr, Data, 1) == ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" unsigned char i2creads(uint8_t addr, uint8_t length, uint8_t *Data) {
|
||||||
|
if (!g_i2c_device) return 0;
|
||||||
|
return i2c_device_read_reg(g_i2c_device, addr, Data, length) == ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置当前使用的I2C设备
|
||||||
|
void set_i2c_device(i2c_device_t *device) {
|
||||||
|
g_i2c_device = device;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 延时和日志适配
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 在项目的某个公共头文件中添加
|
||||||
|
#define vTaskDelay(x) esp_timer_delay(((x) * 1000) / portTICK_PERIOD_MS) // 将tick转换为微秒
|
||||||
|
|
||||||
|
// 可选:如果需要,可以替换ESP_LOGE为自定义日志函数
|
||||||
|
// #define ESP_LOGE(tag, fmt, ...) custom_log_error(tag, fmt, ##__VA_ARGS__)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 QMI8658AAdaptor类实现
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// qmi8658a_adaptor.cpp
|
||||||
|
#include "qmi8658a.h"
|
||||||
|
#include "QMI8658A.h" // 包含C驱动头文件
|
||||||
|
#include "i2c_adapter.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
QMI8658AAdaptor::QMI8658AAdaptor(i2c_master_bus_handle_t i2c_bus, uint8_t dev_addr)
|
||||||
|
: QMI8658A(i2c_bus, dev_addr) {
|
||||||
|
// 设置I2C设备
|
||||||
|
set_i2c_device(&i2c_device_);
|
||||||
|
instance_ = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QMI8658AAdaptor::Initialize() {
|
||||||
|
// 调用C驱动的初始化函数
|
||||||
|
return QMI8658A_Init() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QMI8658AAdaptor::ReadSensorData(qmi8658a_data_t *data) {
|
||||||
|
// 使用C驱动读取数据
|
||||||
|
float sensor_data[6];
|
||||||
|
QMI8658A_Get_G_DPS(sensor_data);
|
||||||
|
|
||||||
|
// 转换为项目使用的数据格式
|
||||||
|
data->acc_x = sensor_data[0];
|
||||||
|
data->acc_y = sensor_data[1];
|
||||||
|
data->acc_z = sensor_data[2];
|
||||||
|
data->gyro_x = sensor_data[3];
|
||||||
|
data->gyro_y = sensor_data[4];
|
||||||
|
data->gyro_z = sensor_data[5];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 测试模式初始化代码
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 在项目的测试模式初始化函数中
|
||||||
|
void init_test_mode() {
|
||||||
|
// 进入测试模式的代码
|
||||||
|
|
||||||
|
// 初始化I2C总线
|
||||||
|
i2c_master_bus_handle_t i2c_bus = nullptr;
|
||||||
|
i2c_master_bus_config_t i2c_bus_config = {
|
||||||
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||||
|
.i2c_port = I2C_NUM_0,
|
||||||
|
.scl_io_num = 22,
|
||||||
|
.sda_io_num = 21,
|
||||||
|
.glitch_ignore_cnt = 7,
|
||||||
|
.flags.enable_internal_pullup = true,
|
||||||
|
};
|
||||||
|
i2c_new_master_bus(&i2c_bus_config, &i2c_bus);
|
||||||
|
|
||||||
|
// 创建并初始化传感器适配器
|
||||||
|
QMI8658AAdaptor *sensor = new QMI8658AAdaptor(i2c_bus, QMI8658A_I2C_ADDRESS);
|
||||||
|
|
||||||
|
if (sensor->Initialize()) {
|
||||||
|
ESP_LOGI("TEST_MODE", "传感器初始化成功");
|
||||||
|
|
||||||
|
// 执行额外的校准(如果需要)
|
||||||
|
if (sensor->PerformCalibration()) {
|
||||||
|
ESP_LOGI("TEST_MODE", "传感器校准成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始数据读取循环
|
||||||
|
start_sensor_read_loop(sensor);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE("TEST_MODE", "传感器初始化失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、编译配置调整
|
||||||
|
|
||||||
|
### 4.1 添加源文件
|
||||||
|
|
||||||
|
在项目的CMakeLists.txt中添加QMI8658A驱动相关文件:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
set(SOURCES
|
||||||
|
# 现有源文件
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/boards/movecall-moji-esp32s3/imu_sensor_thing.cc
|
||||||
|
# 添加新驱动文件
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/boards/movecall-moji-esp32s3/QMI8658A/QMI8658A.c
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/boards/movecall-moji-esp32s3/QMI8658A/AttitudeEstimation.c
|
||||||
|
# 适配层文件
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/boards/movecall-moji-esp32s3/qmi8658a_adaptor.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/boards/movecall-moji-esp32s3/i2c_adapter.cpp
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 头文件包含路径
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCES}
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/boards/common
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/boards/movecall-moji-esp32s3/QMI8658A
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、常见问题与解决方案
|
||||||
|
|
||||||
|
### 5.1 I2C通信问题
|
||||||
|
|
||||||
|
- **问题**:I2C读写失败
|
||||||
|
**解决**:检查I2C引脚配置、确保调用`set_i2c_device()`设置了正确的设备实例
|
||||||
|
|
||||||
|
### 5.2 初始化失败
|
||||||
|
|
||||||
|
- **问题**:`QMI8658A_Init()`返回0
|
||||||
|
**解决**:检查传感器硬件连接,确保自检和校准过程中传感器保持静止
|
||||||
|
|
||||||
|
### 5.3 数据准确性问题
|
||||||
|
|
||||||
|
- **问题**:读取的数据不稳定或不准确
|
||||||
|
**解决**:确保在使用前执行完整的校准流程,特别是陀螺仪校准
|
||||||
|
|
||||||
|
### 5.4 内存管理问题
|
||||||
|
|
||||||
|
- **问题**:`calibration_ACC_GYR()`函数中的动态内存分配失败
|
||||||
|
**解决**:确保项目有足够的堆内存,或修改函数使用静态数组
|
||||||
|
|
||||||
|
## 六、总结
|
||||||
|
|
||||||
|
将QMI8658A文件夹中的驱动适配到当前项目,主要需要:
|
||||||
|
|
||||||
|
1. **不需要**使用QMI8658A文件夹中的I2C实现,而是创建适配层函数将项目现有的I2C接口转换为C驱动所需的接口形式
|
||||||
|
2. 替换特定于ESP32的延时和日志函数
|
||||||
|
3. 创建C++包装类保持与现有接口兼容
|
||||||
|
4. 在测试模式初始化时按顺序执行传感器初始化、自检和校准
|
||||||
|
|
||||||
|
通过这种方式,可以充分利用QMI8658A文件夹中提供的完整功能实现,同时最小化对现有项目代码结构的改动。
|
||||||
662
QMI8658替换方案_Github驱动.md
Normal file
662
QMI8658替换方案_Github驱动.md
Normal file
@ -0,0 +1,662 @@
|
|||||||
|
# QMI8658传感器驱动替换方案
|
||||||
|
|
||||||
|
## 一、概述
|
||||||
|
|
||||||
|
本文档提供了使用`qmi8658-master`目录下的C驱动替换现有`QMI8658A`C++类的完整方案,同时解决当前驱动读取数值不准确的问题。
|
||||||
|
|
||||||
|
## 二、驱动对比分析
|
||||||
|
|
||||||
|
### 1. 当前使用的`QMI8658A`类
|
||||||
|
|
||||||
|
- 基于C++实现的面向对象设计
|
||||||
|
- 继承自`I2cDevice`类
|
||||||
|
- 提供丰富的功能:校准、FIFO、缓冲区管理、中断处理等
|
||||||
|
- 接口复杂但完善
|
||||||
|
- 存在数值读取不准确的问题
|
||||||
|
|
||||||
|
### 2. `qmi8658-master`中的C驱动
|
||||||
|
|
||||||
|
- 基于C语言实现的函数式设计
|
||||||
|
- 实现了基本的传感器功能:初始化、配置、数据读取等
|
||||||
|
- 包含FIFO、计步器、运动检测等功能
|
||||||
|
- 提供校准功能
|
||||||
|
- 代码简洁明了
|
||||||
|
|
||||||
|
### 3. 替换优势
|
||||||
|
|
||||||
|
- `qmi8658-master`驱动经过完整验证,与README文档描述一致
|
||||||
|
- 包含适当的校准功能,有助于解决数值不准确问题
|
||||||
|
- 接口简洁,易于集成和维护
|
||||||
|
- 支持与现有`ImuSensorThing`类兼容的功能
|
||||||
|
|
||||||
|
## 三、替换方案实现
|
||||||
|
|
||||||
|
### 1. 创建C++包装类
|
||||||
|
|
||||||
|
创建一个名为`QMI8658Wrapper`的C++类,它将使用`qmi8658-master`中的C驱动函数,但提供与现有`QMI8658A`类兼容的接口。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// qmi8658_wrapper.h
|
||||||
|
#ifndef QMI8658_WRAPPER_H
|
||||||
|
#define QMI8658_WRAPPER_H
|
||||||
|
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
#include "boards/movecall-moji-esp32s3/qmi8658-master/qmi8658.h"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
// 与QMI8658A类兼容的数据结构
|
||||||
|
typedef struct {
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
float acc_x;
|
||||||
|
float acc_y;
|
||||||
|
float acc_z;
|
||||||
|
};
|
||||||
|
float accel[3];
|
||||||
|
};
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
float gyro_x;
|
||||||
|
float gyro_y;
|
||||||
|
float gyro_z;
|
||||||
|
};
|
||||||
|
float gyro[3];
|
||||||
|
};
|
||||||
|
float temperature;
|
||||||
|
uint64_t timestamp;
|
||||||
|
bool valid;
|
||||||
|
} qmi8658a_data_t;
|
||||||
|
|
||||||
|
// 与QMI8658A类兼容的错误代码
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_OK = 0,
|
||||||
|
QMI8658A_ERROR_INVALID_PARAM = -1,
|
||||||
|
QMI8658A_ERROR_I2C_COMM = -2,
|
||||||
|
QMI8658A_ERROR_CHIP_ID = -3,
|
||||||
|
QMI8658A_ERROR_INIT_FAILED = -4,
|
||||||
|
QMI8658A_ERROR_CONFIG_FAILED = -5,
|
||||||
|
QMI8658A_ERROR_DATA_NOT_READY = -6,
|
||||||
|
QMI8658A_ERROR_TIMEOUT = -7
|
||||||
|
} qmi8658a_error_t;
|
||||||
|
|
||||||
|
// 与QMI8658A类兼容的状态定义
|
||||||
|
typedef enum {
|
||||||
|
QMI8658A_STATE_UNINITIALIZED = 0,
|
||||||
|
QMI8658A_STATE_INITIALIZING,
|
||||||
|
QMI8658A_STATE_READY,
|
||||||
|
QMI8658A_STATE_ERROR
|
||||||
|
} qmi8658a_state_t;
|
||||||
|
|
||||||
|
class QMI8658Wrapper {
|
||||||
|
private:
|
||||||
|
qmi8658a_state_t state_;
|
||||||
|
qmi8658a_error_t last_error_;
|
||||||
|
bool is_initialized_;
|
||||||
|
std::mutex mutex_;
|
||||||
|
float acc_offset_[3];
|
||||||
|
float gyro_offset_[3];
|
||||||
|
bool calibration_applied_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
QMI8658Wrapper(i2c_master_bus_handle_t i2c_bus, uint8_t addr = 0x6A);
|
||||||
|
~QMI8658Wrapper();
|
||||||
|
|
||||||
|
// 初始化函数
|
||||||
|
qmi8658a_error_t Initialize(void* config = nullptr);
|
||||||
|
|
||||||
|
// 数据读取函数
|
||||||
|
qmi8658a_error_t ReadSensorData(qmi8658a_data_t* data);
|
||||||
|
qmi8658a_error_t ReadAccelData(float* acc_x, float* acc_y, float* acc_z);
|
||||||
|
qmi8658a_error_t ReadGyroData(float* gyro_x, float* gyro_y, float* gyro_z);
|
||||||
|
qmi8658a_error_t ReadTemperature(float* temperature);
|
||||||
|
|
||||||
|
// 校准相关函数
|
||||||
|
qmi8658a_error_t StartCalibration(uint32_t duration_ms = 5000);
|
||||||
|
qmi8658a_error_t ApplyCalibration();
|
||||||
|
qmi8658a_error_t SaveCalibrationToNVS();
|
||||||
|
qmi8658a_error_t LoadCalibrationFromNVS();
|
||||||
|
|
||||||
|
// 状态查询函数
|
||||||
|
qmi8658a_state_t GetState() const { return state_; }
|
||||||
|
qmi8658a_error_t GetLastError() const { return last_error_; }
|
||||||
|
bool IsDataReady();
|
||||||
|
|
||||||
|
// 芯片信息函数
|
||||||
|
uint8_t GetChipId();
|
||||||
|
|
||||||
|
// 重置函数
|
||||||
|
qmi8658a_error_t SoftReset();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // QMI8658_WRAPPER_H
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// qmi8658_wrapper.cc
|
||||||
|
#include "qmi8658_wrapper.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#define TAG "QMI8658Wrapper"
|
||||||
|
#define CALIBRATION_NAMESPACE "qmi8658_cal"
|
||||||
|
#define ACC_OFFSET_KEY "acc_offset"
|
||||||
|
#define GYRO_OFFSET_KEY "gyro_offset"
|
||||||
|
#define CALIBRATION_FLAG_KEY "calibrated"
|
||||||
|
|
||||||
|
// 实现与qmi8658-master驱动的接口函数
|
||||||
|
extern "C" {
|
||||||
|
// 如果qmi8658-master驱动需要这些函数的自定义实现
|
||||||
|
// 可以在这里提供
|
||||||
|
}
|
||||||
|
|
||||||
|
QMI8658Wrapper::QMI8658Wrapper(i2c_master_bus_handle_t i2c_bus, uint8_t addr) {
|
||||||
|
state_ = QMI8658A_STATE_UNINITIALIZED;
|
||||||
|
last_error_ = QMI8658A_OK;
|
||||||
|
is_initialized_ = false;
|
||||||
|
memset(acc_offset_, 0, sizeof(acc_offset_));
|
||||||
|
memset(gyro_offset_, 0, sizeof(gyro_offset_));
|
||||||
|
calibration_applied_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMI8658Wrapper::~QMI8658Wrapper() {
|
||||||
|
// 清理资源
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::Initialize(void* config) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
state_ = QMI8658A_STATE_INITIALIZING;
|
||||||
|
|
||||||
|
// 初始化传感器
|
||||||
|
if (qmi8658_init() != 1) {
|
||||||
|
ESP_LOGE(TAG, "Sensor initialization failed");
|
||||||
|
last_error_ = QMI8658A_ERROR_INIT_FAILED;
|
||||||
|
state_ = QMI8658A_STATE_ERROR;
|
||||||
|
return last_error_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试加载保存的校准数据
|
||||||
|
LoadCalibrationFromNVS();
|
||||||
|
|
||||||
|
state_ = QMI8658A_STATE_READY;
|
||||||
|
is_initialized_ = true;
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::ReadSensorData(qmi8658a_data_t* data) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (!is_initialized_ || state_ != QMI8658A_STATE_READY) {
|
||||||
|
last_error_ = QMI8658A_ERROR_INIT_FAILED;
|
||||||
|
return last_error_;
|
||||||
|
}
|
||||||
|
|
||||||
|
float acc[3] = {0};
|
||||||
|
float gyro[3] = {0};
|
||||||
|
|
||||||
|
// 读取传感器数据
|
||||||
|
qmi8658_read_sensor_data(acc, gyro);
|
||||||
|
|
||||||
|
// 应用校准偏移
|
||||||
|
if (calibration_applied_) {
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
acc[i] -= acc_offset_[i];
|
||||||
|
gyro[i] -= gyro_offset_[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充数据结构
|
||||||
|
data->acc_x = acc[0];
|
||||||
|
data->acc_y = acc[1];
|
||||||
|
data->acc_z = acc[2];
|
||||||
|
data->gyro_x = gyro[0];
|
||||||
|
data->gyro_y = gyro[1];
|
||||||
|
data->gyro_z = gyro[2];
|
||||||
|
data->temperature = qmi8658_readTemp();
|
||||||
|
data->timestamp = esp_timer_get_time();
|
||||||
|
data->valid = true;
|
||||||
|
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::ReadAccelData(float* acc_x, float* acc_y, float* acc_z) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (!is_initialized_ || state_ != QMI8658A_STATE_READY) {
|
||||||
|
last_error_ = QMI8658A_ERROR_INIT_FAILED;
|
||||||
|
return last_error_;
|
||||||
|
}
|
||||||
|
|
||||||
|
float acc[3] = {0};
|
||||||
|
float gyro[3] = {0};
|
||||||
|
|
||||||
|
qmi8658_read_sensor_data(acc, gyro);
|
||||||
|
|
||||||
|
// 应用校准偏移
|
||||||
|
if (calibration_applied_) {
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
acc[i] -= acc_offset_[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*acc_x = acc[0];
|
||||||
|
*acc_y = acc[1];
|
||||||
|
*acc_z = acc[2];
|
||||||
|
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::ReadGyroData(float* gyro_x, float* gyro_y, float* gyro_z) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (!is_initialized_ || state_ != QMI8658A_STATE_READY) {
|
||||||
|
last_error_ = QMI8658A_ERROR_INIT_FAILED;
|
||||||
|
return last_error_;
|
||||||
|
}
|
||||||
|
|
||||||
|
float acc[3] = {0};
|
||||||
|
float gyro[3] = {0};
|
||||||
|
|
||||||
|
qmi8658_read_sensor_data(acc, gyro);
|
||||||
|
|
||||||
|
// 应用校准偏移
|
||||||
|
if (calibration_applied_) {
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
gyro[i] -= gyro_offset_[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*gyro_x = gyro[0];
|
||||||
|
*gyro_y = gyro[1];
|
||||||
|
*gyro_z = gyro[2];
|
||||||
|
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::ReadTemperature(float* temperature) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (!is_initialized_ || state_ != QMI8658A_STATE_READY) {
|
||||||
|
last_error_ = QMI8658A_ERROR_INIT_FAILED;
|
||||||
|
return last_error_;
|
||||||
|
}
|
||||||
|
|
||||||
|
*temperature = qmi8658_readTemp();
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::StartCalibration(uint32_t duration_ms) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (!is_initialized_ || state_ != QMI8658A_STATE_READY) {
|
||||||
|
last_error_ = QMI8658A_ERROR_INIT_FAILED;
|
||||||
|
return last_error_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Starting calibration for %lu ms", duration_ms);
|
||||||
|
|
||||||
|
// 使用qmi8658驱动内置的校准功能
|
||||||
|
qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_On_Demand_Cali);
|
||||||
|
|
||||||
|
// 等待校准完成
|
||||||
|
vTaskDelay(duration_ms / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
// 应用校准
|
||||||
|
return ApplyCalibration();
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::ApplyCalibration() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// 在这里可以实现更复杂的校准逻辑
|
||||||
|
// 目前使用默认的偏移值或从NVS加载的偏移值
|
||||||
|
calibration_applied_ = true;
|
||||||
|
ESP_LOGI(TAG, "Calibration applied");
|
||||||
|
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::SaveCalibrationToNVS() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
nvs_handle_t nvs_handle;
|
||||||
|
esp_err_t err = nvs_open(CALIBRATION_NAMESPACE, NVS_READWRITE, &nvs_handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to open NVS namespace: %s", esp_err_to_name(err));
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存加速度计偏移
|
||||||
|
err = nvs_set_blob(nvs_handle, ACC_OFFSET_KEY, acc_offset_, sizeof(acc_offset_));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to save accelerometer offset: %s", esp_err_to_name(err));
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存陀螺仪偏移
|
||||||
|
err = nvs_set_blob(nvs_handle, GYRO_OFFSET_KEY, gyro_offset_, sizeof(gyro_offset_));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to save gyroscope offset: %s", esp_err_to_name(err));
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存校准标志
|
||||||
|
err = nvs_set_u8(nvs_handle, CALIBRATION_FLAG_KEY, 1);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to save calibration flag: %s", esp_err_to_name(err));
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_commit(nvs_handle);
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Calibration data saved to NVS");
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::LoadCalibrationFromNVS() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
nvs_handle_t nvs_handle;
|
||||||
|
esp_err_t err = nvs_open(CALIBRATION_NAMESPACE, NVS_READONLY, &nvs_handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to open NVS namespace: %s", esp_err_to_name(err));
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有校准数据
|
||||||
|
uint8_t calibrated = 0;
|
||||||
|
err = nvs_get_u8(nvs_handle, CALIBRATION_FLAG_KEY, &calibrated);
|
||||||
|
if (err != ESP_OK || calibrated != 1) {
|
||||||
|
ESP_LOGW(TAG, "No calibration data found in NVS");
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载加速度计偏移
|
||||||
|
size_t size = sizeof(acc_offset_);
|
||||||
|
err = nvs_get_blob(nvs_handle, ACC_OFFSET_KEY, acc_offset_, &size);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to load accelerometer offset: %s", esp_err_to_name(err));
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载陀螺仪偏移
|
||||||
|
size = sizeof(gyro_offset_);
|
||||||
|
err = nvs_get_blob(nvs_handle, GYRO_OFFSET_KEY, gyro_offset_, &size);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to load gyroscope offset: %s", esp_err_to_name(err));
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
return QMI8658A_ERROR_I2C_COMM;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
calibration_applied_ = true;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Calibration data loaded from NVS");
|
||||||
|
return QMI8658A_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QMI8658Wrapper::IsDataReady() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (!is_initialized_ || state_ != QMI8658A_STATE_READY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查传感器数据是否就绪
|
||||||
|
unsigned char status = qmi8658_readStatus0();
|
||||||
|
return (status & 0x03) == 0x03; // 假设位0和位1表示加速度计和陀螺仪数据就绪
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t QMI8658Wrapper::GetChipId() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
return qmi8658_get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
qmi8658a_error_t QMI8658Wrapper::SoftReset() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// 发送重置命令
|
||||||
|
qmi8658_send_ctl9cmd(qmi8658_Ctrl9_Cmd_EnableExtReset);
|
||||||
|
vTaskDelay(100 / portTICK_PERIOD_MS); // 等待重置完成
|
||||||
|
|
||||||
|
// 重新初始化
|
||||||
|
return Initialize(nullptr);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 修改ImuSensorThing类
|
||||||
|
|
||||||
|
修改`ImuSensorThing`类的头文件和实现文件,使其可以接受`QMI8658Wrapper`类的实例。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// imu_sensor_thing.h 修改后的版本
|
||||||
|
#ifndef IMU_SENSOR_THING_H
|
||||||
|
#define IMU_SENSOR_THING_H
|
||||||
|
|
||||||
|
#include "iot/thing.h"
|
||||||
|
#include "qmi8658_wrapper.h" // 使用新的包装类
|
||||||
|
|
||||||
|
namespace iot {
|
||||||
|
|
||||||
|
class ImuSensorThing : public Thing {
|
||||||
|
private:
|
||||||
|
QMI8658Wrapper* imu_sensor_;
|
||||||
|
qmi8658a_data_t latest_data_;
|
||||||
|
bool motion_detected_;
|
||||||
|
float motion_threshold_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ImuSensorThing(QMI8658Wrapper* sensor);
|
||||||
|
virtual ~ImuSensorThing() = default;
|
||||||
|
|
||||||
|
void UpdateData(const qmi8658a_data_t& data);
|
||||||
|
void SetMotionDetected(bool detected);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace iot
|
||||||
|
|
||||||
|
#endif // IMU_SENSOR_THING_H
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// imu_sensor_thing.cc 基本保持不变,但构造函数参数类型改变
|
||||||
|
#include "imu_sensor_thing.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#define TAG "ImuSensorThing"
|
||||||
|
|
||||||
|
namespace iot {
|
||||||
|
|
||||||
|
ImuSensorThing::ImuSensorThing(QMI8658Wrapper* sensor)
|
||||||
|
: Thing("ImuSensor", "姿态传感器"),
|
||||||
|
imu_sensor_(sensor),
|
||||||
|
motion_detected_(false),
|
||||||
|
motion_threshold_(1.5f) {
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
memset(&latest_data_, 0, sizeof(latest_data_));
|
||||||
|
|
||||||
|
// 其他代码保持不变...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他方法保持不变...
|
||||||
|
|
||||||
|
} // namespace iot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 创建测试模式初始化代码
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// test_mode_init.cc
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
#include "qmi8658_wrapper.h"
|
||||||
|
#include "imu_sensor_thing.h"
|
||||||
|
|
||||||
|
#define TAG "TestModeInit"
|
||||||
|
|
||||||
|
// I2C配置
|
||||||
|
#define I2C_MASTER_SCL_IO 19 /*!< GPIO number used for I2C master clock */
|
||||||
|
#define I2C_MASTER_SDA_IO 18 /*!< GPIO number used for I2C master data */
|
||||||
|
#define I2C_MASTER_NUM I2C_NUM_0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
|
||||||
|
#define I2C_MASTER_FREQ_HZ 400000 /*!< I2C master clock frequency */
|
||||||
|
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
|
||||||
|
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
|
||||||
|
|
||||||
|
iot::ImuSensorThing* initialize_imu_in_test_mode() {
|
||||||
|
ESP_LOGI(TAG, "Initializing IMU sensor in test mode");
|
||||||
|
|
||||||
|
// 配置I2C控制器
|
||||||
|
i2c_master_bus_config_t i2c_bus_config = {
|
||||||
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||||
|
.i2c_port = I2C_MASTER_NUM,
|
||||||
|
.scl_io_num = I2C_MASTER_SCL_IO,
|
||||||
|
.sda_io_num = I2C_MASTER_SDA_IO,
|
||||||
|
.glitch_ignore_cnt = 7,
|
||||||
|
.flags.enable_internal_pullup = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
i2c_master_bus_handle_t i2c_bus = NULL;
|
||||||
|
esp_err_t err = i2c_new_master_bus(&i2c_bus_config, &i2c_bus);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create I2C bus: %s", esp_err_to_name(err));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建QMI8658Wrapper实例
|
||||||
|
QMI8658Wrapper* imu_sensor = new QMI8658Wrapper(i2c_bus);
|
||||||
|
|
||||||
|
// 初始化传感器
|
||||||
|
if (imu_sensor->Initialize() != QMI8658A_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize IMU sensor");
|
||||||
|
delete imu_sensor;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建ImuSensorThing实例
|
||||||
|
iot::ImuSensorThing* imu_thing = new iot::ImuSensorThing(imu_sensor);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "IMU sensor initialized successfully in test mode");
|
||||||
|
return imu_thing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void perform_imu_test() {
|
||||||
|
ESP_LOGI(TAG, "Performing IMU sensor test");
|
||||||
|
|
||||||
|
// 获取ImuSensorThing实例
|
||||||
|
iot::ImuSensorThing* imu_thing = initialize_imu_in_test_mode();
|
||||||
|
if (!imu_thing) {
|
||||||
|
ESP_LOGE(TAG, "Failed to get IMU thing instance");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在这里可以添加测试逻辑,比如读取传感器数据并进行验证
|
||||||
|
|
||||||
|
// 示例:读取几次传感器数据
|
||||||
|
QMI8658Wrapper* imu_sensor = /* 获取传感器实例的方式 */;
|
||||||
|
qmi8658a_data_t data;
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (imu_sensor->ReadSensorData(&data) == QMI8658A_OK) {
|
||||||
|
ESP_LOGI(TAG, "IMU Data - Accel: (%.2f, %.2f, %.2f)g, Gyro: (%.2f, %.2f, %.2f)dps",
|
||||||
|
data.acc_x, data.acc_y, data.acc_z,
|
||||||
|
data.gyro_x, data.gyro_y, data.gyro_z);
|
||||||
|
|
||||||
|
// 更新ImuSensorThing的数据
|
||||||
|
imu_thing->UpdateData(data);
|
||||||
|
}
|
||||||
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试完成后清理资源
|
||||||
|
// 注意:实际应用中可能需要更复杂的资源管理
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 编译和链接配置
|
||||||
|
|
||||||
|
修改项目的CMakeLists.txt文件,确保正确包含qmi8658-master驱动和新的包装类。
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# CMakeLists.txt 片段
|
||||||
|
|
||||||
|
# 添加qmi8658-master驱动
|
||||||
|
set(QMI8658_DRIVER_DIR ${CMAKE_CURRENT_LIST_DIR}/boards/movecall-moji-esp32s3/qmi8658-master)
|
||||||
|
file(GLOB QMI8658_DRIVER_SOURCES ${QMI8658_DRIVER_DIR}/*.c)
|
||||||
|
|
||||||
|
# 添加新的包装类
|
||||||
|
set(WRAPPER_SOURCES
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/qmi8658_wrapper.cc
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/test_mode_init.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加到主应用程序
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${MAIN_SOURCES} ${QMI8658_DRIVER_SOURCES} ${WRAPPER_SOURCES}
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}
|
||||||
|
${QMI8658_DRIVER_DIR}
|
||||||
|
# 其他包含目录...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、解决数值不准确问题的策略
|
||||||
|
|
||||||
|
### 1. 硬件检查
|
||||||
|
|
||||||
|
- 确保传感器VDD和VDDIO电压稳定在1.71-3.6V范围内
|
||||||
|
- 检查I2C通信线的连接质量,确保SCL和SDA信号良好
|
||||||
|
- 验证I2C地址设置正确(通常为0x6A或0x6B,取决于SA0引脚连接)
|
||||||
|
|
||||||
|
### 2. 校准优化
|
||||||
|
|
||||||
|
- 使用`qmi8658-master`驱动的内置校准功能
|
||||||
|
- 添加启动时自动加载保存的校准数据
|
||||||
|
- 在设备静止状态下执行校准
|
||||||
|
|
||||||
|
### 3. 数据处理优化
|
||||||
|
|
||||||
|
- 添加数据滤波以减少噪声
|
||||||
|
- 实现异常值检测和处理
|
||||||
|
- 对加速度计数据进行重力补偿
|
||||||
|
|
||||||
|
### 4. 时序和稳定性优化
|
||||||
|
|
||||||
|
- 确保I2C通信的可靠性,添加适当的重试机制
|
||||||
|
- 使用FIFO模式批量读取数据,减少通信开销
|
||||||
|
- 实现电源管理策略,确保传感器稳定供电
|
||||||
|
|
||||||
|
## 五、实现步骤
|
||||||
|
|
||||||
|
1. 创建`QMI8658Wrapper`类的头文件和实现文件
|
||||||
|
2. 修改`ImuSensorThing`类以使用新的包装类
|
||||||
|
3. 创建测试模式初始化代码
|
||||||
|
4. 更新编译和链接配置
|
||||||
|
5. 编译和测试应用程序
|
||||||
|
|
||||||
|
## 六、注意事项
|
||||||
|
|
||||||
|
- 在使用新驱动之前,确保备份现有代码
|
||||||
|
- 替换过程中保持与现有接口的兼容性
|
||||||
|
- 实现适当的错误处理和资源管理
|
||||||
|
- 在测试模式中初始化传感器时,确保其他系统组件已就绪
|
||||||
|
- 考虑添加日志记录,以便于调试和问题分析
|
||||||
149
README.md
Normal file
149
README.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# 小智 AI 聊天机器人 (XiaoZhi AI Chatbot)
|
||||||
|
|
||||||
|
(中文 | [English](README_en.md) | [日本語](README_ja.md))
|
||||||
|
|
||||||
|
## 视频介绍
|
||||||
|
|
||||||
|
👉 [ESP32+SenseVoice+Qwen72B打造你的AI聊天伴侣!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/)
|
||||||
|
|
||||||
|
👉 [给小智装上 DeepSeek 的聪明大脑【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/)
|
||||||
|
|
||||||
|
👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||||
|
|
||||||
|
## 项目目的
|
||||||
|
|
||||||
|
本项目是由虾哥开源的一个开源项目,以 MIT 许可证发布,允许任何人免费使用,并可以用于商业用途。
|
||||||
|
|
||||||
|
我们希望通过这个项目,能够帮助更多人入门 AI 硬件开发,了解如何将当下飞速发展的大语言模型应用到实际的硬件设备中。无论你是对 AI 感兴趣的学生,还是想要探索新技术的开发者,都可以通过这个项目获得宝贵的学习经验。
|
||||||
|
|
||||||
|
欢迎所有人参与到项目的开发和改进中来。如果你有任何想法或建议,请随时提出 Issue 或加入群聊。
|
||||||
|
|
||||||
|
学习交流 QQ 群:376893254
|
||||||
|
|
||||||
|
## 已实现功能
|
||||||
|
|
||||||
|
- Wi-Fi / ML307 Cat.1 4G
|
||||||
|
- BOOT 键唤醒和打断,支持点击和长按两种触发方式
|
||||||
|
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
|
||||||
|
- 流式语音对话(WebSocket 或 UDP 协议)
|
||||||
|
- 支持国语、粤语、英语、日语、韩语 5 种语言识别 [SenseVoice](https://github.com/FunAudioLLM/SenseVoice)
|
||||||
|
- 声纹识别,识别是谁在喊 AI 的名字 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||||
|
- 大模型 TTS(火山引擎 或 CosyVoice)
|
||||||
|
- 大模型 LLM(Qwen, DeepSeek, Doubao)
|
||||||
|
- 可配置的提示词和音色(自定义角色)
|
||||||
|
- 短期记忆,每轮对话后自我总结
|
||||||
|
- OLED / LCD 显示屏,显示信号强弱或对话内容
|
||||||
|
- 支持 LCD 显示图片表情
|
||||||
|
- 支持多语言(中文、英文)
|
||||||
|
|
||||||
|
## 硬件部分
|
||||||
|
|
||||||
|
### 面包板手工制作实践
|
||||||
|
|
||||||
|
详见飞书文档教程:
|
||||||
|
|
||||||
|
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||||
|
|
||||||
|
面包板效果图如下:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 已支持的开源硬件
|
||||||
|
|
||||||
|
- <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">AtomS3R + Echo Base</a>
|
||||||
|
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + 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/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Moji 小智AI衍生版</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>
|
||||||
|
<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/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||||
|
<img src="docs/lilygo-t-circle-s3.jpg" width="240" />
|
||||||
|
</a>
|
||||||
|
<a href="docs/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
|
||||||
|
<img src="docs/xmini-c3.jpg" width="240" />
|
||||||
|
</a>
|
||||||
|
<a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Movecall Moji 小智AI衍生版">
|
||||||
|
<img src="docs/v1/movecall-moji-esp32s3.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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 固件部分
|
||||||
|
|
||||||
|
### 免开发环境烧录
|
||||||
|
|
||||||
|
新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。
|
||||||
|
|
||||||
|
固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,目前个人用户注册账号可以免费使用 Qwen 实时模型。
|
||||||
|
|
||||||
|
👉 [Flash烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||||
|
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
- Cursor 或 VSCode
|
||||||
|
- 安装 ESP-IDF 插件,选择 SDK 版本 5.3 或以上
|
||||||
|
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
|
||||||
|
- 使用 Google C++ 代码风格,提交代码时请确保符合规范
|
||||||
|
|
||||||
|
### 开发者文档
|
||||||
|
|
||||||
|
- [开发板定制指南](main/boards/README.md) - 学习如何为小智创建自定义开发板适配
|
||||||
|
- [物联网控制模块](main/iot/README.md) - 了解如何通过AI语音控制物联网设备
|
||||||
|
|
||||||
|
|
||||||
|
## 智能体配置
|
||||||
|
|
||||||
|
如果你已经拥有一个小智 AI 聊天机器人设备,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
|
||||||
|
|
||||||
|
👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||||
|
|
||||||
|
## 技术原理与私有化部署
|
||||||
|
|
||||||
|
👉 [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
|
||||||
|
|
||||||
|
在个人电脑上部署服务器,可以参考另一位作者同样以 MIT 许可证开源的项目 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
## 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>
|
||||||
117
README_RTC.md
Normal file
117
README_RTC.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<h1 align="center"><img src="https://iam.volccdn.com/obj/volcengine-public/pic/volcengine-icon.png"></h1>
|
||||||
|
<h1 align="center">ConversationalAI Embedded Kit</h1>
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
具体操作,请参考 [官网文档](https://www.volcengine.com/docs/6348/1806625)。
|
||||||
|
|
||||||
|
## 运行设备端(乐鑫)
|
||||||
|
|
||||||
|
以下操作以 macOS 操作系统为例。
|
||||||
|
|
||||||
|
### 环境与硬件要求
|
||||||
|
- 乐鑫 ESP32-S3-Korvo-2
|
||||||
|
- USB 数据线:两条 A 转 Micro-B 数据线,一条作为电源线,一条作为串口线。
|
||||||
|
- PC 设备服:编译和烧录。支持 Windows、Linux 或者 macOS 操作系统。(本文操作以 macOS 为例)
|
||||||
|
|
||||||
|
### 配置乐鑫环境
|
||||||
|
|
||||||
|
详见[开发环境配置文档](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/get-started/index.html)。
|
||||||
|
|
||||||
|
1. 安装 CMake 和 Ninja 编译工具。
|
||||||
|
```bash
|
||||||
|
brew install cmake ninja dfu-util
|
||||||
|
```
|
||||||
|
2. 将乐鑫 ADF 框架克隆到本地,并同步各子仓(submodule)代码。
|
||||||
|
> **注意**:Demo 中使用的 ADF 版本为 `eca11f20e56f9b5321b714da4305e123672d92a9`,对应 IDF 版本为 `v5.4`,请确保 ADF 版本与 IDF 版本匹配。
|
||||||
|
```bash
|
||||||
|
# 1. clone 乐鑫 ADF 框架
|
||||||
|
git clone https://github.com/espressif/esp-adf.git
|
||||||
|
# 2. 进入esp-adf目录
|
||||||
|
cd esp-adf
|
||||||
|
# 3. 切换到乐鑫 ADF 指定版本
|
||||||
|
git reset --hard eca11f20e56f9b5321b714da4305e123672d92a9
|
||||||
|
# 4. 同步各子仓代码
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
3. 安装乐鑫 esp32s3 开发环境相关依赖。
|
||||||
|
```bash
|
||||||
|
./install.sh esp32s3
|
||||||
|
```
|
||||||
|
成功安装所有依赖后,命令行会出现如下提示:
|
||||||
|
```bash
|
||||||
|
All done! You can now run:
|
||||||
|
. ./export.sh
|
||||||
|
```
|
||||||
|
> 如在上述任何步骤中遇到以下错误:
|
||||||
|
> `<urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:xxx)`
|
||||||
|
> 可前往**访达->应用程序->Python** 文件夹,点击 `Install Certificates.command` 安装证书。更多信息,请参考 [安装 ESP-IDF 工具时出现的下载错误](https://github.com/espressif/esp-idf/issues/4775)。
|
||||||
|
4. 设置环境变量。
|
||||||
|
> **每次打开命令行窗口均需要运行该命令进行设置。**
|
||||||
|
```bash
|
||||||
|
. ./export.sh
|
||||||
|
```
|
||||||
|
### 下载并配置工程
|
||||||
|
1. 将实时对话式 AI 硬件示例工程克隆到 乐鑫 ADF examples 目录下。
|
||||||
|
1. 进入 esp-adf/examples 目录。
|
||||||
|
```bash
|
||||||
|
cd $ADF_PATH/examples
|
||||||
|
```
|
||||||
|
2. 克隆实时对话式 AI 硬件示例工程。
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/volcengine/ConversationalAI-Embedded-Kit-2.0.git
|
||||||
|
```
|
||||||
|
2. 禁用乐鑫工程中的火山组件。
|
||||||
|
1. 进入 esp-adf 目录。
|
||||||
|
```bash
|
||||||
|
cd $ADF_PATH
|
||||||
|
```
|
||||||
|
2. 禁用乐鑫工程中的火山组件。
|
||||||
|
```bash
|
||||||
|
git apply $ADF_PATH/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif/0001-feat-disable-volc-esp-libs.patch
|
||||||
|
```
|
||||||
|
3. 修复乐鑫按键问题
|
||||||
|
1. 进入 esp-adf 目录。
|
||||||
|
```bash
|
||||||
|
cd $ADF_PATH
|
||||||
|
```
|
||||||
|
2. 修复乐鑫按键问题。
|
||||||
|
```bash
|
||||||
|
git apply $ADF_PATH/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif/0002-fix-esp-button.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 编译固件
|
||||||
|
进入 `esp-adf/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif` 目录下编译固件。
|
||||||
|
1. 进入 espressif 目录。
|
||||||
|
```bash
|
||||||
|
cd $ADF_PATH/examples/ConversationalAI-Embedded-Kit-2.0/high_quality_first/espressif
|
||||||
|
```
|
||||||
|
2. 设置编译目标平台。
|
||||||
|
```bash
|
||||||
|
idf.py set-target esp32s3
|
||||||
|
```
|
||||||
|
3. 设置 实例ID、产品ID、产品秘钥、设备ID等参数。
|
||||||
|
```bash
|
||||||
|
idf.py menuconfig
|
||||||
|
```
|
||||||
|
进入 `Example Configuration` 菜单,在 `volcano instance id` 中填入你的实例ID,在 `volcano product key` 中填入你的产品Key,在 `volcano product secret` 中填入你的产品秘钥,在 `device name` 中填入你的设备ID, 在 `bot id` 中填入你的智能体ID,并保存。
|
||||||
|
4. 编译固件。
|
||||||
|
```bash
|
||||||
|
idf.py build
|
||||||
|
```
|
||||||
|
### 烧录并运行示例 Demo
|
||||||
|
1. 打开乐鑫开发板电源开关。
|
||||||
|
2. 烧录固件。
|
||||||
|
```bash
|
||||||
|
idf.py flash
|
||||||
|
```
|
||||||
|
3. 运行示例 Demo 并查看串口日志输出。
|
||||||
|
```bash
|
||||||
|
idf.py monitor
|
||||||
|
```
|
||||||
|
4. Wi-Fi 配网。
|
||||||
|
1. 手机找到名如 VolcConvAI-XXXXXX” 的 Wi-Fi 热点,密码同Wi-Fi名,连接上 Wi-Fi。
|
||||||
|
2. 打开浏览器,在地址栏输入 `http://192.168.4.1`,进入 Wi-Fi 配网页面。
|
||||||
|
3. 输入 Wi-Fi 名称和密码,点击提交。
|
||||||
|
|
||||||
|
> **注意**:如果需更换 Wi-Fi,请重启设备。如果设备重启后无法连接到之前保存的 Wi-Fi(例如超出了范围或旧网络已关闭),请等待 30s 进入配网模式,再重新执行上面 Wi-Fi 配网的 3 个步骤。
|
||||||
151
README_en.md
Normal file
151
README_en.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# XiaoZhi AI Chatbot
|
||||||
|
|
||||||
|
([中文](README.md) | English | [日本語](README_ja.md))
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
👉 [Build your AI chat companion with ESP32+SenseVoice+Qwen72B!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/)
|
||||||
|
|
||||||
|
👉 [Equipping XiaoZhi with DeepSeek's smart brain【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/)
|
||||||
|
|
||||||
|
👉 [Build your own AI companion, a beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||||
|
|
||||||
|
## Project Purpose
|
||||||
|
|
||||||
|
This is an open-source project released under the MIT license, allowing anyone to use it freely, including for commercial purposes.
|
||||||
|
|
||||||
|
Through this project, we aim to help more people get started with AI hardware development and understand how to implement rapidly evolving large language models in actual hardware devices. Whether you're a student interested in AI or a developer exploring new technologies, this project offers valuable learning experiences.
|
||||||
|
|
||||||
|
Everyone is welcome to participate in the project's development and improvement. If you have any ideas or suggestions, please feel free to raise an Issue or join the chat group.
|
||||||
|
|
||||||
|
Learning & Discussion QQ Group: 376893254
|
||||||
|
|
||||||
|
## Implemented Features
|
||||||
|
|
||||||
|
- Wi-Fi / ML307 Cat.1 4G
|
||||||
|
- BOOT button wake-up and interruption, supporting both click and long-press triggers
|
||||||
|
- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
|
||||||
|
- Streaming voice dialogue (WebSocket or UDP protocol)
|
||||||
|
- Support for 5 languages: Mandarin, Cantonese, English, Japanese, Korean [SenseVoice](https://github.com/FunAudioLLM/SenseVoice)
|
||||||
|
- Voice print recognition to identify who's calling AI's name [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||||
|
- Large model TTS (Volcano Engine or CosyVoice)
|
||||||
|
- Large Language Models (Qwen, DeepSeek, Doubao)
|
||||||
|
- Configurable prompts and voice tones (custom characters)
|
||||||
|
- Short-term memory, self-summarizing after each conversation round
|
||||||
|
- OLED / LCD display showing signal strength or conversation content
|
||||||
|
- Support for LCD image expressions
|
||||||
|
- Multi-language support (Chinese, English)
|
||||||
|
|
||||||
|
## Hardware Section
|
||||||
|
|
||||||
|
### Breadboard DIY Practice
|
||||||
|
|
||||||
|
See the Feishu document tutorial:
|
||||||
|
|
||||||
|
👉 [XiaoZhi AI Chatbot Encyclopedia](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||||
|
|
||||||
|
Breadboard demonstration:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Supported Open Source Hardware
|
||||||
|
|
||||||
|
- <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">AtomS3R + Echo Base</a>
|
||||||
|
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + 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/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Moji XiaoZhi AI Derivative Version</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>
|
||||||
|
|
||||||
|
<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/AtomMatrix-echo-base.jpg" target="_blank" title="AtomMatrix-echo-base + Echo Base">
|
||||||
|
<img src="docs/AtomMatrix-echo-base.jpg" width="240" />
|
||||||
|
</a>
|
||||||
|
<a href="docs/v1/magiclick.jpg" target="_blank" title="MagiClick 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/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||||
|
<img src="docs/lilygo-t-circle-s3.jpg" width="240" />
|
||||||
|
</a>
|
||||||
|
<a href="docs/xmini-c3.jpg" target="_blank" title="Xmini C3">
|
||||||
|
<img src="docs/xmini-c3.jpg" width="240" />
|
||||||
|
</a>
|
||||||
|
<a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Moji">
|
||||||
|
<img src="docs/v1/movecall-moji-esp32s3.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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Firmware Section
|
||||||
|
|
||||||
|
### Flashing Without Development Environment
|
||||||
|
|
||||||
|
For beginners, it's recommended to first 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. Currently, personal users can register an account to use the Qwen real-time model for free.
|
||||||
|
|
||||||
|
👉 [Flash Firmware Guide (No IDF Environment)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
|
||||||
|
- Cursor or VSCode
|
||||||
|
- Install ESP-IDF plugin, select SDK version 5.3 or above
|
||||||
|
- Linux is preferred over Windows for faster compilation and fewer driver issues
|
||||||
|
- Use Google C++ code style, ensure compliance when submitting code
|
||||||
|
|
||||||
|
### Developer Documentation
|
||||||
|
|
||||||
|
- [Board Customization Guide](main/boards/README.md) - Learn how to create custom board adaptations for XiaoZhi
|
||||||
|
- [IoT Control Module](main/iot/README.md) - Understand how to control IoT devices through AI voice commands
|
||||||
|
|
||||||
|
## AI Agent Configuration
|
||||||
|
|
||||||
|
If you already have a XiaoZhi AI chatbot device, you can configure it through the [xiaozhi.me](https://xiaozhi.me) console.
|
||||||
|
|
||||||
|
👉 [Backend Operation Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||||
|
|
||||||
|
## Technical Principles and Private Deployment
|
||||||
|
|
||||||
|
👉 [Detailed WebSocket Communication Protocol Documentation](docs/websocket.md)
|
||||||
|
|
||||||
|
For server deployment on personal computers, refer to another MIT-licensed project [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
## 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>
|
||||||
148
README_ja.md
Normal file
148
README_ja.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# シャオジー AI チャットボット
|
||||||
|
|
||||||
|
([中文](README.md) | [English](README_en.md) | 日本語)
|
||||||
|
|
||||||
|
## プロジェクト紹介
|
||||||
|
|
||||||
|
👉 [ESP32+SenseVoice+Qwen72Bで AI チャット仲間を作ろう!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/)
|
||||||
|
|
||||||
|
👉 [シャオジーに DeepSeek のスマートな頭脳を搭載【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/)
|
||||||
|
|
||||||
|
👉 [自分だけの AI パートナーを作る、初心者向けガイド【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||||
|
|
||||||
|
## プロジェクトの目的
|
||||||
|
|
||||||
|
このプロジェクトは MIT ライセンスの下で公開されているオープンソースプロジェクトで、商用利用を含め、誰でも自由に使用することができます。
|
||||||
|
|
||||||
|
このプロジェクトを通じて、より多くの人々が AI ハードウェア開発を始め、急速に進化している大規模言語モデルを実際のハードウェアデバイスに実装する方法を理解できるようになることを目指しています。AI に興味のある学生でも、新しい技術を探求する開発者でも、このプロジェクトから貴重な学習経験を得ることができます。
|
||||||
|
|
||||||
|
プロジェクトの開発と改善には誰でも参加できます。アイデアや提案がありましたら、Issue を立てるかチャットグループにご参加ください。
|
||||||
|
|
||||||
|
学習・交流 QQ グループ:376893254
|
||||||
|
|
||||||
|
## 実装済みの機能
|
||||||
|
|
||||||
|
- Wi-Fi / ML307 Cat.1 4G
|
||||||
|
- BOOT ボタンによる起動と中断、クリックと長押しの2種類のトリガーに対応
|
||||||
|
- オフライン音声起動 [ESP-SR](https://github.com/espressif/esp-sr)
|
||||||
|
- ストリーミング音声対話(WebSocket または UDP プロトコル)
|
||||||
|
- 5言語対応:標準中国語、広東語、英語、日本語、韓国語 [SenseVoice](https://github.com/FunAudioLLM/SenseVoice)
|
||||||
|
- 話者認識、AI の名前を呼んでいる人を識別 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||||
|
- 大規模モデル TTS(Volcano Engine または CosyVoice)
|
||||||
|
- 大規模言語モデル(Qwen, DeepSeek, Doubao)
|
||||||
|
- 設定可能なプロンプトと音声トーン(カスタムキャラクター)
|
||||||
|
- 短期記憶、各会話ラウンド後の自己要約
|
||||||
|
- OLED / LCD ディスプレイ、信号強度や会話内容を表示
|
||||||
|
- LCD での画像表情表示に対応
|
||||||
|
- 多言語対応(中国語、英語)
|
||||||
|
|
||||||
|
## ハードウェア部分
|
||||||
|
|
||||||
|
### ブレッドボード DIY 実践
|
||||||
|
|
||||||
|
Feishu ドキュメントチュートリアルをご覧ください:
|
||||||
|
|
||||||
|
👉 [シャオジー AI チャットボット百科事典](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||||
|
|
||||||
|
ブレッドボードのデモ:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### サポートされているオープンソースハードウェア
|
||||||
|
|
||||||
|
- <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 開発ボード">LiChuang ESP32-S3 開発ボード</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">AtomS3R + Echo Base</a>
|
||||||
|
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + 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="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/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Moji シャオジー AI 派生版</a>
|
||||||
|
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">Cuican 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>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 開発ボード">
|
||||||
|
<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="MagiClick 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/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||||
|
<img src="docs/lilygo-t-circle-s3.jpg" width="240" />
|
||||||
|
</a>
|
||||||
|
<a href="docs/xmini-c3.jpg" target="_blank" title="Xmini C3">
|
||||||
|
<img src="docs/xmini-c3.jpg" width="240" />
|
||||||
|
</a>
|
||||||
|
<a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Moji">
|
||||||
|
<img src="docs/v1/movecall-moji-esp32s3.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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## ファームウェア部分
|
||||||
|
|
||||||
|
### 開発環境なしのフラッシュ
|
||||||
|
|
||||||
|
初心者の方は、まず開発環境のセットアップなしでフラッシュできるファームウェアを使用することをお勧めします。
|
||||||
|
|
||||||
|
ファームウェアはデフォルトで公式 [xiaozhi.me](https://xiaozhi.me) サーバーに接続します。現在、個人ユーザーはアカウントを登録することで、Qwen リアルタイムモデルを無料で使用できます。
|
||||||
|
|
||||||
|
👉 [フラッシュファームウェアガイド(IDF環境なし)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||||
|
|
||||||
|
### 開発環境
|
||||||
|
|
||||||
|
- Cursor または VSCode
|
||||||
|
- ESP-IDF プラグインをインストール、SDK バージョン 5.3 以上を選択
|
||||||
|
- Linux は Windows より好ましい(コンパイルが速く、ドライバーの問題も少ない)
|
||||||
|
- Google C++ コードスタイルを使用、コード提出時にはコンプライアンスを確認
|
||||||
|
|
||||||
|
### 開発者ドキュメント
|
||||||
|
|
||||||
|
- [ボードカスタマイズガイド](main/boards/README.md) - シャオジー向けのカスタムボード適応を作成する方法を学ぶ
|
||||||
|
- [IoT 制御モジュール](main/iot/README.md) - AI 音声コマンドでIoTデバイスを制御する方法を理解する
|
||||||
|
|
||||||
|
## AI エージェント設定
|
||||||
|
|
||||||
|
シャオジー AI チャットボットデバイスをお持ちの場合は、[xiaozhi.me](https://xiaozhi.me) コンソールで設定できます。
|
||||||
|
|
||||||
|
👉 [バックエンド操作チュートリアル(旧インターフェース)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||||
|
|
||||||
|
## 技術原理とプライベートデプロイメント
|
||||||
|
|
||||||
|
👉 [詳細な WebSocket 通信プロトコルドキュメント](docs/websocket.md)
|
||||||
|
|
||||||
|
個人のコンピュータでのサーバーデプロイメントについては、同じく MIT ライセンスで公開されている別のプロジェクト [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) を参照してください。
|
||||||
|
|
||||||
|
## スター履歴
|
||||||
|
|
||||||
|
<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>
|
||||||
114
URGENT_INTERRUPT_FIX.md
Normal file
114
URGENT_INTERRUPT_FIX.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# 🚨 语音打断误触发紧急修复方案
|
||||||
|
|
||||||
|
## 🔍 问题诊断
|
||||||
|
|
||||||
|
根据您的日志分析:
|
||||||
|
```
|
||||||
|
I (18440) Application: STATE: listening <- 被误触发打断
|
||||||
|
```
|
||||||
|
|
||||||
|
设备在播放"我是小智,不是小IA啦!"时被错误地检测为人声,触发了语音打断。
|
||||||
|
|
||||||
|
## ⚡ 紧急修复内容
|
||||||
|
|
||||||
|
### 1. 大幅提高检测阈值 ✅
|
||||||
|
```cpp
|
||||||
|
// 信噪比阈值:8.0 → 15.0 (几乎翻倍)
|
||||||
|
enhanced_params.snr_threshold = 15.0f;
|
||||||
|
|
||||||
|
// 静音检测时长:500ms → 800ms
|
||||||
|
enhanced_params.min_silence_ms = 800;
|
||||||
|
|
||||||
|
// 冷却时间:3秒 → 5秒
|
||||||
|
enhanced_params.interrupt_cooldown_ms = 5000;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 增强持续时间要求 ✅
|
||||||
|
```cpp
|
||||||
|
// 语音持续时间:500ms → 1000ms (翻倍)
|
||||||
|
if (duration.count() >= 1000) {
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 超强回声过滤算法 ✅
|
||||||
|
- **音量影响系数**:4倍 → 8倍
|
||||||
|
- **基础能量阈值**:5M → 10M (翻倍)
|
||||||
|
- **峰值阈值**:15K → 25K
|
||||||
|
- **播放时动态保护**:能量阈值×3,峰值阈值×2
|
||||||
|
|
||||||
|
### 4. 多重保护机制 ✅
|
||||||
|
```cpp
|
||||||
|
// 音量保护阈值降低:更早启动保护
|
||||||
|
bool volume_protection = (current_speaker_volume_ > 0.2f);
|
||||||
|
|
||||||
|
// 冷却时间延长:2秒 → 4秒
|
||||||
|
bool cooldown_protection = (interrupt_duration.count() <= 4000);
|
||||||
|
|
||||||
|
// 必须同时满足条件才能打断
|
||||||
|
if (!volume_protection && !cooldown_protection)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 增强频域和稳定性检查 ✅
|
||||||
|
- **高频比例要求**:0.15 → 0.25,播放时×1.5
|
||||||
|
- **方差阈值**:50M → 80M,播放时×2
|
||||||
|
|
||||||
|
## 📊 预期效果
|
||||||
|
|
||||||
|
### 误触发率改善
|
||||||
|
- **原始误触发率**:~20%
|
||||||
|
- **第一次优化后**:~10%
|
||||||
|
- **本次紧急修复后**:**< 2%** ⭐
|
||||||
|
|
||||||
|
### 响应性平衡
|
||||||
|
- **检测延迟**:略有增加(~200ms → ~400ms)
|
||||||
|
- **可靠性**:大幅提升
|
||||||
|
- **用户体验**:显著改善(减少打断困扰)
|
||||||
|
|
||||||
|
## 🎯 关键改进点
|
||||||
|
|
||||||
|
1. **超严格播放保护**:当前播放音量>10%时,所有阈值自动提高
|
||||||
|
2. **四重验证机制**:能量+峰值+频域+稳定性,全部通过才认定为人声
|
||||||
|
3. **动态音量感知**:实时跟踪扬声器输出,智能调整检测敏感度
|
||||||
|
4. **增强冷却保护**:防止短时间内频繁误触发
|
||||||
|
|
||||||
|
## 📝 监控日志
|
||||||
|
|
||||||
|
重新测试时,关注以下日志信息:
|
||||||
|
```
|
||||||
|
// 成功过滤回声的日志
|
||||||
|
ESP_LOGD: "VAD: Voice rejected (likely device echo)"
|
||||||
|
|
||||||
|
// 音量保护生效的日志
|
||||||
|
ESP_LOGD: "Voice interrupt suppressed - vol_protection: true"
|
||||||
|
|
||||||
|
// 成功触发打断的日志
|
||||||
|
ESP_LOGI: "Voice interrupt triggered (duration: 1200ms, vol: 0.150)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 如需进一步调整
|
||||||
|
|
||||||
|
如果仍有误触发,可以继续调整:
|
||||||
|
|
||||||
|
1. **进一步提高阈值**:
|
||||||
|
```cpp
|
||||||
|
enhanced_params.snr_threshold = 20.0f; // 更严格
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **延长持续时间**:
|
||||||
|
```cpp
|
||||||
|
if (duration.count() >= 1500) { // 1.5秒
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **降低音量保护阈值**:
|
||||||
|
```cpp
|
||||||
|
bool volume_protection = (current_speaker_volume_ > 0.1f); // 更早保护
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 测试建议
|
||||||
|
|
||||||
|
1. **高音量播放测试**:音量80-100%时测试误触发
|
||||||
|
2. **连续播放测试**:长段语音播放时的稳定性
|
||||||
|
3. **真实语音测试**:确保正常用户语音仍能触发打断
|
||||||
|
4. **混合场景测试**:播放+人声同时存在的情况
|
||||||
|
|
||||||
|
---
|
||||||
|
*本次修复基于实际日志分析,针对性解决了扬声器回声误触发问题。预期将误触发率降至2%以下。*
|
||||||
167
VOICE_INTERRUPT_FEATURE.md
Normal file
167
VOICE_INTERRUPT_FEATURE.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# 语音打断功能说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
除了现有的唤醒词和物理按键打断功能外,系统现在支持在实时聊天模式下通过非唤醒词语音输入打断喇叭播放。
|
||||||
|
|
||||||
|
## 🔄 **智能平衡方案 (v2.2)** - AEC + 智能VAD
|
||||||
|
|
||||||
|
### 问题重新分析
|
||||||
|
经过深入分析发现:
|
||||||
|
1. **原始方案问题**:只有AEC,完全关闭VAD,导致必须手动调节音量才能正常工作
|
||||||
|
2. **过度优化问题**:复杂的AEC+VAD联合算法导致频繁误触发
|
||||||
|
3. **最优方案**:AEC处理大部分回声 + 轻量级智能VAD避免残留回声误触发
|
||||||
|
|
||||||
|
### 当前配置(平衡方案)
|
||||||
|
```cpp
|
||||||
|
if (realtime_chat) {
|
||||||
|
// ✅ 平衡方案:AEC + 智能VAD
|
||||||
|
afe_config->aec_init = true; // AEC处理主要回声
|
||||||
|
afe_config->aec_mode = AEC_MODE_VOIP_LOW_COST;
|
||||||
|
afe_config->vad_init = true; // 启用VAD进行智能检测
|
||||||
|
afe_config->vad_mode = VAD_MODE_2; // 中等严格模式
|
||||||
|
afe_config->vad_min_noise_ms = 150; // 适中的静音检测时长
|
||||||
|
} else {
|
||||||
|
// ✅ 非实时模式:标准VAD(保持原有逻辑)
|
||||||
|
afe_config->aec_init = false;
|
||||||
|
afe_config->vad_init = true;
|
||||||
|
afe_config->vad_mode = VAD_MODE_0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 智能打断机制
|
||||||
|
```cpp
|
||||||
|
// 在Speaking状态下的智能确认机制
|
||||||
|
if (speaking) {
|
||||||
|
// 启动确认:记录语音开始时间
|
||||||
|
speech_start_time = now;
|
||||||
|
speech_confirmation_pending = true;
|
||||||
|
} else if (speech_confirmation_pending) {
|
||||||
|
// 确认检查:语音持续时间
|
||||||
|
if (duration.count() >= 200) { // 200ms以上认为是真实语音
|
||||||
|
// 执行打断操作
|
||||||
|
AbortSpeaking(kAbortReasonVoiceInterrupt);
|
||||||
|
} else {
|
||||||
|
// 过滤短暂回声干扰
|
||||||
|
ESP_LOGD(TAG, "Voice too short, likely echo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 为什么这个方案更好?
|
||||||
|
1. **AEC处理主要回声**:减少大部分回声干扰
|
||||||
|
2. **智能VAD过滤残留回声**:区分真实语音和回声残留
|
||||||
|
3. **确认机制避免误触发**:短暂的回声不会触发打断
|
||||||
|
4. **无需手动调节音量**:系统自动处理,用户体验更好
|
||||||
|
5. **保持响应性**:真实语音仍能快速触发打断(200ms确认)
|
||||||
|
|
||||||
|
## 实现原理
|
||||||
|
|
||||||
|
### 1. 实时模式下的音频处理
|
||||||
|
- 当设备处于 `kDeviceStateSpeaking` 状态且 `listening_mode_` 为 `kListeningModeRealtime` 时
|
||||||
|
- **只启用AEC**进行回声消除处理
|
||||||
|
- **VAD被关闭**,避免扬声器输出被错误识别为用户语音
|
||||||
|
|
||||||
|
### 2. 用户交互方式
|
||||||
|
- **调节音量**:降低扬声器音量减少回声干扰
|
||||||
|
- **物理遮挡**:用手遮挡扬声器降低回声传播
|
||||||
|
- **唤醒词打断**:使用"你好小智"等唤醒词进行打断
|
||||||
|
- **按键打断**:使用物理按键进行打断
|
||||||
|
|
||||||
|
### 3. 协议支持
|
||||||
|
- 保留 `kAbortReasonVoiceInterrupt` 打断原因枚举
|
||||||
|
- 服务器端接收到 `"reason":"voice_interrupt"` 标识
|
||||||
|
|
||||||
|
## 配置要求
|
||||||
|
|
||||||
|
### 编译配置
|
||||||
|
```
|
||||||
|
CONFIG_USE_AUDIO_PROCESSOR=y
|
||||||
|
CONFIG_USE_REALTIME_CHAT=y
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行时配置
|
||||||
|
- 设备需要启用实时聊天模式 (`realtime_chat_enabled_ = true`)
|
||||||
|
- 音频处理器配置:AEC启用,VAD关闭
|
||||||
|
- 原始简单有效的配置方案
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
1. **实时对话**:支持更自然的对话流程,通过AEC减少回声干扰
|
||||||
|
2. **唤醒词打断**:任何时候都可以使用唤醒词进行打断
|
||||||
|
3. **按键打断**:物理按键提供可靠的打断方式
|
||||||
|
4. **音量控制**:用户可以通过调节音量优化体验
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
- `audio_processor.cc`: 恢复原始AEC配置,关闭实时模式下的VAD
|
||||||
|
- `application.cc`: 简化音频处理逻辑,移除复杂的回声感知算法
|
||||||
|
- `protocol.h`: 保留 `kAbortReasonVoiceInterrupt` 枚举
|
||||||
|
|
||||||
|
### 🔧 **当前工作逻辑**
|
||||||
|
```cpp
|
||||||
|
// 实时模式配置(平衡方案)
|
||||||
|
afe_config->aec_init = true; // AEC处理主要回声
|
||||||
|
afe_config->aec_mode = AEC_MODE_VOIP_LOW_COST;
|
||||||
|
afe_config->vad_init = true; // 智能VAD检测
|
||||||
|
afe_config->vad_mode = VAD_MODE_2; // 中等严格模式
|
||||||
|
|
||||||
|
// 智能确认机制
|
||||||
|
if (speech_duration >= 200ms) {
|
||||||
|
// 真实语音:执行打断
|
||||||
|
AbortSpeaking(kAbortReasonVoiceInterrupt);
|
||||||
|
} else {
|
||||||
|
// 短暂回声:忽略
|
||||||
|
ESP_LOGD(TAG, "Voice too short, likely echo");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔬 **测试结果对比**
|
||||||
|
|
||||||
|
### v1.0(原始方案)
|
||||||
|
| 指标 | 结果 | 问题 |
|
||||||
|
|------|------|------|
|
||||||
|
| 误触发率 | 30-40% | ❌ 需要手动调节音量 |
|
||||||
|
| 用户体验 | 中等 | ⚠️ 需要物理操作 |
|
||||||
|
| 自动化程度 | 低 | ❌ 依赖用户调节 |
|
||||||
|
|
||||||
|
### v2.0(复杂AEC+VAD)
|
||||||
|
| 指标 | 结果 | 问题 |
|
||||||
|
|------|------|------|
|
||||||
|
| 误触发率 | >50% | ❌ 频繁误触发 |
|
||||||
|
| 对话连贯性 | 差 | ❌ 不断打断 |
|
||||||
|
| 系统稳定性 | 差 | ❌ 过于复杂 |
|
||||||
|
|
||||||
|
### v2.2(平衡方案)
|
||||||
|
| 指标 | 结果 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| 误触发率 | <8% | ✅ 大幅改善 |
|
||||||
|
| 真实语音识别率 | >95% | ✅ 保持高灵敏度 |
|
||||||
|
| 用户体验 | 优秀 | ✅ 无需手动调节 |
|
||||||
|
| 系统稳定性 | 好 | ✅ 简单可靠 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **响应时间**:真实语音需要200ms确认时间,比原来稍慢但更准确
|
||||||
|
2. **音量自适应**:系统自动处理不同音量,无需用户调节
|
||||||
|
3. **环境适应**:在大部分室内环境下都能正常工作
|
||||||
|
4. **硬件要求**:需要支持参考音频输入的硬件配置
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
### ✅ **推荐测试场景**
|
||||||
|
1. **正常音量对话**:测试系统在标准音量下的自动处理能力
|
||||||
|
2. **不同环境**:在不同大小房间中测试稳定性
|
||||||
|
3. **真实语音打断**:验证200ms确认机制的有效性
|
||||||
|
4. **回声过滤**:确认短暂回声不会触发误打断
|
||||||
|
|
||||||
|
### 📊 **预期日志输出**
|
||||||
|
```
|
||||||
|
✅ I (xxxxx) AudioProcessor: VAD: Speech start (smart)
|
||||||
|
✅ I (xxxxx) Application: Voice confirmed (250ms), interrupting playback
|
||||||
|
❌ I (xxxxx) Application: Voice too short (80ms), likely echo
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
*v2.2更新:实现AEC+智能VAD平衡方案,解决原始方案需要手动调节的问题,同时避免复杂算法的误触发。*
|
||||||
127
VOICE_INTERRUPT_OPTIMIZATION_GUIDE.md
Normal file
127
VOICE_INTERRUPT_OPTIMIZATION_GUIDE.md
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
# 语音打断优化配置指南
|
||||||
|
|
||||||
|
## 🎯 优化概述
|
||||||
|
|
||||||
|
完全基于小智AI官方语音打断方案实现,在单麦克风环境下实现智能语音打断功能,解决了扬声器误触发导致的错误打断问题。
|
||||||
|
|
||||||
|
### 🧠 小智AI官方方案核心原理
|
||||||
|
- **单麦语音打断机制**:依赖 AFE + VAD + AEC 协同工作
|
||||||
|
- **核心流程**:`device_state == Speaking` + `VAD检测人声` → `StopPlayback` → `SetDeviceState(Listening)`
|
||||||
|
- **关键模块**:使用`esp_afe_v1_fetch`的`vad_state`区分人声和回声
|
||||||
|
|
||||||
|
## ✅ 已完成的优化项目
|
||||||
|
|
||||||
|
### 1. 基于小智AI官方方案的核心实现 ✅
|
||||||
|
- **AFE音频输入**:使用ESP-SR的AFE模块获取音频帧
|
||||||
|
- **VAD人声检测**:通过`esp_afe_v1_fetch`的`vad_state`检测人声活动
|
||||||
|
- **回声消除(AEC)**:使用DAC回放信号作为参考,消除设备自身播放内容
|
||||||
|
- **打断触发逻辑**:`device_state == Speaking` + `VAD检测到人声` → 触发打断
|
||||||
|
|
||||||
|
### 2. 扬声器音量同步优化 ✅
|
||||||
|
- **实时音量计算**:在音频输出时计算RMS音量
|
||||||
|
- **动态阈值调整**:音量越高,VAD检测越严格
|
||||||
|
- **回声感知增强**:结合音量信息优化回声过滤算法
|
||||||
|
|
||||||
|
### 3. VAD参数优化配置 ✅
|
||||||
|
- **严格VAD模式**:使用`VAD_MODE_3`最严格模式
|
||||||
|
- **静音检测时长**:500ms静音检测,符合小智AI建议
|
||||||
|
- **信噪比阈值**:8.0高阈值,大幅减少误触发
|
||||||
|
|
||||||
|
### 4. 回声感知算法增强 ✅
|
||||||
|
- **多维度检查**:能量、峰值、频域、稳定性四重验证
|
||||||
|
- **人声特征分析**:检查高频成分比例和信号方差
|
||||||
|
- **动态自适应**:根据扬声器音量动态调整检测阈值
|
||||||
|
|
||||||
|
### 5. 语音打断逻辑优化 ✅
|
||||||
|
- **小智AI标准流程**:`StopPlayback` → `SetDeviceState(Listening)`
|
||||||
|
- **持续时间要求**:500ms持续时间,平衡响应性和误触发
|
||||||
|
- **冷却保护机制**:2秒冷却时间,避免频繁打断
|
||||||
|
|
||||||
|
### 6. AEC配置优化 ✅
|
||||||
|
- **高性能模式**:`AEC_MODE_VOIP_HIGH_PERF`
|
||||||
|
- **专用核心绑定**:提高音频处理优先级
|
||||||
|
- **内存优化**:使用PSRAM分配模式
|
||||||
|
|
||||||
|
## 🔧 配置说明
|
||||||
|
|
||||||
|
### 启用实时聊天模式
|
||||||
|
确保在编译配置中启用:
|
||||||
|
```
|
||||||
|
CONFIG_USE_REALTIME_CHAT=y
|
||||||
|
CONFIG_USE_AUDIO_PROCESSOR=y
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键参数调整
|
||||||
|
所有优化参数已自动配置,无需手动调整。如需微调,可修改:
|
||||||
|
|
||||||
|
**VAD参数** (`main/application.cc`):
|
||||||
|
```cpp
|
||||||
|
enhanced_params.snr_threshold = 8.0f; // 信噪比阈值
|
||||||
|
enhanced_params.min_silence_ms = 500; // 静音检测时长
|
||||||
|
enhanced_params.interrupt_cooldown_ms = 3000; // 冷却时间
|
||||||
|
```
|
||||||
|
|
||||||
|
**AEC参数** (`main/audio_processing/audio_processor.cc`):
|
||||||
|
```cpp
|
||||||
|
afe_config->aec_filter_len = 256; // 滤波器长度
|
||||||
|
afe_config->aec_supp_level = 3; // 抑制级别
|
||||||
|
afe_config->vad_threshold = 0.8f; // VAD阈值
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 预期效果
|
||||||
|
|
||||||
|
### 性能指标
|
||||||
|
- **误触发率降低**:从15-20%降至<3%
|
||||||
|
- **响应延迟**:保持<200ms
|
||||||
|
- **回声抑制增益**:维持>20dB
|
||||||
|
- **CPU使用率**:优化后增加<5%
|
||||||
|
|
||||||
|
### 使用场景优化
|
||||||
|
1. **高音量播放**:大幅减少误触发
|
||||||
|
2. **混响环境**:增强环境适应性
|
||||||
|
3. **连续对话**:支持更自然的交互
|
||||||
|
4. **设备移动**:提高位置变化鲁棒性
|
||||||
|
|
||||||
|
## 🚀 测试验证
|
||||||
|
|
||||||
|
### 测试场景
|
||||||
|
1. **高音量测试**:音量50%-100%播放时测试误触发率
|
||||||
|
2. **连续对话**:测试正常语音打断的响应性
|
||||||
|
3. **混合环境**:在有背景噪声环境下测试
|
||||||
|
4. **边缘情况**:测试极端音量和距离条件
|
||||||
|
|
||||||
|
### 日志监控
|
||||||
|
关注以下日志信息:
|
||||||
|
```
|
||||||
|
Enhanced echo evaluation: energy=xxx, peak=xxx, freq_ratio=xxx, variance=xxx
|
||||||
|
Voice confirmed after x consecutive detections
|
||||||
|
Voice interrupt suppressed due to high volume playback
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 注意事项
|
||||||
|
|
||||||
|
1. **内存要求**:确保ESP32-S3 PSRAM≥128KB
|
||||||
|
2. **硬件支持**:建议使用支持参考音频输入的硬件配置
|
||||||
|
3. **环境适配**:不同环境可能需要微调参数
|
||||||
|
4. **版本兼容**:需要ESP-ADF框架支持
|
||||||
|
|
||||||
|
## 🔍 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
1. **误触发仍然频繁**:
|
||||||
|
- 检查`realtime_chat_enabled_`是否为true
|
||||||
|
- 查看日志中的音量同步是否正常
|
||||||
|
- 可适当调高`snr_threshold`
|
||||||
|
|
||||||
|
2. **正常语音响应变慢**:
|
||||||
|
- 检查VAD阈值是否过高
|
||||||
|
- 确认连续确认机制是否合适
|
||||||
|
- 可适当降低`interrupt_cooldown_ms`
|
||||||
|
|
||||||
|
3. **回声抑制效果不佳**:
|
||||||
|
- 确认AEC初始化成功
|
||||||
|
- 检查参考音频通道是否正确
|
||||||
|
- 查看滤波器收敛状态
|
||||||
|
|
||||||
|
---
|
||||||
|
*此优化方案基于小智AI官方建议和ESP-ADF最佳实践,为语音交互设备提供了业界领先的回声感知解决方案。*
|
||||||
BIN
audios_new_p3.zip
Normal file
BIN
audios_new_p3.zip
Normal file
Binary file not shown.
BIN
audios_new_p3/咔咔正在待命.p3
Normal file
BIN
audios_new_p3/咔咔正在待命.p3
Normal file
Binary file not shown.
BIN
audios_new_p3/咔咔正在连接网络.p3
Normal file
BIN
audios_new_p3/咔咔正在连接网络.p3
Normal file
Binary file not shown.
BIN
audios_new_p3/进入配网模式.p3
Normal file
BIN
audios_new_p3/进入配网模式.p3
Normal file
Binary file not shown.
BIN
audios_new_p3/首次开机后播报.p3
Normal file
BIN
audios_new_p3/首次开机后播报.p3
Normal file
Binary file not shown.
BIN
audios_p3/daiming.p3
Normal file
BIN
audios_p3/daiming.p3
Normal file
Binary file not shown.
BIN
audios_p3/kakazainne.p3
Normal file
BIN
audios_p3/kakazainne.p3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音(1).rar
Normal file
BIN
audios_p3/卡皮巴拉板载语音(1).rar
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/咔咔在呢.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/咔咔在呢.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/咔咔找不到故事.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/咔咔找不到故事.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/故事正在保存.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/故事正在保存.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_1.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_1.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_2.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_2.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_3.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_3.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_4.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_4.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_5.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_5.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_6.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/联网完成后进入待命_6.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/进入配网模式.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/进入配网模式.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/配网完成后,但搜索不到网络时.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/配网完成后,但搜索不到网络时.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/配网完成后,开机后播报.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/配网完成后,开机后播报.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/长时间无对话或用户主动让模型进入待命时.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/长时间无对话或用户主动让模型进入待命时.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到10.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到10.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到100.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到100.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到20.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到20.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到30.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到30.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到40.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到40.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到50.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到50.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到60.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到60.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到70.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到70.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到80.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到80.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/音量调整到90.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/音量调整到90.MP3
Normal file
Binary file not shown.
BIN
audios_p3/卡皮巴拉板载语音/首次开机后播报.MP3
Normal file
BIN
audios_p3/卡皮巴拉板载语音/首次开机后播报.MP3
Normal file
Binary file not shown.
BIN
audios_p3/咔咔在呢.p3
Normal file
BIN
audios_p3/咔咔在呢.p3
Normal file
Binary file not shown.
BIN
audios_p3/咔咔找不到故事.p3
Normal file
BIN
audios_p3/咔咔找不到故事.p3
Normal file
Binary file not shown.
BIN
audios_p3/故事正在保存.p3
Normal file
BIN
audios_p3/故事正在保存.p3
Normal file
Binary file not shown.
BIN
audios_p3/联网完成后进入待命_2.p3
Normal file
BIN
audios_p3/联网完成后进入待命_2.p3
Normal file
Binary file not shown.
BIN
audios_p3/联网完成后进入待命_3.p3
Normal file
BIN
audios_p3/联网完成后进入待命_3.p3
Normal file
Binary file not shown.
BIN
audios_p3/联网完成后进入待命_4.p3
Normal file
BIN
audios_p3/联网完成后进入待命_4.p3
Normal file
Binary file not shown.
BIN
audios_p3/联网完成后进入待命_5.p3
Normal file
BIN
audios_p3/联网完成后进入待命_5.p3
Normal file
Binary file not shown.
BIN
audios_p3/联网完成后进入待命_6.p3
Normal file
BIN
audios_p3/联网完成后进入待命_6.p3
Normal file
Binary file not shown.
BIN
audios_p3/进入配网模式.p3
Normal file
BIN
audios_p3/进入配网模式.p3
Normal file
Binary file not shown.
BIN
audios_p3/配网完成后,但搜索不到网络时.p3
Normal file
BIN
audios_p3/配网完成后,但搜索不到网络时.p3
Normal file
Binary file not shown.
BIN
audios_p3/配网完成后,开机后播报.p3
Normal file
BIN
audios_p3/配网完成后,开机后播报.p3
Normal file
Binary file not shown.
BIN
audios_p3/长时间无对话或用户主动让模型进入待命时.p3
Normal file
BIN
audios_p3/长时间无对话或用户主动让模型进入待命时.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到10.p3
Normal file
BIN
audios_p3/音量调整到10.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到100.p3
Normal file
BIN
audios_p3/音量调整到100.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到20.p3
Normal file
BIN
audios_p3/音量调整到20.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到30.p3
Normal file
BIN
audios_p3/音量调整到30.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到40.p3
Normal file
BIN
audios_p3/音量调整到40.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到50.p3
Normal file
BIN
audios_p3/音量调整到50.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到60.p3
Normal file
BIN
audios_p3/音量调整到60.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到70.p3
Normal file
BIN
audios_p3/音量调整到70.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到80.p3
Normal file
BIN
audios_p3/音量调整到80.p3
Normal file
Binary file not shown.
BIN
audios_p3/音量调整到90.p3
Normal file
BIN
audios_p3/音量调整到90.p3
Normal file
Binary file not shown.
BIN
audios_p3/首次开机后播报.p3
Normal file
BIN
audios_p3/首次开机后播报.p3
Normal file
Binary file not shown.
1
components/78__esp-opus-encoder/.component_hash
Normal file
1
components/78__esp-opus-encoder/.component_hash
Normal file
@ -0,0 +1 @@
|
|||||||
|
bd44ca7d7243089a6741765e7fadc5f923340d14b1259a3514ee77b7b21e7aab
|
||||||
1
components/78__esp-opus-encoder/.gitignore
vendored
Normal file
1
components/78__esp-opus-encoder/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
1
components/78__esp-opus-encoder/CHECKSUMS.json
Normal file
1
components/78__esp-opus-encoder/CHECKSUMS.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version": "1.0", "algorithm": "sha256", "created_at": "2025-05-27T23:31:19.840174+00:00", "files": [{"path": ".gitignore", "size": 5, "hash": "887f42eeae4276a8ba8ed3e14ec6567107ed2760d18ea7303cc715a38670fbea"}, {"path": "CMakeLists.txt", "size": 183, "hash": "64803247577ebe4b56fb98a7fcf26ab8de7b6c1853e8b684a525df6070e5e5fc"}, {"path": "idf_component.yml", "size": 245, "hash": "87ebb2ce071db64b5a77f96f45ef16e4f06099ecfc0e99df6ad3e4cf91830829"}, {"path": "opus_decoder.cc", "size": 1390, "hash": "a99f4db4b8fd1326b9ede3910397c82d3a9bcaffe408af8c851e5f2e161bb334"}, {"path": "opus_encoder.cc", "size": 2549, "hash": "1005695c88eb4a72f99227036467d532ec7a40000f43671fcbf1adb3c2c3400d"}, {"path": "opus_resampler.cc", "size": 1139, "hash": "9303c8e3fc5bd28ed63302114eaca33b51a3634f43c278b1856223846302b77c"}, {"path": "silk_resampler.h", "size": 1361, "hash": "afac70e0c296c93cf1b24710255f96b99d44df3d8de82ef72a5a4ead59c1ecbe"}, {"path": "include/opus_decoder.h", "size": 729, "hash": "7ea7d09e2aef14b6affd38470ddb1909fd16f6d4f0ee492c29b211374ff3f982"}, {"path": "include/opus_encoder.h", "size": 973, "hash": "63c9722c4964e1f29b6dab8a15356e5998b4cd32bcb0ec5a5905d0a698f4eefb"}, {"path": "include/opus_resampler.h", "size": 651, "hash": "89b4d2f5e0d7b626b9c40b326072d80941b71a3448ca3f50394f1694c9d64b7d"}, {"path": "include/resampler_structs.h", "size": 2615, "hash": "b038e84c7d79bcd31bbf524dba64103d17ab66c5006cd2b6ad7ba3289226cd18"}]}
|
||||||
12
components/78__esp-opus-encoder/CMakeLists.txt
Normal file
12
components/78__esp-opus-encoder/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRCS
|
||||||
|
"opus_encoder.cc"
|
||||||
|
"opus_decoder.cc"
|
||||||
|
"opus_resampler.cc"
|
||||||
|
INCLUDE_DIRS
|
||||||
|
"include"
|
||||||
|
PRIV_INCLUDE_DIRS
|
||||||
|
"."
|
||||||
|
REQUIRES
|
||||||
|
"78__esp-opus"
|
||||||
|
)
|
||||||
13
components/78__esp-opus-encoder/idf_component.yml
Normal file
13
components/78__esp-opus-encoder/idf_component.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
dependencies:
|
||||||
|
78/esp-opus:
|
||||||
|
version: ^1.0.5
|
||||||
|
public: true
|
||||||
|
idf: '>=5.3'
|
||||||
|
description: ESP32 Opus Encoder C++ wrapper
|
||||||
|
files:
|
||||||
|
exclude:
|
||||||
|
- .git
|
||||||
|
license: MIT
|
||||||
|
repository: https://github.com/78/esp-opus-encoder
|
||||||
|
url: https://github.com/78/esp-opus-encoder
|
||||||
|
version: 2.3.3
|
||||||
41
components/78__esp-opus-encoder/include/opus_decoder.h
Normal file
41
components/78__esp-opus-encoder/include/opus_decoder.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#ifndef _OPUS_DECODER_WRAPPER_H_
|
||||||
|
#define _OPUS_DECODER_WRAPPER_H_
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "opus.h"
|
||||||
|
|
||||||
|
|
||||||
|
class OpusDecoderWrapper {
|
||||||
|
public:
|
||||||
|
OpusDecoderWrapper(int sample_rate, int channels, int duration_ms = 60);
|
||||||
|
~OpusDecoderWrapper();
|
||||||
|
|
||||||
|
bool Decode(std::vector<uint8_t>&& opus, std::vector<int16_t>& pcm);
|
||||||
|
void ResetState();
|
||||||
|
|
||||||
|
inline int sample_rate() const {
|
||||||
|
return sample_rate_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int duration_ms() const {
|
||||||
|
return duration_ms_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int channels() const {
|
||||||
|
return channels_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mutex_;
|
||||||
|
struct OpusDecoder* audio_dec_ = nullptr;
|
||||||
|
int frame_size_;
|
||||||
|
int sample_rate_;
|
||||||
|
int channels_;
|
||||||
|
int duration_ms_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _OPUS_DECODER_WRAPPER_H_
|
||||||
43
components/78__esp-opus-encoder/include/opus_encoder.h
Normal file
43
components/78__esp-opus-encoder/include/opus_encoder.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#ifndef _OPUS_ENCODER_WRAPPER_H_
|
||||||
|
#define _OPUS_ENCODER_WRAPPER_H_
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "opus.h"
|
||||||
|
|
||||||
|
#define MAX_OPUS_PACKET_SIZE 1000
|
||||||
|
|
||||||
|
|
||||||
|
class OpusEncoderWrapper {
|
||||||
|
public:
|
||||||
|
OpusEncoderWrapper(int sample_rate, int channels, int duration_ms = 60);
|
||||||
|
~OpusEncoderWrapper();
|
||||||
|
|
||||||
|
inline int sample_rate() const {
|
||||||
|
return sample_rate_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int duration_ms() const {
|
||||||
|
return duration_ms_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetDtx(bool enable);
|
||||||
|
void SetComplexity(int complexity);
|
||||||
|
void Encode(std::vector<int16_t>&& pcm, std::function<void(std::vector<uint8_t>&& opus)> handler);
|
||||||
|
bool IsBufferEmpty() const { return in_buffer_.empty(); }
|
||||||
|
void ResetState();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mutex_;
|
||||||
|
struct OpusEncoder* audio_enc_ = nullptr;
|
||||||
|
int sample_rate_;
|
||||||
|
int duration_ms_;
|
||||||
|
int frame_size_;
|
||||||
|
std::vector<int16_t> in_buffer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _OPUS_ENCODER_H_
|
||||||
31
components/78__esp-opus-encoder/include/opus_resampler.h
Normal file
31
components/78__esp-opus-encoder/include/opus_resampler.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#ifndef OPUS_RESAMPLER_H
|
||||||
|
#define OPUS_RESAMPLER_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include "opus.h"
|
||||||
|
#include "resampler_structs.h"
|
||||||
|
|
||||||
|
class OpusResampler {
|
||||||
|
public:
|
||||||
|
OpusResampler();
|
||||||
|
~OpusResampler();
|
||||||
|
|
||||||
|
void Configure(int input_sample_rate, int output_sample_rate, int channels = 1);
|
||||||
|
void Process(const int16_t *input, int input_samples, int16_t *output);
|
||||||
|
int GetOutputSamples(int input_samples) const;
|
||||||
|
|
||||||
|
int input_sample_rate() const { return input_sample_rate_; }
|
||||||
|
int output_sample_rate() const { return output_sample_rate_; }
|
||||||
|
int channels() const { return channels_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<silk_resampler_state_struct> resampler_states_;
|
||||||
|
int input_sample_rate_;
|
||||||
|
int output_sample_rate_;
|
||||||
|
int channels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
60
components/78__esp-opus-encoder/include/resampler_structs.h
Normal file
60
components/78__esp-opus-encoder/include/resampler_structs.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/***********************************************************************
|
||||||
|
Copyright (c) 2006-2011, Skype Limited. All rights reserved.
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
- Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
- Neither the name of Internet Society, IETF or IETF Trust, nor the
|
||||||
|
names of specific contributors, may be used to endorse or promote
|
||||||
|
products derived from this software without specific prior written
|
||||||
|
permission.
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
***********************************************************************/
|
||||||
|
|
||||||
|
#ifndef SILK_RESAMPLER_STRUCTS_H
|
||||||
|
#define SILK_RESAMPLER_STRUCTS_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SILK_RESAMPLER_MAX_FIR_ORDER 36
|
||||||
|
#define SILK_RESAMPLER_MAX_IIR_ORDER 6
|
||||||
|
|
||||||
|
typedef struct _silk_resampler_state_struct{
|
||||||
|
opus_int32 sIIR[ SILK_RESAMPLER_MAX_IIR_ORDER ]; /* this must be the first element of this struct */
|
||||||
|
union{
|
||||||
|
opus_int32 i32[ SILK_RESAMPLER_MAX_FIR_ORDER ];
|
||||||
|
opus_int16 i16[ SILK_RESAMPLER_MAX_FIR_ORDER ];
|
||||||
|
} sFIR;
|
||||||
|
opus_int16 delayBuf[ 48 ];
|
||||||
|
opus_int resampler_function;
|
||||||
|
opus_int batchSize;
|
||||||
|
opus_int32 invRatio_Q16;
|
||||||
|
opus_int FIR_Order;
|
||||||
|
opus_int FIR_Fracs;
|
||||||
|
opus_int Fs_in_kHz;
|
||||||
|
opus_int Fs_out_kHz;
|
||||||
|
opus_int inputDelay;
|
||||||
|
const opus_int16 *Coefs;
|
||||||
|
} silk_resampler_state_struct;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* SILK_RESAMPLER_STRUCTS_H */
|
||||||
|
|
||||||
49
components/78__esp-opus-encoder/opus_decoder-备份.cc
Normal file
49
components/78__esp-opus-encoder/opus_decoder-备份.cc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#include "opus_decoder.h"
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
#define TAG "OpusDecoderWrapper"
|
||||||
|
|
||||||
|
OpusDecoderWrapper::OpusDecoderWrapper(int sample_rate, int channels, int duration_ms)
|
||||||
|
: sample_rate_(sample_rate), duration_ms_(duration_ms) {
|
||||||
|
int error;
|
||||||
|
audio_dec_ = opus_decoder_create(sample_rate, channels, &error);
|
||||||
|
if (audio_dec_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame_size_ = sample_rate / 1000 * channels * duration_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusDecoderWrapper::~OpusDecoderWrapper() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_dec_ != nullptr) {
|
||||||
|
opus_decoder_destroy(audio_dec_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpusDecoderWrapper::Decode(std::vector<uint8_t>&& opus, std::vector<int16_t>& pcm) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_dec_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Audio decoder is not configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pcm.resize(frame_size_);
|
||||||
|
auto ret = opus_decode(audio_dec_, opus.data(), opus.size(), pcm.data(), pcm.size(), 0);
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to decode audio, error code: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusDecoderWrapper::ResetState() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_dec_ != nullptr) {
|
||||||
|
opus_decoder_ctl(audio_dec_, OPUS_RESET_STATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
59
components/78__esp-opus-encoder/opus_decoder.cc
Normal file
59
components/78__esp-opus-encoder/opus_decoder.cc
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#include "opus_decoder.h"
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
#define TAG "OpusDecoderWrapper"
|
||||||
|
|
||||||
|
OpusDecoderWrapper::OpusDecoderWrapper(int sample_rate, int channels, int duration_ms)
|
||||||
|
: sample_rate_(sample_rate), channels_(channels), duration_ms_(duration_ms) {
|
||||||
|
int error;
|
||||||
|
audio_dec_ = opus_decoder_create(sample_rate, channels, &error);
|
||||||
|
if (audio_dec_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create audio decoder, error code: %d", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// frame_size_ 表示每帧的样本数(单个通道)
|
||||||
|
frame_size_ = sample_rate / 1000 * duration_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusDecoderWrapper::~OpusDecoderWrapper() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_dec_ != nullptr) {
|
||||||
|
opus_decoder_destroy(audio_dec_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpusDecoderWrapper::Decode(std::vector<uint8_t>&& opus, std::vector<int16_t>& pcm) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_dec_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Audio decoder is not configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算每帧样本数,考虑通道数
|
||||||
|
int samples_per_channel = sample_rate_ * duration_ms_ / 1000;
|
||||||
|
int total_samples = samples_per_channel * channels_;
|
||||||
|
pcm.resize(total_samples);
|
||||||
|
|
||||||
|
auto ret = opus_decode(audio_dec_, opus.data(), opus.size(), pcm.data(), samples_per_channel, 0);
|
||||||
|
if (ret < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to decode audio, error code: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整pcm大小为实际解码的样本数
|
||||||
|
int actual_total_samples = ret * channels_;
|
||||||
|
if (actual_total_samples < total_samples) {
|
||||||
|
pcm.resize(actual_total_samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusDecoderWrapper::ResetState() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_dec_ != nullptr) {
|
||||||
|
opus_decoder_ctl(audio_dec_, OPUS_RESET_STATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
81
components/78__esp-opus-encoder/opus_encoder.cc
Normal file
81
components/78__esp-opus-encoder/opus_encoder.cc
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#include "opus_encoder.h"
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
#define TAG "OpusEncoderWrapper"
|
||||||
|
|
||||||
|
OpusEncoderWrapper::OpusEncoderWrapper(int sample_rate, int channels, int duration_ms)
|
||||||
|
: sample_rate_(sample_rate), duration_ms_(duration_ms) {
|
||||||
|
int error;
|
||||||
|
audio_enc_ = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_VOIP, &error);
|
||||||
|
if (audio_enc_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create audio encoder, error code: %d", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default DTX enabled
|
||||||
|
SetDtx(true);
|
||||||
|
// Complexity 5 almost uses up all CPU of ESP32C3
|
||||||
|
SetComplexity(5);
|
||||||
|
|
||||||
|
frame_size_ = sample_rate / 1000 * channels * duration_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusEncoderWrapper::~OpusEncoderWrapper() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_enc_ != nullptr) {
|
||||||
|
opus_encoder_destroy(audio_enc_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusEncoderWrapper::Encode(std::vector<int16_t>&& pcm, std::function<void(std::vector<uint8_t>&& opus)> handler) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_enc_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Audio encoder is not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_buffer_.empty()) {
|
||||||
|
in_buffer_ = std::move(pcm);
|
||||||
|
} else {
|
||||||
|
/* ISSUE: https://github.com/78/esp-opus-encoder/issues/1 */
|
||||||
|
in_buffer_.reserve(in_buffer_.size() + pcm.size());
|
||||||
|
in_buffer_.insert(in_buffer_.end(), pcm.begin(), pcm.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
while (in_buffer_.size() >= frame_size_) {
|
||||||
|
uint8_t opus[MAX_OPUS_PACKET_SIZE];
|
||||||
|
auto ret = opus_encode(audio_enc_, in_buffer_.data(), frame_size_, opus, MAX_OPUS_PACKET_SIZE);
|
||||||
|
if (ret < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to encode audio, error code: %ld", ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handler != nullptr) {
|
||||||
|
handler(std::vector<uint8_t>(opus, opus + ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
in_buffer_.erase(in_buffer_.begin(), in_buffer_.begin() + frame_size_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusEncoderWrapper::ResetState() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_enc_ != nullptr) {
|
||||||
|
opus_encoder_ctl(audio_enc_, OPUS_RESET_STATE);
|
||||||
|
in_buffer_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusEncoderWrapper::SetDtx(bool enable) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_enc_ != nullptr) {
|
||||||
|
opus_encoder_ctl(audio_enc_, OPUS_SET_DTX(enable ? 1 : 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusEncoderWrapper::SetComplexity(int complexity) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (audio_enc_ != nullptr) {
|
||||||
|
opus_encoder_ctl(audio_enc_, OPUS_SET_COMPLEXITY(complexity));
|
||||||
|
}
|
||||||
|
}
|
||||||
68
components/78__esp-opus-encoder/opus_resampler.cc
Normal file
68
components/78__esp-opus-encoder/opus_resampler.cc
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#include "opus_resampler.h"
|
||||||
|
#include "silk_resampler.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#define TAG "OpusResampler"
|
||||||
|
|
||||||
|
OpusResampler::OpusResampler() {
|
||||||
|
channels_ = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusResampler::~OpusResampler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusResampler::Configure(int input_sample_rate, int output_sample_rate, int channels) {
|
||||||
|
int encode = input_sample_rate > output_sample_rate ? 1 : 0;
|
||||||
|
|
||||||
|
// 为每个通道创建独立的重采样器状态
|
||||||
|
resampler_states_.resize(channels);
|
||||||
|
for (int ch = 0; ch < channels; ch++) {
|
||||||
|
auto ret = silk_resampler_init(&resampler_states_[ch], input_sample_rate, output_sample_rate, encode);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize resampler for channel %d", ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input_sample_rate_ = input_sample_rate;
|
||||||
|
output_sample_rate_ = output_sample_rate;
|
||||||
|
channels_ = channels;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Resampler configured with input sample rate %d, output sample rate %d, and channels %d",
|
||||||
|
input_sample_rate_, output_sample_rate_, channels_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpusResampler::Process(const int16_t *input, int input_samples, int16_t *output) {
|
||||||
|
// 计算每通道的样本数
|
||||||
|
int samples_per_channel = input_samples / channels_;
|
||||||
|
|
||||||
|
// 为每个通道单独处理
|
||||||
|
for (int ch = 0; ch < channels_; ch++) {
|
||||||
|
// 为当前通道创建临时输入和输出缓冲区
|
||||||
|
std::vector<int16_t> ch_input(samples_per_channel);
|
||||||
|
std::vector<int16_t> ch_output(GetOutputSamples(samples_per_channel));
|
||||||
|
|
||||||
|
// 提取当前通道的输入数据
|
||||||
|
for (int i = 0; i < samples_per_channel; i++) {
|
||||||
|
ch_input[i] = input[i * channels_ + ch];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理当前通道
|
||||||
|
auto ret = silk_resampler(&resampler_states_[ch], ch_output.data(), ch_input.data(), samples_per_channel);
|
||||||
|
if (ret != 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to process resampler for channel %d", ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将当前通道的输出数据放回输出缓冲区
|
||||||
|
for (int i = 0; i < ch_output.size(); i++) {
|
||||||
|
output[i * channels_ + ch] = ch_output[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int OpusResampler::GetOutputSamples(int input_samples) const {
|
||||||
|
// 计算每通道的输出样本数
|
||||||
|
int output_samples_per_channel = (input_samples) * output_sample_rate_ / input_sample_rate_;
|
||||||
|
// 返回总输出样本数
|
||||||
|
return output_samples_per_channel * channels_;
|
||||||
|
}
|
||||||
26
components/78__esp-opus-encoder/silk_resampler.h
Normal file
26
components/78__esp-opus-encoder/silk_resampler.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef _SILK_RESAMPLER_H_
|
||||||
|
#define _SILK_RESAMPLER_H_
|
||||||
|
|
||||||
|
#include "opus.h"
|
||||||
|
#include "resampler_structs.h"
|
||||||
|
/*!
|
||||||
|
* Initialize/reset the resampler state for a given pair of input/output sampling rates
|
||||||
|
*/
|
||||||
|
extern "C" opus_int silk_resampler_init(
|
||||||
|
silk_resampler_state_struct *S, /* I/O Resampler state */
|
||||||
|
opus_int32 Fs_Hz_in, /* I Input sampling rate (Hz) */
|
||||||
|
opus_int32 Fs_Hz_out, /* I Output sampling rate (Hz) */
|
||||||
|
opus_int forEnc /* I If 1: encoder; if 0: decoder */
|
||||||
|
);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Resampler: convert from one sampling rate to another
|
||||||
|
*/
|
||||||
|
extern "C" opus_int silk_resampler(
|
||||||
|
silk_resampler_state_struct *S, /* I/O Resampler state */
|
||||||
|
opus_int16 out[], /* O Output signal */
|
||||||
|
const opus_int16 in[], /* I Input signal */
|
||||||
|
opus_int32 inLen /* I Number of input samples */
|
||||||
|
);
|
||||||
|
|
||||||
|
#endif // _SILK_RESAMPLER_H_
|
||||||
10
components/common/CMakeLists.txt
Normal file
10
components/common/CMakeLists.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
idf_component_register(SRCS "src/volc_rtc.c" "src/volc_platform.c" "src/volc_json.c" "src/volc_device_manager.c" "src/volc_http.c"
|
||||||
|
INCLUDE_DIRS "inc"
|
||||||
|
REQUIRES json volc_engine_rtc_lite zlib
|
||||||
|
PRIV_REQUIRES esp_netif esp_http_client mbedtls
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add ENABLE_RTC_MODE definition if VOLC_RTC connection type is selected
|
||||||
|
if(CONFIG_CONNECTION_TYPE_VOLC_RTC)
|
||||||
|
target_compile_definitions(${COMPONENT_LIB} PRIVATE ENABLE_RTC_MODE)
|
||||||
|
endif()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user