云天 发表于 6 天前

行空板k10——小智语音闹钟、光感智能主动播报

本帖最后由 云天 于 2025-9-12 19:08 编辑

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

【语音闹钟】
      1.让小智设备在指定秒数后响一次或每隔固定秒数循环响的语音提醒,并可随时停掉循环。(”mcp_server.cc"文件)
       (1)把「未来某个时刻要做的事」登记到系统时钟里,然后 CPU 去睡觉,时间到了由中断帮你叫醒。
       esp_timer_create 把回调函数、参数、名字写进 RTOS 的定时器链表。
const esp_timer_create_args_t args = {
                  .callback = [](void* arg) {
                        auto* p = static_cast<std::string*>(arg);
                        auto& app = Application::GetInstance();
                        Protocol& protocol = app.GetProtocol();   // 正确拼写
                        if (app.GetDeviceState() == kDeviceStateListening) {
                            std::string content = "您定的闹钟时间到了,请您" + *p;
                            protocol.SendWakeWordDetected(content);
                        }
                        ESP_LOGI(TAG, "时间到了:%s", p->c_str());
                        delete p;          // 释放堆内存
                  },
                  .arg = p_name,
                  .name = "alarm_timer"
};       (2)一次性闹钟
esp_timer_create(&args, &once);
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)周期时钟
esp_timer_create(&args, &periodic_alarm_);
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 改动实现“即时打断 + 播报”。
AddTool("self.send.text",
            "当用户要**一次性**定时提醒时,调用此工具。seconde_from_now:闹钟多少秒以后响;alarm_name:时钟的描述(名字)",
            PropertyList({
                Property("seconde_from_now", kPropertyTypeInteger, 10, 0, 7200),
                Property("alarm_name", kPropertyTypeString)
            }),
            [](const PropertyList& properties) -> ReturnValue {
                int seconde_from_now = properties["seconde_from_now"].value<int>();
                std::string alarm_name = properties["alarm_name"].value<std::string>();
   
                /* 把 alarm_name 搬进堆,让定时器回调能拿到 */
                auto* p_name = new std::string(alarm_name);
   
                /* 一次性定时器 */
                esp_timer_handle_t once;
                const esp_timer_create_args_t args = {
                  .callback = [](void* arg) {
                        auto* p = static_cast<std::string*>(arg);
                        auto& app = Application::GetInstance();
                        Protocol& protocol = app.GetProtocol();   // 正确拼写
                        if (app.GetDeviceState() == kDeviceStateListening) {
                            std::string content = "您定的闹钟时间到了,请您" + *p;
                            protocol.SendWakeWordDetected(content);
                        }
                        ESP_LOGI(TAG, "时间到了:%s", p->c_str());
                        delete p;          // 释放堆内存
                  },
                  .arg = p_name,
                  .name = "alarm_timer"
                };
                esp_timer_create(&args, &once);
                esp_timer_start_once(once, seconde_from_now * 1000000ULL); // µs
                return true;
});
    /* --------------- 工具注册:周期闹钟 --------------- */
AddTool("self.send.periodic_alarm",
    "周期提醒:period_sec:间隔秒数;alarm_name:提醒内容",
    PropertyList({
      Property("period_sec", kPropertyTypeInteger, 5, 1, 3600),
      Property("alarm_name",kPropertyTypeString)
    }),
    (const PropertyList& props) -> ReturnValue {
      int period_sec = props["period_sec"].value<int>();
      auto* p_name   = new std::string(props["alarm_name"].value<std::string>());

      /* 如果旧闹钟还在,先停掉 */
      if (periodic_alarm_) {
            esp_timer_stop(periodic_alarm_);
            esp_timer_delete(periodic_alarm_);
            periodic_alarm_ = nullptr;
      }

      const esp_timer_create_args_t args = {
            .callback = [](void* arg){
                auto* p = static_cast<std::string*>(arg);
                auto& app = Application::GetInstance();
                Protocol& proto = app.GetProtocol();
                if (app.GetDeviceState() == kDeviceStateListening) {
                  std::string txt = "周期提醒:" + *p;
                  proto.SendWakeWordDetected(txt);
                }
                ESP_LOGI(TAG, "周期闹钟:%s", p->c_str());
            },
            .arg = p_name,
            .name = "periodic_alarm"
      };
      esp_timer_create(&args, &periodic_alarm_);
      esp_timer_start_periodic(periodic_alarm_, period_sec * 1000000ULL);
      return true;
});

/* --------------- 再给一个「停止闹钟」工具 --------------- */
AddTool("self.send.stop_periodic_alarm",
    "停止周期提醒",
    PropertyList(),
    (const PropertyList&) -> ReturnValue {
      if (periodic_alarm_) {
            esp_timer_stop(periodic_alarm_);
            esp_timer_delete(periodic_alarm_);
            periodic_alarm_ = nullptr;
            ESP_LOGI(TAG, "周期闹钟已停止");
            return true;
      }
      ESP_LOGW(TAG, "没有正在运行的周期闹钟");
      return false;
});      
【光感智报】
      1.“光照强度”工具实现“单次读取 + 周期阈值报警 + 随时停”的功能。(”mcp_server.cc"文件)
       实现如下功能:

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

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

       2.温湿度传感器文件附件

       3.行空板k10开发板文件

【往期项目】
       1.基于 ESP-IDF 和行空板 K10 的智能风扇与 LED 灯带控制系统
       2.行空板K10打造智能舵机云台与拍照识别系统
       3.Esp32-S3AI智能摄像头——查询车票路径规划
       4.小智接入coze 实现智能体自由



页: [1]
查看完整版本: 行空板k10——小智语音闹钟、光感智能主动播报