CozyLife 墨水屏 + Find My / Google 双防丢四博 AI 智能音箱方案

CozyLife 墨水屏 + Find My / Google 双防丢四博 AI 智能音箱方案

一、方案概述

本文整理一套基于 四博 AI 智能音箱 + CozyLife 生态 + 墨水屏显示 + Apple Find My / Google 双系统防丢 的智能硬件方案。

该方案并不是普通 AI 音箱,而是一个集成 AI 语音交互、智能家居控制、墨水屏常显、蓝牙音箱、防丢提醒、场景联动、声音克隆、MCP 工具控制 的桌面 AI 终端。

整体产品可以理解为:

复制代码
四博 AI 智能音箱
+ CozyLife APP / 小程序
+ 墨水屏状态显示
+ AI 大模型语音交互
+ 蓝牙音箱
+ 闹钟 / 日程 / 提醒
+ 声音克隆 / 声纹识别
+ Apple Find My 防丢
+ Google 防丢
= 四博 AI 墨水屏双防丢智能音箱

四博资料中已有 ESP32-S3 音视频 / AI 模组、AI 开发宝典、CozyLife 类产品、AI 智能音响、Find My 类产品和双系统 FindMy + Google 防丢器等产品基础,可作为该方案的硬件和软件参考。


二、产品定位

该产品适合以下场景:

复制代码
1. AI 智能音箱
2. 桌面 AI 陪伴终端
3. 智能家居控制中心
4. 家庭防丢提醒设备
5. 品牌 IP 智能音箱
6. 儿童学习陪伴音箱
7. 老人提醒 / 家庭看护设备
8. 礼品和电商差异化产品

相比普通音箱,它多了三个核心差异点:

复制代码
1. 墨水屏:低功耗常显,可显示时间、天气、AI 摘要、设备状态、防丢状态
2. CozyLife:可接入智能家居场景、远程控制、定时、群组、APP 配置
3. 双防丢:支持 Apple Find My 和 Google 防丢功能

三、核心功能设计

复制代码
1. AI 智能音箱
   - 支持语音唤醒
   - 支持 AI 大模型对话
   - 支持连续对话
   - 支持实时打断
   - 支持声音克隆
   - 支持声纹识别
   - 支持蓝牙音箱
   - 支持闹钟 / 日程 / 提醒

2. CozyLife 智能家居
   - 支持 APP 远程控制
   - 支持定时
   - 支持群组
   - 支持场景联动
   - 支持智能音箱控制
   - 支持设备状态上报

3. 墨水屏显示
   - 显示时间 / 日期
   - 显示天气 / 温湿度
   - 显示 AI 对话摘要
   - 显示闹钟 / 日程
   - 显示智能家居场景
   - 显示 Find My / Google 绑定状态
   - 显示二维码 / 配网码
   - 支持低功耗常显

4. 双系统防丢
   - 支持 Apple Find My
   - 支持 Google 防丢
   - 支持本地蜂鸣器寻物
   - 支持低电量提醒
   - 支持遗落 / 丢失状态提醒
   - 支持音箱语音播报防丢状态

5. MCP 自然语言控制
   - "小博,帮我找一下音箱"
   - "小博,显示防丢状态"
   - "小博,把墨水屏切到日程页面"
   - "小博,打开回家模式"
   - "小博,显示配网二维码"

四、系统总体架构

复制代码
┌──────────────────────────────────────────────┐
│      CozyLife 墨水屏双防丢 AI 智能音箱         │
├──────────────────────────────────────────────┤
│  交互层                                      │
│  ├─ 麦克风:语音采集                         │
│  ├─ 喇叭:TTS / 蓝牙音乐 / 提示音             │
│  ├─ 墨水屏:状态常显 / 场景 / 防丢状态        │
│  ├─ 按键 / 触控:唤醒、打断、音量、翻页       │
│  ├─ RGB 灯:状态提示                         │
│  └─ 蜂鸣器:本地寻物提醒                     │
├──────────────────────────────────────────────┤
│  主控层                                      │
│  ├─ ESP32-S3:AI、Wi-Fi、BLE、屏幕、音频       │
│  ├─ VB6824:离线唤醒、离线命令、降噪          │
│  ├─ ES8311 / I2S Codec:音频采集和播放        │
│  ├─ E-Paper Driver:墨水屏驱动                │
│  └─ Power / Battery:电源和低功耗管理         │
├──────────────────────────────────────────────┤
│  防丢层                                      │
│  ├─ Find My / Google 双系统防丢 SoC           │
│  ├─ BLE 广播 / 平台认证协议                   │
│  ├─ 密钥 / Token / 安全存储                   │
│  └─ UART / I2C 与 ESP32-S3 通信               │
├──────────────────────────────────────────────┤
│  固件层                                      │
│  ├─ app_event_bus:事件总线                   │
│  ├─ audio_mgr:音频采集 / 播放                 │
│  ├─ ai_ws_client:AI WebSocket 通信            │
│  ├─ epaper_ui:墨水屏页面管理                 │
│  ├─ cozy_cloud:CozyLife 云端同步             │
│  ├─ tag_bridge:双系统防丢模块通信            │
│  ├─ mcp_tools:自然语言工具控制               │
│  ├─ ota_mgr:OTA 升级                         │
│  └─ power_mgr:低功耗管理                     │
├──────────────────────────────────────────────┤
│  云端 / APP 层                                │
│  ├─ CozyLife APP / 小程序                     │
│  ├─ AI 大模型网关                             │
│  ├─ ASR / TTS / Voice Clone                   │
│  ├─ RAG 知识库                                │
│  ├─ MCP 工具服务                              │
│  └─ Apple / Google 防丢平台                   │
└──────────────────────────────────────────────┘

