基于 ESP32-S3 的四博 AI 双目智能音箱方案设计:双目屏、四路触控、姿态感应、震动反馈与 AI 大模型接入
摘要
本文介绍一套基于 四博 AI-S3 / 双目 AI EYE 硬件体系的 AI 智能音箱方案。该方案以 ESP32-S3 为核心主控,结合双目光屏、四路触控感应、震动马达、三轴姿态传感器、麦克风、喇叭和 AI 大模型服务,实现具备语音交互、表情反馈、姿态识别、触控控制、语音克隆、自建知识库和 MCP 设备控制能力的智能音箱产品。
相比传统蓝牙音箱,本方案更偏向"AI 交互终端 + 桌面陪伴设备 + 智能控制中枢"。设备可通过四博小助手小程序完成配网、绑定智能体、语音克隆和知识库配置,也可以通过自建后端接入小智、豆包、ChatGPT 或私有大模型。
一、方案目标
本方案目标是设计一款技术化、可扩展、可量产的 四博 AI 双目智能音箱,核心能力包括:
-
支持 AI 大模型语音对话;
-
支持 0.71 寸 / 1.28 寸双目屏显示;
-
支持四路触控交互;
-
支持震动马达触觉反馈;
-
支持三轴姿态感应;
-
支持小程序配网和智能体绑定;
-
支持语音克隆、声纹识别、自建知识库;
-
支持 MCP 工具调用,实现语音控制设备功能;
-
支持 OTA、日志、产测和批量生产。
二、系统总体架构
整体架构可以分为四层:
┌──────────────────────────────────────────────┐
│ 四博 AI 双目智能音箱 │
├──────────────────────────────────────────────┤
│ 交互层 │
│ ├─ 双目光屏:0.71 寸 / 1.28 寸 │
│ ├─ 四路触控:唤醒、打断、音量、模式切换 │
│ ├─ 三轴姿态:摇一摇、倾斜、拿起、放下 │
│ └─ 震动马达:触控确认、状态提醒、闹钟反馈 │
├──────────────────────────────────────────────┤
│ 硬件层 │
│ ├─ ESP32-S3 主控 │
│ ├─ VB6824 / ES8311 音频方案 │
│ ├─ Mic + Speaker │
│ ├─ 双屏 SPI LCD │
│ ├─ Touch Sensor │
│ └─ IMU + Motor + Battery │
├──────────────────────────────────────────────┤
│ 固件层 │
│ ├─ audio_task:音频采集与播放 │
│ ├─ ai_task:WebSocket AI 通信 │
│ ├─ eye_task:双目表情渲染 │
│ ├─ touch_task:四路触控扫描 │
│ ├─ imu_task:姿态识别 │
│ ├─ haptic_task:震动马达控制 │
│ └─ ota_task:远程升级 │
├──────────────────────────────────────────────┤
│ 云端 / 小程序层 │
│ ├─ 四博小助手小程序 │
│ ├─ 语音克隆服务 │
│ ├─ 自建知识库 RAG │
│ ├─ LLM 网关 │
│ └─ MCP 工具调用 │
└──────────────────────────────────────────────┘
三、硬件选型
1. 主控芯片
主控建议选择 ESP32-S3 系列模组,例如:
ESPS3-32 N16R8
ESPS3-32 N16R2
ESPS3-32E N16R8
选择 ESP32-S3 的主要原因:
-
双核 Xtensa 240MHz;
-
GPIO 资源较多;
-
支持 Wi-Fi + BLE;
-
适合音视频和 AIoT 应用;
-
可选 PSRAM,适合屏幕动画、音频缓存和复杂任务调度;
-
四博已有 AI-S3、双目 AI EYE、多模态 AI 相关硬件生态。
2. 外设配置
| 模块 | 建议配置 | 说明 |
|---|---|---|
| 主控 | ESP32-S3 N16R8 | 建议带 PSRAM |
| 屏幕 | 0.71 寸 / 1.28 寸双目屏 | 用于表情、状态、动画 |
| 触控 | 4 路电容触控 | 顶部、左右、背部交互 |
| 姿态 | 三轴 IMU | 摇一摇、倾斜、拿起检测 |
| 震动 | 扁平马达 + MOS 管 | 触觉反馈 |
| 音频 | Mic + Speaker | AI 对话、蓝牙音箱 |
| 语音 | VB6824 / ES8311 | 唤醒、音频编解码 |
| 电源 | 锂电池 + 充电管理 | 便携式使用 |
| 配网 | BluFi / SoftAP | 小程序配网 |
| 扩展 | RGB、TF 卡、4G | 选配 |
四、固件工程目录设计
sibo_ai_speaker/
├── CMakeLists.txt
├── sdkconfig.defaults
├── main/
│ ├── app_main.c
│ ├── app_config.h
│ ├── board_pins.h
│ ├── app_event.h
│ ├── touch_mgr.c
│ ├── haptic_mgr.c
│ ├── imu_mgr.c
│ ├── eye_ui.c
│ ├── audio_i2s.c
│ ├── ai_ws_client.c
│ ├── blufi_prov.c
│ ├── mcp_uart.c
│ ├── ota_mgr.c
│ └── power_mgr.c
├── components/
│ ├── display_driver/
│ ├── audio_codec/
│ ├── qmi8658/
│ └── eye_assets/
└── server/
├── main.py
├── rag_service.py
├── voice_clone.py
└── mcp_tools.py
五、基础配置文件
1. app_config.h
#pragma once
#define PRODUCT_NAME "SIBO_AI_DUAL_EYE_SPEAKER"
#define FIRMWARE_VERSION "v1.0.0"
/*
* 屏幕类型:
* 0.71 寸屏幕一般为 160x160
* 1.28 寸屏幕一般为 240x240
*/
#define SCREEN_TYPE_071 0
#define SCREEN_TYPE_128 1
#define CONFIG_EYE_SCREEN_TYPE SCREEN_TYPE_128
#if CONFIG_EYE_SCREEN_TYPE == SCREEN_TYPE_128
#define EYE_LCD_W 240
#define EYE_LCD_H 240
#else
#define EYE_LCD_W 160
#define EYE_LCD_H 160
#endif
#define AUDIO_SAMPLE_RATE 16000
#define AUDIO_BITS_PER_SAMPLE 16
#define AUDIO_FRAME_MS 20
#define AUDIO_FRAME_SAMPLES (AUDIO_SAMPLE_RATE * AUDIO_FRAME_MS / 1000)
#define AI_WS_URL "wss://your-ai-gateway.example.com/device/ws"
#define OTA_URL "https://your-ota.example.com/sibo_ai_speaker/manifest.json"
#define ENABLE_BLUFI_PROVISION 1
#define ENABLE_VOICE_CLONE 1
#define ENABLE_LOCAL_WAKEUP 1
#define ENABLE_MCP_TOOLS 1
#define ENABLE_RAG_KNOWLEDGE 1
2. board_pins.h
实际 GPIO 需要根据最终原理图调整,下面是工程模板。
#pragma once
#include "driver/gpio.h"
#define PIN_I2C_SCL GPIO_NUM_9
#define PIN_I2C_SDA GPIO_NUM_8
#define PIN_MOTOR_PWM GPIO_NUM_21
#define PIN_I2S_BCLK GPIO_NUM_4
#define PIN_I2S_WS GPIO_NUM_5
#define PIN_I2S_DIN_MIC GPIO_NUM_6
#define PIN_I2S_DOUT_SPK GPIO_NUM_7
#define PIN_LCD_SPI_SCLK GPIO_NUM_12
#define PIN_LCD_SPI_MOSI GPIO_NUM_13
#define PIN_LCD_LEFT_CS GPIO_NUM_14
#define PIN_LCD_RIGHT_CS GPIO_NUM_15
#define PIN_LCD_DC GPIO_NUM_16
#define PIN_LCD_RST GPIO_NUM_17
#define PIN_LCD_BL GPIO_NUM_18
#define PIN_BAT_ADC GPIO_NUM_1
六、事件总线设计
为了让触控、姿态、AI 状态、屏幕动画和震动反馈统一调度,可以设计一个全局事件队列。
#pragma once
#include <stdint.h>
typedef enum {
EVT_TOUCH_1,
EVT_TOUCH_2,
EVT_TOUCH_3,
EVT_TOUCH_4,
EVT_TOUCH_LONG_1,
EVT_TOUCH_LONG_2,
EVT_TOUCH_LONG_3,
EVT_TOUCH_LONG_4,
EVT_IMU_SHAKE,
EVT_IMU_TILT_LEFT,
EVT_IMU_TILT_RIGHT,
EVT_IMU_PICKUP,
EVT_AI_IDLE,
EVT_AI_LISTENING,
EVT_AI_THINKING,
EVT_AI_SPEAKING,
EVT_AI_INTERRUPTED,
EVT_BAT_LOW,
EVT_OTA_START,
EVT_OTA_DONE,
} app_evt_type_t;
typedef struct {
app_evt_type_t type;
int32_t value;
int64_t ts_ms;
} app_evt_t;
七、主程序入口
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "app_event.h"
extern void touch_mgr_start(QueueHandle_t q);
extern void haptic_mgr_start(QueueHandle_t q);
extern void imu_mgr_start(QueueHandle_t q);
extern void eye_ui_start(QueueHandle_t q);
extern void audio_i2s_start(QueueHandle_t q);
extern void ai_ws_client_start(QueueHandle_t q);
extern void blufi_prov_start(void);
extern void ota_mgr_start(void);
static const char *TAG = "SIBO_AI";
QueueHandle_t g_app_evt_q;
void app_main(void)
{
ESP_LOGI(TAG, "boot product: SIBO_AI_DUAL_EYE_SPEAKER");
g_app_evt_q = xQueueCreate(32, sizeof(app_evt_t));
#if ENABLE_BLUFI_PROVISION
blufi_prov_start();
#endif
touch_mgr_start(g_app_evt_q);
haptic_mgr_start(g_app_evt_q);
imu_mgr_start(g_app_evt_q);
eye_ui_start(g_app_evt_q);
audio_i2s_start(g_app_evt_q);
ai_ws_client_start(g_app_evt_q);
ota_mgr_start();
app_evt_t evt;
while (1) {
if (xQueueReceive(g_app_evt_q, &evt, portMAX_DELAY) == pdTRUE) {
ESP_LOGI(TAG, "event type=%d value=%ld", evt.type, evt.value);
switch (evt.type) {
case EVT_TOUCH_1:
// 单击触控 1:进入监听
break;
case EVT_TOUCH_LONG_1:
// 长按触控 1:打断 AI 播报
break;
case EVT_TOUCH_2:
// 音量加
break;
case EVT_TOUCH_3:
// 音量减
break;
case EVT_TOUCH_4:
// 切换表情
break;
case EVT_IMU_SHAKE:
// 摇一摇触发随机互动
break;
default:
break;
}
}
}
}
八、四路触控扫描
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/touch_pad.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "app_event.h"
static const char *TAG = "touch_mgr";
static QueueHandle_t s_evt_q;
static const touch_pad_t s_touch[4] = {
TOUCH_PAD_NUM1,
TOUCH_PAD_NUM2,
TOUCH_PAD_NUM3,
TOUCH_PAD_NUM4,
};
static uint16_t s_base[4];
static uint16_t s_th[4];
static int64_t now_ms(void)
{
return esp_timer_get_time() / 1000;
}
static void push_evt(app_evt_type_t type, int value)
{
app_evt_t evt = {
.type = type,
.value = value,
.ts_ms = now_ms()
};
xQueueSend(s_evt_q, &evt, 0);
}
static void touch_calibrate(void)
{
vTaskDelay(pdMS_TO_TICKS(500));
for (int i = 0; i < 4; i++) {
uint32_t sum = 0;
for (int n = 0; n < 32; n++) {
uint16_t v = 0;
touch_pad_read_filtered(s_touch[i], &v);
sum += v;
vTaskDelay(pdMS_TO_TICKS(5));
}
s_base[i] = sum / 32;
s_th[i] = (uint16_t)(s_base[i] * 0.72f);
ESP_LOGI(TAG, "touch[%d] base=%u th=%u", i, s_base[i], s_th[i]);
}
}
static void touch_task(void *arg)
{
uint8_t last_down[4] = {0};
int64_t press_ts[4] = {0};
while (1) {
for (int i = 0; i < 4; i++) {
uint16_t v = 0;
touch_pad_read_filtered(s_touch[i], &v);
bool down = v < s_th[i];
if (down && !last_down[i]) {
press_ts[i] = now_ms();
last_down[i] = 1;
}
if (!down && last_down[i]) {
int64_t dur = now_ms() - press_ts[i];
last_down[i] = 0;
if (dur > 800) {
push_evt(EVT_TOUCH_LONG_1 + i, dur);
} else {
push_evt(EVT_TOUCH_1 + i, dur);
}
}
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
void touch_mgr_start(QueueHandle_t q)
{
s_evt_q = q;
touch_pad_init();
touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER);
for (int i = 0; i < 4; i++) {
touch_pad_config(s_touch[i], 0);
}
touch_pad_filter_start(10);
touch_calibrate();
xTaskCreate(touch_task, "touch_task", 4096, NULL, 5, NULL);
}
九、震动马达驱动
震动马达可以通过 MOS 管控制,ESP32-S3 使用 LEDC 输出 PWM,实现不同强度的震动反馈。
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "board_pins.h"
#include "app_event.h"
#define MOTOR_LEDC_TIMER LEDC_TIMER_0
#define MOTOR_LEDC_MODE LEDC_LOW_SPEED_MODE
#define MOTOR_LEDC_CHANNEL LEDC_CHANNEL_0
#define MOTOR_PWM_FREQ 20000
#define MOTOR_PWM_RES LEDC_TIMER_10_BIT
static QueueHandle_t s_evt_q;
static void motor_set(uint32_t duty)
{
ledc_set_duty(MOTOR_LEDC_MODE, MOTOR_LEDC_CHANNEL, duty);
ledc_update_duty(MOTOR_LEDC_MODE, MOTOR_LEDC_CHANNEL);
}
static void vibrate(uint32_t duty, uint32_t ms)
{
motor_set(duty);
vTaskDelay(pdMS_TO_TICKS(ms));
motor_set(0);
}
static void haptic_task(void *arg)
{
app_evt_t evt;
while (1) {
if (xQueueReceive(s_evt_q, &evt, portMAX_DELAY) == pdTRUE) {
switch (evt.type) {
case EVT_TOUCH_1:
case EVT_TOUCH_2:
case EVT_TOUCH_3:
case EVT_TOUCH_4:
vibrate(450, 35);
break;
case EVT_AI_LISTENING:
vibrate(700, 60);
break;
case EVT_AI_INTERRUPTED:
vibrate(900, 30);
vTaskDelay(pdMS_TO_TICKS(40));
vibrate(900, 30);
break;
case EVT_BAT_LOW:
for (int i = 0; i < 3; i++) {
vibrate(650, 80);
vTaskDelay(pdMS_TO_TICKS(120));
}
break;
default:
break;
}
}
}
}
void haptic_mgr_start(QueueHandle_t q)
{
s_evt_q = q;
ledc_timer_config_t timer = {
.speed_mode = MOTOR_LEDC_MODE,
.timer_num = MOTOR_LEDC_TIMER,
.duty_resolution = MOTOR_PWM_RES,
.freq_hz = MOTOR_PWM_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer);
ledc_channel_config_t ch = {
.speed_mode = MOTOR_LEDC_MODE,
.channel = MOTOR_LEDC_CHANNEL,
.timer_sel = MOTOR_LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = PIN_MOTOR_PWM,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&ch);
xTaskCreate(haptic_task, "haptic_task", 4096, NULL, 6, NULL);
}
十、三轴姿态识别
三轴传感器主要用于增强设备的"活物感",例如:
-
摇一摇重新回答;
-
倾斜切换表情;
-
拿起设备主动问候;
-
放下设备进入低功耗;
-
配合双目屏做眼球跟随。
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "app_event.h"static const char *TAG = "imu_mgr";
static QueueHandle_t s_evt_q;
typedef struct {
float ax;
float ay;
float az;
} accel_g_t;static int64_t get_ms(void)
{
return esp_timer_get_time() / 1000;
}static void push_evt(app_evt_type_t type, int value)
{
app_evt_t evt = {
.type = type,
.value = value,
.ts_ms = get_ms()
};xQueueSend(s_evt_q, &evt, 0);}
static esp_err_t imu_read_accel(accel_g_t out)
{
/
* 实际项目中替换为具体 IMU 驱动:
* qmi8658_read_accel(out);
* mpu6050_read_accel(out);
*/static float t = 0; t += 0.1f; out->ax = sinf(t) * 0.05f; out->ay = cosf(t) * 0.05f; out->az = 1.0f; return ESP_OK;}
static void imu_task(void *arg)
{
accel_g_t a;
float last_mag = 1.0f;
int64_t last_shake_ms = 0;while (1) { if (imu_read_accel(&a) == ESP_OK) { float mag = sqrtf(a.ax * a.ax + a.ay * a.ay + a.az * a.az); float diff = fabsf(mag - last_mag); last_mag = mag; int64_t now = get_ms(); if (diff > 0.45f && now - last_shake_ms > 800) { last_shake_ms = now; push_evt(EVT_IMU_SHAKE, (int)(diff * 1000)); } if (a.ax > 0.45f) { push_evt(EVT_IMU_TILT_RIGHT, (int)(a.ax * 100)); } else if (a.ax < -0.45f) { push_evt(EVT_IMU_TILT_LEFT, (int)(a.ax * 100)); } if (a.az < 0.65f || mag > 1.35f) { push_evt(EVT_IMU_PICKUP, (int)(mag * 100)); } } vTaskDelay(pdMS_TO_TICKS(40)); }}
void imu_mgr_start(QueueHandle_t q)
{
s_evt_q = q;ESP_LOGI(TAG, "imu task start"); xTaskCreate(imu_task, "imu_task", 4096, NULL, 5, NULL);}
十一、双目表情状态机
双目屏不建议只显示静态眼睛,应该和 AI 状态绑定。例如:
| AI 状态 | 双目表情 |
|---|---|
| 空闲 | 正常眨眼 |
| 监听 | 眼睛放大 |
| 思考 | 眼球转动 |
| 说话 | 嘴型 / 眼神节奏变化 |
| 打断 | 眨眼 + 震动 |
| 低电量 | 困倦表情 |
| 摇一摇 | 开心表情 |
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "app_event.h"
#include "app_config.h"
typedef enum {
EYE_MOOD_IDLE,
EYE_MOOD_LISTENING,
EYE_MOOD_THINKING,
EYE_MOOD_SPEAKING,
EYE_MOOD_HAPPY,
EYE_MOOD_SLEEPY,
EYE_MOOD_ANGRY,
EYE_MOOD_LOW_BAT,
} eye_mood_t;
static QueueHandle_t s_evt_q;
static eye_mood_t s_mood = EYE_MOOD_IDLE;
static void eye_draw_frame(eye_mood_t mood, int blink, int offset_x, int offset_y)
{
/*
* 实际项目中接入 esp_lcd_panel_draw_bitmap()
*
* left_eye_draw(mood, blink, offset_x, offset_y);
* right_eye_draw(mood, blink, offset_x, offset_y);
*/
}
static void eye_task(void *arg)
{
app_evt_t evt;
int blink = 0;
int offset_x = 0;
int offset_y = 0;
while (1) {
while (xQueueReceive(s_evt_q, &evt, 0) == pdTRUE) {
switch (evt.type) {
case EVT_AI_IDLE:
s_mood = EYE_MOOD_IDLE;
break;
case EVT_AI_LISTENING:
s_mood = EYE_MOOD_LISTENING;
break;
case EVT_AI_THINKING:
s_mood = EYE_MOOD_THINKING;
break;
case EVT_AI_SPEAKING:
s_mood = EYE_MOOD_SPEAKING;
break;
case EVT_TOUCH_4:
s_mood = EYE_MOOD_HAPPY;
break;
case EVT_IMU_TILT_LEFT:
offset_x = -12;
break;
case EVT_IMU_TILT_RIGHT:
offset_x = 12;
break;
case EVT_BAT_LOW:
s_mood = EYE_MOOD_LOW_BAT;
break;
default:
break;
}
}
blink = (blink + 1) % 120;
eye_draw_frame(s_mood, blink == 0, offset_x, offset_y);
offset_x = offset_x * 8 / 10;
offset_y = offset_y * 8 / 10;
vTaskDelay(pdMS_TO_TICKS(33));
}
}
void eye_ui_start(QueueHandle_t q)
{
s_evt_q = q;
/*
* display_init_left_right(CONFIG_EYE_SCREEN_TYPE);
*/
xTaskCreate(eye_task, "eye_task", 8192, NULL, 4, NULL);
}
十二、AI WebSocket 通信
设备端通过 WebSocket 与 AI 网关通信。推荐协议如下:
{
"type": "hello",
"product": "SIBO_AI_DUAL_EYE_SPEAKER",
"fw": "v1.0.0"
}
AI 网关返回状态:
{
"type": "thinking",
"text": "正在思考"
}
设备根据返回状态切换眼睛表情和震动反馈。
#include "esp_websocket_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include "app_config.h"
#include "app_event.h"
static const char *TAG = "ai_ws";
static QueueHandle_t s_evt_q;
static esp_websocket_client_handle_t s_client;
static void push_evt(app_evt_type_t type)
{
app_evt_t evt = {
.type = type,
.value = 0,
.ts_ms = esp_timer_get_time() / 1000
};
xQueueSend(s_evt_q, &evt, 0);
}
static void handle_json_msg(const char *data, int len)
{
cJSON *root = cJSON_ParseWithLength(data, len);
if (!root) {
return;
}
const cJSON *type = cJSON_GetObjectItem(root, "type");
if (cJSON_IsString(type)) {
if (!strcmp(type->valuestring, "listening")) {
push_evt(EVT_AI_LISTENING);
} else if (!strcmp(type->valuestring, "thinking")) {
push_evt(EVT_AI_THINKING);
} else if (!strcmp(type->valuestring, "speaking")) {
push_evt(EVT_AI_SPEAKING);
} else if (!strcmp(type->valuestring, "idle")) {
push_evt(EVT_AI_IDLE);
}
}
cJSON_Delete(root);
}
static void ws_event_handler(void *handler_args,
esp_event_base_t base,
int32_t event_id,
void *event_data)
{
esp_websocket_event_data_t *d = (esp_websocket_event_data_t *)event_data;
switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
ESP_LOGI(TAG, "websocket connected");
esp_websocket_client_send_text(
s_client,
"{\"type\":\"hello\",\"product\":\"SIBO_AI_DUAL_EYE_SPEAKER\"}",
strlen("{\"type\":\"hello\",\"product\":\"SIBO_AI_DUAL_EYE_SPEAKER\"}"),
portMAX_DELAY
);
break;
case WEBSOCKET_EVENT_DATA:
if (d->op_code == 0x1) {
handle_json_msg(d->data_ptr, d->data_len);
} else if (d->op_code == 0x2) {
/*
* 二进制数据一般为 TTS 音频流
* audio_play_write(d->data_ptr, d->data_len);
*/
}
break;
case WEBSOCKET_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "websocket disconnected");
push_evt(EVT_AI_IDLE);
break;
default:
break;
}
}
void ai_send_pcm_frame(const uint8_t *pcm, size_t len)
{
if (s_client && esp_websocket_client_is_connected(s_client)) {
esp_websocket_client_send_bin(s_client, (const char *)pcm, len, 0);
}
}
void ai_ws_client_start(QueueHandle_t q)
{
s_evt_q = q;
esp_websocket_client_config_t cfg = {
.uri = AI_WS_URL,
.reconnect_timeout_ms = 3000,
.network_timeout_ms = 5000,
};
s_client = esp_websocket_client_init(&cfg);
esp_websocket_register_events(
s_client,
WEBSOCKET_EVENT_ANY,
ws_event_handler,
NULL
);
esp_websocket_client_start(s_client);
}
十三、I2S 音频采集
#include "driver/i2s_std.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "board_pins.h"
#include "app_config.h"
extern void ai_send_pcm_frame(const uint8_t *pcm, size_t len);
static i2s_chan_handle_t s_rx_chan;
static i2s_chan_handle_t s_tx_chan;
static void audio_capture_task(void *arg)
{
int16_t pcm[AUDIO_FRAME_SAMPLES];
while (1) {
size_t bytes_read = 0;
esp_err_t err = i2s_channel_read(
s_rx_chan,
pcm,
sizeof(pcm),
&bytes_read,
pdMS_TO_TICKS(100)
);
if (err == ESP_OK && bytes_read > 0) {
/*
* 这里可以加入:
* 1. VAD
* 2. AEC
* 3. NS
* 4. AGC
*/
ai_send_pcm_frame((const uint8_t *)pcm, bytes_read);
}
}
}
void audio_i2s_start(QueueHandle_t q)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(
I2S_NUM_0,
I2S_ROLE_MASTER
);
i2s_new_channel(&chan_cfg, &s_tx_chan, &s_rx_chan);
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(AUDIO_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_MONO
),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = PIN_I2S_BCLK,
.ws = PIN_I2S_WS,
.dout = PIN_I2S_DOUT_SPK,
.din = PIN_I2S_DIN_MIC,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
i2s_channel_init_std_mode(s_rx_chan, &std_cfg);
i2s_channel_init_std_mode(s_tx_chan, &std_cfg);
i2s_channel_enable(s_rx_chan);
i2s_channel_enable(s_tx_chan);
xTaskCreate(audio_capture_task, "audio_capture", 8192, NULL, 7, NULL);
}
十四、MCP 工具控制
MCP 的作用是把用户自然语言转换成设备可执行动作。例如:
用户说:把眼睛切换成开心表情
设备执行:set_eye_happy
用户说:音量调到 70
设备执行:set_volume 70
用户说:明天早上 8 点叫我
设备执行:set_alarm 08:00
下面是一个 UART MCP 控制模板。
#include "driver/uart.h"
#include "esp_log.h"
#include <string.h>
#define MCP_UART_NUM UART_NUM_1
#define MCP_UART_BAUD 115200
#define MCP_RX_BUF 512
static const char *TAG = "mcp_uart";
static void mcp_send_at(const char *cmd)
{
uart_write_bytes(MCP_UART_NUM, cmd, strlen(cmd));
uart_write_bytes(MCP_UART_NUM, "\r\n", 2);
ESP_LOGI(TAG, "AT>> %s", cmd);
}
void mcp_register_tools(void)
{
mcp_send_at("AT");
mcp_send_at("AT+CONNECT");
/*
* 设置屏幕主题
*/
mcp_send_at("AT+ADDMCP=0,set_screen_theme,设置屏幕为暗主题,3,10,FA,FF");
/*
* 设置 RGB 灯颜色
*/
mcp_send_at("AT+ADDMCP=1,set_lamp_color,设置灯光颜色,F1,3,R,G,B");
/*
* 切换开心表情
*/
mcp_send_at("AT+ADDMCP=0,set_eye_happy,切换为开心表情,2,20,01");
/*
* 设置闹钟
*/
mcp_send_at("AT+ADDMCP=1,set_alarm,设置闹钟,F2,2,H,M");
/*
* 设置音量
*/
mcp_send_at("AT+ADDMCP=1,set_volume,设置音量,F3,1,V");
}
static void handle_mcp_frame(const uint8_t *buf, int len)
{
if (len < 6) {
return;
}
if (buf[0] != 0x55 || buf[1] != 0xAA) {
return;
}
uint8_t cmd = buf[3];
switch (cmd) {
case 0xF1: {
uint8_t r = buf[4];
uint8_t g = buf[5];
uint8_t b = buf[6];
ESP_LOGI(TAG, "set rgb: %02X %02X %02X", r, g, b);
/*
* rgb_led_set(r, g, b);
*/
break;
}
case 0xF2: {
uint8_t h = buf[4];
uint8_t m = buf[5];
ESP_LOGI(TAG, "set alarm: %02d:%02d", h, m);
/*
* alarm_set(h, m);
*/
break;
}
case 0xF3: {
uint8_t volume = buf[4];
ESP_LOGI(TAG, "set volume: %d", volume);
/*
* audio_set_volume(volume);
*/
break;
}
case 0xFC:
/*
* 收到恢复帧后,重启 AI 模组并重新注册 MCP 工具
*/
mcp_register_tools();
break;
default:
break;
}
}
void mcp_uart_start(void)
{
uart_config_t cfg = {
.baud_rate = MCP_UART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
uart_driver_install(MCP_UART_NUM, MCP_RX_BUF, 0, 0, NULL, 0);
uart_param_config(MCP_UART_NUM, &cfg);
mcp_register_tools();
}
十五、后端 AI 网关设计
后端主要负责:
-
设备 WebSocket 接入;
-
ASR 语音识别;
-
RAG 知识库检索;
-
LLM 大模型问答;
-
TTS 语音合成;
-
Voice Clone 声音克隆;
-
MCP 工具调用。
FastAPI 示例
from fastapi import FastAPI, UploadFile, File, WebSocket
from pydantic import BaseModel
from rag_service import kb_ingest, kb_search
from voice_clone import create_voice_profile, synthesize_with_voice
app = FastAPI(title="SIBO AI Speaker Gateway")
class AgentCreateReq(BaseModel):
name: str
model: str = "xiaozhi"
enable_voice_clone: bool = True
enable_kb: bool = True
enable_mcp: bool = True
class ChatReq(BaseModel):
device_id: str
agent_id: str
text: str
@app.post("/api/agent/create")
def create_agent(req: AgentCreateReq):
return {
"agent_id": "agent_sibo_001",
"name": req.name,
"model": req.model,
"features": {
"voice_clone": req.enable_voice_clone,
"kb": req.enable_kb,
"mcp": req.enable_mcp,
}
}
@app.post("/api/kb/upload")
async def upload_kb(agent_id: str, file: UploadFile = File(...)):
content = await file.read()
doc_count = kb_ingest(agent_id, file.filename, content)
return {
"ok": True,
"agent_id": agent_id,
"chunks": doc_count
}
@app.post("/api/voice/clone")
async def voice_clone(agent_id: str, file: UploadFile = File(...)):
wav = await file.read()
voice_id = create_voice_profile(agent_id, wav)
return {
"ok": True,
"voice_id": voice_id
}
@app.websocket("/device/ws")
async def device_ws(ws: WebSocket):
await ws.accept()
await ws.send_json({
"type": "idle",
"msg": "connected"
})
while True:
msg = await ws.receive()
if "bytes" in msg and msg["bytes"]:
pcm = msg["bytes"]
text = asr_stream_decode(pcm)
if text:
await ws.send_json({
"type": "thinking",
"text": text
})
answer = call_llm_with_rag(text)
await ws.send_json({
"type": "speaking",
"text": answer
})
async for audio_chunk in tts_stream(answer):
await ws.send_bytes(audio_chunk)
await ws.send_json({
"type": "idle"
})
elif "text" in msg and msg["text"]:
print("device json:", msg["text"])
def call_llm_with_rag(text: str) -> str:
return "这里是 AI 大模型结合知识库后的回答。"
def asr_stream_decode(pcm: bytes) -> str | None:
return None
async def tts_stream(text: str):
yield b""
十六、知识库 RAG 示例
import hashlib
from typing import List
_MEMORY_DB = {}
def split_text(text: str, max_len: int = 500) -> List[str]:
chunks = []
buf = ""
for line in text.splitlines():
if len(buf) + len(line) > max_len:
chunks.append(buf)
buf = ""
buf += line + "\n"
if buf:
chunks.append(buf)
return chunks
def embed(text: str) -> list[float]:
"""
示例 embedding。
正式项目建议替换为:
1. bge-small
2. text-embedding-3-small
3. 私有 embedding 服务
"""
h = hashlib.md5(text.encode("utf-8")).digest()
return [x / 255.0 for x in h]
def kb_ingest(agent_id: str, filename: str, content: bytes) -> int:
text = content.decode("utf-8", errors="ignore")
chunks = split_text(text)
_MEMORY_DB.setdefault(agent_id, [])
for idx, chunk in enumerate(chunks):
_MEMORY_DB[agent_id].append({
"filename": filename,
"chunk_id": idx,
"text": chunk,
"vector": embed(chunk)
})
return len(chunks)
def kb_search(agent_id: str, query: str, top_k: int = 4) -> str:
items = _MEMORY_DB.get(agent_id, [])
if not items:
return ""
qv = embed(query)
def score(v):
return sum(a * b for a, b in zip(qv, v))
ranked = sorted(
items,
key=lambda x: score(x["vector"]),
reverse=True
)[:top_k]
return "\n\n".join(
f"[{x['filename']}#{x['chunk_id']}]\n{x['text']}"
for x in ranked
)
十七、语音克隆接口示例
import uuid
_VOICE_PROFILES = {}
def create_voice_profile(agent_id: str, wav_bytes: bytes) -> str:
voice_id = f"voice_{uuid.uuid4().hex[:12]}"
/*
* 实际项目中应包含:
* 1. 音频降噪
* 2. VAD 切分
* 3. 说话人 embedding
* 4. voice token 生成
* 5. 与 agent_id 绑定
*/
_VOICE_PROFILES[agent_id] = {
"voice_id": voice_id,
"sample_size": len(wav_bytes)
}
return voice_id
def synthesize_with_voice(agent_id: str, text: str) -> str:
profile = _VOICE_PROFILES.get(agent_id)
if profile:
voice_id = profile["voice_id"]
else:
voice_id = "default"
return f"https://your-cdn.example.com/tts/{voice_id}.mp3"
注意:上面 Python 代码中的多行注释如需直接运行,应改为
#注释或三引号字符串。
十八、小程序配网逻辑
四博小助手可用于设备配网和智能体绑定。典型流程如下:
1. 用户打开四博小助手小程序
2. 创建 AI 智能体
3. 选择设备配置
4. 使用 BluFi 搜索附近设备
5. 选择 Wi-Fi 并输入密码
6. 小程序将 Wi-Fi 信息、agent_id 写入设备
7. 设备联网后连接 AI 网关
8. 进入语音交互状态
前端伪代码:
const API = "https://your-api.example.com";
Page({
data: {
deviceId: "",
ssid: "",
password: "",
agentId: "",
},
async createAgent() {
const res = await wx.request({
url: `${API}/api/agent/create`,
method: "POST",
data: {
name: "四博AI双目音箱",
model: "xiaozhi",
enable_voice_clone: true,
enable_kb: true,
enable_mcp: true
}
});
this.setData({
agentId: res.data.agent_id
});
},
startBlufiBind() {
wx.openBluetoothAdapter({
success: () => {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
services: [],
success: () => {
console.log("start scan blufi device");
}
});
}
});
},
sendWifiToDevice(deviceId, ssid, password) {
const payload = {
op: "blufi_wifi_config",
ssid,
password,
agent_id: this.data.agentId
};
console.log("send blufi payload:", deviceId, payload);
}
});
十九、推荐交互逻辑
触控 1:
单击:进入监听
长按:打断 AI 说话
触控 2:
单击:音量 +
长按:切换蓝牙音箱模式
触控 3:
单击:音量 -
长按:进入配网 / 解绑设备
触控 4:
单击:切换表情
长按:切换智能体 / 知识库角色
姿态:
摇一摇:重新回答 / 随机互动
左倾:上一个表情 / 上一首
右倾:下一个表情 / 下一首
拿起:主动问候
长时间静置:进入休眠
震动:
短震:触控确认
双短震:打断成功
长震:配网成功
三短震:低电量提醒
二十、量产建议
1. 固件分区
nvs Wi-Fi、SN、配置参数
otadata OTA 状态
phy_init RF 参数
factory 出厂固件
ota_0 OTA 固件 A
ota_1 OTA 固件 B
assets 表情素材、音效资源
2. 产测项目
FACTORY_TEST_TOUCH 四路触控自动校准
FACTORY_TEST_LCD 双屏红绿蓝白黑测试
FACTORY_TEST_AUDIO_IN Mic 录音电平检测
FACTORY_TEST_AUDIO_OUT 喇叭播放 1kHz
FACTORY_TEST_MOTOR 马达震动 3 次
FACTORY_TEST_IMU X/Y/Z 三轴静态值检测
FACTORY_TEST_WIFI Wi-Fi RSSI 检测
FACTORY_TEST_BATTERY 电池电压 / 充电状态检测
FACTORY_WRITE_SN 写入 SN、MAC、证书
3. OTA 升级流程
1. 设备启动后读取当前版本号
2. 请求 OTA manifest.json
3. 对比云端版本号
4. 下载新固件
5. 校验 SHA256
6. 写入 ota_0 / ota_1
7. 重启切换分区
8. 上报升级结果
二十一、总结
本方案基于 ESP32-S3 构建了一套完整的四博 AI 双目智能音箱架构。硬件侧集成双目光屏、四路触控、震动马达、三轴姿态传感器、麦克风和喇叭;固件侧通过 FreeRTOS 多任务实现音频采集、AI 通信、屏幕渲染、触控识别、姿态检测和震动反馈;云端侧通过 WebSocket、ASR、LLM、TTS、RAG 和 Voice Clone 实现自然语言交互。
这款产品本质上不只是一个蓝牙音箱,而是一个具有"表情、触觉、姿态和语音人格"的 AI 交互终端。它可以应用在儿童早教、桌面陪伴、智能玩具、品牌 IP 周边、智能家居控制和 B 端定制硬件等多个场景。
从产品角度看,四博 AI 双目智能音箱的核心价值在于:
AI 大模型能力
+ 双目情绪表达
+ 四路触控交互
+ 三轴姿态感应
+ 震动触觉反馈
+ 小程序配置
+ 语音克隆
+ 自建知识库
+ MCP 工具调用
= 可量产的 AI 陪伴型智能音箱方案