v1.8.0: Audio 代码重构与低功耗优化 (#943)
* Reconstruct Audio Code * Remove old IoT implementation * Add MQTT-UDP documentation * OTA升级失败时,可以继续使用
This commit is contained in:
parent
0621578f55
commit
3c71558a5f
@ -4,7 +4,7 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(PROJECT_VER "1.7.7")
|
||||
set(PROJECT_VER "1.8.0")
|
||||
|
||||
# Add this line to disable the specific warning
|
||||
add_compile_options(-Wno-missing-field-initializers)
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
|
||||
|
||||
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:575180511
|
||||
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:1011329060
|
||||
|
||||
### 基于 MCP 控制万物
|
||||
|
||||
@ -125,6 +125,7 @@
|
||||
- [自定义开发板指南](main/boards/README.md) - 学习如何为小智 AI 创建自定义开发板
|
||||
- [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备
|
||||
- [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式
|
||||
- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md)
|
||||
- [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
|
||||
|
||||
## 大模型配置
|
||||
|
||||
@ -14,7 +14,7 @@ This is an open-source ESP32 project, released under the MIT license, allowing a
|
||||
|
||||
We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices.
|
||||
|
||||
If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 575180511
|
||||
If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 1011329060
|
||||
|
||||
### Control Everything with MCP
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
このプロジェクトを通じて、AIハードウェア開発を理解し、急速に進化する大規模言語モデルを実際のハードウェアデバイスに応用できるようになることを目指しています。
|
||||
|
||||
ご意見やご提案があれば、いつでもIssueを提出するか、QQグループ:575180511 にご参加ください。
|
||||
ご意見やご提案があれば、いつでもIssueを提出するか、QQグループ:1011329060 にご参加ください。
|
||||
|
||||
### MCPであらゆるものを制御
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ MCP 的交互主要围绕客户端(后台 API)发现和调用设备上的“
|
||||
|
||||
- **时机:** 设备启动并成功连接到后台 API 后。
|
||||
- **发送方:** 设备。
|
||||
- **消息:** 设备发送基础协议的 "hello" 消息给后台 API,消息中包含设备支持的能力列表,例如通过 `CONFIG_IOT_PROTOCOL_MCP` 配置来表明支持 MCP 协议 (`"mcp": true`)。
|
||||
- **消息:** 设备发送基础协议的 "hello" 消息给后台 API,消息中包含设备支持的能力列表,例如通过支持 MCP 协议 (`"mcp": true`)。
|
||||
- **示例 (非 MCP 负载,而是基础协议消息):**
|
||||
```json
|
||||
{
|
||||
|
||||
393
docs/mqtt-udp.md
Normal file
393
docs/mqtt-udp.md
Normal file
@ -0,0 +1,393 @@
|
||||
# MQTT + UDP 混合通信协议文档
|
||||
|
||||
基于代码实现整理的 MQTT + UDP 混合通信协议文档,概述设备端与服务器之间如何通过 MQTT 进行控制消息传输,通过 UDP 进行音频数据传输的交互方式。
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议概览
|
||||
|
||||
本协议采用混合传输方式:
|
||||
- **MQTT**:用于控制消息、状态同步、JSON 数据交换
|
||||
- **UDP**:用于实时音频数据传输,支持加密
|
||||
|
||||
### 1.1 协议特点
|
||||
|
||||
- **双通道设计**:控制与数据分离,确保实时性
|
||||
- **加密传输**:UDP 音频数据使用 AES-CTR 加密
|
||||
- **序列号保护**:防止数据包重放和乱序
|
||||
- **自动重连**:MQTT 连接断开时自动重连
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体流程概览
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Device as ESP32 设备
|
||||
participant MQTT as MQTT 服务器
|
||||
participant UDP as UDP 服务器
|
||||
|
||||
Note over Device, UDP: 1. 建立 MQTT 连接
|
||||
Device->>MQTT: MQTT Connect
|
||||
MQTT->>Device: Connected
|
||||
|
||||
Note over Device, UDP: 2. 请求音频通道
|
||||
Device->>MQTT: Hello Message (type: "hello", transport: "udp")
|
||||
MQTT->>Device: Hello Response (UDP 连接信息 + 加密密钥)
|
||||
|
||||
Note over Device, UDP: 3. 建立 UDP 连接
|
||||
Device->>UDP: UDP Connect
|
||||
UDP->>Device: Connected
|
||||
|
||||
Note over Device, UDP: 4. 音频数据传输
|
||||
loop 音频流传输
|
||||
Device->>UDP: 加密音频数据 (Opus)
|
||||
UDP->>Device: 加密音频数据 (Opus)
|
||||
end
|
||||
|
||||
Note over Device, UDP: 5. 控制消息交换
|
||||
par 控制消息
|
||||
Device->>MQTT: Listen/TTS/MCP 消息
|
||||
MQTT->>Device: STT/TTS/MCP 响应
|
||||
end
|
||||
|
||||
Note over Device, UDP: 6. 关闭连接
|
||||
Device->>MQTT: Goodbye Message
|
||||
Device->>UDP: Disconnect
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. MQTT 控制通道
|
||||
|
||||
### 3.1 连接建立
|
||||
|
||||
设备通过 MQTT 连接到服务器,连接参数包括:
|
||||
- **Endpoint**:MQTT 服务器地址和端口
|
||||
- **Client ID**:设备唯一标识符
|
||||
- **Username/Password**:认证凭据
|
||||
- **Keep Alive**:心跳间隔(默认240秒)
|
||||
|
||||
### 3.2 Hello 消息交换
|
||||
|
||||
#### 3.2.1 设备端发送 Hello
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 3,
|
||||
"transport": "udp",
|
||||
"features": {
|
||||
"mcp": true
|
||||
},
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 16000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 服务器响应 Hello
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"transport": "udp",
|
||||
"session_id": "xxx",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 24000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
},
|
||||
"udp": {
|
||||
"server": "192.168.1.100",
|
||||
"port": 8888,
|
||||
"key": "0123456789ABCDEF0123456789ABCDEF",
|
||||
"nonce": "0123456789ABCDEF0123456789ABCDEF"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `udp.server`:UDP 服务器地址
|
||||
- `udp.port`:UDP 服务器端口
|
||||
- `udp.key`:AES 加密密钥(十六进制字符串)
|
||||
- `udp.nonce`:AES 加密随机数(十六进制字符串)
|
||||
|
||||
### 3.3 JSON 消息类型
|
||||
|
||||
#### 3.3.1 设备端→服务器
|
||||
|
||||
1. **Listen 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "listen",
|
||||
"state": "start",
|
||||
"mode": "manual"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Abort 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "abort",
|
||||
"reason": "wake_word_detected"
|
||||
}
|
||||
```
|
||||
|
||||
3. **MCP 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "mcp",
|
||||
"payload": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Goodbye 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "goodbye"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 服务器→设备端
|
||||
|
||||
支持的消息类型与 WebSocket 协议一致,包括:
|
||||
- **STT**:语音识别结果
|
||||
- **TTS**:语音合成控制
|
||||
- **LLM**:情感表达控制
|
||||
- **MCP**:物联网控制
|
||||
- **System**:系统控制
|
||||
- **Custom**:自定义消息(可选)
|
||||
|
||||
---
|
||||
|
||||
## 4. UDP 音频通道
|
||||
|
||||
### 4.1 连接建立
|
||||
|
||||
设备收到 MQTT Hello 响应后,使用其中的 UDP 连接信息建立音频通道:
|
||||
1. 解析 UDP 服务器地址和端口
|
||||
2. 解析加密密钥和随机数
|
||||
3. 初始化 AES-CTR 加密上下文
|
||||
4. 建立 UDP 连接
|
||||
|
||||
### 4.2 音频数据格式
|
||||
|
||||
#### 4.2.1 加密音频包结构
|
||||
|
||||
```
|
||||
|type 1byte|flags 1byte|payload_len 2bytes|ssrc 4bytes|timestamp 4bytes|sequence 4bytes|
|
||||
|payload payload_len bytes|
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `type`:数据包类型,固定为 0x01
|
||||
- `flags`:标志位,当前未使用
|
||||
- `payload_len`:负载长度(网络字节序)
|
||||
- `ssrc`:同步源标识符
|
||||
- `timestamp`:时间戳(网络字节序)
|
||||
- `sequence`:序列号(网络字节序)
|
||||
- `payload`:加密的 Opus 音频数据
|
||||
|
||||
#### 4.2.2 加密算法
|
||||
|
||||
使用 **AES-CTR** 模式加密:
|
||||
- **密钥**:128位,由服务器提供
|
||||
- **随机数**:128位,由服务器提供
|
||||
- **计数器**:包含时间戳和序列号信息
|
||||
|
||||
### 4.3 序列号管理
|
||||
|
||||
- **发送端**:`local_sequence_` 单调递增
|
||||
- **接收端**:`remote_sequence_` 验证连续性
|
||||
- **防重放**:拒绝序列号小于期望值的数据包
|
||||
- **容错处理**:允许轻微的序列号跳跃,记录警告
|
||||
|
||||
### 4.4 错误处理
|
||||
|
||||
1. **解密失败**:记录错误,丢弃数据包
|
||||
2. **序列号异常**:记录警告,但仍处理数据包
|
||||
3. **数据包格式错误**:记录错误,丢弃数据包
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态管理
|
||||
|
||||
### 5.1 连接状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
direction TB
|
||||
[*] --> Disconnected
|
||||
Disconnected --> MqttConnecting: StartMqttClient()
|
||||
MqttConnecting --> MqttConnected: MQTT Connected
|
||||
MqttConnecting --> Disconnected: Connect Failed
|
||||
MqttConnected --> RequestingChannel: OpenAudioChannel()
|
||||
RequestingChannel --> ChannelOpened: Hello Exchange Success
|
||||
RequestingChannel --> MqttConnected: Hello Timeout/Failed
|
||||
ChannelOpened --> UdpConnected: UDP Connect Success
|
||||
UdpConnected --> AudioStreaming: Start Audio Transfer
|
||||
AudioStreaming --> UdpConnected: Stop Audio Transfer
|
||||
UdpConnected --> ChannelOpened: UDP Disconnect
|
||||
ChannelOpened --> MqttConnected: CloseAudioChannel()
|
||||
MqttConnected --> Disconnected: MQTT Disconnect
|
||||
```
|
||||
|
||||
### 5.2 状态检查
|
||||
|
||||
设备通过以下条件判断音频通道是否可用:
|
||||
```cpp
|
||||
bool IsAudioChannelOpened() const {
|
||||
return udp_ != nullptr && !error_occurred_ && !IsTimeout();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置参数
|
||||
|
||||
### 6.1 MQTT 配置
|
||||
|
||||
从设置中读取的配置项:
|
||||
- `endpoint`:MQTT 服务器地址
|
||||
- `client_id`:客户端标识符
|
||||
- `username`:用户名
|
||||
- `password`:密码
|
||||
- `keepalive`:心跳间隔(默认240秒)
|
||||
- `publish_topic`:发布主题
|
||||
|
||||
### 6.2 音频参数
|
||||
|
||||
- **格式**:Opus
|
||||
- **采样率**:16000 Hz(设备端)/ 24000 Hz(服务器端)
|
||||
- **声道数**:1(单声道)
|
||||
- **帧时长**:60ms
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理与重连
|
||||
|
||||
### 7.1 MQTT 重连机制
|
||||
|
||||
- 连接失败时自动重试
|
||||
- 支持错误上报控制
|
||||
- 断线时触发清理流程
|
||||
|
||||
### 7.2 UDP 连接管理
|
||||
|
||||
- 连接失败时不自动重试
|
||||
- 依赖 MQTT 通道重新协商
|
||||
- 支持连接状态查询
|
||||
|
||||
### 7.3 超时处理
|
||||
|
||||
基类 `Protocol` 提供超时检测:
|
||||
- 默认超时时间:120 秒
|
||||
- 基于最后接收时间计算
|
||||
- 超时时自动标记为不可用
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全考虑
|
||||
|
||||
### 8.1 传输加密
|
||||
|
||||
- **MQTT**:支持 TLS/SSL 加密(端口8883)
|
||||
- **UDP**:使用 AES-CTR 加密音频数据
|
||||
|
||||
### 8.2 认证机制
|
||||
|
||||
- **MQTT**:用户名/密码认证
|
||||
- **UDP**:通过 MQTT 通道分发密钥
|
||||
|
||||
### 8.3 防重放攻击
|
||||
|
||||
- 序列号单调递增
|
||||
- 拒绝过期数据包
|
||||
- 时间戳验证
|
||||
|
||||
---
|
||||
|
||||
## 9. 性能优化
|
||||
|
||||
### 9.1 并发控制
|
||||
|
||||
使用互斥锁保护 UDP 连接:
|
||||
```cpp
|
||||
std::lock_guard<std::mutex> lock(channel_mutex_);
|
||||
```
|
||||
|
||||
### 9.2 内存管理
|
||||
|
||||
- 动态创建/销毁网络对象
|
||||
- 智能指针管理音频数据包
|
||||
- 及时释放加密上下文
|
||||
|
||||
### 9.3 网络优化
|
||||
|
||||
- UDP 连接复用
|
||||
- 数据包大小优化
|
||||
- 序列号连续性检查
|
||||
|
||||
---
|
||||
|
||||
## 10. 与 WebSocket 协议的比较
|
||||
|
||||
| 特性 | MQTT + UDP | WebSocket |
|
||||
|------|------------|-----------|
|
||||
| 控制通道 | MQTT | WebSocket |
|
||||
| 音频通道 | UDP (加密) | WebSocket (二进制) |
|
||||
| 实时性 | 高 (UDP) | 中等 |
|
||||
| 可靠性 | 中等 | 高 |
|
||||
| 复杂度 | 高 | 低 |
|
||||
| 加密 | AES-CTR | TLS |
|
||||
| 防火墙友好度 | 低 | 高 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 部署建议
|
||||
|
||||
### 11.1 网络环境
|
||||
|
||||
- 确保 UDP 端口可达
|
||||
- 配置防火墙规则
|
||||
- 考虑 NAT 穿透
|
||||
|
||||
### 11.2 服务器配置
|
||||
|
||||
- MQTT Broker 配置
|
||||
- UDP 服务器部署
|
||||
- 密钥管理系统
|
||||
|
||||
### 11.3 监控指标
|
||||
|
||||
- 连接成功率
|
||||
- 音频传输延迟
|
||||
- 数据包丢失率
|
||||
- 解密失败率
|
||||
|
||||
---
|
||||
|
||||
## 12. 总结
|
||||
|
||||
MQTT + UDP 混合协议通过以下设计实现高效的音视频通信:
|
||||
|
||||
- **分离式架构**:控制与数据通道分离,各司其职
|
||||
- **加密保护**:AES-CTR 确保音频数据安全传输
|
||||
- **序列化管理**:防止重放攻击和数据乱序
|
||||
- **自动恢复**:支持连接断开后的自动重连
|
||||
- **性能优化**:UDP 传输保证音频数据的实时性
|
||||
|
||||
该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。
|
||||
@ -84,7 +84,7 @@
|
||||
在建立 WebSocket 连接时,代码示例中设置了以下请求头:
|
||||
|
||||
- `Authorization`: 用于存放访问令牌,形如 `"Bearer <token>"`
|
||||
- `Protocol-Version`: 固定示例中为 `"1"`,与 hello 消息体内的 `version` 字段保持一致
|
||||
- `Protocol-Version`: 协议版本号,与 hello 消息体内的 `version` 字段保持一致
|
||||
- `Device-Id`: 设备物理网卡 MAC 地址
|
||||
- `Client-Id`: 软件生成的 UUID(擦除 NVS 或重新烧录完整固件会重置)
|
||||
|
||||
@ -92,11 +92,44 @@
|
||||
|
||||
---
|
||||
|
||||
## 3. JSON 消息结构
|
||||
## 3. 二进制协议版本
|
||||
|
||||
设备支持多种二进制协议版本,通过配置中的 `version` 字段指定:
|
||||
|
||||
### 3.1 版本1(默认)
|
||||
直接发送 Opus 音频数据,无额外元数据。Websocket 协议会区分 text 与 binary。
|
||||
|
||||
### 3.2 版本2
|
||||
使用 `BinaryProtocol2` 结构:
|
||||
```c
|
||||
struct BinaryProtocol2 {
|
||||
uint16_t version; // 协议版本
|
||||
uint16_t type; // 消息类型 (0: OPUS, 1: JSON)
|
||||
uint32_t reserved; // 保留字段
|
||||
uint32_t timestamp; // 时间戳(毫秒,用于服务器端AEC)
|
||||
uint32_t payload_size; // 负载大小(字节)
|
||||
uint8_t payload[]; // 负载数据
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
### 3.3 版本3
|
||||
使用 `BinaryProtocol3` 结构:
|
||||
```c
|
||||
struct BinaryProtocol3 {
|
||||
uint8_t type; // 消息类型
|
||||
uint8_t reserved; // 保留字段
|
||||
uint16_t payload_size; // 负载大小
|
||||
uint8_t payload[]; // 负载数据
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. JSON 消息结构
|
||||
|
||||
WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。
|
||||
|
||||
### 3.1 设备端→服务器
|
||||
### 4.1 设备端→服务器
|
||||
|
||||
1. **Hello**
|
||||
- 连接成功后,由设备端发送,告知服务器基本参数。
|
||||
@ -183,7 +216,7 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
|
||||
---
|
||||
|
||||
### 3.2 服务器→设备端
|
||||
### 4.2 服务器→设备端
|
||||
|
||||
1. **Hello**
|
||||
- 服务器端返回的握手确认消息。
|
||||
@ -227,17 +260,43 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
}
|
||||
```
|
||||
|
||||
6. **音频数据:二进制帧**
|
||||
6. **System**
|
||||
- 系统控制命令,常用于远程升级更新。
|
||||
- 例:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "system",
|
||||
"command": "reboot"
|
||||
}
|
||||
```
|
||||
- 支持的命令:
|
||||
- `"reboot"`:重启设备
|
||||
|
||||
7. **Custom**(可选)
|
||||
- 自定义消息,当 `CONFIG_RECEIVE_CUSTOM_MESSAGE` 启用时支持。
|
||||
- 例:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "custom",
|
||||
"payload": {
|
||||
"message": "自定义内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
8. **音频数据:二进制帧**
|
||||
- 当服务器发送音频二进制帧(Opus 编码)时,设备端解码并播放。
|
||||
- 若设备端正在处于 "listening" (录音)状态,收到的音频帧会被忽略或清空以防冲突。
|
||||
|
||||
---
|
||||
|
||||
## 4. 音频编解码
|
||||
## 5. 音频编解码
|
||||
|
||||
1. **设备端发送录音数据**
|
||||
- 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。
|
||||
- 如果设备端每次编码生成的二进制帧大小为 N 字节,则会通过 WebSocket 的 **binary** 消息发送这块数据。
|
||||
- 根据协议版本,可能直接发送 Opus 数据(版本1)或使用带元数据的二进制协议(版本2/3)。
|
||||
|
||||
2. **设备端播放收到的音频**
|
||||
- 收到服务器的二进制帧时,同样认定是 Opus 数据。
|
||||
@ -246,7 +305,7 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见状态流转
|
||||
## 6. 常见状态流转
|
||||
|
||||
以下为常见设备端关键状态流转,与 WebSocket 消息对应:
|
||||
|
||||
@ -307,7 +366,7 @@ stateDiagram
|
||||
|
||||
---
|
||||
|
||||
## 6. 错误处理
|
||||
## 7. 错误处理
|
||||
|
||||
1. **连接失败**
|
||||
- 如果 `Connect(url)` 返回失败或在等待服务器 "hello" 消息时超时,触发 `on_network_error_()` 回调。设备会提示"无法连接到服务"或类似错误信息。
|
||||
@ -319,7 +378,7 @@ stateDiagram
|
||||
|
||||
---
|
||||
|
||||
## 7. 其它注意事项
|
||||
## 8. 其它注意事项
|
||||
|
||||
1. **鉴权**
|
||||
- 设备通过设置 `Authorization: Bearer <token>` 提供鉴权,服务器端需验证是否有效。
|
||||
@ -331,17 +390,23 @@ stateDiagram
|
||||
3. **音频负载**
|
||||
- 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。为了获得更好的音乐播放效果,服务器下行音频可能使用 24000 采样率。
|
||||
|
||||
4. **物联网控制推荐 MCP 协议**
|
||||
4. **协议版本配置**
|
||||
- 通过设置中的 `version` 字段配置二进制协议版本(1、2 或 3)
|
||||
- 版本1:直接发送 Opus 数据
|
||||
- 版本2:使用带时间戳的二进制协议,适用于服务器端 AEC
|
||||
- 版本3:使用简化的二进制协议
|
||||
|
||||
5. **物联网控制推荐 MCP 协议**
|
||||
- 设备与服务器之间的物联网能力发现、状态同步、控制指令等,建议全部通过 MCP 协议(type: "mcp")实现。原有的 type: "iot" 方案已废弃。
|
||||
- MCP 协议可在 WebSocket、MQTT 等多种底层协议上传输,具备更好的扩展性和标准化能力。
|
||||
- 详细用法请参考 [MCP 协议文档](./mcp-protocol.md) 及 [MCP 物联网控制用法](./mcp-usage.md)。
|
||||
|
||||
5. **错误或异常 JSON**
|
||||
6. **错误或异常 JSON**
|
||||
- 当 JSON 中缺少必要字段,例如 `{"type": ...}`,设备端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。
|
||||
|
||||
---
|
||||
|
||||
## 8. 消息示例
|
||||
## 9. 消息示例
|
||||
|
||||
下面给出一个典型的双向消息示例(流程简化示意):
|
||||
|
||||
@ -418,13 +483,13 @@ stateDiagram
|
||||
|
||||
---
|
||||
|
||||
## 9. 总结
|
||||
## 10. 总结
|
||||
|
||||
本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧,完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、MCP 指令下发等。其核心特征:
|
||||
|
||||
- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。
|
||||
- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流。
|
||||
- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、MCP、WakeWord 等。
|
||||
- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流,支持多种协议版本。
|
||||
- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、MCP、WakeWord、System、Custom 等。
|
||||
- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。
|
||||
|
||||
服务器与设备端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
set(SOURCES "audio_codecs/audio_codec.cc"
|
||||
"audio_codecs/no_audio_codec.cc"
|
||||
"audio_codecs/box_audio_codec.cc"
|
||||
"audio_codecs/es8311_audio_codec.cc"
|
||||
"audio_codecs/es8374_audio_codec.cc"
|
||||
"audio_codecs/es8388_audio_codec.cc"
|
||||
"audio_codecs/dummy_audio_codec.cc"
|
||||
"audio_processing/audio_debugger.cc"
|
||||
set(SOURCES "audio/audio_codec.cc"
|
||||
"audio/audio_service.cc"
|
||||
"audio/codecs/no_audio_codec.cc"
|
||||
"audio/codecs/box_audio_codec.cc"
|
||||
"audio/codecs/es8311_audio_codec.cc"
|
||||
"audio/codecs/es8374_audio_codec.cc"
|
||||
"audio/codecs/es8388_audio_codec.cc"
|
||||
"audio/codecs/dummy_audio_codec.cc"
|
||||
"audio/processors/audio_debugger.cc"
|
||||
"led/single_led.cc"
|
||||
"led/circular_strip.cc"
|
||||
"led/gpio_led.cc"
|
||||
@ -15,23 +16,16 @@ set(SOURCES "audio_codecs/audio_codec.cc"
|
||||
"protocols/protocol.cc"
|
||||
"protocols/mqtt_protocol.cc"
|
||||
"protocols/websocket_protocol.cc"
|
||||
"iot/thing.cc"
|
||||
"iot/thing_manager.cc"
|
||||
"mcp_server.cc"
|
||||
"system_info.cc"
|
||||
"application.cc"
|
||||
"ota.cc"
|
||||
"settings.cc"
|
||||
"background_task.cc"
|
||||
"device_state_event.cc"
|
||||
"main.cc"
|
||||
)
|
||||
|
||||
set(INCLUDE_DIRS "." "display" "audio_codecs" "protocols" "audio_processing")
|
||||
|
||||
# 添加 IOT 相关文件
|
||||
file(GLOB IOT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/iot/things/*.cc)
|
||||
list(APPEND SOURCES ${IOT_SOURCES})
|
||||
set(INCLUDE_DIRS "." "display" "audio" "protocols")
|
||||
|
||||
# 添加板级公共文件
|
||||
file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc)
|
||||
@ -223,18 +217,16 @@ file(GLOB BOARD_SOURCES
|
||||
list(APPEND SOURCES ${BOARD_SOURCES})
|
||||
|
||||
if(CONFIG_USE_AUDIO_PROCESSOR)
|
||||
list(APPEND SOURCES "audio_processing/afe_audio_processor.cc")
|
||||
list(APPEND SOURCES "audio/processors/afe_audio_processor.cc")
|
||||
else()
|
||||
list(APPEND SOURCES "audio_processing/no_audio_processor.cc")
|
||||
list(APPEND SOURCES "audio/processors/no_audio_processor.cc")
|
||||
endif()
|
||||
if(CONFIG_USE_AFE_WAKE_WORD)
|
||||
list(APPEND SOURCES "audio_processing/afe_wake_word.cc")
|
||||
list(APPEND SOURCES "audio/wake_words/afe_wake_word.cc")
|
||||
elseif(CONFIG_USE_ESP_WAKE_WORD)
|
||||
list(APPEND SOURCES "audio_processing/esp_wake_word.cc")
|
||||
list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc")
|
||||
elseif(CONFIG_USE_CUSTOM_WAKE_WORD)
|
||||
list(APPEND SOURCES "audio_processing/custom_wake_word.cc")
|
||||
else()
|
||||
list(APPEND SOURCES "audio_processing/no_wake_word.cc")
|
||||
list(APPEND SOURCES "audio/wake_words/custom_wake_word.cc")
|
||||
endif()
|
||||
|
||||
# 根据Kconfig选择语言目录
|
||||
@ -256,8 +248,8 @@ file(GLOB COMMON_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/common/*.p3)
|
||||
|
||||
# 如果目标芯片是 ESP32,则排除特定文件
|
||||
if(CONFIG_IDF_TARGET_ESP32)
|
||||
list(REMOVE_ITEM SOURCES "audio_codecs/box_audio_codec.cc"
|
||||
"audio_codecs/es8388_audio_codec.cc"
|
||||
list(REMOVE_ITEM SOURCES "audio/codecs/box_audio_codec.cc"
|
||||
"audio/codecs/es8388_audio_codec.cc"
|
||||
"led/gpio_led.cc"
|
||||
)
|
||||
endif()
|
||||
|
||||
@ -472,17 +472,6 @@ config RECEIVE_CUSTOM_MESSAGE
|
||||
help
|
||||
启用接收自定义消息功能,允许设备接收来自服务器的自定义消息(最好通过 MQTT 协议)
|
||||
|
||||
choice IOT_PROTOCOL
|
||||
prompt "IoT Protocol"
|
||||
default IOT_PROTOCOL_MCP
|
||||
help
|
||||
IoT 协议,用于获取设备状态与发送控制指令
|
||||
config IOT_PROTOCOL_MCP
|
||||
bool "MCP 2024-11-05"
|
||||
config IOT_PROTOCOL_XIAOZHI
|
||||
bool "Xiaozhi IoT 1.0 (Deprecated)"
|
||||
endchoice
|
||||
|
||||
choice I2S_TYPE_TAIJIPI_S3
|
||||
depends on BOARD_TYPE_ESP32S3_Taiji_Pi
|
||||
prompt "taiji-pi-S3 I2S Type"
|
||||
|
||||
@ -6,26 +6,8 @@
|
||||
#include "mqtt_protocol.h"
|
||||
#include "websocket_protocol.h"
|
||||
#include "font_awesome_symbols.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "mcp_server.h"
|
||||
#include "audio_debugger.h"
|
||||
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
#include "afe_audio_processor.h"
|
||||
#else
|
||||
#include "no_audio_processor.h"
|
||||
#endif
|
||||
|
||||
#if CONFIG_USE_AFE_WAKE_WORD
|
||||
#include "afe_wake_word.h"
|
||||
#elif CONFIG_USE_ESP_WAKE_WORD
|
||||
#include "esp_wake_word.h"
|
||||
#elif CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
#include "custom_wake_word.h"
|
||||
#else
|
||||
#include "no_wake_word.h"
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <esp_log.h>
|
||||
@ -53,7 +35,6 @@ static const char* const STATE_STRINGS[] = {
|
||||
|
||||
Application::Application() {
|
||||
event_group_ = xEventGroupCreate();
|
||||
background_task_ = new BackgroundTask(4096 * 7);
|
||||
|
||||
#if CONFIG_USE_DEVICE_AEC
|
||||
aec_mode_ = kAecOnDeviceSide;
|
||||
@ -63,22 +44,6 @@ Application::Application() {
|
||||
aec_mode_ = kAecOff;
|
||||
#endif
|
||||
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
audio_processor_ = std::make_unique<AfeAudioProcessor>();
|
||||
#else
|
||||
audio_processor_ = std::make_unique<NoAudioProcessor>();
|
||||
#endif
|
||||
|
||||
#if CONFIG_USE_AFE_WAKE_WORD
|
||||
wake_word_ = std::make_unique<AfeWakeWord>();
|
||||
#elif CONFIG_USE_ESP_WAKE_WORD
|
||||
wake_word_ = std::make_unique<EspWakeWord>();
|
||||
#elif CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
wake_word_ = std::make_unique<CustomWakeWord>();
|
||||
#else
|
||||
wake_word_ = std::make_unique<NoWakeWord>();
|
||||
#endif
|
||||
|
||||
esp_timer_create_args_t clock_timer_args = {
|
||||
.callback = [](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
@ -97,9 +62,6 @@ Application::~Application() {
|
||||
esp_timer_stop(clock_timer_handle_);
|
||||
esp_timer_delete(clock_timer_handle_);
|
||||
}
|
||||
if (background_task_ != nullptr) {
|
||||
delete background_task_;
|
||||
}
|
||||
vEventGroupDelete(event_group_);
|
||||
}
|
||||
|
||||
@ -108,9 +70,10 @@ void Application::CheckNewVersion(Ota& ota) {
|
||||
int retry_count = 0;
|
||||
int retry_delay = 10; // 初始重试延迟为10秒
|
||||
|
||||
auto& board = Board::GetInstance();
|
||||
while (true) {
|
||||
SetDeviceState(kDeviceStateActivating);
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto display = board.GetDisplay();
|
||||
display->SetStatus(Lang::Strings::CHECKING_NEW_VERSION);
|
||||
|
||||
if (!ota.CheckVersion()) {
|
||||
@ -148,40 +111,38 @@ void Application::CheckNewVersion(Ota& ota) {
|
||||
std::string message = std::string(Lang::Strings::NEW_VERSION) + ota.GetFirmwareVersion();
|
||||
display->SetChatMessage("system", message.c_str());
|
||||
|
||||
auto& board = Board::GetInstance();
|
||||
board.SetPowerSaveMode(false);
|
||||
wake_word_->StopDetection();
|
||||
// 预先关闭音频输出,避免升级过程有音频操作
|
||||
auto codec = board.GetAudioCodec();
|
||||
codec->EnableInput(false);
|
||||
codec->EnableOutput(false);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
audio_decode_queue_.clear();
|
||||
}
|
||||
background_task_->WaitForCompletion();
|
||||
delete background_task_;
|
||||
background_task_ = nullptr;
|
||||
audio_service_.Stop();
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
ota.StartUpgrade([display](int progress, size_t speed) {
|
||||
bool upgrade_success = ota.StartUpgrade([display](int progress, size_t speed) {
|
||||
char buffer[64];
|
||||
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
|
||||
display->SetChatMessage("system", buffer);
|
||||
});
|
||||
|
||||
// If upgrade success, the device will reboot and never reach here
|
||||
display->SetStatus(Lang::Strings::UPGRADE_FAILED);
|
||||
ESP_LOGI(TAG, "Firmware upgrade failed...");
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
Reboot();
|
||||
return;
|
||||
if (!upgrade_success) {
|
||||
// Upgrade failed, restart audio service and continue running
|
||||
ESP_LOGE(TAG, "Firmware upgrade failed, restarting audio service and continuing operation...");
|
||||
audio_service_.Start(); // Restart audio service
|
||||
board.SetPowerSaveMode(true); // Restore power save mode
|
||||
Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "sad", Lang::Sounds::P3_EXCLAMATION);
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
// Continue to normal operation (don't break, just fall through)
|
||||
} else {
|
||||
// Upgrade success, reboot immediately
|
||||
ESP_LOGI(TAG, "Firmware upgrade successful, rebooting...");
|
||||
display->SetChatMessage("system", "Upgrade successful, rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message
|
||||
Reboot();
|
||||
return; // This line will never be reached after reboot
|
||||
}
|
||||
}
|
||||
|
||||
// No new version, mark the current version as valid
|
||||
ota.MarkCurrentVersionValid();
|
||||
if (!ota.HasActivationCode() && !ota.HasActivationChallenge()) {
|
||||
xEventGroupSetBits(event_group_, CHECK_NEW_VERSION_DONE_EVENT);
|
||||
xEventGroupSetBits(event_group_, MAIN_EVENT_CHECK_NEW_VERSION_DONE);
|
||||
// Exit the loop if done checking new version
|
||||
break;
|
||||
}
|
||||
@ -197,7 +158,7 @@ void Application::CheckNewVersion(Ota& ota) {
|
||||
ESP_LOGI(TAG, "Activating... %d/%d", i + 1, 10);
|
||||
esp_err_t err = ota.Activate();
|
||||
if (err == ESP_OK) {
|
||||
xEventGroupSetBits(event_group_, CHECK_NEW_VERSION_DONE_EVENT);
|
||||
xEventGroupSetBits(event_group_, MAIN_EVENT_CHECK_NEW_VERSION_DONE);
|
||||
break;
|
||||
} else if (err == ESP_ERR_TIMEOUT) {
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
@ -236,7 +197,7 @@ void Application::ShowActivationCode(const std::string& code, const std::string&
|
||||
auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(),
|
||||
[digit](const digit_sound& ds) { return ds.digit == digit; });
|
||||
if (it != digit_sounds.end()) {
|
||||
PlaySound(it->sound);
|
||||
audio_service_.PlaySound(it->sound);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -248,8 +209,7 @@ void Application::Alert(const char* status, const char* message, const char* emo
|
||||
display->SetEmotion(emotion);
|
||||
display->SetChatMessage("system", message);
|
||||
if (!sound.empty()) {
|
||||
ResetDecoder();
|
||||
PlaySound(sound);
|
||||
audio_service_.PlaySound(sound);
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,59 +222,17 @@ void Application::DismissAlert() {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::PlaySound(const std::string_view& sound) {
|
||||
// Wait for the previous sound to finish
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
audio_decode_cv_.wait(lock, [this]() {
|
||||
return audio_decode_queue_.empty();
|
||||
});
|
||||
}
|
||||
background_task_->WaitForCompletion();
|
||||
|
||||
const char* data = sound.data();
|
||||
size_t size = sound.size();
|
||||
for (const char* p = data; p < data + size; ) {
|
||||
auto p3 = (BinaryProtocol3*)p;
|
||||
p += sizeof(BinaryProtocol3);
|
||||
|
||||
auto payload_size = ntohs(p3->payload_size);
|
||||
AudioStreamPacket packet;
|
||||
packet.sample_rate = 16000;
|
||||
packet.frame_duration = 60;
|
||||
packet.payload.resize(payload_size);
|
||||
memcpy(packet.payload.data(), p3->payload, payload_size);
|
||||
p += payload_size;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
audio_decode_queue_.emplace_back(std::move(packet));
|
||||
}
|
||||
}
|
||||
|
||||
void Application::EnterAudioTestingMode() {
|
||||
ESP_LOGI(TAG, "Entering audio testing mode");
|
||||
ResetDecoder();
|
||||
SetDeviceState(kDeviceStateAudioTesting);
|
||||
}
|
||||
|
||||
void Application::ExitAudioTestingMode() {
|
||||
ESP_LOGI(TAG, "Exiting audio testing mode");
|
||||
SetDeviceState(kDeviceStateWifiConfiguring);
|
||||
// Copy audio_testing_queue_ to audio_decode_queue_
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
audio_decode_queue_ = std::move(audio_testing_queue_);
|
||||
audio_decode_cv_.notify_all();
|
||||
}
|
||||
|
||||
void Application::ToggleChatState() {
|
||||
if (device_state_ == kDeviceStateActivating) {
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
return;
|
||||
} else if (device_state_ == kDeviceStateWifiConfiguring) {
|
||||
EnterAudioTestingMode();
|
||||
audio_service_.EnableAudioTesting(true);
|
||||
SetDeviceState(kDeviceStateAudioTesting);
|
||||
return;
|
||||
} else if (device_state_ == kDeviceStateAudioTesting) {
|
||||
ExitAudioTestingMode();
|
||||
audio_service_.EnableAudioTesting(false);
|
||||
SetDeviceState(kDeviceStateWifiConfiguring);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -350,7 +268,8 @@ void Application::StartListening() {
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
return;
|
||||
} else if (device_state_ == kDeviceStateWifiConfiguring) {
|
||||
EnterAudioTestingMode();
|
||||
audio_service_.EnableAudioTesting(true);
|
||||
SetDeviceState(kDeviceStateAudioTesting);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -380,7 +299,8 @@ void Application::StartListening() {
|
||||
|
||||
void Application::StopListening() {
|
||||
if (device_state_ == kDeviceStateAudioTesting) {
|
||||
ExitAudioTestingMode();
|
||||
audio_service_.EnableAudioTesting(false);
|
||||
SetDeviceState(kDeviceStateWifiConfiguring);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -409,43 +329,22 @@ void Application::Start() {
|
||||
/* Setup the display */
|
||||
auto display = board.GetDisplay();
|
||||
|
||||
/* Setup the audio codec */
|
||||
/* Setup the audio service */
|
||||
auto codec = board.GetAudioCodec();
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);
|
||||
opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
|
||||
opus_encoder_->SetComplexity(0);
|
||||
if (aec_mode_ != kAecOff) {
|
||||
ESP_LOGI(TAG, "AEC mode: %d, setting opus encoder complexity to 0", aec_mode_);
|
||||
opus_encoder_->SetComplexity(0);
|
||||
} else {
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
ESP_LOGI(TAG, "Audio processor detected, setting opus encoder complexity to 5");
|
||||
opus_encoder_->SetComplexity(5);
|
||||
#else
|
||||
ESP_LOGI(TAG, "Audio processor not detected, setting opus encoder complexity to 0");
|
||||
opus_encoder_->SetComplexity(0);
|
||||
#endif
|
||||
}
|
||||
audio_service_.Initialize(codec);
|
||||
audio_service_.Start();
|
||||
|
||||
if (codec->input_sample_rate() != 16000) {
|
||||
input_resampler_.Configure(codec->input_sample_rate(), 16000);
|
||||
reference_resampler_.Configure(codec->input_sample_rate(), 16000);
|
||||
}
|
||||
codec->Start();
|
||||
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
xTaskCreatePinnedToCore([](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
app->AudioLoop();
|
||||
vTaskDelete(NULL);
|
||||
}, "audio_loop", 4096 * 2, this, 8, &audio_loop_task_handle_, 1);
|
||||
#else
|
||||
xTaskCreate([](void* arg) {
|
||||
Application* app = (Application*)arg;
|
||||
app->AudioLoop();
|
||||
vTaskDelete(NULL);
|
||||
}, "audio_loop", 4096 * 2, this, 8, &audio_loop_task_handle_);
|
||||
#endif
|
||||
AudioServiceCallbacks callbacks;
|
||||
callbacks.on_send_queue_available = [this]() {
|
||||
xEventGroupSetBits(event_group_, MAIN_EVENT_SEND_AUDIO);
|
||||
};
|
||||
callbacks.on_wake_word_detected = [this](const std::string& wake_word) {
|
||||
xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED);
|
||||
};
|
||||
callbacks.on_vad_change = [this](bool speaking) {
|
||||
xEventGroupSetBits(event_group_, MAIN_EVENT_VAD_CHANGE);
|
||||
};
|
||||
audio_service_.SetCallbacks(callbacks);
|
||||
|
||||
/* Start the clock timer to update the status bar */
|
||||
esp_timer_start_periodic(clock_timer_handle_, 1000000);
|
||||
@ -464,9 +363,7 @@ void Application::Start() {
|
||||
display->SetStatus(Lang::Strings::LOADING_PROTOCOL);
|
||||
|
||||
// Add MCP common tools before initializing the protocol
|
||||
#if CONFIG_IOT_PROTOCOL_MCP
|
||||
McpServer::GetInstance().AddCommonTools();
|
||||
#endif
|
||||
|
||||
if (ota.HasMqttConfig()) {
|
||||
protocol_ = std::make_unique<MqttProtocol>();
|
||||
@ -478,13 +375,12 @@ void Application::Start() {
|
||||
}
|
||||
|
||||
protocol_->OnNetworkError([this](const std::string& message) {
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
|
||||
last_error_message_ = message;
|
||||
xEventGroupSetBits(event_group_, MAIN_EVENT_ERROR);
|
||||
});
|
||||
protocol_->OnIncomingAudio([this](AudioStreamPacket&& packet) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (device_state_ == kDeviceStateSpeaking && audio_decode_queue_.size() < MAX_AUDIO_PACKETS_IN_QUEUE) {
|
||||
audio_decode_queue_.emplace_back(std::move(packet));
|
||||
protocol_->OnIncomingAudio([this](std::unique_ptr<AudioStreamPacket> packet) {
|
||||
if (device_state_ == kDeviceStateSpeaking) {
|
||||
audio_service_.PushPacketToDecodeQueue(std::move(packet));
|
||||
}
|
||||
});
|
||||
protocol_->OnAudioChannelOpened([this, codec, &board]() {
|
||||
@ -493,15 +389,6 @@ void Application::Start() {
|
||||
ESP_LOGW(TAG, "Server sample rate %d does not match device output sample rate %d, resampling may cause distortion",
|
||||
protocol_->server_sample_rate(), codec->output_sample_rate());
|
||||
}
|
||||
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
protocol_->SendIotDescriptors(thing_manager.GetDescriptorsJson());
|
||||
std::string states;
|
||||
if (thing_manager.GetStatesJson(states, false)) {
|
||||
protocol_->SendIotStates(states);
|
||||
}
|
||||
#endif
|
||||
});
|
||||
protocol_->OnAudioChannelClosed([this, &board]() {
|
||||
board.SetPowerSaveMode(true);
|
||||
@ -525,7 +412,6 @@ void Application::Start() {
|
||||
});
|
||||
} else if (strcmp(state->valuestring, "stop") == 0) {
|
||||
Schedule([this]() {
|
||||
background_task_->WaitForCompletion();
|
||||
if (device_state_ == kDeviceStateSpeaking) {
|
||||
if (listening_mode_ == kListeningModeManualStop) {
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
@ -558,36 +444,11 @@ void Application::Start() {
|
||||
display->SetEmotion(emotion_str.c_str());
|
||||
});
|
||||
}
|
||||
#if CONFIG_RECEIVE_CUSTOM_MESSAGE
|
||||
} else if (strcmp(type->valuestring, "custom") == 0) {
|
||||
auto payload = cJSON_GetObjectItem(root, "payload");
|
||||
ESP_LOGI(TAG, "Received custom message: %s", cJSON_PrintUnformatted(root));
|
||||
if (cJSON_IsObject(payload)) {
|
||||
Schedule([this, display, payload_str = std::string(cJSON_PrintUnformatted(payload))]() {
|
||||
display->SetChatMessage("system", payload_str.c_str());
|
||||
});
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid custom message format: missing payload");
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_IOT_PROTOCOL_MCP
|
||||
} else if (strcmp(type->valuestring, "mcp") == 0) {
|
||||
auto payload = cJSON_GetObjectItem(root, "payload");
|
||||
if (cJSON_IsObject(payload)) {
|
||||
McpServer::GetInstance().ParseMessage(payload);
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
} else if (strcmp(type->valuestring, "iot") == 0) {
|
||||
auto commands = cJSON_GetObjectItem(root, "commands");
|
||||
if (cJSON_IsArray(commands)) {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
for (int i = 0; i < cJSON_GetArraySize(commands); ++i) {
|
||||
auto command = cJSON_GetArrayItem(commands, i);
|
||||
thing_manager.Invoke(command);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else if (strcmp(type->valuestring, "system") == 0) {
|
||||
auto command = cJSON_GetObjectItem(root, "command");
|
||||
if (cJSON_IsString(command)) {
|
||||
@ -610,112 +471,24 @@ void Application::Start() {
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Alert command requires status, message and emotion");
|
||||
}
|
||||
#if CONFIG_RECEIVE_CUSTOM_MESSAGE
|
||||
} else if (strcmp(type->valuestring, "custom") == 0) {
|
||||
auto payload = cJSON_GetObjectItem(root, "payload");
|
||||
ESP_LOGI(TAG, "Received custom message: %s", cJSON_PrintUnformatted(root));
|
||||
if (cJSON_IsObject(payload)) {
|
||||
Schedule([this, display, payload_str = std::string(cJSON_PrintUnformatted(payload))]() {
|
||||
display->SetChatMessage("system", payload_str.c_str());
|
||||
});
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid custom message format: missing payload");
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown message type: %s", type->valuestring);
|
||||
}
|
||||
});
|
||||
bool protocol_started = protocol_->Start();
|
||||
|
||||
audio_debugger_ = std::make_unique<AudioDebugger>();
|
||||
audio_processor_->Initialize(codec);
|
||||
audio_processor_->OnOutput([this](std::vector<int16_t>&& data) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (audio_send_queue_.size() >= MAX_AUDIO_PACKETS_IN_QUEUE) {
|
||||
ESP_LOGW(TAG, "Too many audio packets in queue, drop the newest packet");
|
||||
return;
|
||||
}
|
||||
}
|
||||
background_task_->Schedule([this, data = std::move(data)]() mutable {
|
||||
opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
|
||||
AudioStreamPacket packet;
|
||||
packet.payload = std::move(opus);
|
||||
#ifdef CONFIG_USE_SERVER_AEC
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(timestamp_mutex_);
|
||||
if (!timestamp_queue_.empty()) {
|
||||
packet.timestamp = timestamp_queue_.front();
|
||||
timestamp_queue_.pop_front();
|
||||
} else {
|
||||
packet.timestamp = 0;
|
||||
}
|
||||
|
||||
if (timestamp_queue_.size() > 3) { // 限制队列长度3
|
||||
timestamp_queue_.pop_front(); // 该包发送前先出队保持队列长度
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (audio_send_queue_.size() >= MAX_AUDIO_PACKETS_IN_QUEUE) {
|
||||
ESP_LOGW(TAG, "Too many audio packets in queue, drop the oldest packet");
|
||||
audio_send_queue_.pop_front();
|
||||
}
|
||||
audio_send_queue_.emplace_back(std::move(packet));
|
||||
xEventGroupSetBits(event_group_, SEND_AUDIO_EVENT);
|
||||
});
|
||||
});
|
||||
});
|
||||
audio_processor_->OnVadStateChange([this](bool speaking) {
|
||||
if (device_state_ == kDeviceStateListening) {
|
||||
Schedule([this, speaking]() {
|
||||
if (speaking) {
|
||||
voice_detected_ = true;
|
||||
} else {
|
||||
voice_detected_ = false;
|
||||
}
|
||||
auto led = Board::GetInstance().GetLed();
|
||||
led->OnStateChanged();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wake_word_->Initialize(codec);
|
||||
wake_word_->OnWakeWordDetected([this](const std::string& wake_word) {
|
||||
Schedule([this, &wake_word]() {
|
||||
if (!protocol_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device_state_ == kDeviceStateIdle) {
|
||||
wake_word_->EncodeWakeWordData();
|
||||
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
SetDeviceState(kDeviceStateConnecting);
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
wake_word_->StartDetection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
|
||||
#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
AudioStreamPacket packet;
|
||||
// Encode and send the wake word data to the server
|
||||
while (wake_word_->GetWakeWordOpus(packet.payload)) {
|
||||
protocol_->SendAudio(packet);
|
||||
}
|
||||
// Set the chat state to wake word detected
|
||||
protocol_->SendWakeWordDetected(wake_word);
|
||||
#else
|
||||
// Play the pop up sound to indicate the wake word is detected
|
||||
// And wait 60ms to make sure the queue has been processed by audio task
|
||||
ResetDecoder();
|
||||
PlaySound(Lang::Sounds::P3_POPUP);
|
||||
vTaskDelay(pdMS_TO_TICKS(60));
|
||||
#endif
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
} else if (device_state_ == kDeviceStateSpeaking) {
|
||||
AbortSpeaking(kAbortReasonWakeWordDetected);
|
||||
} else if (device_state_ == kDeviceStateActivating) {
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
}
|
||||
});
|
||||
});
|
||||
wake_word_->StartDetection();
|
||||
|
||||
// Wait for the new version check to finish
|
||||
xEventGroupWaitBits(event_group_, CHECK_NEW_VERSION_DONE_EVENT, pdTRUE, pdFALSE, portMAX_DELAY);
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
|
||||
has_server_time_ = ota.HasServerTime();
|
||||
@ -724,8 +497,7 @@ void Application::Start() {
|
||||
display->ShowNotification(message.c_str());
|
||||
display->SetChatMessage("system", "");
|
||||
// Play the success sound to indicate the device is ready
|
||||
ResetDecoder();
|
||||
PlaySound(Lang::Sounds::P3_SUCCESS);
|
||||
audio_service_.PlaySound(Lang::Sounds::P3_SUCCESS);
|
||||
}
|
||||
|
||||
// Print heap stats
|
||||
@ -746,19 +518,6 @@ void Application::OnClockTimer() {
|
||||
// SystemInfo::PrintTaskCpuUsage(pdMS_TO_TICKS(1000));
|
||||
// SystemInfo::PrintTaskList();
|
||||
SystemInfo::PrintHeapStats();
|
||||
|
||||
// If we have synchronized server time, set the status to clock "HH:MM" if the device is idle
|
||||
if (has_server_time_) {
|
||||
if (device_state_ == kDeviceStateIdle) {
|
||||
Schedule([this]() {
|
||||
// Set status to clock "HH:MM"
|
||||
time_t now = time(NULL);
|
||||
char time_str[64];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M ", localtime(&now));
|
||||
Board::GetInstance().GetDisplay()->SetStatus(time_str);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -768,7 +527,7 @@ void Application::Schedule(std::function<void()> callback) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
main_tasks_.push_back(std::move(callback));
|
||||
}
|
||||
xEventGroupSetBits(event_group_, SCHEDULE_EVENT);
|
||||
xEventGroupSetBits(event_group_, MAIN_EVENT_SCHEDULE);
|
||||
}
|
||||
|
||||
// The Main Event Loop controls the chat state and websocket connection
|
||||
@ -779,20 +538,36 @@ void Application::MainEventLoop() {
|
||||
vTaskPrioritySet(NULL, 3);
|
||||
|
||||
while (true) {
|
||||
auto bits = xEventGroupWaitBits(event_group_, SCHEDULE_EVENT | SEND_AUDIO_EVENT, pdTRUE, pdFALSE, portMAX_DELAY);
|
||||
auto bits = xEventGroupWaitBits(event_group_, MAIN_EVENT_SCHEDULE |
|
||||
MAIN_EVENT_SEND_AUDIO |
|
||||
MAIN_EVENT_WAKE_WORD_DETECTED |
|
||||
MAIN_EVENT_VAD_CHANGE |
|
||||
MAIN_EVENT_ERROR, pdTRUE, pdFALSE, portMAX_DELAY);
|
||||
if (bits & MAIN_EVENT_ERROR) {
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
Alert(Lang::Strings::ERROR, last_error_message_.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
|
||||
}
|
||||
|
||||
if (bits & SEND_AUDIO_EVENT) {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
auto packets = std::move(audio_send_queue_);
|
||||
lock.unlock();
|
||||
for (auto& packet : packets) {
|
||||
if (!protocol_->SendAudio(packet)) {
|
||||
if (bits & MAIN_EVENT_SEND_AUDIO) {
|
||||
while (auto packet = audio_service_.PopPacketFromSendQueue()) {
|
||||
if (!protocol_->SendAudio(std::move(packet))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bits & SCHEDULE_EVENT) {
|
||||
if (bits & MAIN_EVENT_WAKE_WORD_DETECTED) {
|
||||
OnWakeWordDetected();
|
||||
}
|
||||
|
||||
if (bits & MAIN_EVENT_VAD_CHANGE) {
|
||||
if (device_state_ == kDeviceStateListening) {
|
||||
auto led = Board::GetInstance().GetLed();
|
||||
led->OnStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
if (bits & MAIN_EVENT_SCHEDULE) {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
auto tasks = std::move(main_tasks_);
|
||||
lock.unlock();
|
||||
@ -803,170 +578,43 @@ void Application::MainEventLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// The Audio Loop is used to input and output audio data
|
||||
void Application::AudioLoop() {
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
while (true) {
|
||||
OnAudioInput();
|
||||
if (codec->output_enabled()) {
|
||||
OnAudioOutput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Application::OnAudioOutput() {
|
||||
if (busy_decoding_audio_) {
|
||||
void Application::OnWakeWordDetected() {
|
||||
if (!protocol_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
const int max_silence_seconds = 10;
|
||||
if (device_state_ == kDeviceStateIdle) {
|
||||
audio_service_.EncodeWakeWord();
|
||||
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
if (audio_decode_queue_.empty()) {
|
||||
// Disable the output if there is no audio data for a long time
|
||||
if (device_state_ == kDeviceStateIdle) {
|
||||
auto duration = std::chrono::duration_cast<std::chrono::seconds>(now - last_output_time_).count();
|
||||
if (duration > max_silence_seconds) {
|
||||
codec->EnableOutput(false);
|
||||
if (!protocol_->IsAudioChannelOpened()) {
|
||||
SetDeviceState(kDeviceStateConnecting);
|
||||
if (!protocol_->OpenAudioChannel()) {
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto packet = std::move(audio_decode_queue_.front());
|
||||
audio_decode_queue_.pop_front();
|
||||
lock.unlock();
|
||||
audio_decode_cv_.notify_all();
|
||||
|
||||
// Synchronize the sample rate and frame duration
|
||||
SetDecodeSampleRate(packet.sample_rate, packet.frame_duration);
|
||||
|
||||
busy_decoding_audio_ = true;
|
||||
if (!background_task_->Schedule([this, codec, packet = std::move(packet)]() mutable {
|
||||
busy_decoding_audio_ = false;
|
||||
if (aborted_) {
|
||||
return;
|
||||
auto wake_word = audio_service_.GetLastWakeWord();
|
||||
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
|
||||
#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
// Encode and send the wake word data to the server
|
||||
while (auto packet = audio_service_.PopWakeWordPacket()) {
|
||||
protocol_->SendAudio(std::move(packet));
|
||||
}
|
||||
|
||||
std::vector<int16_t> pcm;
|
||||
if (!opus_decoder_->Decode(std::move(packet.payload), pcm)) {
|
||||
return;
|
||||
}
|
||||
// Resample if the sample rate is different
|
||||
if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
|
||||
int target_size = output_resampler_.GetOutputSamples(pcm.size());
|
||||
std::vector<int16_t> resampled(target_size);
|
||||
output_resampler_.Process(pcm.data(), pcm.size(), resampled.data());
|
||||
pcm = std::move(resampled);
|
||||
}
|
||||
codec->OutputData(pcm);
|
||||
#ifdef CONFIG_USE_SERVER_AEC
|
||||
std::lock_guard<std::mutex> lock(timestamp_mutex_);
|
||||
timestamp_queue_.push_back(packet.timestamp);
|
||||
// Set the chat state to wake word detected
|
||||
protocol_->SendWakeWordDetected(wake_word);
|
||||
#else
|
||||
// Play the pop up sound to indicate the wake word is detected
|
||||
audio_service_.PlaySound(Lang::Sounds::P3_POPUP);
|
||||
#endif
|
||||
last_output_time_ = std::chrono::steady_clock::now();
|
||||
})) {
|
||||
busy_decoding_audio_ = false;
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
} else if (device_state_ == kDeviceStateSpeaking) {
|
||||
AbortSpeaking(kAbortReasonWakeWordDetected);
|
||||
} else if (device_state_ == kDeviceStateActivating) {
|
||||
SetDeviceState(kDeviceStateIdle);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::OnAudioInput() {
|
||||
if (device_state_ == kDeviceStateAudioTesting) {
|
||||
if (audio_testing_queue_.size() >= AUDIO_TESTING_MAX_DURATION_MS / OPUS_FRAME_DURATION_MS) {
|
||||
ExitAudioTestingMode();
|
||||
return;
|
||||
}
|
||||
std::vector<int16_t> data;
|
||||
int samples = OPUS_FRAME_DURATION_MS * 16000 / 1000;
|
||||
if (ReadAudio(data, 16000, samples)) {
|
||||
background_task_->Schedule([this, data = std::move(data)]() mutable {
|
||||
opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
|
||||
AudioStreamPacket packet;
|
||||
packet.payload = std::move(opus);
|
||||
packet.frame_duration = OPUS_FRAME_DURATION_MS;
|
||||
packet.sample_rate = 16000;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
audio_testing_queue_.push_back(std::move(packet));
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (wake_word_->IsDetectionRunning()) {
|
||||
std::vector<int16_t> data;
|
||||
int samples = wake_word_->GetFeedSize();
|
||||
if (samples > 0) {
|
||||
if (ReadAudio(data, 16000, samples)) {
|
||||
wake_word_->Feed(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (audio_processor_->IsRunning()) {
|
||||
std::vector<int16_t> data;
|
||||
int samples = audio_processor_->GetFeedSize();
|
||||
if (samples > 0) {
|
||||
if (ReadAudio(data, 16000, samples)) {
|
||||
audio_processor_->Feed(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(OPUS_FRAME_DURATION_MS / 2));
|
||||
}
|
||||
|
||||
bool Application::ReadAudio(std::vector<int16_t>& data, int sample_rate, int samples) {
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
if (!codec->input_enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (codec->input_sample_rate() != sample_rate) {
|
||||
data.resize(samples * codec->input_sample_rate() / sample_rate);
|
||||
if (!codec->InputData(data)) {
|
||||
return false;
|
||||
}
|
||||
if (codec->input_channels() == 2) {
|
||||
auto mic_channel = std::vector<int16_t>(data.size() / 2);
|
||||
auto reference_channel = std::vector<int16_t>(data.size() / 2);
|
||||
for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) {
|
||||
mic_channel[i] = data[j];
|
||||
reference_channel[i] = data[j + 1];
|
||||
}
|
||||
auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size()));
|
||||
auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size()));
|
||||
input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data());
|
||||
reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data());
|
||||
data.resize(resampled_mic.size() + resampled_reference.size());
|
||||
for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) {
|
||||
data[j] = resampled_mic[i];
|
||||
data[j + 1] = resampled_reference[i];
|
||||
}
|
||||
} else {
|
||||
auto resampled = std::vector<int16_t>(input_resampler_.GetOutputSamples(data.size()));
|
||||
input_resampler_.Process(data.data(), data.size(), resampled.data());
|
||||
data = std::move(resampled);
|
||||
}
|
||||
} else {
|
||||
data.resize(samples);
|
||||
if (!codec->InputData(data)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 音频调试:发送原始音频数据
|
||||
if (audio_debugger_) {
|
||||
audio_debugger_->Feed(data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::AbortSpeaking(AbortReason reason) {
|
||||
ESP_LOGI(TAG, "Abort speaking");
|
||||
aborted_ = true;
|
||||
@ -987,8 +635,6 @@ void Application::SetDeviceState(DeviceState state) {
|
||||
auto previous_state = device_state_;
|
||||
device_state_ = state;
|
||||
ESP_LOGI(TAG, "STATE: %s", STATE_STRINGS[device_state_]);
|
||||
// The state is changed, wait for all background tasks to finish
|
||||
background_task_->WaitForCompletion();
|
||||
|
||||
// Send the state change event
|
||||
DeviceStateEventManager::GetInstance().PostStateChangeEvent(previous_state, state);
|
||||
@ -1002,51 +648,39 @@ void Application::SetDeviceState(DeviceState state) {
|
||||
case kDeviceStateIdle:
|
||||
display->SetStatus(Lang::Strings::STANDBY);
|
||||
display->SetEmotion("neutral");
|
||||
audio_processor_->Stop();
|
||||
wake_word_->StartDetection();
|
||||
audio_service_.EnableVoiceProcessing(false);
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
break;
|
||||
case kDeviceStateConnecting:
|
||||
display->SetStatus(Lang::Strings::CONNECTING);
|
||||
display->SetEmotion("neutral");
|
||||
display->SetChatMessage("system", "");
|
||||
timestamp_queue_.clear();
|
||||
break;
|
||||
case kDeviceStateListening:
|
||||
display->SetStatus(Lang::Strings::LISTENING);
|
||||
display->SetEmotion("neutral");
|
||||
// Update the IoT states before sending the start listening command
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
UpdateIotStates();
|
||||
#endif
|
||||
|
||||
// Make sure the audio processor is running
|
||||
if (!audio_processor_->IsRunning()) {
|
||||
if (!audio_service_.IsAudioProcessorRunning()) {
|
||||
// Send the start listening command
|
||||
protocol_->SendStartListening(listening_mode_);
|
||||
if (previous_state == kDeviceStateSpeaking) {
|
||||
audio_decode_queue_.clear();
|
||||
audio_decode_cv_.notify_all();
|
||||
// FIXME: Wait for the speaker to empty the buffer
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
}
|
||||
opus_encoder_->ResetState();
|
||||
audio_processor_->Start();
|
||||
wake_word_->StopDetection();
|
||||
audio_service_.EnableVoiceProcessing(true);
|
||||
audio_service_.EnableWakeWordDetection(false);
|
||||
}
|
||||
break;
|
||||
case kDeviceStateSpeaking:
|
||||
display->SetStatus(Lang::Strings::SPEAKING);
|
||||
|
||||
if (listening_mode_ != kListeningModeRealtime) {
|
||||
audio_processor_->Stop();
|
||||
audio_service_.EnableVoiceProcessing(false);
|
||||
// Only AFE wake word can be detected in speaking mode
|
||||
#if CONFIG_USE_AFE_WAKE_WORD
|
||||
wake_word_->StartDetection();
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
#else
|
||||
wake_word_->StopDetection();
|
||||
audio_service_.EnableWakeWordDetection(false);
|
||||
#endif
|
||||
}
|
||||
ResetDecoder();
|
||||
audio_service_.ResetDecoder();
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
@ -1054,41 +688,6 @@ void Application::SetDeviceState(DeviceState state) {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::ResetDecoder() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
opus_decoder_->ResetState();
|
||||
audio_decode_queue_.clear();
|
||||
audio_decode_cv_.notify_all();
|
||||
last_output_time_ = std::chrono::steady_clock::now();
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
codec->EnableOutput(true);
|
||||
}
|
||||
|
||||
void Application::SetDecodeSampleRate(int sample_rate, int frame_duration) {
|
||||
if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
opus_decoder_.reset();
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, frame_duration);
|
||||
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
|
||||
ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate());
|
||||
output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate());
|
||||
}
|
||||
}
|
||||
|
||||
void Application::UpdateIotStates() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
std::string states;
|
||||
if (thing_manager.GetStatesJson(states, true)) {
|
||||
protocol_->SendIotStates(states);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::Reboot() {
|
||||
ESP_LOGI(TAG, "Rebooting...");
|
||||
esp_restart();
|
||||
@ -1124,6 +723,10 @@ bool Application::CanEnterSleepMode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!audio_service_.IsIdle()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now it is safe to enter sleep mode
|
||||
return true;
|
||||
}
|
||||
@ -1143,15 +746,15 @@ void Application::SetAecMode(AecMode mode) {
|
||||
auto display = board.GetDisplay();
|
||||
switch (aec_mode_) {
|
||||
case kAecOff:
|
||||
audio_processor_->EnableDeviceAec(false);
|
||||
audio_service_.EnableDeviceAec(false);
|
||||
display->ShowNotification(Lang::Strings::RTC_MODE_OFF);
|
||||
break;
|
||||
case kAecOnServerSide:
|
||||
audio_processor_->EnableDeviceAec(false);
|
||||
audio_service_.EnableDeviceAec(false);
|
||||
display->ShowNotification(Lang::Strings::RTC_MODE_ON);
|
||||
break;
|
||||
case kAecOnDeviceSide:
|
||||
audio_processor_->EnableDeviceAec(true);
|
||||
audio_service_.EnableDeviceAec(true);
|
||||
display->ShowNotification(Lang::Strings::RTC_MODE_ON);
|
||||
break;
|
||||
}
|
||||
@ -1162,3 +765,7 @@ void Application::SetAecMode(AecMode mode) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Application::PlaySound(const std::string_view& sound) {
|
||||
audio_service_.PlaySound(sound);
|
||||
}
|
||||
@ -8,26 +8,21 @@
|
||||
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <list>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
|
||||
#include <opus_encoder.h>
|
||||
#include <opus_decoder.h>
|
||||
#include <opus_resampler.h>
|
||||
|
||||
#include "protocol.h"
|
||||
#include "ota.h"
|
||||
#include "background_task.h"
|
||||
#include "audio_processor.h"
|
||||
#include "wake_word.h"
|
||||
#include "audio_debugger.h"
|
||||
#include "audio_service.h"
|
||||
#include "device_state_event.h"
|
||||
|
||||
#define SCHEDULE_EVENT (1 << 0)
|
||||
#define SEND_AUDIO_EVENT (1 << 1)
|
||||
#define CHECK_NEW_VERSION_DONE_EVENT (1 << 2)
|
||||
#define MAIN_EVENT_SCHEDULE (1 << 0)
|
||||
#define MAIN_EVENT_SEND_AUDIO (1 << 1)
|
||||
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
|
||||
#define MAIN_EVENT_VAD_CHANGE (1 << 3)
|
||||
#define MAIN_EVENT_ERROR (1 << 4)
|
||||
#define MAIN_EVENT_CHECK_NEW_VERSION_DONE (1 << 5)
|
||||
|
||||
enum AecMode {
|
||||
kAecOff,
|
||||
@ -35,10 +30,6 @@ enum AecMode {
|
||||
kAecOnServerSide,
|
||||
};
|
||||
|
||||
#define OPUS_FRAME_DURATION_MS 60
|
||||
#define MAX_AUDIO_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS)
|
||||
#define AUDIO_TESTING_MAX_DURATION_MS 10000
|
||||
|
||||
class Application {
|
||||
public:
|
||||
static Application& GetInstance() {
|
||||
@ -51,7 +42,7 @@ public:
|
||||
|
||||
void Start();
|
||||
DeviceState GetDeviceState() const { return device_state_; }
|
||||
bool IsVoiceDetected() const { return voice_detected_; }
|
||||
bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); }
|
||||
void Schedule(std::function<void()> callback);
|
||||
void SetDeviceState(DeviceState state);
|
||||
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");
|
||||
@ -60,72 +51,41 @@ public:
|
||||
void ToggleChatState();
|
||||
void StartListening();
|
||||
void StopListening();
|
||||
void UpdateIotStates();
|
||||
void Reboot();
|
||||
void WakeWordInvoke(const std::string& wake_word);
|
||||
void PlaySound(const std::string_view& sound);
|
||||
bool CanEnterSleepMode();
|
||||
void SendMcpMessage(const std::string& payload);
|
||||
void SetAecMode(AecMode mode);
|
||||
bool ReadAudio(std::vector<int16_t>& data, int sample_rate, int samples);
|
||||
AecMode GetAecMode() const { return aec_mode_; }
|
||||
BackgroundTask* GetBackgroundTask() const { return background_task_; }
|
||||
void PlaySound(const std::string_view& sound);
|
||||
AudioService& GetAudioService() { return audio_service_; }
|
||||
|
||||
private:
|
||||
Application();
|
||||
~Application();
|
||||
|
||||
std::unique_ptr<WakeWord> wake_word_;
|
||||
std::unique_ptr<AudioProcessor> audio_processor_;
|
||||
std::unique_ptr<AudioDebugger> audio_debugger_;
|
||||
std::mutex mutex_;
|
||||
std::list<std::function<void()>> main_tasks_;
|
||||
std::deque<std::function<void()>> main_tasks_;
|
||||
std::unique_ptr<Protocol> protocol_;
|
||||
EventGroupHandle_t event_group_ = nullptr;
|
||||
esp_timer_handle_t clock_timer_handle_ = nullptr;
|
||||
volatile DeviceState device_state_ = kDeviceStateUnknown;
|
||||
ListeningMode listening_mode_ = kListeningModeAutoStop;
|
||||
AecMode aec_mode_ = kAecOff;
|
||||
std::string last_error_message_;
|
||||
AudioService audio_service_;
|
||||
|
||||
bool has_server_time_ = false;
|
||||
bool aborted_ = false;
|
||||
bool voice_detected_ = false;
|
||||
bool busy_decoding_audio_ = false;
|
||||
int clock_ticks_ = 0;
|
||||
TaskHandle_t check_new_version_task_handle_ = nullptr;
|
||||
|
||||
// Audio encode / decode
|
||||
TaskHandle_t audio_loop_task_handle_ = nullptr;
|
||||
BackgroundTask* background_task_ = nullptr;
|
||||
std::chrono::steady_clock::time_point last_output_time_;
|
||||
std::list<AudioStreamPacket> audio_send_queue_;
|
||||
std::list<AudioStreamPacket> audio_decode_queue_;
|
||||
std::condition_variable audio_decode_cv_;
|
||||
std::list<AudioStreamPacket> audio_testing_queue_;
|
||||
|
||||
// 新增:用于维护音频包的timestamp队列
|
||||
std::list<uint32_t> timestamp_queue_;
|
||||
std::mutex timestamp_mutex_;
|
||||
|
||||
std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
|
||||
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
|
||||
|
||||
OpusResampler input_resampler_;
|
||||
OpusResampler reference_resampler_;
|
||||
OpusResampler output_resampler_;
|
||||
|
||||
void MainEventLoop();
|
||||
void OnAudioInput();
|
||||
void OnAudioOutput();
|
||||
void ResetDecoder();
|
||||
void SetDecodeSampleRate(int sample_rate, int frame_duration);
|
||||
void OnWakeWordDetected();
|
||||
void CheckNewVersion(Ota& ota);
|
||||
void ShowActivationCode(const std::string& code, const std::string& message);
|
||||
void OnClockTimer();
|
||||
void SetListeningMode(ListeningMode mode);
|
||||
void AudioLoop();
|
||||
void EnterAudioTestingMode();
|
||||
void ExitAudioTestingMode();
|
||||
};
|
||||
|
||||
#endif // _APPLICATION_H_
|
||||
|
||||
88
main/audio/README.md
Normal file
88
main/audio/README.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Audio Service Architecture
|
||||
|
||||
The audio service is a core component responsible for managing all audio-related functionalities, including capturing audio from the microphone, processing it, encoding/decoding, and playing back audio through the speaker. It is designed to be modular and efficient, running its main operations in dedicated FreeRTOS tasks to ensure real-time performance.
|
||||
|
||||
## Key Components
|
||||
|
||||
- **`AudioService`**: The central orchestrator. It initializes and manages all other audio components, tasks, and data queues.
|
||||
- **`AudioCodec`**: A hardware abstraction layer (HAL) for the physical audio codec chip. It handles the raw I2S communication for audio input and output.
|
||||
- **`AudioProcessor`**: Performs real-time audio processing on the microphone input stream. This typically includes Acoustic Echo Cancellation (AEC), noise suppression, and Voice Activity Detection (VAD). `AfeAudioProcessor` is the default implementation, utilizing the ESP-ADF Audio Front-End.
|
||||
- **`WakeWord`**: Detects keywords (e.g., "你好,小智", "Hi, ESP") from the audio stream. It runs independently from the main audio processor until a wake word is detected.
|
||||
- **`OpusEncoderWrapper` / `OpusDecoderWrapper`**: Manages the encoding of PCM audio to the Opus format and decoding Opus packets back to PCM. Opus is used for its high compression and low latency, making it ideal for voice streaming.
|
||||
- **`OpusResampler`**: A utility to convert audio streams between different sample rates (e.g., resampling from the codec's native sample rate to the required 16kHz for processing).
|
||||
|
||||
## Threading Model
|
||||
|
||||
The service operates on three primary tasks to handle the different stages of the audio pipeline concurrently:
|
||||
|
||||
1. **`AudioInputTask`**: Solely responsible for reading raw PCM data from the `AudioCodec`. It then feeds this data to either the `WakeWord` engine or the `AudioProcessor` based on the current state.
|
||||
2. **`AudioOutputTask`**: Responsible for playing audio. It retrieves decoded PCM data from the `audio_playback_queue_` and sends it to the `AudioCodec` to be played on the speaker.
|
||||
3. **`OpusCodecTask`**: A worker task that handles both encoding and decoding. It fetches raw audio from `audio_encode_queue_`, encodes it into Opus packets, and places them in the `audio_send_queue_`. Concurrently, it fetches Opus packets from `audio_decode_queue_`, decodes them into PCM, and places the result in the `audio_playback_queue_`.
|
||||
|
||||
## Data Flow
|
||||
|
||||
There are two primary data flows: audio input (uplink) and audio output (downlink).
|
||||
|
||||
### 1. Audio Input (Uplink) Flow
|
||||
|
||||
This flow captures audio from the microphone, processes it, encodes it, and prepares it for sending to a server.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Device
|
||||
Mic[("Microphone")] -->|I2S| Codec(AudioCodec)
|
||||
|
||||
subgraph AudioInputTask
|
||||
Codec -->|Raw PCM| Read(ReadAudioData)
|
||||
Read -->|16kHz PCM| Processor(AudioProcessor)
|
||||
end
|
||||
|
||||
subgraph OpusCodecTask
|
||||
Processor -->|Clean PCM| EncodeQueue(audio_encode_queue_)
|
||||
EncodeQueue --> Encoder(OpusEncoder)
|
||||
Encoder -->|Opus Packet| SendQueue(audio_send_queue_)
|
||||
end
|
||||
|
||||
SendQueue --> |"PopPacketFromSendQueue()"| App(Application Layer)
|
||||
end
|
||||
|
||||
App -->|Network| Server((Cloud Server))
|
||||
```
|
||||
|
||||
- The `AudioInputTask` continuously reads raw PCM data from the `AudioCodec`.
|
||||
- This data is fed into an `AudioProcessor` for cleaning (AEC, VAD).
|
||||
- The processed PCM data is pushed into the `audio_encode_queue_`.
|
||||
- The `OpusCodecTask` picks up the PCM data, encodes it into Opus format, and pushes the resulting packet to the `audio_send_queue_`.
|
||||
- The application can then retrieve these Opus packets and send them over the network.
|
||||
|
||||
### 2. Audio Output (Downlink) Flow
|
||||
|
||||
This flow receives encoded audio data, decodes it, and plays it on the speaker.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Server((Cloud Server)) -->|Network| App(Application Layer)
|
||||
|
||||
subgraph Device
|
||||
App -->|"PushPacketToDecodeQueue()"| DecodeQueue(audio_decode_queue_)
|
||||
|
||||
subgraph OpusCodecTask
|
||||
DecodeQueue -->|Opus Packet| Decoder(OpusDecoder)
|
||||
Decoder -->|PCM| PlaybackQueue(audio_playback_queue_)
|
||||
end
|
||||
|
||||
subgraph AudioOutputTask
|
||||
PlaybackQueue -->|PCM| Codec(AudioCodec)
|
||||
end
|
||||
|
||||
Codec -->|I2S| Speaker[("Speaker")]
|
||||
end
|
||||
```
|
||||
|
||||
- The application receives Opus packets from the network and pushes them into the `audio_decode_queue_`.
|
||||
- The `OpusCodecTask` retrieves these packets, decodes them back into PCM data, and pushes the data to the `audio_playback_queue_`.
|
||||
- The `AudioOutputTask` takes the PCM data from the queue and sends it to the `AudioCodec` for playback.
|
||||
|
||||
## Power Management
|
||||
|
||||
To conserve energy, the audio codec's input (ADC) and output (DAC) channels are automatically disabled after a period of inactivity (`AUDIO_POWER_TIMEOUT_MS`). A timer (`audio_power_timer_`) periodically checks for activity and manages the power state. The channels are automatically re-enabled when new audio needs to be captured or played.
|
||||
544
main/audio/audio_service.cc
Normal file
544
main/audio/audio_service.cc
Normal file
@ -0,0 +1,544 @@
|
||||
#include "audio_service.h"
|
||||
#include <esp_log.h>
|
||||
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
#include "processors/afe_audio_processor.h"
|
||||
#else
|
||||
#include "processors/no_audio_processor.h"
|
||||
#endif
|
||||
|
||||
#if CONFIG_USE_AFE_WAKE_WORD
|
||||
#include "wake_words/afe_wake_word.h"
|
||||
#elif CONFIG_USE_ESP_WAKE_WORD
|
||||
#include "wake_words/esp_wake_word.h"
|
||||
#elif CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
#include "wake_words/custom_wake_word.h"
|
||||
#endif
|
||||
|
||||
#define TAG "AudioService"
|
||||
|
||||
|
||||
AudioService::AudioService() {
|
||||
event_group_ = xEventGroupCreate();
|
||||
}
|
||||
|
||||
AudioService::~AudioService() {
|
||||
if (event_group_ != nullptr) {
|
||||
vEventGroupDelete(event_group_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AudioService::Initialize(AudioCodec* codec) {
|
||||
codec_ = codec;
|
||||
codec_->Start();
|
||||
|
||||
/* Setup the audio codec */
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);
|
||||
opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
|
||||
opus_encoder_->SetComplexity(0);
|
||||
|
||||
if (codec->input_sample_rate() != 16000) {
|
||||
input_resampler_.Configure(codec->input_sample_rate(), 16000);
|
||||
reference_resampler_.Configure(codec->input_sample_rate(), 16000);
|
||||
}
|
||||
|
||||
audio_debugger_ = std::make_unique<AudioDebugger>();
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
audio_processor_ = std::make_unique<AfeAudioProcessor>();
|
||||
#else
|
||||
audio_processor_ = std::make_unique<NoAudioProcessor>();
|
||||
#endif
|
||||
|
||||
#if CONFIG_USE_AFE_WAKE_WORD
|
||||
wake_word_ = std::make_unique<AfeWakeWord>();
|
||||
#elif CONFIG_USE_ESP_WAKE_WORD
|
||||
wake_word_ = std::make_unique<EspWakeWord>();
|
||||
#elif CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
wake_word_ = std::make_unique<CustomWakeWord>();
|
||||
#else
|
||||
wake_word_ = nullptr;
|
||||
#endif
|
||||
|
||||
audio_processor_->OnOutput([this](std::vector<int16_t>&& data) {
|
||||
PushTaskToEncodeQueue(kAudioTaskTypeEncodeToSendQueue, std::move(data));
|
||||
});
|
||||
|
||||
audio_processor_->OnVadStateChange([this](bool speaking) {
|
||||
voice_detected_ = speaking;
|
||||
if (callbacks_.on_vad_change) {
|
||||
callbacks_.on_vad_change(speaking);
|
||||
}
|
||||
});
|
||||
|
||||
if (wake_word_) {
|
||||
wake_word_->OnWakeWordDetected([this](const std::string& wake_word) {
|
||||
if (callbacks_.on_wake_word_detected) {
|
||||
callbacks_.on_wake_word_detected(wake_word);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
esp_timer_create_args_t audio_power_timer_args = {
|
||||
.callback = [](void* arg) {
|
||||
AudioService* audio_service = (AudioService*)arg;
|
||||
audio_service->CheckAndUpdateAudioPowerState();
|
||||
},
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "audio_power_timer",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
esp_timer_create(&audio_power_timer_args, &audio_power_timer_);
|
||||
}
|
||||
|
||||
void AudioService::Start() {
|
||||
service_stopped_ = false;
|
||||
xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING | AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING);
|
||||
|
||||
esp_timer_start_periodic(audio_power_timer_, 1000000);
|
||||
|
||||
/* Start the audio input task */
|
||||
#if CONFIG_USE_AUDIO_PROCESSOR
|
||||
xTaskCreatePinnedToCore([](void* arg) {
|
||||
AudioService* audio_service = (AudioService*)arg;
|
||||
audio_service->AudioInputTask();
|
||||
vTaskDelete(NULL);
|
||||
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_, 1);
|
||||
#else
|
||||
xTaskCreate([](void* arg) {
|
||||
AudioService* audio_service = (AudioService*)arg;
|
||||
audio_service->AudioInputTask();
|
||||
vTaskDelete(NULL);
|
||||
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_);
|
||||
#endif
|
||||
|
||||
/* Start the audio output task */
|
||||
xTaskCreate([](void* arg) {
|
||||
AudioService* audio_service = (AudioService*)arg;
|
||||
audio_service->AudioOutputTask();
|
||||
vTaskDelete(NULL);
|
||||
}, "audio_output", 2048, this, 3, &audio_output_task_handle_);
|
||||
|
||||
/* Start the opus codec task */
|
||||
xTaskCreate([](void* arg) {
|
||||
AudioService* audio_service = (AudioService*)arg;
|
||||
audio_service->OpusCodecTask();
|
||||
vTaskDelete(NULL);
|
||||
}, "opus_codec", 4096 * 7, this, 2, &opus_codec_task_handle_);
|
||||
}
|
||||
|
||||
void AudioService::Stop() {
|
||||
esp_timer_stop(audio_power_timer_);
|
||||
service_stopped_ = true;
|
||||
xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING |
|
||||
AS_EVENT_WAKE_WORD_RUNNING |
|
||||
AS_EVENT_AUDIO_PROCESSOR_RUNNING);
|
||||
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_encode_queue_.clear();
|
||||
audio_decode_queue_.clear();
|
||||
audio_playback_queue_.clear();
|
||||
audio_testing_queue_.clear();
|
||||
audio_queue_cv_.notify_all();
|
||||
}
|
||||
|
||||
bool AudioService::ReadAudioData(std::vector<int16_t>& data, int sample_rate, int samples) {
|
||||
if (!codec_->input_enabled()) {
|
||||
codec_->EnableInput(true);
|
||||
esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000);
|
||||
}
|
||||
|
||||
if (codec_->input_sample_rate() != sample_rate) {
|
||||
data.resize(samples * codec_->input_sample_rate() / sample_rate);
|
||||
if (!codec_->InputData(data)) {
|
||||
return false;
|
||||
}
|
||||
if (codec_->input_channels() == 2) {
|
||||
auto mic_channel = std::vector<int16_t>(data.size() / 2);
|
||||
auto reference_channel = std::vector<int16_t>(data.size() / 2);
|
||||
for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) {
|
||||
mic_channel[i] = data[j];
|
||||
reference_channel[i] = data[j + 1];
|
||||
}
|
||||
auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size()));
|
||||
auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size()));
|
||||
input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data());
|
||||
reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data());
|
||||
data.resize(resampled_mic.size() + resampled_reference.size());
|
||||
for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) {
|
||||
data[j] = resampled_mic[i];
|
||||
data[j + 1] = resampled_reference[i];
|
||||
}
|
||||
} else {
|
||||
auto resampled = std::vector<int16_t>(input_resampler_.GetOutputSamples(data.size()));
|
||||
input_resampler_.Process(data.data(), data.size(), resampled.data());
|
||||
data = std::move(resampled);
|
||||
}
|
||||
} else {
|
||||
data.resize(samples);
|
||||
if (!codec_->InputData(data)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Update the last input time */
|
||||
last_input_time_ = std::chrono::steady_clock::now();
|
||||
debug_statistics_.input_count++;
|
||||
|
||||
// 音频调试:发送原始音频数据
|
||||
if (audio_debugger_) {
|
||||
audio_debugger_->Feed(data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioService::AudioInputTask() {
|
||||
while (true) {
|
||||
EventBits_t bits = xEventGroupWaitBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING |
|
||||
AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING,
|
||||
pdFALSE, pdFALSE, portMAX_DELAY);
|
||||
|
||||
if (service_stopped_) {
|
||||
break;
|
||||
}
|
||||
if (audio_input_need_warmup_) {
|
||||
audio_input_need_warmup_ = false;
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Used for audio testing in NetworkConfiguring mode by clicking the BOOT button */
|
||||
if (bits & AS_EVENT_AUDIO_TESTING_RUNNING) {
|
||||
if (audio_testing_queue_.size() >= AUDIO_TESTING_MAX_DURATION_MS / OPUS_FRAME_DURATION_MS) {
|
||||
ESP_LOGW(TAG, "Audio testing queue is full, stopping audio testing");
|
||||
EnableAudioTesting(false);
|
||||
continue;
|
||||
}
|
||||
std::vector<int16_t> data;
|
||||
int samples = OPUS_FRAME_DURATION_MS * 16000 / 1000;
|
||||
if (ReadAudioData(data, 16000, samples)) {
|
||||
// If input channels is 2, we need to fetch the left channel data
|
||||
if (codec_->input_channels() == 2) {
|
||||
auto mono_data = std::vector<int16_t>(data.size() / 2);
|
||||
for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
|
||||
mono_data[i] = data[j];
|
||||
}
|
||||
data = std::move(mono_data);
|
||||
}
|
||||
PushTaskToEncodeQueue(kAudioTaskTypeEncodeToTestingQueue, std::move(data));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Feed the wake word */
|
||||
if (bits & AS_EVENT_WAKE_WORD_RUNNING) {
|
||||
std::vector<int16_t> data;
|
||||
int samples = wake_word_->GetFeedSize();
|
||||
if (samples > 0) {
|
||||
if (ReadAudioData(data, 16000, samples)) {
|
||||
wake_word_->Feed(data);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Feed the audio processor */
|
||||
if (bits & AS_EVENT_AUDIO_PROCESSOR_RUNNING) {
|
||||
std::vector<int16_t> data;
|
||||
int samples = audio_processor_->GetFeedSize();
|
||||
if (samples > 0) {
|
||||
if (ReadAudioData(data, 16000, samples)) {
|
||||
audio_processor_->Feed(data);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "Should not be here, bits: %lx", bits);
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Audio input task stopped");
|
||||
}
|
||||
|
||||
void AudioService::AudioOutputTask() {
|
||||
while (true) {
|
||||
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_queue_cv_.wait(lock, [this]() { return !audio_playback_queue_.empty() || service_stopped_; });
|
||||
if (service_stopped_) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto task = std::move(audio_playback_queue_.front());
|
||||
audio_playback_queue_.pop_front();
|
||||
audio_queue_cv_.notify_all();
|
||||
lock.unlock();
|
||||
|
||||
if (!codec_->output_enabled()) {
|
||||
codec_->EnableOutput(true);
|
||||
esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000);
|
||||
}
|
||||
codec_->OutputData(task->pcm);
|
||||
|
||||
/* Update the last output time */
|
||||
last_output_time_ = std::chrono::steady_clock::now();
|
||||
debug_statistics_.playback_count++;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Audio output task stopped");
|
||||
}
|
||||
|
||||
void AudioService::OpusCodecTask() {
|
||||
while (true) {
|
||||
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_queue_cv_.wait(lock, [this]() {
|
||||
return service_stopped_ ||
|
||||
(!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) ||
|
||||
(!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE);
|
||||
});
|
||||
if (service_stopped_) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Decode the audio from decode queue */
|
||||
if (!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE) {
|
||||
auto packet = std::move(audio_decode_queue_.front());
|
||||
audio_decode_queue_.pop_front();
|
||||
audio_queue_cv_.notify_all();
|
||||
lock.unlock();
|
||||
|
||||
auto task = std::make_unique<AudioTask>();
|
||||
task->type = kAudioTaskTypeDecodeToPlaybackQueue;
|
||||
|
||||
SetDecodeSampleRate(packet->sample_rate, packet->frame_duration);
|
||||
if (opus_decoder_->Decode(std::move(packet->payload), task->pcm)) {
|
||||
// Resample if the sample rate is different
|
||||
if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
|
||||
int target_size = output_resampler_.GetOutputSamples(task->pcm.size());
|
||||
std::vector<int16_t> resampled(target_size);
|
||||
output_resampler_.Process(task->pcm.data(), task->pcm.size(), resampled.data());
|
||||
task->pcm = std::move(resampled);
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
audio_playback_queue_.push_back(std::move(task));
|
||||
audio_queue_cv_.notify_all();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to decode audio");
|
||||
lock.lock();
|
||||
}
|
||||
debug_statistics_.decode_count++;
|
||||
}
|
||||
|
||||
/* Encode the audio to send queue */
|
||||
if (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) {
|
||||
auto task = std::move(audio_encode_queue_.front());
|
||||
audio_encode_queue_.pop_front();
|
||||
audio_queue_cv_.notify_all();
|
||||
lock.unlock();
|
||||
opus_encoder_->Encode(std::move(task->pcm), [this, &task](std::vector<uint8_t>&& opus) {
|
||||
auto packet = std::make_unique<AudioStreamPacket>();
|
||||
packet->payload = std::move(opus);
|
||||
packet->frame_duration = OPUS_FRAME_DURATION_MS;
|
||||
packet->sample_rate = 16000;
|
||||
|
||||
if (task->type == kAudioTaskTypeEncodeToSendQueue) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_send_queue_.push_back(std::move(packet));
|
||||
}
|
||||
if (callbacks_.on_send_queue_available) {
|
||||
callbacks_.on_send_queue_available();
|
||||
}
|
||||
} else if (task->type == kAudioTaskTypeEncodeToTestingQueue) {
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_testing_queue_.push_back(std::move(packet));
|
||||
}
|
||||
});
|
||||
debug_statistics_.encode_count++;
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Opus codec task stopped");
|
||||
}
|
||||
|
||||
void AudioService::SetDecodeSampleRate(int sample_rate, int frame_duration) {
|
||||
if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
opus_decoder_.reset();
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, frame_duration);
|
||||
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
|
||||
ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate());
|
||||
output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate());
|
||||
}
|
||||
}
|
||||
|
||||
void AudioService::PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t>&& pcm) {
|
||||
auto task = std::make_unique<AudioTask>();
|
||||
task->type = type;
|
||||
task->pcm = std::move(pcm);
|
||||
|
||||
/* Push the task to the encode queue */
|
||||
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_queue_cv_.wait(lock, [this]() { return audio_encode_queue_.size() < MAX_ENCODE_TASKS_IN_QUEUE; });
|
||||
audio_encode_queue_.push_back(std::move(task));
|
||||
audio_queue_cv_.notify_all();
|
||||
}
|
||||
|
||||
bool AudioService::PushPacketToDecodeQueue(std::unique_ptr<AudioStreamPacket> packet, bool wait) {
|
||||
std::unique_lock<std::mutex> lock(audio_queue_mutex_);
|
||||
if (audio_decode_queue_.size() >= MAX_DECODE_PACKETS_IN_QUEUE) {
|
||||
if (wait) {
|
||||
audio_queue_cv_.wait(lock, [this]() { return audio_decode_queue_.size() < MAX_DECODE_PACKETS_IN_QUEUE; });
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
audio_decode_queue_.push_back(std::move(packet));
|
||||
audio_queue_cv_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioStreamPacket> AudioService::PopPacketFromSendQueue() {
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
if (audio_send_queue_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto packet = std::move(audio_send_queue_.front());
|
||||
audio_send_queue_.pop_front();
|
||||
audio_queue_cv_.notify_all();
|
||||
return packet;
|
||||
}
|
||||
|
||||
void AudioService::EncodeWakeWord() {
|
||||
if (wake_word_) {
|
||||
wake_word_->EncodeWakeWordData();
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& AudioService::GetLastWakeWord() const {
|
||||
return wake_word_->GetLastDetectedWakeWord();
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioStreamPacket> AudioService::PopWakeWordPacket() {
|
||||
auto packet = std::make_unique<AudioStreamPacket>();
|
||||
if (wake_word_->GetWakeWordOpus(packet->payload)) {
|
||||
return packet;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AudioService::EnableWakeWordDetection(bool enable) {
|
||||
if (!wake_word_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "%s wake word detection", enable ? "Enabling" : "Disabling");
|
||||
if (enable) {
|
||||
if (!wake_word_initialized_) {
|
||||
wake_word_->Initialize(codec_);
|
||||
wake_word_initialized_ = true;
|
||||
}
|
||||
wake_word_->Start();
|
||||
xEventGroupSetBits(event_group_, AS_EVENT_WAKE_WORD_RUNNING);
|
||||
} else {
|
||||
wake_word_->Stop();
|
||||
xEventGroupClearBits(event_group_, AS_EVENT_WAKE_WORD_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioService::EnableVoiceProcessing(bool enable) {
|
||||
ESP_LOGD(TAG, "%s voice processing", enable ? "Enabling" : "Disabling");
|
||||
if (enable) {
|
||||
if (!audio_processor_initialized_) {
|
||||
audio_processor_->Initialize(codec_);
|
||||
audio_processor_initialized_ = true;
|
||||
}
|
||||
|
||||
/* We should make sure no audio is playing */
|
||||
ResetDecoder();
|
||||
audio_input_need_warmup_ = true;
|
||||
audio_processor_->Start();
|
||||
xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_PROCESSOR_RUNNING);
|
||||
} else {
|
||||
audio_processor_->Stop();
|
||||
xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_PROCESSOR_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioService::EnableAudioTesting(bool enable) {
|
||||
ESP_LOGI(TAG, "%s audio testing", enable ? "Enabling" : "Disabling");
|
||||
if (enable) {
|
||||
xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING);
|
||||
} else {
|
||||
xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING);
|
||||
/* Copy audio_testing_queue_ to audio_decode_queue_ */
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
audio_decode_queue_ = std::move(audio_testing_queue_);
|
||||
audio_queue_cv_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioService::EnableDeviceAec(bool enable) {
|
||||
ESP_LOGI(TAG, "%s device AEC", enable ? "Enabling" : "Disabling");
|
||||
audio_processor_->EnableDeviceAec(enable);
|
||||
}
|
||||
|
||||
void AudioService::SetCallbacks(AudioServiceCallbacks& callbacks) {
|
||||
callbacks_ = callbacks;
|
||||
}
|
||||
|
||||
void AudioService::PlaySound(const std::string_view& sound) {
|
||||
const char* data = sound.data();
|
||||
size_t size = sound.size();
|
||||
for (const char* p = data; p < data + size; ) {
|
||||
auto p3 = (BinaryProtocol3*)p;
|
||||
p += sizeof(BinaryProtocol3);
|
||||
|
||||
auto payload_size = ntohs(p3->payload_size);
|
||||
auto packet = std::make_unique<AudioStreamPacket>();
|
||||
packet->sample_rate = 16000;
|
||||
packet->frame_duration = 60;
|
||||
packet->payload.resize(payload_size);
|
||||
memcpy(packet->payload.data(), p3->payload, payload_size);
|
||||
p += payload_size;
|
||||
|
||||
PushPacketToDecodeQueue(std::move(packet), true);
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioService::IsIdle() {
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
return audio_encode_queue_.empty() && audio_decode_queue_.empty() && audio_playback_queue_.empty() && audio_testing_queue_.empty();
|
||||
}
|
||||
|
||||
void AudioService::ResetDecoder() {
|
||||
std::lock_guard<std::mutex> lock(audio_queue_mutex_);
|
||||
opus_decoder_->ResetState();
|
||||
audio_decode_queue_.clear();
|
||||
audio_playback_queue_.clear();
|
||||
audio_testing_queue_.clear();
|
||||
audio_queue_cv_.notify_all();
|
||||
}
|
||||
|
||||
void AudioService::CheckAndUpdateAudioPowerState() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto input_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_input_time_).count();
|
||||
auto output_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_output_time_).count();
|
||||
if (input_elapsed > AUDIO_POWER_TIMEOUT_MS && codec_->input_enabled()) {
|
||||
codec_->EnableInput(false);
|
||||
}
|
||||
if (output_elapsed > AUDIO_POWER_TIMEOUT_MS && codec_->output_enabled()) {
|
||||
codec_->EnableOutput(false);
|
||||
}
|
||||
if (!codec_->input_enabled() && !codec_->output_enabled()) {
|
||||
esp_timer_stop(audio_power_timer_);
|
||||
}
|
||||
}
|
||||
157
main/audio/audio_service.h
Normal file
157
main/audio/audio_service.h
Normal file
@ -0,0 +1,157 @@
|
||||
#ifndef AUDIO_SERVICE_H
|
||||
#define AUDIO_SERVICE_H
|
||||
|
||||
#include <memory>
|
||||
#include <deque>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
#include <opus_encoder.h>
|
||||
#include <opus_decoder.h>
|
||||
#include <opus_resampler.h>
|
||||
|
||||
#include "audio_codec.h"
|
||||
#include "audio_processor.h"
|
||||
#include "processors/audio_debugger.h"
|
||||
#include "wake_word.h"
|
||||
#include "protocol.h"
|
||||
|
||||
|
||||
/*
|
||||
* There are two types of audio data flow:
|
||||
* 1. (MIC) -> [Processors] -> {Encode Queue} -> [Opus Encoder] -> {Send Queue} -> (Server)
|
||||
* 2. (Server) -> {Decode Queue} -> [Opus Decoder] -> {Playback Queue} -> (Speaker)
|
||||
*
|
||||
* We use one task for MIC / Speaker / Processors, and one task for Opus Encoder / Opus Decoder.
|
||||
*
|
||||
* Decode Queue and Send Queue are the main queues, because Opus packets are quite smaller than PCM packets.
|
||||
*
|
||||
*/
|
||||
|
||||
#define OPUS_FRAME_DURATION_MS 60
|
||||
#define MAX_ENCODE_TASKS_IN_QUEUE 2
|
||||
#define MAX_PLAYBACK_TASKS_IN_QUEUE 2
|
||||
#define MAX_DECODE_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS)
|
||||
#define MAX_SEND_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS)
|
||||
#define AUDIO_TESTING_MAX_DURATION_MS 10000
|
||||
|
||||
#define AUDIO_POWER_TIMEOUT_MS 15000
|
||||
#define AUDIO_POWER_CHECK_INTERVAL_MS 1000
|
||||
|
||||
|
||||
#define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0)
|
||||
#define AS_EVENT_WAKE_WORD_RUNNING (1 << 1)
|
||||
#define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2)
|
||||
#define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3)
|
||||
|
||||
struct AudioServiceCallbacks {
|
||||
std::function<void(void)> on_send_queue_available;
|
||||
std::function<void(const std::string&)> on_wake_word_detected;
|
||||
std::function<void(bool)> on_vad_change;
|
||||
std::function<void(void)> on_audio_testing_queue_full;
|
||||
};
|
||||
|
||||
|
||||
enum AudioTaskType {
|
||||
kAudioTaskTypeEncodeToSendQueue,
|
||||
kAudioTaskTypeEncodeToTestingQueue,
|
||||
kAudioTaskTypeDecodeToPlaybackQueue,
|
||||
};
|
||||
|
||||
struct AudioTask {
|
||||
AudioTaskType type;
|
||||
std::vector<int16_t> pcm;
|
||||
};
|
||||
|
||||
struct DebugStatistics {
|
||||
uint32_t input_count = 0;
|
||||
uint32_t decode_count = 0;
|
||||
uint32_t encode_count = 0;
|
||||
uint32_t playback_count = 0;
|
||||
};
|
||||
|
||||
class AudioService {
|
||||
public:
|
||||
AudioService();
|
||||
~AudioService();
|
||||
|
||||
void Initialize(AudioCodec* codec);
|
||||
void Start();
|
||||
void Stop();
|
||||
void EncodeWakeWord();
|
||||
std::unique_ptr<AudioStreamPacket> PopWakeWordPacket();
|
||||
const std::string& GetLastWakeWord() const;
|
||||
bool IsVoiceDetected() const { return voice_detected_; }
|
||||
bool IsIdle();
|
||||
bool IsWakeWordRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_WAKE_WORD_RUNNING; }
|
||||
bool IsAudioProcessorRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_AUDIO_PROCESSOR_RUNNING; }
|
||||
|
||||
void EnableWakeWordDetection(bool enable);
|
||||
void EnableVoiceProcessing(bool enable);
|
||||
void EnableAudioTesting(bool enable);
|
||||
void EnableDeviceAec(bool enable);
|
||||
|
||||
void SetCallbacks(AudioServiceCallbacks& callbacks);
|
||||
|
||||
bool PushPacketToDecodeQueue(std::unique_ptr<AudioStreamPacket> packet, bool wait = false);
|
||||
std::unique_ptr<AudioStreamPacket> PopPacketFromSendQueue();
|
||||
void PlaySound(const std::string_view& sound);
|
||||
bool ReadAudioData(std::vector<int16_t>& data, int sample_rate, int samples);
|
||||
void ResetDecoder();
|
||||
|
||||
private:
|
||||
AudioCodec* codec_ = nullptr;
|
||||
AudioServiceCallbacks callbacks_;
|
||||
std::unique_ptr<AudioProcessor> audio_processor_;
|
||||
std::unique_ptr<WakeWord> wake_word_;
|
||||
std::unique_ptr<AudioDebugger> audio_debugger_;
|
||||
std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
|
||||
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
|
||||
OpusResampler input_resampler_;
|
||||
OpusResampler reference_resampler_;
|
||||
OpusResampler output_resampler_;
|
||||
DebugStatistics debug_statistics_;
|
||||
|
||||
EventGroupHandle_t event_group_;
|
||||
|
||||
// Audio encode / decode
|
||||
TaskHandle_t audio_input_task_handle_ = nullptr;
|
||||
TaskHandle_t audio_output_task_handle_ = nullptr;
|
||||
TaskHandle_t opus_codec_task_handle_ = nullptr;
|
||||
std::mutex audio_queue_mutex_;
|
||||
std::condition_variable audio_queue_cv_;
|
||||
std::deque<std::unique_ptr<AudioStreamPacket>> audio_decode_queue_;
|
||||
std::deque<std::unique_ptr<AudioStreamPacket>> audio_send_queue_;
|
||||
std::deque<std::unique_ptr<AudioStreamPacket>> audio_testing_queue_;
|
||||
std::deque<std::unique_ptr<AudioTask>> audio_encode_queue_;
|
||||
std::deque<std::unique_ptr<AudioTask>> audio_playback_queue_;
|
||||
|
||||
// For server AEC
|
||||
std::deque<uint32_t> timestamp_queue_;
|
||||
std::mutex timestamp_mutex_;
|
||||
|
||||
bool wake_word_initialized_ = false;
|
||||
bool audio_processor_initialized_ = false;
|
||||
bool voice_detected_ = false;
|
||||
bool service_stopped_ = true;
|
||||
bool audio_input_need_warmup_ = false;
|
||||
|
||||
esp_timer_handle_t audio_power_timer_ = nullptr;
|
||||
std::chrono::steady_clock::time_point last_input_time_;
|
||||
std::chrono::steady_clock::time_point last_output_time_;
|
||||
|
||||
void AudioInputTask();
|
||||
void AudioOutputTask();
|
||||
void OpusCodecTask();
|
||||
void PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t>&& pcm);
|
||||
void SetDecodeSampleRate(int sample_rate, int frame_duration);
|
||||
void CheckAndUpdateAudioPowerState();
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -74,63 +74,6 @@ NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_
|
||||
ESP_LOGI(TAG, "Duplex channels created");
|
||||
}
|
||||
|
||||
ATK_NoAudioCodecDuplex::ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
|
||||
duplex_ = true;
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
output_sample_rate_ = output_sample_rate;
|
||||
|
||||
i2s_chan_config_t chan_cfg = {
|
||||
.id = I2S_NUM_0,
|
||||
.role = I2S_ROLE_MASTER,
|
||||
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
|
||||
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
|
||||
.auto_clear_after_cb = true,
|
||||
.auto_clear_before_cb = false,
|
||||
.intr_priority = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = {
|
||||
.sample_rate_hz = (uint32_t)output_sample_rate_,
|
||||
.clk_src = I2S_CLK_SRC_DEFAULT,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
|
||||
#ifdef I2S_HW_VERSION_2
|
||||
.ext_clk_freq_hz = 0,
|
||||
#endif
|
||||
},
|
||||
.slot_cfg = {
|
||||
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
|
||||
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
|
||||
.slot_mode = I2S_SLOT_MODE_STEREO,
|
||||
.slot_mask = I2S_STD_SLOT_BOTH,
|
||||
.ws_width = I2S_DATA_BIT_WIDTH_16BIT,
|
||||
.ws_pol = false,
|
||||
.bit_shift = true,
|
||||
#ifdef I2S_HW_VERSION_2
|
||||
.left_align = true,
|
||||
.big_endian = false,
|
||||
.bit_order_lsb = false
|
||||
#endif
|
||||
},
|
||||
.gpio_cfg = {
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = bclk,
|
||||
.ws = ws,
|
||||
.dout = dout,
|
||||
.din = din,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false
|
||||
}
|
||||
}
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
|
||||
ESP_LOGI(TAG, "Duplex channels created");
|
||||
}
|
||||
|
||||
|
||||
NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din) {
|
||||
duplex_ = false;
|
||||
@ -377,18 +320,12 @@ int NoAudioCodec::Read(int16_t* dest, int samples) {
|
||||
int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) {
|
||||
size_t bytes_read;
|
||||
|
||||
// PDM 解调后的数据位宽为 16 位
|
||||
std::vector<int16_t> bit16_buffer(samples);
|
||||
if (i2s_channel_read(rx_handle_, bit16_buffer.data(), samples * sizeof(int16_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
|
||||
// PDM 解调后的数据位宽为 16 位,直接读取到目标缓冲区
|
||||
if (i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Read Failed!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 计算实际读取的样本数
|
||||
samples = bytes_read / sizeof(int16_t);
|
||||
|
||||
// 将 16 位数据直接复制到目标缓冲区
|
||||
memcpy(dest, bit16_buffer.data(), samples * sizeof(int16_t));
|
||||
|
||||
return samples;
|
||||
return bytes_read / sizeof(int16_t);
|
||||
}
|
||||
@ -20,11 +20,6 @@ public:
|
||||
NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
|
||||
};
|
||||
|
||||
class ATK_NoAudioCodecDuplex : public NoAudioCodec {
|
||||
public:
|
||||
ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
|
||||
};
|
||||
|
||||
class NoAudioCodecSimplex : public NoAudioCodec {
|
||||
public:
|
||||
NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din);
|
||||
@ -14,9 +14,8 @@ public:
|
||||
virtual void Initialize(AudioCodec* codec) = 0;
|
||||
virtual void Feed(const std::vector<int16_t>& data) = 0;
|
||||
virtual void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) = 0;
|
||||
virtual void StartDetection() = 0;
|
||||
virtual void StopDetection() = 0;
|
||||
virtual bool IsDetectionRunning() = 0;
|
||||
virtual void Start() = 0;
|
||||
virtual void Stop() = 0;
|
||||
virtual size_t GetFeedSize() = 0;
|
||||
virtual void EncodeWakeWordData() = 0;
|
||||
virtual bool GetWakeWordOpus(std::vector<uint8_t>& opus) = 0;
|
||||
@ -81,21 +81,17 @@ void AfeWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_
|
||||
wake_word_detected_callback_ = callback;
|
||||
}
|
||||
|
||||
void AfeWakeWord::StartDetection() {
|
||||
void AfeWakeWord::Start() {
|
||||
xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT);
|
||||
}
|
||||
|
||||
void AfeWakeWord::StopDetection() {
|
||||
void AfeWakeWord::Stop() {
|
||||
xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
|
||||
if (afe_data_ != nullptr) {
|
||||
afe_iface_->reset_buffer(afe_data_);
|
||||
}
|
||||
}
|
||||
|
||||
bool AfeWakeWord::IsDetectionRunning() {
|
||||
return xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT;
|
||||
}
|
||||
|
||||
void AfeWakeWord::Feed(const std::vector<int16_t>& data) {
|
||||
if (afe_data_ == nullptr) {
|
||||
return;
|
||||
@ -128,7 +124,7 @@ void AfeWakeWord::AudioDetectionTask() {
|
||||
StoreWakeWordData(res->data, res->data_size / sizeof(int16_t));
|
||||
|
||||
if (res->wakeup_state == WAKENET_DETECTED) {
|
||||
StopDetection();
|
||||
Stop();
|
||||
last_detected_wake_word_ = wake_words_[res->wakenet_model_index - 1];
|
||||
|
||||
if (wake_word_detected_callback_) {
|
||||
@ -26,9 +26,8 @@ public:
|
||||
void Initialize(AudioCodec* codec);
|
||||
void Feed(const std::vector<int16_t>& data);
|
||||
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
|
||||
void StartDetection();
|
||||
void StopDetection();
|
||||
bool IsDetectionRunning();
|
||||
void Start();
|
||||
void Stop();
|
||||
size_t GetFeedSize();
|
||||
void EncodeWakeWordData();
|
||||
bool GetWakeWordOpus(std::vector<uint8_t>& opus);
|
||||
@ -76,21 +76,17 @@ void CustomWakeWord::OnWakeWordDetected(std::function<void(const std::string& wa
|
||||
wake_word_detected_callback_ = callback;
|
||||
}
|
||||
|
||||
void CustomWakeWord::StartDetection() {
|
||||
void CustomWakeWord::Start() {
|
||||
xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT);
|
||||
}
|
||||
|
||||
void CustomWakeWord::StopDetection() {
|
||||
void CustomWakeWord::Stop() {
|
||||
xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
|
||||
if (afe_data_ != nullptr) {
|
||||
afe_iface_->reset_buffer(afe_data_);
|
||||
}
|
||||
}
|
||||
|
||||
bool CustomWakeWord::IsDetectionRunning() {
|
||||
return xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT;
|
||||
}
|
||||
|
||||
void CustomWakeWord::Feed(const std::vector<int16_t>& data) {
|
||||
if (afe_data_ == nullptr) {
|
||||
return;
|
||||
@ -158,7 +154,7 @@ void CustomWakeWord::AudioDetectionTask() {
|
||||
ESP_LOGI(TAG, "Custom wake word '%s' detected successfully!", CONFIG_CUSTOM_WAKE_WORD);
|
||||
|
||||
// 停止检测
|
||||
StopDetection();
|
||||
Stop();
|
||||
last_detected_wake_word_ = CONFIG_CUSTOM_WAKE_WORD_DISPLAY;
|
||||
|
||||
// 调用回调
|
||||
@ -31,9 +31,8 @@ public:
|
||||
void Initialize(AudioCodec* codec);
|
||||
void Feed(const std::vector<int16_t>& data);
|
||||
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
|
||||
void StartDetection();
|
||||
void StopDetection();
|
||||
bool IsDetectionRunning();
|
||||
void Start();
|
||||
void Stop();
|
||||
size_t GetFeedSize();
|
||||
void EncodeWakeWordData();
|
||||
bool GetWakeWordOpus(std::vector<uint8_t>& opus);
|
||||
@ -50,22 +50,18 @@ void EspWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_
|
||||
wake_word_detected_callback_ = callback;
|
||||
}
|
||||
|
||||
void EspWakeWord::StartDetection() {
|
||||
void EspWakeWord::Start() {
|
||||
xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT);
|
||||
}
|
||||
|
||||
void EspWakeWord::StopDetection() {
|
||||
void EspWakeWord::Stop() {
|
||||
xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
|
||||
}
|
||||
|
||||
bool EspWakeWord::IsDetectionRunning() {
|
||||
return xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT;
|
||||
}
|
||||
|
||||
void EspWakeWord::Feed(const std::vector<int16_t>& data) {
|
||||
int res = wakenet_iface_->detect(wakenet_data_, (int16_t *)data.data());
|
||||
if (res > 0) {
|
||||
StopDetection();
|
||||
Stop();
|
||||
last_detected_wake_word_ = wakenet_iface_->get_word_name(wakenet_data_, res);
|
||||
|
||||
if (wake_word_detected_callback_) {
|
||||
@ -27,9 +27,8 @@ public:
|
||||
void Initialize(AudioCodec* codec);
|
||||
void Feed(const std::vector<int16_t>& data);
|
||||
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
|
||||
void StartDetection();
|
||||
void StopDetection();
|
||||
bool IsDetectionRunning();
|
||||
void Start();
|
||||
void Stop();
|
||||
size_t GetFeedSize();
|
||||
void EncodeWakeWordData();
|
||||
bool GetWakeWordOpus(std::vector<uint8_t>& opus);
|
||||
@ -1,45 +0,0 @@
|
||||
#include "no_wake_word.h"
|
||||
#include <esp_log.h>
|
||||
|
||||
#define TAG "NoWakeWord"
|
||||
|
||||
void NoWakeWord::Initialize(AudioCodec* codec) {
|
||||
codec_ = codec;
|
||||
}
|
||||
|
||||
void NoWakeWord::Feed(const std::vector<int16_t>& data) {
|
||||
// Do nothing - no wake word processing
|
||||
}
|
||||
|
||||
void NoWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
|
||||
// Do nothing - no wake word processing
|
||||
}
|
||||
|
||||
void NoWakeWord::StartDetection() {
|
||||
// Do nothing - no wake word processing
|
||||
}
|
||||
|
||||
void NoWakeWord::StopDetection() {
|
||||
// Do nothing - no wake word processing
|
||||
}
|
||||
|
||||
bool NoWakeWord::IsDetectionRunning() {
|
||||
return false; // No wake word processing
|
||||
}
|
||||
|
||||
size_t NoWakeWord::GetFeedSize() {
|
||||
return 0; // No specific feed size requirement
|
||||
}
|
||||
|
||||
void NoWakeWord::EncodeWakeWordData() {
|
||||
// Do nothing - no encoding needed
|
||||
}
|
||||
|
||||
bool NoWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) {
|
||||
opus.clear();
|
||||
return false; // No opus data available
|
||||
}
|
||||
|
||||
const std::string& NoWakeWord::GetLastDetectedWakeWord() const {
|
||||
return last_detected_wake_word_;
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
#ifndef NO_WAKE_WORD_H
|
||||
#define NO_WAKE_WORD_H
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "wake_word.h"
|
||||
#include "audio_codec.h"
|
||||
|
||||
class NoWakeWord : public WakeWord {
|
||||
public:
|
||||
NoWakeWord() = default;
|
||||
~NoWakeWord() = default;
|
||||
|
||||
void Initialize(AudioCodec* codec) override;
|
||||
void Feed(const std::vector<int16_t>& data) override;
|
||||
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) override;
|
||||
void StartDetection() override;
|
||||
void StopDetection() override;
|
||||
bool IsDetectionRunning() override;
|
||||
size_t GetFeedSize() override;
|
||||
void EncodeWakeWordData() override;
|
||||
bool GetWakeWordOpus(std::vector<uint8_t>& opus) override;
|
||||
const std::string& GetLastDetectedWakeWord() const override;
|
||||
|
||||
private:
|
||||
AudioCodec* codec_ = nullptr;
|
||||
std::string last_detected_wake_word_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -1,70 +0,0 @@
|
||||
#include "background_task.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
#define TAG "BackgroundTask"
|
||||
|
||||
BackgroundTask::BackgroundTask(uint32_t stack_size) {
|
||||
xTaskCreate([](void* arg) {
|
||||
BackgroundTask* task = (BackgroundTask*)arg;
|
||||
task->BackgroundTaskLoop();
|
||||
}, "background_task", stack_size, this, 2, &background_task_handle_);
|
||||
}
|
||||
|
||||
BackgroundTask::~BackgroundTask() {
|
||||
if (background_task_handle_ != nullptr) {
|
||||
vTaskDelete(background_task_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
bool BackgroundTask::Schedule(std::function<void()> callback) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (waiting_for_completion_ > 0) {
|
||||
return false;
|
||||
}
|
||||
if (active_tasks_ >= 30) {
|
||||
int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
if (free_sram < 10000) {
|
||||
ESP_LOGW(TAG, "active_tasks_ == %d, free_sram == %u", active_tasks_, free_sram);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
active_tasks_++;
|
||||
background_tasks_.emplace_back([this, cb = std::move(callback)]() {
|
||||
cb();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
active_tasks_--;
|
||||
if (background_tasks_.empty() && active_tasks_ == 0) {
|
||||
condition_variable_.notify_all();
|
||||
}
|
||||
}
|
||||
});
|
||||
condition_variable_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackgroundTask::WaitForCompletion() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
waiting_for_completion_++;
|
||||
condition_variable_.wait(lock, [this]() {
|
||||
return background_tasks_.empty() && active_tasks_ == 0;
|
||||
});
|
||||
waiting_for_completion_--;
|
||||
}
|
||||
|
||||
void BackgroundTask::BackgroundTaskLoop() {
|
||||
ESP_LOGI(TAG, "background_task started");
|
||||
while (true) {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
condition_variable_.wait(lock, [this]() { return !background_tasks_.empty(); });
|
||||
|
||||
std::list<std::function<void()>> tasks = std::move(background_tasks_);
|
||||
lock.unlock();
|
||||
|
||||
for (auto& task : tasks) {
|
||||
task();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
#ifndef BACKGROUND_TASK_H
|
||||
#define BACKGROUND_TASK_H
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <mutex>
|
||||
#include <list>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
|
||||
class BackgroundTask {
|
||||
public:
|
||||
BackgroundTask(uint32_t stack_size = 4096 * 2);
|
||||
~BackgroundTask();
|
||||
|
||||
bool Schedule(std::function<void()> callback);
|
||||
void WaitForCompletion();
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::list<std::function<void()>> background_tasks_;
|
||||
std::condition_variable condition_variable_;
|
||||
TaskHandle_t background_task_handle_ = nullptr;
|
||||
int active_tasks_ = 0;
|
||||
int waiting_for_completion_ = 0;
|
||||
|
||||
void BackgroundTaskLoop();
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -118,12 +118,12 @@ mkdir main/boards/my-custom-board
|
||||
|
||||
```cpp
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "mcp_server.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
@ -220,12 +220,9 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// IoT设备初始化
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
// 可以添加更多IoT设备
|
||||
// MCP Tools 初始化
|
||||
void InitializeTools() {
|
||||
// 参考 MCP 文档
|
||||
}
|
||||
|
||||
public:
|
||||
@ -235,7 +232,7 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeDisplay();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
GetBacklight()->SetBrightness(100);
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codec.h"
|
||||
#include "es8311_audio_codec.h"
|
||||
#include "no_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "led/single_led.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "i2c_device.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
@ -25,6 +23,68 @@
|
||||
LV_FONT_DECLARE(font_puhui_20_4);
|
||||
LV_FONT_DECLARE(font_awesome_20_4);
|
||||
|
||||
|
||||
class ATK_NoAudioCodecDuplex : public NoAudioCodec {
|
||||
public:
|
||||
ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
|
||||
duplex_ = true;
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
output_sample_rate_ = output_sample_rate;
|
||||
|
||||
i2s_chan_config_t chan_cfg = {
|
||||
.id = I2S_NUM_0,
|
||||
.role = I2S_ROLE_MASTER,
|
||||
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
|
||||
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
|
||||
.auto_clear_after_cb = true,
|
||||
.auto_clear_before_cb = false,
|
||||
.intr_priority = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
|
||||
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = {
|
||||
.sample_rate_hz = (uint32_t)output_sample_rate_,
|
||||
.clk_src = I2S_CLK_SRC_DEFAULT,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
|
||||
#ifdef I2S_HW_VERSION_2
|
||||
.ext_clk_freq_hz = 0,
|
||||
#endif
|
||||
},
|
||||
.slot_cfg = {
|
||||
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
|
||||
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
|
||||
.slot_mode = I2S_SLOT_MODE_STEREO,
|
||||
.slot_mask = I2S_STD_SLOT_BOTH,
|
||||
.ws_width = I2S_DATA_BIT_WIDTH_16BIT,
|
||||
.ws_pol = false,
|
||||
.bit_shift = true,
|
||||
#ifdef I2S_HW_VERSION_2
|
||||
.left_align = true,
|
||||
.big_endian = false,
|
||||
.bit_order_lsb = false
|
||||
#endif
|
||||
},
|
||||
.gpio_cfg = {
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = bclk,
|
||||
.ws = ws,
|
||||
.dout = dout,
|
||||
.din = din,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false
|
||||
}
|
||||
}
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
|
||||
ESP_LOGI(TAG, "Duplex channels created");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class XL9555_IN : public I2cDevice {
|
||||
public:
|
||||
XL9555_IN(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
|
||||
@ -205,13 +265,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
atk_dnesp32s3_box() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
@ -219,7 +272,6 @@ public:
|
||||
xl9555_in_->SetOutputState(5, 1);
|
||||
xl9555_in_->SetOutputState(7, 1);
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#include "wifi_board.h"
|
||||
#include "es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "power_save_timer.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "power_manager.h"
|
||||
@ -336,14 +335,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
thing_manager.AddThing(iot::CreateThing("Battery"));
|
||||
}
|
||||
|
||||
public:
|
||||
atk_dnesp32s3_box0() :
|
||||
right_button_(R_BUTTON_GPIO, false),
|
||||
@ -356,7 +347,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeSt7789Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "es8388_audio_codec.h"
|
||||
#include "codecs/es8388_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
@ -144,13 +143,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
// 初始化摄像头:ov2640;
|
||||
// 根据正点原子官方示例参数
|
||||
void InitializeCamera() {
|
||||
@ -214,7 +206,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeSt7789Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeCamera();
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#include "ml307_board.h"
|
||||
#include "es8388_audio_codec.h"
|
||||
#include "codecs/es8388_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "assets/lang_config.h"
|
||||
@ -176,13 +175,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
atk_dnesp32s3m_4g() : Ml307Board(Module_4G_TX_PIN, Module_4G_RX_PIN),
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
@ -193,7 +185,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeSt7735Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "es8388_audio_codec.h"
|
||||
#include "codecs/es8388_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "assets/lang_config.h"
|
||||
@ -186,13 +185,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
atk_dnesp32s3m_wifi() :
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
@ -203,7 +195,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeSt7735Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
@ -101,19 +100,12 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
}
|
||||
|
||||
public:
|
||||
AtomMatrixEchoBaseBoard() : face_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
I2cDetect();
|
||||
InitializePi4ioe();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
@ -197,13 +196,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
AtomS3EchoBaseBoard() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
@ -212,7 +204,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeGc9107Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
@ -157,12 +156,6 @@ private:
|
||||
camera_->SetHMirror(false);
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
}
|
||||
|
||||
virtual Camera* GetCamera() override {
|
||||
return camera_;
|
||||
}
|
||||
@ -174,7 +167,6 @@ public:
|
||||
I2cDetect();
|
||||
CheckEchoBaseConnection();
|
||||
InitializePi4ioe();
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
@ -273,13 +272,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
AtomS3rEchoBaseBoard() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
@ -290,7 +282,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeGc9107Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
@ -173,22 +172,12 @@ private:
|
||||
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
CompactWifiBoardLCD() :
|
||||
boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) {
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "display/oled_display.h"
|
||||
|
||||
@ -134,14 +133,8 @@ private:
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Lamp"));
|
||||
#elif CONFIG_IOT_PROTOCOL_MCP
|
||||
void InitializeTools() {
|
||||
static LampController lamp(LAMP_GPIO);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
@ -150,7 +143,7 @@ public:
|
||||
InitializeDisplayI2c();
|
||||
InitializeSsd1306Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "dual_network_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/oled_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@ -7,7 +7,6 @@
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
@ -155,14 +154,8 @@ private:
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Lamp"));
|
||||
#elif CONFIG_IOT_PROTOCOL_MCP
|
||||
void InitializeTools() {
|
||||
static LampController lamp(LAMP_GPIO);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
@ -175,7 +168,7 @@ public:
|
||||
InitializeDisplayI2c();
|
||||
InitializeSsd1306Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@ -7,7 +7,6 @@
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
@ -150,15 +149,8 @@ private:
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
thing_manager.AddThing(iot::CreateThing("Lamp"));
|
||||
#elif CONFIG_IOT_PROTOCOL_MCP
|
||||
void InitializeTools() {
|
||||
static LampController lamp(LAMP_GPIO);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
@ -167,7 +159,7 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@ -7,7 +7,6 @@
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
@ -179,24 +178,12 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
#elif CONFIG_IOT_PROTOCOL_MCP
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
CompactWifiBoardS3Cam() :
|
||||
boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeCamera();
|
||||
if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
|
||||
GetBacklight()->RestoreBrightness();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/oled_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@ -7,7 +7,6 @@
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
@ -153,14 +152,8 @@ private:
|
||||
}
|
||||
|
||||
// 物联网初始化,逐步迁移到 MCP 协议
|
||||
void InitializeIot() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Lamp"));
|
||||
#elif CONFIG_IOT_PROTOCOL_MCP
|
||||
void InitializeTools() {
|
||||
static LampController lamp(LAMP_GPIO);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
@ -172,7 +165,7 @@ public:
|
||||
InitializeDisplayI2c();
|
||||
InitializeSsd1306Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
81
main/boards/common/adc_battery_monitor.cc
Normal file
81
main/boards/common/adc_battery_monitor.cc
Normal file
@ -0,0 +1,81 @@
|
||||
#include "adc_battery_monitor.h"
|
||||
|
||||
AdcBatteryMonitor::AdcBatteryMonitor(adc_unit_t adc_unit, adc_channel_t adc_channel, float upper_resistor, float lower_resistor, gpio_num_t charging_pin)
|
||||
: charging_pin_(charging_pin) {
|
||||
|
||||
// Initialize charging pin
|
||||
gpio_config_t gpio_cfg = {
|
||||
.pin_bit_mask = 1ULL << charging_pin,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&gpio_cfg));
|
||||
|
||||
// Initialize ADC battery estimation
|
||||
adc_battery_estimation_t adc_cfg = {
|
||||
.internal = {
|
||||
.adc_unit = adc_unit,
|
||||
.adc_bitwidth = ADC_BITWIDTH_12,
|
||||
.adc_atten = ADC_ATTEN_DB_12,
|
||||
},
|
||||
.adc_channel = adc_channel,
|
||||
.upper_resistor = upper_resistor,
|
||||
.lower_resistor = lower_resistor
|
||||
};
|
||||
adc_cfg.charging_detect_cb = [](void *user_data) -> bool {
|
||||
AdcBatteryMonitor *self = (AdcBatteryMonitor *)user_data;
|
||||
return gpio_get_level(self->charging_pin_) == 1;
|
||||
};
|
||||
adc_cfg.charging_detect_user_data = this;
|
||||
adc_battery_estimation_handle_ = adc_battery_estimation_create(&adc_cfg);
|
||||
|
||||
// Initialize timer
|
||||
esp_timer_create_args_t timer_cfg = {
|
||||
.callback = [](void *arg) {
|
||||
AdcBatteryMonitor *self = (AdcBatteryMonitor *)arg;
|
||||
self->CheckBatteryStatus();
|
||||
},
|
||||
.arg = this,
|
||||
.name = "adc_battery_monitor",
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_cfg, &timer_handle_));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000));
|
||||
}
|
||||
|
||||
AdcBatteryMonitor::~AdcBatteryMonitor() {
|
||||
if (adc_battery_estimation_handle_) {
|
||||
ESP_ERROR_CHECK(adc_battery_estimation_destroy(adc_battery_estimation_handle_));
|
||||
}
|
||||
}
|
||||
|
||||
bool AdcBatteryMonitor::IsCharging() {
|
||||
bool is_charging = false;
|
||||
ESP_ERROR_CHECK(adc_battery_estimation_get_charging_state(adc_battery_estimation_handle_, &is_charging));
|
||||
return is_charging;
|
||||
}
|
||||
|
||||
bool AdcBatteryMonitor::IsDischarging() {
|
||||
return !IsCharging();
|
||||
}
|
||||
|
||||
uint8_t AdcBatteryMonitor::GetBatteryLevel() {
|
||||
float capacity = 0;
|
||||
ESP_ERROR_CHECK(adc_battery_estimation_get_capacity(adc_battery_estimation_handle_, &capacity));
|
||||
return capacity;
|
||||
}
|
||||
|
||||
void AdcBatteryMonitor::OnChargingStatusChanged(std::function<void(bool)> callback) {
|
||||
on_charging_status_changed_ = callback;
|
||||
}
|
||||
|
||||
void AdcBatteryMonitor::CheckBatteryStatus() {
|
||||
bool new_charging_status = IsCharging();
|
||||
if (new_charging_status != is_charging_) {
|
||||
is_charging_ = new_charging_status;
|
||||
if (on_charging_status_changed_) {
|
||||
on_charging_status_changed_(is_charging_);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
main/boards/common/adc_battery_monitor.h
Normal file
30
main/boards/common/adc_battery_monitor.h
Normal file
@ -0,0 +1,30 @@
|
||||
#ifndef ADC_BATTERY_MONITOR_H
|
||||
#define ADC_BATTERY_MONITOR_H
|
||||
|
||||
#include <functional>
|
||||
#include <driver/gpio.h>
|
||||
#include <adc_battery_estimation.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
class AdcBatteryMonitor {
|
||||
public:
|
||||
AdcBatteryMonitor(adc_unit_t adc_unit, adc_channel_t adc_channel, float upper_resistor, float lower_resistor, gpio_num_t charging_pin = GPIO_NUM_NC);
|
||||
~AdcBatteryMonitor();
|
||||
|
||||
bool IsCharging();
|
||||
bool IsDischarging();
|
||||
uint8_t GetBatteryLevel();
|
||||
|
||||
void OnChargingStatusChanged(std::function<void(bool)> callback);
|
||||
|
||||
private:
|
||||
gpio_num_t charging_pin_;
|
||||
adc_battery_estimation_handle_t adc_battery_estimation_handle_ = nullptr;
|
||||
esp_timer_handle_t timer_handle_ = nullptr;
|
||||
bool is_charging_ = false;
|
||||
std::function<void(bool)> on_charging_status_changed_;
|
||||
|
||||
void CheckBatteryStatus();
|
||||
};
|
||||
|
||||
#endif // ADC_BATTERY_MONITOR_H
|
||||
@ -29,7 +29,7 @@ namespace audio_wifi_config
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!app->ReadAudio(audio_data, 16000, 480)) { // 16kHz, 480 samples corresponds to 30ms data
|
||||
if (!app->GetAudioService().ReadAudioData(audio_data, 16000, 480)) { // 16kHz, 480 samples corresponds to 30ms data
|
||||
// 读取音频失败,短暂延迟后重试
|
||||
ESP_LOGI(kLogTag, "Failed to read audio data, retrying.");
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
|
||||
@ -18,7 +18,7 @@ AdcButton::AdcButton(const button_adc_config_t& adc_config) : Button(nullptr) {
|
||||
Button::Button(button_handle_t button_handle) : button_handle_(button_handle) {
|
||||
}
|
||||
|
||||
Button::Button(gpio_num_t gpio_num, bool active_high, uint16_t long_press_time, uint16_t short_press_time) : gpio_num_(gpio_num) {
|
||||
Button::Button(gpio_num_t gpio_num, bool active_high, uint16_t long_press_time, uint16_t short_press_time, bool enable_power_save) : gpio_num_(gpio_num) {
|
||||
if (gpio_num == GPIO_NUM_NC) {
|
||||
return;
|
||||
}
|
||||
@ -29,7 +29,7 @@ Button::Button(gpio_num_t gpio_num, bool active_high, uint16_t long_press_time,
|
||||
button_gpio_config_t gpio_config = {
|
||||
.gpio_num = gpio_num,
|
||||
.active_level = static_cast<uint8_t>(active_high ? 1 : 0),
|
||||
.enable_power_save = false,
|
||||
.enable_power_save = enable_power_save,
|
||||
.disable_pull = false
|
||||
};
|
||||
ESP_ERROR_CHECK(iot_button_new_gpio_device(&button_config, &gpio_config, &button_handle_));
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
class Button {
|
||||
public:
|
||||
Button(button_handle_t button_handle);
|
||||
Button(gpio_num_t gpio_num, bool active_high = false, uint16_t long_press_time = 0, uint16_t short_press_time = 0);
|
||||
Button(gpio_num_t gpio_num, bool active_high = false, uint16_t long_press_time = 0, uint16_t short_press_time = 0, bool enable_power_save = false);
|
||||
~Button();
|
||||
|
||||
void OnPressDown(std::function<void()> callback);
|
||||
@ -40,4 +40,10 @@ public:
|
||||
};
|
||||
#endif
|
||||
|
||||
class PowerSaveButton : public Button {
|
||||
public:
|
||||
PowerSaveButton(gpio_num_t gpio_num) : Button(gpio_num, false, 0, 0, true) {
|
||||
}
|
||||
};
|
||||
|
||||
#endif // BUTTON_H_
|
||||
|
||||
@ -66,12 +66,6 @@ void Ml307Board::StartNetwork() {
|
||||
ESP_LOGI(TAG, "ML307 Revision: %s", module_revision.c_str());
|
||||
ESP_LOGI(TAG, "ML307 IMEI: %s", imei.c_str());
|
||||
ESP_LOGI(TAG, "ML307 ICCID: %s", iccid.c_str());
|
||||
|
||||
// Close all previous connections
|
||||
modem_->ResetConnections();
|
||||
|
||||
// Enable sleep mode
|
||||
modem_->SetSleepMode(true, 30);
|
||||
}
|
||||
|
||||
NetworkInterface* Ml307Board::GetNetwork() {
|
||||
|
||||
114
main/boards/common/sleep_timer.cc
Normal file
114
main/boards/common/sleep_timer.cc
Normal file
@ -0,0 +1,114 @@
|
||||
#include "sleep_timer.h"
|
||||
#include "application.h"
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
|
||||
#define TAG "SleepTimer"
|
||||
|
||||
|
||||
SleepTimer::SleepTimer(int seconds_to_light_sleep, int seconds_to_deep_sleep)
|
||||
: seconds_to_light_sleep_(seconds_to_light_sleep), seconds_to_deep_sleep_(seconds_to_deep_sleep) {
|
||||
esp_timer_create_args_t timer_args = {
|
||||
.callback = [](void* arg) {
|
||||
auto self = static_cast<SleepTimer*>(arg);
|
||||
self->CheckTimer();
|
||||
},
|
||||
.arg = this,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "sleep_timer",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &sleep_timer_));
|
||||
}
|
||||
|
||||
SleepTimer::~SleepTimer() {
|
||||
esp_timer_stop(sleep_timer_);
|
||||
esp_timer_delete(sleep_timer_);
|
||||
}
|
||||
|
||||
void SleepTimer::SetEnabled(bool enabled) {
|
||||
if (enabled && !enabled_) {
|
||||
ticks_ = 0;
|
||||
enabled_ = enabled;
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(sleep_timer_, 1000000));
|
||||
ESP_LOGI(TAG, "Sleep timer enabled");
|
||||
} else if (!enabled && enabled_) {
|
||||
ESP_ERROR_CHECK(esp_timer_stop(sleep_timer_));
|
||||
enabled_ = enabled;
|
||||
WakeUp();
|
||||
ESP_LOGI(TAG, "Sleep timer disabled");
|
||||
}
|
||||
}
|
||||
|
||||
void SleepTimer::OnEnterLightSleepMode(std::function<void()> callback) {
|
||||
on_enter_light_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void SleepTimer::OnExitLightSleepMode(std::function<void()> callback) {
|
||||
on_exit_light_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void SleepTimer::OnEnterDeepSleepMode(std::function<void()> callback) {
|
||||
on_enter_deep_sleep_mode_ = callback;
|
||||
}
|
||||
|
||||
void SleepTimer::CheckTimer() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (!app.CanEnterSleepMode()) {
|
||||
ticks_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ticks_++;
|
||||
if (seconds_to_light_sleep_ != -1 && ticks_ >= seconds_to_light_sleep_) {
|
||||
if (!in_light_sleep_mode_) {
|
||||
in_light_sleep_mode_ = true;
|
||||
if (on_enter_light_sleep_mode_) {
|
||||
on_enter_light_sleep_mode_();
|
||||
}
|
||||
|
||||
app.Schedule([this, &app]() {
|
||||
while (in_light_sleep_mode_) {
|
||||
auto& board = Board::GetInstance();
|
||||
board.GetDisplay()->UpdateStatusBar(true);
|
||||
lv_refr_now(nullptr);
|
||||
lvgl_port_stop();
|
||||
|
||||
// 配置timer唤醒源(30秒后自动唤醒)
|
||||
esp_sleep_enable_timer_wakeup(30 * 1000000);
|
||||
|
||||
// 进入light sleep模式
|
||||
esp_light_sleep_start();
|
||||
lvgl_port_resume();
|
||||
|
||||
auto wakeup_reason = esp_sleep_get_wakeup_cause();
|
||||
if (wakeup_reason != ESP_SLEEP_WAKEUP_TIMER) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
WakeUp();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (seconds_to_deep_sleep_ != -1 && ticks_ >= seconds_to_deep_sleep_) {
|
||||
if (on_enter_deep_sleep_mode_) {
|
||||
on_enter_deep_sleep_mode_();
|
||||
}
|
||||
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
}
|
||||
|
||||
void SleepTimer::WakeUp() {
|
||||
ticks_ = 0;
|
||||
if (in_light_sleep_mode_) {
|
||||
in_light_sleep_mode_ = false;
|
||||
if (on_exit_light_sleep_mode_) {
|
||||
on_exit_light_sleep_mode_();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
main/boards/common/sleep_timer.h
Normal file
32
main/boards/common/sleep_timer.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <esp_timer.h>
|
||||
#include <esp_pm.h>
|
||||
|
||||
class SleepTimer {
|
||||
public:
|
||||
SleepTimer(int seconds_to_light_sleep = 20, int seconds_to_deep_sleep = -1);
|
||||
~SleepTimer();
|
||||
|
||||
void SetEnabled(bool enabled);
|
||||
void OnEnterLightSleepMode(std::function<void()> callback);
|
||||
void OnExitLightSleepMode(std::function<void()> callback);
|
||||
void OnEnterDeepSleepMode(std::function<void()> callback);
|
||||
void WakeUp();
|
||||
|
||||
private:
|
||||
void CheckTimer();
|
||||
|
||||
esp_timer_handle_t sleep_timer_ = nullptr;
|
||||
bool enabled_ = false;
|
||||
int ticks_ = 0;
|
||||
int seconds_to_light_sleep_;
|
||||
int seconds_to_deep_sleep_;
|
||||
bool in_light_sleep_mode_ = false;
|
||||
|
||||
std::function<void()> on_enter_light_sleep_mode_;
|
||||
std::function<void()> on_exit_light_sleep_mode_;
|
||||
std::function<void()> on_enter_deep_sleep_mode_;
|
||||
};
|
||||
@ -4,8 +4,7 @@
|
||||
{
|
||||
"name": "df-k10",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_SPIRAM_MODE_OCT=y",
|
||||
"CONFIG_IOT_PROTOCOL_MCP=y"
|
||||
"CONFIG_SPIRAM_MODE_OCT=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include "led/circular_strip.h"
|
||||
@ -258,11 +257,6 @@ public:
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeCamera();
|
||||
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
#endif
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -6,8 +6,7 @@
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=10",
|
||||
"CONFIG_ESP_PHY_MAX_TX_POWER=10",
|
||||
"CONFIG_SPIRAM_MODE_OCT=y",
|
||||
"CONFIG_IOT_PROTOCOL_MCP=y"
|
||||
"CONFIG_SPIRAM_MODE_OCT=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "esp32_camera.h"
|
||||
|
||||
#include "led/gpio_led.h"
|
||||
@ -30,12 +29,6 @@ class DfrobotEsp32S3AiCam : public WifiBoard {
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
}
|
||||
|
||||
void InitializeCamera() {
|
||||
camera_config_t config = {};
|
||||
config.ledc_channel = LEDC_CHANNEL_2; // LEDC通道选择 用于生成XCLK时钟 但是S3不用
|
||||
@ -73,13 +66,7 @@ class DfrobotEsp32S3AiCam : public WifiBoard {
|
||||
DfrobotEsp32S3AiCam() :
|
||||
boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeCamera();
|
||||
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
#endif
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/gpio_led.h"
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
@ -100,12 +99,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
}
|
||||
|
||||
|
||||
void InitializeGpio(gpio_num_t gpio_num_) {
|
||||
gpio_config_t config = {
|
||||
@ -128,7 +121,6 @@ public:
|
||||
// 上拉io48 置高电平
|
||||
InitializeGpio(GPIO_NUM_48);
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "power_manager.h"
|
||||
#include "power_save_timer.h"
|
||||
@ -124,20 +123,11 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto &thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
thing_manager.AddThing(iot::CreateThing("Battery"));
|
||||
}
|
||||
|
||||
public:
|
||||
DuChatX() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
InitializePowerSaveTimer();
|
||||
InitializePowerManager();
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/box_audio_codec.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "backlight.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "EchoEar",
|
||||
"name": "echoear",
|
||||
"sdkconfig_append": []
|
||||
}
|
||||
]
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
#include <wifi_station.h>
|
||||
|
||||
#include "application.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "display/lcd_display.h"
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/box_audio_codec.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "esp_lcd_ili9341.h"
|
||||
#include "font_awesome_symbols.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -143,20 +142,12 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
EspBox3Board() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
InitializeSpi();
|
||||
InitializeIli9341Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
@ -205,20 +204,12 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
EspBoxBoardLite() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
InitializeSpi();
|
||||
InitializeIli9341Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/box_audio_codec.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "esp_lcd_ili9341.h"
|
||||
#include "font_awesome_symbols.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -143,20 +142,12 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
EspBox3Board() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
InitializeSpi();
|
||||
InitializeIli9341Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
"CONFIG_MMAP_FILE_NAME_LENGTH=25",
|
||||
"CONFIG_ESP_CONSOLE_NONE=y",
|
||||
"CONFIG_USE_ESP_WAKE_WORD=y",
|
||||
"CONFIG_IOT_PROTOCOL_MCP=y",
|
||||
"CONFIG_COMPILER_OPTIMIZATION_SIZE=y"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
@ -7,7 +7,6 @@
|
||||
#include "pin_config.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
@ -17,7 +16,6 @@
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_panel_io_additions.h>
|
||||
|
||||
#include "audio_codecs/box_audio_codec.h"
|
||||
#include "esp_io_expander_tca9554.h"
|
||||
|
||||
#define TAG "ESP_S3_LCD_EV_Board"
|
||||
@ -175,17 +173,10 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
}
|
||||
|
||||
public:
|
||||
ESP_S3_LCD_EV_Board() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeCodecI2c();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeRGB_GC9503V_Display();
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "font_awesome_symbols.h"
|
||||
#include "application.h"
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
@ -162,13 +161,6 @@ private:
|
||||
gpio_config(&io_conf_2);
|
||||
}
|
||||
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Battery"));
|
||||
}
|
||||
|
||||
|
||||
void BlinkGreenFor5s() {
|
||||
auto* led = static_cast<CircularStrip*>(GetLed());
|
||||
if (!led) {
|
||||
@ -201,7 +193,6 @@ public:
|
||||
InitializeADC();
|
||||
InitializeI2c();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/timers.h>
|
||||
#include <freertos/task.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#include "board.h"
|
||||
#include "boards/common/wifi_board.h"
|
||||
#include "boards/esp32-cgc-144/config.h"
|
||||
#include "iot/thing.h"
|
||||
|
||||
#include "audio_codec.h"
|
||||
|
||||
#define TAG "BoardControl"
|
||||
|
||||
namespace iot {
|
||||
|
||||
class BoardControl : public Thing {
|
||||
public:
|
||||
BoardControl() : Thing("BoardControl", "当前 AI 机器人管理和控制") {
|
||||
// 修改音量调节
|
||||
properties_.AddNumberProperty("volume", "当前音量值", [this]() -> int {
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
return codec->output_volume();
|
||||
});
|
||||
|
||||
// 定义设备可以被远程执行的指令
|
||||
#if defined(ESP32_CGC_144_lite)
|
||||
methods_.AddMethod("SetVolume", "设置音量", ParameterList({
|
||||
Parameter("volume", "0到100之间的整数", kValueTypeNumber, true)
|
||||
}), [this](const ParameterList& parameters) {
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
// 获取传入的音量值
|
||||
int volume = parameters["volume"].number();
|
||||
// 限制音量值
|
||||
if (volume > 66) {
|
||||
volume = 66;
|
||||
}
|
||||
codec->SetOutputVolume(static_cast<uint8_t>(volume));
|
||||
});
|
||||
#else
|
||||
methods_.AddMethod("SetVolume", "设置音量", ParameterList({
|
||||
Parameter("volume", "0到100之间的整数", kValueTypeNumber, true)
|
||||
}), [this](const ParameterList& parameters) {
|
||||
auto codec = Board::GetInstance().GetAudioCodec();
|
||||
// 获取传入的音量值
|
||||
int volume = parameters["volume"].number();
|
||||
// 限制音量值
|
||||
if (volume > 100) {
|
||||
volume = 100;
|
||||
}
|
||||
codec->SetOutputVolume(static_cast<uint8_t>(volume));
|
||||
});
|
||||
#endif
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace iot
|
||||
|
||||
DECLARE_THING(BoardControl);
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@ -8,7 +8,6 @@
|
||||
#include "power_save_timer.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
@ -157,16 +156,8 @@ void InitializePowerManager() {
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
thing_manager.AddThing(iot::CreateThing("BoardControl"));
|
||||
thing_manager.AddThing(iot::CreateThing("Battery"));
|
||||
thing_manager.AddThing(iot::CreateThing("Lamp"));
|
||||
#elif CONFIG_IOT_PROTOCOL_MCP
|
||||
void InitializeTools() {
|
||||
static LampController lamp(LAMP_GPIO);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
@ -177,7 +168,7 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeSt7735Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@ -7,7 +7,6 @@
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
#include "lamp_controller.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
@ -153,15 +152,8 @@ private:
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
#if CONFIG_IOT_PROTOCOL_XIAOZHI
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
thing_manager.AddThing(iot::CreateThing("Lamp"));
|
||||
#elif CONFIG_IOT_PROTOCOL_MCP
|
||||
void InitializeTools() {
|
||||
static LampController lamp(LAMP_GPIO);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
@ -170,7 +162,7 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include "esp_lcd_sh8601.h"
|
||||
#include "font_awesome_symbols.h"
|
||||
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "led/single_led.h"
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include "i2c_device.h"
|
||||
@ -215,13 +214,6 @@ private:
|
||||
}, this);
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
CustomBoard() {
|
||||
InitializeI2c();
|
||||
@ -229,7 +221,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeSpd2010Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include "i2c_device.h"
|
||||
@ -425,14 +424,6 @@ private:
|
||||
}, this);
|
||||
}
|
||||
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
CustomBoard() {
|
||||
InitializeI2c();
|
||||
@ -440,7 +431,6 @@ public:
|
||||
InitializeSpi();
|
||||
Initializest77916Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/no_audio_codec.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include "i2c_device.h"
|
||||
@ -374,13 +373,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
}
|
||||
|
||||
public:
|
||||
CustomBoard() :
|
||||
boot_button_(BOOT_BUTTON_GPIO) {
|
||||
@ -389,7 +381,6 @@ public:
|
||||
InitializeSpi();
|
||||
Initializest77916Display();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/box_audio_codec.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -269,12 +268,6 @@ private:
|
||||
|
||||
camera_ = new Esp32Camera(config);
|
||||
}
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
|
||||
}
|
||||
|
||||
public:
|
||||
Esp32S3Korvo2V3Board() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
@ -290,7 +283,6 @@ public:
|
||||
#else
|
||||
InitializeSt7789Display();
|
||||
#endif
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
@ -219,14 +218,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
thing_manager.AddThing(iot::CreateThing("Battery"));
|
||||
}
|
||||
|
||||
public:
|
||||
GenJuTech_s3_1_54TFT() :
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
@ -239,7 +230,6 @@ public:
|
||||
InitializeSpi();
|
||||
InitializeButtons();
|
||||
InitializeSt7789Display();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "i2c_device.h"
|
||||
#include "iot/thing_manager.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
@ -232,25 +231,18 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Screen"));
|
||||
thing_manager.AddThing(iot::CreateThing("Battery"));
|
||||
}
|
||||
public:
|
||||
JiuchuanDevBoard() :
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
pwr_button_(PWR_BUTTON_GPIO,true),
|
||||
wifi_button(WIFI_BUTTON_GPIO),
|
||||
cmd_button(CMD_BUTTON_GPIO) {
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
pwr_button_(PWR_BUTTON_GPIO,true),
|
||||
wifi_button(WIFI_BUTTON_GPIO),
|
||||
cmd_button(CMD_BUTTON_GPIO) {
|
||||
|
||||
InitializeI2c();
|
||||
InitializePowerManager();
|
||||
InitializePowerSaveTimer();
|
||||
InitializeButtons();
|
||||
InitializeGC9301isplay();
|
||||
InitializeIot();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
#include "ml307_board.h"
|
||||
#include "audio_codecs/box_audio_codec.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/oled_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "led/single_led.h"
|
||||
#include "assets/lang_config.h"
|
||||
|
||||
@ -176,12 +175,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
}
|
||||
|
||||
public:
|
||||
KevinBoxBoard() : Ml307Board(ML307_TX_PIN, ML307_RX_PIN),
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
@ -194,7 +187,6 @@ public:
|
||||
Enable4GModule();
|
||||
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
#include "dual_network_board.h"
|
||||
#include "audio_codecs/box_audio_codec.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/oled_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "led/single_led.h"
|
||||
#include "iot/thing_manager.h"
|
||||
#include "config.h"
|
||||
#include "power_save_timer.h"
|
||||
#include "axp2101.h"
|
||||
@ -229,13 +228,6 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
auto& thing_manager = iot::ThingManager::GetInstance();
|
||||
thing_manager.AddThing(iot::CreateThing("Speaker"));
|
||||
thing_manager.AddThing(iot::CreateThing("Battery"));
|
||||
}
|
||||
|
||||
public:
|
||||
KevinBoxBoard() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN),
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
@ -250,7 +242,6 @@ public:
|
||||
|
||||
InitializeButtons();
|
||||
InitializePowerSaveTimer();
|
||||
InitializeIot();
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "wifi_board.h"
|
||||
#include "audio_codecs/es8311_audio_codec.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
@ -34,6 +34,14 @@ private:
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
|
||||
|
||||
// Print I2C bus info
|
||||
if (i2c_master_probe(codec_i2c_bus_, 0x18, 1000) != ESP_OK) {
|
||||
while (true) {
|
||||
ESP_LOGE(TAG, "Failed to probe I2C bus, please check if you have installed the correct firmware");
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
@ -52,19 +60,19 @@ private:
|
||||
}
|
||||
|
||||
// 物联网初始化,添加对 AI 可见设备
|
||||
void InitializeIot() {
|
||||
void InitializeTools() {
|
||||
led_strip_ = new CircularStrip(BUILTIN_LED_GPIO, 8);
|
||||
new LedStripControl(led_strip_);
|
||||
}
|
||||
|
||||
public:
|
||||
KevinBoxBoard() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
// 把 ESP32C3 的 VDD SPI 引脚作为普通 GPIO 口使用
|
||||
esp_efuse_write_field_bit(ESP_EFUSE_VDD_SPI_AS_GPIO);
|
||||
|
||||
InitializeCodecI2c();
|
||||
InitializeButtons();
|
||||
InitializeIot();
|
||||
InitializeTools();
|
||||
|
||||
// 把 ESP32C3 的 VDD SPI 引脚作为普通 GPIO 口使用
|
||||
esp_efuse_write_field_bit(ESP_EFUSE_VDD_SPI_AS_GPIO);
|
||||
}
|
||||
|
||||
virtual Led* GetLed() override {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user