五、硬件方案设计

1. 主控选型

推荐使用 ESP32-S3 N16R8 / N16R2 作为主控。

ESP32-S3 适合该方案的原因:

复制代码
1. 双核 240MHz,适合多任务调度
2. GPIO 资源充足,可接音频、墨水屏、按键、RGB、防丢模块
3. 支持 Wi-Fi + BLE
4. 支持 PSRAM,适合音频缓存、UI 缓存和 WebSocket 数据流
5. 适合 AI 音频、智能家居、屏幕显示类产品

2. 防丢模块建议

Apple Find My 和 Google 防丢属于平台级认证生态,量产时不建议由 ESP32-S3 主控直接实现协议。推荐采用独立防丢模块:

复制代码
方案 A:独立双系统防丢 SoC
- 内部支持 Find My 协议栈
- 内部支持 Google 防丢协议栈
- 独立 BLE 广播
- 独立密钥安全存储
- 独立低功耗运行
- 通过 UART/I2C 向 ESP32-S3 上报状态

方案 B:两个独立防丢模块
- 一个 Apple Find My 模块
- 一个 Google 防丢模块
- ESP32-S3 统一做状态显示和业务控制

方案 C:认证防丢模块 + ESP32-S3
- 防丢模块负责认证协议
- ESP32-S3 负责 AI、CozyLife、墨水屏和音频

这种设计可以把认证协议和主业务逻辑解耦,后期维护和认证风险更低。

3. 推荐 BOM 框架

模块 推荐方案 说明
主控 ESP32-S3 N16R8 AI、Wi-Fi、BLE、墨水屏、音频
语音 VB6824 离线唤醒、离线命令、降噪
Codec ES8311 / I2S Codec 麦克风采集、喇叭播放
功放 3W~5W D 类功放 音箱播放
屏幕 2.13 / 2.9 / 3.7 寸墨水屏 低功耗常显
防丢 Find My + Google 双系统模块 独立认证协议
蜂鸣器 80dB~100dB 本地寻物
RGB WS2812 / PWM RGB 状态提示
电源 Type-C + 锂电池可选 桌面 / 便携
存储 16MB Flash + 8MB PSRAM 资源、缓存、OTA
通信 Wi-Fi 2.4G + BLE CozyLife / AI / 配网

六、固件工程目录

复制代码
sibo_ai_epaper_speaker/
├── CMakeLists.txt
├── sdkconfig.defaults
├── partitions.csv
├── main/
│   ├── app_main.c
│   ├── app_config.h
│   ├── board_pins.h
│   ├── app_event_bus.c
│   ├── app_event_bus.h
│   ├── audio_mgr.c
│   ├── ai_ws_client.c
│   ├── epaper_ui.c
│   ├── cozy_cloud.c
│   ├── tag_bridge.c
│   ├── tag_bridge.h
│   ├── mcp_tools.c
│   ├── key_mgr.c
│   ├── power_mgr.c
│   ├── ota_mgr.c
│   └── factory_test.c
├── components/
│   ├── epaper_driver/
│   ├── audio_codec/
│   ├── json_parser/
│   ├── qr_render/
│   └── font_assets/
└── server/
    ├── ai_gateway.py
    ├── mcp_server.py
    └── cozy_bridge.py

七、基础配置

app_config.h

复制代码
#pragma once

#define PRODUCT_NAME                  "SIBO_AI_EPAPER_SPEAKER"
#define FIRMWARE_VERSION              "v1.0.0"

#define ENABLE_WIFI                   1
#define ENABLE_BLE                    1
#define ENABLE_BLUFI                  1
#define ENABLE_COZYLIFE               1
#define ENABLE_AI_ASSISTANT           1
#define ENABLE_EPAPER                 1
#define ENABLE_FINDMY_GOOGLE_TAG      1
#define ENABLE_MCP                    1
#define ENABLE_OTA                    1

#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 COZY_DEVICE_TYPE              "ai_epaper_speaker"

#define EPAPER_WIDTH                  296
#define EPAPER_HEIGHT                 128

#define TAG_UART_BAUD                 115200
#define TAG_QUERY_INTERVAL_MS         5000

#define DEFAULT_VOLUME                45
#define MAX_VOLUME                    100

board_pins.h

复制代码
#pragma once

#include "driver/gpio.h"

#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_EPAPER_SCLK               GPIO_NUM_12
#define PIN_EPAPER_MOSI               GPIO_NUM_13
#define PIN_EPAPER_CS                 GPIO_NUM_14
#define PIN_EPAPER_DC                 GPIO_NUM_15
#define PIN_EPAPER_RST                GPIO_NUM_16
#define PIN_EPAPER_BUSY               GPIO_NUM_17

#define PIN_TAG_UART_TX               GPIO_NUM_18
#define PIN_TAG_UART_RX               GPIO_NUM_19

