Compare commits

...

1 Commits

Author SHA1 Message Date
f9dc7d4861 feat: 触屏版迁移到按键版,两键实现全部交互功能
1. 按键驱动重构:GPIO中断+手动去抖 → iot_button组件(单击/双击/长按)
2. 新增key_nav按键导航管理器:上下文状态机 + Set界面焦点蓝色边框高亮
3. 移除所有触摸手势/点击事件(ScreenHome/ScreenImg/ScreenSet)
4. 应援灯颜色切换优化:DISPOFF→直接写GRAM→DISPON,消除分band刷新
5. 亮度调节按键化:BOOT +10% / KEY -10% / KEY长按退出
6. 休眠管理适配:按键唤醒统一由key_nav处理
7. 新增迁移总结文档 docs/touch-to-button-migration.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:57:14 +08:00
47 changed files with 4533 additions and 322 deletions

View File

@ -1,4 +1,18 @@
dependencies:
espressif/button:
component_hash: d0afa32f0e50d60bc0c6fc23f7eea98adc6b02cfe70b590bc52c23c506745287
dependencies:
- name: espressif/cmake_utilities
registry_url: https://components.espressif.com
require: private
version: '*'
- name: idf
require: private
version: '>=4.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 4.1.6
espressif/cmake_utilities:
component_hash: 351350613ceafba240b761b4ea991e0f231ac7a9f59a9ee901f751bddc0bb18f
dependencies:
@ -83,6 +97,7 @@ dependencies:
type: service
version: 8.3.11
direct_dependencies:
- espressif/button
- espressif/esp_jpeg
- espressif/esp_lcd_st77916
- espressif/esp_lcd_touch
@ -90,6 +105,6 @@ direct_dependencies:
- espressif/esp_lvgl_port
- idf
- lvgl/lvgl
manifest_hash: 2e5e8224c989c28cde3f3a2470ecd63548cf45b6263cdcb5a1550808138e5c02
manifest_hash: a0a3d185d974878ce463b89760b1fa5ac9edcd2f81a6f817ef246b7d1165dd7b
target: esp32c3
version: 2.0.0

View File

@ -0,0 +1,331 @@
# 触屏版 → 按键版迁移总结
> 适用于 ESP32-C3 / ESP32-S3 电子吧唧项目,从触摸屏交互迁移到两键物理按键交互。
## 一、迁移背景
取消触摸芯片以降低硬件成本,所有用户交互通过两个物理按键实现:
- **BOOT按键**GPIO9确认/执行/返回
- **KEY按键**GPIO8导航/切换
---
## 二、变更文件清单
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `main/button/button.c` | 重构 | GPIO中断+手动去抖 → iot_button 组件 |
| `main/button/include/button.h` | 重构 | 新增事件类型枚举,回调签名变更 |
| `main/key_nav/key_nav.c` | **新增** | 按键导航管理器(核心模块) |
| `main/key_nav/include/key_nav.h` | **新增** | 导航上下文和焦点状态枚举 |
| `main/main.c` | 修改 | 移除 boot_btn_handler集成 key_nav_init |
| `main/ui/screens/ui_ScreenHome.c` | 修改 | 移除手势事件,界面导航交由 key_nav |
| `main/ui/screens/ui_ScreenHome.h` | 修改 | 移除 ui_event_ScreenHome 声明 |
| `main/ui/screens/ui_ScreenImg.c` | 修改 | 移除手势/点击事件,保留 SCREEN_LOADED 事件 |
| `main/ui/screens/ui_ScreenSet.c` | 修改 | 移除手势/滑块/节能/删除点击事件,新增按键版颜色切换 |
| `main/ui/screens/ui_ScreenSet.h` | 修改 | 新增 flashlight_switch_color/restart_blink 声明 |
| `main/lcd/lcd.c` | 修改 | 新增 lcd_fill_color() 直接写 GRAM |
| `main/lcd/include/lcd.h` | 修改 | 新增 lcd_fill_color() 声明 |
| `main/sleep_mgr/sleep_mgr.c` | 修改 | 移除按键回调注册,交由 key_nav 统一处理 |
| `main/CMakeLists.txt` | 修改 | 添加 key_nav 源文件和头文件路径 |
| `main/idf_component.yml` | 修改 | 添加 `button: ">=3.2.0"` 依赖 |
| `sdkconfig` | 修改 | 新增 IoT Button 配置项 |
---
## 三、核心架构变更
### 3.1 按键驱动重构button模块
**触屏版**:自定义 GPIO 中断 + FreeRTOS 队列 + 手动去抖200ms时间戳判断
```c
// 旧方案
gpio_isr_handler_add(PIN_BTN_BOOT, gpio_isr_handler, (void *)PIN_BTN_BOOT);
xTaskCreate(btn_task, "btn_task", 3072, NULL, 5, NULL); // 队列消费任务
```
**按键版**ESP-IDF 官方 `iot_button` 组件,原生支持单击/双击/长按
```c
// 新方案
button_config_t btn_cfg = {
.long_press_time = 2000, // 长按2秒
.short_press_time = 0, // 使用默认180ms双击检测窗口
};
button_gpio_config_t gpio_cfg = {
.gpio_num = PIN_BTN_BOOT,
.active_level = 0, // 低电平有效
};
iot_button_new_gpio_device(&btn_cfg, &gpio_cfg, &boot_btn_handle);
iot_button_register_cb(boot_btn_handle, BUTTON_SINGLE_CLICK, NULL, boot_click_cb, NULL);
iot_button_register_cb(boot_btn_handle, BUTTON_DOUBLE_CLICK, NULL, boot_double_click_cb, NULL);
iot_button_register_cb(boot_btn_handle, BUTTON_LONG_PRESS_START, NULL, boot_long_press_cb, NULL);
```
**回调签名变更**
```c
// 旧版(只有按下事件)
typedef void (*btn_event_cb_t)(int gpio_num, void *usr_data);
// 新版(区分单击/双击/长按)
typedef enum {
BTN_EVT_CLICK,
BTN_EVT_DOUBLE_CLICK,
BTN_EVT_LONG_PRESS,
} btn_event_type_t;
typedef void (*btn_event_cb_t)(int gpio_num, btn_event_type_t event, void *usr_data);
```
**注册接口变更**
```c
// 旧版
button_on_boot_press(cb, usr_data);
button_on_key2_press(cb, usr_data);
// 新版
button_on_boot_event(cb, usr_data);
button_on_key2_event(cb, usr_data);
```
**idf_component.yml 依赖**
```yaml
dependencies:
button: ">=3.2.0"
```
### 3.2 新增按键导航管理器key_nav模块
这是本次迁移的**核心新增模块**,集中管理所有按键行为和界面导航逻辑。
**设计思想**
- 上下文状态机:根据当前界面/模式决定按键行为
- 焦点管理系统Set界面的图标选中和蓝色边框高亮
- 任务派发模式iot_button 回调在 esp_timer 上下文中执行(不能 vTaskDelay通过 xTaskCreate 派发到独立任务
**上下文枚举**
```c
typedef enum {
NAV_CTX_HOME, // Home界面
NAV_CTX_IMG, // Img界面正常浏览
NAV_CTX_IMG_DELETE, // Img界面删除模式
NAV_CTX_SET, // Set界面焦点导航
NAV_CTX_SET_BRIGHTNESS, // Set界面亮度调节模式
NAV_CTX_FLASHLIGHT, // 应援灯全屏模式
} nav_context_t;
```
**Set界面焦点枚举**
```c
typedef enum {
SET_FOCUS_NONE = -1, // 无选中
SET_FOCUS_LOW_POWER = 0,// 节能
SET_FOCUS_FLASHLIGHT, // 应援灯
SET_FOCUS_DELETE, // 删除
SET_FOCUS_BRIGHTNESS, // 亮度
SET_FOCUS_COUNT, // 焦点总数(用于循环)
} set_focus_item_t;
```
**任务派发模式**(关键模式,必须遵循):
```c
// iot_button 回调在 esp_timer 上下文中,不能 vTaskDelay
// 必须派发到独立 FreeRTOS 任务执行需要延时的操作
static void dispatch_task(TaskFunction_t func, const char *name)
{
xTaskCreate(func, name, 3072, NULL, 5, NULL);
}
static void boot_event_handler(int gpio_num, btn_event_type_t event, void *usr_data)
{
if (event == BTN_EVT_CLICK) {
dispatch_task(nav_task_home_boot_click, "h_boot"); // 派发到独立任务
}
}
```
**焦点高亮实现**
```c
#define FOCUS_BORDER_COLOR 0x2196F3 // Material Blue
#define FOCUS_BORDER_WIDTH 3
static void set_focus_border(int index) {
lv_obj_set_style_border_color(obj, lv_color_hex(FOCUS_BORDER_COLOR), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, FOCUS_BORDER_WIDTH, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(obj, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT);
}
```
### 3.3 各界面触摸事件移除
#### ScreenHome
- 移除 `ui_event_ScreenHome()` 函数下滑→Set、左滑/右滑→Img 手势)
- 移除 `lv_obj_add_event_cb(ui_ScreenHome, ui_event_ScreenHome, LV_EVENT_ALL, NULL)`
- 移除 `#include "ui_ScreenSet.h"` 依赖
#### ScreenImg
- 移除 `ui_event_ImageDel()`(删除按钮点击)
- 移除 `ui_event_ImageReturn()`(返回按钮点击)
- 移除所有手势事件上滑→Home、下滑→Set、左滑→下一张、右滑→上一张
- 保留 `LV_EVENT_SCREEN_LOADED` 事件(首次加载图片初始化)
- 保留 `should_show_container` 标志逻辑从Set删除图标进入时显示
- 移除 `lv_obj_add_event_cb` 对 ImageDel 和 ImageReturn 的注册
#### ScreenSet
- 移除 `ui_event_ScreenSet()`(上滑返回手势)
- 移除 `ui_event_SliderBrightness()`(滑块值变更事件)
- 移除 `ui_event_ImgLowPower()`(节能图标点击事件)
- 移除 `ui_event_ImgDelete()`(删除图标点击事件)
- 保留 `ui_event_ImgFlashlight()``LV_EVENT_CLICKED`(由 key_nav 通过 `lv_event_send` 触发)
- 移除 `lv_obj_add_event_cb` 对节能、删除、滑块、手势的注册
---
## 四、新增功能和优化
### 4.1 按键导航系统
| 界面 | BOOT单击 | KEY单击 | BOOT双击 | KEY长按 |
|------|----------|---------|----------|---------|
| Home | →Set | →Img | - | - |
| Img(浏览) | →Home | 下一张图 | →Home | - |
| Img(删除) | 确认删除 | 取消删除 | →Home | - |
| Set(无焦点) | →Home | 选中第一个 | →Home | - |
| Set(有焦点) | 激活功能 | 下一个焦点 | →Home | - |
| Set(亮度调节) | 亮度+10% | 亮度-10% | →Home | 退出调节 |
| 应援灯 | 切换颜色 | 退出→Set | →Home | - |
### 4.2 Set界面焦点蓝色边框高亮
- 用 LVGL `border` 样式实现焦点指示Material Blue #2196F3
- 焦点切换时自动清除旧边框、设置新边框
- 离开界面或返回 Home 自动清除所有边框
- 进入删除模式时ImageDel 控件也显示蓝色边框提示
### 4.3 应援灯颜色切换优化
触屏版直接修改 LVGL 样式LVGL 30行分band渲染导致从上到下的视觉刷新感。
按键版优化为 **LCD 硬件级切换**
```c
void flashlight_switch_color(void) {
lcd_disp_on_off(false); // DISPOFF 0x28LCD停止输出
lcd_fill_color(new_color); // 直接写GRAM绕过LVGL~35ms同步阻塞
lcd_disp_on_off(true); // DISPON 0x29LCD瞬间恢复GRAM已完整
}
```
新增 `lcd_fill_color()` 函数:
```c
// 绕过 LVGL 分band渲染直接用 esp_lcd_panel_draw_bitmap 写 GRAM
// 40行为单位DMA 内存,同步阻塞写入
void lcd_fill_color(uint32_t color_rgb) {
lv_color_t c = lv_color_hex(color_rgb); // RGB888→RGB565含byte swap
uint16_t *buf = heap_caps_malloc(LCD_WID * 40 * sizeof(uint16_t), MALLOC_CAP_DMA);
for (int y = 0; y < LCD_HIGH; y += 40) {
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, LCD_WID, y + lines, buf);
}
heap_caps_free(buf);
}
```
### 4.4 亮度调节按键化
触屏版通过滑块拖动调节。按键版改为:
- BOOT单击 +10%KEY单击 -10%
- 范围 10%~100%
- 同步更新滑块控件值和百分比标签
- KEY长按2秒退出调节模式
### 4.5 休眠管理适配
- 移除 sleep_mgr 中的按键回调注册(旧版在 sleep_mgr_init 中注册 KEY2 回调)
- 按键唤醒逻辑统一由 key_nav 处理:
```c
if (sleep_mgr_is_screen_off()) {
sleep_mgr_notify_activity(); // 唤醒屏幕
return; // 仅唤醒,不触发业务
}
sleep_mgr_notify_activity(); // 重置休眠计时器
```
### 4.6 main.c 简化
- 移除 ~50 行的 `boot_btn_handler()` 函数(手电筒退出+界面切换+亮度恢复逻辑)
- 所有按键处理逻辑集中到 key_nav 模块
- 初始化顺序调整:`button_init()``sleep_mgr_init()``key_nav_init()`
---
## 五、关键技术要点(迁移到 S3 时注意)
### 5.1 iot_button 状态机特性
- `BUTTON_SINGLE_CLICK``BUTTON_DOUBLE_CLICK` 互斥,不会同时触发
- 按键释放后等待 `short_press_time`默认180ms决定是单击还是双击
- `short_press_time` 控制双击检测窗口,不是去抖时间(去抖由 `CONFIG_BUTTON_DEBOUNCE_TICKS` 控制默认10ms
- 设为 0 表示使用默认值 180ms
### 5.2 iot_button 回调上下文
- 回调在 `esp_timer` 任务中执行
- **禁止在回调中调用 `vTaskDelay()`**,否则阻塞 esp_timer 任务,导致 LVGL tick 停止
- **解决方案**:通过 `xTaskCreate` 派发到独立 FreeRTOS 任务
### 5.3 LVGL 操作线程安全
- 修改 UI 必须加锁:`lvgl_port_lock(timeout_ms)` / `lvgl_port_unlock()`
- 建议超时 50-100ms避免死锁
- 永久等待用 `lvgl_port_lock(-1)`
### 5.4 手电筒退出时序
```
亮度→0 → flashlight_exit() → vTaskDelay(80ms) → 切换界面 → vTaskDelay(150ms) → 恢复亮度
```
- 80ms 等待 overlay 删除LVGL 需要 15+ 刷新周期)
- 150ms 等待新界面渲染完成
- 先切换界面再恢复亮度,避免用户看到旧界面
### 5.5 GPIO 引脚适配
S3 项目需要根据实际硬件修改 button.h 中的 GPIO 定义:
```c
#define PIN_BTN_BOOT 9 // 根据S3硬件原理图修改
#define PIN_BTN_KEY2 8 // 根据S3硬件原理图修改
```
### 5.6 sdkconfig 配置
新增 IoT Button 相关配置menuconfig 或直接修改 sdkconfig
```
CONFIG_BUTTON_PERIOD_TIME_MS=5
CONFIG_BUTTON_DEBOUNCE_TICKS=2
CONFIG_BUTTON_SHORT_PRESS_TIME_MS=180
CONFIG_BUTTON_LONG_PRESS_TIME_MS=1500
```
---
## 六、迁移步骤(适用于 ESP32-S3 项目)
1. **添加依赖**`idf_component.yml` 添加 `button: ">=3.2.0"`
2. **重构 button 模块**:替换 GPIO 中断为 iot_button修改回调签名
3. **新建 key_nav 模块**:复制 `main/key_nav/` 目录,根据 S3 项目的界面结构调整上下文枚举和任务函数
4. **移除触摸事件**:各 Screen 文件中移除手势事件函数和 `lv_obj_add_event_cb` 注册
5. **适配 main.c**:移除旧的按键处理函数,添加 `key_nav_init()` 调用
6. **适配 sleep_mgr**:移除按键回调注册,由 key_nav 统一处理唤醒
7. **添加 lcd_fill_color()**(如需应援灯颜色切换优化)
8. **更新 CMakeLists.txt**:添加 key_nav 源文件和头文件路径
9. **适配 GPIO 引脚**:根据 S3 硬件原理图修改按键 GPIO 定义
10. **编译测试**`idf.py build` 验证,逐个功能测试
---
## 七、资源变化
| 指标 | 触屏版 | 按键版 | 变化 |
|------|--------|--------|------|
| button 任务栈 | 3072Bbtn_task 队列消费) | 0iot_button 内部管理) | 节省 3KB |
| key_nav 任务栈 | 无 | 3072B每次按键临时创建执行完销毁 | 临时占用 |
| 触摸事件代码 | ~200 行(手势+点击回调) | 0 | 移除 |
| key_nav 代码 | 0 | ~530 行 | 新增 |
| iot_button 组件 | 无 | ~20KB Flash | 新增依赖 |

View File

