四博 CozyLife WiFi AI 智能音箱 S3 技术方案
1. 方案定位
本方案定义为一款 支持 CozyLife 远程管理、本地 SD 卡播放、AI 对话、OTA、客户系统接入的 WiFi AI 智能音箱。
推荐硬件核心:
主控:ESP32-S3R8 / ESPS3-32-N16R8
存储:16MB Flash + 8MB PSRAM
语音前端:VB6824,可选
本地存储:MicroSD / TF 卡
联网:WiFi + BLE BluFi
音频:I2S 功放 + 喇叭 + 麦克风
显示:1.3 / 1.54 / 2.0 寸 LCD,可选
云端:CozyLife APP / CozyLife Cloud / 客户自有平台 / 小智后端
扩展:AT+MCP / MQTT / HTTP / WebSocket / UART / GPIO
四博资料中 AI-S3 标准开发板是全开源 AI-Speaker 形态,带 240×240 分辨率 1.3 寸屏,支持"四博小助手"小程序、声音克隆、知识库、自建大模型和 MCP;AI 硬件选型表中也提到 S3 电子吧唧可配合四博小助手实现本地与云端智能体、声音克隆、知识库接入、MCP 扩展能力,以及素材与固件在线更新。
本方案的产品卖点可以定义为:
1. CozyLife APP 远程管理:
远程上传音乐、提示音、图片、视频、闹钟、播放列表和配置。
2. 本地 SD 卡播放:
支持 SD 卡音乐、TTS 缓存、提示音、离线内容包、日志和配置备份。
3. AI WiFi 音箱:
支持唤醒词、AI 对话、云端大模型、知识库、声音克隆、天气、音乐、闹钟。
4. ESP32-S3 二次开发:
客户可通过 ESP-IDF、HTTP、MQTT、UART、MCP 接入自有系统。
5. 远程运维:
支持 OTA、资源包更新、远程日志、设备状态上报、批量配置、灰度发布。
6. 量产友好:
支持 SN、批次号、产测、SD 卡预置内容、恢复出厂、断点续传和异常回滚。
2. 系统总体架构
┌──────────────────────────────────────────────────────────────┐
│ CozyLife WiFi AI 智能音箱 S3 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ UART / I2C / GPIO │
│ │ VB6824 语音前端 │◄──────────────────────────────┐ │
│ │ - 离线唤醒 │ │ │
│ │ - AEC / 降噪 │ │ │
│ │ - 改唤醒词 │ │ │
│ │ - 语音状态上报 │ │ │
│ └─────────┬────────────┘ │ │
│ │ MIC / 回采音频 │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ESP32-S3R8 + 16M Flash + 8M PSRAM │ │
│ │ - WiFi STA / SoftAP / BLE BluFi │ │
│ │ - CozyLife 云连接 / MQTT / HTTPS │ │
│ │ - SD 卡文件系统 / 本地媒体库 │ │
│ │ - AI WebSocket 对话 │ │
│ │ - 本地音乐 / 提示音 / TTS 播放 │ │
│ │ - OTA / 资源包更新 / 日志上报 │ │
│ │ - LCD UI / 按键 / RGB / 音量控制 │ │
│ │ - MCP 工具调用 / 客户系统接入 │ │
│ └───────┬───────────────┬───────────────┬────────────────┘ │
│ │ │ │ │
│ ┌───────▼──────┐ ┌──────▼───────┐ ┌─────▼─────────┐ │
│ │ MicroSD / TF │ │ I2S AMP + SPK │ │ LCD / RGB / KEY│ │
│ │ 音乐/素材/日志│ │ 音频播放 │ │ 人机交互 │ │
│ └──────────────┘ └──────────────┘ └───────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
云端:
┌──────────────────────────────────────────────────────────────┐
│ CozyLife APP / 小程序 / 客户后台 │
│ ↓ │
│ CozyLife Cloud / Customer Cloud │
│ ├── 设备绑定 / 权限 / 分组 │
│ ├── 素材管理 / SD 卡内容同步 │
│ ├── 播放列表 / 闹钟 / 场景 │
│ ├── OTA / 参数配置 / 日志 │
│ └── AI 后端:ASR / LLM / TTS / 知识库 / MCP │
└──────────────────────────────────────────────────────────────┘
3. 硬件方案
3.1 主控:ESP32-S3
推荐使用:
ESPS3-32-N16R8
ESP32-S3R8
16MB Flash
8MB PSRAM
2.4GHz WiFi
BLE
乐鑫系模组选型手册中,ESPS3-32 / ESPS3-32E 属于 ESP32-S3 系列,面向音视频 / AI 市场;ESPS3-32 有 N4、N8、N8R2、N16R2、N16R8 等子型号,兼容 ESP32-S3-WROOM-1 系列模组。
ESP32-S3 在本方案中负责:
1. WiFi 联网与 CozyLife 远程管理;
2. SD 卡文件系统;
3. 本地音乐播放;
4. AI WebSocket 对话;
5. OTA 与资源同步;
6. LCD / LED / 按键 UI;
7. MCP 工具调用;
8. 客户系统 API 接入。
3.2 语音前端:VB6824,可选但推荐
如果音箱需要更好的语音唤醒、AEC、改唤醒词和语音打断能力,建议增加 VB6824。四博 AI 硬件选型资料中提到 ESP32-C2 / C3 / S3 + VB6824 语音方案已经用于电子吧唧、S3 双目、S3 拍学机、地球仪、拍拍灯等产品,VB6824 负责音频编解码、AEC、语音唤醒、改唤醒词等功能,使主控芯片专注通信和 UI。
3.3 SD 卡设计
推荐两种接法:
方案 A:SDMMC 1-bit
优点:速度较好,占用 IO 少于 4-bit SDMMC。
适合:音频播放、资源更新、TTS 缓存。
方案 B:SPI SD Card
优点:接线灵活,便于和屏幕、传感器共板。
缺点:速度低于 SDMMC。
适合:成本敏感、小容量音频素材场景。
SD 卡用途:
/sdcard/cozylife/audio 本地音乐
/sdcard/cozylife/prompt 提示音
/sdcard/cozylife/tts_cache TTS 缓存
/sdcard/cozylife/image 屏幕图片
/sdcard/cozylife/video 屏幕动图或视频素材,带屏版本使用
/sdcard/cozylife/playlist 播放列表
/sdcard/cozylife/manifest 远程资源清单
/sdcard/cozylife/log 运行日志
/sdcard/cozylife/config 客户配置
3.4 音频链路
推荐链路:
麦克风
↓
VB6824 / ESP32-S3 音频输入
↓
AI 云端 ASR / LLM / TTS
↓
ESP32-S3 本地播放器
↓
I2S 数字功放
↓
喇叭
本地 SD 卡音乐链路:
SD Card WAV / MP3 / Opus
↓
解码器
↓
I2S Output
↓
功放
↓
喇叭
量产首版建议优先支持:
1. WAV PCM16:实现简单,稳定;
2. MP3:需要解码库;
3. Opus:适合 TTS 缓存和网络音频;
4. AAC:如客户有内容平台需求再加。
4. 软件架构
建议使用 ESP-IDF + FreeRTOS,基于 DOIT_AI 工程扩展。开发宝典中 AI-S3 相关工程使用 ESP-IDF 5.4.1,S3 电子吧唧工程也要求 VSCode 安装 ESP-IDF 插件并设置目标芯片为 ESP32-S3。
main/
app_main.c
components/
board/
board_config.h
board_init.c
storage/
sdcard_manager.c
nvs_config.c
file_sha256.c
cozy/
cozy_cloud.c
cozy_manifest.c
cozy_sync.c
cozy_remote_cmd.c
audio/
audio_focus.c
wav_player.c
mp3_player.c
tts_cache.c
volume_manager.c
ai/
ai_session.c
websocket_ai_client.c
mcp_uart_bridge.c
ui/
display_manager.c
led_manager.c
key_manager.c
ota/
ota_manager.c
resource_ota.c
customer/
customer_http.c
customer_mqtt.c
customer_uart.c
factory/
factory_test.c
FreeRTOS 任务建议:
| 任务 | 优先级 | 说明 |
|---|---|---|
wifi_task |
中高 | WiFi 连接、重连、配网 |
cozy_cloud_task |
中高 | MQTT / HTTPS 远程管理 |
cozy_sync_task |
中 | SD 卡资源清单同步 |
audio_player_task |
高 | 本地音乐、提示音、TTS 播放 |
ai_session_task |
高 | AI 对话 WebSocket |
mcp_uart_task |
中高 | 语义控制和状态同步 |
ui_task |
中 | 屏幕、RGB、按键 |
ota_task |
低 | 固件 OTA |
log_upload_task |
低 | 日志压缩和上传 |
factory_task |
低 | 产测 |
5. 远程管理设计
CozyLife 远程管理建议分成三类。
5.1 设备管理
1. 设备绑定 / 解绑;
2. 在线 / 离线状态;
3. WiFi 信号强度;
4. SD 卡容量;
5. 当前播放状态;
6. 当前音量;
7. 固件版本;
8. 资源版本;
9. 设备日志;
10. 故障码。
5.2 内容管理
1. 远程上传音乐;
2. 远程上传提示音;
3. 远程上传图片 / 视频素材;
4. 远程下发播放列表;
5. 远程设置闹钟;
6. 远程设置欢迎语;
7. 远程设置 AI 人设;
8. 远程同步知识库配置;
9. 远程设置声音克隆音色;
10. 远程删除过期资源。
AI 硬件选型表中 CozyLife 场景明确提到:可播动图、视频、SD 卡中的音乐、定时闹钟,可通过 CozyLife APP 上传图片、视频更新素材,可 AI 对讲,也可蓝牙上网。 本方案将其扩展为 WiFi 音箱后,远程内容管理主要通过 WiFi 完成,蓝牙可保留为配网或备用链路。
5.3 运维管理
1. 固件 OTA;
2. 资源包 OTA;
3. 重启设备;
4. 恢复出厂;
5. 远程抓日志;
6. 远程诊断 SD 卡;
7. 远程切换 AI 后端;
8. 远程切换客户业务环境;
9. 灰度升级;
10. 批量配置。
开发宝典中开源后端服务支持设备连接自己的后端,提供 OTA 接口和 WebSocket 接口,并可配置 LLM、TTS、人设等参数;文档也提到在服务器部署后反馈速度会更快,有显卡资源时还可自建 LLM 和 TTS 服务。
6. SD 卡资源清单设计
远程管理不建议直接"盲目覆盖文件",建议采用 manifest 清单机制。
6.1 SD 卡目录
/sdcard/cozylife/
├── audio/
│ ├── music_001.wav
│ ├── music_002.mp3
│ └── story_001.opus
│
├── prompt/
│ ├── boot.wav
│ ├── wifi_ok.wav
│ └── alarm.wav
│
├── image/
│ ├── cover_001.jpg
│ └── avatar_001.jpg
│
├── video/
│ └── bg_001.mjpeg
│
├── playlist/
│ └── default.json
│
├── manifest/
│ ├── current.json
│ └── pending.json
│
├── config/
│ └── device.json
│
└── log/
└── runtime.log
6.2 manifest 示例
{
"version": 32,
"device_group": "kids_speaker_cn",
"base_url": "https://cdn.customer.com/cozylife/resources/",
"files": [
{
"id": "boot_prompt",
"type": "prompt",
"path": "prompt/boot.wav",
"url": "prompt/boot_v3.wav",
"sha256": "b7e23ec29af22b0b4e41da31e868d57226121c84...",
"size": 184320
},
{
"id": "story_001",
"type": "audio",
"path": "audio/story_001.mp3",
"url": "audio/story_001_v5.mp3",
"sha256": "5a6f5a98d1c3219c8e2cbb23b2e1...",
"size": 5148291
}
],
"playlist": [
{
"id": "default",
"name": "默认播放列表",
"items": ["story_001", "boot_prompt"]
}
],
"alarms": [
{
"id": "morning",
"time": "07:30",
"repeat": "MON,TUE,WED,THU,FRI",
"action": "play",
"file_id": "story_001"
}
]
}
6.3 同步流程
1. 设备连接 WiFi。
2. 向 CozyLife Cloud 上报 device_id、fw_version、resource_version、sd_free。
3. 云端返回 manifest_url。
4. 设备下载 pending.json。
5. 对比 current.json 和 pending.json。
6. 下载缺失或变更文件到 .tmp。
7. SHA256 校验。
8. 校验成功后 rename 为正式文件。
9. 更新 current.json。
10. 上报 sync_success。
7. 开发与编译流程
git clone https://github.com/SmartArduino/DOIT_AI.git
cd DOIT_AI
idf.py set-target esp32s3
idf.py menuconfig
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor
建议在 menuconfig 中新增板型:
Xiaozhi Assistant
Board Type:
Sibo CozyLife WiFi Speaker S3
Network Configuration Mode:
BluFi / SoftAP
Storage:
Enable SD Card
SD Card Mode: SDMMC 1-bit / SPI
Audio:
I2S Speaker
Local WAV Player
TTS Cache
Cloud:
CozyLife Remote Management
MQTT Enable
HTTPS Resource Sync
AI:
WebSocket AI
MCP Enable
OTA:
Firmware OTA
Resource OTA
开发宝典中 AT+MCP 协议适合做客户系统接入:通过 UART 和 AT+ADDMCP 把自然语言映射成 MCU 可执行的二进制控制帧,并可上报开机、配网、联网、监听、说话、升级、激活等状态。
8. 关键代码示例
下面代码以 ESP-IDF 风格给出,GPIO 仅为示例,实际项目需要按 PCB 调整。
8.1 板级配置 board_config.h
#pragma once
#include "driver/gpio.h"
#include "driver/uart.h"
#include "driver/spi_master.h"
/*
* CozyLife WiFi AI Speaker based on ESP32-S3
*/
#define BOARD_NAME "SIBO_COZYLIFE_WIFI_SPEAKER_S3"
/* ---------- 主控 ---------- */
#define SOC_NAME "ESP32-S3R8"
#define FLASH_SIZE_MB 16
#define PSRAM_SIZE_MB 8
/* ---------- AI / VB6824 UART ---------- */
#define AI_UART UART_NUM_1
#define AI_UART_BAUD 115200
#define AI_UART_TX_GPIO GPIO_NUM_17
#define AI_UART_RX_GPIO GPIO_NUM_18
#define AI_RESET_GPIO GPIO_NUM_21
/* ---------- SDMMC 1-bit ---------- */
#define SDMMC_CLK_GPIO GPIO_NUM_39
#define SDMMC_CMD_GPIO GPIO_NUM_40
#define SDMMC_D0_GPIO GPIO_NUM_41
#define SD_MOUNT_POINT "/sdcard"
/* ---------- I2S Speaker ---------- */
#define I2S_BCLK_GPIO GPIO_NUM_6
#define I2S_LRCK_GPIO GPIO_NUM_7
#define I2S_DOUT_GPIO GPIO_NUM_8
#define AMP_EN_GPIO GPIO_NUM_5
/* ---------- LCD,可选 ---------- */
#define LCD_SPI_HOST SPI2_HOST
#define LCD_CS_GPIO GPIO_NUM_10
#define LCD_DC_GPIO GPIO_NUM_11
#define LCD_RST_GPIO GPIO_NUM_12
#define LCD_BL_GPIO GPIO_NUM_13
/* ---------- Keys ---------- */
#define KEY_BOOT_GPIO GPIO_NUM_0
#define KEY_PLAY_GPIO GPIO_NUM_14
#define KEY_VOL_UP_GPIO GPIO_NUM_15
#define KEY_VOL_DOWN_GPIO GPIO_NUM_16
/* ---------- RGB LED ---------- */
#define RGB_LED_GPIO GPIO_NUM_38
/* ---------- CozyLife Cloud ---------- */
#define COZY_DEVICE_ID_MAX_LEN 32
#define COZY_TOKEN_MAX_LEN 128
#define COZY_MQTT_URI "mqtts://mqtt.customer.com"
#define COZY_API_BASE "https://api.customer.com"
8.2 SD 卡挂载 sdcard_manager.c
#include <stdio.h>
#include <sys/stat.h>
#include "esp_log.h"
#include "esp_err.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"
#include "board_config.h"
#define TAG "SDCARD"
static sdmmc_card_t *s_card = NULL;
static void mkdir_if_not_exist(const char *path)
{
struct stat st = {0};
if (stat(path, &st) != 0) {
mkdir(path, 0775);
}
}
esp_err_t sdcard_init(void)
{
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 8,
.allocation_unit_size = 16 * 1024,
};
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.max_freq_khz = SDMMC_FREQ_DEFAULT;
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
/*
* SDMMC 1-bit 模式,节省 IO。
*/
slot_config.width = 1;
slot_config.clk = SDMMC_CLK_GPIO;
slot_config.cmd = SDMMC_CMD_GPIO;
slot_config.d0 = SDMMC_D0_GPIO;
ESP_LOGI(TAG, "mounting SD card...");
esp_err_t ret = esp_vfs_fat_sdmmc_mount(
SD_MOUNT_POINT,
&host,
&slot_config,
&mount_config,
&s_card
);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "failed to mount SD card: %s", esp_err_to_name(ret));
return ret;
}
sdmmc_card_print_info(stdout, s_card);
mkdir_if_not_exist("/sdcard/cozylife");
mkdir_if_not_exist("/sdcard/cozylife/audio");
mkdir_if_not_exist("/sdcard/cozylife/prompt");
mkdir_if_not_exist("/sdcard/cozylife/tts_cache");
mkdir_if_not_exist("/sdcard/cozylife/image");
mkdir_if_not_exist("/sdcard/cozylife/video");
mkdir_if_not_exist("/sdcard/cozylife/playlist");
mkdir_if_not_exist("/sdcard/cozylife/manifest");
mkdir_if_not_exist("/sdcard/cozylife/config");
mkdir_if_not_exist("/sdcard/cozylife/log");
ESP_LOGI(TAG, "SD card mounted");
return ESP_OK;
}
void sdcard_deinit(void)
{
if (s_card) {
esp_vfs_fat_sdcard_unmount(SD_MOUNT_POINT, s_card);
s_card = NULL;
}
}
bool sdcard_is_ready(void)
{
return s_card != NULL;
}
8.3 本地 WAV 播放器示例
这个示例用于播放 SD 卡中的 PCM16 WAV 文件。MP3 / Opus 可以后续替换为解码器输出 PCM 后复用同一 I2S 输出链路。
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include "esp_log.h"
#include "driver/i2s.h"
#include "driver/gpio.h"
#include "board_config.h"
#define TAG "WAV_PLAYER"
#define I2S_PORT I2S_NUM_0
#define I2S_DMA_BUF_LEN 512
typedef struct {
char riff[4];
uint32_t file_size;
char wave[4];
char fmt[4];
uint32_t fmt_size;
uint16_t audio_format;
uint16_t channels;
uint32_t sample_rate;
uint32_t byte_rate;
uint16_t block_align;
uint16_t bits_per_sample;
char data[4];
uint32_t data_size;
} wav_header_t;
esp_err_t audio_i2s_init(uint32_t sample_rate)
{
gpio_set_direction(AMP_EN_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(AMP_EN_GPIO, 1);
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_TX,
.sample_rate = sample_rate,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 6,
.dma_buf_len = I2S_DMA_BUF_LEN,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0,
};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCLK_GPIO,
.ws_io_num = I2S_LRCK_GPIO,
.data_out_num = I2S_DOUT_GPIO,
.data_in_num = I2S_PIN_NO_CHANGE,
};
ESP_ERROR_CHECK(i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL));
ESP_ERROR_CHECK(i2s_set_pin(I2S_PORT, &pin_config));
return ESP_OK;
}
static bool wav_header_valid(const wav_header_t *h)
{
return memcmp(h->riff, "RIFF", 4) == 0 &&
memcmp(h->wave, "WAVE", 4) == 0 &&
h->audio_format == 1 &&
h->bits_per_sample == 16;
}
esp_err_t wav_play_file(const char *path)
{
FILE *fp = fopen(path, "rb");
if (!fp) {
ESP_LOGE(TAG, "open failed: %s", path);
return ESP_FAIL;
}
wav_header_t header = {0};
fread(&header, 1, sizeof(header), fp);
if (!wav_header_valid(&header)) {
ESP_LOGE(TAG, "invalid WAV: %s", path);
fclose(fp);
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "play WAV %s, %lu Hz, ch=%u, data=%lu",
path,
header.sample_rate,
header.channels,
header.data_size);
i2s_set_clk(
I2S_PORT,
header.sample_rate,
I2S_BITS_PER_SAMPLE_16BIT,
header.channels == 1 ? I2S_CHANNEL_MONO : I2S_CHANNEL_STEREO
);
uint8_t buffer[2048];
size_t bytes_read = 0;
size_t bytes_written = 0;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
esp_err_t ret = i2s_write(
I2S_PORT,
buffer,
bytes_read,
&bytes_written,
portMAX_DELAY
);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2s_write failed");
break;
}
}
i2s_zero_dma_buffer(I2S_PORT);
fclose(fp);
ESP_LOGI(TAG, "play finished");
return ESP_OK;
}
8.4 CozyLife 资源同步框架
8.4.1 下载文件到 SD 卡
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_http_client.h"
#define TAG "COZY_SYNC"
typedef struct {
const char *url;
const char *local_path;
const char *tmp_path;
} cozy_download_job_t;
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
{
FILE *fp = (FILE *)evt->user_data;
if (evt->event_id == HTTP_EVENT_ON_DATA && fp && evt->data_len > 0) {
fwrite(evt->data, 1, evt->data_len, fp);
}
return ESP_OK;
}
esp_err_t cozy_download_to_file(const cozy_download_job_t *job)
{
if (!job || !job->url || !job->tmp_path || !job->local_path) {
return ESP_ERR_INVALID_ARG;
}
FILE *fp = fopen(job->tmp_path, "wb");
if (!fp) {
ESP_LOGE(TAG, "open tmp failed: %s", job->tmp_path);
return ESP_FAIL;
}
esp_http_client_config_t config = {
.url = job->url,
.event_handler = http_event_handler,
.user_data = fp,
.timeout_ms = 30000,
.buffer_size = 4096,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
ESP_LOGI(TAG, "download: %s", job->url);
esp_err_t err = esp_http_client_perform(client);
int status = esp_http_client_get_status_code(client);
esp_http_client_cleanup(client);
fclose(fp);
if (err != ESP_OK || status < 200 || status >= 300) {
ESP_LOGE(TAG, "download failed err=%s status=%d",
esp_err_to_name(err),
status);
remove(job->tmp_path);
return ESP_FAIL;
}
/*
* TODO:
* 1. 计算 tmp 文件 SHA256。
* 2. 与 manifest 中 sha256 比对。
* 3. 成功后原子替换。
*/
remove(job->local_path);
rename(job->tmp_path, job->local_path);
ESP_LOGI(TAG, "download ok: %s", job->local_path);
return ESP_OK;
}
8.4.2 manifest 同步任务
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define TAG "COZY_MANIFEST"
typedef struct {
int version;
char manifest_url[256];
} cozy_cloud_update_t;
static bool cozy_cloud_check_update(cozy_cloud_update_t *update)
{
/*
* TODO:
* 向 CozyLife Cloud 请求:
* GET /v1/device/{device_id}/manifest
*
* 返回:
* {
* "version": 32,
* "manifest_url": "https://cdn.xxx/current.json"
* }
*/
update->version = 32;
snprintf(update->manifest_url,
sizeof(update->manifest_url),
"https://cdn.customer.com/cozylife/manifest/current.json");
return true;
}
static void cozy_apply_manifest(const char *manifest_path)
{
/*
* TODO:
* 1. 用 cJSON 解析 manifest。
* 2. 遍历 files。
* 3. 判断本地文件是否存在、大小是否一致、SHA 是否一致。
* 4. 缺失或变更则调用 cozy_download_to_file。
* 5. 更新 current.json。
*/
ESP_LOGI(TAG, "apply manifest: %s", manifest_path);
}
void cozy_sync_task(void *arg)
{
while (1) {
cozy_cloud_update_t update = {0};
if (cozy_cloud_check_update(&update)) {
ESP_LOGI(TAG, "remote resource version=%d url=%s",
update.version,
update.manifest_url);
cozy_download_job_t job = {
.url = update.manifest_url,
.tmp_path = "/sdcard/cozylife/manifest/pending.json.tmp",
.local_path = "/sdcard/cozylife/manifest/pending.json",
};
if (cozy_download_to_file(&job) == ESP_OK) {
cozy_apply_manifest("/sdcard/cozylife/manifest/pending.json");
}
}
/*
* 正常产品建议 10~60 分钟轮询一次;
* 如果 MQTT 在线,可由云端下发 sync_now 命令立即触发。
*/
vTaskDelay(pdMS_TO_TICKS(10 * 60 * 1000));
}
}
8.5 远程控制命令设计
建议 CozyLife Cloud 通过 MQTT 下发命令,设备执行后上报结果。
MQTT Topic
下行:
cozylife/{product_id}/{device_id}/cmd
上行:
cozylife/{product_id}/{device_id}/state
cozylife/{product_id}/{device_id}/event
cozylife/{product_id}/{device_id}/log
命令示例
{
"msg_id": "20260424-0001",
"cmd": "play_local",
"params": {
"file": "/sdcard/cozylife/audio/story_001.wav",
"volume": 65
}
}
{
"msg_id": "20260424-0002",
"cmd": "sync_assets",
"params": {
"force": true
}
}
{
"msg_id": "20260424-0003",
"cmd": "set_alarm",
"params": {
"time": "07:30",
"repeat": "MON,TUE,WED,THU,FRI",
"file_id": "morning_music"
}
}
远程命令处理代码
#include <string.h>
#include "esp_log.h"
#include "cJSON.h"
#define TAG "REMOTE_CMD"
extern esp_err_t wav_play_file(const char *path);
extern void cozy_sync_now(void);
extern void volume_set_percent(uint8_t volume);
extern void alarm_set_from_json(cJSON *params);
extern void ota_start_by_url(const char *url);
static void remote_reply(const char *msg_id, const char *result)
{
/*
* TODO:
* MQTT publish:
* cozylife/{product_id}/{device_id}/event
*/
ESP_LOGI(TAG, "reply msg_id=%s result=%s", msg_id, result);
}
void remote_cmd_handle_json(const char *json)
{
cJSON *root = cJSON_Parse(json);
if (!root) {
ESP_LOGE(TAG, "invalid json");
return;
}
cJSON *msg_id = cJSON_GetObjectItem(root, "msg_id");
cJSON *cmd = cJSON_GetObjectItem(root, "cmd");
cJSON *params = cJSON_GetObjectItem(root, "params");
if (!cJSON_IsString(cmd)) {
cJSON_Delete(root);
return;
}
const char *id = cJSON_IsString(msg_id) ? msg_id->valuestring : "unknown";
if (strcmp(cmd->valuestring, "play_local") == 0) {
cJSON *file = cJSON_GetObjectItem(params, "file");
cJSON *volume = cJSON_GetObjectItem(params, "volume");
if (cJSON_IsNumber(volume)) {
volume_set_percent((uint8_t)volume->valueint);
}
if (cJSON_IsString(file)) {
esp_err_t ret = wav_play_file(file->valuestring);
remote_reply(id, ret == ESP_OK ? "ok" : "failed");
}
} else if (strcmp(cmd->valuestring, "sync_assets") == 0) {
cozy_sync_now();
remote_reply(id, "sync_started");
} else if (strcmp(cmd->valuestring, "set_alarm") == 0) {
alarm_set_from_json(params);
remote_reply(id, "ok");
} else if (strcmp(cmd->valuestring, "ota") == 0) {
cJSON *url = cJSON_GetObjectItem(params, "url");
if (cJSON_IsString(url)) {
ota_start_by_url(url->valuestring);
remote_reply(id, "ota_started");
}
} else if (strcmp(cmd->valuestring, "reboot") == 0) {
remote_reply(id, "rebooting");
esp_restart();
} else {
ESP_LOGW(TAG, "unknown cmd: %s", cmd->valuestring);
remote_reply(id, "unknown_cmd");
}
cJSON_Delete(root);
}
8.6 AT+MCP:语音控制 SD 卡与客户系统
开发宝典中的 AT+MCP 协议规定 AI 模组与 MCU 使用 UART 通信,默认 115200, 8N1,AI 模组收到合法指令后回复 OK;AT+ADDMCP 可将自然语言映射为 MCU 可执行的二进制帧;当 MCU 收到 55 AA 01 FC AA 55 时,需要重启 AI 模组并重新发送 MCP 映射。
本音箱建议注册以下 MCP 工具:
play_local_music 播放 SD 卡本地音乐
set_speaker_volume 设置音量
sync_cozylife_assets 同步 CozyLife 远程素材
set_alarm 设置闹钟
set_ai_role 切换 AI 人设
customer_scene 接入客户系统场景控制
MCP 注册示例
static void mcp_register_tools(void)
{
mcp_send_line("AT");
mcp_send_line("AT+WIFICFG=0");
mcp_send_line("AT+CONNECT");
/*
* 用户:"播放本地音乐"
* AI -> MCU: 55 AA 02 F1 01 AA 55
*/
mcp_send_line(
"AT+ADDMCP=1,play_local_music,播放SD卡本地音乐,F1,1,music_id"
);
/*
* 用户:"音量调到60"
* AI -> MCU: 55 AA 02 F2 3C AA 55
*/
mcp_send_line(
"AT+ADDMCP=1,set_speaker_volume,设置音箱音量,F2,1,volume"
);
/*
* 用户:"同步最新素材"
*/
mcp_send_line(
"AT+ADDMCP=0,sync_cozylife_assets,同步CozyLife远程素材,2,F3,01"
);
/*
* 用户:"明天早上七点半叫我"
*/
mcp_send_line(
"AT+ADDMCP=1,set_alarm,设置音箱闹钟,F4,3,hour,minute,repeat"
);
/*
* 用户:"打开客户系统的一号场景"
*/
mcp_send_line(
"AT+ADDMCP=1,customer_scene,控制客户业务系统,F5,2,scene_id,action"
);
}
MCP 帧解析与执行
#define CMD_PLAY_LOCAL 0xF1
#define CMD_VOLUME 0xF2
#define CMD_SYNC_ASSETS 0xF3
#define CMD_SET_ALARM 0xF4
#define CMD_CUSTOMER_SCENE 0xF5
#define CMD_STATUS 0xFF
#define CMD_RECOVER 0xFC
static void mcp_handle_frame(uint8_t cmd, const uint8_t *data, uint8_t len)
{
if (cmd == CMD_RECOVER) {
ai_reset_module();
mcp_register_tools();
return;
}
if (cmd == CMD_STATUS && len >= 1) {
/*
* 0x01 开机中
* 0x02 配网模式
* 0x03 空闲
* 0x04 联网中
* 0x05 监听
* 0x06 说话
* 0x07 升级
* 0x08 激活
*/
ui_set_ai_status(data[0]);
return;
}
switch (cmd) {
case CMD_PLAY_LOCAL:
if (len >= 1) {
char path[128];
snprintf(path, sizeof(path),
"/sdcard/cozylife/audio/music_%03u.wav",
data[0]);
wav_play_file(path);
}
break;
case CMD_VOLUME:
if (len >= 1) {
volume_set_percent(data[0]);
}
break;
case CMD_SYNC_ASSETS:
cozy_sync_now();
break;
case CMD_SET_ALARM:
if (len >= 3) {
alarm_set(data[0], data[1], data[2]);
}
break;
case CMD_CUSTOMER_SCENE:
if (len >= 2) {
customer_scene_control(data[0], data[1]);
}
break;
default:
ESP_LOGW("MCP", "unknown cmd=0x%02X len=%u", cmd, len);
break;
}
}
8.7 客户系统接入示例
客户系统接入推荐三种方式:
1. MQTT:
音箱作为客户云平台设备节点,上报状态和接收命令。
2. HTTP:
音箱调用客户 REST API,例如查询订单、控制设备、触发场景。
3. UART / RS485:
音箱作为语音控制入口,控制客户 MCU、灯控板、功放板或其他设备。
MCP 转客户 MQTT
#include <stdio.h>
#include "esp_log.h"
#define TAG "CUSTOMER"
void customer_scene_control(uint8_t scene_id, uint8_t action)
{
char topic[128];
char payload[128];
snprintf(topic, sizeof(topic),
"customer/device/%s/scene",
"cozylife_speaker_001");
snprintf(payload, sizeof(payload),
"{\"scene_id\":%u,\"action\":%u}",
scene_id,
action);
ESP_LOGI(TAG, "publish topic=%s payload=%s", topic, payload);
/*
* TODO:
* esp_mqtt_client_publish(client, topic, payload, 0, 1, 0);
*/
}
HTTP 调用客户接口
#include "esp_http_client.h"
#include "esp_log.h"
#define TAG "CUSTOMER_HTTP"
esp_err_t customer_http_call_scene(uint8_t scene_id, uint8_t action)
{
char body[128];
snprintf(body, sizeof(body),
"{\"scene_id\":%u,\"action\":%u}",
scene_id,
action);
esp_http_client_config_t config = {
.url = "https://api.customer.com/v1/scene/control",
.method = HTTP_METHOD_POST,
.timeout_ms = 8000,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_header(client, "Authorization", "Bearer ${token}");
esp_http_client_set_post_field(client, body, strlen(body));
esp_err_t err = esp_http_client_perform(client);
int status = esp_http_client_get_status_code(client);
ESP_LOGI(TAG, "customer api status=%d err=%s",
status,
esp_err_to_name(err));
esp_http_client_cleanup(client);
return err;
}
9. AI 对话与本地播放仲裁
音箱同时具备 AI 对话、本地 SD 音乐、提示音、闹钟,因此必须做音频焦点管理。
优先级:
1. 系统告警:
OTA 失败、低电量、SD 卡异常。
2. 用户语音监听:
唤醒后停止本地音乐或降低音量。
3. AI TTS:
AI 回复播放。
4. 闹钟:
可被语音"停止闹钟"打断。
5. 本地 SD 音乐:
普通音乐播放。
6. UI 提示音:
按键音、联网成功音。
代码示例:
typedef enum {
AUDIO_SRC_NONE = 0,
AUDIO_SRC_SYSTEM_ALERT,
AUDIO_SRC_AI_LISTENING,
AUDIO_SRC_AI_TTS,
AUDIO_SRC_ALARM,
AUDIO_SRC_SD_MUSIC,
AUDIO_SRC_UI_TONE,
} audio_source_t;
static audio_source_t s_audio_focus = AUDIO_SRC_NONE;
static int audio_priority(audio_source_t src)
{
switch (src) {
case AUDIO_SRC_SYSTEM_ALERT: return 100;
case AUDIO_SRC_AI_LISTENING: return 90;
case AUDIO_SRC_AI_TTS: return 80;
case AUDIO_SRC_ALARM: return 70;
case AUDIO_SRC_SD_MUSIC: return 50;
case AUDIO_SRC_UI_TONE: return 30;
default: return 0;
}
}
bool audio_request_focus(audio_source_t src)
{
if (audio_priority(src) < audio_priority(s_audio_focus)) {
return false;
}
if (s_audio_focus == AUDIO_SRC_SD_MUSIC &&
src == AUDIO_SRC_AI_LISTENING) {
/*
* AI 唤醒时暂停本地音乐。
*/
sd_music_pause();
}
if (s_audio_focus == AUDIO_SRC_AI_TTS &&
src == AUDIO_SRC_AI_LISTENING) {
/*
* 打断 TTS。
*/
tts_stop();
}
s_audio_focus = src;
return true;
}
10. OTA 与资源更新
10.1 固件 OTA
factory app
ota_0
ota_1
固件 OTA 用于升级:
1. 主程序;
2. WiFi / CozyLife 云连接;
3. AI 协议;
4. SD 卡管理;
5. 播放器;
6. 客户接口。
10.2 资源 OTA
资源 OTA 不建议走 ESP-IDF app 分区,而是放在 SD 卡中:
1. 音频资源;
2. 图片资源;
3. 视频资源;
4. 播放列表;
5. 闹钟;
6. AI 人设;
7. 提示音;
8. 客户配置。
资源 OTA 走 manifest,优势是:
1. 不影响主控固件;
2. 支持断点续传;
3. 支持单文件更新;
4. 支持 SD 卡大容量;
5. 支持运营人员远程换素材;
6. 适合 CozyLife APP 内容管理。
11. 产测方案
11.1 工厂测试项
主控:
- ESP32-S3 Flash / PSRAM
- MAC 地址
- SN 写入
- NVS 读写
WiFi:
- 扫描 AP
- 连接测试路由器
- CozyLife Cloud 连接
- MQTT 发布 / 订阅
SD 卡:
- 挂载
- 容量检测
- 写入测试
- 读取测试
- WAV 播放测试
音频:
- I2S 功放
- 喇叭播放 1kHz
- 麦克风录音
- 音量按键
AI:
- 唤醒词
- AI 对话
- TTS 播放
- MCP 命令
远程管理:
- manifest 下载
- 文件下载
- SHA256 校验
- OTA 检查
- 日志上传
UI:
- 屏幕显示
- RGB LED
- 按键单击 / 双击 / 长按
11.2 产测串口命令
FACTORY ENTER
FACTORY SET_SN=CLSPK20260001
FACTORY WIFI_SCAN
FACTORY WIFI_CONNECT=ssid,password
FACTORY SD_MOUNT
FACTORY SD_WRITE
FACTORY SD_READ
FACTORY PLAY=/sdcard/cozylife/prompt/test.wav
FACTORY MIC_LEVEL
FACTORY CLOUD_CONNECT
FACTORY MQTT_TEST
FACTORY MANIFEST_SYNC
FACTORY OTA_CHECK
FACTORY KEY_TEST
FACTORY LED_TEST
FACTORY EXIT
12. 推荐产品版本
12.1 基础 WiFi 音箱版
ESP32-S3R8 + 16M Flash + 8M PSRAM
WiFi + BLE BluFi
MicroSD
I2S 功放 + 3W 喇叭
麦克风
CozyLife APP 远程管理
SD 卡音乐
AI 对话
OTA
适合:故事机、桌面音箱、教育音箱、内容运营音箱。
12.2 带屏 CozyLife 音箱版
ESP32-S3R8
VB6824
MicroSD
1.54 / 2.0 寸 LCD
本地音乐 + 图片 / 视频素材
CozyLife APP 上传素材
AI 对话背景图
闹钟 / 天气 / 歌词 / 封面
MCP 扩展
适合:AI 电子吧唧、桌面陪伴音箱、IP 粉丝音箱、内容创作者周边。
12.3 B 端客户系统版
ESP32-S3R8
VB6824
MicroSD
WiFi
CozyLife / 客户云双平台
MQTT / HTTP / UART
客户知识库
客户业务 API
远程运维
批量 OTA
日志上报
适合:品牌 B 端、教育机构、酒店、展厅、门店语音终端、客户私有 AI 系统入口。
13. 方案总结
CozyLife WiFi AI 智能音箱建议采用 ESP32-S3R8 + 16M Flash + 8M PSRAM + MicroSD + I2S 音频 + 可选 VB6824 的架构:ESP32-S3 负责 WiFi、SD 卡、本地播放、CozyLife 远程管理、AI WebSocket、OTA 和客户系统接入;MicroSD 负责本地音乐、提示音、TTS 缓存、图片 / 视频素材、播放列表和日志;CozyLife APP / Cloud 负责远程上传素材、管理播放列表、下发闹钟、远程 OTA 和运维;VB6824 负责更高质量的语音唤醒、AEC、改唤醒词和打断能力;AT+MCP / MQTT / HTTP 则用于把自然语言控制转成客户系统可执行的业务指令。
这套方案既能做消费级 WiFi AI 音箱,也能做内容运营型 CozyLife 音箱,还能作为客户自有系统的 AI 语音入口,适合二次开发和快速量产。