#define PIN_BUZZER_PWM                GPIO_NUM_21
#define PIN_RGB_LED                   GPIO_NUM_48

#define PIN_KEY_WAKE                  GPIO_NUM_38
#define PIN_KEY_VOL_UP                GPIO_NUM_39
#define PIN_KEY_VOL_DOWN              GPIO_NUM_40
#define PIN_KEY_PAGE                  GPIO_NUM_41

八、分区表设计

墨水屏字体、图片资源、二维码缓存、提示音等可以放到 assets 分区。

复制代码
# partitions.csv
# Name,     Type, SubType, Offset,   Size
nvs,        data, nvs,     0x9000,   0x6000
otadata,    data, ota,     0xf000,   0x2000
phy_init,   data, phy,     0x11000,  0x1000
factory,    app,  factory, 0x20000,  3M
ota_0,      app,  ota_0,   ,         3M
ota_1,      app,  ota_1,   ,         3M
assets,     data, spiffs,  ,         4M
storage,    data, nvs,     ,         0x10000

九、事件总线设计

设备需要同时处理 AI 状态、墨水屏刷新、CozyLife 状态、防丢模块事件、按键和 OTA,因此建议使用事件总线。

复制代码
#pragma once

#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"

typedef enum {
    EVT_KEY_WAKE,
    EVT_KEY_WAKE_LONG,
    EVT_KEY_VOL_UP,
    EVT_KEY_VOL_DOWN,
    EVT_KEY_PAGE,

    EVT_AI_IDLE,
    EVT_AI_LISTENING,
    EVT_AI_THINKING,
    EVT_AI_SPEAKING,
    EVT_AI_INTERRUPTED,

    EVT_COZY_ONLINE,
    EVT_COZY_OFFLINE,
    EVT_COZY_SCENE_CHANGED,

    EVT_EPAPER_REFRESH,
    EVT_EPAPER_SHOW_QR,
    EVT_EPAPER_NEXT_PAGE,

    EVT_TAG_FINDMY_BOUND,
    EVT_TAG_GOOGLE_BOUND,
    EVT_TAG_LOST_MODE,
    EVT_TAG_NEARBY,
    EVT_TAG_LOW_BAT,
    EVT_TAG_BUZZER_ON,
    EVT_TAG_BUZZER_OFF,

    EVT_OTA_START,
    EVT_OTA_DONE,
    EVT_OTA_FAIL,
} app_evt_type_t;

typedef struct {
    app_evt_type_t type;
    int32_t value;
    int64_t ts_ms;
} app_evt_t;

void app_event_bus_init(void);
QueueHandle_t app_event_bus_register(const char *name, uint32_t len);
void app_event_post(app_evt_type_t type, int32_t value);

事件广播实现:

复制代码
#include <string.h>
#include "esp_timer.h"
#include "app_event_bus.h"

#define SUB_MAX 12

typedef struct {
    char name[24];
    QueueHandle_t q;
} app_sub_t;

static app_sub_t s_subs[SUB_MAX];
static int s_sub_count = 0;

void app_event_bus_init(void)
{
    memset(s_subs, 0, sizeof(s_subs));
    s_sub_count = 0;
}

QueueHandle_t app_event_bus_register(const char *name, uint32_t len)
{
    if (s_sub_count >= SUB_MAX) {
        return NULL;
    }

    QueueHandle_t q = xQueueCreate(len, sizeof(app_evt_t));
    if (!q) {
        return NULL;
    }

    strncpy(s_subs[s_sub_count].name, name, sizeof(s_subs[s_sub_count].name) - 1);
    s_subs[s_sub_count].q = q;
    s_sub_count++;

    return q;
}

void app_event_post(app_evt_type_t type, int32_t value)
{
    app_evt_t evt = {
        .type = type,
        .value = value,
        .ts_ms = esp_timer_get_time() / 1000
    };

    for (int i = 0; i < s_sub_count; i++) {
        if (s_subs[i].q) {
            xQueueSend(s_subs[i].q, &evt, 0);
        }
    }
}

十、主程序入口

复制代码
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

#include "app_config.h"
#include "app_event_bus.h"

extern void key_mgr_start(void);
extern void audio_mgr_start(void);
extern void ai_ws_client_start(void);
extern void epaper_ui_start(void);
extern void cozy_cloud_start(void);
extern void tag_bridge_start(void);
extern void mcp_tools_start(void);
extern void ota_mgr_start(void);
extern void power_mgr_start(void);

static const char *TAG = "app_main";

