飞书文档https://x509p6c8to.feishu.cn/wiki/P6Xhw6JtXilaQhkgcpEcAxmynHgxiaozhi-esp32是一个开源项目,采用 MIT 许可证发布,允许任何人免费使用,也可用于商业用途。旨在帮助更多人入门 AI 硬件开发,了解如何将大语言模型应用到实际硬件设备中,适合对 AI 感兴趣的学生和想要探索新技术的开发者,可通过此项目获得宝贵的学习经验。
已实现功能
- 网络连接:支持 Wi-Fi 以及 ML307 Cat.1 4G 网络。
- 按键交互:BOOT 键可用于唤醒和打断操作,支持点击和长按两种触发方式。
- 语音唤醒:具备离线语音唤醒功能,基于 ESP - SR。
- 语音对话:支持流式语音对话,采用 WebSocket 或 UDP 协议。
- 语言识别:支持国语、粤语、英语、日语、韩语 5 种语言识别,借助 SenseVoice。
- 声纹识别:能够识别是谁在喊 AI 的名字,基于 [3D Speaker](https://github.com/modelscope/3D - Speaker)。
- 语音合成与大模型:支持大模型 TTS(火山引擎 或 CosyVoice)和大模型 LLM(Qwen, DeepSeek, Doubao)。
- 自定义配置:可配置提示词和音色,实现自定义角色。
- 记忆功能:具备短期记忆,每轮对话后会自我总结。
- 显示功能:支持 OLED / LCD 显示屏,可显示信号强弱或对话内容,还支持 LCD 显示图片表情。
- 多语言支持:支持多语言(中文、英文)。
代码结构
基于https://github.com/78/xiaozhi-esp32/tree/v1.5.5版本分析
仓库包含多个文件和文件夹,主要结构如下:
.gitignore git工程管理文件
CMakeLists.txt 工程cmake构建文件
LICENSE MIT License 允许用户自由使用、修改和分发代码,只需在软件或文档中包含原作者的版权声明和许可声明即可。
README.md README说明文档_中文
README_en.md README说明文档_英文
README_ja.md README说明文档_日文
partitions.csv 分区配置文件:16M Flash的分区表,默认选择,可以通过idf.py menuconfig修改,建议选择支持16M的硬件,后续扩展开发和OTA更方便
partitions_32M_sensecap.csv 分区配置文件:32M Flash的分区表
partitions_4M.csv 分区配置文件:4M Flash的分区表
partitions_8M.csv 分区配置文件:8M Flash的分区表
sdkconfig.defaults idf sdk默认配置,工程目前支持esp32s3和esp32c3两款芯片,只有s3支持唤醒词,基于乐鑫esp-sr实现
sdkconfig.defaults.esp32c3 idf sdk esp32c3默认配置
sdkconfig.defaults.esp32s3 idf sdk esp32s3默认配置
docs/ 文档文件,里面包含README文档使用的图片资料,还有一份服务器websocket交互协议
scripts/ 脚本文件,包含音频编码测试脚本、烧录调试脚本、版本打包相关脚本
.github/ GitHub Actions 的配置文件(.yaml 格式),用于定义项目的自动化工作流程,例如代码的持续集成(CI),可以在每次代码推送到仓库时自动运行测试、代码检查等任务
main/ 主要源码文件夹
工程主要使用C++语言进行实现,那如何看懂一个C++工程呢?首先要求我们有C++相关的基础,因为我们已经学习过C语言,所以上手C++也会很快,主要是面向对象的特性和C++的一些高级特性即可:

这部分的学习可以参考:https://www.runoob.com/cplusplus/cpp-tutorial.html 进行即可。
你可以快速过一遍,后面配合工程、AI工具进行具体学习。
掌握 C++ 的面向对象编程(OOP)概念。
类和对象
- 类的定义和使用
- 对象的创建和初始化
成员函数
- 成员函数的定义和调用
- 内联函数
构造函数和析构函数
- 默认构造函数
- 参数化构造函数
- 析构函数
访问控制
- 公有(public)、私有(private)、保护(protected)
继承
- 单继承
- 多继承(了解其复杂性)
多态
- 虚函数
- 纯虚函数和抽象类
运算符重载
- 运算符重载的基本概念
- 常见运算符的重载
友元函数和友元类
- 友元函数
- 友元类
模板
- 函数模板
- 类模板
掌握 C++ 的高级主题和最佳实践。
STL (标准模板库)
- 容器(vector, list, map, set)
- 算法(sort, find, for_each)
- 迭代器
异常处理
- try-catch 块
- 自定义异常类
智能指针
- std::unique_ptr
- std::shared_ptr
- std::weak_ptr
RAII ( Resource Acquisition Is Initialization )
- RAII 原则及其应用
文件操作
- 文件流(ifstream, ofstream, fstream)
- 文件读写操作
多线程编程
- 线程的基本概念
- std::thread
- 同步机制(mutex, lock_guard, unique_lock)
正则表达式
- 正则表达式的基本概念
- 使用 std::regex 进行字符串匹配
C++11/14/17/20 新特性
- auto 关键字
- lambda 表达式
工程类图
看C++工程和看C工程有个不同,那就是我们必须要先了解工程的类图,类图很重要,它可以加速我们熟悉工程架构和代码理解。
系统架构
- 架构设计:通过类之间的继承、组合、依赖关系,了解工程的架构和数据流向。
- 架构风格:判断是分层架构、组件化架构,还是事件驱动模型。
代码理解
- 降低认知成本:图形化展示比阅读代码更直观。
- 关键路径分析:让我们聚焦核心类,忽略辅助类。
整个工程的类图如下,主要围绕Application Board ThingManager 三个大类进行实现:

Application Board ThingManager 的说明类
- Application 类作为整个应用程序的核心管理类,负责协调和控制各个功能模块的运行,包含了设备状态管理、音频处理、固件升级、任务调度等功能。
- Board 类是一个抽象基类,定义了硬件设备的接口,为不同的硬件平台提供统一的操作方法,具体的硬件实现由派生类完成。
- ThingManager 类作为物联网设备管理类,负责管理多个 Thing 对象,提供了设备描述信息获取、设备状态获取和设备方法调用等功能。
三者之间的关系
- Application 类依赖于 Board 类,通过 Board::GetInstance() 获取硬件设备的实例,实现与硬件的交互。
- Application 类会使用 ThingManager 类来管理物联网设备,通过 ThingManager::GetInstance() 获取设备管理实例,实现设备的描述信息获取、状态获取和方法调用等功能。
- Board 类和 ThingManager 类之间没有直接的依赖关系,但它们都为 Application 类提供了必要的功能支持。
综上所述,Application 类负责整个应用程序的核心逻辑,Board 类提供硬件设备的接口,ThingManager 类负责物联网设备的管理,三者协同工作,实现了一个完整的应用程序。
main****源码说明
了解了整个工程架构后,我们就可以开始找到程序的入口函数,逐步了解整个工程的实现细节了。
整个工程的入口函数在xiaozhi-esp32/main/main.cc中,初始化默认事件循环和NVS后,通过Application::GetInstance().Start()直接启动应用。
extern "C" void app_main(void)
{
// 初始化默认事件循环
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 初始化 NVS flash 以存储 WiFi 配置
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "Erasing NVS flash to fix corruption");
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 启动应用程序
Application::GetInstance().Start();
// 主线程将退出并释放堆栈内存
}
获取单例对象与设置设备状态
应用启动时,会获取板卡对象,后续所有的板卡相关资源,例如灯光、屏幕、音频codec等等,都由board对象进行获取。
void Application::Start() {
auto& board = Board::GetInstance();
SetDeviceState(kDeviceStateStarting);
.....
}
获取板卡对象时,对象由create_board创建。
static Board& GetInstance() {
static Board* instance = static_cast<Board*>(create_board());
return *instance;
}
#define DECLARE_BOARD(BOARD_CLASS_NAME) \
void* create_board() { \
return new BOARD_CLASS_NAME(); \
}
具体使用哪个板卡类创建对象呢?由xiaozhi-esp32/main/CMakeLists.txt进行选择,CMakeLists.txt根据menuconfig选择的板卡名称决定