@ -11,6 +11,7 @@ idf_component_register(
"./battery/battery.c"
"./button/button.c"
"./sleep_mgr/sleep_mgr.c"
"./key_nav/key_nav.c"
"./ui/ui.c"
"./ui/components/ui_comp_hook.c"
"./ui/screens/ui_ScreenHome.c"
@ -39,6 +40,7 @@ idf_component_register(
"./battery/include/"
"./button/include/"
"./sleep_mgr/include/"
"./key_nav/include/"
"./ui/"
"./ui/screens/"
# "./axis/include/"

View File

@ -1,19 +1,10 @@
#include "button.h"
#include "driver/gpio.h"
#include "iot_button.h"
#include "button_gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "BTN";
// 去抖间隔(微秒)
#define DEBOUNCE_US 200000
// 按键事件队列
static QueueHandle_t btn_evt_queue = NULL;
// 回调存储
typedef struct {
btn_event_cb_t cb;
@ -23,84 +14,101 @@ typedef struct {
static btn_cb_t boot_cb = {0};
static btn_cb_t key2_cb = {0};
// 去抖时间戳
static int64_t last_boot_us = 0;
static int64_t last_key2_us = 0;
// iot_button句柄
static button_handle_t boot_btn_handle = NULL;
static button_handle_t key2_btn_handle = NULL;
// GPIO中断服务函数ISR中不做耗时操作仅发送事件到队列
static void IRAM_ATTR gpio_isr_handler(void *arg)
// BOOT按键回调
static void boot_click_cb(void *arg, void *usr_data)
{
int gpio_num = (int)arg;
xQueueSendFromISR(btn_evt_queue, &gpio_num, NULL);
ESP_LOGI(TAG, "BOOT单击");
if (boot_cb.cb) boot_cb.cb(PIN_BTN_BOOT, BTN_EVT_CLICK, boot_cb.usr_data);
}
// 按键事件处理任务
static void btn_task(void *pvParameters)
static void boot_double_click_cb(void *arg, void *usr_data)
{
int gpio_num;
while (1) {
if (xQueueReceive(btn_evt_queue, &gpio_num, portMAX_DELAY)) {
int64_t now = esp_timer_get_time();
ESP_LOGI(TAG, "BOOT双击");
if (boot_cb.cb) boot_cb.cb(PIN_BTN_BOOT, BTN_EVT_DOUBLE_CLICK, boot_cb.usr_data);
}
if (gpio_num == PIN_BTN_BOOT) {
if (now - last_boot_us > DEBOUNCE_US) {
last_boot_us = now;
ESP_LOGI(TAG, "BOOT按键按下 (GPIO%d)", gpio_num);
if (boot_cb.cb) {
boot_cb.cb(gpio_num, boot_cb.usr_data);
}
}
} else if (gpio_num == PIN_BTN_KEY2) {
if (now - last_key2_us > DEBOUNCE_US) {
last_key2_us = now;
ESP_LOGI(TAG, "KEY2按键按下 (GPIO%d)", gpio_num);
if (key2_cb.cb) {
key2_cb.cb(gpio_num, key2_cb.usr_data);
}
}
}
}
}
static void boot_long_press_cb(void *arg, void *usr_data)
{
ESP_LOGI(TAG, "BOOT长按");
if (boot_cb.cb) boot_cb.cb(PIN_BTN_BOOT, BTN_EVT_LONG_PRESS, boot_cb.usr_data);
}
// KEY2按键回调
static void key2_click_cb(void *arg, void *usr_data)
{
ESP_LOGI(TAG, "KEY2单击");
if (key2_cb.cb) key2_cb.cb(PIN_BTN_KEY2, BTN_EVT_CLICK, key2_cb.usr_data);
}
static void key2_double_click_cb(void *arg, void *usr_data)
{
ESP_LOGI(TAG, "KEY2双击");
if (key2_cb.cb) key2_cb.cb(PIN_BTN_KEY2, BTN_EVT_DOUBLE_CLICK, key2_cb.usr_data);
}
static void key2_long_press_cb(void *arg, void *usr_data)
{
ESP_LOGI(TAG, "KEY2长按");
if (key2_cb.cb) key2_cb.cb(PIN_BTN_KEY2, BTN_EVT_LONG_PRESS, key2_cb.usr_data);
}
esp_err_t button_init(void)
{
btn_evt_queue = xQueueCreate(10, sizeof(int));
esp_err_t ret;
// 配置GPIO为输入模式内部上拉下降沿触发中断
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << PIN_BTN_BOOT) | (1ULL << PIN_BTN_KEY2),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
// 通用按键配置
button_config_t btn_cfg = {
.long_press_time = 2000, // 长按2秒
.short_press_time = 0, // 使用默认180ms双击检测窗口非去抖
};
gpio_config(&io_conf);
// 安装GPIO中断服务如果已安装则跳过
esp_err_t ret = gpio_install_isr_service(0);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "GPIO ISR服务安装失败");
// BOOT按键GPIO配置GPIO9低电平有效
button_gpio_config_t boot_gpio_cfg = {
.gpio_num = PIN_BTN_BOOT,
.active_level = 0,
};
ret = iot_button_new_gpio_device(&btn_cfg, &boot_gpio_cfg, &boot_btn_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "BOOT按键创建失败: %s", esp_err_to_name(ret));
return ret;
}
gpio_isr_handler_add(PIN_BTN_BOOT, gpio_isr_handler, (void *)PIN_BTN_BOOT);
gpio_isr_handler_add(PIN_BTN_KEY2, gpio_isr_handler, (void *)PIN_BTN_KEY2);
// KEY2按键GPIO配置GPIO8低电平有效
button_gpio_config_t key2_gpio_cfg = {
.gpio_num = PIN_BTN_KEY2,
.active_level = 0,
};
ret = iot_button_new_gpio_device(&btn_cfg, &key2_gpio_cfg, &key2_btn_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "KEY2按键创建失败: %s", esp_err_to_name(ret));
return ret;
}
// 按键处理任务
xTaskCreate(btn_task, "btn_task", 3072, NULL, 5, NULL);
// 注册BOOT按键事件第3参数event_args传NULL使用默认参数
iot_button_register_cb(boot_btn_handle, BUTTON_SINGLE_CLICK, NULL, boot_click_cb, NULL);
iot_button_register_cb(boot_btn_handle, BUTTON_DOUBLE_CLICK, NULL, boot_double_click_cb, NULL);
iot_button_register_cb(boot_btn_handle, BUTTON_LONG_PRESS_START, NULL, boot_long_press_cb, NULL);
ESP_LOGI(TAG, "按键初始化完成 (BOOT=GPIO%d, KEY2=GPIO%d)", PIN_BTN_BOOT, PIN_BTN_KEY2);
// 注册KEY2按键事件
iot_button_register_cb(key2_btn_handle, BUTTON_SINGLE_CLICK, NULL, key2_click_cb, NULL);
iot_button_register_cb(key2_btn_handle, BUTTON_DOUBLE_CLICK, NULL, key2_double_click_cb, NULL);
iot_button_register_cb(key2_btn_handle, BUTTON_LONG_PRESS_START, NULL, key2_long_press_cb, NULL);
ESP_LOGI(TAG, "按键初始化完成 (iot_button, BOOT=GPIO%d, KEY2=GPIO%d)", PIN_BTN_BOOT, PIN_BTN_KEY2);
return ESP_OK;
}
void button_on_boot_press(btn_event_cb_t cb, void *usr_data)
void button_on_boot_event(btn_event_cb_t cb, void *usr_data)
{
boot_cb.cb = cb;
boot_cb.usr_data = usr_data;
}
void button_on_key2_press(btn_event_cb_t cb, void *usr_data)
void button_on_key2_event(btn_event_cb_t cb, void *usr_data)
{
key2_cb.cb = cb;
key2_cb.usr_data = usr_data;

View File

@ -5,14 +5,21 @@
#define PIN_BTN_BOOT 9 // GPIO9 BOOT按键低电平有效
#define PIN_BTN_KEY2 8 // GPIO8 KEY2按键低电平有效
// 按键事件回调函数类型
typedef void (*btn_event_cb_t)(int gpio_num, void *usr_data);
// 按键事件类型
typedef enum {
BTN_EVT_CLICK, // 单击
BTN_EVT_DOUBLE_CLICK, // 双击
BTN_EVT_LONG_PRESS, // 长按(达到阈值时触发一次)
} btn_event_type_t;
// 初始化按键驱动GPIO中断 + 软件去抖)
// 按键事件回调函数类型
typedef void (*btn_event_cb_t)(int gpio_num, btn_event_type_t event, void *usr_data);
// 初始化按键驱动基于iot_button组件
esp_err_t button_init(void);
// 注册BOOT按键按下回调
void button_on_boot_press(btn_event_cb_t cb, void *usr_data);
// 注册BOOT按键事件回调(单击/双击/长按统一回调)
void button_on_boot_event(btn_event_cb_t cb, void *usr_data);
// 注册KEY2按键按下回调
void button_on_key2_press(btn_event_cb_t cb, void *usr_data);
// 注册KEY2按键事件回调(单击/双击/长按统一回调)
void button_on_key2_event(btn_event_cb_t cb, void *usr_data);

View File

@ -5,4 +5,5 @@ dependencies:
esp_lcd_st77916: "^2.0.2"
esp_lcd_touch: 1.1.2
esp_lcd_touch_cst816s: 1.1.0
esp_jpeg: 1.3.1
esp_jpeg: 1.3.1
button: ">=3.2.0"

View File

@ -0,0 +1,39 @@
#pragma once
#include "lvgl.h"
#include <stdbool.h>
#include <stdint.h>
// 当前界面/模式上下文
typedef enum {
NAV_CTX_HOME, // Home界面
NAV_CTX_IMG, // Img界面正常浏览
NAV_CTX_IMG_DELETE, // Img界面删除模式
NAV_CTX_SET, // Set界面焦点导航
NAV_CTX_SET_BRIGHTNESS, // Set界面亮度调节模式
NAV_CTX_FLASHLIGHT, // 应援灯全屏模式
} nav_context_t;
// Set界面焦点索引
typedef enum {
SET_FOCUS_NONE = -1, // 无选中
SET_FOCUS_LOW_POWER = 0,// 节能
SET_FOCUS_FLASHLIGHT, // 应援灯
SET_FOCUS_DELETE, // 删除
SET_FOCUS_BRIGHTNESS, // 亮度
SET_FOCUS_COUNT, // 焦点总数(用于循环)
} set_focus_item_t;
// 初始化按键导航管理器在button_init之后调用
void key_nav_init(void);
// 获取当前导航上下文
nav_context_t key_nav_get_context(void);
// 设置导航上下文(界面切换时调用)
void key_nav_set_context(nav_context_t ctx);
// 获取Set界面当前焦点
set_focus_item_t key_nav_get_set_focus(void);
// 重置Set界面焦点进入/离开Set时调用
void key_nav_reset_set_focus(void);

535
main/key_nav/key_nav.c Normal file
View File

@ -0,0 +1,535 @@
#include "key_nav.h"
#include "button.h"
#include "sleep_mgr.h"
#include "pages.h"
#include "esp_log.h"
#include "esp_lvgl_port.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "../ui/ui.h"
#include "../ui/screens/ui_ScreenSet.h"
#include "../ui/screens/ui_ScreenImg.h"
static const char *TAG = "KEY_NAV";
// 当前导航上下文
static nav_context_t current_ctx = NAV_CTX_HOME;
// Set界面焦点状态
static set_focus_item_t set_focus = SET_FOCUS_NONE;
// Set界面可聚焦的LVGL对象在Set界面init后有效
static lv_obj_t *set_focus_objects[SET_FOCUS_COUNT] = {NULL};
// 蓝色边框样式常量
#define FOCUS_BORDER_COLOR 0x2196F3 // Material Blue
#define FOCUS_BORDER_WIDTH 3
// ==================== Set界面焦点管理 ====================
// 更新焦点对象指针Set界面初始化后调用
static void update_focus_objects(void)
{
set_focus_objects[SET_FOCUS_LOW_POWER] = ui_ImgLowPower;
set_focus_objects[SET_FOCUS_FLASHLIGHT] = ui_ImgFlashlight;
set_focus_objects[SET_FOCUS_DELETE] = ui_ImgDelete;
set_focus_objects[SET_FOCUS_BRIGHTNESS] = ui_SliderBrightness;
}
// 清除所有焦点边框
static void clear_all_focus_borders(void)
{
if (!lvgl_port_lock(50)) return;
for (int i = 0; i < SET_FOCUS_COUNT; i++) {
if (set_focus_objects[i]) {
lv_obj_set_style_border_width(set_focus_objects[i], 0, LV_PART_MAIN | LV_STATE_DEFAULT);
}
}
lvgl_port_unlock();
}
// 设置指定对象的焦点边框
static void set_focus_border(int index)
{
if (index < 0 || index >= SET_FOCUS_COUNT) return;
if (!set_focus_objects[index]) return;
if (!lvgl_port_lock(50)) return;
lv_obj_set_style_border_color(set_focus_objects[index],
lv_color_hex(FOCUS_BORDER_COLOR),
LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(set_focus_objects[index],
FOCUS_BORDER_WIDTH,
LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(set_focus_objects[index],
LV_OPA_COVER,
LV_PART_MAIN | LV_STATE_DEFAULT);
lvgl_port_unlock();
}
// Set焦点移动到下一个
static void set_focus_next(void)
{
update_focus_objects();
clear_all_focus_borders();
// 从当前焦点移到下一个循环NONE→0→1→2→3→NONE
set_focus++;
if (set_focus >= SET_FOCUS_COUNT) {
set_focus = SET_FOCUS_NONE;
ESP_LOGI(TAG, "Set焦点无选中");
return;
}
set_focus_border(set_focus);
ESP_LOGI(TAG, "Set焦点%d", set_focus);
}
// ==================== 全局返回Home ====================
// 派发到独立任务执行iot_button回调在esp_timer中不能vTaskDelay
typedef struct {
int gpio_num;
btn_event_type_t event;
} nav_evt_t;
static void nav_task_go_home(void *arg)
{
ESP_LOGI(TAG, "双击BOOT返回Home");
// 如果手电筒活跃,先退出
if (flashlight_is_active()) {
uint8_t saved_br = flashlight_get_saved_brightness();
flashlight_exit();
vTaskDelay(pdMS_TO_TICKS(80));
// 切换到Home
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
lvgl_port_unlock();
}
vTaskDelay(pdMS_TO_TICKS(150));
pwm_set_brightness(saved_br);
} else {
// 隐藏Img删除容器如果在Img界面
if (lvgl_port_lock(100)) {
lv_obj_t *scr = lv_scr_act();
if (scr == ui_ScreenImg) {
ui_ScreenImg_hide_delete_container();
}
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
lvgl_port_unlock();
}
}
current_ctx = NAV_CTX_HOME;
set_focus = SET_FOCUS_NONE;
vTaskDelete(NULL);
}
// ==================== Home界面按键 ====================
static void nav_task_home_boot_click(void *arg)
{
// BOOT单击切换到Set
ESP_LOGI(TAG, "Home: BOOT单击→Set");
if (lvgl_port_lock(100)) {
ui_ScreenSet_set_previous(&ui_ScreenHome, &ui_ScreenHome_screen_init);
_ui_screen_change(&ui_ScreenSet, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenSet_screen_init);
lvgl_port_unlock();
}
current_ctx = NAV_CTX_SET;
set_focus = SET_FOCUS_NONE;
vTaskDelete(NULL);
}
static void nav_task_home_key_click(void *arg)
{
// KEY单击切换到Img
ESP_LOGI(TAG, "Home: KEY单击→Img");
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
lvgl_port_unlock();
}
current_ctx = NAV_CTX_IMG;
vTaskDelete(NULL);
}
// ==================== Img界面按键 ====================
static void nav_task_img_key_click(void *arg)
{
if (current_ctx == NAV_CTX_IMG_DELETE) {
// 删除模式KEY单击→取消删除
ESP_LOGI(TAG, "Img删除模式: KEY单击→取消");
if (lvgl_port_lock(100)) {
ui_ScreenImg_hide_delete_container();
lvgl_port_unlock();
}
current_ctx = NAV_CTX_IMG;
} else {
// 正常浏览KEY单击→下一张图片
ESP_LOGI(TAG, "Img: KEY单击→下一张");
const char *next = get_next_image();
if (next) {
if (lvgl_port_lock(100)) {
update_ui_ImgBle(next);
lvgl_port_unlock();
}
}
}
vTaskDelete(NULL);
}
static void nav_task_img_boot_click(void *arg)
{
if (current_ctx == NAV_CTX_IMG_DELETE) {
// 删除模式BOOT单击→确认删除
ESP_LOGI(TAG, "Img删除模式: BOOT单击→删除");
if (lvgl_port_lock(100)) {
if (delete_current_image()) {
const char *next = get_current_image();
if (next) {
update_ui_ImgBle(next);
}
}
// 删除后隐藏容器
ui_ScreenImg_hide_delete_container();
lvgl_port_unlock();
}
current_ctx = NAV_CTX_IMG;
} else {
// 正常浏览BOOT单击→返回Home
ESP_LOGI(TAG, "Img: BOOT单击→Home");
if (lvgl_port_lock(100)) {
ui_ScreenImg_hide_delete_container();
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
lvgl_port_unlock();
}
current_ctx = NAV_CTX_HOME;
}
vTaskDelete(NULL);
}
// ==================== Set界面按键 ====================
static void nav_task_set_key_click(void *arg)
{
if (current_ctx == NAV_CTX_SET_BRIGHTNESS) {
// 亮度调节模式KEY单击→亮度-10%
uint8_t br = pwm_get_brightness();
if (br > 10) br -= 10; else br = 10;
pwm_set_brightness(br);
// 同步更新UI
if (lvgl_port_lock(100)) {
if (ui_SliderBrightness) {
lv_slider_set_value(ui_SliderBrightness, br, LV_ANIM_OFF);
}
if (ui_LabelBrightness) {
char buf[8];
snprintf(buf, sizeof(buf), "%d%%", br);
lv_label_set_text(ui_LabelBrightness, buf);
}
lvgl_port_unlock();
}
ESP_LOGI(TAG, "亮度-10%%: %d%%", br);
} else {
// 焦点导航KEY单击→下一个图标
set_focus_next();
}
vTaskDelete(NULL);
}
static void nav_task_set_boot_click(void *arg)
{
if (current_ctx == NAV_CTX_SET_BRIGHTNESS) {
// 亮度调节模式BOOT单击→亮度+10%
uint8_t br = pwm_get_brightness();
if (br < 100) br += 10; else br = 100;
pwm_set_brightness(br);
if (lvgl_port_lock(100)) {
if (ui_SliderBrightness) {
lv_slider_set_value(ui_SliderBrightness, br, LV_ANIM_OFF);
}
if (ui_LabelBrightness) {
char buf[8];
snprintf(buf, sizeof(buf), "%d%%", br);
lv_label_set_text(ui_LabelBrightness, buf);
}
lvgl_port_unlock();
}
ESP_LOGI(TAG, "亮度+10%%: %d%%", br);
} else if (set_focus == SET_FOCUS_NONE) {
// 无焦点BOOT单击→返回前一界面
ESP_LOGI(TAG, "Set: BOOT单击→返回前一界面");
clear_all_focus_borders();
if (lvgl_port_lock(100)) {
// 尝试返回前一界面默认Home
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
lvgl_port_unlock();
}
current_ctx = NAV_CTX_HOME;
set_focus = SET_FOCUS_NONE;
} else {
// 有焦点BOOT单击→激活选中功能
ESP_LOGI(TAG, "Set: BOOT激活焦点 %d", set_focus);
switch (set_focus) {
case SET_FOCUS_LOW_POWER: {
// 切换节能模式
bool enabled = sleep_mgr_is_enabled();
sleep_mgr_set_enabled(!enabled);
// 更新imgbtn的checked状态
if (lvgl_port_lock(100)) {
if (ui_ImgLowPower) {
if (!enabled) {
lv_obj_add_state(ui_ImgLowPower, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(ui_ImgLowPower, LV_STATE_CHECKED);
}
// 强制重绘确保imgbtn图片立即切换
lv_obj_invalidate(ui_ImgLowPower);
}
lvgl_port_unlock();
}
ESP_LOGI(TAG, "节能模式: %s", !enabled ? "开启" : "关闭");
break;
}
case SET_FOCUS_FLASHLIGHT: {
// 进入应援灯模式
ESP_LOGI(TAG, "进入应援灯模式");
if (lvgl_port_lock(100)) {
clear_all_focus_borders();
lvgl_port_unlock();
}
// show_flashlight是static的需要通过模拟点击触发
// 直接调用外部接口
if (lvgl_port_lock(100)) {
lv_event_send(ui_ImgFlashlight, LV_EVENT_CLICKED, NULL);
lvgl_port_unlock();
}
current_ctx = NAV_CTX_FLASHLIGHT;
break;
}
case SET_FOCUS_DELETE: {
// 跳转Img删除模式
ESP_LOGI(TAG, "进入删除模式");
clear_all_focus_borders();
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
ui_ScreenImg_show_delete_container();
// 高亮确认删除图标提示用户BOOT=删除)
if (ui_ImageDel) {
lv_obj_set_style_border_color(ui_ImageDel,
lv_color_hex(FOCUS_BORDER_COLOR),
LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(ui_ImageDel,
FOCUS_BORDER_WIDTH,
LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(ui_ImageDel,
LV_OPA_COVER,
LV_PART_MAIN | LV_STATE_DEFAULT);
}
lvgl_port_unlock();
}
current_ctx = NAV_CTX_IMG_DELETE;
set_focus = SET_FOCUS_NONE;
break;
}
case SET_FOCUS_BRIGHTNESS: {
// 进入亮度调节模式并直接增加10%(无需二次按键激活)
current_ctx = NAV_CTX_SET_BRIGHTNESS;
uint8_t br = pwm_get_brightness();
if (br <= 90) br += 10; else br = 100;
pwm_set_brightness(br);
if (lvgl_port_lock(100)) {
if (ui_SliderBrightness) {
lv_slider_set_value(ui_SliderBrightness, br, LV_ANIM_OFF);
}
if (ui_LabelBrightness) {
char buf[8];
snprintf(buf, sizeof(buf), "%d%%", br);
lv_label_set_text(ui_LabelBrightness, buf);
}
lvgl_port_unlock();
}
ESP_LOGI(TAG, "进入亮度调节+10%%: %d%%", br);
break;
}
default:
break;
}
}
vTaskDelete(NULL);
}
// Set界面KEY长按退出亮度调节模式
static void nav_task_set_key_long(void *arg)
{
if (current_ctx == NAV_CTX_SET_BRIGHTNESS) {
ESP_LOGI(TAG, "长按KEY退出亮度调节");
current_ctx = NAV_CTX_SET;
// 焦点移到下一个(循环回无选中)
set_focus_next();
}
vTaskDelete(NULL);
}
// ==================== 应援灯模式按键 ====================
static void nav_task_flashlight_boot_click(void *arg)
{
// BOOT单击切换颜色
// flashlight_switch_color 使用 DISPOFF→直接写GRAM→DISPON无需操作PWM
ESP_LOGI(TAG, "应援灯: BOOT单击→切换颜色");
flashlight_switch_color();
// DISPOFF/DISPON已处理显示切换只需重启闪烁
flashlight_restart_blink();
vTaskDelete(NULL);
}
static void nav_task_flashlight_key_click(void *arg)
{
// KEY单击退出应援灯返回Set
ESP_LOGI(TAG, "应援灯: KEY单击→退出到Set");
uint8_t saved_br = flashlight_get_saved_brightness();
flashlight_exit();
vTaskDelay(pdMS_TO_TICKS(80));
if (lvgl_port_lock(100)) {
// 不切换界面Set界面还在下面
lvgl_port_unlock();
}
vTaskDelay(pdMS_TO_TICKS(100));
pwm_set_brightness(saved_br);
current_ctx = NAV_CTX_SET;
set_focus = SET_FOCUS_NONE;
vTaskDelete(NULL);
}
// ==================== 按键事件总调度 ====================
// 创建任务执行按键处理避免在esp_timer上下文中vTaskDelay
#define NAV_TASK_STACK 3072
static void dispatch_task(TaskFunction_t func, const char *name)
{
xTaskCreate(func, name, NAV_TASK_STACK, NULL, 5, NULL);
}
// BOOT按键事件处理
static void boot_event_handler(int gpio_num, btn_event_type_t event, void *usr_data)
{
// 先处理屏幕唤醒
if (sleep_mgr_is_screen_off()) {
sleep_mgr_notify_activity();
return; // 仅唤醒,不触发业务
}
// 通知活动(重置休眠计时器)
sleep_mgr_notify_activity();
// 双击BOOT全局返回Home
if (event == BTN_EVT_DOUBLE_CLICK) {
dispatch_task(nav_task_go_home, "go_home");
return;
}
// 单击BOOT根据当前上下文分发
if (event == BTN_EVT_CLICK) {
switch (current_ctx) {
case NAV_CTX_HOME:
dispatch_task(nav_task_home_boot_click, "h_boot");
break;
case NAV_CTX_IMG:
case NAV_CTX_IMG_DELETE:
dispatch_task(nav_task_img_boot_click, "i_boot");
break;
case NAV_CTX_SET:
case NAV_CTX_SET_BRIGHTNESS:
dispatch_task(nav_task_set_boot_click, "s_boot");
break;
case NAV_CTX_FLASHLIGHT:
dispatch_task(nav_task_flashlight_boot_click, "f_boot");
break;
}
}
}
// KEY2按键事件处理
static void key2_event_handler(int gpio_num, btn_event_type_t event, void *usr_data)
{
// 先处理屏幕唤醒
if (sleep_mgr_is_screen_off()) {
sleep_mgr_notify_activity();
return;
}
sleep_mgr_notify_activity();
if (event == BTN_EVT_CLICK) {
switch (current_ctx) {
case NAV_CTX_HOME:
dispatch_task(nav_task_home_key_click, "h_key");
break;
case NAV_CTX_IMG:
case NAV_CTX_IMG_DELETE:
dispatch_task(nav_task_img_key_click, "i_key");
break;
case NAV_CTX_SET:
case NAV_CTX_SET_BRIGHTNESS:
dispatch_task(nav_task_set_key_click, "s_key");
break;
case NAV_CTX_FLASHLIGHT:
dispatch_task(nav_task_flashlight_key_click, "f_key");
break;
}
} else if (event == BTN_EVT_LONG_PRESS) {
switch (current_ctx) {
case NAV_CTX_SET_BRIGHTNESS:
dispatch_task(nav_task_set_key_long, "s_klong");
break;
default:
break;
}
}
}
// ==================== 初始化 ====================
void key_nav_init(void)
{
current_ctx = NAV_CTX_HOME;
set_focus = SET_FOCUS_NONE;
button_on_boot_event(boot_event_handler, NULL);
button_on_key2_event(key2_event_handler, NULL);
ESP_LOGI(TAG, "按键导航管理器初始化完成");
}
nav_context_t key_nav_get_context(void)
{
return current_ctx;
}
void key_nav_set_context(nav_context_t ctx)
{
current_ctx = ctx;
}
set_focus_item_t key_nav_get_set_focus(void)
{
return set_focus;
}
void key_nav_reset_set_focus(void)
{
clear_all_focus_borders();
set_focus = SET_FOCUS_NONE;
}

View File

@ -8,4 +8,5 @@ void lvgl_lcd_init();
void touch_init();
void get_touch(uint16_t* touchx,uint16_t* touchy);
void lcd_clear_screen_black(void); // 清空LCD GRAM为黑色
void lcd_fill_color(uint32_t color_rgb); // 填充LCD GRAM为指定颜色同步阻塞
void lcd_disp_on_off(bool on_off); // LCD显示开关

View File

@ -373,6 +373,36 @@ void get_touch(uint16_t* touchx,uint16_t* touchy){
printf("%x\n",max);
}
// 填充LCD GRAM为指定颜色同步阻塞返回后GRAM完全更新
// 用于手电筒颜色切换绕过LVGL 30行分band渲染直接写GRAM消除从上到下的刷新层次感
void lcd_fill_color(uint32_t color_rgb) {
if (panel_handle == NULL) return;
// RGB888 → RGB565与LVGL格式一致含byte swap
lv_color_t c = lv_color_hex(color_rgb);
uint16_t color16 = c.full;
size_t band_pixels = LCD_WID * 40;
uint16_t *buf = heap_caps_malloc(band_pixels * sizeof(uint16_t), MALLOC_CAP_DMA);
if (!buf) {
ESP_LOGE(LCD_TAG, "lcd_fill_color: malloc failed");
return;
}
// 填充颜色只需一次所有band复用同一缓冲区
for (size_t i = 0; i < band_pixels; i++) {
buf[i] = color16;
}
// 逐band写入GRAMesp_lcd_panel_draw_bitmap为同步阻塞等DMA完成才返回
for (int y = 0; y < LCD_HIGH; y += 40) {
int lines = (y + 40 > LCD_HIGH) ? (LCD_HIGH - y) : 40;
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, LCD_WID, y + lines, buf);
}
heap_caps_free(buf);
}
// LCD显示开关封装供main.c在UI渲染完成后调用
void lcd_disp_on_off(bool on_off) {
if (panel_handle != NULL) {

View File

@ -21,6 +21,7 @@
#include "battery.h"
#include "button.h"
#include "sleep_mgr.h"
#include "key_nav.h"
#include "ui/ui.h"
#include "ui/screens/ui_ScreenSet.h"
#include "ui/screens/ui_ScreenImg.h"
@ -63,56 +64,7 @@
// return err;
// }
// BOOT按键按下处理低功耗模式下只唤醒屏幕正常模式下返回ScreenHome
void boot_btn_handler(int gpio_num, void *usr_data) {
// 检查屏幕是否关闭(低功耗模式)
bool screen_was_off = sleep_mgr_is_screen_off();
if (screen_was_off) {
// 低功耗模式下:只唤醒屏幕,不切换界面
ESP_LOGI("BTN_HANDLER", "BOOT按键低功耗模式仅唤醒屏幕");
sleep_mgr_notify_activity(); // 唤醒屏幕,恢复亮度
} else {
// 正常模式下返回ScreenHome界面
ESP_LOGI("BTN_HANDLER", "BOOT按键正常模式返回ScreenHome");
// 检查当前是否在ScreenImg界面如果是则先隐藏ContainerDle
lv_obj_t *current_screen = lv_scr_act();
if (current_screen == ui_ScreenImg) {
ui_ScreenImg_hide_delete_container();
ESP_LOGI("BTN_HANDLER", "从ScreenImg离开已隐藏ContainerDle");
}
// 先通知活动
sleep_mgr_notify_activity();
// 退出手电筒会降亮度到0但不恢复亮度
bool was_flashlight_active = flashlight_is_active();
uint8_t flashlight_saved_brightness = 0;
if (was_flashlight_active) {
flashlight_saved_brightness = flashlight_get_saved_brightness();
flashlight_exit(); // 降亮度到0删除overlay
ESP_LOGI("BTN_HANDLER", "手电筒已退出亮度降为0");
// 延迟80ms确保overlay完全删除至少15个刷新周期
vTaskDelay(pdMS_TO_TICKS(80));
}
// 切换到ScreenHome界面
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
ESP_LOGI("BTN_HANDLER", "已切换到ScreenHome界面");
// 如果刚从手电筒退出,延迟恢复亮度(等待界面渲染完成)
if (was_flashlight_active) {
// 延迟150ms等待Home界面完全渲染
// 不使用强制刷新让LVGL自动处理避免多层同时重绘
vTaskDelay(pdMS_TO_TICKS(150));
pwm_set_brightness(flashlight_saved_brightness);
ESP_LOGI("BTN_HANDLER", "亮度已恢复到%d%%", flashlight_saved_brightness);
}
}
}
// 按键处理已移至key_nav模块key_nav.c支持单击/双击/长按和多界面上下文导航
// 初始化I2C
esp_err_t i2c_init(void){
@ -273,14 +225,13 @@ void app_main(void)
ESP_ERROR_CHECK(button_init());
ESP_LOGI("MAIN", "12. 按键已初始化");
// 注册BOOT按键回调返回ScreenHome界面
extern void boot_btn_handler(int gpio_num, void *usr_data);
button_on_boot_press(boot_btn_handler, NULL);
ESP_LOGI("MAIN", "12.1 BOOT按键回调已注册");
// 初始化休眠管理器依赖按键和UI必须最后初始化
// 初始化休眠管理器依赖按键和UI
sleep_mgr_init();
ESP_LOGI("MAIN", "13. 休眠管理器已初始化");
ESP_LOGI("MAIN", "12.1 休眠管理器已初始化");
// 初始化按键导航管理器注册BOOT/KEY2所有按键回调必须在button_init和sleep_mgr_init之后
key_nav_init();
ESP_LOGI("MAIN", "13. 按键导航管理器已初始化");
ESP_LOGI("MAIN", "系统初始化完成成功!");// 系统初始化完成成功
// =====================================================================

View File

@ -1,5 +1,4 @@
#include "sleep_mgr.h"
#include "button.h"
#include "pages.h"
#include "esp_log.h"
#include "esp_timer.h"
@ -62,12 +61,6 @@ void sleep_mgr_notify_activity(void)
}
}
// 按键活动回调BOOT和KEY2共用
static void btn_activity_cb(int gpio_num, void *usr_data)
{
sleep_mgr_notify_activity();
}
// 关闭屏幕(熄屏进入低功耗)
static void screen_turn_off(void)
{
@ -162,9 +155,7 @@ void sleep_mgr_init(void)
{
last_activity_us = esp_timer_get_time();
// 注意BOOT按键由main.c的boot_btn_handler统一处理唤醒+退出手电筒+返回Home
// 这里只注册KEY2按键唤醒功能
button_on_key2_press(btn_activity_cb, NULL);
// 按键唤醒由key_nav模块统一处理这里不再注册按键回调
xTaskCreate(sleep_mgr_task, "sleep_mgr", 3072, NULL, 3, NULL);
ESP_LOGI(TAG, "休眠管理器初始化完成(超时=%ds", SLEEP_TIMEOUT_MS / 1000);

View File

@ -4,28 +4,10 @@
// Project name: Lcd_Pro
#include "../ui.h"
#include "ui_ScreenSet.h" // 引入ScreenSet的函数声明
lv_obj_t *ui_ScreenHome = NULL;lv_obj_t *ui_Label1 = NULL;lv_obj_t *ui_Image3 = NULL;lv_obj_t *ui_Arc1 = NULL;lv_obj_t *ui_LabelHome = NULL;
// event funtions
void ui_event_ScreenHome( lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM ) {
lv_indev_wait_release(lv_indev_get_act());
// 设置返回到Home界面
ui_ScreenSet_set_previous(&ui_ScreenHome, &ui_ScreenHome_screen_init);
_ui_screen_change( &ui_ScreenSet, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenSet_screen_init);
}
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_LEFT ) {
lv_indev_wait_release(lv_indev_get_act());
_ui_screen_change( &ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
}
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_RIGHT ) {
lv_indev_wait_release(lv_indev_get_act());
_ui_screen_change( &ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
}
}
// 手势事件已移除界面导航由key_nav按键模块统一管理
// build funtions
@ -76,7 +58,7 @@ lv_obj_set_style_text_color(ui_LabelHome, lv_color_hex(0x03F7F2), LV_PART_MAIN |
lv_obj_set_style_text_opa(ui_LabelHome, 255, LV_PART_MAIN| LV_STATE_DEFAULT);
lv_obj_set_style_text_font(ui_LabelHome, &lv_font_montserrat_26, LV_PART_MAIN| LV_STATE_DEFAULT);
lv_obj_add_event_cb(ui_ScreenHome, ui_event_ScreenHome, LV_EVENT_ALL, NULL);
// 手势事件回调已移除不再注册ui_event_ScreenHome
}

View File

@ -13,7 +13,6 @@ extern "C" {
// SCREEN: ui_ScreenHome
extern void ui_ScreenHome_screen_init(void);
extern void ui_ScreenHome_screen_destroy(void);
extern void ui_event_ScreenHome( lv_event_t * e);
extern lv_obj_t *ui_ScreenHome;
extern lv_obj_t *ui_Label1;
extern lv_obj_t *ui_Image3;

View File

@ -4,9 +4,8 @@
// Project name: Lcd_Pro
#include "../ui.h"
#include "ui_ScreenSet.h" // 引入ScreenSet的函数声明
#include "../../pages/include/pages.h" // 引入图片管理函数
#include "esp_log.h" // 用于日志输出
#include "esp_log.h"
extern void init_spiffs_image_list(void);
extern void update_ui_ImgBle(const char *img_name);
@ -41,101 +40,33 @@ void ui_ScreenImg_hide_delete_container(void) {
}
}
// ImageReturn 点击事件:隐藏容器
void ui_event_ImageReturn(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if (event_code == LV_EVENT_CLICKED) {
ui_ScreenImg_hide_delete_container();
}
}
// 触摸点击事件已移除,删除/返回操作由key_nav按键模块统一管理
// ImageDel 点击事件:删除当前图片
void ui_event_ImageDel(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if (event_code == LV_EVENT_CLICKED) {
// 删除当前图片
if (delete_current_image()) {
// 删除成功,隐藏容器
ui_ScreenImg_hide_delete_container();
// 获取下一张图片(内部已更新索引)
const char *next_img = get_current_image();
if (next_img) {
// 显示下一张图片
update_ui_ImgBle(next_img);
} else {
// 没有图片了,可以显示提示信息
// 这里暂不处理,保持当前界面
}
} else {
// 删除失败,仍然隐藏容器
ui_ScreenImg_hide_delete_container();
}
}
}
// event funtions
// 界面加载事件:首次进入时初始化图片列表
void ui_event_ScreenImg( lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
// 界面加载完成事件:首次进入时才初始化图片
if ( event_code == LV_EVENT_SCREEN_LOADED ) {
if (first_load) {
first_load = false;
if ( event_code == LV_EVENT_SCREEN_LOADED ) {
if (first_load) {
first_load = false;
init_spiffs_image_list();
const char *first_img = get_current_image();
if (first_img) {
update_ui_ImgBle(first_img);
} else {
ESP_LOGI("ScreenImg", "SPIFFS无可用图片显示默认UI图片");
}
}
// 初始化图片列表
init_spiffs_image_list();
// 获取第一张可用图片
const char *first_img = get_current_image();
if (first_img) {
// 显示第一张SPIFFS图片
update_ui_ImgBle(first_img);
// 检查是否需要显示 ContainerDle从Set删除图标进入时
if (should_show_container) {
should_show_container = false;
} else {
// 没有图片保持显示UI资源图片 ui_img_s1_png
ESP_LOGI("ScreenImg", "SPIFFS无可用图片显示默认UI图片");
ui_ScreenImg_hide_delete_container();
}
}
// 每次加载界面时检查是否需要显示 ContainerDle
// 只有从 ScreenSet 点击 ImgDelete 时 should_show_container 才为 true
if (should_show_container) {
// 需要显示,保持显示状态(已在 ui_ScreenImg_show_delete_container 中显示)
should_show_container = false; // 清除标志(下次进入默认不显示)
} else {
// 不需要显示,确保隐藏
ui_ScreenImg_hide_delete_container();
}
}
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_TOP ) {
lv_indev_wait_release(lv_indev_get_act());
// 离开界面前隐藏容器
ui_ScreenImg_hide_delete_container();
_ui_screen_change( &ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
}
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM ) {
lv_indev_wait_release(lv_indev_get_act());
// 离开界面前隐藏容器
ui_ScreenImg_hide_delete_container();
// 设置返回到Img界面
ui_ScreenSet_set_previous(&ui_ScreenImg, &ui_ScreenImg_screen_init);
_ui_screen_change( &ui_ScreenSet, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenSet_screen_init);
}
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_LEFT ) {
lv_indev_wait_release(lv_indev_get_act());
const char *next_img = get_next_image();
if(next_img) {
update_ui_ImgBle(next_img);
}
}
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_RIGHT ) {
lv_indev_wait_release(lv_indev_get_act());
const char *prev_img = get_prev_image();
if(prev_img) {
update_ui_ImgBle(prev_img);
}
}
// 手势事件已移除界面导航由key_nav按键模块统一管理
}
// build funtions
@ -190,10 +121,7 @@ lv_obj_set_align( ui_ImageReturn, LV_ALIGN_CENTER );
lv_obj_add_flag( ui_ImageReturn, LV_OBJ_FLAG_CLICKABLE ); // 可点击
lv_obj_clear_flag( ui_ImageReturn, LV_OBJ_FLAG_SCROLLABLE );
// 添加点击事件回调
lv_obj_add_event_cb(ui_ImageDel, ui_event_ImageDel, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_ImageReturn, ui_event_ImageReturn, LV_EVENT_ALL, NULL);
// 触摸点击回调已移除,仅保留界面加载事件用于首次图片初始化
lv_obj_add_event_cb(ui_ScreenImg, ui_event_ScreenImg, LV_EVENT_ALL, NULL);
// 注意:不在此处加载图片,延迟到 LV_EVENT_SCREEN_LOADED 事件中加载

View File

@ -7,6 +7,7 @@
#include "ui_ScreenImg.h" // 用于调用 ui_ScreenImg_show_delete_container
#include "../../pages/include/pages.h"
#include "../../sleep_mgr/include/sleep_mgr.h"
#include "../../lcd/include/lcd.h" // lcd_fill_color
#include "esp_lvgl_port.h" // LVGL锁机制
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@ -238,7 +239,57 @@ static void show_flashlight(void) {
pwm_set_brightness(100);
}
// ImgFlashlight点击事件显示手电筒
// 按键版颜色切换由key_nav在独立任务中调用
// 使用LCD硬件DISPOFF/DISPON + 直接GRAM写入彻底消除颜色切换的视觉刷新感
// DISPOFF: LCD控制器停止从GRAM输出像素硬件级不依赖背光
// 直接写GRAM: 绕过LVGL 30行分band渲染管线同步阻塞写入
// DISPON: LCD控制器瞬间恢复显示此时GRAM已是完整新颜色
// 总黑屏时间约35ms人眼几乎无法感知
void flashlight_switch_color(void) {
if (!flashlight_overlay) return;
if (lvgl_port_lock(-1)) {
// 停止闪烁和淡入淡出定时器
if (flashlight_timer) {
lv_timer_del(flashlight_timer);
flashlight_timer = NULL;
}
if (fade_timer) {
lv_timer_del(fade_timer);
fade_timer = NULL;
}
// LCD硬件关闭显示DISPOFF 0x28停止从GRAM输出像素到面板
lcd_disp_on_off(false);
// 更新LVGL样式保持内部状态一致
flashlight_color_index = (flashlight_color_index + 1) % FLASHLIGHT_COLOR_COUNT;
lv_obj_set_style_bg_color(flashlight_overlay,
lv_color_hex(flashlight_color_values[flashlight_color_index]),
0);
// 直接写GRAM同步阻塞~35ms期间LCD不显示任何内容
lcd_fill_color(flashlight_color_values[flashlight_color_index]);
// LCD硬件恢复显示DISPON 0x29瞬间显示新GRAM内容
lcd_disp_on_off(true);
lvgl_port_unlock();
}
}
// 步骤2重启闪烁定时器颜色切换渲染完成后调用
void flashlight_restart_blink(void) {
if (!flashlight_overlay) return;
if (lvgl_port_lock(100)) {
flashlight_bright = true;
flashlight_timer = lv_timer_create(flashlight_blink_timer_cb, 500, NULL);
lvgl_port_unlock();
}
}
// 触摸点击/手势事件已移除所有交互由key_nav按键模块统一管理
// show_flashlight() 通过 lv_event_send(ui_ImgFlashlight, LV_EVENT_CLICKED) 由key_nav触发
static void ui_event_ImgFlashlight(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
@ -246,54 +297,6 @@ static void ui_event_ImgFlashlight(lv_event_t *e) {
}
}
// ImgDelete 点击事件:跳转到 ScreenImg 并显示删除容器
static void ui_event_ImgDelete(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
// 跳转到 ScreenImg 界面
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
// 显示删除容器
ui_ScreenImg_show_delete_container();
}
}
// event funtions
void ui_event_ScreenSet( lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_TOP ) {
lv_indev_wait_release(lv_indev_get_act());
// 返回到之前的界面如果没有记录则默认返回Home
if (previous_screen && previous_screen_init) {
_ui_screen_change(previous_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, previous_screen_init);
} else {
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
}
}
}
void ui_event_SliderBrightness( lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);lv_obj_t * target = lv_event_get_target(e);
if ( event_code == LV_EVENT_VALUE_CHANGED) {
int32_t val = lv_slider_get_value(target);
if (val < 10) { val = 10; lv_slider_set_value(target, 10, LV_ANIM_OFF); }
pwm_set_brightness((uint8_t)val);
_ui_slider_set_text_value( ui_LabelBrightness, target, "", "%");
}
}
// ImgLowPower点击事件切换休眠模式
void ui_event_ImgLowPower( lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if ( event_code == LV_EVENT_VALUE_CHANGED) {
lv_obj_t * target = lv_event_get_target(e);
// checked=true时显示s12(休眠模式)false时显示s11(正常模式)
bool checked = lv_obj_has_state(target, LV_STATE_CHECKED);
sleep_mgr_set_enabled(checked);
}
}
// build funtions
void ui_ScreenSet_screen_init(void)
@ -439,11 +442,9 @@ lv_obj_set_style_text_color(ui_LabelPowerLevel, lv_color_hex(0xFFFFFF), LV_PART_
lv_obj_set_style_text_opa(ui_LabelPowerLevel, 255, LV_PART_MAIN| LV_STATE_DEFAULT);
lv_obj_set_style_text_font(ui_LabelPowerLevel, &lv_font_montserrat_20, LV_PART_MAIN| LV_STATE_DEFAULT);
lv_obj_add_event_cb(ui_ImgLowPower, ui_event_ImgLowPower, LV_EVENT_ALL, NULL);
// 仅保留手电筒的CLICKED事件由key_nav通过lv_event_send触发
lv_obj_add_event_cb(ui_ImgFlashlight, ui_event_ImgFlashlight, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_ImgDelete, ui_event_ImgDelete, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_SliderBrightness, ui_event_SliderBrightness, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_ScreenSet, ui_event_ScreenSet, LV_EVENT_ALL, NULL);
// 触摸手势/滑块/节能/删除事件已移除由key_nav按键模块管理
}