void app_main(void)
{
    ESP_LOGI(TAG, "boot %s %s", PRODUCT_NAME, FIRMWARE_VERSION);

    app_event_bus_init();

    key_mgr_start();
    audio_mgr_start();
    power_mgr_start();

#if ENABLE_EPAPER
    epaper_ui_start();
#endif

#if ENABLE_COZYLIFE
    cozy_cloud_start();
#endif

#if ENABLE_FINDMY_GOOGLE_TAG
    tag_bridge_start();
#endif

#if ENABLE_AI_ASSISTANT
    ai_ws_client_start();
#endif

#if ENABLE_MCP
    mcp_tools_start();
#endif

#if ENABLE_OTA
    ota_mgr_start();
#endif

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

十一、双防丢模块通信设计

推荐 ESP32-S3 通过 UART 与独立防丢模块通信。防丢模块负责 Find My / Google 协议,ESP32-S3 负责读取状态、显示状态、发起蜂鸣器命令。

1. 通信帧格式

复制代码
Frame:
+--------+--------+------+-----+----------+------+
| 0x55   | 0xAA   | LEN  | CMD | PAYLOAD  | XOR  |
+--------+--------+------+-----+----------+------+

CMD:
0x01  查询状态
0x02  启动蜂鸣器
0x03  停止蜂鸣器
0x04  查询绑定状态
0x05  查询电量
0x06  进入配对模式

Event:
0x81  Find My 已绑定
0x82  Google 已绑定
0x83  低电量
0x84  遗失模式
0x85  附近找到

2. tag_bridge.h

复制代码
#pragma once

#include <stdint.h>
#include <stdbool.h>

typedef enum {
    TAG_BIND_NONE       = 0,
    TAG_BIND_FINDMY     = 1 << 0,
    TAG_BIND_GOOGLE     = 1 << 1,
} tag_bind_mask_t;

typedef struct {
    uint8_t bind_mask;
    uint8_t battery_percent;
    bool lost_mode;
    bool buzzer_on;
    int8_t rssi;
} tag_status_t;

void tag_bridge_start(void);
void tag_query_status(void);
void tag_start_buzzer(void);
void tag_stop_buzzer(void);
void tag_enter_pair_mode(void);
bool tag_get_status(tag_status_t *out);

3. tag_bridge.c

复制代码
#include <string.h>
#include "driver/uart.h"
#include "esp_log.h"
#include "board_pins.h"
#include "app_config.h"
#include "app_event_bus.h"
#include "tag_bridge.h"

#define TAG_UART_NUM            UART_NUM_1
#define TAG_RX_BUF_SIZE         512

#define TAG_CMD_QUERY_STATUS    0x01
#define TAG_CMD_BUZZER_ON       0x02
#define TAG_CMD_BUZZER_OFF      0x03
#define TAG_CMD_PAIR_MODE       0x06

#define TAG_EVT_FINDMY_BOUND    0x81
#define TAG_EVT_GOOGLE_BOUND    0x82
#define TAG_EVT_LOW_BAT         0x83
#define TAG_EVT_LOST_MODE       0x84
#define TAG_EVT_NEARBY          0x85

static const char *TAG = "tag_bridge";
static tag_status_t s_status;

static uint8_t calc_xor(const uint8_t *buf, int len)
{
    uint8_t x = 0;

    for (int i = 0; i < len; i++) {
        x ^= buf[i];
    }

    return x;
}

static void tag_send_cmd(uint8_t cmd, const uint8_t *payload, uint8_t payload_len)
{
    uint8_t frame[64];
    int idx = 0;

    frame[idx++] = 0x55;
    frame[idx++] = 0xAA;
    frame[idx++] = payload_len + 1;
    frame[idx++] = cmd;

    if (payload && payload_len > 0) {
        memcpy(&frame[idx], payload, payload_len);
        idx += payload_len;
    }

    frame[idx] = calc_xor(frame, idx);
    idx++;

    uart_write_bytes(TAG_UART_NUM, (const char *)frame, idx);
}

void tag_query_status(void)
{
    tag_send_cmd(TAG_CMD_QUERY_STATUS, NULL, 0);
}

void tag_start_buzzer(void)
{
    tag_send_cmd(TAG_CMD_BUZZER_ON, NULL, 0);
    s_status.buzzer_on = true;
    app_event_post(EVT_TAG_BUZZER_ON, 0);
}

void tag_stop_buzzer(void)
{
    tag_send_cmd(TAG_CMD_BUZZER_OFF, NULL, 0);
    s_status.buzzer_on = false;
    app_event_post(EVT_TAG_BUZZER_OFF, 0);
}

void tag_enter_pair_mode(void)
{
    tag_send_cmd(TAG_CMD_PAIR_MODE, NULL, 0);
}

bool tag_get_status(tag_status_t *out)
{
    if (!out) {
        return false;
    }

    memcpy(out, &s_status, sizeof(tag_status_t));
    return true;
}

static void tag_handle_event(uint8_t cmd, const uint8_t *payload, int len)
{
    switch (cmd) {
    case TAG_EVT_FINDMY_BOUND:
        s_status.bind_mask |= TAG_BIND_FINDMY;
        app_event_post(EVT_TAG_FINDMY_BOUND, 1);
        break;

    case TAG_EVT_GOOGLE_BOUND:
        s_status.bind_mask |= TAG_BIND_GOOGLE;
        app_event_post(EVT_TAG_GOOGLE_BOUND, 1);
        break;

    case TAG_EVT_LOW_BAT:
        s_status.battery_percent = len > 0 ? payload[0] : 0;
        app_event_post(EVT_TAG_LOW_BAT, s_status.battery_percent);
        break;

    case TAG_EVT_LOST_MODE:
        s_status.lost_mode = true;
        app_event_post(EVT_TAG_LOST_MODE, 1);
        break;

    case TAG_EVT_NEARBY:
        s_status.lost_mode = false;
        app_event_post(EVT_TAG_NEARBY, 1);
        break;

    default:
        ESP_LOGW(TAG, "unknown tag event: 0x%02X", cmd);
        break;
    }
}

static void tag_parse_frame(const uint8_t *buf, int len)
{
    if (len < 5) {
        return;
    }

    if (buf[0] != 0x55 || buf[1] != 0xAA) {
        return;
    }

    uint8_t frame_len = buf[2];
    uint8_t cmd = buf[3];
    const uint8_t *payload = &buf[4];
    int payload_len = frame_len - 1;

    uint8_t xor_recv = buf[3 + frame_len];
    uint8_t xor_calc = calc_xor(buf, 3 + frame_len);

    if (xor_recv != xor_calc) {
        ESP_LOGW(TAG, "xor error");
        return;
    }

    tag_handle_event(cmd, payload, payload_len);
}

static void tag_uart_task(void *arg)
{
    uint8_t buf[TAG_RX_BUF_SIZE];

    while (1) {
        int len = uart_read_bytes(
            TAG_UART_NUM,
            buf,
            sizeof(buf),
            pdMS_TO_TICKS(200)
        );

        if (len > 0) {
            tag_parse_frame(buf, len);
        }
    }
}

static void tag_poll_task(void *arg)
{
    while (1) {
        tag_query_status();
        vTaskDelay(pdMS_TO_TICKS(TAG_QUERY_INTERVAL_MS));
    }
}

void tag_bridge_start(void)
{
    uart_config_t cfg = {
        .baud_rate = TAG_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(TAG_UART_NUM, TAG_RX_BUF_SIZE, 0, 0, NULL, 0);
    uart_param_config(TAG_UART_NUM, &cfg);
    uart_set_pin(
        TAG_UART_NUM,
        PIN_TAG_UART_TX,
        PIN_TAG_UART_RX,
        UART_PIN_NO_CHANGE,
        UART_PIN_NO_CHANGE
    );

    memset(&s_status, 0, sizeof(s_status));

    xTaskCreate(tag_uart_task, "tag_uart", 4096, NULL, 6, NULL);
    xTaskCreate(tag_poll_task, "tag_poll", 4096, NULL, 4, NULL);
}

十二、墨水屏 UI 页面设计

墨水屏适合做低功耗常显,建议设计多个页面。

复制代码
Page 0:首页
- 时间
- 日期
- 天气
- AI 状态
- CozyLife 在线状态

Page 1:防丢状态页
- Find My:已绑定 / 未绑定
- Google:已绑定 / 未绑定
- 防丢电量
- 遗失模式
- 蜂鸣器状态

Page 2:智能家居页
- 当前场景
- 房间温湿度
- 设备数量
- 回家 / 离家模式

Page 3:AI 摘要页
- 用户问题摘要
- AI 回复摘要
- 当前模型

Page 4:配网页
- CozyLife 绑定二维码
- BluFi 配网提示

epaper_ui.c

复制代码
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "app_event_bus.h"
#include "tag_bridge.h"
#include "app_config.h"

typedef enum {
    EPAPER_PAGE_HOME = 0,
    EPAPER_PAGE_TAG,
    EPAPER_PAGE_COZY,
    EPAPER_PAGE_AI,
    EPAPER_PAGE_QR,
    EPAPER_PAGE_MAX,
} epaper_page_t;

static QueueHandle_t s_epaper_q;
static epaper_page_t s_page = EPAPER_PAGE_HOME;

static char s_ai_summary[128] = "Hello, I am Sibo AI.";
static char s_cozy_scene[64] = "Home";
static bool s_cozy_online = false;

static void epaper_clear(void)
{
    /*
     * epaper_driver_clear();
     */
}

static void epaper_draw_text(int x, int y, const char *text)
{
    /*
     * epaper_driver_draw_text(x, y, text);
     */
}

static void epaper_update(void)
{
    /*
     * epaper_driver_update();
     */
}

static void draw_home_page(void)
{
    epaper_clear();

    epaper_draw_text(0, 0, "SIBO AI SPEAKER");
    epaper_draw_text(0, 20, "Time: 08:30");
    epaper_draw_text(0, 40, s_cozy_online ? "CozyLife: Online" : "CozyLife: Offline");
    epaper_draw_text(0, 60, "AI: Ready");
    epaper_draw_text(0, 90, "Press PAGE to switch");

    epaper_update();
}

static void draw_tag_page(void)
{
    tag_status_t st;
    char line[64];

    tag_get_status(&st);

    epaper_clear();

    epaper_draw_text(0, 0, "Anti-Lost Status");

    snprintf(line, sizeof(line), "Find My: %s",
             (st.bind_mask & TAG_BIND_FINDMY) ? "Bound" : "Unbound");
    epaper_draw_text(0, 24, line);

    snprintf(line, sizeof(line), "Google: %s",
             (st.bind_mask & TAG_BIND_GOOGLE) ? "Bound" : "Unbound");
    epaper_draw_text(0, 44, line);

    snprintf(line, sizeof(line), "Battery: %d%%", st.battery_percent);
    epaper_draw_text(0, 64, line);

    snprintf(line, sizeof(line), "Lost: %s", st.lost_mode ? "YES" : "NO");
    epaper_draw_text(0, 84, line);

    snprintf(line, sizeof(line), "Buzzer: %s", st.buzzer_on ? "ON" : "OFF");
    epaper_draw_text(0, 104, line);

    epaper_update();
}

static void draw_cozy_page(void)
{
    epaper_clear();

    epaper_draw_text(0, 0, "CozyLife Scene");
    epaper_draw_text(0, 24, s_cozy_scene);
    epaper_draw_text(0, 48, "Devices: 12");
    epaper_draw_text(0, 72, "Scene: Home");
    epaper_draw_text(0, 96, "Voice Control Ready");

    epaper_update();
}

static void draw_ai_page(void)
{
    epaper_clear();

    epaper_draw_text(0, 0, "AI Summary");
    epaper_draw_text(0, 24, s_ai_summary);

    epaper_update();
}

static void draw_qr_page(void)
{
    epaper_clear();

    epaper_draw_text(0, 0, "Scan to Bind");
    epaper_draw_text(0, 24, "CozyLife / BluFi");

    /*
     * qr_render_draw("cozylife://bind?device_id=xxx");
     */

    epaper_update();
}

static void epaper_draw_page(void)
{
    switch (s_page) {
    case EPAPER_PAGE_HOME:
        draw_home_page();
        break;

    case EPAPER_PAGE_TAG:
        draw_tag_page();
        break;

    case EPAPER_PAGE_COZY:
        draw_cozy_page();
        break;

    case EPAPER_PAGE_AI:
        draw_ai_page();
        break;

    case EPAPER_PAGE_QR:
        draw_qr_page();
        break;

    default:
        draw_home_page();
        break;
    }
}

static void epaper_task(void *arg)
{
    app_evt_t evt;

    epaper_draw_page();

    while (1) {
        if (xQueueReceive(s_epaper_q, &evt, portMAX_DELAY) == pdTRUE) {
            switch (evt.type) {
            case EVT_KEY_PAGE:
            case EVT_EPAPER_NEXT_PAGE:
                s_page++;
                if (s_page >= EPAPER_PAGE_MAX) {
                    s_page = EPAPER_PAGE_HOME;
                }
                epaper_draw_page();
                break;

            case EVT_TAG_FINDMY_BOUND:
            case EVT_TAG_GOOGLE_BOUND:
            case EVT_TAG_LOW_BAT:
            case EVT_TAG_LOST_MODE:
            case EVT_TAG_NEARBY:
                if (s_page == EPAPER_PAGE_TAG) {
                    epaper_draw_page();
                }
                break;

            case EVT_COZY_ONLINE:
                s_cozy_online = true;
                epaper_draw_page();
                break;

            case EVT_COZY_OFFLINE:
                s_cozy_online = false;
                epaper_draw_page();
                break;

            case EVT_AI_THINKING:
                strncpy(s_ai_summary, "AI is thinking...", sizeof(s_ai_summary) - 1);
                epaper_draw_page();
                break;

            case EVT_AI_SPEAKING:
                strncpy(s_ai_summary, "AI is speaking...", sizeof(s_ai_summary) - 1);
                epaper_draw_page();
                break;

            case EVT_EPAPER_SHOW_QR:
                s_page = EPAPER_PAGE_QR;
                epaper_draw_page();
                break;

            default:
                break;
            }
        }
    }
}

void epaper_ui_start(void)
{
    s_epaper_q = app_event_bus_register("epaper", 16);

    /*
     * epaper_driver_init();
     */

    xTaskCreate(epaper_task, "epaper_task", 8192, NULL, 4, NULL);
}

十三、CozyLife 状态模型

AI 音箱可以作为 CozyLife 生态中的"显示 + 语音 + 防丢状态聚合终端"。

复制代码
typedef struct {
    char device_id[32];
    bool online;
    int volume;
    bool ai_enabled;
    bool alarm_enabled;
    bool findmy_bound;
    bool google_bound;
    bool lost_mode;
    int tag_battery;
    char epaper_page[16];
    char scene_name[32];
} cozy_shadow_t;

static cozy_shadow_t s_shadow = {
    .device_id = "sibo_epaper_speaker_001",
    .online = false,
    .volume = 45,
    .ai_enabled = true,
    .alarm_enabled = false,
    .findmy_bound = false,
    .google_bound = false,
    .lost_mode = false,
    .tag_battery = 100,
    .epaper_page = "home",
    .scene_name = "Home"
};

状态上报 JSON:

复制代码
#include "cJSON.h"

static char *cozy_build_status_json(void)
{
    cJSON *root = cJSON_CreateObject();

    cJSON_AddStringToObject(root, "device_id", s_shadow.device_id);
    cJSON_AddBoolToObject(root, "online", s_shadow.online);
    cJSON_AddNumberToObject(root, "volume", s_shadow.volume);
    cJSON_AddBoolToObject(root, "ai_enabled", s_shadow.ai_enabled);
    cJSON_AddBoolToObject(root, "alarm_enabled", s_shadow.alarm_enabled);
    cJSON_AddBoolToObject(root, "findmy_bound", s_shadow.findmy_bound);
    cJSON_AddBoolToObject(root, "google_bound", s_shadow.google_bound);
    cJSON_AddBoolToObject(root, "lost_mode", s_shadow.lost_mode);
    cJSON_AddNumberToObject(root, "tag_battery", s_shadow.tag_battery);
    cJSON_AddStringToObject(root, "epaper_page", s_shadow.epaper_page);
    cJSON_AddStringToObject(root, "scene_name", s_shadow.scene_name);

    char *json = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);

    return json;
}

CozyLife 命令解析:

复制代码
static void cozy_handle_cmd(const char *json)
{
    cJSON *root = cJSON_Parse(json);
    if (!root) {
        return;
    }

    cJSON *cmd = cJSON_GetObjectItem(root, "cmd");
    cJSON *value = cJSON_GetObjectItem(root, "value");

    if (!cJSON_IsString(cmd)) {
        cJSON_Delete(root);
        return;
    }

    if (!strcmp(cmd->valuestring, "set_volume")) {
        if (cJSON_IsNumber(value)) {
            s_shadow.volume = value->valueint;
            app_event_post(EVT_KEY_VOL_UP, s_shadow.volume);
        }
    } else if (!strcmp(cmd->valuestring, "show_qr")) {
        app_event_post(EVT_EPAPER_SHOW_QR, 0);
    } else if (!strcmp(cmd->valuestring, "next_page")) {
        app_event_post(EVT_EPAPER_NEXT_PAGE, 0);
    } else if (!strcmp(cmd->valuestring, "find_device")) {
        tag_start_buzzer();
    } else if (!strcmp(cmd->valuestring, "stop_find_device")) {
        tag_stop_buzzer();
    } else if (!strcmp(cmd->valuestring, "enter_pair_mode")) {
        tag_enter_pair_mode();
    }

    cJSON_Delete(root);
}

十四、MCP 工具控制

MCP 用来把自然语言变成设备动作。

例如:

复制代码
"小博,帮我找一下这个音箱"
"小博,显示防丢状态"
"小博,把墨水屏切到日程页面"
"小博,打开回家模式"
"小博,显示配网二维码"

MCP 工具注册

复制代码
#include "driver/uart.h"
#include "esp_log.h"
#include <string.h>

#define MCP_UART_NUM          UART_NUM_2
#define MCP_UART_BAUD         115200
#define MCP_RX_BUF            512

static const char *TAG = "mcp_tools";

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);
}

