零知ESP32-S3 部署AI小智 2.1,继电器和音量控制以及页面展示音量
项目基于零知ESP32-S3部署 AI小智 2.0版本,新增 控制继电器和按键控制音量,以及屏幕显示当前音量;
目录
Q3:编译报错:invalid use of incomplete type 'lv_timer_t' {aka 'struct lv_timer_t'}?
✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 “配置环境” 转移到 “创意实现”,极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
项目概述
项目基于零知ESP32-S3部署 AI小智 2.0版本,新增 控制继电器和按键控制音量,以及屏幕显示当前音量;
项目难点及解决方案
问题描述:音量百分数展示不影响原来的布局,且两秒后隐藏
解决方案:
布局方面:
将原来emotion_bar_从status_bar_中分离开来,单独用emotion_bar_展示,和status_bar_平级且呈垂直排列 ;新建volume_label_和notification_label_,volume_label_用于展示音量,notification_label_设置空文本用来占位,以保证volume_label_、status_label_和notification_label_在水平轴上呈等距分布
效果方面:
用LVGL9.x版本的定时器 lv_timer_t 实现,确保多次点击后效果不丢失
一、系统接线部分
1.1 硬件清单
| 组件名称 | 型号规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | 零知ESP32-S3 | 1 | 主控制器 |
| 底板 | 小智扩展底板 | 1 | 零知ESP32-S3扩展板 |
| 扬声器 | 腔体喇叭扬声器3.2W | 1 | 音频输出 |
| LCD显示屏 | 2.0寸SPI ST7789 | 1 | 数据显示 |
| 灯泡 | 白炽灯 | 1 | 灯光展示 |
| 继电器 | 一路5V继电器 | 1 | 继电器控制 |
| 杜邦线 | 母对母、公对母 | 若干 | 系统连接 |
1.2 接线方案表
AI 小智1.0版本中有介绍扩展板、喇叭和显示屏的连接方式,包括代码下载和验证,不清楚的可以跳转查看,这里只提及新添加功能,根据代码中的引脚定义,硬件接线方案如下:
| ESP32-S3引脚 | 连接模块 | 功能描述 | 代码定义 |
|---|---|---|---|
| IO17 | 继电器IN口 | 控制继电器吸合 | GPIO_NUM_17 |
| IO38 | 扩展板右上1按键 | 控制音量+ |
|
| IO39 | 扩展板右上2按键 | 控制音量- | GPIO_NUM_39 |
1.3 具体接线图
请注意:图中这款继电器的供电电压需要5V,除了线要接对,开发板的供电电压也要达到5V。普通锂电池电压不够,实物连接时建议用充电宝代替,尽量不要用电脑连接Type-C作为供电时测试;
1.4 接线实物图
1.5 ESP32-S3引脚使用状态表