View File

@ -13,7 +13,6 @@ extern "C" {
// SCREEN: ui_ScreenSet
extern void ui_ScreenSet_screen_init(void);
extern void ui_ScreenSet_screen_destroy(void);
extern void ui_event_ScreenSet( lv_event_t * e);
extern void ui_ScreenSet_set_previous(lv_obj_t **screen, void (*init_func)(void)); // 设置返回的界面
extern lv_obj_t *ui_ScreenSet;
extern lv_obj_t *ui_GlobalContainer;
@ -22,7 +21,6 @@ extern lv_obj_t *ui_ImgLowPower;
extern lv_obj_t *ui_ImgFlashlight;
extern lv_obj_t *ui_ImgDelete;
extern lv_obj_t *ui_ContainerCentral;
extern void ui_event_SliderBrightness( lv_event_t * e);
extern lv_obj_t *ui_SliderBrightness;
extern lv_obj_t *ui_ImgSun;
extern lv_obj_t *ui_LabelBrightness;
@ -35,6 +33,8 @@ extern lv_obj_t *ui_LabelPowerLevel;
extern void flashlight_exit(void); // 退出手电筒模式(不恢复亮度)
extern bool flashlight_is_active(void); // 查询手电筒是否激活
extern uint8_t flashlight_get_saved_brightness(void); // 获取手电筒模式前保存的亮度值
extern void flashlight_switch_color(void); // 按键版颜色切换(停定时器+黑屏+换色)
extern void flashlight_restart_blink(void); // 重启闪烁定时器
#ifdef __cplusplus
} /*extern "C"*/

View File

@ -0,0 +1 @@
d0afa32f0e50d60bc0c6fc23f7eea98adc6b02cfe70b590bc52c23c506745287

View File