这里的名称选择后,会根据xiaozhi-esp32/main/Kconfig.projbuild文件,设置某个宏为true,并存储到xiaozhi-esp32/sdkconfig中
xiaozhi-esp32/main/Kconfig.projbuild:
choice BOARD_TYPE
prompt "Board Type"
default BOARD_TYPE_BREAD_COMPACT_WIFI
help
Board type. 开发板类型
config BOARD_TYPE_BREAD_COMPACT_WIFI
bool "面包板新版接线(WiFi)"
config BOARD_TYPE_BREAD_COMPACT_WIFI_LCD
bool "面包板新版接线(WiFi)+ LCD"
config BOARD_TYPE_BREAD_COMPACT_ML307
bool "面包板新版接线(ML307 AT)"
config BOARD_TYPE_BREAD_COMPACT_ESP32
bool "面包板 ESP32 DevKit"
config BOARD_TYPE_ESP_BOX_3
iaozhi-esp32/sdkconfig:
CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI=y
# CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD is not set
# CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307 is not set
# CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32 is not set
最终在cmake构建时,会编译对应的.c文件
# 根据 BOARD_TYPE 配置添加对应的板级文件
if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
set(BOARD_TYPE "bread-compact-wifi")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307)
set(BOARD_TYPE "bread-compact-ml307")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32)
set(BOARD_TYPE "bread-compact-esp32")
elseif(CONFIG_BOARD_TYPE_DF_K10)
set(BOARD_TYPE "df-k10")
elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
set(BOARD_TYPE "esp-box-3")
.....
.....
....
file(GLOB BOARD_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc
${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.c
)
至此,获取到Board实例,然后通过SetDeviceState把设备状态设定为 kDeviceStateStarting,表示应用程序正在启动。
配置显示与音频编解码器
auto display = board.GetDisplay();
auto codec = board.GetAudioCodec();
opus_decode_sample_rate_ = codec->output_sample_rate();
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(opus_decode_sample_rate_, 1);
opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
- board.GetDisplay():从 Board 对象获取显示设备。
- board.GetAudioCodec():从 Board 对象获取音频编解码器。
- opus_decode_sample_rate_:记录音频编解码器的输出采样率。
- opus_decoder_:创建一个 OpusDecoderWrapper 对象,用于音频解码。
- opus_encoder_:创建一个 OpusEncoderWrapper 对象,用于音频编码。
根据不同板子类型设置音频编码器复杂度
if (board.GetBoardType() == "ml307") {
ESP_LOGI(TAG, "ML307 board detected, setting opus encoder complexity to 5");
opus_encoder_->SetComplexity(5);
} else {
ESP_LOGI(TAG, "WiFi board detected, setting opus encoder complexity to 3");
opus_encoder_->SetComplexity(3);
}
- 若检测到是 ml307 (4G-AT通信)板子,就将 Opus 编码器的复杂度设为 5,以节省带宽;若为其他板子,则设为 3,以节省 CPU 资源。
配置重采样器
if (codec->input_sample_rate() != 16000) {
input_resampler_.Configure(codec->input_sample_rate(), 16000);
reference_resampler_.Configure(codec->input_sample_rate(), 16000);
}
- 若音频编解码器的输入采样率并非 16000 Hz,就对输入重采样器和参考重采样器进行配置,将输入采样率转换为 16000 Hz。
设置音频编解码器的回调函数
codec->OnInputReady([this, codec]() {
BaseType_t higher_priority_task_woken = pdFALSE;
xEventGroupSetBitsFromISR(event_group_, AUDIO_INPUT_READY_EVENT, &higher_priority_task_woken);
return higher_priority_task_woken == pdTRUE;
});
codec->OnOutputReady([this]() {
BaseType_t higher_priority_task_woken = pdFALSE;
xEventGroupSetBitsFromISR(event_group_, AUDIO_OUTPUT_READY_EVENT, &higher_priority_task_woken);
return higher_priority_task_woken == pdTRUE;
});
codec->Start();
- OnInputReady 和 OnOutputReady:分别设置音频输入和输出准备好时的回调函数。当音频输入或输出准备好时,会设置相应的事件标志。
- codec->Start():启动音频编解码器。
创建主循环任务
xTaskCreate([](void* arg) {
Application* app = (Application*)arg;
app->MainLoop();
vTaskDelete(NULL);
}, "main_loop", 4096 * 2, this, 4, nullptr);
- xTaskCreate:创建一个新的任务,任务名为 main_loop,该任务会调用 Application 对象的 MainLoop() 函数。
启动网络并初始化协议
board.StartNetwork();
display->SetStatus(Lang::Strings::LOADING_PROTOCOL);
#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
protocol_ = std::make_unique<WebsocketProtocol>();
#else
protocol_ = std::make_unique<MqttProtocol>();
#endif
- board.StartNetwork():启动网络连接。
- display->SetStatus(Lang::Strings::LOADING_PROTOCOL):在显示设备上显示加载协议的状态信息。
- 根据配置,选择使用 WebSocket 协议或 MQTT 协议。
设置协议的回调函数
protocol_->OnNetworkError([this](const std::string& message) {
SetDeviceState(kDeviceStateIdle);
Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
});
protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) {
std::lock_guard<std::mutex> lock(mutex_);
if (device_state_ == kDeviceStateSpeaking) {
audio_decode_queue_.emplace_back(std::move(data));
}
});
// 其他回调函数...
protocol_->Start();
- OnNetworkError:当网络出现错误时,将设备状态设置为 kDeviceStateIdle,并发出警报。
- OnIncomingAudio:当接收到音频数据时,若设备处于 kDeviceStateSpeaking 状态,就把音频数据加入解码队列。
检查固件版本
ota_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL);
ota_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
ota_.SetHeader("Client-Id", board.GetUuid());
ota_.SetHeader("Accept-Language", Lang::CODE);
auto app_desc = esp_app_get_description();
ota_.SetHeader("User-Agent", std::string(BOARD_NAME "/") + app_desc->version);
xTaskCreate([](void* arg) {
Application* app = (Application*)arg;
app->CheckNewVersion();
vTaskDelete(NULL);
}, "check_new_version", 4096 * 2, this, 2, nullptr);
- 对 OTA(Over-the-Air)更新相关的信息进行设置,包括检查版本的 URL 和请求头。
- 创建一个新任务来检查是否有新的固件版本。
初始化音频处理器(可选)
因为有的板卡不支持本地跑降兆模型,所以这里通过menuconfig配置为可选
#if CONFIG_USE_AUDIO_PROCESSOR
audio_processor_.Initialize(codec->input_channels(), codec->input_reference());
audio_processor_.OnOutput([this](std::vector<int16_t>&& data) {
background_task_->Schedule([this, data = std::move(data)]() mutable {
opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
Schedule([this, opus = std::move(opus)]() {
protocol_->SendAudio(opus);
});
});
});
});
#endif
- 若启用了音频处理器,就对其进行初始化,并设置输出回调函数。当音频处理器有输出时,会对音频数据进行编码并通过协议发送出去。
初始化唤醒词检测(可选)
因为有的板卡不支持本地跑唤醒词模型,所以这里通过menuconfig配置为可选
#if CONFIG_USE_WAKE_WORD_DETECT
wake_word_detect_.Initialize(codec->input_channels(), codec->input_reference());
wake_word_detect_.OnVadStateChange([this](bool speaking) {
Schedule([this, speaking]() {
if (device_state_ == kDeviceStateListening) {
if (speaking) {
voice_detected_ = true;
} else {
voice_detected_ = false;
}
auto led = Board::GetInstance().GetLed();
led->OnStateChanged();
}
});
});
// 其他唤醒词检测回调函数...
wake_word_detect_.StartDetection();
#endif
- 若启用了唤醒词检测,就对其进行初始化,并设置相关的回调函数。当检测到语音活动状态变化或唤醒词时,会执行相应的操作。
最终设置
SetDeviceState(kDeviceStateIdle);
esp_timer_start_periodic(clock_timer_handle_, 1000000);
- 把设备状态设置为 kDeviceStateIdle,表示应用程序已准备好。
- 启动一个周期性定时器。
至此,板卡部分初始化完成。后续的总体流程总结如下:
设备端初始化
- 设备上电、初始化 Application:
- 初始化音频编解码器、显示屏、LED 等
- 连接网络
- 创建并初始化实现 Protocol 接口的 WebSocket 协议实例(WebsocketProtocol)
- 进入主循环等待事件(音频输入、音频输出、调度任务等)。
建立 WebSocket 连接
- 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 OpenAudioChannel():
- 根据编译配置获取 WebSocket URL(CONFIG_WEBSOCKET_URL)
- 设置若干请求头(Authorization, Protocol-Version, Device-Id, Client-Id)
- 调用 Connect() 与服务器建立 WebSocket 连接
发送客户端 "hello" 消息
-
连接成功后,设备会发送一条 JSON 消息,示例结构如下:
json
{
"type": "hello",
"version": 1,
"transport": "websocket",
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
} -
其中 "frame_duration" 的值对应 OPUS_FRAME_DURATION_MS(例如 60ms)。
服务器回复 "hello"
- 设备等待服务器返回一条包含 "type": "hello" 的 JSON 消息,并检查 "transport": "websocket" 是否匹配。
- 如果匹配,则认为服务器已就绪,标记音频通道打开成功。
- 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。
后续消息交互
- 设备端和服务器端之间可发送两种主要类型的数据:
- 二进制音频数据(Opus 编码)
- 文本 JSON 消息(用于传输聊天状态、TTS/STT 事件、IoT 命令等)
- 在代码里,接收回调主要分为:
- OnData(...):
- 当 binary 为 true 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。
- 当 binary 为 false 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理(见下文消息结构)。
- 当服务器或网络出现断连,回调 OnDisconnected() 被触发:
- 设备会调用 on_audio_channel_closed_(),并最终回到空闲状态。
关闭 WebSocket 连接
- 设备在需要结束语音会话时,会调用 CloseAudioChannel() 主动断开连接,并回到空闲状态。
- 或者如果服务器端主动断开,也会引发同样的回调流程。
工程依赖组件
xiaozhi-esp32/main/idf_component.yml
工程中很多驱动和模块都是以组件方式进行管理,乐鑫组件库参考:https://components.espressif.com/,善用组件库可以大大提高我们的开发效率。
## IDF Component Manager Manifest File
dependencies:
## LCD驱动
waveshare/esp_lcd_sh8601: "1.0.2"
espressif/esp_lcd_ili9341: "==1.2.0"
espressif/esp_lcd_gc9a01: "^2.0.1"
espressif/esp_lcd_st77916: "^1.0.1"
espressif/esp_lcd_spd2010: "==1.0.2"
espressif/esp_io_expander_tca9554: "==2.0.0"
espressif/esp_lcd_panel_io_additions: "^1.0.1"
78/esp_lcd_nv3023: "~1.0.0"
## wifi连接
78/esp-wifi-connect: "~2.3.1"
## opus编码
78/esp-opus-encoder: "~2.1.0"
## ml307 4G 模块库
78/esp-ml307: "~1.7.2"
## 字库
78/xiaozhi-fonts: "~1.3.2"
## led库
espressif/led_strip: "^2.4.1"
## codec
espressif/esp_codec_dev: "~1.3.2"
## 音频前端算法-唤醒、降噪、VAD
espressif/esp-sr: "^1.9.0"
## 按键库
espressif/button: "^3.3.1"
## lvgl显示框架
lvgl/lvgl: "~9.2.2"
## 乐鑫LVGL适配库
esp_lvgl_port: "~2.4.4"
## Required IDF version
idf:
version: ">=5.3"
类说明
Application****类说明
Application 类是一个核心类,负责管理整个应用程序的生命周期、状态转换、音频处理、网络通信以及系统更新等功能。以下是对 Application 类的详细分析:
类的基本信息
单例模式:
通过 static Application& GetInstance() 方法实现单例模式,确保整个应用程序中只有一个 Application 实例。同时,通过 Application(const Application&) = delete; 和 Application& operator=(const Application&) = delete; 禁止拷贝构造和赋值操作,保证单例的唯一性。
枚举类型:
定义了 DeviceState 枚举,用于表示设备的不同状态,如 kDeviceStateIdle(空闲状态)、kDeviceStateListening(监听状态)等。
成员变量
-
状态管理:
volatile DeviceState device_state_:记录设备的当前状态,使用 volatile 关键字确保在多任务环境下状态的可见性。
bool keep_listening_:表示是否持续监听的标志。
bool aborted_:表示操作是否被中止的标志。
bool voice_detected_:表示是否检测到语音的标志。 -
任务调度:
std::list<std::function<void()>> main_tasks_:存储待执行的任务,使用 std::function 可以灵活地存储不同类型的任务。
EventGroupHandle_t event_group_:用于事件管理,通过事件组来触发不同的操作。
esp_timer_handle_t clock_timer_handle_:时钟定时器句柄,用于定时执行某些操作。 -
音频处理:
std::list<std::vector<uint8_t>> audio_decode_queue_:音频解码队列,存储待解码的音频数据。
std::unique_ptr<OpusEncoderWrapper> opus_encoder_ 和 std::unique_ptr<OpusDecoderWrapper> opus_decoder_:分别用于音频编码和解码。
OpusResampler input_resampler_、OpusResampler reference_resampler_ 和 OpusResampler output_resampler_:用于音频采样率的转换。 -
其他组件:
Ota ota_:用于固件升级管理。
std::unique_ptr<Protocol> protocol_:网络协议的智能指针,负责与服务器进行通信。
BackgroundTask* background_task_:后台任务指针,用于执行一些耗时的操作
成员函数
构造函数和析构函数
构造函数 Application::Application():初始化事件组、后台任务和时钟定时器。
析构函数 Application::~Application():停止并删除时钟定时器,释放后台任务的内存,删除事件组
核心功能函数
void Start():应用程序的启动函数,主要完成以下操作:
设置设备状态为 kDeviceStateStarting。
初始化显示和音频编解码器,配置音频采样率和编解码器的复杂度。
启动音频输入和输出的事件监听。
创建主循环任务,处理事件和任务调度。
启动网络连接,初始化网络协议。
检查固件更新,启动时钟定时器。
void MainLoop():主循环函数,通过事件组等待不同的事件(如音频输入就绪、音频输出就绪、任务调度事件),并根据事件类型执行相应的操作。
状态管理函数
void SetDeviceState(DeviceState state):设置设备的当前状态。
DeviceState GetDeviceState() const:获取设备的当前状态。
音频处理函数
void InputAudio():处理音频输入,包括采样率转换、唤醒词检测、音频编码和发送等操作。
void OutputAudio():处理音频输出,包括音频解码、采样率转换和输出等操作。
void ResetDecoder():重置解码器状态,清空音频解码队列。
void PlaySound(const std::string_view& sound):播放指定的声音,将声音数据添加到音频解码队列。
网络通信函数
void StartListening():开始监听音频输入,打开音频通道并发送开始监听的消息。
void StopListening():停止监听音频输入,发送停止监听的消息并设置设备状态为空闲。
void WakeWordInvoke(const std::string& wake_word):当检测到唤醒词时,根据设备状态进行相应的操作,如切换到聊天状态、中止语音播放等。
系统管理函数
void CheckNewVersion():检查是否有新的固件版本可用,如果有则进行升级操作。
void ShowActivationCode():显示激活码,并通过语音播报。
void Alert(const char* status, const char* message, const char* emotion, const std::string_view& sound):显示警报信息,包括状态、消息、表情和声音。
void DismissAlert():取消警报信息,恢复设备的空闲状态。
void Reboot():重启设备。
任务调度函数
void Schedule(std::function<void()> callback):将任务添加到主任务列表,并触发任务调度事件。
- 代码逻辑流程
- 初始化:在 Start() 函数中完成设备的初始化,包括显示、音频、网络等组件的配置。
- 主循环:MainLoop() 函数不断等待事件的发生,根据事件类型调用相应的处理函数。
- 音频处理:InputAudio() 和 OutputAudio() 分别处理音频的输入和输出,通过编解码器和采样率转换器进行数据处理。
- 网络通信:通过 protocol_ 与服务器进行通信,处理音频通道的打开和关闭、消息的发送和接收等操作。
- 系统管理:CheckNewVersion() 检查固件更新,ShowActivationCode() 显示激活码,Alert() 和 DismissAlert() 处理警报信息。
Board****类
功能概述
Board 类是一个抽象基类,定义了硬件设备的接口,为不同的硬件平台提供统一的操作方法,具体的硬件实现由派生类完成。
-
单例模式:通过 GetInstance 方法实现单例模式,确保整个应用程序中只有一个 Board 实例。
static Board& GetInstance() {
static Board* instance = static_cast<Board*>(create_board());
return *instance;
} -
抽象方法:定义了多个纯虚函数,如 GetBoardType、GetAudioCodec、CreateHttp 等,要求派生类必须实现这些方法,以提供具体的硬件操作。
virtual std::string GetBoardType() = 0;
virtual AudioCodec* GetAudioCodec() = 0;
virtual Http* CreateHttp() = 0;
硬件接口封装:提供了一系列方法来获取硬件设备的指针,如 GetBacklight、GetLed、GetDisplay 等,方便应用程序与硬件进行交互。
virtual Backlight* GetBacklight() { return nullptr; }
virtual Led* GetLed();
virtual Display* GetDisplay();
ThingManager****类
功能概述
ThingManager 类作为物联网设备管理类,负责管理多个 Thing 对象,提供了设备描述信息获取、设备状态获取和设备方法调用等功能。
-
单例模式:通过 GetInstance 方法实现单例模式,确保整个应用程序中只有一个 ThingManager 实例。
static ThingManager& GetInstance() {
static ThingManager instance;
return instance;
} -
设备管理:通过 AddThing 方法添加 Thing 对象到管理列表中,方便对多个设备进行统一管理。
void ThingManager::AddThing(Thing* thing) {
things_.push_back(thing);
} -
描述信息和状态获取:提供 GetDescriptorsJson 和 GetStatesJson 方法,分别用于获取所有设备的描述信息和状态信息,并以 JSON 格式返回。
std::string ThingManager::GetDescriptorsJson() {
std::string json_str = "[";
for (auto& thing : things_) {
json_str += thing->GetDescriptorJson() + ",";
}
if (json_str.back() == ',') {
json_str.pop_back();
}
json_str += "]";
return json_str;
}bool ThingManager::GetStatesJson(std::string& json, bool delta) {
// ...
} -
方法调用:提供 Invoke 方法,根据传入的 JSON 命令,查找对应的 Thing 对象并调用其方法。
void ThingManager::Invoke(const cJSON* command) {
auto name = cJSON_GetObjectItem(command, "name");
for (auto& thing : things_) {
if (thing->name() == name->valuestring) {
thing->Invoke(command);
return;
}
}
}
类详细说明
main/iot****ThingManager 类说明
main/iot 中的类主要围绕设备(Thing)的属性、方法和参数管理展开交互,设备这些属性和方法可以被模型控制和调用,工程把这部分的功能划为设备物联网模块,一个设备可以创建多个Thing,然后通过ThingManager进行管理,例如工程创建了电池、背光、LED、扬声器四个Thing,使得模型可以读取电池电量、背光数值,控制背光等等操作,我们也可以自定义自己设备的Thing添加给模型调用。

