diff --git a/dependencies.lock b/dependencies.lock index 34e1584..6825770 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -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 diff --git a/docs/touch-to-button-migration.md b/docs/touch-to-button-migration.md new file mode 100644 index 0000000..90a4bf2 --- /dev/null +++ b/docs/touch-to-button-migration.md @@ -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 0x28:LCD停止输出 + lcd_fill_color(new_color); // 直接写GRAM,绕过LVGL(~35ms同步阻塞) + lcd_disp_on_off(true); // DISPON 0x29:LCD瞬间恢复,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 任务栈 | 3072B(btn_task 队列消费) | 0(iot_button 内部管理) | 节省 3KB | +| key_nav 任务栈 | 无 | 3072B(每次按键临时创建,执行完销毁) | 临时占用 | +| 触摸事件代码 | ~200 行(手势+点击回调) | 0 | 移除 | +| key_nav 代码 | 0 | ~530 行 | 新增 | +| iot_button 组件 | 无 | ~20KB Flash | 新增依赖 | diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f6802f5..f89c469 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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/" diff --git a/main/button/button.c b/main/button/button.c index 646d20b..b746cdd 100644 --- a/main/button/button.c +++ b/main/button/button.c @@ -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; diff --git a/main/button/include/button.h b/main/button/include/button.h index 37425ce..2e1578a 100644 --- a/main/button/include/button.h +++ b/main/button/include/button.h @@ -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); diff --git a/main/idf_component.yml b/main/idf_component.yml index 0e3ab61..14bea77 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -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 \ No newline at end of file + esp_jpeg: 1.3.1 + button: ">=3.2.0" \ No newline at end of file diff --git a/main/key_nav/include/key_nav.h b/main/key_nav/include/key_nav.h new file mode 100644 index 0000000..d314cd8 --- /dev/null +++ b/main/key_nav/include/key_nav.h @@ -0,0 +1,39 @@ +#pragma once +#include "lvgl.h" +#include +#include + +// 当前界面/模式上下文 +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); diff --git a/main/key_nav/key_nav.c b/main/key_nav/key_nav.c new file mode 100644 index 0000000..5ae97ea --- /dev/null +++ b/main/key_nav/key_nav.c @@ -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; +} diff --git a/main/lcd/include/lcd.h b/main/lcd/include/lcd.h index b3b2c19..926c606 100644 --- a/main/lcd/include/lcd.h +++ b/main/lcd/include/lcd.h @@ -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显示开关 \ No newline at end of file diff --git a/main/lcd/lcd.c b/main/lcd/lcd.c index 1c7c332..ef0cd06 100644 --- a/main/lcd/lcd.c +++ b/main/lcd/lcd.c @@ -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写入GRAM(esp_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) { diff --git a/main/main.c b/main/main.c index f55ba5d..93fb747 100644 --- a/main/main.c +++ b/main/main.c @@ -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", "系统初始化完成成功!");// 系统初始化完成成功 // ===================================================================== diff --git a/main/sleep_mgr/sleep_mgr.c b/main/sleep_mgr/sleep_mgr.c index 04d3a7d..6b27b68 100644 --- a/main/sleep_mgr/sleep_mgr.c +++ b/main/sleep_mgr/sleep_mgr.c @@ -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); diff --git a/main/ui/screens/ui_ScreenHome.c b/main/ui/screens/ui_ScreenHome.c index d41a6cc..6ee1f36 100644 --- a/main/ui/screens/ui_ScreenHome.c +++ b/main/ui/screens/ui_ScreenHome.c @@ -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 } diff --git a/main/ui/screens/ui_ScreenHome.h b/main/ui/screens/ui_ScreenHome.h index 55f83da..0ae97fc 100644 --- a/main/ui/screens/ui_ScreenHome.h +++ b/main/ui/screens/ui_ScreenHome.h @@ -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; diff --git a/main/ui/screens/ui_ScreenImg.c b/main/ui/screens/ui_ScreenImg.c index d7926f4..17a7f2d 100644 --- a/main/ui/screens/ui_ScreenImg.c +++ b/main/ui/screens/ui_ScreenImg.c @@ -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 事件中加载 diff --git a/main/ui/screens/ui_ScreenSet.c b/main/ui/screens/ui_ScreenSet.c index 2634df4..0d3c67f 100644 --- a/main/ui/screens/ui_ScreenSet.c +++ b/main/ui/screens/ui_ScreenSet.c @@ -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按键模块管理 } diff --git a/main/ui/screens/ui_ScreenSet.h b/main/ui/screens/ui_ScreenSet.h index ef78013..a38669a 100644 --- a/main/ui/screens/ui_ScreenSet.h +++ b/main/ui/screens/ui_ScreenSet.h @@ -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"*/ diff --git a/managed_components/espressif__button/.component_hash b/managed_components/espressif__button/.component_hash new file mode 100644 index 0000000..f178404 --- /dev/null +++ b/managed_components/espressif__button/.component_hash @@ -0,0 +1 @@ +d0afa32f0e50d60bc0c6fc23f7eea98adc6b02cfe70b590bc52c23c506745287 \ No newline at end of file diff --git a/managed_components/espressif__button/CHANGELOG.md b/managed_components/espressif__button/CHANGELOG.md new file mode 100644 index 0000000..8f61fdc --- /dev/null +++ b/managed_components/espressif__button/CHANGELOG.md @@ -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 diff --git a/managed_components/espressif__button/CHECKSUMS.json b/managed_components/espressif__button/CHECKSUMS.json new file mode 100644 index 0000000..96b84a3 --- /dev/null +++ b/managed_components/espressif__button/CHECKSUMS.json @@ -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"}]} \ No newline at end of file diff --git a/managed_components/espressif__button/CMakeLists.txt b/managed_components/espressif__button/CMakeLists.txt new file mode 100644 index 0000000..50e09d8 --- /dev/null +++ b/managed_components/espressif__button/CMakeLists.txt @@ -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}) diff --git a/managed_components/espressif__button/Kconfig b/managed_components/espressif__button/Kconfig new file mode 100644 index 0000000..dd2e518 --- /dev/null +++ b/managed_components/espressif__button/Kconfig @@ -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 diff --git a/managed_components/espressif__button/README.md b/managed_components/espressif__button/README.md new file mode 100644 index 0000000..3fd7d12 --- /dev/null +++ b/managed_components/espressif__button/README.md @@ -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=*" +``` \ No newline at end of file diff --git a/managed_components/espressif__button/button_adc.c b/managed_components/espressif__button/button_adc.c new file mode 100644 index 0000000..5bca066 --- /dev/null +++ b/managed_components/espressif__button/button_adc.c @@ -0,0 +1,327 @@ +/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#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; +} diff --git a/managed_components/espressif__button/button_gpio.c b/managed_components/espressif__button/button_gpio.c new file mode 100644 index 0000000..fc446f1 --- /dev/null +++ b/managed_components/espressif__button/button_gpio.c @@ -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; +} diff --git a/managed_components/espressif__button/button_matrix.c b/managed_components/espressif__button/button_matrix.c new file mode 100644 index 0000000..a3073f3 --- /dev/null +++ b/managed_components/espressif__button/button_matrix.c @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#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; +} diff --git a/managed_components/espressif__button/idf_component.yml b/managed_components/espressif__button/idf_component.yml new file mode 100644 index 0000000..2160e6c --- /dev/null +++ b/managed_components/espressif__button/idf_component.yml @@ -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 diff --git a/managed_components/espressif__button/include/button_adc.h b/managed_components/espressif__button/include/button_adc.h new file mode 100644 index 0000000..827a441 --- /dev/null +++ b/managed_components/espressif__button/include/button_adc.h @@ -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 diff --git a/managed_components/espressif__button/include/button_gpio.h b/managed_components/espressif__button/include/button_gpio.h new file mode 100644 index 0000000..a2cd7ee --- /dev/null +++ b/managed_components/espressif__button/include/button_gpio.h @@ -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 diff --git a/managed_components/espressif__button/include/button_matrix.h b/managed_components/espressif__button/include/button_matrix.h new file mode 100644 index 0000000..16ea909 --- /dev/null +++ b/managed_components/espressif__button/include/button_matrix.h @@ -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 diff --git a/managed_components/espressif__button/include/button_types.h b/managed_components/espressif__button/include/button_types.h new file mode 100644 index 0000000..3970c0a --- /dev/null +++ b/managed_components/espressif__button/include/button_types.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#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 diff --git a/managed_components/espressif__button/include/iot_button.h b/managed_components/espressif__button/include/iot_button.h new file mode 100644 index 0000000..416dcaf --- /dev/null +++ b/managed_components/espressif__button/include/iot_button.h @@ -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 diff --git a/managed_components/espressif__button/interface/button_interface.h b/managed_components/espressif__button/interface/button_interface.h new file mode 100644 index 0000000..d61d3fd --- /dev/null +++ b/managed_components/espressif__button/interface/button_interface.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#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 diff --git a/managed_components/espressif__button/iot_button.c b/managed_components/espressif__button/iot_button.c new file mode 100644 index 0000000..28539a4 --- /dev/null +++ b/managed_components/espressif__button/iot_button.c @@ -0,0 +1,710 @@ +/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#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; +} diff --git a/managed_components/espressif__button/license.txt b/managed_components/espressif__button/license.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/managed_components/espressif__button/license.txt @@ -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. diff --git a/managed_components/espressif__button/test_apps/CMakeLists.txt b/managed_components/espressif__button/test_apps/CMakeLists.txt new file mode 100644 index 0000000..180743a --- /dev/null +++ b/managed_components/espressif__button/test_apps/CMakeLists.txt @@ -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) diff --git a/managed_components/espressif__button/test_apps/main/CMakeLists.txt b/managed_components/espressif__button/test_apps/main/CMakeLists.txt new file mode 100644 index 0000000..1df85f6 --- /dev/null +++ b/managed_components/espressif__button/test_apps/main/CMakeLists.txt @@ -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) diff --git a/managed_components/espressif__button/test_apps/main/adc_button_test.c b/managed_components/espressif__button/test_apps/main/adc_button_test.c new file mode 100644 index 0000000..2b73a94 --- /dev/null +++ b/managed_components/espressif__button/test_apps/main/adc_button_test.c @@ -0,0 +1,134 @@ +/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#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]); + } +} diff --git a/managed_components/espressif__button/test_apps/main/auto_test.c b/managed_components/espressif__button/test_apps/main/auto_test.c new file mode 100644 index 0000000..967cd95 --- /dev/null +++ b/managed_components/espressif__button/test_apps/main/auto_test.c @@ -0,0 +1,288 @@ +/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#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)); +} diff --git a/managed_components/espressif__button/test_apps/main/button_test_main.c b/managed_components/espressif__button/test_apps/main/button_test_main.c new file mode 100644 index 0000000..547fd07 --- /dev/null +++ b/managed_components/espressif__button/test_apps/main/button_test_main.c @@ -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(); +} diff --git a/managed_components/espressif__button/test_apps/main/custom_button_test.c b/managed_components/espressif__button/test_apps/main/custom_button_test.c new file mode 100644 index 0000000..e4df2b6 --- /dev/null +++ b/managed_components/espressif__button/test_apps/main/custom_button_test.c @@ -0,0 +1,104 @@ +/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#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); +} diff --git a/managed_components/espressif__button/test_apps/main/gpio_button_test.c b/managed_components/espressif__button/test_apps/main/gpio_button_test.c new file mode 100644 index 0000000..064c1b8 --- /dev/null +++ b/managed_components/espressif__button/test_apps/main/gpio_button_test.c @@ -0,0 +1,186 @@ +/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#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); +} diff --git a/managed_components/espressif__button/test_apps/main/matrix_button_test.c b/managed_components/espressif__button/test_apps/main/matrix_button_test.c new file mode 100644 index 0000000..80e54b5 --- /dev/null +++ b/managed_components/espressif__button/test_apps/main/matrix_button_test.c @@ -0,0 +1,75 @@ +/* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#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]); + } + } +} diff --git a/managed_components/espressif__button/test_apps/pytest_button.py b/managed_components/espressif__button/test_apps/pytest_button.py new file mode 100644 index 0000000..1aa4dce --- /dev/null +++ b/managed_components/espressif__button/test_apps/pytest_button.py @@ -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) diff --git a/managed_components/espressif__button/test_apps/sdkconfig.defaults b/managed_components/espressif__button/test_apps/sdkconfig.defaults new file mode 100644 index 0000000..3778471 --- /dev/null +++ b/managed_components/espressif__button/test_apps/sdkconfig.defaults @@ -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 diff --git a/sdkconfig b/sdkconfig index 65258af..a5e9766 100644 --- a/sdkconfig +++ b/sdkconfig @@ -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 # diff --git a/按键版点击吧唧功能规划.md b/按键版点击吧唧功能规划.md index 270f7ff..78ba34a 100644 --- a/按键版点击吧唧功能规划.md +++ b/按键版点击吧唧功能规划.md @@ -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,用户不会"迷路"