static void mcp_register_tools(void)
{
    mcp_send_at("AT");
    mcp_send_at("AT+CONNECT");

    mcp_send_at("AT+ADDMCP=0,show_home,显示首页,2,E1,00");
    mcp_send_at("AT+ADDMCP=0,show_tag_status,显示防丢状态,2,E1,01");
    mcp_send_at("AT+ADDMCP=0,show_qr,显示配网二维码,2,E1,02");

    mcp_send_at("AT+ADDMCP=0,find_speaker,查找音箱,2,F1,01");
    mcp_send_at("AT+ADDMCP=0,stop_find_speaker,停止查找音箱,2,F1,00");

    mcp_send_at("AT+ADDMCP=1,set_volume,设置音量,F2,1,V");
    mcp_send_at("AT+ADDMCP=0,scene_home,打开回家模式,2,F3,01");
    mcp_send_at("AT+ADDMCP=0,scene_sleep,打开睡眠模式,2,F3,02");
}

MCP 命令解析

复制代码
static void handle_mcp_frame(const uint8_t *buf, int len)
{
    if (len < 5) {
        return;
    }

    if (buf[0] != 0x55 || buf[1] != 0xAA) {
        return;
    }

    uint8_t cmd = buf[3];

    switch (cmd) {
    case 0xE1:
        if (buf[4] == 0x00) {
            app_event_post(EVT_EPAPER_REFRESH, 0);
        } else if (buf[4] == 0x01) {
            app_event_post(EVT_EPAPER_NEXT_PAGE, 1);
        } else if (buf[4] == 0x02) {
            app_event_post(EVT_EPAPER_SHOW_QR, 0);
        }
        break;

    case 0xF1:
        if (buf[4] == 0x01) {
            tag_start_buzzer();
        } else {
            tag_stop_buzzer();
        }
        break;

    case 0xF2:
        /*
         * buf[4] = volume
         */
        break;

    case 0xF3:
        /*
         * buf[4] = scene id
         */
        app_event_post(EVT_COZY_SCENE_CHANGED, buf[4]);
        break;

    case 0xFC:
        mcp_register_tools();
        break;

    default:
        ESP_LOGW(TAG, "unknown mcp cmd=0x%02X", cmd);
        break;
    }
}