Thing 类与 PropertyList、MethodList 类
- Thing 类包含 PropertyList 和 MethodList:Thing 类作为物联网设备的抽象表示,内部包含了 PropertyList 对象 properties_ 和 MethodList 对象 methods_。这意味着每个 Thing 实例都可以拥有自己的属性集合和方法集合。
- 添加属性和方法:在创建 Thing 实例后,可以通过 properties_ 的相关方法(如 AddBooleanProperty、AddNumberProperty、AddStringProperty)向设备添加属性,通过 methods_ 的相关方法(如 AddMethod)向设备添加方法。例如,在 Battery 类(继承自 Thing)的构造函数中,使用 properties_.AddNumberProperty 和 properties_.AddBooleanProperty 为电池设备添加电量和充电状态属性。
PropertyList 与 Property 类
- PropertyList 管理 Property 集合:PropertyList 类内部使用 std::vector<Property> 来存储多个 Property 对象。通过 AddBooleanProperty、AddNumberProperty 和 AddStringProperty 等方法,PropertyList 可以将不同类型的 Property 对象添加到集合中。
- 获取属性信息:PropertyList 提供了 GetDescriptorJson 和 GetStateJson 方法,用于生成所有属性的描述信息和当前状态的 JSON 字符串。这些方法会遍历内部存储的 Property 对象,调用每个 Property 的 GetDescriptorJson 和 GetStateJson 方法,将结果组合成一个完整的 JSON 字符串。
MethodList 与 Method 类
- MethodList 管理 Method 集合:MethodList 类类似 PropertyList,内部使用 std::vector<Method> 来存储多个 Method 对象。通过 AddMethod 方法,MethodList 可以将新的 Method 对象添加到集合中。
Method 与 ParameterList 类
- Method 包含 ParameterList:Method 类内部包含一个 ParameterList 对象 parameters_,用于管理该方法所需的参数。在创建 Method 实例时,需要传入一个 ParameterList 对象,以定义方法的参数。
- 执行方法时传递参数:当调用 Method 的 Invoke 方法时,会将 ParameterList 作为参数传递给回调函数 callback_。回调函数可以根据 ParameterList 中的参数值执行相应的操作。
ParameterList 与 Parameter 类
- ParameterList 管理 Parameter 集合:ParameterList 类内部使用 std::vector<Parameter> 来存储多个 Parameter 对象。通过 AddParameter 方法,ParameterList 可以将新的 Parameter 对象添加到集合中。
- 获取参数信息:ParameterList 提供了 GetDescriptorJson 方法,用于生成所有参数的描述信息的 JSON 字符串。该方法会遍历内部存储的 Parameter 对象,调用每个 Parameter 的 GetDescriptorJson 方法,将结果组合成一个完整的 JSON 字符串。
ThingManager 类
- ThingManager 管理 Thing 集合:ThingManager 类采用单例模式,通过 GetInstance 方法获取唯一实例。该类内部有一个 std::vector<Thing*> 类型的成员变量 things_,用于存储所有的 Thing 对象。
- 添加和管理设备:在各个 InitializeIot 函数中,通过 iot::ThingManager::GetInstance().AddThing 方法将不同类型的 Thing(如 Speaker、Battery、Backlight 等)添加到 ThingManager 的管理列表中。ThingManager 可以对这些 Thing 对象进行统一管理,例如遍历所有设备,获取它们的属性和方法信息等。
Speaker 类:
- 继承自 Thing 类,代表扬声器设备。在其构造函数中,定义了设备的属性(如当前音量值)和可远程执行的指令(如设置音量)。
Backlight 类:
- 继承自 Thing 类,代表屏幕背光设备。在其构造函数中,定义了设备的属性(如当前亮度百分比)和可远程执行的指令(如设置亮度)。
Battery 类:
- 继承自 Thing 类,代表电池管理设备。在其构造函数中,定义了设备的属性(如当前电量百分比、是否充电中)。
如果需要自定义一个属于自己设备的Thing对象,我们可以参考上述的Speaker 、Backlight、Battery 进行,例如要创建一个表示智能灯泡的 Thing 对象,伪代码如下:
创建 Thing 实例:
iot::Thing light("SmartLight", "A smart light bulb");
添加属性:
light.properties_.AddBooleanProperty("isOn", "Whether the light is on", []() { return isLightOn; });
light.properties_.AddNumberProperty("brightness", "The brightness of the light", []() { return lightBrightness; });
添加方法:
iot::ParameterList turnOnParams;// 可以添加参数到 turnOnParams 中,这里假设无参数
iot::Method turnOnMethod("turnOn", "Turn on the light", turnOnParams, [](const iot::ParameterList& params) {// 实现开灯逻辑
isLightOn = true;});
light.methods_.AddMethod(turnOnMethod);
将 Thing 添加到 ThingManager 中:
iot::ThingManager::GetInstance().AddThing(&light);
通过以上步骤,就完成了一个智能灯泡设备的创建和注册,并且可以通过 ThingManager 对其进行管理,通过 PropertyList 和 MethodList 对其属性和方法进行操作。
main/led Led****类说明
main/led 目录下的代码中,主要涉及 Led、NoLed、SingleLed 和 CircularStrip这几个类

