第10章 WebSocket模块
约 5567 字大约 19 分钟
2026-03-23
1 WebSocket 介绍
1.1 概述
WebSocket 协议是一种应用层通信协议, 是一种在单个 TCP 连接上进行全双工通信的协议,实现了客户端与服务器之间的持久性连接,允许双方随时主动发送数据。
主要特点:
- 持久连接:一次握手,长期连接
- 双向通信:服务器可以主动推送消息给客户端
- 低延迟:相比 HTTP 轮询,延迟显著降低
- 轻量级:头部信息很小,数据帧开销小
- 支持二进制和文本数据
1.2 通信过程
WebSocket 协议的通信是基于客户端和服务器之间的连接,通过升级 HTTP 连接来完成。它的工作过程如下:
(1) 建立连接
客户端首先发送一个 HTTP 请求到服务器,请求升级协议为 WebSocket。这个请求包含了一个 Upgrade 头部,表示希望将协议从 HTTP 升级为 WebSocket。
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13如果服务器支持 WebSocket 协议,它会回应一个 HTTP 101 响应,表示协议升级成功,连接已经建立为 WebSocket 连接。响应头示例如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: x3JJHMbDL1EzLkh9Qd1F3rAt01M=(2) 数据传输
一旦连接建立,客户端和服务器就可以通过 WebSocket 通道发送和接收消息。WebSocket 消息可以是文本(如 JSON 或 HTML)或二进制数据(如图像、音频或视频)。
WebSocket 协议的传输是基于帧(Frame)的,每个消息可以拆分成一个或多个帧进行传输。
(3) 关闭连接
当通信结束时,任意一方(客户端或服务器)可以发起关闭请求,发送一个 Close 帧。关闭帧会通知另一方该连接将被关闭,另一方应当发送一个确认的关闭帧。
完成握手后,连接将关闭。
1.3 协议帧格式
WebSocket 协议通过帧(Frame)进行数据的传输,每个 WebSocket 帧包含以下内容:
- 帧头(Header):包含控制信息,如是否是最后一个数据帧、数据的长度等。
- 数据载荷(Payload Data):包含实际的数据内容。
帧头格式:
| 字段名 (Field) | 位数 (Bits) | 说明 (Description) |
|---|---|---|
| FIN | 1 | 结束标志:1 表示该帧是消息的最后一帧;0 表示后续还有分片帧。 |
| RSV1 | 1 | 保留位 1:必须为 0(除非协商了扩展)。 |
| RSV2 | 1 | 保留位 2:必须为 0(除非协商了扩展)。 |
| RSV3 | 1 | 保留位 3:必须为 0(除非协商了扩展)。 |
| Opcode | 4 | 操作码:定义帧的数据类型 0x1:文本帧(Text Frame) 0x2:二进制帧(Binary Frame) 0x8:关闭帧(Close Frame) 0x9:Ping 帧(Ping Frame) 0xA:Pong 帧(Pong Frame) |
| Mask | 1 | 掩码标志:1 表示 Payload 数据被掩码(客户端→服务端必须为1);0 表示无掩码。 |
| Payload Len | 7 | 载荷长度:数据的长度,可以是 7 位、7+16 位或者 7+64 位,取决于数据的长度。 |
| Masking-Key | 0 或 32 | 掩码密钥:如果 Mask 标志为 1,则会有一个掩码密钥,用于对数据进行异或操作以增加安全性。 |
1.4 WebSocket应用场景
- 实时聊天: WebSocket 是在线聊天应用中非常常见的协议,客户端和服务器之间可以实时交换消息,不需要每次都建立新的连接。
- 在线游戏:在多人在线游戏中,游戏状态的实时更新需要低延迟的数据交换,WebSocket 能有效满足这一需求。
- 金融和股票交易系统:需要实时的市场数据更新,WebSocket 可以帮助客户端接收来自服务器的实时股票报价和交易信息。
- 物联网(IoT): WebSocket 用于传输实时传感器数据和控制信息,适用于智能家居、工业自动化等 IoT 场景。
- 实时通知系统: WebSocket 可用于推送通知,例如社交媒体、新闻推送、邮件通知等。
2 安装组件


