行空板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]