Led 是抽象基类:Led 类定义了一个纯虚函数 OnStateChanged(),这使得 Led 类成为抽象类,不能直接实例化。其主要作用是为派生类提供一个统一的接口,强制派生类实现 OnStateChanged() 方法,以根据设备状态设置 LED 的状态。
class Led {
public:
virtual ~Led() = default;
// Set the led state based on the device state
virtual void OnStateChanged() = 0;
};
-
NoLed 继承自 Led:NoLed 类继承了 Led 类,并重写了 OnStateChanged() 方法,但该方法为空实现。这通常用于表示一个没有实际 LED 功能的占位符。
class NoLed : public Led {
public:
virtual void OnStateChanged() override {}
}; -
SingleLed 继承自 Led:SingleLed 类继承了 Led 类,实现了 OnStateChanged() 方法,用于根据设备状态控制单个 LED 的颜色、闪烁等行为。
class SingleLed : public Led {
public:
SingleLed(gpio_num_t gpio);
virtual ~SingleLed();
void OnStateChanged() override;
// 其他成员函数和变量
}; -
GpioLed 继承自 Led:GpioLed 类同样继承了 Led 类,实现了 OnStateChanged() 方法,通过 GPIO 控制 LED 的亮度、闪烁和渐变效果。
class GpioLed : public Led {
public:
GpioLed(gpio_num_t gpio, int output_invert=0);
virtual ~GpioLed();
void OnStateChanged() override;
// 其他成员函数和变量
}; -
CircularStrip 类:继承自 Led 类,实现 OnStateChanged() 方法。该类用于控制环形 LED 灯带,支持设置亮度、颜色,以及实现闪烁、呼吸、跑马灯等动画效果。
class CircularStrip : public Led {
public:
CircularStrip(gpio_num_t gpio, uint8_t max_leds);
virtual ~CircularStrip();
void OnStateChanged() override;
// 其他成员函数和变量
};
SingleLed 、CircularStrip 和 GpioLed 依赖于 Application 类:SingleLed 、CircularStrip 和 GpioLed 类的 OnStateChanged() 方法中,都通过 Application::GetInstance().GetDeviceState() 获取设备的当前状态,然后根据状态来控制 LED 的行为。这表明 SingleLed 和 GpioLed 类依赖于 Application 类提供的设备状态信息。
void xxxxLed::OnStateChanged() {
auto& app = Application::GetInstance();
auto device_state = app.GetDeviceState();
// 根据设备状态控制 LED
}
Board 类聚合 Led 类的对象:在各个 Board 类(如 KevinBoxBoard、CompactWifiBoardLCD 等)中,通过 GetLed() 方法返回一个 Led 类的指针,指向具体的 Led 派生类对象(如 SingleLed)。这表明 Board 类聚合了 Led 类的对象,Board 类可以使用 Led 类提供的接口来控制 LED。
class KevinBoxBoard : public WifiBoard{
public:
virtual Led* GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
};
我们可以看到,这里面基本上实现了大部分场景的灯光需求,如果我们自己的硬件有特殊的灯光需求,我们可以继承Led重新实现即可。
main/display****Display 类说明
main/display 目录下的类主要围绕 Display 类展开,这些类通过继承、组合以及方法调用等方式进行交互,以实现显示功能的管理和控制。

