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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:57:14 +08:00

332 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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