31浏览
查看: 31|回复: 0

[K10项目分享] 行空板k10——小智语音闹钟、光感智能主动播报

[复制链接]
本帖最后由 云天 于 2025-9-12 19:08 编辑

【项目背景】
       基于行空板K10(ESP32-S3)安装修改后的小智AI固件,结合板载温湿度传感器、光照强度传感器,打造“语音闹钟+环境智报”方案,解决儿童学习场景下“忘喝水、光线暗”等健康痛点,可用自然语音设置提醒并实时监测温湿度、光照。
【项目设计】
       利用行空板K10小智AI的MCP工具链注册一次性/周期闹钟与传感器指令,通过esp_timer调度回调,周期读取AHT20/LTR303(温湿度/光照值)缓存数据,低于阈值即调用SendWakeWordDetected打断播报,中断级响应。
        项目基于 VS Code 与 ESP-IDF 开发框架,依托小智 AI 开源代码构建。(具体配置可参考这前项目文章)
行空板k10——小智语音闹钟、光感智能主动播报图5

【项目亮点】
        用行空板K10安装的"小智"声纹,在闹钟响起或环境异常时主动唤醒并即刻播报,让同一个AI完成"听-说-提醒"完整闭环。
行空板k10——小智语音闹钟、光感智能主动播报图4