3 代码实现
3.1 初始化webSocket连接
3.1.1 相关信息
地址:
wss://api.tenclass.net/xiaozhi/v1/token:
test-token
3.1.2 步骤
1. 添加初始化、启动、停止三个函数
2. 通过事件标志组实现启动函数在等待websocket连接成功后才能调用结束,变为阻塞函数3.1.3 代码
dri_websocket.h
#ifndef __DRI_WEBSOCKET_H__
#define __DRI_WEBSOCKET_H__
#include <stdio.h>
#include "esp_system.h"
#include "esp_event.h"
#include "esp_crt_bundle.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_websocket_client.h"
#include "esp_event.h"
#include <cJSON.h>
#include "com_debug.h"
#include "dri_esp32s3.h"
// 宏定义 Websocket服务器的URI
#define CONFIG_WEBSOCKET_URI "wss://api.tenclass.net/xiaozhi/v1/"
// 宏定义 Websocket服务器的API Token
#define WS_TOKEN "test-token"
// 宏定义 Websocket事件标志组,表示连接成功的位
#define WEBSOCKET_EVENT_CONNECTED_BIT (1 << 0)
// 全局声明变量
extern EventGroupHandle_t websocket_event_group;
/**
* @brief websocket 初始化
*
* @param text_cb 文本数据回调函数
* @param bin_cb 二进制数据回调函数
*/
void dri_websocket_init(void);
/**
* @brief websocket 启动
*
*/
void dri_websocket_start(void);
/**
* @brief websocket 关闭
*
*/
void dri_websocket_stop(void);
#endif /* __DRI_WEBSOCKET_H__ */dri_websocket.c
#include "dri_websocket.h"
// 定义websocket客户端对象
esp_websocket_client_handle_t client = NULL;
// 定义变量作为输出的前缀
static const char *TAG = "websocket";
// 定义事件标志组句柄
EventGroupHandle_t websocket_event_group = NULL;
// 静态函数;websocket事件处理函数
static void websocket_event_handler(void)
{
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
switch (event_id)
{
case WEBSOCKET_EVENT_BEGIN:
ESP_LOGI(TAG, "WEBSOCKET 开始连接");
break;
case WEBSOCKET_EVENT_CONNECTED:
ESP_LOGI(TAG, "WEBSOCKET 连接成功");
break;
case WEBSOCKET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "WEBSOCKET 连接断开");
ESP_LOGE(TAG, "HTTP status code %d", data->error_handle.esp_ws_handshake_status_code);
if (data->error_handle.error_type == WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT)
{
ESP_LOGE(TAG, "reported from esp-tls %d", data->error_handle.esp_tls_last_esp_err);
ESP_LOGE(TAG, "reported from tls stack %d", data->error_handle.esp_tls_stack_err);
ESP_LOGE(TAG, "captured as transport's socket errno %d", data->error_handle.esp_transport_sock_errno);
}
break;
case WEBSOCKET_EVENT_DATA:
ESP_LOGI(TAG, "WEBSOCKET 收到数据");
ESP_LOGI(TAG, "Received opcode=%d", data->op_code);
if (data->op_code == 0x08 && data->data_len == 2)
{
ESP_LOGW(TAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]);
}
else
{
ESP_LOGW(TAG, "Received=%.*s\n\n", data->data_len, (char *)data->data_ptr);
}
break;
case WEBSOCKET_EVENT_ERROR:
ESP_LOGI(TAG, "WEBSOCKET 错误事件");
ESP_LOGE(TAG, "HTTP status code %d", data->error_handle.esp_ws_handshake_status_code);
if (data->error_handle.error_type == WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT)
{
ESP_LOGE(TAG, "reported from esp-tls %d", data->error_handle.esp_tls_last_esp_err);
ESP_LOGE(TAG, "reported from tls stack %d", data->error_handle.esp_tls_stack_err);
ESP_LOGE(TAG, "captured as transport's socket errno %d", data->error_handle.esp_transport_sock_errno);
}
break;
case WEBSOCKET_EVENT_FINISH:
ESP_LOGI(TAG, "WEBSOCKET 事件完成");
break;
}
}
/**
* @brief websocket 初始化
*
* @param text_cb 文本数据回调函数
* @param bin_cb 二进制数据回调函数
*/
void dri_websocket_init(websocket_text_callback_t text_cb, websocket_binary_callback_t bin_cb)
{
// 1. 设置websocket客户端配置参数
esp_websocket_client_config_t websocket_cfg = {
.uri = CONFIG_WEBSOCKET_URI,
.crt_bundle_attach = esp_crt_bundle_attach};
// 2. 初始化websocket客户端
client = esp_websocket_client_init(&websocket_cfg);
// 3. 注册websocket事件处理函数
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
// 4. 设置websocket的请求头,例如添加Authorization头用于认证
// 4.1 Authorization
esp_websocket_client_append_header(client, "Authorization", "Bearer " WS_TOKEN);
// 4.2 Protocol-Version
esp_websocket_client_append_header(client, "Protocol-Version", "1");
// 4.3 Device-Id (小智服务器要求 Device-Id 的值不能包含大写字母)
uint8_t mac_str[18];
dri_esp32s3_get_mac(mac_str);
MY_LOGI("MAC地址: %s", mac_str);
esp_websocket_client_append_header(client, "Device-Id", (const char *)mac_str);
// 4.4 Client-Id (小智服务器要求 Client-Id 的值不能包含大写字母)
uint8_t uuid_str[37];
dri_esp32s3_get_uuid(uuid_str);
MY_LOGI("UUID: %s", uuid_str);
esp_websocket_client_append_header(client, "Client-Id", (const char *)uuid_str);
// 5. 创建时间标志组
websocket_event_group = xEventGroupCreate();
}
/**
* @brief websocket 启动, 当websocket连接成功,该函数才能结束
*
*/
void dri_websocket_start(void)
{
if (client != NULL && !esp_websocket_client_is_connected(client))
{
// 启动websocket连接
esp_websocket_client_start(client);
// 等待事件标志组置位, 如果不置位就一直阻塞
xEventGroupWaitBits(websocket_event_group, WEBSOCKET_EVENT_CONNECTED_BIT, pdTRUE, pdFALSE, portMAX_DELAY);
MY_LOGI("连接成功!");
}
}
/**
* @brief websocket 关闭
*
*/
void dri_websocket_stop(void)
{
if (client != NULL && esp_websocket_client_is_connected(client))
{
esp_websocket_client_close(client, portMAX_DELAY);
}
}3.2 服务器回复解析
3.2.1 服务器回复的种类
当我们发送唤醒词或者opus片段后,服务器会给我们回复,回复的种类包括:
1)语音识别结果
我们发送到服务器的opus片段,服务器会做语音识别,并以json的形式发回来,我们可以在设备上显示:
{
"session_id": "e373d277",
"text": "今天天气怎么样",
"type": "stt"
}2)大模型意图识别表情回复
大模型在回复我们时,会回复我们表情,我们可以根据这个表情在设备上显示相应动画,让机器人更生动
{
"emotion": "happy",
"session_id": "e373d277",
"text": "😊",
"type": "llm"
}3)文字回复
文字回复共有三种状态,start,stop,sentence_start。
每次对话开始时先发送start。每句话会发送一次sentence_start。对话结束时发送stop。
服务器会将大模型的回复以文字的形式发给我们,我们可以在设备上显示:
{
"session_id": "e373d277",
"state": "start",
"type": "tts"
}
{
"session_id": "e373d277",
"state": "sentence_start",
"text": "今天北京天气挺好的,晴朗,",
"type": "tts"
}
{
"session_id": "e373d277",
"state": "sentence_start",
"text": "气温21度,西北风5级",
"type": "tts"
}
{
"session_id": "e373d277",
"state": "stop",
"type": "tts"
}4)opus 音频片段
大模型会将回复的音频片段以binary包的形式发回来。我们需要将所有binary包解码并播放。
5) hello回复
{
"type": "hello",
"transport": "websocket",
"audio_params": {
"format": "opus",
"sample_rate": 24000,
"channels": 1,
"frame_duration": 60
}
}3.2.2 步骤
1. 在初始化的时候添加两个回调函数,出别处理文本回复和二进制回复
2. 在事件处理函数中,接收到数据的时候,根据接收的回复类型不同调用不同函数
3. main.c 中实现对不同类型回复的处理3.2.3 代码
dri_websocket.h
// 代码省略 ...
// 宏定义 Websocket服务器的URI
#define CONFIG_WEBSOCKET_URI "wss://api.tenclass.net/xiaozhi/v1/"
// 宏定义 Websocket服务器的API Token
#define WS_TOKEN "test-token"
// 宏定义 Websocket事件标志组,表示连接成功的位
#define WEBSOCKET_EVENT_CONNECTED_BIT (1 << 0)
// 全局声明变量
extern EventGroupHandle_t websocket_event_group;
/**
* @brief websocket 初始化
*
* @param text_cb 文本数据回调函数
* @param bin_cb 二进制数据回调函数
*/
void dri_websocket_init(websocket_text_callback_t text_cb, websocket_binary_callback_t bin_cb);
// 代码省略 ...
#endif /* __DRI_WEBSOCKET_H__ */dri_weksocket.c
// 代码省略 ...
// 定义函数指针,文本处理回调函数
websocket_text_callback_t g_text_cb = NULL;
// 定义函数指针,二进制处理回调函数
websocket_binary_callback_t g_bin_cb = NULL;
// 静态函数;websocket事件处理函数
static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
// 代码省略 ...
switch (event_id)
{
// 代码省略 ...
case WEBSOCKET_EVENT_DATA:
ESP_LOGI(TAG, "WEBSOCKET 收到数据");
ESP_LOGI(TAG, "Received opcode=%d", data->op_code);
// 根据opcode判断数据类型,并调用相应的回调函数
if (data->op_code == 0x1)
{
// 调用处理文本的回调函数
g_text_cb((char *)data->data_ptr, data->data_len);
}
else if (data->op_code == 0x2)
{
// 调用处理二进制的回调函数
g_bin_cb((uint8_t *)data->data_ptr, data->data_len);
}
else if (data->op_code == 0x08 && data->data_len == 2)
{
ESP_LOGW(TAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]);
}
else
{
ESP_LOGW(TAG, "Received=%.*s\n\n", data->data_len, (char *)data->data_ptr);
}
break;
// 代码省略 ...
}
}
/**
* @brief websocket 初始化
*
* @param text_cb 文本数据回调函数
* @param bin_cb 二进制数据回调函数
*/
void dri_websocket_init(websocket_text_callback_t text_cb, websocket_binary_callback_t bin_cb)
{
// 代码省略 ...
// 6. 保存回调函数
g_text_cb = text_cb;
g_bin_cb = bin_cb;
}
// 代码省略 ...mian.c
#include <stdio.h>
#include <string.h>
#include "esp_task.h"
#include "cJSON.h"
#include "com_debug.h"
#include "audio_processor.h"
#include "dri_wifi.h"
#include "int_button.h"
#include "dri_http.h"
#include "dri_websocket.h"
// 定义音频处理器对象
audio_processor_t *audio_processor = NULL;
// 定义语音活动检测的回调函数
void app_main_vad_cb(vad_state_t vad_state)
{
if (vad_state == VAD_SPEECH)
{
//MY_LOGI("检测到声音!");
}
else
{
//MY_LOGI("检测到静音!");
}
}
// 定义检测到唤醒词次回调函数
void app_main_wakeup_cb(void)
{
MY_LOGI("AI小智被唤醒!");
}
// 重定义wifi联网成功后的弱函数
void dri_wifi_connected_cb(void)
{
MY_LOGI("WIFI联网成功!....");
}
// 重定义按键SW2长按事件触发后的弱函数
void int_button_sw2_long_press(void)
{
MY_LOGI("按键SW2长按事件触发!....");
// 重新配网
dri_wifi_reset_provisioning();
}
// 处理websocket服务器回复的文本数据的回调函数
void app_main_websocket_text_cb(char *data, size_t len)
{
// 1. 解析服务器回复的JSON数据
cJSON *root = cJSON_Parse(data);
if (root == NULL) {
MY_LOGE("JSON解析失败");
return;
}
// 2. 从JSON数据中提取type字段
cJSON *type = cJSON_GetObjectItem(root, "type");
if (!cJSON_IsString(type)) {
MY_LOGE("提取类型失败!");
return;
}
MY_LOGI("type字段值: %s", type->valuestring);
// 根据type字段的值进行不同的处理
// hello 类型的回复
if (strcmp(type->valuestring, "hello") == 0) {
MY_LOGI("收到Hello消息!");
// MY_LOGI("data: \n %s \n", data);
// 解析json数据
cJSON *root = cJSON_Parse(data);
// 从 root 中取出字段 session_id
cJSON *session_id_item = cJSON_GetObjectItem(root, "session_id");
// 将 session_id 字段的值复制到全局变量 session_id 中
strcpy(session_id, session_id_item->valuestring);
// 将事件标志组中表示收到session_id的位置位
xEventGroupSetBits(websocket_event_group, WEBSOCKET_EVENT_SESSION_ID_BIT);
// 释放JSON对象占用的内存
cJSON_Delete(root);
}
// SST类型的回复
else if (strcmp(type->valuestring, "tts") == 0) {
MY_LOGI("收到TTS消息(文本消息)!");
}
// STT类型的回复
else if (strcmp(type->valuestring, "stt") == 0) {
MY_LOGI("收到STT(语音识别)!");
}
// LLM类型的回复
else if (strcmp(type->valuestring, "llm") == 0) {
MY_LOGI("收到LLM消息(表情包)!");
}
// 释放JSON对象占用的内存
free(root);
}
// 处理websocket服务器回复的二进制数据的回调函数
void app_main_websocket_bin_cb(uint8_t *data, size_t len)
{
// 将opus数据写入音频处理器播放
audio_processor_write_opus_data(audio_processor, data, len);
}
void app_main(void)
{
MY_LOGI("Websocket测试:");
// 创建音频处理器对象
audio_processor = audio_processor_create();
// 注册回调函数
audio_processor_register_sr_vad_callback(audio_processor, app_main_vad_cb);
audio_processor_register_sr_wakeup_callback(audio_processor, app_main_wakeup_cb);
// 启动音频处理器
audio_processor_start(audio_processor);
// 初始化WIFI
dri_wifi_init();
// 初始化按键SW2
int_button_sw2_init();
// 发送HTTP请求获取激活码
dri_http_request_active_code();
// 初始化websocket
dri_websocket_init(app_main_websocket_text_cb, app_main_websocket_bin_cb);
// 启动websocket连接
dri_websocket_start();
while (1)
{
vTaskDelay(10);
}
}3.3 发送hello消息
3.3.1 帧格式
发送hello消息 建立连接 并获取session_id

3.3.2 步骤
1. 封装发送hello消息的函数
2. 在main.c中文本处理函数总解析hello消息的回复,会保存session_id
3. 将发送hello消息的函数变为阻塞函数,只有获取到session_id后才能停止调用3.3.3 代码
dri_websocket.h
// 代码省略 ...
//定义session_id 用来本地使用 但是要在外部的回调函数接收
extern char session_id[9];
// 宏定义 Websocket事件标志组,表示接收到session_id的位
#define WEBSOCKET_EVENT_SESSION_ID_BIT (1 << 1)
// 代码省略 ...
/**
* @brief 向websocket服务器发送hello消息
*
*/
void dri_websocket_send_hello(void);
// 代码省略 ...dri_websocket.c
// 代码省略 ...
// 定义字符数组,存放session_id, 长度是9个字节,包含结束符
char session_id[9];
// 代码省略 ...
// websocket发送hello函数封装(建立通道)
/**
* @brief 向websocket服务器发送hello消息, 该函数为阻塞函数,从响应内容中获取到session_id后才能结束
*
*/
void dri_websocket_send_hello(void)
{
/*
{
"type": "hello",
"version": 1,
"transport": "websocket",
"features": {
"mcp": true
},
"audio_params": {
"format": "opus",
"sample_rate": 16000,
"channels": 1,
"frame_duration": 60
}
}
*/
if (client != NULL && esp_websocket_client_is_connected(client))
{
// 创建要发送的JSON数据
// 创建json跟对象
cJSON *root = cJSON_CreateObject();
// 添加字段
cJSON_AddStringToObject(root, "type", "hello");
cJSON_AddNumberToObject(root, "version", 1);
cJSON_AddStringToObject(root, "transport", "websocket");
// // 创建features对象
// cJSON *features = cJSON_CreateObject();
// cJSON_AddBoolToObject(features, "mcp", true);
// cJSON_AddItemToObject(root, "features", features);
// 创建audio_params
cJSON *audio_params = cJSON_CreateObject();
cJSON_AddStringToObject(audio_params, "format", "opus");
cJSON_AddNumberToObject(audio_params, "sample_rate", 16000);
cJSON_AddNumberToObject(audio_params, "channels", 1);
cJSON_AddNumberToObject(audio_params, "frame_duration", 60);
cJSON_AddItemToObject(root, "audio_params", audio_params);
// 将JSON对象转换为字符串
char *json_str = cJSON_PrintUnformatted(root);
// websocket发送文本数据
esp_websocket_client_send_text(client, json_str, strlen(json_str), portMAX_DELAY);
MY_LOGI("发送Hello消息完成!");
// 释放资源
cJSON_Delete(root);
free(json_str);
// 等待标志位被置位
xEventGroupWaitBits(websocket_event_group, WEBSOCKET_EVENT_SESSION_ID_BIT, pdTRUE, pdFALSE, portMAX_DELAY);
MY_LOGI("获取到 session_id: %s", session_id);
}
else
{
MY_LOGE("Websocket未连接, 无法发送消息");
}
}main.c
main中回调函数如果是hello 则得到session和通知hello任务完成
// 代码省略 ...
// 处理websocket服务器回复的文本数据的回调函数
void app_main_websocket_text_cb(char *data, size_t len)
{
// 代码省略 ...
// 根据type字段的值进行不同的处理
// hello 类型的回复
if (strcmp(type->valuestring, "hello") == 0) {
MY_LOGI("收到Hello消息!");
// MY_LOGI("data: \n %s \n", data);
// 解析json数据
cJSON *root = cJSON_Parse(data);
// 从 root 中取出字段 session_id
cJSON *session_id_item = cJSON_GetObjectItem(root, "session_id");
// 将 session_id 字段的值复制到全局变量 session_id 中
strcpy(session_id, session_id_item->valuestring);
// 将事件标志组中表示收到session_id的位置位
xEventGroupSetBits(websocket_event_group, WEBSOCKET_EVENT_SESSION_ID_BIT);
// 释放JSON对象占用的内存
cJSON_Delete(root);
}
// 代码省略 ...
}
// 代码省略 ...3.3.4 测试
main.c
// 代码省略 ...
void app_main(void)
{
// 代码省略 ...
// 发送hello消息
dri_websocket_send_hello();
// 关闭连接
dri_websocket_stop();
while (1)
{
vTaskDelay(10);
}
}3.4 发送唤醒词