@ -0,0 +1,220 @@
# ChangeLog
## v4.1.6 - 2026-02-09
### Fix:
* Added error checking for `gpio_config` in `iot_button_new_gpio_device`
* Fixed issue where button cannot be recognized after restart when button is held down during restart. [#654](https://github.com/espressif/esp-iot-solution/issues/654)
## v4.1.5 - 2025-12-3
### Fix:
* Fixed the unreasonable function name `iot_button_get_ticks_time` and renamed it to `iot_button_get_pressed_time`
## v4.1.4 - 2025-10-08
### Fix:
* Fixed requires in CMake for IDF6.
## v4.1.3 - 2025-04-11
### Fix:
* Added initialization for gpio_config. [!485](https://github.com/espressif/esp-iot-solution/pull/485)
## v4.1.2 - 2025-03-24
### Fix:
* fix incorrect long press start and release check.
## v4.1.1 - 2025-03-13
### Improve:
* include stdbool.h before using bool
## v4.1.0 - 2025-02-28
### Improve:
* Update the version of dependent cmake_utilities to *
## v4.0.0 - 2025-1-9
### Enhancements:
* Use the factory pattern to reduce the build size.
* Change the state machine to use enumerated values.
### Break change:
* Standardize the return value of the iot_button interface to esp_err_t.
* Remove support for the old ADC driver.
* Modify the callback registration interface to:
```c
esp_err_t iot_button_register_cb(button_handle_t btn_handle, button_event_t event, button_event_args_t *event_args, button_cb_t cb, void *usr_data);
```
* Modify the callback unregistration interface to:
```c
esp_err_t iot_button_unregister_cb(button_handle_t btn_handle, button_event_t event, button_event_args_t *event_args);
```
## v3.5.0 - 2024-12-27
### Enhancements:
* Add config to disable gpio button internal pull resistor.
## v3.4.1 - 2024-12-6
### Fix:
* Fix the issue where `BUTTON_LONG_PRESS_START` is not triggered when the polling interval exceeds 20ms.
* Remove the `BUTTON_LONG_PRESS_TOLERANCE_MS` configuration option.
## v3.4.0 - 2024-10-22
### Enhancements:
* Supports a maximum button polling interval of 500ms.
* Fixed a potential counter overflow issue.
### Break change:
* The return value of `iot_button_get_ticks_time` has been changed from `uint16_t` to `uint32_t`.
## v3.3.2 - 2024-8-28
### Enhancements:
* Support macro CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP in power save mode.
* Supports retrieving and printing the string corresponding to a button event.
* Fixed the bug where the event was not assigned to `BUTTON_LONG_PRESS_START` before the `BUTTON_LONG_PRESS_START` event occurred.
## v3.3.1 - 2024-8-8
### Enhancements:
* Add Button Event **BUTTON_PRESS_END**.
## v3.3.0 - 2024-8-7
### Enhancements:
* Add Callback **button_power_save_cb_t** to support enter power save manually.
* Increase the maximum polling interval supported by the button from 20ms to 50ms.
## v3.2.3 - 2024-7-2
* Fixed the issue where the GPIO button in low-power mode continuously woke up the CPU after being pressed, causing abnormal power consumption.
## v3.2.2 - 2024-6-17
* Fix the compilation error for chips that do not support ADC.
## v3.2.1 - 2024-6-17
### bugfix
- Fixed ignored ADC button tied to GND. thanks `demianzenkov` for the fix.
## v3.2.0 - 2023-11-13
### Enhancements:
* The power consumption of GPIO buttons is lower during light sleep mode.
## v3.1.3 - 2023-11-13
* Resolved issue 'ADC_ATTEN_DB_11 is deprecated'.
## v3.1.2 - 2023-10-24
### bugfix
* Fixed a bug where iot_button_delete feature crashes for custom button
## v3.1.1 - 2023-10-18
### bugfix
* Fixed a bug where multiple callbacks feature crashes for BUTTON_MULTIPLE_CLICK
## v3.1.0 - 2023-10-9
### Enhancements:
* Support matrix keypad
## v3.0.1 - 2023-9-1
### Enhancements:
* Resolves bug for iot_button_unregister_event function returned error when reallocating with 0 byte.
* Update Test cases to test iot_button_unregister_event_cb
* Add api iot_button_stop & iot_button_resume for power save.
## v3.0.0 - 2023-8-15
### Enhancements:
* Add support to register multiple callbacks for a button_event
* Update iot_button_unregister_cb, to unregister all the callbacks for that event
* Add iot_button_unregister_event to unregister specific callbacks of that event
* Add iot_button_count_event to return number of callbacks registered for the event.
* Update iot_button_count_cb, to return sum of number of registered callbacks.
* Add support for Long press on specific time
* Add iot_button_register_event, which takes specific event_config_t data as input.
* Add BUTTON_LONG_PRESS_UP to trigger callback at the latest time of button release
* Update BUTTON_LONG_PRESS_START to trigger callback as the time passes for long_press.
* Add support to trigger callback for specified number of clicks.
## v2.5.6 - 2023-8-22
### bugfix
* Fixed a bug where the Serial trigger interval in button_long_press_hold event fires at an incorrect time
## v2.5.5 - 2023-8-3
* Add modify api which can change long_press_time and short_press_time
## v2.5.4 - 2023-7-27
### Enhancements:
* Add test apps and ci auto test
## v2.5.3 - 2023-7-26
### Enhancements:
* `repeat` defined in struct button_dev_t is reset to 0 after event `BUTTON_PRESS_REPEAT_DONE`
## v2.5.2 - 2023-7-18
### Enhancements:
* Set "event" member to BUTTON_PRESS_REPEAT before calling the BUTTON_PRESS_REPEAT callback
## v2.5.1 - 2023-3-14
### Enhancements:
* Update doc and code specification
* Avoid overwriting callback by @franz-ms-muc in #252
## v2.5.0 - 2023-2-1
### Enhancements:
* Support custom button
* Add BUTTON_PRESS_REPEAT_DONE event

View File

@ -0,0 +1 @@
{"version":"1.0","algorithm":"sha256","created_at":"2026-02-18T14:13:45.576284+00:00","files":[{"path":"CHANGELOG.md","size":5564,"hash":"25e3c60e848e3852ade35ab4643fbf09084a24faf00c824d9473e710e551ed39"},{"path":"CMakeLists.txt","size":694,"hash":"f249b726151e2d1b325ed7f57125d4b7512f8c4960bdcbf89fa9c542c1996fb8"},{"path":"Kconfig","size":1385,"hash":"5ea358f4e061a732c3c0d565826d18dcd3fc393a0fe67a3c317ceec2f669f68b"},{"path":"README.md","size":1729,"hash":"adc2c93639fabed0e77ff75b209c13f37bb97a5c09fe0b9d3688376faeda1735"},{"path":"button_adc.c","size":12504,"hash":"a7854da4177e851f6f3ac88294614e2856c8d75489069d12e8d47cac00fe624d"},{"path":"button_gpio.c","size":6278,"hash":"cd087192cddc026f5eb24cd7bff9e93a5428fbf8b27d8028ef869d846a0618f9"},{"path":"button_matrix.c","size":3616,"hash":"8a2316485a31c1d40b7e662a1f7fd86cf9c85bdfa670d290c164f5f349616e81"},{"path":"idf_component.yml","size":521,"hash":"5b54dee47213f21c72dfdf2de0c458fce56d03f867553686e419841cc84866e2"},{"path":"iot_button.c","size":27670,"hash":"d04002d274b2670e687b6a41f87a58690697874368661ca6c9c86bec7767feb7"},{"path":"license.txt","size":11358,"hash":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"include/button_adc.h","size":2487,"hash":"5d33d87d329aa854d0879d276f8cf0bf5f8afa17b3b485a66edb0f63c9f9d24a"},{"path":"include/button_gpio.h","size":2174,"hash":"9c46c0929b449dc751eaaa128deff18dde32370242a1214c59fba9b277408129"},{"path":"include/button_matrix.h","size":3244,"hash":"5b95aa72eb47cfa3f1d603317e844d8c823b97175a6cf577857014548bf0e5ee"},{"path":"include/button_types.h","size":1758,"hash":"1b956e32616cc8e397a263afc0b6e1d8d555335fc9c0c342d8c46ae556065179"},{"path":"include/iot_button.h","size":7874,"hash":"bc602c9199bedfa37ed32cfd6a52ea16f3d50d6c94b4f029641e49acdaae2107"},{"path":"interface/button_interface.h","size":771,"hash":"7fc7b7c596a9fe4e42cfe1529f484345757ef4c7b6b46ada5040ecf60da051ad"},{"path":"test_apps/CMakeLists.txt","size":350,"hash":"234fd5c4b8c16494d8169c1490c649d23306e4e20f08ae14b128cd56c17893d5"},{"path":"test_apps/pytest_button.py","size":755,"hash":"c5e633c4175f5d6475f1a10cb6fb800629dc82faf86bc6058ac4b90c6e3104d4"},{"path":"test_apps/sdkconfig.defaults","size":213,"hash":"9a34a6cb08c49ec24007587e0c5d492f44b5a862d9c0f583cf9f6f643669b564"},{"path":"test_apps/main/CMakeLists.txt","size":319,"hash":"47424e5a240820d7500fd781fde76251c8d6c0f6b0731746361b2e7ffdb228bb"},{"path":"test_apps/main/adc_button_test.c","size":5089,"hash":"a4c1ae51c024504b505ebb819ff18149926a7c1174b8e771c43c02a709af680f"},{"path":"test_apps/main/auto_test.c","size":10263,"hash":"c13e39223314b73ba73de55dbea73048835fff09bc805c580af5f3534f26b290"},{"path":"test_apps/main/button_test_main.c","size":1342,"hash":"841b79a2a6bef5382e8abd325927031c049522bc1731e56aebf95cd8ea01a17f"},{"path":"test_apps/main/custom_button_test.c","size":3816,"hash":"e06d21ebfd46727cc52cc52cced2115818aa586f4d3d813e6eef6a2c389e2ac7"},{"path":"test_apps/main/gpio_button_test.c","size":7194,"hash":"bf415ed691c44eb3c322413c8cf8628b8e2db810e3677612afac54a203432de0"},{"path":"test_apps/main/matrix_button_test.c","size":2949,"hash":"33bc629d59e853e2c6f1280f5d2e97bddf3796d7d1c261bd2c1ae08eb13896e8"}]}

View File

@ -0,0 +1,22 @@
set(PRIVREQ esp_timer)
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.3")
set(REQ esp_driver_gpio)
else()
set(REQ driver)
endif()
set(SRC_FILES "button_gpio.c" "iot_button.c" "button_matrix.c")
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.0")
list(APPEND REQ esp_adc)
if(CONFIG_SOC_ADC_SUPPORTED)
list(APPEND SRC_FILES "button_adc.c")
endif()
endif()
idf_component_register(SRCS ${SRC_FILES}
INCLUDE_DIRS include interface
REQUIRES ${REQ}
PRIV_REQUIRES ${PRIVREQ})
include(package_manager)
cu_pkg_define_version(${CMAKE_CURRENT_LIST_DIR})

View File

@ -0,0 +1,55 @@
menu "IoT Button"
config BUTTON_PERIOD_TIME_MS
int "BUTTON PERIOD TIME (MS)"
range 2 500
default 5
help
"Button scan interval"
config BUTTON_DEBOUNCE_TICKS
int "BUTTON DEBOUNCE TICKS"
range 1 7
default 2
help
"One CONFIG_BUTTON_DEBOUNCE_TICKS equal to CONFIG_BUTTON_PERIOD_TIME_MS"
config BUTTON_SHORT_PRESS_TIME_MS
int "BUTTON SHORT PRESS TIME (MS)"
range 50 800
default 180
config BUTTON_LONG_PRESS_TIME_MS
int "BUTTON LONG PRESS TIME (MS)"
range 500 5000
default 1500
config BUTTON_LONG_PRESS_HOLD_SERIAL_TIME_MS
int "BUTTON LONG_PRESS_HOLD SERIAL TIME (MS)"
range 2 1000
default 20
help
"Long press hold Serial trigger interval"
config ADC_BUTTON_MAX_CHANNEL
int "ADC BUTTON MAX CHANNEL"
range 1 5
default 3
help
"Maximum number of channels for ADC buttons"
config ADC_BUTTON_MAX_BUTTON_PER_CHANNEL
int "ADC BUTTON MAX BUTTON PER CHANNEL"
range 1 10
default 8
help
"Maximum number of buttons per channel"
config ADC_BUTTON_SAMPLE_TIMES
int "ADC BUTTON SAMPLE TIMES"
range 1 4
default 1
help
"Number of samples per scan"
endmenu

View File

@ -0,0 +1,42 @@
[![Component Registry](https://components.espressif.com/components/espressif/button/badge.svg)](https://components.espressif.com/components/espressif/button)
# Component: Button
[Online documentation](https://docs.espressif.com/projects/esp-iot-solution/en/latest/input_device/button.html)
After creating a new button object by calling function `button_create()`, the button object can create press events, every press event can have its own callback.
List of supported events:
* Button pressed
* Button released
* Button pressed repeat
* Button press repeat done
* Button single click
* Button double click
* Button multiple click
* Button long press start
* Button long press hold
* Button long press up
* Button Press end
![](https://dl.espressif.com/AE/esp-iot-solution/button_3.3.1.svg)
There are three ways this driver can handle buttons:
1. Buttons connected to standard digital GPIO
2. Multiple buttons connected to single ADC channel
3. Matrix keyboard employs multiple GPIOs for operation.
4. Custom button connect to any driver
The component supports the following functionalities:
1. Creation of an unlimited number of buttons, accommodating various types simultaneously.
2. Multiple callback functions for a single event.
3. Allowing customization of the consecutive key press count to any desired number.
4. Facilitating the setup of callbacks for any specified long-press duration.
5. Support power save mode (Only for gpio button)
## Add component to your project
Please use the component manager command `add-dependency` to add the `button` to your project's dependency, during the `CMake` step the component will be downloaded automatically
```
idf.py add-dependency "espressif/button=*"
```

View File

@ -0,0 +1,327 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_check.h"
#include "esp_timer.h"
#include "esp_idf_version.h"
#include "soc/soc_caps.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "button_adc.h"
#include "button_interface.h"
static const char *TAG = "adc_button";
#define DEFAULT_VREF 1100
#define NO_OF_SAMPLES CONFIG_ADC_BUTTON_SAMPLE_TIMES //Multisampling
/*!< Using atten bigger than 6db by default, it will be 11db or 12db in different target */
#define DEFAULT_ADC_ATTEN (ADC_ATTEN_DB_6 + 1)
#define ADC_BUTTON_WIDTH SOC_ADC_RTC_MAX_BITWIDTH
#define ADC_BUTTON_CHANNEL_MAX SOC_ADC_MAX_CHANNEL_NUM
#define ADC_BUTTON_ATTEN DEFAULT_ADC_ATTEN
#define ADC_BUTTON_MAX_CHANNEL CONFIG_ADC_BUTTON_MAX_CHANNEL
#define ADC_BUTTON_MAX_BUTTON CONFIG_ADC_BUTTON_MAX_BUTTON_PER_CHANNEL
// ESP32C3 ADC2 it has been deprecated.
#if (SOC_ADC_PERIPH_NUM >= 2) && !CONFIG_IDF_TARGET_ESP32C3
#define ADC_UNIT_NUM 2
#else
#define ADC_UNIT_NUM 1
#endif
typedef struct {
uint16_t min;
uint16_t max;
} button_data_t;
typedef struct {
uint8_t channel;
uint8_t is_init;
button_data_t btns[ADC_BUTTON_MAX_BUTTON]; /* all button on the channel */
uint64_t last_time; /* the last time of adc sample */
} btn_adc_channel_t;
typedef enum {
ADC_NONE_INIT = 0,
ADC_INIT_BY_ADC_BUTTON,
ADC_INIT_BY_USER,
} adc_init_info_t;
typedef struct {
adc_init_info_t is_configured;
adc_cali_handle_t adc_cali_handle;
adc_oneshot_unit_handle_t adc_handle;
btn_adc_channel_t ch[ADC_BUTTON_MAX_CHANNEL];
uint8_t ch_num;
} btn_adc_unit_t;
typedef struct {
btn_adc_unit_t unit[ADC_UNIT_NUM];
} button_adc_t;
typedef struct {
button_driver_t base;
adc_unit_t unit_id;
uint32_t ch;
uint32_t index;
} button_adc_obj;
static button_adc_t g_button = {0};
static int find_unused_channel(adc_unit_t unit_id)
{
for (size_t i = 0; i < ADC_BUTTON_MAX_CHANNEL; i++) {
if (0 == g_button.unit[unit_id].ch[i].is_init) {
return i;
}
}
return -1;
}
static int find_channel(adc_unit_t unit_id, uint8_t channel)
{
for (size_t i = 0; i < ADC_BUTTON_MAX_CHANNEL; i++) {
if (channel == g_button.unit[unit_id].ch[i].channel) {
return i;
}
}
return -1;
}
static bool adc_calibration_init(adc_unit_t unit, adc_atten_t atten, adc_cali_handle_t *out_handle)
{
adc_cali_handle_t handle = NULL;
esp_err_t ret = ESP_ERR_NOT_SUPPORTED;
bool calibrated = false;
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
if (!calibrated) {
ESP_LOGI(TAG, "calibration scheme version is %s", "Curve Fitting");
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = unit,
.atten = atten,
.bitwidth = ADC_BUTTON_WIDTH,
};
ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
if (ret == ESP_OK) {
calibrated = true;
}
}
#endif
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
if (!calibrated) {
ESP_LOGI(TAG, "calibration scheme version is %s", "Line Fitting");
adc_cali_line_fitting_config_t cali_config = {
.unit_id = unit,
.atten = atten,
.bitwidth = ADC_BUTTON_WIDTH,
};
ret = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
if (ret == ESP_OK) {
calibrated = true;
}
}
#endif
*out_handle = handle;
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Calibration Success");
} else if (ret == ESP_ERR_NOT_SUPPORTED || !calibrated) {
ESP_LOGW(TAG, "eFuse not burnt, skip software calibration");
} else if (ret == ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG, "Calibration not supported");
} else {
ESP_LOGE(TAG, "Invalid arg or no memory");
}
return calibrated;
}
static bool adc_calibration_deinit(adc_cali_handle_t handle)
{
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
if (adc_cali_delete_scheme_curve_fitting(handle) == ESP_OK) {
return true;
}
#endif
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
if (adc_cali_delete_scheme_line_fitting(handle) == ESP_OK) {
return true;
}
#endif
return false;
}
esp_err_t button_adc_del(button_driver_t *button_driver)
{
button_adc_obj *adc_btn = __containerof(button_driver, button_adc_obj, base);
ESP_RETURN_ON_FALSE(adc_btn->ch < ADC_BUTTON_CHANNEL_MAX, ESP_ERR_INVALID_ARG, TAG, "channel out of range");
ESP_RETURN_ON_FALSE(adc_btn->index < ADC_BUTTON_MAX_BUTTON, ESP_ERR_INVALID_ARG, TAG, "button_index out of range");
int ch_index = find_channel(adc_btn->unit_id, adc_btn->ch);
ESP_RETURN_ON_FALSE(ch_index >= 0, ESP_ERR_INVALID_ARG, TAG, "can't find the channel");
g_button.unit[adc_btn->unit_id].ch[ch_index].btns[adc_btn->index].max = 0;
g_button.unit[adc_btn->unit_id].ch[ch_index].btns[adc_btn->index].min = 0;
/** check button usage on the channel*/
uint8_t unused_button = 0;
for (size_t i = 0; i < ADC_BUTTON_MAX_BUTTON; i++) {
if (0 == g_button.unit[adc_btn->unit_id].ch[ch_index].btns[i].max) {
unused_button++;
}
}
if (unused_button == ADC_BUTTON_MAX_BUTTON && g_button.unit[adc_btn->unit_id].ch[ch_index].is_init) { /**< if all button is unused, deinit the channel */
g_button.unit[adc_btn->unit_id].ch[ch_index].is_init = 0;
g_button.unit[adc_btn->unit_id].ch[ch_index].channel = ADC_BUTTON_CHANNEL_MAX;
ESP_LOGD(TAG, "all button is unused on channel%d, deinit the channel", g_button.unit[adc_btn->unit_id].ch[ch_index].channel);
}
/** check channel usage on the adc*/
uint8_t unused_ch = 0;
for (size_t i = 0; i < ADC_BUTTON_MAX_CHANNEL; i++) {
if (0 == g_button.unit[adc_btn->unit_id].ch[i].is_init) {
unused_ch++;
}
}
if (unused_ch == ADC_BUTTON_MAX_CHANNEL && g_button.unit[adc_btn->unit_id].is_configured) { /**< if all channel is unused, deinit the adc */
if (g_button.unit[adc_btn->unit_id].is_configured == ADC_INIT_BY_ADC_BUTTON) {
esp_err_t ret = adc_oneshot_del_unit(g_button.unit[adc_btn->unit_id].adc_handle);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "adc oneshot del unit fail");
adc_calibration_deinit(g_button.unit[adc_btn->unit_id].adc_cali_handle);
}
g_button.unit[adc_btn->unit_id].is_configured = ADC_NONE_INIT;
memset(&g_button.unit[adc_btn->unit_id], 0, sizeof(btn_adc_unit_t));
ESP_LOGD(TAG, "all channel is unused, , deinit adc");
}
free(adc_btn);
return ESP_OK;
}
static uint32_t get_adc_voltage(adc_unit_t unit_id, uint8_t channel)
{
uint32_t adc_reading = 0;
int adc_raw = 0;
for (int i = 0; i < NO_OF_SAMPLES; i++) {
adc_oneshot_read(g_button.unit[unit_id].adc_handle, channel, &adc_raw);
adc_reading += adc_raw;
}
adc_reading /= NO_OF_SAMPLES;
//Convert adc_reading to voltage in mV
int voltage = 0;
adc_cali_raw_to_voltage(g_button.unit[unit_id].adc_cali_handle, adc_reading, &voltage);
ESP_LOGV(TAG, "Raw: %"PRIu32"\tVoltage: %dmV", adc_reading, voltage);
return voltage;
}
uint8_t button_adc_get_key_level(button_driver_t *button_driver)
{
button_adc_obj *adc_btn = __containerof(button_driver, button_adc_obj, base);
static uint16_t vol = 0;
uint32_t ch = adc_btn->ch;
uint32_t index = adc_btn->index;
ESP_RETURN_ON_FALSE(ch < ADC_BUTTON_CHANNEL_MAX, 0, TAG, "channel out of range");
ESP_RETURN_ON_FALSE(index < ADC_BUTTON_MAX_BUTTON, 0, TAG, "button_index out of range");
int ch_index = find_channel(adc_btn->unit_id, ch);
ESP_RETURN_ON_FALSE(ch_index >= 0, 0, TAG, "The button_index is not init");
/** It starts only when the elapsed time is more than 1ms */
if ((esp_timer_get_time() - g_button.unit[adc_btn->unit_id].ch[ch_index].last_time) > 1000) {
vol = get_adc_voltage(adc_btn->unit_id, ch);
g_button.unit[adc_btn->unit_id].ch[ch_index].last_time = esp_timer_get_time();
}
if (vol <= g_button.unit[adc_btn->unit_id].ch[ch_index].btns[index].max &&
vol >= g_button.unit[adc_btn->unit_id].ch[ch_index].btns[index].min) {
return BUTTON_ACTIVE;
}
return BUTTON_INACTIVE;
}
esp_err_t iot_button_new_adc_device(const button_config_t *button_config, const button_adc_config_t *adc_config, button_handle_t *ret_button)
{
esp_err_t ret = ESP_OK;
ESP_RETURN_ON_FALSE(button_config && adc_config && ret_button, ESP_ERR_INVALID_ARG, TAG, "Invalid argument");
ESP_RETURN_ON_FALSE(adc_config->unit_id < ADC_UNIT_NUM, ESP_ERR_INVALID_ARG, TAG, "adc_handle out of range");
ESP_RETURN_ON_FALSE(adc_config->adc_channel < ADC_BUTTON_CHANNEL_MAX, ESP_ERR_INVALID_ARG, TAG, "channel out of range");
ESP_RETURN_ON_FALSE(adc_config->button_index < ADC_BUTTON_MAX_BUTTON, ESP_ERR_INVALID_ARG, TAG, "button_index out of range");
ESP_RETURN_ON_FALSE(adc_config->max > 0, ESP_ERR_INVALID_ARG, TAG, "key max voltage invalid");
button_adc_obj *adc_btn = calloc(1, sizeof(button_adc_obj));
ESP_RETURN_ON_FALSE(adc_btn, ESP_ERR_NO_MEM, TAG, "calloc fail");
adc_btn->unit_id = adc_config->unit_id;
int ch_index = find_channel(adc_btn->unit_id, adc_config->adc_channel);
if (ch_index >= 0) { /**< the channel has been initialized */
ESP_GOTO_ON_FALSE(g_button.unit[adc_btn->unit_id].ch[ch_index].btns[adc_config->button_index].max == 0, ESP_ERR_INVALID_STATE, err, TAG, "The button_index has been used");
} else { /**< this is a new channel */
int unused_ch_index = find_unused_channel(adc_config->unit_id);
ESP_GOTO_ON_FALSE(unused_ch_index >= 0, ESP_ERR_INVALID_STATE, err, TAG, "exceed max channel number, can't create a new channel");
ch_index = unused_ch_index;
}
/** initialize adc */
if (0 == g_button.unit[adc_btn->unit_id].is_configured) {
esp_err_t ret;
if (NULL == adc_config->adc_handle) {
//ADC1 Init
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = adc_btn->unit_id,
};
ret = adc_oneshot_new_unit(&init_config, &g_button.unit[adc_btn->unit_id].adc_handle);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "adc oneshot new unit fail!");
g_button.unit[adc_btn->unit_id].is_configured = ADC_INIT_BY_ADC_BUTTON;
} else {
g_button.unit[adc_btn->unit_id].adc_handle = *adc_config->adc_handle;
ESP_LOGI(TAG, "ADC1 has been initialized");
g_button.unit[adc_btn->unit_id].is_configured = ADC_INIT_BY_USER;
}
}
/** initialize adc channel */
if (0 == g_button.unit[adc_btn->unit_id].ch[ch_index].is_init) {
//ADC1 Config
adc_oneshot_chan_cfg_t oneshot_config = {
.bitwidth = ADC_BUTTON_WIDTH,
.atten = ADC_BUTTON_ATTEN,
};
esp_err_t ret = adc_oneshot_config_channel(g_button.unit[adc_btn->unit_id].adc_handle, adc_config->adc_channel, &oneshot_config);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "adc oneshot config channel fail!");
//-------------ADC1 Calibration Init---------------//
adc_calibration_init(adc_btn->unit_id, ADC_BUTTON_ATTEN, &g_button.unit[adc_btn->unit_id].adc_cali_handle);
g_button.unit[adc_btn->unit_id].ch[ch_index].channel = adc_config->adc_channel;
g_button.unit[adc_btn->unit_id].ch[ch_index].is_init = 1;
g_button.unit[adc_btn->unit_id].ch[ch_index].last_time = 0;
}
g_button.unit[adc_btn->unit_id].ch[ch_index].btns[adc_config->button_index].max = adc_config->max;
g_button.unit[adc_btn->unit_id].ch[ch_index].btns[adc_config->button_index].min = adc_config->min;
g_button.unit[adc_btn->unit_id].ch_num++;
adc_btn->ch = adc_config->adc_channel;
adc_btn->index = adc_config->button_index;
adc_btn->base.get_key_level = button_adc_get_key_level;
adc_btn->base.del = button_adc_del;
ret = iot_button_create(button_config, &adc_btn->base, ret_button);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "Create button failed");
return ESP_OK;
err:
if (adc_btn) {
free(adc_btn);
}
return ret;
}