语音闹钟】
        1.让小智设备在指定秒数后响一次每隔固定秒数循环响的语音提醒,并可随时停掉循环(”mcp_server.cc"文件)
       (1)把「未来某个时刻要做的事」登记到系统时钟里,然后 CPU 去睡觉,时间到了由中断帮你叫醒
       esp_timer_create 把回调函数、参数、名字写进 RTOS 的定时器链表。
  1. const esp_timer_create_args_t args = {
  2.                     .callback = [](void* arg) {
  3.                         auto* p = static_cast<std::string*>(arg);
  4.                         auto& app = Application::GetInstance();
  5.                         Protocol& protocol = app.GetProtocol();   // 正确拼写
  6.                         if (app.GetDeviceState() == kDeviceStateListening) {
  7.                             std::string content = "您定的闹钟时间到了,请您" + *p;
  8.                             protocol.SendWakeWordDetected(content);
  9.                         }
  10.                         ESP_LOGI(TAG, "时间到了:%s", p->c_str());
  11.                         delete p;          // 释放堆内存
  12.                     },
  13.                     .arg = p_name,
  14.                     .name = "alarm_timer"
  15. };
复制代码
      (2)一次性闹钟
  1. esp_timer_create(&args, &once);
  2. esp_timer_start_once(once, seconde_from_now * 1000000ULL); // µs
复制代码
        把“一次性闹钟”精确到微秒级插进 ESP-IDF 的软定时器队列,然后 CPU 就可以去休眠;等 RTC/FRC 计数器走到 target_time = now + seconde_from_now·1 000 000 µs时,硬件中断触发 → 调度回调 → 你的提醒语音响起。
       (3)周期时钟
  1. esp_timer_create(&args, &periodic_alarm_);
  2. esp_timer_start_periodic(periodic_alarm_, period_sec * 1000000ULL);
复制代码
      esp_timer_start_periodic先把 period_sec * 1 000 000转成微秒,再取当前 esp_timer_get_time()累加,得到第一次到期的 target_us,以后每次到期再 target_us += period_us
       (4)把 CPU 从“轮询”变成“事件”
       到期后 RTC 或 FRC 定时器产生中断 → 中断里只做“把回调插到 RTOS 队列”立即返回 → 空闲任务真正执行回调。
       因此主循环 100 % 空出来干别的,功耗瞬间降到最低。
      2.伪装唤醒,即刻播报
      protocol.SendWakeWordDetected(content); 的“主动播报”本质伪装成一次“唤醒词已触发”事件,让后续语音合成链路自动跑起来,无需再经过真正的唤醒词识别,从而0 改动实现“即时打断 + 播报”。
  1. AddTool("self.send.text",
  2.             "当用户要**一次性**定时提醒时,调用此工具。seconde_from_now:闹钟多少秒以后响;alarm_name:时钟的描述(名字)",
  3.             PropertyList({
  4.                 Property("seconde_from_now", kPropertyTypeInteger, 10, 0, 7200),
  5.                 Property("alarm_name", kPropertyTypeString)
  6.             }),
  7.             [](const PropertyList& properties) -> ReturnValue {
  8.                 int seconde_from_now = properties["seconde_from_now"].value<int>();
  9.                 std::string alarm_name = properties["alarm_name"].value<std::string>();
  10.    
  11.                 /* 把 alarm_name 搬进堆,让定时器回调能拿到 */
  12.                 auto* p_name = new std::string(alarm_name);
  13.    
  14.                 /* 一次性定时器 */
  15.                 esp_timer_handle_t once;
  16.                 const esp_timer_create_args_t args = {
  17.                     .callback = [](void* arg) {
  18.                         auto* p = static_cast<std::string*>(arg);
  19.                         auto& app = Application::GetInstance();
  20.                         Protocol& protocol = app.GetProtocol();   // 正确拼写
  21.                         if (app.GetDeviceState() == kDeviceStateListening) {
  22.                             std::string content = "您定的闹钟时间到了,请您" + *p;
  23.                             protocol.SendWakeWordDetected(content);
  24.                         }
  25.                         ESP_LOGI(TAG, "时间到了:%s", p->c_str());
  26.                         delete p;          // 释放堆内存
  27.                     },
  28.                     .arg = p_name,
  29.                     .name = "alarm_timer"
  30.                 };
  31.                 esp_timer_create(&args, &once);
  32.                 esp_timer_start_once(once, seconde_from_now * 1000000ULL); // µs
  33.                 return true;
  34. });
  35.     /* --------------- 工具注册:周期闹钟 --------------- */
  36. AddTool("self.send.periodic_alarm",
  37.     "周期提醒:period_sec:间隔秒数;alarm_name:提醒内容",
  38.     PropertyList({
  39.         Property("period_sec", kPropertyTypeInteger, 5, 1, 3600),
  40.         Property("alarm_name",  kPropertyTypeString)
  41.     }),
  42.     [this](const PropertyList& props) -> ReturnValue {
  43.         int period_sec = props["period_sec"].value<int>();
  44.         auto* p_name   = new std::string(props["alarm_name"].value<std::string>());
  45.         /* 如果旧闹钟还在,先停掉 */
  46.         if (periodic_alarm_) {
  47.             esp_timer_stop(periodic_alarm_);
  48.             esp_timer_delete(periodic_alarm_);
  49.             periodic_alarm_ = nullptr;
  50.         }
  51.         const esp_timer_create_args_t args = {
  52.             .callback = [](void* arg){
  53.                 auto* p = static_cast<std::string*>(arg);
  54.                 auto& app = Application::GetInstance();
  55.                 Protocol& proto = app.GetProtocol();
  56.                 if (app.GetDeviceState() == kDeviceStateListening) {
  57.                     std::string txt = "周期提醒:" + *p;
  58.                     proto.SendWakeWordDetected(txt);
  59.                 }
  60.                 ESP_LOGI(TAG, "周期闹钟:%s", p->c_str());
  61.             },
  62.             .arg = p_name,
  63.             .name = "periodic_alarm"
  64.         };
  65.         esp_timer_create(&args, &periodic_alarm_);
  66.         esp_timer_start_periodic(periodic_alarm_, period_sec * 1000000ULL);
  67.         return true;
  68. });
  69. /* --------------- 再给一个「停止闹钟」工具 --------------- */
  70. AddTool("self.send.stop_periodic_alarm",
  71.     "停止周期提醒",
  72.     PropertyList(),
  73.     [this](const PropertyList&) -> ReturnValue {
  74.         if (periodic_alarm_) {
  75.             esp_timer_stop(periodic_alarm_);
  76.             esp_timer_delete(periodic_alarm_);
  77.             periodic_alarm_ = nullptr;
  78.             ESP_LOGI(TAG, "周期闹钟已停止");
  79.             return true;
  80.         }
  81.         ESP_LOGW(TAG, "没有正在运行的周期闹钟");
  82.         return false;
  83. });      
复制代码
行空板k10——小智语音闹钟、光感智能主动播报图6

光感智报

        1.“光照强度”工具实现“单次读取 + 周期阈值报警 + 随时停”的功能。(”mcp_server.cc"文件)
       实现如下功能:
  • 即时查询:单条指令返回当前 lux 数值,无后台任务,零资源占用。
  • 阈值巡航:10 s 周期自动采样,仅当 lux < 用户阈值才触发播报,节省 CPU 与喇叭占用。
  • 语音打断:沿用 SendWakeWordDetected 机制,可中断当前识别/音乐,立即播出告警,延迟 < 200 ms。
  • 一键静默:停止周期定时器并释放堆内存,防止重复注册与泄漏。
  • 容错设计:strtol 全量检查非法字符、负值、超大值;采样失败仅放弃本轮,不崩溃、不误报。
  • 读取温湿度值:获取板载温湿度传感器数据。

  1. AddTool("self.get_temp_humid",
  2.         "获取板载温湿度传感器数据",
  3.         PropertyList(), [&board](const PropertyList &properties) -> ReturnValue
  4.         {
  5.             std::string temp_humid = board.get_temp_humid_sensor(); // 实时取
  6.             ESP_LOGI(TAG, "mcp调用%s", temp_humid.c_str());
  7.          return temp_humid;
复制代码
       2.传感器初始化(df_k10_board.cc)
       上电即自检 → 失败自动回退 → 3 s 周期回调 → 全程零阻塞,为后续语音播报/联动控制提供实时、干净、防抖的温湿度+光照数据流。
  1.    void InitializeAht20Sensor()
  2.     {
  3.         // 初始化传感器
  4.         aht20_sensor_ = new Aht20Sensor(i2c_bus_);
  5.         esp_err_t err = aht20_sensor_->Initialize();
  6.         if (err != ESP_OK)
  7.         {
  8.             ESP_LOGE(TAG, "Failed to initialize AHT20 sensor (err=0x%x)", err);
  9.             return;
  10.         }
  11.         // 设置温湿度数据回调
  12.         aht20_sensor_->SetAht20SensorCallback([this](float temp, float hum)
  13.         {
  14.            temperature_ = temp;
  15.            humidity_ = hum;
  16.            //ESP_LOGI(TAG, "Temperature: %.2f C, Humidity: %.2f %%", temp, hum);
  17.         });
  18.         // 启动周期性读取(每3秒一次)
  19.         err = aht20_sensor_->StartReading(3000);
  20.         if (err != ESP_OK)
  21.         {
  22.             ESP_LOGE(TAG, "Failed to start periodic readings (err=0x%x)", err);
  23.         }
  24.     }
  25.     void InitializeLtr303Sensor()
  26.     {
  27.         // 初始化光照传感器(假设设备地址为0x23)
  28.         ltr303_sensor_ = new Ltr3xxSensor(i2c_bus_, LTR329_I2CADDR_DEFAULT);
  29.         esp_err_t err = ltr303_sensor_->Initialize();
  30.         if (err != ESP_OK)
  31.         {
  32.             ESP_LOGE(TAG, "Failed to initialize LTR303 sensor (err=0x%x)", err);
  33.             return;
  34.         }
  35.         // 配置传感器参数
  36.         ltr303_sensor_->setGain(LTR3XX_GAIN_4);
  37.         ltr303_sensor_->setIntegrationTime(LTR3XX_INTEGTIME_50);
  38.         ltr303_sensor_->setMeasurementRate(LTR3XX_MEASRATE_50);
  39.         ltr303_sensor_->SetLtr3xxSensorCallback([this](uint16_t visible, uint16_t IR)
  40.         {
  41.              visible_ = visible;
  42.              IR_ = IR;
  43.         });
  44.         // 启动周期性读取(3秒间隔)
  45.         err = ltr303_sensor_->StartReading(3000);
  46.         if (err != ESP_OK)
  47.         {
  48.             ESP_LOGE(TAG, "Failed to start periodic readings (err=0x%x)", err);
  49.         }
  50.     }
复制代码
       3.格式化播报(df_k10_board.cc)
       用 64 B 栈缓冲、零动态申请、零 I²C 阻塞,给上层语音工具提供‘即拿即播’的安全字符串。
  1.    std::string get_temp_humid_sensor() override//写虚函数重写时,永远加上 override——让编译器帮你检查,而不是在运行时才发现调错了函数。虚函数 = 用 virtual 修饰的成员函数,让 C++ 在运行时根据“真实对象类型”来调用对应版本,从而实现面向对象的多态。
  2.     {
  3.         if (aht20_sensor_)
  4.         {
  5.             float temp, hum;
  6.             aht20_sensor_->GetLastMeasurement(&temp, &hum);
  7.             char text[64];
  8.             snprintf(text, sizeof(text), "温度:%.1f°C湿度:%.1f%%", temp, hum);//snprintf = “带安全锁的 printf”,把格式化字符串安全地装进已知大小的字符数组里——写 C/C++ 嵌入式代码,用它就对了。
  9.             ESP_LOGI(TAG, "要播报的数据信息 %s", text);
  10.             return std::string(text);
  11.         }
  12.         return "Sensor not available";
  13.     }
  14.     std::string get_als_sensor_num() override
  15.     {
  16.         if (ltr303_sensor_)
  17.         {
  18.             // uint16_t visible, IR;
  19.             // ltr303_sensor_->readBothChannels(visible, IR);
  20.             // return "可见光数值为:"+ std::to_string(visible) + "LUX, 红外线为:" + std::to_string(IR);
  21.             char text[32];
  22.             snprintf(text, sizeof(text), "%d", visible_);
  23.             return std::string(text);
  24.         }
  25.         return "Sensor not available";
  26.     }
  27.     std::string get_als_sensor_str() override
  28.     {
  29.         if (ltr303_sensor_)
  30.         {
  31.             // uint16_t visible, IR;
  32.             // ltr303_sensor_->readBothChannels(visible, IR);
  33.             // return "可见光数值为:"+ std::to_string(visible) + "LUX, 红外线为:" + std::to_string(IR);
  34.             char text[32];
  35.             snprintf(text, sizeof(text), "光照:%dlux", visible_);
  36.             return std::string(text);
  37.         }
  38.         return "Sensor not available";
  39.     }
  40. };
复制代码
4.“传感器虚接口·缺省返回N/A”(board.h)
       基类先给所有传感器接口一个“N/A”备胎,保证就算派生类没实现,上层语音工具也不会拿到空字符串而崩溃。
  1. /* 传感器接口:默认返回 N/A,派生类可 override */
  2.     virtual std::string get_temp_humid_sensor() { return "N/A"; }
  3.     virtual std::string get_als_sensor_num()        { return "N/A"; }
  4.     virtual std::string get_als_sensor_str()        { return "N/A"; }
复制代码
行空板k10——小智语音闹钟、光感智能主动播报图7

【演示视频】


【附件文件】
       1.mcp服务文件附件
下载附件mcp_server.zip
       2.温湿度传感器文件附件
下载附件aht20.zip
       3.行空板k10开发板文件
下载附件df_k10_board.zip
【往期项目】
       1.基于 ESP-IDF 和行空板 K10 的智能风扇与 LED 灯带控制系统
       2.行空板K10打造智能舵机云台与拍照识别系统
       3.Esp32-S3AI智能摄像头——查询车票路径规划
       4.小智接入coze 实现智能体自由



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail