本帖最后由 云天 于 2026-8-10 15:47 编辑
这是一个将AI视觉与语音完美融合的创客项目,使用行空板K10打造了一个能听懂人话、会自己“看”东西的桌面物品看守器,专治办公室里的“笔失踪”难题。
【一、项目背景】 办公室的“笔去哪儿了”
在办公室,桌面上的笔似乎总有一种魔力——别人想写个字、签个名,会随手拿起,用完后却忘了归还,直接带走了。等你需要时,桌上只剩一个空荡荡的笔筒。
“我的笔呢?”成了办公室里每天重复最多的问题。
于是,“桌面看守器” 的想法诞生了——用行空板K10和小智AI,做一个能听懂人话、会自己“看”东西的桌面管家。你对它说一句:“帮我看好这支笔,每隔10秒检查一次”,它就会盯着你的笔,如果笔不见了,它会立刻语音提醒:“注意!你的笔可能被人拿走了!”,同时24灯珠LED灯点亮。
【二、硬件准备】
硬件
说明
行空板K10 核心主控,集成了ESP32-S3、2.8寸彩色屏幕、200万像素摄像头、麦克风、扬声器等 Type-C数据线 用于供电和烧录程序 被看守的物品 一支笔(或其他容易丢失的小物件)
行空板K10的核心参数:
主控:ESP32-S3 双核Xtensa LX7 @240MHz
内存:512KB SRAM + 8MB PSRAM
存储:16MB Flash
屏幕:2.8英寸TFT LCD,320×240分辨率,ILI9341驱动
摄像头:200万像素,GC2145传感器
无线:Wi-Fi 4 + Bluetooth 5
【三、软件准备】
3.1 开发环境
工具
版本要求
ESP-IDF v6.0.2(或更高版本,本文基于 v6.0.2 测试) xiaozhi-esp32 最新源码(2026年8月) Python 3.x Git 最新版
3.2 下载代码
3.3 设置ESP-IDF环境 确保你已经安装并配置好ESP-IDF v6.0.2环境:
# 假设ESP-IDF安装在 D:\esp-idf-v6.0.2
cd D:\esp-idf-v6.0.2
export.bat # Windows
# 或 source ./export.sh (Linux/macOS)
然后进入项目目录进行编译。
【四、核心技术实现】
4.1 设置目标芯片与配置板型
行空板K10使用ESP32-S3芯片,需要先设置目标芯片:
idf.py set-target esp32s3
然后进入配置菜单,选择对应的板型:
在 Xiaozhi Assistant -> Board Type 中选择 DFRobot 行空板 K10。
Xiaozhi Assistant -> Board Type -> DFRobot 行空板 K10
摄像头配置要点(重要):
传感器型号:GC2145(Component config -> Espressif Camera Sensors Configurations -> Camera Sensor Configuration -> Select and Set Camera Sensor -> GC2145 -> Auto detect GC2145)
输出格式:RGB565
800x600 20fps, DVP 8-bit, 20M input(Component config -> Espressif Camera Sensors Configurations -> Camera Sensor Configuration -> Select and Set Camera Sensor -> GC2145 -> Select default output format for DVP interface (RGB565 800x600 20fps, DVP 8-bit, 20M input) -> RGB565 800x600 20fps, DVP 8-bit, 20M input)
字节序交换:启用 Enable software camera buffer endianness swapping(Xiaozhi Assistant -> Camera Configuration -> Enable software camera buffer endianness swapping )
PSRAM模式:根据板子选 Octal Mode PSRAM 或 Quad Mode PSRAM(Component config -> ESP PSRAM -> SPI RAM config -> Mode (QUAD/OCT) -> Octal Mode PSRAM )
4.2 解决摄像头驱动问题(关键步骤)
在实际编译过程中,我发现摄像头无法初始化,日志报错:
E (928) EspVideo: no supported pixel format found 复制代码 问题原因:GC2145传感器输出的是大端RGB565格式(FOURCC = 'RGBR' 或 'RBGR'),但 EspVideo 驱动中没有为这种格式分配优先级,导致驱动认为“不支持”。
解决方法:修改 common/esp_video.cc,在 get_rank 函数中添加对大端RGB565格式的识别。
找到类似如下的 get_rank lambda 定义,在 switch 中添加:
// 在 esp_video.cc 的构造函数中,找到 get_rank lambda
auto get_rank = [](uint32_t fmt) -> int {
switch (fmt) {
case V4L2_PIX_FMT_YUYV:
case V4L2_PIX_FMT_UYVY:
case V4L2_PIX_FMT_YUV422P:
return 10;
case V4L2_PIX_FMT_RGB565:
return 11;
// ★★★ 新增对大端 RGB565 的支持 ★★★
#ifdef V4L2_PIX_FMT_RGB565X
case V4L2_PIX_FMT_RGB565X:
#else
// 如果系统中没有定义 V4L2_PIX_FMT_RGB565X,则直接使用实际检测到的值
case 0x52424752: // 根据日志,检测到的是 0x52424752('RBGR')
#endif
return 11; // 与大端 RGB565 同等优先级
// ... 其他 case ...
default:
return 1 << 29; // unsupported
}
}; 复制代码 4.3 注册MCP监控工具
小智AI通过MCP(模型上下文协议)来调用工具。我们需要为它注册两个新工具:self.camera.start_monitor(启动监控)和 self.camera.stop_monitor(停止监控)。
首先在文件顶部包含必要的头文件:
#include "mcp_server.h"
#include "esp_timer.h" 复制代码 在 df_k10_board.cc 的类定义中添加成员变量:
class Df_K10Board : public WifiBoard {
private:
esp_timer_handle_t cup_monitor_timer_ = nullptr;
struct CallbackData {
std::string target;
int interval; 复制代码 在构造函数中调用 InitializeTools():
Df_K10Board() {
InitializeI2c();
InitializeIoExpander();
InitializeSpi();
InitializeIli9341Display();
InitializeButtons();
InitializeIot();
InitializeCamera();
InitializeTools(); // 确保摄像头已初始化后再注册工具
} 复制代码 在 df_k10_board.cc 中实现 InitializeTools():
void Df_K10Board::InitializeTools() {
auto& mcp = McpServer::GetInstance();
// 启动监控工具
mcp.AddTool(
"self.camera.start_monitor",
"启动摄像头周期监控,持续监视某个物体(如水杯、遥控器等)。"
"系统会每隔指定的秒数自动抓拍一张照片,并询问AI该物体是否还在。"
"如果物体消失,小智会立即语音播报提醒。"
"当用户说“看着我的水杯”、“帮我盯着遥控器”、“监控杯子是否还在”等类似指令时,请调用此工具。",
PropertyList({
Property("target", kPropertyTypeString),
Property("interval_sec", kPropertyTypeInteger, 10, 1, 300)
}),
[this](const PropertyList& props) -> ReturnValue {
// ... 实现启动定时器逻辑
}
);
// 停止监控工具
mcp.AddTool(
"self.camera.stop_monitor",
"停止当前正在进行的摄像头监控。"
"当用户说“停止监控”、“别看了”、“结束监控”等指令时,调用此工具。",
PropertyList(),
[this](const PropertyList&) -> ReturnValue {
// ... 实现停止定时器逻辑
}
);
} 复制代码 监控回调的核心流程(定时器回调):
// 1. 抓拍
if (!camera->Capture()) {
ESP_LOGW(TAG, "抓拍失败,本次跳过");
return;
}
// 2. 调用AI视觉识别
std::string question = "请仔细检查图片中是否有“" + d->target + "”,如果还在只回答“有”,如果不在只回答“没有”。";
std::string answer;
try {
answer = camera->Explain(question);
} catch (const std::exception& e) {
ESP_LOGE(TAG, "AI识别异常: %s", e.what());
return;
}
// 3. 判断结果并播报
bool is_present = (answer.find("有") != std::string::npos);
if (!is_present) {
auto& app = Application::GetInstance();
Protocol& proto = app.GetProtocol();
std::string alert = "注意!" + d->target + " 可能不见了!";
if (app.GetDeviceState() == kDeviceStateListening ||
app.GetDeviceState() == kDeviceStateIdle) {
proto.SendWakeWordDetected(alert); // 主动播报提醒
}
ESP_LOGI(TAG, "警报:%s", alert.c_str());
} else {
ESP_LOGI(TAG, "%s 还在", d->target.c_str());
} 复制代码 在 InitializeIot() 中初始化 P0 灯带 void InitializeIot() {
// 原有的板载 LED(3颗)
led_strip_ = new CircularStrip(BUILTIN_LED_GPIO, 3);
new LedStripControl(led_strip_);
// ★ 新增:初始化 P0 上的 24 颗 WS2818 灯带
external_led_strip_ = new CircularStrip(GPIO_NUM_0, 24);
external_led_strip_->SetAllColor({0, 0, 0}); // 默认熄灭
} 复制代码
4.4 主动播报的实现
当监控发现物品不见时,需要让设备主动说话提醒用户。这里利用了 SendWakeWordDetected 方法——它模拟了一次“唤醒词检测”,让系统立即打断当前状态并播报提醒内容。
if (app.GetDeviceState() == kDeviceStateListening ||
app.GetDeviceState() == kDeviceStateIdle) {
proto.SendWakeWordDetected(alert);
} 复制代码 状态说明:
在这两种状态下播报不会干扰设备正在进行的其他操作(如播放回复)。
4.5 修改 application.h 增加 GetProtocol() 方法
由于我们在定时器回调中需要获取 Protocol 实例来调用 SendWakeWordDetected(),而 Application 类默认没有提供 GetProtocol() 公有接口,因此需要手动添加:
4.6 编译与烧录
# 编译
idf.py build
# 烧录(替换COMx为实际端口,如COM11)
idf.py -p COMx flash
# 监控日志(调试用)
idf.py -p COMx monitor 复制代码 如果需要生成合并固件(一个文件)供 flash_download_tool 使用:esptool --chip esp32s3 merge-bin -o xiaozhi.bin --flash-mode dio --flash-size 16MB --flash-freq 80m 0x0 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0xd000 build/ota_data_initial.bin 0x20000 build/xiaozhi.bin 0x800000 build/generated_assets.bin 复制代码 小智AI行空板k10固件:xiaozhi.zip
然后用 flash_download_tool 选择ESP32-S3,将 merge.bin 烧录到地址 0x0 即可。
【五、项目效果与使用演示】
对话示例
启动监控:
用户:“小智,帮我看好这支笔,每隔20秒检查一次。”
小智:“好的,我来帮你盯着这支笔,如果它不见了我会提醒你的。”
系统后台:启动周期定时器,每20秒抓拍一次,调用AI视觉判断笔是否还在。
触发警报:
用户:(拿走桌上的笔)
小智(主动播报):“注意!你的笔可能被人拿走了!”
系统后台:日志显示 警报:注意!笔 可能不见了!
停止监控:
用户:“小智,停止监控。”
小智:“已停止监控。
【六、项目总结与展望】
6.1 项目亮点 自然语音交互:通过日常语言即可启动/停止监控,无需编程或按键操作。
AI视觉理解:不是简单的“画面变化检测”,而是真正“理解”画面内容——AI会判断“笔还在不在”,不会因为光线变化或有人路过就误报。
主动播报提醒:物品不见时,设备会主动“说话”提醒,无需用户主动询问。
周期可调:用户可自定义检查间隔,在“实时性”和“功耗/费用”之间取得平衡。
6.2 应用场景扩展 【附录:完整代码】
df_k10_board.cc(工具注册完整实现) void Df_K10Board::InitializeTools() {
auto& mcp = McpServer::GetInstance();
// 启动监控工具
mcp.AddTool(
"self.camera.start_monitor",
"启动摄像头周期监控,持续监视某个物体(如水杯、遥控器等)。"
"系统会每隔指定的秒数自动抓拍一张照片,并询问AI该物体是否还在。"
"如果物体消失,小智会立即语音播报提醒。"
"当用户说“看着我的水杯”、“帮我盯着遥控器”、“监控杯子是否还在”等类似指令时,请调用此工具。"
"参数:\n"
" target:要监控的物体名称(中文,例如“水杯”)\n"
" interval_sec:检查间隔秒数,默认10秒,范围1~300秒",
PropertyList({
Property("target", kPropertyTypeString),
Property("interval_sec", kPropertyTypeInteger, 20, 1, 300)
}),
[this](const PropertyList& props) -> ReturnValue {
std::string target = props["target"].value<std::string>();
int interval = props["interval_sec"].value<int>();
// 如果已有监控定时器,先停掉
if (cup_monitor_timer_) {
esp_timer_stop(cup_monitor_timer_);
esp_timer_delete(cup_monitor_timer_);
cup_monitor_timer_ = nullptr;
}
if (cup_monitor_data_) {
delete cup_monitor_data_;
cup_monitor_data_ = nullptr;
}
// ★ 只创建一次,传入三个参数(包含 led_strip 指针)
cup_monitor_data_ = new Df_K10Board::CallbackData{target, interval, external_led_strip_};
const esp_timer_create_args_t args = {
.callback = [](void* arg) {
auto* d = static_cast<Df_K10Board::CallbackData*>(arg);
auto& board = Board::GetInstance();
auto camera = board.GetCamera();
if (!camera) {
ESP_LOGE(TAG, "摄像头未初始化");
return;
}
// 1. 抓拍
if (!camera->Capture()) {
ESP_LOGW(TAG, "抓拍失败,本次跳过");
return;
}
// 2. 构造提问
std::string question = "请仔细检查图片中是否有“" + d->target + "”,如果还在只回答“还在”,如果不在只回答“没有”。";
std::string answer;
try {
answer = camera->Explain(question);
} catch (const std::exception& e) {
ESP_LOGE(TAG, "AI识别异常: %s", e.what());
return;
}
// 3. 判断结果
bool is_present = (answer.find("还在") != std::string::npos);
if (!is_present) {
auto& app = Application::GetInstance();
Protocol& proto = app.GetProtocol();
std::string alert = "注意!" + d->target + " 可能不见了!";
if (app.GetDeviceState() == kDeviceStateListening ||
app.GetDeviceState() == kDeviceStateIdle ||
app.GetDeviceState() == kDeviceStateSpeaking) {
proto.SendWakeWordDetected(alert);
// ★ 通过 d->led_strip 控制灯带
if (d->led_strip) {
d->led_strip->SetAllColor({51, 0, 0}); // 低亮度红色
}
}
ESP_LOGI(TAG, "警报:%s", alert.c_str());
} else {
// ★ 通过 d->led_strip 熄灭灯带
if (d->led_strip) {
d->led_strip->SetAllColor({0, 0, 0});
}
ESP_LOGI(TAG, "%s 还在", d->target.c_str());
}
},
.arg = cup_monitor_data_,
.name = "cup_monitor"
};
esp_timer_create(&args, &cup_monitor_timer_);
esp_timer_start_periodic(cup_monitor_timer_, (uint64_t)interval * 1000000ULL);
ESP_LOGI(TAG, "开始监控 %s,间隔 %d 秒", target.c_str(), interval);
cJSON* json = cJSON_CreateObject();
cJSON_AddStringToObject(json, "status", "started");
cJSON_AddStringToObject(json, "info", ("正在监控 " + target + ",每 " + std::to_string(interval) + " 秒检查一次").c_str());
return json;
}
);
// 停止监控工具
mcp.AddTool(
"self.camera.stop_monitor",
"停止当前正在进行的摄像头监控。"
"当用户说“停止监控”、“别看了”、“结束监控”等指令时,调用此工具。",
PropertyList(),
[this](const PropertyList&) -> ReturnValue {
if (cup_monitor_timer_) {
esp_timer_stop(cup_monitor_timer_);
esp_timer_delete(cup_monitor_timer_);
cup_monitor_timer_ = nullptr;
}
if (cup_monitor_data_) {
delete cup_monitor_data_;
cup_monitor_data_ = nullptr;
}
// ★ 停止监控时熄灭外部灯带(这里可以直接使用成员变量,因为此 lambda 捕获了 this)
if (external_led_strip_) {
external_led_strip_->SetAllColor({0, 0, 0});
}
ESP_LOGI(TAG, "摄像头监控已停止");
return true;
}
);
} 复制代码