十五、AI WebSocket 客户端

设备通过 WebSocket 连接后端,后端负责 ASR、LLM、TTS、RAG 和 Voice Clone。

复制代码
#include <string.h>
#include "esp_websocket_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include "app_event_bus.h"
#include "app_config.h"

static const char *TAG = "ai_ws";
static esp_websocket_client_handle_t s_ws;

static void ai_handle_json(const char *data, int len)
{
    cJSON *root = cJSON_ParseWithLength(data, len);
    if (!root) {
        return;
    }

    cJSON *type = cJSON_GetObjectItem(root, "type");

    if (cJSON_IsString(type)) {
        if (!strcmp(type->valuestring, "idle")) {
            app_event_post(EVT_AI_IDLE, 0);
        } else if (!strcmp(type->valuestring, "listening")) {
            app_event_post(EVT_AI_LISTENING, 0);
        } else if (!strcmp(type->valuestring, "thinking")) {
            app_event_post(EVT_AI_THINKING, 0);
        } else if (!strcmp(type->valuestring, "speaking")) {
            app_event_post(EVT_AI_SPEAKING, 0);
        } else if (!strcmp(type->valuestring, "interrupted")) {
            app_event_post(EVT_AI_INTERRUPTED, 0);
        }
    }

    cJSON_Delete(root);
}

