四博 CozyLife WiFi AI 智能音箱 S3 技术方案

四博 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 模组收到合法指令后回复 OKAT+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 语音入口,适合二次开发和快速量产。

相关推荐
武帝为此2 小时前
【数据质量校验简介】
人工智能·python·机器学习
ai产品老杨2 小时前
【架构解析】高并发 AI 视频流管理平台:实现 X86/ARM 异构部署与 GB28181 全链路源码交付
arm开发·人工智能·架构
TG_yunshuguoji2 小时前
亚马逊云代理商:如何在AWS上部署Hermes Agent?
人工智能·云计算·aws·hermes agent·hermes
BU摆烂会噶2 小时前
【工作流的常见模式】LangGraph 常用模式:路由模式(条件分支)
数据库·人工智能·python·langchain
互联网推荐官2 小时前
上海小程序开发的接口安全与数据通信设计:工程实践中的关键决策
大数据·人工智能·物联网·软件工程
7yewh2 小时前
针对灵巧手机械结构的探究
网络·人工智能·单片机·深度学习·嵌入式
禹凕4 小时前
PyTorch——安装(有无 NVIDIA 显卡的完整配置方案)
人工智能·pytorch·python
大龄程序员狗哥10 小时前
第25篇:Q-Learning算法解析——强化学习中的经典“价值”学习(原理解析)
人工智能·学习·算法
陶陶然Yay10 小时前
神经网络常见层Numpy封装参考(5):其他层
人工智能·神经网络·numpy