本帖最后由 云天 于 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)
- }),
- [this](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(),
- [this](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([this](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([this](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[64];
- 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[32];
- 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[32];
- 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"; }
复制代码
【演示视频】
【附件文件】
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 实现智能体自由
|