dri_websocket.h
/**
* @brief 发送唤醒词
*
*/
void dri_websocket_send_wakeup_word(void);dri_websocket.c
/**
* @brief 发送唤醒词
* 我们口头唤醒的时候,只是唤醒了sr组件,但是我们在说唤醒词的时候,sr组件并没有把我们的音频发送出去
* 我们需要再口头唤醒的时候,使用ws发送一个唤醒词的请求给服务器 同步唤醒
*/
void dri_websocket_send_wakeup_word(void)
{
if (client != NULL && esp_websocket_client_is_connected(client))
{
// 1. 整理要发送给小智后台的json字符串
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "session_id", session_id);
cJSON_AddStringToObject(root, "type", "listen");
cJSON_AddStringToObject(root, "state", "detect");
cJSON_AddStringToObject(root, "text", "你好小智");
char *json_str = cJSON_PrintUnformatted(root);
// 2. 发送唤醒词请求
esp_websocket_client_send_text(client, json_str, strlen(json_str), portMAX_DELAY);
// 3. 释放内存
cJSON_Delete(root);
free(json_str);
}
}3.5 发送开启监听请求

dri_websocket.h
/**
* @brief 发送开始监听请求
*/
void dri_websocket_send_start_listen(void);dri_websocket.c
/**
* @brief 发送开始监听请求
*/
void dri_websocket_send_start_listen(void)
{
if (client != NULL && esp_websocket_client_is_connected(client))
{
/**
* 使用cJSON 参考下边json数据生成json字符串
* {
"session_id": "<会话ID>",
"type": "listen",
"state": "start",
"mode": "<监听模式>"
}
*/
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "session_id", session_id);
cJSON_AddStringToObject(root, "type", "listen");
cJSON_AddStringToObject(root, "state", "start");
cJSON_AddStringToObject(root, "mode", "auto");
char *json_str = cJSON_PrintUnformatted(root);
// 发送文本数据
esp_websocket_client_send_text(client, json_str, strlen(json_str), portMAX_DELAY);
// 释放
cJSON_Delete(root);
free(json_str);
}
}3.6 发送结束监听请求