二、安装与使用部分
2.1 开源平台-输入 小智 并搜索-代码下载自动打开
2.2 连接-验证-上传
三、代码讲解部分
3.1 状态栏调整
//创建状态栏 status_bar_
status_bar_ = lv_obj_create(container_);
lv_obj_set_size(status_bar_, LV_HOR_RES, 25); // 高度固定为 25px
//// 背景色、文字色设置
lv_obj_set_style_radius(status_bar_, 0, 0);
lv_obj_set_style_bg_color(status_bar_, current_theme_.background, 0);
lv_obj_set_style_text_color(status_bar_, current_theme_.text, 0);
//新建表情栏 emotion_bar_,从status_bar_分离且同级
emotion_bar_ = lv_obj_create(container_);
lv_obj_set_size(emotion_bar_, LV_HOR_RES, 75); // 高度固定为 75px
//// 统一背景颜色
lv_obj_set_style_radius(emotion_bar_, 0, 0);
lv_obj_set_style_bg_color(emotion_bar_, current_theme_.background, 0);
lv_obj_set_style_text_color(emotion_bar_, current_theme_.text, 0);
// 设置状态栏的子元素之间均分剩余空间(两端贴边)
lv_obj_set_flex_align(status_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// 设置表情栏居中对齐
lv_obj_set_flex_align(emotion_bar_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
//新建 volume_label_,用于展示音量
volume_label_ = lv_label_create(status_bar_);
lv_label_set_text(volume_label_, "");
lv_obj_set_style_text_color(volume_label_, current_theme_.text, 0);
lv_obj_set_style_text_font(volume_label_, &font_puhui_16_4, 0);
lv_obj_set_flex_grow(volume_label_, 0); // 不拉伸
//状态标签 status_label_
status_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(status_label_, 0); // 不拉伸
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_color(status_label_, current_theme_.text, 0);
lv_label_set_text(status_label_, "初始化");
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_LEFT, 0);
//新建通知标签 notification_label_,暂无内容,用于对称布局
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(notification_label_, 0); // 不拉伸
lv_label_set_long_mode(notification_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_color(notification_label_, current_theme_.text, 0);
lv_label_set_text(notification_label_, "");
// 表情标签 emotion_label_
lv_obj_t* center_container = lv_obj_create(emotion_bar_);
lv_obj_set_flex_grow(center_container, 1); // 占据剩余空间
lv_obj_set_style_bg_opa(center_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(center_container, 0, 0);
lv_obj_set_style_pad_all(center_container, 0, 0);
lv_obj_set_width(center_container, LV_SIZE_CONTENT);
lv_obj_set_height(center_container, LV_SIZE_CONTENT);
emotion_label_ = lv_label_create(center_container);
lv_obj_set_style_text_font(emotion_label_, &font_awesome_30_4, 0);
lv_obj_set_style_text_color(emotion_label_, current_theme_.text, 0);
// lv_label_set_text(emotion_label_, FONT_AWESOME_AI_CHIP);
lv_obj_center(emotion_label_); // 水平垂直居中
LV_FLEX_ALIGN_SPACE_BETWEEN 会让状态栏中的标签贴边,平均分布在状态栏中
3.2 显示音量逻辑
void Display::updateVolumeLabel(const char* text) {
if (text && strlen(text) > 0) {
lv_label_set_text_fmt(volume_label_, "音量: %s%%", text);
} else {
lv_label_set_text(volume_label_, ""); // 完全清空
}
}
void Display::ShowSound(const char* volume) {
lvgl_port_lock(0);
// 先取消已存在的定时器
if (volume_timer_) {
lv_timer_del(volume_timer_);
volume_timer_ = nullptr;
}
// 更新显示
updateVolumeLabel(volume);
// 创建新的定时器(仅当有内容时)
if (volume && strlen(volume) > 0) {
volume_timer_ = lv_timer_create(hideVolumeTask, 2000, this);
lv_timer_set_repeat_count(volume_timer_, 1); // 只触发一次
}
lvgl_port_unlock();
}
// 静态定时器回调
void Display::hideVolumeTask(lv_timer_t* timer) {
Display* self = static_cast<Display*>(lv_timer_get_user_data(timer));
lvgl_port_lock(0);
self->updateVolumeLabel(""); // 清空文本
self->volume_timer_ = nullptr;
lv_timer_del(timer);
lvgl_port_unlock();
}
设置两秒定时器,触发后清空volume_label_中的文本;重复调用ShowSound(),会先销毁旧定时器,避免重复触发清空;
3.3 继电器和按键的初始化及注册
//继电器硬件引脚配置逻辑
constexpr gpio_num_t krelayPin = GPIO_NUM_17;
// 音量+按钮 IO38
constexpr gpio_num_t kButtonInvolume = GPIO_NUM_38;
// 音量-按钮 IO39
constexpr gpio_num_t kButtonRevolume = GPIO_NUM_39;
//声明实体
std::shared_ptr<ai_vox::iot::Entity> g_relay_iot_entity;
// Relay
// 1.定义了继电器的属性
std::vector<ai_vox::iot::Property> relay_properties({
{
"state", // 属性名
"继电器开关状态", // 属性描述
ai_vox::iot::ValueType::kBool // 属性类型
},
// 添加更多属性
});
// 2.定义继电器功能
std::vector<ai_vox::iot::Function> relay_functions({
{"TurnOnrelay", // 功能名
"打开继电器", // 功能描述
{
// no parameters
}},
{"TurnOffrelay", // 功能名
"关闭继电器", // 功能描述
{
// 无参数
}},
// 添加更多功能
});
// 3.创建Relay实体
g_relay_iot_entity = std::make_shared<ai_vox::iot::Entity>("Relay", // name
"继电器", // description
std::move(relay_properties), // properties
std::move(relay_functions) // functions
);
// 4.初始化舵机实体的默认值
g_relay_iot_entity->UpdateState("state", false);
// 5.注册舵机实体到 AI Vox 引擎
ai_vox_engine.RegisterIotEntity(g_relay_iot_entity);
//创建 GPI38 按键设备,返回句柄,错误则终止程序
ESP_ERROR_CHECK(iot_button_new_gpio_device(&btn_cfg, &gpio_cfg1, &g_button_invulome_handle));
//创建 GPI39 按键设备,返回句柄,错误则终止程序
ESP_ERROR_CHECK(iot_button_new_gpio_device(&btn_cfg, &gpio_cfg2, &g_button_revulome_handle));
GPIO_NUM_38和GPIO_NUM_39这两个IO口在扩展板上面连接了按键,位于右上角,可以直接使用
3.4 处理事件
//音量+按键回调注册
ESP_ERROR_CHECK(iot_button_register_cb(
g_button_invulome_handle,
BUTTON_PRESS_DOWN,
nullptr,
[](void* button_handle, void* usr_data) {
printf("involume button pressed\n");
auto volume = g_audio_output_device->volume();
int new_volume = static_cast<int>(volume) + 1;
if (new_volume > 100) {
new_volume = 100;
}
printf("Speaker volume: %d\n", new_volume);
g_audio_output_device->set_volume(static_cast<uint8_t>(new_volume));
g_speaker_iot_entity->UpdateState("volume", new_volume);
g_display->ShowSound(std::to_string(new_volume).c_str());
},
nullptr));
......
//处理 Relay 设备指令
if (iot_message_event->name == "Relay") {
if (iot_message_event->function == "TurnOnrelay") {
printf("turn on relay\n");
digitalWrite(krelayPin, HIGH); // 硬件操作:开启
g_relay_iot_entity->UpdateState("state", true); // 更新 舵机 状态实体(关键:硬件状态同步)
} else if (iot_message_event->function == "TurnOffrelay") {
printf("turn off relay\n");
digitalWrite(krelayPin, LOW); // 硬件操作:关闭 舵机
g_relay_iot_entity->UpdateState("state", false); // 同步状态到实体
}
处理消息后注意同步状态,但随着设备重启,状态也会重置;代码中还定义了长按BUTTON_LONG_PRESS_HOLD对应功能,其他功能例如BUTTON_DOUBLE_CLICK可以自行拓展
四、项目结果演示
4.1 视频演示1 继电器控制白炽灯亮
零知ESP32-S3 语音控制 220V 灯泡
4.2 视频演示2 屏幕显示当前音量
零知ESP32-S3 按键控制“音量+”、“音量-”
五、常见问题解答
Q1:继电器的工作原理?
A:在下面是继电器的工作原理,VCC和GND分别接开发板的 +5V 和 GND ,IO输入默认低电平,公共端和常闭端相连(图1),高电平则通过继电器的吸合,让公共端和常开端相连(图2),因此公共端接入的电压是根据所控制的电器决定的,切忌随意选择电压进而导致电器损坏等后果;

Q2:继电器不工作?
A:了解了上面继电器的工作原理,在不接入公共端电压时,可以用万用表的“蜂鸣器”档,测试公共端在高低电平的情况下,跟上下“NC”和“NO”两个端口的通断情况,切换没问题再接入公共端测试;如果继电器吸合(切换)不成功,查看VCC和GND两端的电压是否达到继电器的供电电压(此项目用的5V供电继电器),或者重新换个继电器试试;
Q3:编译报错:invalid use of incomplete type 'lv_timer_t' {aka 'struct lv_timer_t'}?
A:项目用到的LVGL当前最新版本是9.2.2,完全隐藏了
lv_timer_t结构体成员,禁止直接访问 timer->user_data,尽量用同一个版本;
Q4:音量显示设置两秒后消失,但实际效果不佳?
A:确保每次点击仅有一个定时器,就算频繁点击也不会出现野指针和多次删除
lv_timer_del(volume_timer_); //先取消已存在的定时器
// 创建新的定时器(仅当有内容时)
if (volume && strlen(volume) > 0) {
volume_timer_ = lv_timer_create(hideVolumeTask, 2000, this); //设置定时器为两秒
lv_timer_set_repeat_count(volume_timer_, 1); // 只触发一次
}
更多推荐



所有评论(0)