View File

@ -0,0 +1,158 @@
/* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_log.h"
#include "esp_err.h"
#include "esp_check.h"
#include "driver/gpio.h"
#include "button_gpio.h"
#include "esp_sleep.h"
#include "button_interface.h"
#include "iot_button.h"
static const char *TAG = "gpio_button";
typedef struct {
button_driver_t base; /**< button driver */
int32_t gpio_num; /**< num of gpio */
uint8_t active_level; /**< gpio level when press down */
bool enable_power_save; /**< enable power save */
} button_gpio_obj;
static esp_err_t button_gpio_del(button_driver_t *button_driver)
{
button_gpio_obj *gpio_btn = __containerof(button_driver, button_gpio_obj, base);
esp_err_t ret = gpio_reset_pin(gpio_btn->gpio_num);
free(gpio_btn);
return ret;
}
static uint8_t button_gpio_get_key_level(button_driver_t *button_driver)
{
button_gpio_obj *gpio_btn = __containerof(button_driver, button_gpio_obj, base);
int level = gpio_get_level(gpio_btn->gpio_num);
return level == gpio_btn->active_level ? 1 : 0;
}
static esp_err_t button_gpio_enable_gpio_wakeup(uint32_t gpio_num, uint8_t active_level, bool enable)
{
esp_err_t ret;
if (enable) {
gpio_intr_enable(gpio_num);
ret = gpio_wakeup_enable(gpio_num, active_level == 0 ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
} else {
gpio_intr_disable(gpio_num);
ret = gpio_wakeup_disable(gpio_num);
}
return ret;
}
static esp_err_t button_gpio_set_intr(int gpio_num, gpio_int_type_t intr_type, gpio_isr_t isr_handler)
{
static bool isr_service_installed = false;
esp_err_t ret = gpio_set_intr_type(gpio_num, intr_type);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Set gpio interrupt type failed");
if (!isr_service_installed) {
ret = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Install gpio interrupt service failed");
isr_service_installed = true;
}
ret = gpio_isr_handler_add(gpio_num, isr_handler, (void *)gpio_num);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Add gpio interrupt handler failed");
return ESP_OK;
}
static void button_power_save_isr_handler(void* arg)
{
/*!< resume the button */
iot_button_resume();
/*!< disable gpio wakeup not need active level*/
button_gpio_enable_gpio_wakeup((uint32_t)arg, 0, false);
}
static esp_err_t button_enter_power_save(button_driver_t *button_driver)
{
button_gpio_obj *gpio_btn = __containerof(button_driver, button_gpio_obj, base);
return button_gpio_enable_gpio_wakeup(gpio_btn->gpio_num, gpio_btn->active_level, true);
}
esp_err_t iot_button_new_gpio_device(const button_config_t *button_config, const button_gpio_config_t *gpio_cfg, button_handle_t *ret_button)
{
button_gpio_obj *gpio_btn = NULL;
esp_err_t ret = ESP_OK;
ESP_GOTO_ON_FALSE(button_config && gpio_cfg && ret_button, ESP_ERR_INVALID_ARG, err, TAG, "Invalid argument");
ESP_GOTO_ON_FALSE(GPIO_IS_VALID_GPIO(gpio_cfg->gpio_num), ESP_ERR_INVALID_ARG, err, TAG, "GPIO number error");
*ret_button = NULL;
gpio_btn = (button_gpio_obj *)calloc(1, sizeof(button_gpio_obj));
ESP_GOTO_ON_FALSE(gpio_btn, ESP_ERR_NO_MEM, err, TAG, "No memory for gpio button");
gpio_btn->gpio_num = gpio_cfg->gpio_num;
gpio_btn->active_level = gpio_cfg->active_level;
gpio_btn->enable_power_save = gpio_cfg->enable_power_save;
gpio_config_t gpio_conf = {0};
gpio_conf.intr_type = GPIO_INTR_DISABLE;
gpio_conf.mode = GPIO_MODE_INPUT;
gpio_conf.pin_bit_mask = (1ULL << gpio_cfg->gpio_num);
if (!gpio_cfg->disable_pull) {
if (gpio_cfg->active_level) {
gpio_conf.pull_down_en = GPIO_PULLDOWN_ENABLE;
gpio_conf.pull_up_en = GPIO_PULLUP_DISABLE;
} else {
gpio_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_conf.pull_up_en = GPIO_PULLUP_ENABLE;
}
}
ret = gpio_config(&gpio_conf);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ret, err, TAG, "GPIO config failed");
if (gpio_cfg->enable_power_save) {
#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP
if (!esp_sleep_is_valid_wakeup_gpio(gpio_cfg->gpio_num)) {
ESP_LOGE(TAG, "GPIO %ld is not a valid wakeup source under CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE", gpio_cfg->gpio_num);
return ESP_FAIL;
}
gpio_hold_en(gpio_cfg->gpio_num);
#endif
/* Enable wake up from GPIO */
esp_err_t ret = gpio_wakeup_enable(gpio_cfg->gpio_num, gpio_cfg->active_level == 0 ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_ERR_INVALID_STATE, err, TAG, "Enable gpio wakeup failed");
#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP
#if SOC_PM_SUPPORT_EXT1_WAKEUP
ret = esp_sleep_enable_ext1_wakeup_io((1ULL << gpio_cfg->gpio_num), gpio_cfg->active_level == 0 ? ESP_EXT1_WAKEUP_ANY_LOW : ESP_EXT1_WAKEUP_ANY_HIGH);
#else
/*!< Not support etc: esp32c2, esp32c3. Target must support ext1 wakeup */
ret = ESP_FAIL;
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "Target must support ext1 wakeup");
#endif
#else
ret = esp_sleep_enable_gpio_wakeup();
#endif
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "Configure gpio as wakeup source failed");
gpio_btn->base.enable_power_save = true;
gpio_btn->base.enter_power_save = button_enter_power_save;
}
gpio_btn->base.get_key_level = button_gpio_get_key_level;
gpio_btn->base.del = button_gpio_del;
ret = iot_button_create(button_config, &gpio_btn->base, ret_button);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "Create button failed");
if (gpio_cfg->enable_power_save) {
ret = button_gpio_set_intr(gpio_btn->gpio_num, gpio_cfg->active_level == 0 ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL, button_power_save_isr_handler);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "Set gpio interrupt failed");
}
return ESP_OK;
err:
if (gpio_btn) {
free(gpio_btn);
}
if (ret_button) {
iot_button_delete(*ret_button);
}
return ret;
}

View File

@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <inttypes.h>
#include "esp_log.h"
#include "esp_check.h"
#include "driver/gpio.h"
#include "button_matrix.h"
#include "button_interface.h"
static const char *TAG = "matrix_button";
typedef struct {
button_driver_t base; /**< base button driver */
int32_t row_gpio_num; /**< row gpio */
int32_t col_gpio_num; /**< col gpio */
} button_matrix_obj;
static esp_err_t button_matrix_gpio_init(int32_t gpio_num, gpio_mode_t mode)
{
ESP_RETURN_ON_FALSE(GPIO_IS_VALID_GPIO(gpio_num), ESP_ERR_INVALID_ARG, TAG, "gpio_num error");
gpio_config_t gpio_conf = {0};
gpio_conf.intr_type = GPIO_INTR_DISABLE;
gpio_conf.pull_down_en = GPIO_PULLDOWN_ENABLE;
gpio_conf.pin_bit_mask = (1ULL << gpio_num);
gpio_conf.mode = mode;
gpio_config(&gpio_conf);
return ESP_OK;
}
esp_err_t button_matrix_del(button_driver_t *button_driver)
{
button_matrix_obj *matrix_btn = __containerof(button_driver, button_matrix_obj, base);
//Reset an gpio to default state (select gpio function, enable pullup and disable input and output).
gpio_reset_pin(matrix_btn->row_gpio_num);
gpio_reset_pin(matrix_btn->col_gpio_num);
free(matrix_btn);
return ESP_OK;
}
uint8_t button_matrix_get_key_level(button_driver_t *button_driver)
{
button_matrix_obj *matrix_btn = __containerof(button_driver, button_matrix_obj, base);
gpio_set_level(matrix_btn->row_gpio_num, 1);
uint8_t level = gpio_get_level(matrix_btn->col_gpio_num);
gpio_set_level(matrix_btn->row_gpio_num, 0);
return level;
}
esp_err_t iot_button_new_matrix_device(const button_config_t *button_config, const button_matrix_config_t *matrix_config, button_handle_t *ret_button, size_t *size)
{
esp_err_t ret = ESP_OK;
ESP_RETURN_ON_FALSE(button_config && matrix_config && ret_button, ESP_ERR_INVALID_ARG, TAG, "Invalid argument");
ESP_RETURN_ON_FALSE(matrix_config->col_gpios && matrix_config->row_gpios, ESP_ERR_INVALID_ARG, TAG, "Invalid matrix config");
ESP_RETURN_ON_FALSE(matrix_config->col_gpio_num > 0 && matrix_config->row_gpio_num > 0, ESP_ERR_INVALID_ARG, TAG, "Invalid matrix config");
ESP_RETURN_ON_FALSE(*size == matrix_config->row_gpio_num * matrix_config->col_gpio_num, ESP_ERR_INVALID_ARG, TAG, "Invalid size");
button_matrix_obj *matrix_btn = calloc(*size, sizeof(button_matrix_obj));
for (int i = 0; i < matrix_config->row_gpio_num; i++) {
button_matrix_gpio_init(matrix_config->row_gpios[i], GPIO_MODE_OUTPUT);
}
for (int i = 0; i < matrix_config->col_gpio_num; i++) {
button_matrix_gpio_init(matrix_config->col_gpios[i], GPIO_MODE_INPUT);
}
for (int i = 0; i < *size; i++) {
matrix_btn[i].base.get_key_level = button_matrix_get_key_level;
matrix_btn[i].base.del = button_matrix_del;
matrix_btn[i].row_gpio_num = matrix_config->row_gpios[i / matrix_config->col_gpio_num];
matrix_btn[i].col_gpio_num = matrix_config->col_gpios[i % matrix_config->col_gpio_num];
ESP_LOGD(TAG, "row_gpio_num: %"PRId32", col_gpio_num: %"PRId32"", matrix_btn[i].row_gpio_num, matrix_btn[i].col_gpio_num);
ret = iot_button_create(button_config, &matrix_btn[i].base, &ret_button[i]);
ESP_GOTO_ON_FALSE(ret == ESP_OK, ESP_FAIL, err, TAG, "Create button failed");
}
*size = matrix_config->row_gpio_num * matrix_config->col_gpio_num;
return ESP_OK;
err:
if (matrix_btn) {
free(matrix_btn);
}
return ret;
}

View File

@ -0,0 +1,12 @@
dependencies:
cmake_utilities: '*'
idf: '>=4.0'
description: GPIO and ADC and Matrix button driver
documentation: https://docs.espressif.com/projects/esp-iot-solution/en/latest/input_device/button.html
issues: https://github.com/espressif/esp-iot-solution/issues
repository: git://github.com/espressif/esp-iot-solution.git
repository_info:
commit_sha: 25a38823765ee35808d4c69a19d81033d6f91049
path: components/button
url: https://github.com/espressif/esp-iot-solution/tree/master/components/button
version: 4.1.6

View File

