基于 ESP32-S3 的四博 AI 双目智能音箱方案设计:双目屏、四路触控、姿态感应、震动反馈与 AI 大模型接入

基于 ESP32-S3 的四博 AI 双目智能音箱方案设计:双目屏、四路触控、姿态感应、震动反馈与 AI 大模型接入

摘要

本文介绍一套基于 四博 AI-S3 / 双目 AI EYE 硬件体系的 AI 智能音箱方案。该方案以 ESP32-S3 为核心主控,结合双目光屏、四路触控感应、震动马达、三轴姿态传感器、麦克风、喇叭和 AI 大模型服务,实现具备语音交互、表情反馈、姿态识别、触控控制、语音克隆、自建知识库和 MCP 设备控制能力的智能音箱产品。

相比传统蓝牙音箱,本方案更偏向"AI 交互终端 + 桌面陪伴设备 + 智能控制中枢"。设备可通过四博小助手小程序完成配网、绑定智能体、语音克隆和知识库配置,也可以通过自建后端接入小智、豆包、ChatGPT 或私有大模型。


一、方案目标

本方案目标是设计一款技术化、可扩展、可量产的 四博 AI 双目智能音箱,核心能力包括:

  1. 支持 AI 大模型语音对话;

  2. 支持 0.71 寸 / 1.28 寸双目屏显示;

  3. 支持四路触控交互;

  4. 支持震动马达触觉反馈;

  5. 支持三轴姿态感应;

  6. 支持小程序配网和智能体绑定;

  7. 支持语音克隆、声纹识别、自建知识库;

  8. 支持 MCP 工具调用,实现语音控制设备功能;

  9. 支持 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 网关设计

后端主要负责:

  1. 设备 WebSocket 接入;

  2. ASR 语音识别;

  3. RAG 知识库检索;

  4. LLM 大模型问答;

  5. TTS 语音合成;

  6. Voice Clone 声音克隆;

  7. 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 陪伴型智能音箱方案

相关推荐
IT_陈寒2 小时前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
新新技术迷2 小时前
Node给AI接口做SSE代理与鉴权
人工智能
redreamSo3 小时前
大模型是不是到顶了?瓶颈到底在哪
人工智能·openai
Oo9203 小时前
Tool Use 背后的技术逻辑
人工智能
姗姗来迟了3 小时前
Vue3封装AI流式对话组件踩坑实录
人工智能
码上天下4 小时前
用Pinia管理AI多会话状态
人工智能
用户054324329704 小时前
Next.js接大模型流式SSE实操踩坑
人工智能
Assby4 小时前
从 Function Calling 到 MCP:理解 Agent 工具调用的底层通信机制
人工智能·后端
小星AI5 小时前
Claude Code 从入门到精通,一步到位
人工智能
后端小肥肠5 小时前
Codex + Obsidian 做人生副本视频:输入主题文案,直通剪映草稿
人工智能·aigc·agent