static void ws_event_handler(void *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, "AI websocket connected");

        esp_websocket_client_send_text(
            s_ws,
            "{\"type\":\"hello\",\"product\":\"SIBO_AI_EPAPER_SPEAKER\"}",
            strlen("{\"type\":\"hello\",\"product\":\"SIBO_AI_EPAPER_SPEAKER\"}"),
            portMAX_DELAY
        );
        break;

    case WEBSOCKET_EVENT_DATA:
        if (d->op_code == 0x1) {
            ai_handle_json(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, "AI websocket disconnected");
        app_event_post(EVT_AI_IDLE, 0);
        break;

    default:
        break;
    }
}

void ai_ws_send_pcm(const uint8_t *data, size_t len)
{
    if (s_ws && esp_websocket_client_is_connected(s_ws)) {
        esp_websocket_client_send_bin(s_ws, (const char *)data, len, 0);
    }
}

void ai_ws_client_start(void)
{
    esp_websocket_client_config_t cfg = {
        .uri = AI_WS_URL,
        .reconnect_timeout_ms = 3000,
        .network_timeout_ms = 5000,
    };

    s_ws = esp_websocket_client_init(&cfg);

    esp_websocket_register_events(
        s_ws,
        WEBSOCKET_EVENT_ANY,
        ws_event_handler,
        NULL
    );

    esp_websocket_client_start(s_ws);
}