dri_websocket.h
/**
* @brief 发送结束监听请求
*/
void dri_websocket_send_stop_listen(void);dri_websocket.c
/**
* @brief 发送结束监听请求
*/
void dri_websocket_send_stop_listen(void)
{
if (client != NULL && esp_websocket_client_is_connected(client))
{
/**
* 使用cJSON 参考下边json数据生成json字符串
* {
"session_id": "<会话ID>",
"type": "listen",
"state": "stop"
}
*/
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "session_id", session_id);
cJSON_AddStringToObject(root, "type", "listen");
cJSON_AddStringToObject(root, "state", "stop");
char *json_str = cJSON_PrintUnformatted(root);
// 发送文本数据
esp_websocket_client_send_text(client, json_str, strlen(json_str), portMAX_DELAY);
// 释放
cJSON_Delete(root);
free(json_str);
}
}3.7 发送中断请求

dri_websocket.h
/**
* @brief 发送中断请求
*/
void dri_websocket_send_abort(void);dri_websocket.c
/**
* @brief 发送中断请求
*/
void dri_websocket_send_abort(void)
{
if (client != NULL && esp_websocket_client_is_connected(client))
{
/**
* 使用cJSON 参考下边json数据生成json字符串
* {
"session_id": "<会话ID>",
"type": "abort",
"reason": "wake_word_detected" // 可选
}
*/
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "session_id", session_id);
cJSON_AddStringToObject(root, "type", "abort");
cJSON_AddStringToObject(root, "reason", "wake_word_detected");
char *json_str = cJSON_PrintUnformatted(root);
// 发送文本数据
esp_websocket_client_send_text(client, json_str, strlen(json_str), portMAX_DELAY);
// 释放
cJSON_Delete(root);
free(json_str);
}
}3.8 发送opus数据
dri_websocket.h
/**
* @brief 发送opus数据
*/
void dri_websocket_send_opus(char *data, int len);dri_websocket.c
/**
* @brief 发送opus数据
*/
void dri_websocket_send_opus(char *data, int len)
{
if (client != NULL && esp_websocket_client_is_connected(client))
{
esp_websocket_client_send_bin(client, data, len, portMAX_DELAY);
}
}4. 状态机
5.1 什么是状态机
嵌入式开发中的状态机就是将设备的各种工作模式(如待机、运行、发送、接收)定义成明确的状态,然后根据事件(中断、定时、数据)在这些状态间按规则切换,确保系统行为确定、实时且不阻塞。
5.2 需求分析
1. 六种状态
START 起始状态
IDLE 空闲状态,已完成初始化
CONNETOIN 正在连接Websocket服务器
CONNECTED 已连接Websocket服务器
LISTENING 正在监听
SPEAKING 正在发声
2. 完成websocket初始化,进入IDLE状态
3. 完成websocket_start, 进入CONNECTED状态
4. 发送完开始监听的消息,进入LISTENING状态
5. 接收到语音回复,进入SPEADERING状态
6. websockt WEBSOCKET_EVENT_FINISH 事件触发,恢复到IDLE状态5.3 状态机模块封装
com_status.h
#ifndef __COM_STATUS_H__
#define __COM_STATUS_H__
typedef enum
{
START, // 默认状态,未做任何配置
IDLE, // WS已经初始化
CONNECTION, // WS正在连接中
CONNECTED, // WS连接成功
LISTENING, // WS连接成功,正在监听
SPEADERING // WS连接成功,正在说话
} com_status_t;
extern dev_status_t dev_status;
/**
* @brief 切换状态
*/
void com_status_switch_status(dev_status_t status);
#endif /* __COM_STATUS_H__ */com_status.c
#include "com_status.h"
#include "com_debug.h"
dev_status_t dev_status = IDLE;
char *com_status_str[] = {"START", "IDLE", "CONNECTION", "CONNECTED", "LISTENING","SPEADERING"};
/**
* @brief 切换状态
*/
void com_status_switch_status(dev_status_t status)
{
if (dev_status != status)
{
dev_status = status;
// 输出新状态和老状态
MY_LOGE("status change from %s to %s", dev_status_str[dev_status], dev_status_str[status]);
}
}5.4 状态机的使用
在app_main中配置前后设置状态
com_status_change(START);
//....
com_status_change(IDLE);唤醒回调函数中的状态机配置:
static void app_main_wakenet_cb(void *arg)
{
MY_LOGI("xiaozhi is wakeup");
// 只有WS初始化 没有连接的时候,需要先连接 才能发送唤醒
if (dev_status == IDLE)
{
// 让状态进入正在连接中状态
Com_Status_Set(CONNECTION);
Bsp_WS_Start();
Bsp_WS_Send_Hello();
}
else if (dev_status == SPEADERING)
{
// 如果当前唤醒的时候,小智正在讲话,我们首先要先发送一个中断请求,然后再次唤醒
Bsp_WS_Send_Abort();
}
// 唤醒的时候 说的唤醒词并没有发送给下一步处理, 所以我们需要封装一个发给给小智的唤醒词函数
// 发送唤醒词
Bsp_WS_Send_Wakenet();
// 如果唤醒成功,则状态为连接成功状态(这个状态就是小智被唤醒后的日常状态)
Com_Status_Set(CONNECTED);
}一旦开始回复的状态设置
else if (strcmp(type_str, "tts") == 0)
{
MY_LOGI("收到tts消息");
// 回复的语音的文字版本
// 判断state是start 则开始播放声音,那我们就进入SPEADERING状态 如果state是stop 说明播放完成 则进入日常状态CONNECTED
cJSON *state = cJSON_GetObjectItem(root, "state");
char *state_str = cJSON_GetStringValue(state);
if (strcmp(state_str, "start") == 0)
{
Com_Status_Set(SPEADERING);
}
else if (strcmp(state_str, "stop") == 0)
{
Com_Status_Set(CONNECTED);
}
}检测到是否说话,决定的状态
static void app_main_vad_cb(vad_state_t vad_state, void *arg)
{
if (vad_state == VAD_SILENCE)
{
MY_LOGI("检测到静音.....\r\n");
// 检测到静音其实只和 正在监听状态LISTENING有关,此时需要停止监听
// 如果我是CONNECTED和SPEADERING状态,和静音是没有任何关系的
if (dev_status == LISTENING)
{
Bsp_WS_Send_Stop();
Com_Status_Set(CONNECTED);
}
}
else
{
MY_LOGI("检测到声音.....\r\n");
// 如果当前是SPEADERING状态,检测到声音 只要不是唤醒词都无所谓
// 如果当前是LISTENING状态,检测到声音,正常不需要处理 本来就是正在接收声音
// 如果是空闲状态,检测到声音,则进入正在监听状态LISTENING
if (dev_status == CONNECTED)
{
Com_Status_Set(LISTENING);
Bsp_WS_Send_Listen();
}
}
}在websoket断开的状态中配置状态
case WEBSOCKET_EVENT_FINISH:
// 说明我们和小智的连接已经断开了,下次想要再次使用小智,我们就要重新唤醒
// 我们需要做什么事情呢?????
// 1. 把SR组件中的是否已经唤醒的标志 设置为0,SR的出口就关闭了,我们的话就不会向编码器写入了
audio_process_close_wakeup(audio_process);
// 2. 把状态机的状态 设置为IDLE,下次唤醒的时候 就可以重新连接
Com_Status_Set(IDLE);
break;
}在sr中从mic读取数据写入缓冲区的时候,防止自聊天
// 如果当前为唤醒状态 并且检测到人声 那么需要把音频数据保存到缓冲区
if (audio_sr->is_wakeed == true && vad_state == VAD_SPEECH && dev_status == LISTENING)
{
int16_t *data = result->raw_data;
xRingbufferSend(audio_sr->ringBuffer, (void *)data, feed_size, portMAX_DELAY);
}