目录

一、系统接线部分

1.1 硬件清单

1.2 接线方案表

1.3 具体接线图

1.4 接线实物图

1.5 ESP32-S3引脚使用状态表​编辑

二、安装与使用部分

三、代码讲解部分

3.1 状态栏调整

3.2 显示音量逻辑

3.3 继电器和按键的初始化及注册

3.4 处理事件

四、项目结果演示

4.1 视频演示1 继电器控制白炽灯亮   

4.2 视频演示2 屏幕显示当前音量 

五、常见问题解答

Q1:继电器的工作原理?

Q2:继电器不工作?

Q3:编译报错:invalid use of incomplete type 'lv_timer_t' {aka 'struct lv_timer_t'}?

Q4:音量显示设置两秒后消失,但实际效果不佳?


    ✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 “配置环境” 转移到 “创意实现”,极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!

✔访问零知实验室,获取更多实战项目和教程资源吧!

www.lingzhilab.com

项目概述

       项目基于零知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按键 控制音量+

GPIO_NUM_38

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);  // 只触发一次
  }

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