Display 类是核心类,它定义了显示功能的基本接口和状态管理。其主要功能包括:
-
初始化:在构造函数中加载显示主题、创建通知定时器、更新显示定时器和电源管理锁。
-
析构:在析构函数中停止并删除定时器,删除相关的显示对象和释放电源管理锁。
-
功能接口:提供了设置状态、显示通知、设置表情、设置聊天消息、设置图标、设置主题等一系列功能接口。
-
锁机制:定义了纯虚函数 Lock 和 Unlock,用于实现显示对象的锁定和解锁,需要派生类具体实现。
Display类构造函数中创建了notification_timer_、update_timer_两个定时器。
notification_timer_用于更新显示页面中notification_label_和status_label_的显示。
update_timer_用于根据板卡Board对象更新静音图标、电池图标、网络图标和设备状态。
DisplayLockGuard 类是一个辅助类,用于管理 Display 对象的锁定和解锁。其交互方式如下:
- 构造函数:在构造时调用 Display 对象的 Lock 方法尝试锁定显示对象,如果锁定失败会输出错误日志。
- 析构函数:在析构时调用 Display 对象的 Unlock 方法解锁显示对象。
NoDisplay 类继承自 Display 类,是一个特殊的显示类,用于表示没有实际显示设备的情况。其交互方式如下:
- 重写 Lock 方法:始终返回 true,表示无需锁定。
- 重写 Unlock 方法:方法体为空,不执行任何操作。
OledDisplay类继承自 Display 类,用于实现基于 SSD1306 OLED 显示屏的显示功能。
- 构造函数 OledDisplay:初始化 OLED 显示屏和 LVGL 库,并根据分辨率设置 UI。
- 析构函数 ~OledDisplay:释放相关资源,包括 LVGL 对象和 OLED 面板。
- SetChatMessage 方法:设置聊天消息的显示内容。
在OledDisplay中适配了SetupUI_128x64和SetupUI_128x32两个分辨率的UI,派生类OledDisplay会重写基类的虚函数,以实现特定显示器的功能。例如,OledDisplay 类重写了 SetChatMessage 方法,通过重写该方法,OledDisplay 类可以根据 OLED 显示器的特点处理聊天消息的显示。
void OledDisplay::SetChatMessage(const char* role, const char* content) {
DisplayLockGuard lock(this);
if (chat_message_label_ == nullptr) {
return;
}
std::string content_str = content;
std::replace(content_str.begin(), content_str.end(), '\n', ' ');
if (content_right_ == nullptr) {
lv_label_set_text(chat_message_label_, content_str.c_str());
} else {
if (content == nullptr || content[0] == '\0') {
lv_obj_add_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_label_set_text(chat_message_label_, content_str.c_str());
lv_obj_clear_flag(content_right_, LV_OBJ_FLAG_HIDDEN);
}
}
}
LcdDisplay:继承自 Display,是 LCD 显示相关类的基类,包含 LCD 面板的 I/O 句柄和面板句柄等成员,重写基类的虚函数 SetupUI、SetEmotion、SetIcon 等方法。
RgbLcdDisplay、SpiLcdDisplay :继承自LcdDisplay,方便我们根据不同接口的LCD屏幕进行显示部分初始化,在创建具体的显示器对象时,派生类的构造函数会调用基类的构造函数进行初始化。例如,RgbLcdDisplay、SpiLcdDisplay 等类的构造函数会调用 LcdDisplay 的构造函数:
// RgbLcdDisplay构造函数
RgbLcdDisplay::RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
int width, int height, int offset_x, int offset_y,
bool mirror_x, bool mirror_y, bool swap_xy,
DisplayFonts fonts)
: LcdDisplay(panel_io, panel, fonts) {
// ...
}
// SpiLcdDisplay构造函数
SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
int width, int height, int offset_x, int offset_y,
bool mirror_x, bool mirror_y, bool swap_xy,
DisplayFonts fonts)
: LcdDisplay(panel_io, panel, fonts) {
// ...
}
下方是应用创建一个自定义显示屏CustomLcdDisplay的调用步骤:
创建 Display 派生类对象(如 CustomLcdDisplay)
-> 调用基类(如 SpiLcdDisplay)构造函数
-> 调用基类(如 LcdDisplay)构造函数
-> 调用基类(如 Display)构造函数
-> 加载主题设置
-> 创建通知定时器和更新定时器
-> 创建电源管理锁
-> 初始化 LCD 面板句柄和字体
-> 初始化特定的 LCD 显示设置
-> 使用 DisplayLockGuard 锁定显示对象
-> 进行额外的显示设置(如调整样式、添加事件回调)
main/audio_processing****说明
audio_processing主要包含两个类WakeWordDetect和AudioProcessor
WakeWordDetect 类主要负责唤醒词的检测,同时也具备语音活动检测(VAD)的功能。其核心作用是监听音频输入,当检测到预设的唤醒词时,触发相应的操作,并在语音活动状态发生变化时通知系统。
AudioProcessor 类主要用于音频的实时处理,包括语音通信中的音频增强、增益控制等功能。它接收音频数据,对其进行处理,并将处理后的音频数据输出。
这两个类在音频处理流程中相互配合,WakeWordDetect 负责唤醒设备和VAD检测,而 AudioProcessor 负责处理唤醒后,实时交互的音频数据,使得音频变得更干净。

