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

相关推荐
Coovally AI模型快速验证1 小时前
IJCV 2026|让重复视频片段拥有“唯一”字幕,判别性提示 CDP,检索性能提升 15%
人工智能·计算机视觉·实时音视频
贺子杰1 小时前
潜意识“假推理”:LLM 幻觉的可解释性追踪方案
人工智能·深度学习
zzzzzz3101 小时前
别再用 playwright-stealth 了!CloakBrowser 源码级反检测才是正解
人工智能
小撒的私房菜1 小时前
Day 4:让 Agent 记住你——短期记忆实现
人工智能·后端
古希腊掌管代码的神THU1 小时前
【清华代码熊】MTP (Multi-Token Prediction)源码详解
人工智能·深度学习·自然语言处理
极客老王说Agent1 小时前
实在Agent委外加工智能化管控方案与落地案例:从数字劳动力到离散制造全链路闭环
人工智能·ai·制造
Elastic 中国社区官方博客1 小时前
jina-embeddings-v5-omni:用于文本、图像、音频和视频的 embeddings
大数据·人工智能·elasticsearch·搜索引擎·ai·音视频·jina
郑寿昌1 小时前
AI时代动画游戏新职业方向
人工智能·游戏
一次旅行1 小时前
今日AI 新闻简报
人工智能·ai编程·ai写作