@ -0,0 +1,57 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "driver/gpio.h"
#include "esp_adc/adc_oneshot.h"
#include "button_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief adc button configuration
*
*/
typedef struct {
adc_oneshot_unit_handle_t *adc_handle; /**< handle of adc unit, if NULL will create new one internal, else will use the handle */
adc_unit_t unit_id; /**< ADC unit */
uint8_t adc_channel; /**< Channel of ADC */
uint8_t button_index; /**< button index on the channel */
uint16_t min; /**< min voltage in mv corresponding to the button */
uint16_t max; /**< max voltage in mv corresponding to the button */
} button_adc_config_t;
/**
* @brief Create a new ADC button device
*
* This function initializes and configures a new ADC button device using the given configuration parameters.
* It manages the ADC unit, channels, and button-specific parameters, and ensures proper resource allocation
* for the ADC button object.
*
* @param[in] button_config Configuration for the button device, including callbacks and debounce parameters.
* @param[in] adc_config Configuration for the ADC channel and button, including the ADC unit, channel,
* button index, and voltage range (min and max).
* @param[out] ret_button Handle to the newly created button device.
*
* @return
* - ESP_OK: Successfully created the ADC button device.
* - ESP_ERR_INVALID_ARG: Invalid argument provided.
* - ESP_ERR_NO_MEM: Memory allocation failed.
* - ESP_ERR_INVALID_STATE: The requested button index or channel is already in use, or no channels are available.
* - ESP_FAIL: Failed to initialize or configure the ADC or button device.
*
* @note
* - If the ADC unit is not already configured, it will be initialized with the provided or default settings.
* - If the ADC channel is not initialized, it will be configured for the specified unit and calibrated.
* - This function ensures that ADC resources are reused whenever possible to optimize resource allocation.
*/
esp_err_t iot_button_new_adc_device(const button_config_t *button_config, const button_adc_config_t *adc_config, button_handle_t *ret_button);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,53 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "esp_err.h"
#include "button_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief gpio button configuration
*
*/
typedef struct {
int32_t gpio_num; /**< num of gpio */
uint8_t active_level; /**< gpio level when press down */
bool enable_power_save; /**< enable power save mode */
bool disable_pull; /**< disable internal pull up or down */
} button_gpio_config_t;
/**
* @brief Create a new GPIO button device
*
* This function initializes and configures a GPIO-based button device using the given configuration parameters.
* It sets up the GPIO pin, configures its input mode, and optionally enables power-saving features or wake-up functionality.
*
* @param[in] button_config Configuration for the button device, including callbacks and debounce parameters.
* @param[in] gpio_cfg Configuration for the GPIO, including the pin number, active level, and power-save options.
* @param[out] ret_button Handle to the newly created GPIO button device.
*
* @return
* - ESP_OK: Successfully created the GPIO button device.
* - ESP_ERR_INVALID_ARG: Invalid argument provided, such as an invalid GPIO number.
* - ESP_ERR_NO_MEM: Memory allocation failed.
* - ESP_ERR_INVALID_STATE: Failed to configure GPIO wake-up or interrupt settings.
* - ESP_FAIL: General failure, such as unsupported wake-up configuration on the target.
*
* @note
* - If power-saving is enabled, the GPIO will be configured as a wake-up source for light sleep.
* - Pull-up or pull-down resistors are configured based on the `active_level` and the `disable_pull` flag.
* - This function checks for the validity of the GPIO as a wake-up source when power-saving is enabled.
* - If power-saving is not supported by the hardware or configuration, the function will return an error.
*/
esp_err_t iot_button_new_gpio_device(const button_config_t *button_config, const button_gpio_config_t *gpio_config, button_handle_t *ret_button);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "esp_err.h"
#include "button_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Button matrix key configuration.
* Just need to configure the GPIO associated with this GPIO in the matrix keyboard.
*
* Matrix Keyboard Layout (3x3):
* ----------------------------------------
* | Button 1 | Button 2 | Button 3 |
* | (R1-C1) | (R1-C2) | (R1-C3) |
* |--------------------------------------|
* | Button 4 | Button 5 | Button 6 |
* | (R2-C1) | (R2-C2) | (R2-C3) |
* |--------------------------------------|
* | Button 7 | Button 8 | Button 9 |
* | (R3-C1) | (R3-C2) | (R3-C3) |
* ----------------------------------------
*
* - Button matrix key is driven using row scanning.
* - Buttons within the same column cannot be detected simultaneously,
* but buttons within the same row can be detected without conflicts.
*/
typedef struct {
int32_t *row_gpios; /**< GPIO number list for the row */
int32_t *col_gpios; /**< GPIO number list for the column */
uint32_t row_gpio_num; /**< Number of GPIOs associated with the row */
uint32_t col_gpio_num; /**< Number of GPIOs associated with the column */
} button_matrix_config_t;
/**
* @brief Create a new button matrix device
*
* This function initializes and configures a button matrix device using the specified row and column GPIOs.
* Each button in the matrix is represented as an independent button object, and its handle is returned in the `ret_button` array.
*
* @param[in] button_config Configuration for the button device, including callbacks and debounce parameters.
* @param[in] matrix_config Configuration for the matrix, including row and column GPIOs and their counts.
* @param[out] ret_button Array of handles for the buttons in the matrix.
* @param[inout] size Pointer to the total number of buttons in the matrix. Must match the product of row and column GPIO counts.
* On success, this value is updated to reflect the size of the button matrix.
*
* @return
* - ESP_OK: Successfully created the button matrix device.
* - ESP_ERR_INVALID_ARG: Invalid argument provided, such as null pointers or mismatched matrix dimensions.
* - ESP_ERR_NO_MEM: Memory allocation failed.
* - ESP_FAIL: General failure, such as button creation failure for one or more buttons.
*
* @note
* - Each row GPIO is configured as an output, while each column GPIO is configured as an input.
* - The total number of buttons in the matrix must equal the product of the row and column GPIO counts.
* - The `ret_button` array must be large enough to store handles for all buttons in the matrix.
* - If any button creation fails, the function will free all allocated resources and return an error.
*/
esp_err_t iot_button_new_matrix_device(const button_config_t *button_config, const button_matrix_config_t *matrix_config, button_handle_t *ret_button, size_t *size);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "button_interface.h"
#ifdef __cplusplus
extern "C" {
#endif
enum {
BUTTON_INACTIVE = 0,
BUTTON_ACTIVE,
};
typedef struct button_dev_t *button_handle_t;
/**
* @brief Button configuration
*
*/
typedef struct {
uint16_t long_press_time; /**< Trigger time(ms) for long press, if 0 default to BUTTON_LONG_PRESS_TIME_MS */
uint16_t short_press_time; /**< Trigger time(ms) for short press, if 0 default to BUTTON_SHORT_PRESS_TIME_MS */
} button_config_t;
/**
* @brief Create a new IoT button instance
*
* This function initializes a new button instance with the specified configuration
* and driver. It also sets up internal resources such as the button timer if not
* already initialized.
*
* @param[in] config Pointer to the button configuration structure
* @param[in] driver Pointer to the button driver structure
* @param[out] ret_button Pointer to where the handle of the created button will be stored
*
* @return
* - ESP_OK: Successfully created the button
* - ESP_ERR_INVALID_ARG: Invalid arguments passed to the function
* - ESP_ERR_NO_MEM: Memory allocation failed
*
* @note
* - The first call to this function logs the IoT Button version.
* - The function initializes a global button timer if it is not already running.
* - Timer is started only if the driver does not enable power-saving mode.
*/
esp_err_t iot_button_create(const button_config_t *config, const button_driver_t *driver, button_handle_t *ret_button);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,278 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "sdkconfig.h"
#include "esp_err.h"
#include "button_types.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef void (* button_cb_t)(void *button_handle, void *usr_data);
typedef void (* button_power_save_cb_t)(void *usr_data);
/**
* @brief Structs to store power save callback info
*
*/
typedef struct {
button_power_save_cb_t enter_power_save_cb; /**< Callback function when entering power save mode */
void *usr_data; /**< User data for the callback */
} button_power_save_config_t;
/**
* @brief Button events
*
*/
typedef enum {
BUTTON_PRESS_DOWN = 0,
BUTTON_PRESS_UP,
BUTTON_PRESS_REPEAT,
BUTTON_PRESS_REPEAT_DONE,
BUTTON_SINGLE_CLICK,
BUTTON_DOUBLE_CLICK,
BUTTON_MULTIPLE_CLICK,
BUTTON_LONG_PRESS_START,
BUTTON_LONG_PRESS_HOLD,
BUTTON_LONG_PRESS_UP,
BUTTON_PRESS_END,
BUTTON_EVENT_MAX,
BUTTON_NONE_PRESS,
} button_event_t;
/**
* @brief Button events arg
*
*/
typedef union {
/**
* @brief Long press time event data
*
*/
struct long_press_t {
uint16_t press_time; /**< press time(ms) for the corresponding callback to trigger */
} long_press; /**< long press struct, for event BUTTON_LONG_PRESS_START and BUTTON_LONG_PRESS_UP */
/**
* @brief Multiple clicks event data
*
*/
struct multiple_clicks_t {
uint16_t clicks; /**< number of clicks, to trigger the callback */
} multiple_clicks; /**< multiple clicks struct, for event BUTTON_MULTIPLE_CLICK */
} button_event_args_t;
/**
* @brief Button parameter
*
*/
typedef enum {
BUTTON_LONG_PRESS_TIME_MS = 0,
BUTTON_SHORT_PRESS_TIME_MS,
BUTTON_PARAM_MAX,
} button_param_t;
/**
* @brief Delete a button
*
* @param btn_handle A button handle to delete
*
* @return
* - ESP_OK Success
* - ESP_FAIL Failure
*/
esp_err_t iot_button_delete(button_handle_t btn_handle);
/**
* @brief Register the button event callback function.
*
* @param btn_handle A button handle to register
* @param event Button event
* @param event_args Button event arguments
* @param cb Callback function.
* @param usr_data user data
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG Arguments is invalid.
* - ESP_ERR_INVALID_STATE The Callback is already registered. No free Space for another Callback.
* - ESP_ERR_NO_MEM No more memory allocation for the event
*/
esp_err_t iot_button_register_cb(button_handle_t btn_handle, button_event_t event, button_event_args_t *event_args, button_cb_t cb, void *usr_data);
/**
* @brief Unregister all the callbacks associated with the event.
*
* @param btn_handle A button handle to unregister
* @param event Button event
* @param event_args Used for unregistering a specific callback.
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG Arguments is invalid.
* - ESP_ERR_INVALID_STATE No callbacks registered for the event
*/
esp_err_t iot_button_unregister_cb(button_handle_t btn_handle, button_event_t event, button_event_args_t *event_args);
/**
* @brief counts total callbacks registered
*
* @param btn_handle A button handle to the button
*
* @return
* - 0 if no callbacks registered, or 1 .. (BUTTON_EVENT_MAX-1) for the number of Registered Buttons.
* - ESP_ERR_INVALID_ARG if btn_handle is invalid
*/
size_t iot_button_count_cb(button_handle_t btn_handle);
/**
* @brief how many callbacks are registered for the event
*
* @param btn_handle A button handle to the button
*
* @param event Button event
*
* @return
* - 0 if no callbacks registered, or 1 .. (BUTTON_EVENT_MAX-1) for the number of Registered Buttons.
* - ESP_ERR_INVALID_ARG if btn_handle is invalid
*/
size_t iot_button_count_event_cb(button_handle_t btn_handle, button_event_t event);
/**
* @brief Get button event
*
* @param btn_handle Button handle
*
* @return Current button event. See button_event_t
*/
button_event_t iot_button_get_event(button_handle_t btn_handle);
/**
* @brief Get the string representation of a button event.
*
* This function returns the corresponding string for a given button event.
* If the event value is outside the valid range, the function returns error string "event value is invalid".
*
* @param[in] event The button event to be converted to a string.
*
* @return
* - Pointer to the event string if the event is valid.
* - "invalid event" if the event value is invalid.
*/
const char *iot_button_get_event_str(button_event_t event);
/**
* @brief Log the current button event as a string.
*
* This function prints the string representation of the current event associated with the button.
*
* @param[in] btn_handle Handle to the button object.
*
* @return
* - ESP_OK: Successfully logged the event string.
* - ESP_FAIL: Invalid button handle.
*/
esp_err_t iot_button_print_event(button_handle_t btn_handle);
/**
* @brief Get button repeat times
*
* @param btn_handle Button handle
*
* @return button pressed times. For example, double-click return 2, triple-click return 3, etc.
*/
uint8_t iot_button_get_repeat(button_handle_t btn_handle);
/**
* @brief Get button pressed time
*
* @param btn_handle Button handle
*
* @return Actual time from press down to up (ms).
*/
uint32_t iot_button_get_pressed_time(button_handle_t btn_handle);
/**
* @brief Get button ticks time
*
* @deprecated This function is deprecated and will be removed in a future release.
* Please use iot_button_get_pressed_time() instead.
*
* @param btn_handle Button handle
*
* @return Actual time from press down to up (ms).
*/
__attribute__((deprecated("Use iot_button_get_pressed_time() instead")))
uint32_t iot_button_get_ticks_time(button_handle_t btn_handle);
/**
* @brief Get button long press hold count
*
* @param btn_handle Button handle
*
* @return Count of trigger cb(BUTTON_LONG_PRESS_HOLD)
*/
uint16_t iot_button_get_long_press_hold_cnt(button_handle_t btn_handle);
/**
* @brief Dynamically change the parameters of the iot button
*
* @param btn_handle Button handle
* @param param Button parameter
* @param value new value
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG Arguments is invalid.
*/
esp_err_t iot_button_set_param(button_handle_t btn_handle, button_param_t param, void *value);
/**
* @brief Get button key level
*
* @param btn_handle Button handle
* @return
* - 1 if key is pressed
* - 0 if key is released or invalid button handle
*/
uint8_t iot_button_get_key_level(button_handle_t btn_handle);
/**
* @brief resume button timer, if button timer is stopped. Make sure iot_button_create() is called before calling this API.
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE timer state is invalid.
*/
esp_err_t iot_button_resume(void);
/**
* @brief stop button timer, if button timer is running. Make sure iot_button_create() is called before calling this API.
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE timer state is invalid
*/
esp_err_t iot_button_stop(void);
/**
* @brief Register a callback function for power saving.
* The config->enter_power_save_cb function will be called when all keys stop working.
*
* @param config Button power save config
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE No button registered
* - ESP_ERR_INVALID_ARG Arguments is invalid
* - ESP_ERR_NO_MEM Not enough memory
*/
esp_err_t iot_button_register_power_save_cb(const button_power_save_config_t *config);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct button_driver_t button_driver_t; /*!< Type of button object */
struct button_driver_t {
/*!< (optional) Need Support Power Save */
bool enable_power_save;
/*!< (necessary) Get key level */
uint8_t (*get_key_level)(button_driver_t *button_driver);
/*!< (optional) Enter Power Save cb */
esp_err_t (*enter_power_save)(button_driver_t *button_driver);
/*!< (optional) Del the hardware driver and cleanup */
esp_err_t (*del)(button_driver_t *button_driver);
};
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,710 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "driver/gpio.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_check.h"
#include "iot_button.h"
#include "sdkconfig.h"
#include "button_interface.h"
static const char *TAG = "button";
static portMUX_TYPE s_button_lock = portMUX_INITIALIZER_UNLOCKED;
#define BUTTON_ENTER_CRITICAL() portENTER_CRITICAL(&s_button_lock)
#define BUTTON_EXIT_CRITICAL() portEXIT_CRITICAL(&s_button_lock)
#define BTN_CHECK(a, str, ret_val) \
if (!(a)) { \
ESP_LOGE(TAG, "%s(%d): %s", __FUNCTION__, __LINE__, str); \
return (ret_val); \
}
static const char *button_event_str[] = {
"BUTTON_PRESS_DOWN",
"BUTTON_PRESS_UP",
"BUTTON_PRESS_REPEAT",
"BUTTON_PRESS_REPEAT_DONE",
"BUTTON_SINGLE_CLICK",
"BUTTON_DOUBLE_CLICK",
"BUTTON_MULTIPLE_CLICK",
"BUTTON_LONG_PRESS_START",
"BUTTON_LONG_PRESS_HOLD",
"BUTTON_LONG_PRESS_UP",
"BUTTON_PRESS_END",
"BUTTON_EVENT_MAX",
"BUTTON_NONE_PRESS",
};
enum {
PRESS_DOWN_CHECK = 0,
PRESS_UP_CHECK,
PRESS_REPEAT_DOWN_CHECK,
PRESS_REPEAT_UP_CHECK,
PRESS_LONG_PRESS_UP_CHECK,
};
/**
* @brief Structs to store callback info
*
*/
typedef struct {
button_cb_t cb;
void *usr_data;
button_event_args_t event_args;
} button_cb_info_t;
/**
* @brief Structs to record individual key parameters
*
*/
typedef struct button_dev_t {
uint32_t ticks; /*!< Count for the current button state. */
uint32_t long_press_ticks; /*!< Trigger ticks for long press, */
uint32_t short_press_ticks; /*!< Trigger ticks for repeat press */
uint32_t long_press_hold_cnt; /*!< Record long press hold count */
uint8_t repeat;
uint8_t state: 3;
uint8_t debounce_cnt: 4; /*!< Max 15 */
uint8_t button_level: 1;
button_event_t event;
button_driver_t *driver;
button_cb_info_t *cb_info[BUTTON_EVENT_MAX];
size_t size[BUTTON_EVENT_MAX];
int count[2];
struct button_dev_t *next;
} button_dev_t;
//button handle list head.
static button_dev_t *g_head_handle = NULL;
static esp_timer_handle_t g_button_timer_handle = NULL;
static bool g_is_timer_running = false;
static button_power_save_config_t power_save_usr_cfg = {0};
#define TICKS_INTERVAL CONFIG_BUTTON_PERIOD_TIME_MS
#define DEBOUNCE_TICKS CONFIG_BUTTON_DEBOUNCE_TICKS //MAX 8
#define SHORT_TICKS (CONFIG_BUTTON_SHORT_PRESS_TIME_MS /TICKS_INTERVAL)
#define LONG_TICKS (CONFIG_BUTTON_LONG_PRESS_TIME_MS /TICKS_INTERVAL)
#define SERIAL_TICKS (CONFIG_BUTTON_LONG_PRESS_HOLD_SERIAL_TIME_MS /TICKS_INTERVAL)
#define TOLERANCE (CONFIG_BUTTON_PERIOD_TIME_MS*4)
#define CALL_EVENT_CB(ev) \
if (btn->cb_info[ev]) { \
for (int i = 0; i < btn->size[ev]; i++) { \
btn->cb_info[ev][i].cb(btn, btn->cb_info[ev][i].usr_data); \
} \
} \
#define TIME_TO_TICKS(time, congfig_time) (0 == (time))?congfig_time:(((time) / TICKS_INTERVAL))?((time) / TICKS_INTERVAL):1
/**
* @brief Button driver core function, driver state machine.
*/
static void button_handler(button_dev_t *btn)
{
uint8_t read_gpio_level = btn->driver->get_key_level(btn->driver);
/** ticks counter working.. */
if ((btn->state) > 0) {
btn->ticks++;
}
/**< button debounce handle */
if (read_gpio_level != btn->button_level) {
if (++(btn->debounce_cnt) >= DEBOUNCE_TICKS) {
btn->button_level = read_gpio_level;
btn->debounce_cnt = 0;
}
} else {
btn->debounce_cnt = 0;
}
/** State machine */
switch (btn->state) {
case PRESS_DOWN_CHECK:
if (btn->button_level == BUTTON_ACTIVE) {
btn->event = (uint8_t)BUTTON_PRESS_DOWN;
CALL_EVENT_CB(BUTTON_PRESS_DOWN);
btn->ticks = 0;
btn->repeat = 1;
btn->state = PRESS_UP_CHECK;
} else {
btn->event = (uint8_t)BUTTON_NONE_PRESS;
}
break;
case PRESS_UP_CHECK:
if (btn->button_level != BUTTON_ACTIVE) {
btn->event = (uint8_t)BUTTON_PRESS_UP;
CALL_EVENT_CB(BUTTON_PRESS_UP);
btn->ticks = 0;
btn->state = PRESS_REPEAT_DOWN_CHECK;
} else if (btn->ticks >= btn->long_press_ticks) {
btn->event = (uint8_t)BUTTON_LONG_PRESS_START;
btn->state = PRESS_LONG_PRESS_UP_CHECK;
/** Calling callbacks for BUTTON_LONG_PRESS_START */
uint32_t pressed_time = iot_button_get_pressed_time(btn);
int32_t diff = pressed_time - btn->long_press_ticks * TICKS_INTERVAL;
if (btn->cb_info[btn->event] && btn->count[0] == 0) {
if (abs(diff) <= TOLERANCE && btn->cb_info[btn->event][btn->count[0]].event_args.long_press.press_time == (btn->long_press_ticks * TICKS_INTERVAL)) {
do {
btn->cb_info[btn->event][btn->count[0]].cb(btn, btn->cb_info[btn->event][btn->count[0]].usr_data);
btn->count[0]++;
if (btn->count[0] >= btn->size[btn->event]) {
break;
}
} while (btn->cb_info[btn->event][btn->count[0]].event_args.long_press.press_time == btn->long_press_ticks * TICKS_INTERVAL);
}
}
}
break;
case PRESS_REPEAT_DOWN_CHECK:
if (btn->button_level == BUTTON_ACTIVE) {
btn->event = (uint8_t)BUTTON_PRESS_DOWN;
CALL_EVENT_CB(BUTTON_PRESS_DOWN);
btn->event = (uint8_t)BUTTON_PRESS_REPEAT;
btn->repeat++;
CALL_EVENT_CB(BUTTON_PRESS_REPEAT); // repeat hit
btn->ticks = 0;
btn->state = PRESS_REPEAT_UP_CHECK;
} else if (btn->ticks > btn->short_press_ticks) {
if (btn->repeat == 1) {
btn->event = (uint8_t)BUTTON_SINGLE_CLICK;
CALL_EVENT_CB(BUTTON_SINGLE_CLICK);
} else if (btn->repeat == 2) {
btn->event = (uint8_t)BUTTON_DOUBLE_CLICK;
CALL_EVENT_CB(BUTTON_DOUBLE_CLICK); // repeat hit
}
btn->event = (uint8_t)BUTTON_MULTIPLE_CLICK;
/** Calling the callbacks for MULTIPLE BUTTON CLICKS */
for (int i = 0; i < btn->size[btn->event]; i++) {
if (btn->repeat == btn->cb_info[btn->event][i].event_args.multiple_clicks.clicks) {
btn->cb_info[btn->event][i].cb(btn, btn->cb_info[btn->event][i].usr_data);
}
}
btn->event = (uint8_t)BUTTON_PRESS_REPEAT_DONE;
CALL_EVENT_CB(BUTTON_PRESS_REPEAT_DONE); // repeat hit
btn->repeat = 0;
btn->state = 0;
btn->event = (uint8_t)BUTTON_PRESS_END;
CALL_EVENT_CB(BUTTON_PRESS_END);
}
break;
case 3:
if (btn->button_level != BUTTON_ACTIVE) {
btn->event = (uint8_t)BUTTON_PRESS_UP;
CALL_EVENT_CB(BUTTON_PRESS_UP);
if (btn->ticks < btn->short_press_ticks) {
btn->ticks = 0;
btn->state = PRESS_REPEAT_DOWN_CHECK; //repeat press
} else {
btn->state = PRESS_DOWN_CHECK;
btn->event = (uint8_t)BUTTON_PRESS_END;
CALL_EVENT_CB(BUTTON_PRESS_END);
}
}
break;
case PRESS_LONG_PRESS_UP_CHECK:
if (btn->button_level == BUTTON_ACTIVE) {
//continue hold trigger
if (btn->ticks >= (btn->long_press_hold_cnt + 1) * SERIAL_TICKS + btn->long_press_ticks) {
btn->event = (uint8_t)BUTTON_LONG_PRESS_HOLD;
btn->long_press_hold_cnt++;
CALL_EVENT_CB(BUTTON_LONG_PRESS_HOLD);
}
/** Calling callbacks for BUTTON_LONG_PRESS_START based on press_time */
uint32_t pressed_time = iot_button_get_pressed_time(btn);
if (btn->cb_info[BUTTON_LONG_PRESS_START]) {
button_cb_info_t *cb_info = btn->cb_info[BUTTON_LONG_PRESS_START];
uint16_t time = cb_info[btn->count[0]].event_args.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL > time) {
for (int i = btn->count[0] + 1; i < btn->size[BUTTON_LONG_PRESS_START]; i++) {
time = cb_info[i].event_args.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL <= time) {
btn->count[0] = i;
break;
}
}
}
if (btn->count[0] < btn->size[BUTTON_LONG_PRESS_START] && abs((int)pressed_time - (int)time) <= TOLERANCE) {
btn->event = (uint8_t)BUTTON_LONG_PRESS_START;
do {
cb_info[btn->count[0]].cb(btn, cb_info[btn->count[0]].usr_data);
btn->count[0]++;
if (btn->count[0] >= btn->size[BUTTON_LONG_PRESS_START]) {
break;
}
} while (time == cb_info[btn->count[0]].event_args.long_press.press_time);
}
}
/** Updating counter for BUTTON_LONG_PRESS_UP press_time */
if (btn->cb_info[BUTTON_LONG_PRESS_UP]) {
button_cb_info_t *cb_info = btn->cb_info[BUTTON_LONG_PRESS_UP];
uint16_t time = cb_info[btn->count[1] + 1].event_args.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL > time) {
for (int i = btn->count[1] + 1; i < btn->size[BUTTON_LONG_PRESS_UP]; i++) {
time = cb_info[i].event_args.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL <= time) {
btn->count[1] = i;
break;
}
}
}
if (btn->count[1] + 1 < btn->size[BUTTON_LONG_PRESS_UP] && abs((int)pressed_time - (int)time) <= TOLERANCE) {
do {
btn->count[1]++;
if (btn->count[1] + 1 >= btn->size[BUTTON_LONG_PRESS_UP]) {
break;
}
} while (time == cb_info[btn->count[1] + 1].event_args.long_press.press_time);
}
}
} else { //releasd
btn->event = BUTTON_LONG_PRESS_UP;
/** calling callbacks for BUTTON_LONG_PRESS_UP press_time */
if (btn->cb_info[btn->event] && btn->count[1] >= 0) {
button_cb_info_t *cb_info = btn->cb_info[btn->event];
do {
cb_info[btn->count[1]].cb(btn, cb_info[btn->count[1]].usr_data);
if (!btn->count[1]) {
break;
}
btn->count[1]--;
} while (cb_info[btn->count[1]].event_args.long_press.press_time == cb_info[btn->count[1] + 1].event_args.long_press.press_time);
/** Reset the counter */
btn->count[1] = -1;
}
/** Reset counter */
if (btn->cb_info[BUTTON_LONG_PRESS_START]) {
btn->count[0] = 0;
}
btn->event = (uint8_t)BUTTON_PRESS_UP;
CALL_EVENT_CB(BUTTON_PRESS_UP);
btn->state = PRESS_DOWN_CHECK; //reset
btn->long_press_hold_cnt = 0;
btn->event = (uint8_t)BUTTON_PRESS_END;
CALL_EVENT_CB(BUTTON_PRESS_END);
}
break;
}
}
static void button_cb(void *args)
{
button_dev_t *target;
/*!< When all buttons enter the BUTTON_NONE_PRESS state, the system enters low-power mode */
bool enter_power_save_flag = true;
for (target = g_head_handle; target; target = target->next) {
button_handler(target);
if (!(target->driver->enable_power_save && target->debounce_cnt == 0 && target->event == BUTTON_NONE_PRESS)) {
enter_power_save_flag = false;
}
}
if (enter_power_save_flag) {
/*!< Stop esp timer for power save */
if (g_is_timer_running) {
esp_timer_stop(g_button_timer_handle);
g_is_timer_running = false;
}
for (target = g_head_handle; target; target = target->next) {
if (target->driver->enable_power_save && target->driver->enter_power_save) {
target->driver->enter_power_save(target->driver);
}
}
/*!< Notify the user that the Button has entered power save mode by calling this callback function. */
if (power_save_usr_cfg.enter_power_save_cb) {
power_save_usr_cfg.enter_power_save_cb(power_save_usr_cfg.usr_data);
}
}
}
esp_err_t iot_button_register_cb(button_handle_t btn_handle, button_event_t event, button_event_args_t *event_args, button_cb_t cb, void *usr_data)
{
ESP_RETURN_ON_FALSE(NULL != btn_handle, ESP_ERR_INVALID_ARG, TAG, "Pointer of handle is invalid");
button_dev_t *btn = (button_dev_t *) btn_handle;
ESP_RETURN_ON_FALSE(event < BUTTON_EVENT_MAX, ESP_ERR_INVALID_ARG, TAG, "event is invalid");
ESP_RETURN_ON_FALSE(NULL != cb, ESP_ERR_INVALID_ARG, TAG, "Pointer of cb is invalid");
ESP_RETURN_ON_FALSE(event != BUTTON_MULTIPLE_CLICK || event_args, ESP_ERR_INVALID_ARG, TAG, "event is invalid");
if (event_args) {
ESP_RETURN_ON_FALSE(!(event == BUTTON_LONG_PRESS_START || event == BUTTON_LONG_PRESS_UP) || event_args->long_press.press_time > btn->short_press_ticks * TICKS_INTERVAL, ESP_ERR_INVALID_ARG, TAG, "event_args is invalid");
ESP_RETURN_ON_FALSE(event != BUTTON_MULTIPLE_CLICK || event_args->multiple_clicks.clicks, ESP_ERR_INVALID_ARG, TAG, "event_args is invalid");
}
if (!btn->cb_info[event]) {
btn->cb_info[event] = calloc(1, sizeof(button_cb_info_t));
BTN_CHECK(NULL != btn->cb_info[event], "calloc cb_info failed", ESP_ERR_NO_MEM);
if (event == BUTTON_LONG_PRESS_START) {
btn->count[0] = 0;
} else if (event == BUTTON_LONG_PRESS_UP) {
btn->count[1] = -1;
}
} else {
button_cb_info_t *p = realloc(btn->cb_info[event], sizeof(button_cb_info_t) * (btn->size[event] + 1));
BTN_CHECK(NULL != p, "realloc cb_info failed", ESP_ERR_NO_MEM);
btn->cb_info[event] = p;
}
btn->cb_info[event][btn->size[event]].cb = cb;
btn->cb_info[event][btn->size[event]].usr_data = usr_data;
btn->size[event]++;
/** Inserting the event_args in sorted manner */
if (event == BUTTON_LONG_PRESS_START || event == BUTTON_LONG_PRESS_UP) {
uint16_t press_time = btn->long_press_ticks * TICKS_INTERVAL;
if (event_args) {
press_time = event_args->long_press.press_time;
}
BTN_CHECK(press_time / TICKS_INTERVAL > btn->short_press_ticks, "press_time event_args is less than short_press_ticks", ESP_ERR_INVALID_ARG);
if (btn->size[event] >= 2) {
for (int i = btn->size[event] - 2; i >= 0; i--) {
if (btn->cb_info[event][i].event_args.long_press.press_time > press_time) {
btn->cb_info[event][i + 1] = btn->cb_info[event][i];
btn->cb_info[event][i].event_args.long_press.press_time = press_time;
btn->cb_info[event][i].cb = cb;
btn->cb_info[event][i].usr_data = usr_data;
} else {
btn->cb_info[event][i + 1].event_args.long_press.press_time = press_time;
btn->cb_info[event][i + 1].cb = cb;
btn->cb_info[event][i + 1].usr_data = usr_data;
break;
}
}
} else {
btn->cb_info[event][btn->size[event] - 1].event_args.long_press.press_time = press_time;
}
int32_t press_ticks = press_time / TICKS_INTERVAL;
if (btn->short_press_ticks < press_ticks && press_ticks < btn->long_press_ticks) {
iot_button_set_param(btn, BUTTON_LONG_PRESS_TIME_MS, (void*)(intptr_t)press_time);
}
}
if (event == BUTTON_MULTIPLE_CLICK) {
uint16_t clicks = btn->long_press_ticks * TICKS_INTERVAL;
if (event_args) {
clicks = event_args->multiple_clicks.clicks;
}
if (btn->size[event] >= 2) {
for (int i = btn->size[event] - 2; i >= 0; i--) {
if (btn->cb_info[event][i].event_args.multiple_clicks.clicks > clicks) {
btn->cb_info[event][i + 1] = btn->cb_info[event][i];
btn->cb_info[event][i].event_args.multiple_clicks.clicks = clicks;
btn->cb_info[event][i].cb = cb;
btn->cb_info[event][i].usr_data = usr_data;
} else {
btn->cb_info[event][i + 1].event_args.multiple_clicks.clicks = clicks;
btn->cb_info[event][i + 1].cb = cb;
btn->cb_info[event][i + 1].usr_data = usr_data;
break;
}
}
} else {
btn->cb_info[event][btn->size[event] - 1].event_args.multiple_clicks.clicks = clicks;
}
}
return ESP_OK;
}
esp_err_t iot_button_unregister_cb(button_handle_t btn_handle, button_event_t event, button_event_args_t *event_args)
{
ESP_RETURN_ON_FALSE(NULL != btn_handle, ESP_ERR_INVALID_ARG, TAG, "Pointer of handle is invalid");
ESP_RETURN_ON_FALSE(event < BUTTON_EVENT_MAX, ESP_ERR_INVALID_ARG, TAG, "event is invalid");
button_dev_t *btn = (button_dev_t *) btn_handle;
ESP_RETURN_ON_FALSE(btn->cb_info[event], ESP_ERR_INVALID_STATE, TAG, "No callbacks registered for the event");
int check = -1;
if ((event == BUTTON_LONG_PRESS_START || event == BUTTON_LONG_PRESS_UP) && event_args) {
if (event_args->long_press.press_time != 0) {
goto unregister_event;
}
}
if (event == BUTTON_MULTIPLE_CLICK && event_args) {
if (event_args->multiple_clicks.clicks != 0) {
goto unregister_event;
}
}
if (btn->cb_info[event]) {
free(btn->cb_info[event]);
/** Reset the counter */
if (event == BUTTON_LONG_PRESS_START) {
btn->count[0] = 0;
} else if (event == BUTTON_LONG_PRESS_UP) {
btn->count[1] = -1;
}
}
btn->cb_info[event] = NULL;
btn->size[event] = 0;
return ESP_OK;
unregister_event:
for (int i = 0; i < btn->size[event]; i++) {
if ((event == BUTTON_LONG_PRESS_START || event == BUTTON_LONG_PRESS_UP) && event_args->long_press.press_time) {
if (event_args->long_press.press_time != btn->cb_info[event][i].event_args.long_press.press_time) {
continue;
}
}
if (event == BUTTON_MULTIPLE_CLICK && event_args->multiple_clicks.clicks) {
if (event_args->multiple_clicks.clicks != btn->cb_info[event][i].event_args.multiple_clicks.clicks) {
continue;
}
}
check = i;
for (int j = i; j <= btn->size[event] - 1; j++) {
btn->cb_info[event][j] = btn->cb_info[event][j + 1];
}
if (btn->size[event] != 1) {
button_cb_info_t *p = realloc(btn->cb_info[event], sizeof(button_cb_info_t) * (btn->size[event] - 1));
BTN_CHECK(NULL != p, "realloc cb_info failed", ESP_ERR_NO_MEM);
btn->cb_info[event] = p;
btn->size[event]--;
} else {
free(btn->cb_info[event]);
btn->cb_info[event] = NULL;
btn->size[event] = 0;
}
break;
}
ESP_RETURN_ON_FALSE(check != -1, ESP_ERR_NOT_FOUND, TAG, "No such callback registered for the event");
return ESP_OK;
}
size_t iot_button_count_cb(button_handle_t btn_handle)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", ESP_ERR_INVALID_ARG);
button_dev_t *btn = (button_dev_t *) btn_handle;
size_t ret = 0;
for (size_t i = 0; i < BUTTON_EVENT_MAX; i++) {
if (btn->cb_info[i]) {
ret += btn->size[i];
}
}
return ret;
}
size_t iot_button_count_event_cb(button_handle_t btn_handle, button_event_t event)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", ESP_ERR_INVALID_ARG);
button_dev_t *btn = (button_dev_t *) btn_handle;
return btn->size[event];
}
button_event_t iot_button_get_event(button_handle_t btn_handle)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", BUTTON_NONE_PRESS);
button_dev_t *btn = (button_dev_t *) btn_handle;
return btn->event;
}
const char *iot_button_get_event_str(button_event_t event)
{
BTN_CHECK(event <= BUTTON_NONE_PRESS && event >= BUTTON_PRESS_DOWN, "event value is invalid", "invalid event");
return button_event_str[event];
}
esp_err_t iot_button_print_event(button_handle_t btn_handle)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", ESP_FAIL);
button_dev_t *btn = (button_dev_t *) btn_handle;
ESP_LOGI(TAG, "%s", button_event_str[btn->event]);
return ESP_OK;
}
uint8_t iot_button_get_repeat(button_handle_t btn_handle)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", 0);
button_dev_t *btn = (button_dev_t *) btn_handle;
return btn->repeat;
}
uint32_t iot_button_get_pressed_time(button_handle_t btn_handle)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", 0);
button_dev_t *btn = (button_dev_t *) btn_handle;
return (btn->ticks * TICKS_INTERVAL);
}
uint32_t iot_button_get_ticks_time(button_handle_t btn_handle)
{
return iot_button_get_pressed_time(btn_handle);
}
uint16_t iot_button_get_long_press_hold_cnt(button_handle_t btn_handle)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", 0);
button_dev_t *btn = (button_dev_t *) btn_handle;
return btn->long_press_hold_cnt;
}
esp_err_t iot_button_set_param(button_handle_t btn_handle, button_param_t param, void *value)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", ESP_ERR_INVALID_ARG);
button_dev_t *btn = (button_dev_t *) btn_handle;
BUTTON_ENTER_CRITICAL();
switch (param) {
case BUTTON_LONG_PRESS_TIME_MS:
btn->long_press_ticks = (int32_t)value / TICKS_INTERVAL;
break;
case BUTTON_SHORT_PRESS_TIME_MS:
btn->short_press_ticks = (int32_t)value / TICKS_INTERVAL;
break;
default:
break;
}
BUTTON_EXIT_CRITICAL();
return ESP_OK;
}
uint8_t iot_button_get_key_level(button_handle_t btn_handle)
{
BTN_CHECK(NULL != btn_handle, "Pointer of handle is invalid", 0);
button_dev_t *btn = (button_dev_t *)btn_handle;
uint8_t level = btn->driver->get_key_level(btn->driver);
return level;
}
esp_err_t iot_button_resume(void)
{
if (!g_button_timer_handle) {
return ESP_ERR_INVALID_STATE;
}
if (!g_is_timer_running) {
esp_timer_start_periodic(g_button_timer_handle, TICKS_INTERVAL * 1000U);
g_is_timer_running = true;
}
return ESP_OK;
}
esp_err_t iot_button_stop(void)
{
BTN_CHECK(g_button_timer_handle, "Button timer handle is invalid", ESP_ERR_INVALID_STATE);
BTN_CHECK(g_is_timer_running, "Button timer is not running", ESP_ERR_INVALID_STATE);
esp_err_t err = esp_timer_stop(g_button_timer_handle);
BTN_CHECK(ESP_OK == err, "Button timer stop failed", ESP_FAIL);
g_is_timer_running = false;
return ESP_OK;
}
esp_err_t iot_button_register_power_save_cb(const button_power_save_config_t *config)
{
BTN_CHECK(g_head_handle, "No button registered", ESP_ERR_INVALID_STATE);
BTN_CHECK(config->enter_power_save_cb, "Enter power save callback is invalid", ESP_ERR_INVALID_ARG);
power_save_usr_cfg.enter_power_save_cb = config->enter_power_save_cb;
power_save_usr_cfg.usr_data = config->usr_data;
return ESP_OK;
}
esp_err_t iot_button_create(const button_config_t *config, const button_driver_t *driver, button_handle_t *ret_button)
{
if (!g_head_handle) {
ESP_LOGI(TAG, "IoT Button Version: %d.%d.%d", BUTTON_VER_MAJOR, BUTTON_VER_MINOR, BUTTON_VER_PATCH);
}
ESP_RETURN_ON_FALSE(driver && config && ret_button, ESP_ERR_INVALID_ARG, TAG, "Invalid argument");
button_dev_t *btn = (button_dev_t *) calloc(1, sizeof(button_dev_t));
ESP_RETURN_ON_FALSE(btn, ESP_ERR_NO_MEM, TAG, "Button memory alloc failed");
btn->driver = (button_driver_t *)driver;
btn->long_press_ticks = TIME_TO_TICKS(config->long_press_time, LONG_TICKS);
btn->short_press_ticks = TIME_TO_TICKS(config->short_press_time, SHORT_TICKS);
btn->event = BUTTON_NONE_PRESS;
btn->button_level = BUTTON_INACTIVE;
btn->next = g_head_handle;
g_head_handle = btn;
if (!g_button_timer_handle) {
esp_timer_create_args_t button_timer = {0};
button_timer.arg = NULL;
button_timer.callback = button_cb;
button_timer.dispatch_method = ESP_TIMER_TASK;
button_timer.name = "button_timer";
esp_timer_create(&button_timer, &g_button_timer_handle);
}
if (!driver->enable_power_save && !g_is_timer_running) {
esp_timer_start_periodic(g_button_timer_handle, TICKS_INTERVAL * 1000U);
g_is_timer_running = true;
}
*ret_button = (button_handle_t)btn;
return ESP_OK;
}
esp_err_t iot_button_delete(button_handle_t btn_handle)
{
esp_err_t ret = ESP_OK;
ESP_RETURN_ON_FALSE(NULL != btn_handle, ESP_ERR_INVALID_ARG, TAG, "Pointer of handle is invalid");
button_dev_t *btn = (button_dev_t *)btn_handle;
for (int i = 0; i < BUTTON_EVENT_MAX; i++) {
if (btn->cb_info[i]) {
free(btn->cb_info[i]);
}
}
ret = btn->driver->del(btn->driver);
ESP_RETURN_ON_FALSE(ESP_OK == ret, ret, TAG, "Failed to delete button driver");
button_dev_t **curr;
for (curr = &g_head_handle; *curr;) {
button_dev_t *entry = *curr;
if (entry == btn) {
*curr = entry->next;
free(entry);
} else {
curr = &entry->next;
}
}
/* count button number */
uint16_t number = 0;
button_dev_t *target = g_head_handle;
while (target) {
target = target->next;
number++;
}
ESP_LOGD(TAG, "remain btn number=%d", number);
if (0 == number && g_is_timer_running) { /**< if all button is deleted, stop the timer */
esp_timer_stop(g_button_timer_handle);
esp_timer_delete(g_button_timer_handle);
g_button_timer_handle = NULL;
g_is_timer_running = false;
}
return ESP_OK;
}

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,9 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components"
"../../button")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(button_test)

