这是一个将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;
- }
- );
- }
复制代码
|