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 量产。