View File

@ -0,0 +1,8 @@
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.0")
list(APPEND PRIVREQ esp_adc)
endif()
idf_component_register(SRC_DIRS "."
PRIV_INCLUDE_DIRS "."
PRIV_REQUIRES esp_event unity test_utils button ${PRIVREQ}
WHOLE_ARCHIVE)

View File

@ -0,0 +1,134 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/timers.h"
#include "freertos/semphr.h"
#include "freertos/event_groups.h"
#include "esp_idf_version.h"
#include "esp_log.h"
#include "unity.h"
#include "iot_button.h"
#include "button_adc.h"
static const char *TAG = "ADC BUTTON TEST";
static void button_event_cb(void *arg, void *data)
{
button_event_t event = iot_button_get_event(arg);
ESP_LOGI(TAG, "BTN[%d] %s", (int)data, iot_button_get_event_str(event));
if (BUTTON_PRESS_REPEAT == event || BUTTON_PRESS_REPEAT_DONE == event) {
ESP_LOGI(TAG, "\tREPEAT[%d]", iot_button_get_repeat(arg));
}
if (BUTTON_PRESS_UP == event || BUTTON_LONG_PRESS_HOLD == event || BUTTON_LONG_PRESS_UP == event) {
ESP_LOGI(TAG, "\tPressed Time[%"PRIu32"]", iot_button_get_pressed_time(arg));
}
if (BUTTON_MULTIPLE_CLICK == event) {
ESP_LOGI(TAG, "\tMULTIPLE[%d]", (int)data);
}
}
TEST_CASE("adc button test", "[button][adc]")
{
/** ESP32-S3-Korvo2 board */
const button_config_t btn_cfg = {0};
button_adc_config_t btn_adc_cfg = {
.unit_id = ADC_UNIT_1,
.adc_channel = 4,
};
button_handle_t btns[6] = {NULL};
const uint16_t vol[6] = {380, 820, 1180, 1570, 1980, 2410};
for (size_t i = 0; i < 6; i++) {
btn_adc_cfg.button_index = i;
if (i == 0) {
btn_adc_cfg.min = (0 + vol[i]) / 2;
} else {
btn_adc_cfg.min = (vol[i - 1] + vol[i]) / 2;
}
if (i == 5) {
btn_adc_cfg.max = (vol[i] + 3000) / 2;
} else {
btn_adc_cfg.max = (vol[i] + vol[i + 1]) / 2;
}
esp_err_t ret = iot_button_new_adc_device(&btn_cfg, &btn_adc_cfg, &btns[i]);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btns[i]);
iot_button_register_cb(btns[i], BUTTON_PRESS_DOWN, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_UP, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_REPEAT, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_REPEAT_DONE, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_SINGLE_CLICK, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_DOUBLE_CLICK, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_LONG_PRESS_START, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_LONG_PRESS_HOLD, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_LONG_PRESS_UP, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_END, NULL, button_event_cb, (void *)i);
}
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
for (size_t i = 0; i < 6; i++) {
iot_button_delete(btns[i]);
}
}
TEST_CASE("adc button test memory leak", "[button][adc][memory leak]")
{
/** ESP32-S3-Korvo2 board */
const button_config_t btn_cfg = {0};
button_adc_config_t btn_adc_cfg = {
.unit_id = ADC_UNIT_1,
.adc_channel = 4,
};
button_handle_t btns[6] = {NULL};
const uint16_t vol[6] = {380, 820, 1180, 1570, 1980, 2410};
for (size_t i = 0; i < 6; i++) {
btn_adc_cfg.button_index = i;
if (i == 0) {
btn_adc_cfg.min = (0 + vol[i]) / 2;
} else {
btn_adc_cfg.min = (vol[i - 1] + vol[i]) / 2;
}
if (i == 5) {
btn_adc_cfg.max = (vol[i] + 3000) / 2;
} else {
btn_adc_cfg.max = (vol[i] + vol[i + 1]) / 2;
}
esp_err_t ret = iot_button_new_adc_device(&btn_cfg, &btn_adc_cfg, &btns[i]);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btns[i]);
iot_button_register_cb(btns[i], BUTTON_PRESS_DOWN, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_UP, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_REPEAT, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_REPEAT_DONE, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_SINGLE_CLICK, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_DOUBLE_CLICK, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_LONG_PRESS_START, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_LONG_PRESS_HOLD, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_LONG_PRESS_UP, NULL, button_event_cb, (void *)i);
iot_button_register_cb(btns[i], BUTTON_PRESS_END, NULL, button_event_cb, (void *)i);
}
for (size_t i = 0; i < 6; i++) {
iot_button_delete(btns[i]);
}
}

View File

@ -0,0 +1,288 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "unity.h"
#include "iot_button.h"
#include "button_gpio.h"
#include "driver/gpio.h"
static const char *TAG = "BUTTON AUTO TEST";
#define GPIO_OUTPUT_IO_45 45
#define BUTTON_IO_NUM 0
#define BUTTON_ACTIVE_LEVEL 0
static EventGroupHandle_t g_check = NULL;
static SemaphoreHandle_t g_auto_check_pass = NULL;
static button_event_t state = BUTTON_PRESS_DOWN;
static void button_auto_press_test_task(void *arg)
{
// test BUTTON_PRESS_DOWN
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(100));
// // test BUTTON_PRESS_UP
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
vTaskDelay(pdMS_TO_TICKS(200));
// test BUTTON_PRESS_REPEAT
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(100));
// test BUTTON_PRESS_REPEAT_DONE
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
vTaskDelay(pdMS_TO_TICKS(200));
// test BUTTON_SINGLE_CLICK
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
vTaskDelay(pdMS_TO_TICKS(200));
// test BUTTON_DOUBLE_CLICK
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
vTaskDelay(pdMS_TO_TICKS(200));
// test BUTTON_MULTIPLE_CLICK
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
for (int i = 0; i < 4; i++) {
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
vTaskDelay(pdMS_TO_TICKS(100));
}
vTaskDelay(pdMS_TO_TICKS(100));
// test BUTTON_LONG_PRESS_START
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(1600));
// test BUTTON_LONG_PRESS_HOLD and BUTTON_LONG_PRESS_UP
xEventGroupWaitBits(g_check, BIT(0) | BIT(1), pdTRUE, pdTRUE, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
ESP_LOGI(TAG, "Auto Press Success!");
vTaskDelete(NULL);
}
static void button_auto_check_cb_1(void *arg, void *data)
{
if (iot_button_get_event(arg) == state) {
xEventGroupSetBits(g_check, BIT(1));
}
}
static void button_auto_check_cb(void *arg, void *data)
{
if (iot_button_get_event(arg) == state) {
ESP_LOGI(TAG, "Auto check: button event %s pass", iot_button_get_event_str(state));
xEventGroupSetBits(g_check, BIT(0));
if (++state >= BUTTON_EVENT_MAX) {
xSemaphoreGive(g_auto_check_pass);
return;
}
}
}
TEST_CASE("gpio button auto-test", "[button][iot][auto]")
{
state = BUTTON_PRESS_DOWN;
g_check = xEventGroupCreate();
g_auto_check_pass = xSemaphoreCreateBinary();
xEventGroupSetBits(g_check, BIT(0) | BIT(1));
const button_config_t btn_cfg = {0};
const button_gpio_config_t btn_gpio_cfg = {
.gpio_num = BUTTON_IO_NUM,
.active_level = BUTTON_ACTIVE_LEVEL,
};
button_handle_t btn = NULL;
esp_err_t ret = iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &btn);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btn);
/* register iot_button callback for all the button_event */
for (uint8_t i = 0; i < BUTTON_EVENT_MAX; i++) {
if (i == BUTTON_MULTIPLE_CLICK) {
button_event_args_t args = {
.multiple_clicks.clicks = 4,
};
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_auto_check_cb_1, NULL);
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_auto_check_cb, NULL);
} else {
iot_button_register_cb(btn, i, NULL, button_auto_check_cb_1, NULL);
iot_button_register_cb(btn, i, NULL, button_auto_check_cb, NULL);
}
}
TEST_ASSERT_EQUAL(ESP_OK, iot_button_set_param(btn, BUTTON_LONG_PRESS_TIME_MS, (void *)1500));
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL << GPIO_OUTPUT_IO_45),
.pull_down_en = 0,
.pull_up_en = 0,
};
gpio_config(&io_conf);
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
xTaskCreate(button_auto_press_test_task, "button_auto_press_test_task", 1024 * 4, NULL, 10, NULL);
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(g_auto_check_pass, pdMS_TO_TICKS(6000)));
for (uint8_t i = 0; i < BUTTON_EVENT_MAX; i++) {
button_event_args_t args;
if (i == BUTTON_MULTIPLE_CLICK) {
args.multiple_clicks.clicks = 4;
iot_button_unregister_cb(btn, i, &args);
} else if (i == BUTTON_LONG_PRESS_UP || i == BUTTON_LONG_PRESS_START) {
args.long_press.press_time = 1500;
iot_button_unregister_cb(btn, i, &args);
} else {
iot_button_unregister_cb(btn, i, NULL);
}
}
TEST_ASSERT_EQUAL(ESP_OK, iot_button_delete(btn));
vEventGroupDelete(g_check);
vSemaphoreDelete(g_auto_check_pass);
vTaskDelay(pdMS_TO_TICKS(100));
}
#define TOLERANCE (CONFIG_BUTTON_PERIOD_TIME_MS * 4)
uint16_t long_press_time[5] = {2000, 2500, 3000, 3500, 4000};
static SemaphoreHandle_t long_press_check = NULL;
static SemaphoreHandle_t long_press_auto_check_pass = NULL;
unsigned int status = 0;
static void button_auto_long_press_test_task(void *arg)
{
// Test for BUTTON_LONG_PRESS_START
for (int i = 0; i < 5; i++) {
xSemaphoreTake(long_press_check, portMAX_DELAY);
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
status = (BUTTON_LONG_PRESS_START << 16) | long_press_time[i];
if (i > 0) {
vTaskDelay(pdMS_TO_TICKS(long_press_time[i] - long_press_time[i - 1]));
} else {
vTaskDelay(pdMS_TO_TICKS(long_press_time[i]));
}
}
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
xSemaphoreGive(long_press_auto_check_pass);
vTaskDelay(pdMS_TO_TICKS(100));
// Test for BUTTON_LONG_PRESS_UP
for (int i = 0; i < 5; i++) {
xSemaphoreTake(long_press_check, portMAX_DELAY);
status = (BUTTON_LONG_PRESS_UP << 16) | long_press_time[i];
gpio_set_level(GPIO_OUTPUT_IO_45, 0);
vTaskDelay(pdMS_TO_TICKS(long_press_time[i] + 10));
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
}
ESP_LOGI(TAG, "Auto Long Press Success!");
vTaskDelete(NULL);
}
static void button_long_press_auto_check_cb(void *arg, void *data)
{
uint32_t value = (uint32_t)data;
uint16_t event = (0xffff0000 & value) >> 16;
uint16_t time = 0xffff & value;
uint32_t pressed_time = iot_button_get_pressed_time(arg);
int32_t diff = pressed_time - time;
if (status == value && abs(diff) <= TOLERANCE) {
ESP_LOGI(TAG, "Auto check: button event: %s and time: %d pass", iot_button_get_event_str(event), time);
if (event == BUTTON_LONG_PRESS_UP && time == long_press_time[4]) {
xSemaphoreGive(long_press_auto_check_pass);
}
xSemaphoreGive(long_press_check);
}
}
TEST_CASE("gpio button long_press auto-test", "[button][long_press][auto]")
{
ESP_LOGI(TAG, "Starting the test");
long_press_check = xSemaphoreCreateBinary();
long_press_auto_check_pass = xSemaphoreCreateBinary();
xSemaphoreGive(long_press_check);
const button_config_t btn_cfg = {0};
const button_gpio_config_t btn_gpio_cfg = {
.gpio_num = BUTTON_IO_NUM,
.active_level = BUTTON_ACTIVE_LEVEL,
};
button_handle_t btn = NULL;
esp_err_t ret = iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &btn);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btn);
for (int i = 0; i < 5; i++) {
button_event_args_t args = {
.long_press.press_time = long_press_time[i],
};
uint32_t data = (BUTTON_LONG_PRESS_START << 16) | long_press_time[i];
iot_button_register_cb(btn, BUTTON_LONG_PRESS_START, &args, button_long_press_auto_check_cb, (void*)data);
}
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL << GPIO_OUTPUT_IO_45),
.pull_down_en = 0,
.pull_up_en = 0,
};
gpio_config(&io_conf);
gpio_set_level(GPIO_OUTPUT_IO_45, 1);
xTaskCreate(button_auto_long_press_test_task, "button_auto_long_press_test_task", 1024 * 4, NULL, 10, NULL);
xSemaphoreTake(long_press_auto_check_pass, portMAX_DELAY);
iot_button_unregister_cb(btn, BUTTON_LONG_PRESS_START, NULL);
for (int i = 0; i < 5; i++) {
button_event_args_t args = {
.long_press.press_time = long_press_time[i]
};
uint32_t data = (BUTTON_LONG_PRESS_UP << 16) | long_press_time[i];
iot_button_register_cb(btn, BUTTON_LONG_PRESS_UP, &args, button_long_press_auto_check_cb, (void*)data);
}
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(long_press_auto_check_pass, pdMS_TO_TICKS(17000)));
TEST_ASSERT_EQUAL(ESP_OK, iot_button_delete(btn));
vSemaphoreDelete(long_press_check);
vSemaphoreDelete(long_press_auto_check_pass);
vTaskDelay(pdMS_TO_TICKS(100));
}