WakeWordDetect****类
在 Initialize 方法中,WakeWordDetect 类会基于乐鑫ESP_SR组件加载唤醒词模型,配置音频前端(AFE)参数,包括回声消除(AEC)、语音增强(SE)、语音活动检测(VAD)和唤醒词检测(WakeNet)等功能,同时启动一个独立的任务 AudioDetectionTask 来执行音频检测。
void WakeWordDetect::Initialize(int channels, bool reference) {
// 加载唤醒词模型
srmodel_list_t *models = esp_srmodel_init("model");
for (int i = 0; i < models->num; i++) {
if (strstr(models->model_name[i], ESP_WN_PREFIX) != NULL) {
wakenet_model_ = models->model_name[i];
auto words = esp_srmodel_get_wake_words(models, wakenet_model_);
std::stringstream ss(words);
std::string word;
while (std::getline(ss, word, ';')) {
wake_words_.push_back(word);
}
}
}
// 配置AFE参数
afe_config_t afe_config = {
// 配置参数...
};
afe_detection_data_ = esp_afe_sr_v1.create_from_config(&afe_config);
// 启动音频检测任务
xTaskCreate([](void* arg) {
auto this_ = (WakeWordDetect*)arg;
this_->AudioDetectionTask();
vTaskDelete(NULL);
}, "audio_detection", 4096 * 2, this, 3, nullptr);
}
Feed 方法用于接收音频数据,并将其存储在输入缓冲区中。当缓冲区中的数据量达到一定阈值时,会将数据馈送到 AFE 进行处理。
void WakeWordDetect::Feed(const std::vector<int16_t>& data) {
input_buffer_.insert(input_buffer_.end(), data.begin(), data.end());
auto feed_size = esp_afe_sr_v1.get_feed_chunksize(afe_detection_data_) * channels_;
while (input_buffer_.size() >= feed_size) {
esp_afe_sr_v1.feed(afe_detection_data_, input_buffer_.data());
input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + feed_size);
}
}
在 AudioDetectionTask 任务中,会不断从 AFE 中获取处理结果。当检测到唤醒词时,会停止检测,并调用 wake_word_detected_callback_ 回调函数通知外部系统。
void WakeWordDetect::AudioDetectionTask() {
while (true) {
xEventGroupWaitBits(event_group_, DETECTION_RUNNING_EVENT, pdFALSE, pdTRUE, portMAX_DELAY);
auto res = esp_afe_sr_v1.fetch(afe_detection_data_);
if (res->wakeup_state == WAKENET_DETECTED) {
StopDetection();
last_detected_wake_word_ = wake_words_[res->wake_word_index - 1];
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
}
}
}
}
同样在 AudioDetectionTask 中,会根据 AFE 返回的 VAD 状态,判断语音活动状态是否发生变化。如果发生变化,会调用 vad_state_change_callback_ 回调函数通知外部系统。
void WakeWordDetect::AudioDetectionTask() {
while (true) {
// ...
if (vad_state_change_callback_) {
if (res->vad_state == AFE_VAD_SPEECH && !is_speaking_) {
is_speaking_ = true;
vad_state_change_callback_(true);
} else if (res->vad_state == AFE_VAD_SILENCE && is_speaking_) {
is_speaking_ = false;
vad_state_change_callback_(false);
}
}
// ...
}
}
WakeWordDetect 类主要与 Application、Protocol 类存在交互关系。
-
Application 包含 WakeWordDetect:Application 类在 Start 方法中初始化 WakeWordDetect 对象,并为其注册回调函数。
#if CONFIG_USE_WAKE_WORD_DETECT
wake_word_detect_.Initialize(codec->input_channels(), codec->input_reference());
#endifwake_word_detect_.OnVadStateChange([this](bool speaking) {
Schedule(this, speaking {
if (device_state_ == kDeviceStateListening) {
if (speaking) {
voice_detected_ = true;
} else {
voice_detected_ = false;
}
auto led = Board::GetInstance().GetLed();
led->OnStateChanged();
}
});
});wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) {
Schedule(this, &wake_word {
if (device_state_ == kDeviceStateIdle) {
xxxx
} else if (device_state_ == kDeviceStateSpeaking) {
xxxx
} else if (device_state_ == kDeviceStateActivating) {
xxxx
}
// Resume detection
wake_word_detect_.StartDetection();
});
}); -
WakeWordDetect 调用 Application 的回调函数:当 WakeWordDetect 检测到唤醒词或语音活动状态变化时,会调用 Application 注册的回调函数。
-
Application 调用 Protocol 的方法:Application 在接收到 WakeWordDetect 的回调后,会调用 Protocol 的方法将唤醒词数据发送到服务器。
if (!protocol_->OpenAudioChannel()) {
wake_word_detect_.StartDetection();
return;
}std::vector<uint8_t> opus;
// Encode and send the wake word data to the server
while (wake_word_detect_.GetWakeWordOpus(opus)) {
protocol_->SendAudio(opus);
}
// Set the chat state to wake word detected
protocol_->SendWakeWordDetected(wake_word);
AudioProcessor****类
在 Initialize 方法中,AudioProcessor 类会配置 AFE 参数,包括语音通信相关的功能,如自动增益控制(AGC)等,同时启动一个独立的任务 AudioProcessorTask 来执行音频处理。
void AudioProcessor::Initialize(int channels, bool reference) {
channels_ = channels;
reference_ = reference;
int ref_num = reference_ ? 1 : 0;
afe_config_t afe_config = {
.aec_init = false,
.se_init = true,
.vad_init = false,
.wakenet_init = false,
// 其他配置参数...
};
afe_communication_data_ = esp_afe_vc_v1.create_from_config(&afe_config);
xTaskCreate([](void* arg) {
auto this_ = (AudioProcessor*)arg;
this_->AudioProcessorTask();
vTaskDelete(NULL);
}, "audio_communication", 4096 * 2, this, 3, NULL);
}
Input 方法用于接收音频数据,并将其存储在输入缓冲区中。当缓冲区中的数据量达到一定阈值时,会将数据馈送到 AFE 进行处理。
void AudioProcessor::Input(const std::vector<int16_t>& data) {
input_buffer_.insert(input_buffer_.end(), data.begin(), data.end());
auto feed_size = esp_afe_vc_v1.get_feed_chunksize(afe_communication_data_) * channels_;
while (input_buffer_.size() >= feed_size) {
auto chunk = input_buffer_.data();
esp_afe_vc_v1.feed(afe_communication_data_, chunk);
input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + feed_size);
}
}
在 AudioProcessorTask 任务中,会不断从 AFE 中获取处理后的音频数据,并调用 output_callback_ 回调函数将数据输出给外部系统。
void AudioProcessor::AudioProcessorTask() {
while (true) {
xEventGroupWaitBits(event_group_, PROCESSOR_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY);
auto res = esp_afe_vc_v1.fetch(afe_communication_data_);
if (res != nullptr && res->ret_value != ESP_FAIL) {
if (output_callback_) {
output_callback_(std::vector<int16_t>(res->data, res->data + res->data_size / sizeof(int16_t)));
}
}
}
}
AudioProcessor类主要与 Application、Protocol 类存在交互关系。
-
Application 包含 AudioProcessor:Application 类在 Start 方法中初始化 AudioProcessor对象,并为其注册回调函数OnOutput。
audio_processor_.Initialize(codec->input_channels(), codec->input_reference()); audio_processor_.OnOutput([this](std::vector<int16_t>&& data) { background_task_->Schedule([this, data = std::move(data)]() mutable { opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) { Schedule([this, opus = std::move(opus)]() { protocol_->SendAudio(opus); }); }); }); });
-
Application 主循环中通过codec采集音频,采集的音频发送到AudioProcessor中进行音频处理,当然,如果不开启CONFIG_USE_AUDIO_PROCESSOR,则会直接把音频发送到服务器。
void Application::InputAudio() {
xxxx
#if CONFIG_USE_AUDIO_PROCESSOR
if (audio_processor_.IsRunning()) {
audio_processor_.Input(data);
}
#else
if (device_state_ == kDeviceStateListening) {
background_task_->Schedule(this, data = std::move(data) mutable {
opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
Schedule(this, opus = std::move(opus) {
protocol_->SendAudio(opus);
});
});
});
}
#endif
} -
AudioProcessor处理完的音频回调,通过 AudioProcessor 发送到服务器。
audio_processor_.OnOutput([this](std::vector<int16_t>&& data) { background_task_->Schedule([this, data = std::move(data)]() mutable { opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) { Schedule([this, opus = std::move(opus)]() { protocol_->SendAudio(opus); }); }); }); });
main/audio_codecs****AudioCodec类说明
AudioCodec:定义了音频编解码器的通用接口和功能,包含设置音量、启用输入输出、启动编解码器等方法,同时定义了纯虚函数 Read 和 Write 供派生类实现。
- NoAudioCodec 类:继承自 AudioCodec,是一个无音频编解码器的基类,实现了基本的读写方法。
- NoAudioCodecDuplex 类:继承自 NoAudioCodec,用于双工模式下的无音频编解码。
- NoAudioCodecSimplex 类:继承自 NoAudioCodec,用于单工模式下的无音频编解码。
- NoAudioCodecSimplexPdm 类:继承自 NoAudioCodec,用于单工 PDM 模式下的无音频编解码。
- ATK_NoAudioCodecDuplex 类:继承自 NoAudioCodec,是特定的双工无音频编解码器。
- Es8311AudioCodec 类:继承自 AudioCodec,是基于 ES8311 芯片的音频编解码器。
- Es8388AudioCodec 类:继承自 AudioCodec,是基于 ES8388 芯片的音频编解码器。
这些类继承自 AudioCodec 类,并实现了 Read 和 Write 方法,以实现具体的音频编解码功能。
AudioCodec基类中的方法通过多态调用派生类的实现,以完成音频数据的输入和输出,同时,AudioCodec 类提供了事件回调机制,通过 OnInputReady 和 OnOutputReady 方法注册回调函数,当音频输入或输出准备好时,会触发相应的回调函数。