十六、CozyLife APP 配置字段

复制代码
{
  "device_id": "sibo_epaper_speaker_001",
  "name": "四博AI墨水屏音箱",
  "type": "ai_epaper_speaker",
  "features": {
    "ai": true,
    "epaper": true,
    "findmy": true,
    "google_finder": true,
    "bluetooth_speaker": true,
    "voice_clone": true,
    "voiceprint": true
  },
  "state": {
    "online": true,
    "volume": 45,
    "epaper_page": "home",
    "findmy_bound": true,
    "google_bound": true,
    "lost_mode": false,
    "tag_battery": 86
  },
  "actions": [
    "show_home",
    "show_tag_status",
    "show_qr",
    "find_speaker",
    "stop_find_speaker",
    "set_volume",
    "scene_home",
    "scene_sleep"
  ]
}

十七、低功耗策略

复制代码
1. 墨水屏只在状态变化时刷新
2. AI 空闲时降低麦克风采样频率
3. 防丢模块独立低功耗运行
4. ESP32-S3 根据供电状态进入 light sleep
5. Wi-Fi 保持低频上报 CozyLife 状态
6. Find My / Google 广播由独立防丢模块负责
7. 蜂鸣器只在寻物时工作
8. OTA 和墨水屏全刷避免同时执行,降低峰值电流

十八、量产测试项目

复制代码
FACTORY_TEST_AUDIO_IN        麦克风采集测试
FACTORY_TEST_AUDIO_OUT       喇叭播放测试
FACTORY_TEST_EPAPER          墨水屏黑白刷新测试
FACTORY_TEST_KEY             按键测试
FACTORY_TEST_RGB             RGB 灯测试
FACTORY_TEST_TAG_UART        防丢模块 UART 通信测试
FACTORY_TEST_FINDMY_BIND     Find My 绑定状态检测
FACTORY_TEST_GOOGLE_BIND     Google 绑定状态检测
FACTORY_TEST_BUZZER          蜂鸣器测试
FACTORY_TEST_WIFI            Wi-Fi RSSI 测试
FACTORY_TEST_BLE             BLE 广播测试
FACTORY_WRITE_SN             写入 SN
FACTORY_WRITE_CERT           写入证书
FACTORY_OTA_TEST             OTA 测试

十九、总结

该方案的核心不是单纯做一个 AI 音箱,而是将 AI 语音、CozyLife 智能家居、墨水屏常显、Apple Find My、Google 防丢 组合成一个桌面智能终端。

最终产品能力可以总结为:

复制代码
AI 大模型语音交互
+ CozyLife APP / 小程序控制
+ 墨水屏低功耗常显
+ 蓝牙音箱
+ 闹钟提醒
+ 声音克隆
+ 声纹识别
+ Apple Find My 防丢
+ Google 防丢
+ MCP 自然语言控制
= 四博 AI 墨水屏双防丢智能音箱

推荐落地架构:

复制代码
ESP32-S3:
负责 AI、CozyLife、墨水屏、音频和系统逻辑。

独立防丢模块:
负责 Apple Find My / Google 防丢协议和 BLE 广播。

CozyLife APP:
负责用户配置、状态查看、场景联动和设备管理。

AI 后端:
负责语音理解、TTS、声音克隆、RAG 和 MCP 工具调用。

这种架构边界清晰,认证风险低,固件维护简单,也更适合后续做多 SKU 量产。

相关推荐
奔跑中的小象3 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南3 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang3 小时前
LangGraph 状态机实战
人工智能
刹那芳华19923 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作4 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门4 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜4 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟4 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦4 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生5 小时前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能