基于 ESP32-S3 的四博AI双目智能音箱方案:双目同显/异显、素材上传、触摸、G-sensor、舵机、Wi-Fi/4G/TWS音频扩展
1. 项目背景
传统AI智能音箱主要围绕语音交互展开,典型链路是"唤醒词 → 语音采集 → 云端AI → TTS播放"。这种交互方式能完成问答、控制和内容播放,但缺少视觉反馈和动作反馈,用户感知比较单一。
四博AI双目智能音箱方案在传统AI音箱基础上增加了两块眼睛屏幕、四路触摸传感器、G-sensor姿态检测、震动马达、舵机动作和素材上传能力,使设备从"会说话的音箱"升级成"有表情、有动作、有感知、有反馈的AI陪伴终端"。
该方案基于 ESP32-S3 平台,适合客户进行二次开发,可接入客户自己的后端、自己的小程序、自己的AI服务和自己的素材系统。四博资料中 ESPS3-32 / ESPS3-32E 属于 ESP32-S3 系列,面向音视频/AI市场;AI开发宝典中也已有 RoPet_ESPS3_AI_EYE 双目工程和相关硬件说明,可作为该方案的开发基础。
2. 方案功能定义
本方案建议实现以下能力:
1. 基于 ESP32-S3 / ESPS3-32E 主控,方便客户二次开发
2. 支持客户接入自己的后端、自己的小程序、自己的AI服务
3. 支持双目同显和双目异显
4. 支持客户上传图片、动图、MJPEG视频、QOI素材
5. 支持四路触摸传感器
6. 支持 G-sensor 姿态传感器
7. 支持震动马达反馈
8. 支持舵机驱动头部、尾部、耳朵等结构
9. 联网支持 Wi-Fi、4G、BLE配网
10. 支持连接 TWS 耳机和蓝牙音箱
11. 支持AI对话、TTS播放、表情联动和动作联动
12. 支持OTA升级和素材远程更新
需要说明的是:ESP32-S3 主要支持 Wi-Fi 和 BLE,不适合直接承担传统 Bluetooth Classic A2DP/HFP 音频连接。若产品要求稳定连接 TWS 耳机或蓝牙音箱,建议增加外置蓝牙音频SoC,由 ESP32-S3 通过 UART/I2S 控制外置蓝牙音频模块。
3. 系统总体架构
+------------------------------------------------------+
| 四博AI双目智能音箱系统 |
+------------------------------------------------------+
| 主控层 |
| └── ESPS3-32 / ESPS3-32E / ESP32-S3 |
| |
| AI通信层 |
| ├── Wi-Fi:连接客户后端 / 小智AI / Coze / 私有LLM |
| ├── 4G:通过4G模组进行移动联网 |
| └── BLE:配网、绑定、小程序近场控制 |
| |
| 显示层 |
| ├── 左眼屏幕 |
| ├── 右眼屏幕 |
| ├── 双目同显 |
| ├── 双目异显 |
| └── 图片 / 动图 / MJPEG / QOI素材播放 |
| |
| 交互层 |
| ├── 四路触摸传感器 |
| ├── G-sensor姿态传感器 |
| ├── 震动马达 |
| └── 舵机:头部 / 尾部 / 耳朵 |
| |
| 音频层 |
| ├── MIC语音采集 |
| ├── 本地喇叭播放 |
| ├── TTS语音播放 |
| └── 外置BT Audio SoC:TWS耳机 / 蓝牙音箱 |
+------------------------------------------------------+
4. 推荐工程目录
sibo_ai_dual_eye_speaker/
├── CMakeLists.txt
├── sdkconfig.defaults
├── partitions.csv
└── main/
├── app_main.c
├── app_config.h
├── board_pins.h
├── system_state.c
├── system_state.h
├── eye_display.c
├── eye_display.h
├── asset_manager.c
├── asset_manager.h
├── touch_manager.c
├── touch_manager.h
├── gsensor_manager.c
├── gsensor_manager.h
├── motor_manager.c
├── motor_manager.h
├── servo_manager.c
├── servo_manager.h
├── network_manager.c
├── network_manager.h
├── audio_route.c
├── audio_route.h
├── ai_client.c
├── ai_client.h
├── app_protocol.c
└── app_protocol.h
5. 分区表设计
双目方案需要存放表情图、动画帧、短视频素材、提示音和客户自定义资源,因此建议使用 OTA + FATFS / LittleFS 分区。
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x400000,
ota_1, app, ota_1, , 0x400000,
assets, data, fat, , 0x600000,
config, data, nvs, , 0x10000,
素材目录建议:
/assets/
├── eyes/
│ ├── idle_left.qoi
│ ├── idle_right.qoi
│ ├── happy_left.qoi
│ ├── happy_right.qoi
│ ├── blink_left.qoi
│ ├── blink_right.qoi
│ ├── thinking_left.qoi
│ ├── thinking_right.qoi
│ ├── speaking_left.qoi
│ └── speaking_right.qoi
├── video/
│ ├── boot.mjpeg
│ ├── thinking.mjpeg
│ └── speaking.mjpeg
├── audio/
│ ├── wake.wav
│ ├── hello.wav
│ └── error.wav
└── manifest.json
6. board_pins.h 引脚定义
实际引脚需按客户PCB调整,下面只作为工程模板:
#pragma once
/* 双目屏幕 SPI 总线 */
#define PIN_LCD_SPI_MOSI 11
#define PIN_LCD_SPI_CLK 12
#define PIN_LCD_LEFT_CS 13
#define PIN_LCD_RIGHT_CS 14
#define PIN_LCD_DC 15
#define PIN_LCD_RST 16
#define PIN_LCD_BL 17
/* I2S音频 */
#define PIN_I2S_BCLK 4
#define PIN_I2S_WS 5
#define PIN_I2S_DOUT 6
#define PIN_I2S_DIN 7
#define PIN_AMP_EN 8
/* 四路触摸 */
#define PIN_TOUCH_0 1
#define PIN_TOUCH_1 2
#define PIN_TOUCH_2 3
#define PIN_TOUCH_3 9
/* G-sensor I2C */
#define PIN_I2C_SDA 41
#define PIN_I2C_SCL 42
/* 马达和舵机 */
#define PIN_MOTOR 10
#define PIN_SERVO_HEAD 18
#define PIN_SERVO_TAIL 19
/* 外置蓝牙音频模块 */
#define PIN_BT_AUDIO_TX 43
#define PIN_BT_AUDIO_RX 44
#define PIN_BT_AUDIO_STATE 45
7. 双目显示状态机
双目显示建议抽象成"显示模式 + 表情状态 + 左右眼素材路径"。
#pragma once
#include <stdbool.h>
typedef enum {
EYE_MODE_SYNC = 0, // 双目同显
EYE_MODE_ASYNC, // 双目异显
EYE_MODE_VIDEO, // 双目视频播放
EYE_MODE_CUSTOM // 客户自定义素材
} eye_mode_t;
typedef enum {
EYE_EXPR_IDLE = 0,
EYE_EXPR_WAKEUP,
EYE_EXPR_HAPPY,
EYE_EXPR_BLINK,
EYE_EXPR_THINKING,
EYE_EXPR_SPEAKING,
EYE_EXPR_SLEEP,
EYE_EXPR_ERROR,
EYE_EXPR_CUSTOM
} eye_expr_t;
typedef struct {
eye_mode_t mode;
eye_expr_t left_expr;
eye_expr_t right_expr;
char left_asset[128];
char right_asset[128];
int fps;
bool loop;
} eye_display_ctx_t;
void eye_display_init(void);
void eye_display_show_sync(eye_expr_t expr);
void eye_display_show_async(eye_expr_t left, eye_expr_t right);
void eye_display_play_assets(const char *left_path, const char *right_path, int fps, bool loop);
void eye_display_stop(void);
实现示例:
#include <stdio.h>
#include <string.h>
#include "eye_display.h"
static eye_display_ctx_t s_eye = {
.mode = EYE_MODE_SYNC,
.left_expr = EYE_EXPR_IDLE,
.right_expr = EYE_EXPR_IDLE,
.fps = 15,
.loop = true,
};
static const char *expr_to_asset(eye_expr_t expr, bool left)
{
switch (expr) {
case EYE_EXPR_WAKEUP:
return left ? "/assets/eyes/wakeup_left.qoi" : "/assets/eyes/wakeup_right.qoi";
case EYE_EXPR_HAPPY:
return left ? "/assets/eyes/happy_left.qoi" : "/assets/eyes/happy_right.qoi";
case EYE_EXPR_BLINK:
return left ? "/assets/eyes/blink_left.qoi" : "/assets/eyes/blink_right.qoi";
case EYE_EXPR_THINKING:
return left ? "/assets/eyes/thinking_left.qoi" : "/assets/eyes/thinking_right.qoi";
case EYE_EXPR_SPEAKING:
return left ? "/assets/eyes/speaking_left.qoi" : "/assets/eyes/speaking_right.qoi";
case EYE_EXPR_SLEEP:
return left ? "/assets/eyes/sleep_left.qoi" : "/assets/eyes/sleep_right.qoi";
case EYE_EXPR_ERROR:
return left ? "/assets/eyes/error_left.qoi" : "/assets/eyes/error_right.qoi";
case EYE_EXPR_IDLE:
default:
return left ? "/assets/eyes/idle_left.qoi" : "/assets/eyes/idle_right.qoi";
}
}
void eye_display_init(void)
{
printf("[eye] init left/right lcd\n");
/*
* 实际项目:
* lcd_bus_init();
* lcd_left_panel_init();
* lcd_right_panel_init();
* lvgl_port_init();
*/
}
void eye_display_show_sync(eye_expr_t expr)
{
s_eye.mode = EYE_MODE_SYNC;
s_eye.left_expr = expr;
s_eye.right_expr = expr;
const char *left = expr_to_asset(expr, true);
const char *right = expr_to_asset(expr, false);
printf("[eye] sync expr=%d L=%s R=%s\n", expr, left, right);
/*
* image_decode_and_draw_left(left);
* image_decode_and_draw_right(right);
*/
}
void eye_display_show_async(eye_expr_t left, eye_expr_t right)
{
s_eye.mode = EYE_MODE_ASYNC;
s_eye.left_expr = left;
s_eye.right_expr = right;
const char *left_asset = expr_to_asset(left, true);
const char *right_asset = expr_to_asset(right, false);
printf("[eye] async L=%s R=%s\n", left_asset, right_asset);
/*
* image_decode_and_draw_left(left_asset);
* image_decode_and_draw_right(right_asset);
*/
}
void eye_display_play_assets(const char *left_path, const char *right_path, int fps, bool loop)
{
if (!left_path || !right_path) {
return;
}
s_eye.mode = EYE_MODE_CUSTOM;
s_eye.fps = fps;
s_eye.loop = loop;
strncpy(s_eye.left_asset, left_path, sizeof(s_eye.left_asset) - 1);
strncpy(s_eye.right_asset, right_path, sizeof(s_eye.right_asset) - 1);
printf("[eye] play custom L=%s R=%s fps=%d loop=%d\n",
s_eye.left_asset,
s_eye.right_asset,
s_eye.fps,
s_eye.loop);
/*
* anim_player_start_left(s_eye.left_asset, fps, loop);
* anim_player_start_right(s_eye.right_asset, fps, loop);
*/
}
void eye_display_stop(void)
{
printf("[eye] stop display animation\n");
/*
* anim_player_stop_left();
* anim_player_stop_right();
*/
}
8. 客户素材上传与远程更新
客户可以通过自己的小程序上传素材到后端,后端再下发素材更新指令给设备。设备负责下载、校验、保存和刷新显示。
协议示例
{
"cmd": "asset_update",
"data": {
"asset_id": "happy_v2",
"asset_type": "eye_image",
"left_url": "https://cdn.example.com/assets/happy_left.qoi",
"right_url": "https://cdn.example.com/assets/happy_right.qoi",
"left_path": "/assets/eyes/happy_left.qoi",
"right_path": "/assets/eyes/happy_right.qoi",
"md5": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"fps": 15,
"auto_apply": true
}
}
设备端代码
#include <stdio.h>
#include <string.h>
#include "cJSON.h"
#include "eye_display.h"
static int http_download_file(const char *url, const char *path)
{
printf("[http] download %s -> %s\n", url, path);
/*
* esp_http_client_config_t config = {
* .url = url,
* .timeout_ms = 15000,
* };
* esp_http_client_handle_t client = esp_http_client_init(&config);
* ...
*/
return 0;
}
static bool asset_verify_md5(const char *path, const char *md5)
{
printf("[asset] verify path=%s md5=%s\n", path, md5);
return true;
}
void asset_manager_handle_update(const char *json)
{
cJSON *root = cJSON_Parse(json);
if (!root) {
printf("[asset] parse failed\n");
return;
}
cJSON *data = cJSON_GetObjectItem(root, "data");
if (!data) {
cJSON_Delete(root);
return;
}
const char *left_url = cJSON_GetObjectItem(data, "left_url")->valuestring;
const char *right_url = cJSON_GetObjectItem(data, "right_url")->valuestring;
const char *left_path = cJSON_GetObjectItem(data, "left_path")->valuestring;
const char *right_path = cJSON_GetObjectItem(data, "right_path")->valuestring;
const char *md5 = cJSON_GetObjectItem(data, "md5")->valuestring;
int fps = 15;
cJSON *fps_item = cJSON_GetObjectItem(data, "fps");
if (fps_item) {
fps = fps_item->valueint;
}
bool auto_apply = false;
cJSON *apply_item = cJSON_GetObjectItem(data, "auto_apply");
if (apply_item) {
auto_apply = cJSON_IsTrue(apply_item);
}
if (http_download_file(left_url, left_path) != 0) {
printf("[asset] left download failed\n");
cJSON_Delete(root);
return;
}
if (http_download_file(right_url, right_path) != 0) {
printf("[asset] right download failed\n");
cJSON_Delete(root);
return;
}
if (!asset_verify_md5(left_path, md5)) {
printf("[asset] md5 verify failed\n");
cJSON_Delete(root);
return;
}
printf("[asset] update success\n");
if (auto_apply) {
eye_display_play_assets(left_path, right_path, fps, true);
}
cJSON_Delete(root);
}
9. 四路触摸交互
四路触摸可以分别映射为不同交互区:
TOUCH0:头部触摸,唤醒 / 开心反馈
TOUCH1:左侧触摸,左眼眨眼 / 上一个素材
TOUCH2:右侧触摸,右眼眨眼 / 下一个素材
TOUCH3:下巴触摸,进入AI对话
代码示例:
typedef enum {
TOUCH_EVT_HEAD = 0,
TOUCH_EVT_LEFT,
TOUCH_EVT_RIGHT,
TOUCH_EVT_CHIN
} touch_event_t;
void motor_vibrate_once(int ms);
void ai_client_start_listen(void);
void touch_manager_handle_event(touch_event_t evt)
{
switch (evt) {
case TOUCH_EVT_HEAD:
printf("[touch] head\n");
eye_display_show_sync(EYE_EXPR_HAPPY);
motor_vibrate_once(80);
break;
case TOUCH_EVT_LEFT:
printf("[touch] left\n");
eye_display_show_async(EYE_EXPR_BLINK, EYE_EXPR_IDLE);
break;
case TOUCH_EVT_RIGHT:
printf("[touch] right\n");
eye_display_show_async(EYE_EXPR_IDLE, EYE_EXPR_BLINK);
break;
case TOUCH_EVT_CHIN:
printf("[touch] chin, start AI chat\n");
eye_display_show_sync(EYE_EXPR_THINKING);
motor_vibrate_once(50);
ai_client_start_listen();
break;
default:
break;
}
}
10. G-sensor姿态联动
G-sensor可以识别摇晃、抬起、放下、左倾、右倾等动作,用于增强设备"生命感"。
typedef enum {
GSENSOR_EVT_NONE = 0,
GSENSOR_EVT_SHAKE,
GSENSOR_EVT_PICK_UP,
GSENSOR_EVT_PUT_DOWN,
GSENSOR_EVT_TILT_LEFT,
GSENSOR_EVT_TILT_RIGHT
} gsensor_event_t;
void servo_tail_wag(void);
void servo_head_nod(void);
void gsensor_handle_event(gsensor_event_t evt)
{
switch (evt) {
case GSENSOR_EVT_SHAKE:
printf("[gsensor] shake\n");
eye_display_show_sync(EYE_EXPR_HAPPY);
motor_vibrate_once(100);
servo_tail_wag();
break;
case GSENSOR_EVT_PICK_UP:
printf("[gsensor] pick up\n");
eye_display_show_sync(EYE_EXPR_WAKEUP);
servo_head_nod();
break;
case GSENSOR_EVT_PUT_DOWN:
printf("[gsensor] put down\n");
eye_display_show_sync(EYE_EXPR_SLEEP);
break;
case GSENSOR_EVT_TILT_LEFT:
printf("[gsensor] tilt left\n");
eye_display_show_async(EYE_EXPR_BLINK, EYE_EXPR_IDLE);
break;
case GSENSOR_EVT_TILT_RIGHT:
printf("[gsensor] tilt right\n");
eye_display_show_async(EYE_EXPR_IDLE, EYE_EXPR_BLINK);
break;
default:
break;
}
}
11. 马达与舵机驱动
11.1 震动马达
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "board_pins.h"
void motor_init(void)
{
gpio_config_t io = {
.pin_bit_mask = 1ULL << PIN_MOTOR,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io);
gpio_set_level(PIN_MOTOR, 0);
}
void motor_vibrate_once(int ms)
{
gpio_set_level(PIN_MOTOR, 1);
vTaskDelay(pdMS_TO_TICKS(ms));
gpio_set_level(PIN_MOTOR, 0);
}
void motor_vibrate_pattern(void)
{
motor_vibrate_once(60);
vTaskDelay(pdMS_TO_TICKS(80));
motor_vibrate_once(60);
}
11.2 舵机控制
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "board_pins.h"
#define SERVO_FREQ_HZ 50
#define SERVO_TIMER LEDC_TIMER_0
#define SERVO_MODE LEDC_LOW_SPEED_MODE
#define SERVO_HEAD_CHANNEL LEDC_CHANNEL_0
#define SERVO_TAIL_CHANNEL LEDC_CHANNEL_1
static void servo_channel_init(int gpio, ledc_channel_t channel)
{
ledc_channel_config_t ch = {
.gpio_num = gpio,
.speed_mode = SERVO_MODE,
.channel = channel,
.timer_sel = SERVO_TIMER,
.duty = 0,
.hpoint = 0,
};
ledc_channel_config(&ch);
}
void servo_init(void)
{
ledc_timer_config_t timer = {
.speed_mode = SERVO_MODE,
.timer_num = SERVO_TIMER,
.duty_resolution = LEDC_TIMER_13_BIT,
.freq_hz = SERVO_FREQ_HZ,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&timer);
servo_channel_init(PIN_SERVO_HEAD, SERVO_HEAD_CHANNEL);
servo_channel_init(PIN_SERVO_TAIL, SERVO_TAIL_CHANNEL);
}
static uint32_t servo_angle_to_duty(int angle)
{
if (angle < 0) {
angle = 0;
}
if (angle > 180) {
angle = 180;
}
int min_us = 500;
int max_us = 2500;
int pulse_us = min_us + (max_us - min_us) * angle / 180;
return (uint32_t)(pulse_us * 8192 / 20000);
}
static void servo_set_angle(ledc_channel_t channel, int angle)
{
uint32_t duty = servo_angle_to_duty(angle);
ledc_set_duty(SERVO_MODE, channel, duty);
ledc_update_duty(SERVO_MODE, channel);
}
void servo_head_nod(void)
{
servo_set_angle(SERVO_HEAD_CHANNEL, 80);
vTaskDelay(pdMS_TO_TICKS(200));
servo_set_angle(SERVO_HEAD_CHANNEL, 105);
vTaskDelay(pdMS_TO_TICKS(200));
servo_set_angle(SERVO_HEAD_CHANNEL, 90);
}
void servo_tail_wag(void)
{
servo_set_angle(SERVO_TAIL_CHANNEL, 60);
vTaskDelay(pdMS_TO_TICKS(180));
servo_set_angle(SERVO_TAIL_CHANNEL, 120);
vTaskDelay(pdMS_TO_TICKS(180));
servo_set_angle(SERVO_TAIL_CHANNEL, 90);
}
12. Wi-Fi / 4G / BLE联网策略
AI开发宝典中包含 SoftAP、BluFi 配网,以及 Wi-Fi 和 4G 模式切换相关内容,因此双目方案可以复用这些网络能力。实际产品中建议设计统一的 network_manager,由它负责 Wi-Fi、4G、BLE配网三种状态切换。
typedef enum {
NET_MODE_NONE = 0,
NET_MODE_WIFI,
NET_MODE_4G,
NET_MODE_BLE_CONFIG
} net_mode_t;
typedef struct {
net_mode_t mode;
bool wifi_connected;
bool modem_ready;
bool ble_active;
} network_ctx_t;
static network_ctx_t s_net = {
.mode = NET_MODE_NONE,
};
void network_switch_to_wifi(void)
{
printf("[net] switch to Wi-Fi\n");
s_net.mode = NET_MODE_WIFI;
/*
* esp_wifi_start();
* esp_wifi_connect();
*/
}
void network_switch_to_4g(void)
{
printf("[net] switch to 4G\n");
s_net.mode = NET_MODE_4G;
/*
* modem_power_on();
* esp_modem_start_ppp();
*/
}
void network_start_ble_config(void)
{
printf("[net] start BLE config\n");
s_net.mode = NET_MODE_BLE_CONFIG;
/*
* blufi_init();
* blufi_start_adv();
*/
}
void network_auto_connect(void)
{
if (wifi_config_exists()) {
network_switch_to_wifi();
} else if (modem_4g_available()) {
network_switch_to_4g();
} else {
network_start_ble_config();
}
}
13. TWS耳机和蓝牙音箱扩展
为了连接 TWS 耳机或蓝牙音箱,推荐架构如下:
ESP32-S3
├── I2S PCM音频输出 -> 外置BT Audio SoC
├── UART控制命令 -> 外置BT Audio SoC
├── GPIO状态输入 -> 连接状态 / 配对状态
└── 本地I2S Codec -> 内置喇叭
音频路由代码:
typedef enum {
AUDIO_ROUTE_LOCAL_SPEAKER = 0,
AUDIO_ROUTE_TWS,
AUDIO_ROUTE_BT_SPEAKER
} audio_route_t;
static audio_route_t s_route = AUDIO_ROUTE_LOCAL_SPEAKER;
void bt_audio_send_cmd(const char *cmd)
{
printf("[bt-audio] cmd=%s\n", cmd);
/*
* uart_write_bytes(BT_AUDIO_UART, cmd, strlen(cmd));
* uart_write_bytes(BT_AUDIO_UART, "\r\n", 2);
*/
}
void audio_route_set(audio_route_t route)
{
s_route = route;
switch (route) {
case AUDIO_ROUTE_LOCAL_SPEAKER:
printf("[audio] route local speaker\n");
// amp_enable(true);
// bt_audio_send_cmd("AT+DISCONNECT");
break;
case AUDIO_ROUTE_TWS:
printf("[audio] route TWS\n");
// amp_enable(false);
bt_audio_send_cmd("AT+CONNECT_LAST");
break;
case AUDIO_ROUTE_BT_SPEAKER:
printf("[audio] route BT speaker\n");
// amp_enable(false);
bt_audio_send_cmd("AT+PAIR");
break;
default:
break;
}
}
14. AI状态与双目表情联动
AI对话过程中,建议把设备状态、眼睛表情、马达和舵机统一联动。
typedef enum {
AI_STATE_IDLE = 0,
AI_STATE_WAKEUP,
AI_STATE_LISTENING,
AI_STATE_THINKING,
AI_STATE_SPEAKING,
AI_STATE_ERROR
} ai_state_t;
void ai_state_update(ai_state_t state)
{
switch (state) {
case AI_STATE_IDLE:
eye_display_show_sync(EYE_EXPR_IDLE);
break;
case AI_STATE_WAKEUP:
eye_display_show_sync(EYE_EXPR_WAKEUP);
motor_vibrate_once(60);
break;
case AI_STATE_LISTENING:
eye_display_show_sync(EYE_EXPR_THINKING);
break;
case AI_STATE_THINKING:
eye_display_play_assets(
"/assets/eyes/thinking_left.qoi",
"/assets/eyes/thinking_right.qoi",
15,
true
);
break;
case AI_STATE_SPEAKING:
eye_display_show_sync(EYE_EXPR_SPEAKING);
servo_head_nod();
break;
case AI_STATE_ERROR:
eye_display_show_sync(EYE_EXPR_ERROR);
motor_vibrate_pattern();
break;
default:
break;
}
}
15. 小程序 / 后端统一控制协议
客户自己的小程序和后端只需要按协议下发命令,设备端通过统一协议解析即可。
控制双目显示
{
"cmd": "eye_display",
"data": {
"mode": "async",
"left": "happy",
"right": "blink"
}
}
控制舵机动作
{
"cmd": "servo_action",
"data": {
"target": "tail",
"action": "wag",
"duration_ms": 1200
}
}
切换音频输出
{
"cmd": "audio_route",
"data": {
"route": "tws"
}
}
统一解析代码
#include <stdio.h>
#include <string.h>
#include "cJSON.h"
static eye_expr_t parse_eye_expr(const char *name)
{
if (!name) {
return EYE_EXPR_IDLE;
}
if (strcmp(name, "happy") == 0) {
return EYE_EXPR_HAPPY;
}
if (strcmp(name, "blink") == 0) {
return EYE_EXPR_BLINK;
}
if (strcmp(name, "thinking") == 0) {
return EYE_EXPR_THINKING;
}
if (strcmp(name, "speaking") == 0) {
return EYE_EXPR_SPEAKING;
}
if (strcmp(name, "sleep") == 0) {
return EYE_EXPR_SLEEP;
}
return EYE_EXPR_IDLE;
}
void app_protocol_handle_json(const char *json)
{
cJSON *root = cJSON_Parse(json);
if (!root) {
printf("[proto] json parse failed\n");
return;
}
cJSON *cmd = cJSON_GetObjectItem(root, "cmd");
cJSON *data = cJSON_GetObjectItem(root, "data");
if (!cmd || !data) {
cJSON_Delete(root);
return;
}
if (strcmp(cmd->valuestring, "eye_display") == 0) {
const char *mode = cJSON_GetObjectItem(data, "mode")->valuestring;
const char *left = cJSON_GetObjectItem(data, "left")->valuestring;
const char *right = cJSON_GetObjectItem(data, "right")->valuestring;
if (strcmp(mode, "sync") == 0) {
eye_display_show_sync(parse_eye_expr(left));
} else {
eye_display_show_async(parse_eye_expr(left), parse_eye_expr(right));
}
} else if (strcmp(cmd->valuestring, "servo_action") == 0) {
const char *action = cJSON_GetObjectItem(data, "action")->valuestring;
if (strcmp(action, "wag") == 0) {
servo_tail_wag();
} else if (strcmp(action, "nod") == 0) {
servo_head_nod();
}
} else if (strcmp(cmd->valuestring, "audio_route") == 0) {
const char *route = cJSON_GetObjectItem(data, "route")->valuestring;
if (strcmp(route, "speaker") == 0) {
audio_route_set(AUDIO_ROUTE_LOCAL_SPEAKER);
} else if (strcmp(route, "tws") == 0) {
audio_route_set(AUDIO_ROUTE_TWS);
} else if (strcmp(route, "bt_speaker") == 0) {
audio_route_set(AUDIO_ROUTE_BT_SPEAKER);
}
} else if (strcmp(cmd->valuestring, "asset_update") == 0) {
asset_manager_handle_update(json);
}
cJSON_Delete(root);
}
16. app_main.c 示例
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void app_main(void)
{
printf("SIBO AI Dual Eye Speaker Start\n");
network_auto_connect();
eye_display_init();
motor_init();
servo_init();
audio_route_set(AUDIO_ROUTE_LOCAL_SPEAKER);
eye_display_show_sync(EYE_EXPR_IDLE);
while (1) {
/*
* 实际工程主循环建议处理:
* 1. 触摸事件
* 2. G-sensor事件
* 3. AI状态事件
* 4. 小程序/后端命令
* 5. 双目动画刷新
* 6. 网络状态切换
*/
vTaskDelay(pdMS_TO_TICKS(20));
}
}
17. 总结
四博AI双目智能音箱方案不是简单给音箱增加两块屏幕,而是将 ESP32-S3 主控、双目显示、AI语音、客户素材上传、四路触摸、G-sensor、震动马达、舵机、Wi-Fi/4G/BLE联网、TWS耳机/蓝牙音箱扩展统一成一个可量产、可二次开发的AI硬件平台。
它适合以下产品方向:
AI桌面宠物
AI双目智能音箱
AI儿童陪伴机器人
AI语音机器人
AI互动摆件
AI智能玩具
AI情绪陪伴终端
AI学习机外设
AI小夜灯升级版
对B端客户来说,该方案的最大价值是开放度高:客户可以接入自己的小程序、自己的后端、自己的AI模型、自己的IP素材和自己的业务系统,在四博已有硬件与工程基础上快速做出差异化AI产品。