main/protocols****Protocol 类说明
main/protocols 目录下的代码中,主要定义了 Protocol 基类以及两个派生类 WebsocketProtocol 和 MqttProtocol
- Protocol 类:作为基类,定义了网络通信协议的通用接口和功能,如处理音频数据、JSON 数据、网络错误,以及启动、打开和关闭音频通道等操作。
- WebsocketProtocol 类:继承自 Protocol 类,使用 WebSocket 协议实现具体的通信功能。
- MqttProtocol 类:同样继承自 Protocol 类,使用 MQTT 协议实现具体的通信功能。

具体的websocket协议内容可以参考
以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述客户端(设备)与服务器之间如何通过 WebSocket 进行交互。该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。
---
## 1. 总体流程概览
1. **设备端初始化**
- 设备上电、初始化 `Application`:
- 初始化音频编解码器、显示屏、LED 等
- 连接网络
- 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`)
- 进入主循环等待事件(音频输入、音频输出、调度任务等)。
2. **建立 WebSocket 连接**
- 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`:
- 根据编译配置获取 WebSocket URL(`CONFIG_WEBSOCKET_URL`)
- 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`)
- 调用 `Connect()` 与服务器建立 WebSocket 连接
3. **发送客户端 "hello" 消息**
- 连接成功后,设备会发送一条 JSON 消息,示例结构如下:
```json
{
"type": "hello",
"version": 1,
"transport": "websocket",
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
```
- 其中 `"frame_duration"` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms)。
4. **服务器回复 "hello"**
- 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。
- 如果匹配,则认为服务器已就绪,标记音频通道打开成功。
- 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。
5. **后续消息交互**
- 设备端和服务器端之间可发送两种主要类型的数据:
1. **二进制音频数据**(Opus 编码)
2. **文本 JSON 消息**(用于传输聊天状态、TTS/STT 事件、IoT 命令等)
- 在代码里,接收回调主要分为:
- `OnData(...)`:
- 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。
- 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理(见下文消息结构)。
- 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发:
- 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。
6. **关闭 WebSocket 连接**
- 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。
- 或者如果服务器端主动断开,也会引发同样的回调流程。
---
## 2. 通用请求头
在建立 WebSocket 连接时,代码示例中设置了以下请求头:
- `Authorization`: 用于存放访问令牌,形如 `"Bearer <token>"`
- `Protocol-Version`: 固定示例中为 `"1"`
- `Device-Id`: 设备物理网卡 MAC 地址
- `Client-Id`: 设备 UUID(可在应用中唯一标识设备)
这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。
---
## 3. JSON 消息结构
WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。
### 3.1 客户端→服务器
1. **Hello**
- 连接成功后,由客户端发送,告知服务器基本参数。
- 例:
```json
{
"type": "hello",
"version": 1,
"transport": "websocket",
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
```
2. **Listen**
- 表示客户端开始或停止录音监听。
- 常见字段:
- `"session_id"`:会话标识
- `"type": "listen"`
- `"state"`:`"start"`, `"stop"`, `"detect"`(唤醒检测已触发)
- `"mode"`:`"auto"`, `"manual"` 或 `"realtime"`,表示识别模式。
- 例:开始监听
```json
{
"session_id": "xxx",
"type": "listen",
"state": "start",
"mode": "manual"
}
```
3. **Abort**
- 终止当前说话(TTS 播放)或语音通道。
- 例:
```json
{
"session_id": "xxx",
"type": "abort",
"reason": "wake_word_detected"
}
```
- `reason` 值可为 `"wake_word_detected"` 或其他。
4. **Wake Word Detected**
- 用于客户端向服务器告知检测到唤醒词。
- 例:
```json
{
"session_id": "xxx",
"type": "listen",
"state": "detect",
"text": "你好小明"
}
```
5. **IoT**
- 发送当前设备的物联网相关信息:
- **Descriptors**(描述设备功能、属性等)
- **States**(设备状态的实时更新)
- 例:
```json
{
"session_id": "xxx",
"type": "iot",
"descriptors": { ... }
}
```
或
```json
{
"session_id": "xxx",
"type": "iot",
"states": { ... }
}
```
---
### 3.2 服务器→客户端
1. **Hello**
- 服务器端返回的握手确认消息。
- 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。
- 可能会带有 `audio_params`,表示服务器期望的音频参数,或与客户端对齐的配置。
- 成功接收后客户端会设置事件标志,表示 WebSocket 通道就绪。
2. **STT**
- `{"type": "stt", "text": "..."}`
- 表示服务器端识别到了用户语音。(例如语音转文本结果)
- 设备可能将此文本显示到屏幕上,后续再进入回答等流程。
3. **LLM**
- `{"type": "llm", "emotion": "happy", "text": "😀"}`
- 服务器指示设备调整表情动画 / UI 表达。
4. **TTS**
- `{"type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,客户端进入 "speaking" 播放状态。
- `{"type": "tts", "state": "stop"}`:表示本次 TTS 结束。
- `{"type": "tts", "state": "sentence_start", "text": "..."}`
- 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。
5. **IoT**
- `{"type": "iot", "commands": [ ... ]}`
- 服务器向设备发送物联网的动作指令,设备解析并执行(如打开灯、设置温度等)。
6. **音频数据:二进制帧**
- 当服务器发送音频二进制帧(Opus 编码)时,客户端解码并播放。
- 若客户端正在处于 "listening" (录音)状态,收到的音频帧会被忽略或清空以防冲突。
---
## 4. 音频编解码
1. **客户端发送录音数据**
- 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。
- 如果客户端每次编码生成的二进制帧大小为 N 字节,则会通过 WebSocket 的 **binary** 消息发送这块数据。
2. **客户端播放收到的音频**
- 收到服务器的二进制帧时,同样认定是 Opus 数据。
- 设备端会进行解码,然后交由音频输出接口播放。
- 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。
---
## 5. 常见状态流转
以下简述设备端关键状态流转,与 WebSocket 消息对应:
1. **Idle** → **Connecting**
- 用户触发或唤醒后,设备调用 `OpenAudioChannel()` → 建立 WebSocket 连接 → 发送 `"type":"hello"`。
2. **Connecting** → **Listening**
- 成功建立连接后,若继续执行 `SendStartListening(...)`,则进入录音状态。此时设备会持续编码麦克风数据并发送到服务器。
3. **Listening** → **Speaking**
- 收到服务器 TTS Start 消息 (`{"type":"tts","state":"start"}`) → 停止录音并播放接收到的音频。
4. **Speaking** → **Idle**
- 服务器 TTS Stop (`{"type":"tts","state":"stop"}`) → 音频播放结束。若未继续进入自动监听,则返回 Idle;如果配置了自动循环,则再度进入 Listening。
5. **Listening** / **Speaking** → **Idle**(遇到异常或主动中断)
- 调用 `SendAbortSpeaking(...)` 或 `CloseAudioChannel()` → 中断会话 → 关闭 WebSocket → 状态回到 Idle。
---
## 6. 错误处理
1. **连接失败**
- 如果 `Connect(url)` 返回失败或在等待服务器 "hello" 消息时超时,触发 `on_network_error_()` 回调。设备会提示"无法连接到服务"或类似错误信息。
2. **服务器断开**
- 如果 WebSocket 异常断开,回调 `OnDisconnected()`:
- 设备回调 `on_audio_channel_closed_()`
- 切换到 Idle 或其他重试逻辑。
---
## 7. 其它注意事项
1. **鉴权**
- 设备通过设置 `Authorization: Bearer <token>` 提供鉴权,服务器端需验证是否有效。
- 如果令牌过期或无效,服务器可拒绝握手或在后续断开。
2. **会话控制**
- 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理,WebSocket 协议为空。
3. **音频负载**
- 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。
4. **IoT 指令**
- `"type":"iot"` 的消息用户端代码对接 `thing_manager` 执行具体命令,因设备定制而不同。服务器端需确保下发格式与客户端保持一致。
5. **错误或异常 JSON**
- 当 JSON 中缺少必要字段,例如 `{"type": ...}`,客户端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。
---
## 8. 消息示例
下面给出一个典型的双向消息示例(流程简化示意):
1. **客户端 → 服务器**(握手)
```json
{
"type": "hello",
"version": 1,
"transport": "websocket",
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
```
2. **服务器 → 客户端**(握手应答)
```json
{
"type": "hello",
"transport": "websocket",
"audio_params": {
"sample_rate": 16000
}
}
```
3. **客户端 → 服务器**(开始监听)
```json
{
"session_id": "",
"type": "listen",
"state": "start",
"mode": "auto"
}
```
同时客户端开始发送二进制帧(Opus 数据)。
4. **服务器 → 客户端**(ASR 结果)
```json
{
"type": "stt",
"text": "用户说的话"
}
```
5. **服务器 → 客户端**(TTS开始)
```json
{
"type": "tts",
"state": "start"
}
```
接着服务器发送二进制音频帧给客户端播放。
6. **服务器 → 客户端**(TTS结束)
```json
{
"type": "tts",
"state": "stop"
}
```
客户端停止播放音频,若无更多指令,则回到空闲状态。
---
## 9. 总结
本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧,完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、IoT 指令下发等。其核心特征:
- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。
- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流。
- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、IoT、WakeWord 等。
- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。
服务器与客户端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。
main/boards****Board 类说明
boards文件夹中,包含了所有已经适配过的板卡,因为每个板卡的外设、IO、资源不一样,所以和板卡有关的模块都可以放在此文件夹中进行初始化。
Board****类
Board 类是一个抽象基类,它定义了一系列纯虚函数,为不同类型的开发板提供了统一的接口。WifiBoard 和 Ml307Board 类继承自 Board 类,分别实现了通过 WiFi 和 ML307(4G模组)进行网络连接的功能。
Board 类:
- 作为抽象基类,包含了一系列纯虚函数,如 GetBoardType、GetAudioCodec 等,派生类需要实现这些函数。
- 包含了一些通用的成员变量和方法,如 uuid_ 和 GenerateUuid 方法,用于生成设备的唯一标识。
- 提供了 GetInstance 静态方法,用于获取 Board 类的单例对象。
WifiBoard 类:
- 继承自 Board 类,实现了通过 WiFi 进行网络连接的功能。
- 包含一个 wifi_config_mode_ 成员变量,用于表示是否进入 WiFi 配置模式。
- 提供了 EnterWifiConfigMode 和 ResetWifiConfiguration 方法,用于进入和重置 WiFi 配置。
Ml307Board 类:
- 继承自 Board 类,使用 ML307 调制解调器进行网络连接。
- 包含一个 modem_ 成员变量,用于管理 ML307 调制解调器。
- 提供了 WaitForNetworkReady 方法,用于等待网络连接就绪。

当前有了WiFi和4G两个类,那我们就可以根据自己的板卡类型,继承对应类实现我们板卡具体外设了,例如多合一板卡,使用的Code是ES8311,屏幕是ST7789,分辨率是172*320,我们就可以创建XiaozhiAioS3类继承WifiBoard,然后初始化对应模块的驱动即可。
I2cDevice****类
I2cDevice 类用于与 I2C 设备进行通信。
class I2cDevice {
public:
I2cDevice(i2c_master_bus_handle_t i2c_bus, uint8_t addr);
protected:
i2c_master_dev_handle_t i2c_device_;
void WriteReg(uint8_t reg, uint8_t value);
uint8_t ReadReg(uint8_t reg);
void ReadRegs(uint8_t reg, uint8_t* buffer, size_t length);
};
- 在构造函数中初始化 I2C 设备。
- 提供 WriteReg、ReadReg 和 ReadRegs 方法,用于读写 I2C 设备的寄存器
Button****类
Button 类用于处理按键事件。
class Button {
public:
#if CONFIG_SOC_ADC_SUPPORTED
Button(const button_adc_config_t& cfg);
#endif
Button(gpio_num_t gpio_num, bool active_high = false);
~Button();
void OnPressDown(std::function<void()> callback);
void OnPressUp(std::function<void()> callback);
void OnLongPress(std::function<void()> callback);
void OnClick(std::function<void()> callback);
void OnDoubleClick(std::function<void()> callback);
private:
gpio_num_t gpio_num_;
button_handle_t button_handle_ = nullptr;
std::function<void()> on_press_down_;
std::function<void()> on_press_up_;
std::function<void()> on_long_press_;
std::function<void()> on_click_;
std::function<void()> on_double_click_;
};
- 通过构造函数初始化按键。
- 使用 OnPressDown、OnPressUp、OnLongPress、OnClick 和 OnDoubleClick 方法注册按键事件的回调函数。