View File

@ -0,0 +1,41 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "unity.h"
#include "unity_test_runner.h"
#include "unity_test_utils_memory.h"
#include "esp_heap_caps.h"
#include "sdkconfig.h"
#define LEAKS (400)
void setUp(void)
{
unity_utils_record_free_mem();
}
void tearDown(void)
{
unity_utils_evaluate_leaks_direct(LEAKS);
}
void app_main(void)
{
/*
* ____ _ _ _______ _
*| _ \ | | | | |__ __| | |
*| |_) | _ _ | |_ | |_ ___ _ __ | | ___ ___ | |_
*| _ < | | | || __|| __|/ _ \ | '_ \ | | / _ \/ __|| __|
*| |_) || |_| || |_ | |_| (_) || | | | | || __/\__ \| |_
*|____/ \__,_| \__| \__|\___/ |_| |_| |_| \___||___/ \__|
*/
printf(" ____ _ _ _______ _ \n");
printf(" | _ \\ | | | | |__ __| | | \n");
printf(" | |_) | _ _ | |_ | |_ ___ _ __ | | ___ ___ | |_ \n");
printf(" | _ < | | | || __|| __|/ _ \\ | '_ \\ | | / _ \\/ __|| __|\n");
printf(" | |_) || |_| || |_ | |_| (_) || | | | | || __/\\__ \\| |_ \n");
printf(" |____/ \\__,_| \\__| \\__|\\___/ |_| |_| |_| \\___||___/ \\__|\n");
unity_run_menu();
}

View File

@ -0,0 +1,104 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "unity.h"
#include "iot_button.h"
#include "driver/gpio.h"
static const char *TAG = "CUSTOM BUTTON TEST";
#define BUTTON_IO_NUM 0
#define BUTTON_ACTIVE_LEVEL 0
static void button_event_cb(void *arg, void *data)
{
button_event_t event = iot_button_get_event(arg);
ESP_LOGI(TAG, "%s", iot_button_get_event_str(event));
if (BUTTON_PRESS_REPEAT == event || BUTTON_PRESS_REPEAT_DONE == event) {
ESP_LOGI(TAG, "\tREPEAT[%d]", iot_button_get_repeat(arg));
}
if (BUTTON_PRESS_UP == event || BUTTON_LONG_PRESS_HOLD == event || BUTTON_LONG_PRESS_UP == event) {
ESP_LOGI(TAG, "\tPressed Time[%"PRIu32"]", iot_button_get_pressed_time(arg));
}
if (BUTTON_MULTIPLE_CLICK == event) {
ESP_LOGI(TAG, "\tMULTIPLE[%d]", (int)data);
}
}
typedef struct {
button_driver_t base;
int32_t gpio_num; /**< num of gpio */
uint8_t active_level; /**< gpio level when press down */
} custom_gpio_obj;
static uint8_t button_get_key_level(button_driver_t *button_driver)
{
custom_gpio_obj *custom_btn = __containerof(button_driver, custom_gpio_obj, base);
int level = gpio_get_level(custom_btn->gpio_num);
return level == custom_btn->active_level ? 1 : 0;
}
static esp_err_t button_del(button_driver_t *button_driver)
{
return ESP_OK;
}
TEST_CASE("custom button test", "[button][custom]")
{
gpio_config_t gpio_conf = {
.pin_bit_mask = 1ULL << BUTTON_IO_NUM,
.mode = GPIO_MODE_INPUT,
.pull_up_en = 1,
.pull_down_en = 0,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&gpio_conf);
custom_gpio_obj *custom_btn = (custom_gpio_obj *)calloc(1, sizeof(custom_gpio_obj));
TEST_ASSERT_NOT_NULL(custom_btn);
custom_btn->active_level = BUTTON_ACTIVE_LEVEL;
custom_btn->gpio_num = BUTTON_IO_NUM;
button_handle_t btn = NULL;
const button_config_t btn_cfg = {0};
custom_btn->base.get_key_level = button_get_key_level;
custom_btn->base.del = button_del;
esp_err_t ret = iot_button_create(&btn_cfg, &custom_btn->base, &btn);
TEST_ASSERT(ESP_OK == ret);
TEST_ASSERT_NOT_NULL(btn);
iot_button_register_cb(btn, BUTTON_PRESS_DOWN, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT_DONE, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_SINGLE_CLICK, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_DOUBLE_CLICK, NULL, button_event_cb, NULL);
/*!< Multiple Click must provide button_event_args_t */
/*!< Double Click */
button_event_args_t args = {
.multiple_clicks.clicks = 2,
};
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)2);
/*!< Triple Click */
args.multiple_clicks.clicks = 3;
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)3);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_START, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_HOLD, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_END, NULL, button_event_cb, NULL);
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
iot_button_delete(btn);
}

View File

@ -0,0 +1,186 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "unity.h"
#include "iot_button.h"
#include "button_gpio.h"
static const char *TAG = "GPIO BUTTON TEST";
#define BUTTON_IO_NUM 0
#define BUTTON_ACTIVE_LEVEL 0
static void button_event_cb(void *arg, void *data)
{
button_event_t event = iot_button_get_event(arg);
ESP_LOGI(TAG, "%s", iot_button_get_event_str(event));
if (BUTTON_PRESS_REPEAT == event || BUTTON_PRESS_REPEAT_DONE == event) {
ESP_LOGI(TAG, "\tREPEAT[%d]", iot_button_get_repeat(arg));
}
if (BUTTON_PRESS_UP == event || BUTTON_LONG_PRESS_HOLD == event || BUTTON_LONG_PRESS_UP == event) {
ESP_LOGI(TAG, "\tPressed Time[%"PRIu32"]", iot_button_get_pressed_time(arg));
}
if (BUTTON_MULTIPLE_CLICK == event) {
ESP_LOGI(TAG, "\tMULTIPLE[%d]", (int)data);
}
}
TEST_CASE("gpio button test", "[button][gpio]")
{
const button_config_t btn_cfg = {0};
const button_gpio_config_t btn_gpio_cfg = {
.gpio_num = BUTTON_IO_NUM,
.active_level = BUTTON_ACTIVE_LEVEL,
};
button_handle_t btn = NULL;
esp_err_t ret = iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &btn);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btn);
iot_button_register_cb(btn, BUTTON_PRESS_DOWN, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT_DONE, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_SINGLE_CLICK, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_DOUBLE_CLICK, NULL, button_event_cb, NULL);
/*!< Multiple Click must provide button_event_args_t */
/*!< Double Click */
button_event_args_t args = {
.multiple_clicks.clicks = 2,
};
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)2);
/*!< Triple Click */
args.multiple_clicks.clicks = 3;
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)3);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_START, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_HOLD, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_END, NULL, button_event_cb, NULL);
uint8_t level = 0;
level = iot_button_get_key_level(btn);
ESP_LOGI(TAG, "button level is %d", level);
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
iot_button_delete(btn);
}
TEST_CASE("gpio button get event test", "[button][gpio][event test]")
{
const button_config_t btn_cfg = {0};
const button_gpio_config_t btn_gpio_cfg = {
.gpio_num = BUTTON_IO_NUM,
.active_level = BUTTON_ACTIVE_LEVEL,
};
button_handle_t btn = NULL;
esp_err_t ret = iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &btn);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btn);
uint8_t level = 0;
level = iot_button_get_key_level(btn);
ESP_LOGI(TAG, "button level is %d", level);
while (1) {
button_event_t event = iot_button_get_event(btn);
if (event != BUTTON_NONE_PRESS) {
ESP_LOGI(TAG, "event is %s", iot_button_get_event_str(event));
}
vTaskDelay(pdMS_TO_TICKS(1));
}
iot_button_delete(btn);
}
TEST_CASE("gpio button test power save", "[button][gpio][power save]")
{
const button_config_t btn_cfg = {0};
const button_gpio_config_t btn_gpio_cfg = {
.gpio_num = BUTTON_IO_NUM,
.active_level = BUTTON_ACTIVE_LEVEL,
.enable_power_save = true,
};
button_handle_t btn = NULL;
esp_err_t ret = iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &btn);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btn);
iot_button_register_cb(btn, BUTTON_PRESS_DOWN, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT_DONE, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_SINGLE_CLICK, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_DOUBLE_CLICK, NULL, button_event_cb, NULL);
/*!< Multiple Click must provide button_event_args_t */
/*!< Double Click */
button_event_args_t args = {
.multiple_clicks.clicks = 2,
};
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)2);
/*!< Triple Click */
args.multiple_clicks.clicks = 3;
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)3);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_START, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_HOLD, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_END, NULL, button_event_cb, NULL);
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
iot_button_delete(btn);
}
TEST_CASE("gpio button test memory leak", "[button][gpio][memory leak]")
{
const button_config_t btn_cfg = {0};
const button_gpio_config_t btn_gpio_cfg = {
.gpio_num = BUTTON_IO_NUM,
.active_level = BUTTON_ACTIVE_LEVEL,
};
button_handle_t btn = NULL;
esp_err_t ret = iot_button_new_gpio_device(&btn_cfg, &btn_gpio_cfg, &btn);
TEST_ASSERT(ret == ESP_OK);
TEST_ASSERT_NOT_NULL(btn);
iot_button_register_cb(btn, BUTTON_PRESS_DOWN, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_REPEAT_DONE, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_SINGLE_CLICK, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_DOUBLE_CLICK, NULL, button_event_cb, NULL);
/*!< Multiple Click must provide button_event_args_t */
/*!< Double Click */
button_event_args_t args = {
.multiple_clicks.clicks = 2,
};
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)2);
/*!< Triple Click */
args.multiple_clicks.clicks = 3;
iot_button_register_cb(btn, BUTTON_MULTIPLE_CLICK, &args, button_event_cb, (void *)3);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_START, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_HOLD, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_LONG_PRESS_UP, NULL, button_event_cb, NULL);
iot_button_register_cb(btn, BUTTON_PRESS_END, NULL, button_event_cb, NULL);
iot_button_delete(btn);
}

View File

@ -0,0 +1,75 @@
/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "unity.h"
#include "iot_button.h"
#include "button_matrix.h"
static const char *TAG = "MATRIX BUTTON TEST";
static void button_event_cb(void *arg, void *data)
{
button_event_t event = iot_button_get_event(arg);
ESP_LOGI(TAG, "BUTTON[%d] %s", (int)data, iot_button_get_event_str(event));
if (BUTTON_PRESS_REPEAT == event || BUTTON_PRESS_REPEAT_DONE == event) {
ESP_LOGI(TAG, "\tREPEAT[%d]", iot_button_get_repeat(arg));
}
if (BUTTON_PRESS_UP == event || BUTTON_LONG_PRESS_HOLD == event || BUTTON_LONG_PRESS_UP == event) {
ESP_LOGI(TAG, "\tPressed Time[%"PRIu32"]", iot_button_get_pressed_time(arg));
}
if (BUTTON_MULTIPLE_CLICK == event) {
ESP_LOGI(TAG, "\tMULTIPLE[%d]", (int)data);
}
}
TEST_CASE("matrix keyboard button test", "[button][matrix key]")
{
const button_config_t btn_cfg = {0};
const button_matrix_config_t matrix_cfg = {
.row_gpios = (int32_t[]){4, 5, 6, 7},
.col_gpios = (int32_t[]){3, 8, 16, 15},
.row_gpio_num = 4,
.col_gpio_num = 4,
};
button_handle_t btns[16] = {0};
size_t btn_num = 16;
esp_err_t ret = iot_button_new_matrix_device(&btn_cfg, &matrix_cfg, btns, &btn_num);
TEST_ASSERT(ret == ESP_OK);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
int index = i * 4 + j;
TEST_ASSERT_NOT_NULL(btns[index]);
iot_button_register_cb(btns[index], BUTTON_PRESS_DOWN, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_PRESS_UP, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_PRESS_REPEAT, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_PRESS_REPEAT_DONE, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_SINGLE_CLICK, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_DOUBLE_CLICK, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_LONG_PRESS_START, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_LONG_PRESS_HOLD, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_LONG_PRESS_UP, NULL, button_event_cb, (void *)index);
iot_button_register_cb(btns[index], BUTTON_PRESS_END, NULL, button_event_cb, (void *)index);
}
}
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
iot_button_delete(btns[i * 4 + j]);
}
}
}

View File

@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
'''
Steps to run these cases:
- Build
- . ${IDF_PATH}/export.sh
- pip install idf_build_apps
- python tools/build_apps.py components/button/test_apps -t esp32s3
- Test
- pip install -r tools/requirements/requirement.pytest.txt
- pytest components/button/test_apps --target esp32s3
'''
import pytest
from pytest_embedded import Dut
@pytest.mark.target('esp32s3')
@pytest.mark.env('button')
@pytest.mark.parametrize(
'config',
[
'defaults',
],
)
def test_button(dut: Dut)-> None:
dut.expect_exact('Press ENTER to see the list of tests.')
dut.write('[auto]')
dut.expect_unity_test_output(timeout = 300)

View File

@ -0,0 +1,9 @@
# For IDF 5.0
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_TASK_WDT_EN=n
# For IDF4.4
CONFIG_ESP32S2_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP_TASK_WDT=n

View File

@ -2233,6 +2233,19 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
# CONFIG_WIFI_PROV_STA_FAST_SCAN is not set
# end of Wi-Fi Provisioning Manager
#
# IoT Button
#
CONFIG_BUTTON_PERIOD_TIME_MS=5
CONFIG_BUTTON_DEBOUNCE_TICKS=2
CONFIG_BUTTON_SHORT_PRESS_TIME_MS=180
CONFIG_BUTTON_LONG_PRESS_TIME_MS=1500
CONFIG_BUTTON_LONG_PRESS_HOLD_SERIAL_TIME_MS=20
CONFIG_ADC_BUTTON_MAX_CHANNEL=3
CONFIG_ADC_BUTTON_MAX_BUTTON_PER_CHANNEL=8
CONFIG_ADC_BUTTON_SAMPLE_TIMES=1
# end of IoT Button
#
# CMake Utilities
#

View File

@ -1,16 +1,123 @@
1、BOOT按键
2、KEY按键
# 按键版电子吧唧功能规划
1、在Home界面单击KEY按键实现切换到Img界面、在Img界面继续单击Key按键切换下一张图片、可以循环进行显示双击BOOT按键返回Home界面
2、在Home界面单击BOOT界面可切换到Set界面双击BOOT按键返回Home界面
3、在Set界面按下KEY按键可以按顺序命中到节能图标、应援灯图标、删除图标和亮度调节图标在按下Key按键命中后点击BOOT按键可以达到点击图标的效果启用改图标对应功能双击BOOT按键返回Home界面
1如果命中节能图标时按下BOOT按键切换到节能模式、再次按下BOOT按键取消节能模式双击BOOT按键返回Home界面
2继续单击Key按键后切换命中到下一个图标比如应援灯图标单击BOOT按键进入应援灯模式、再次单击BOOT按键切换颜色双击BOOT按键返回Home界面
3继续单击Key按键后切换命中到下一个图标如删除图标单击BOOT按键后切换到Img显示界面后默认蓝色加粗边框显示该删除组件再次单击BOOT按键后进行删除当前图片顺位显示下一张图片取消显示ContainerDle逻辑与触屏版一致、单击KEY按键后切换下 一张图片显示长按2秒KEY按键取消删除取消显示ContainerDle逻辑与触屏版一致双击BOOT按键返回Home界面
4继续单击Key按键后切换命中到下一个图标如亮度调节图标单击BOOT按钮进行选中激活单击按下KEY按键亮度降低10%双击按下BOOT按键亮度增加10%双击BOOT按键返回Home界面
1在第3项Set界面中如果用户没有按下BOOT按键启用其中的功能那么单击KEY按键可以一直切换命中其他的图标
2在SET界面按下KEY选中该组件时是否可以使用蓝色加粗边框显示该组件被选中呢如果继续按下KEY按键那么下一个组件蓝色加粗边框显示上一个取消边框显示并且在双击BOOT按键返回Home界面或者离开SET后这些边框显示都取消
下次重新进入时都默认不显示、除非被Key按键单击选中了
3在删除图片时
4本次项目启用按键功能达到业务实现的原因为取消的触摸芯片的使用、节约成本
## 硬件按键
- **BOOT按键** (GPIO9):确认/执行/返回
- **KEY按键** (GPIO8):导航/切换
## 按键驱动
采用 ESP-IDF 官方 `iot_button` 组件,原生支持单击、双击、长按检测,替代原有自定义 GPIO 中断驱动。
## 全局规则
| 操作 | 功能 |
|------|------|
| 双击BOOT | 任何界面/模式下→返回Home |
| 任意键按下 | 熄屏状态→唤醒屏幕(仅唤醒,不触发业务) |
## 各界面按键功能
### 1. Home界面
| 操作 | 功能 |
|------|------|
| KEY单击 | 切换到Img界面图片浏览 |
| BOOT单击 | 切换到Set界面设置 |
| BOOT双击 | 无操作已在Home |
### 2. Img界面图片浏览
#### 正常浏览模式
| 操作 | 功能 |
|------|------|
| KEY单击 | 显示下一张图片(循环) |
| BOOT单击 | 返回Home界面 |
| BOOT双击 | 返回Home界面 |
#### 删除模式从Set界面进入显示ContainerDle
| 操作 | 功能 |
|------|------|
| BOOT单击 | 确认删除当前图片,顺位显示下一张 |
| KEY单击 | 取消删除隐藏ContainerDle回到正常浏览模式 |
| BOOT双击 | 返回Home界面 |
### 3. Set界面设置
#### 焦点导航
进入Set界面默认无焦点选中。KEY单击按顺序轮询选中功能图标
```
无选中 → 节能 → 应援灯 → 删除 → 亮度 → 无选中(循环)
```
- 选中的图标显示**蓝色加粗边框**
- 切换到下一个图标时,上一个取消边框
- 离开Set界面或返回Home后边框状态全部清除
- 下次进入Set界面默认无选中
#### 未选中状态
| 操作 | 功能 |
|------|------|
| KEY单击 | 选中第一个图标(节能) |
| BOOT单击 | 返回前一界面Home或Img |
| BOOT双击 | 返回Home界面 |
#### 已选中图标但未激活功能
| 操作 | 功能 |
|------|------|
| KEY单击 | 切换到下一个图标 |
| BOOT单击 | 激活当前选中图标的功能 |
| BOOT双击 | 返回Home界面 |
#### 各功能详细操作
**1节能模式**
- BOOT单击激活切换节能模式开/关
- 激活后立即生效,焦点保持在节能图标
- 继续KEY单击→移到下一个图标
**2应援灯手电筒**
- BOOT单击激活进入应援灯全屏模式
- 进入后按键上下文切换为应援灯模式:
| 操作 | 功能 |
|------|------|
| BOOT单击 | 切换颜色(红→绿→蓝循环) |
| KEY单击 | 退出应援灯返回Set界面 |
| BOOT双击 | 退出应援灯返回Home界面 |
**3删除图片**
- BOOT单击激活跳转Img界面显示删除容器ContainerDle
- 进入删除模式后按键行为见上方「Img界面-删除模式」
**4亮度调节**
- BOOT单击激活进入亮度调节模式
- 进入后按键行为:
| 操作 | 功能 |
|------|------|
| KEY单击 | 亮度-10%最低10% |
| BOOT单击 | 亮度+10%最高100% |
| KEY长按2秒 | 退出亮度调节,继续焦点轮询 |
| BOOT双击 | 返回Home界面 |
## 实施步骤
1. 添加 `iot_button` 组件依赖,替代自定义按键驱动
2. 新建按键导航管理器key_nav模块管理界面上下文和焦点状态
3. 改造各界面:移除触摸手势,添加按键驱动的导航逻辑
4. Set界面添加蓝色边框高亮样式
5. 适配休眠管理器
## 设计说明
1. **取消触摸芯片**:本次改版取消触摸芯片以降低成本,所有交互通过两个物理按键实现
2. **iot_button组件**使用ESP官方组件自带去抖、单击/双击/长按检测,稳定可靠
3. **上下文隔离**:不同界面/模式的按键行为完全独立,通过导航管理器统一调度
4. **双击BOOT全局返回**任何状态下双击BOOT都能回Home用户不会"迷路"