# Dzbj_ESP32_S3 纯电子吧唧 — 两键实现三键功能规划

> 更新日期：2026-03-25
> 目标项目：Dzbj_ESP32_S3（纯电子吧唧，无 AI 对话模式）
> 参考实现：Baji_Rtc_Toy_Key（宝石角按键版）的 key_nav 模块
> 硬件平台：ESP32-S3-N16R8，与 Baji_Rtc_Toy 开发板相同

---

## 一、硬件现状

### 1.1 物理按键

| 按键 | GPIO | 电气特性 | 可用于业务控制 |
|------|------|---------|:---:|
| **BOOT** | GPIO0 | 低电平有效，内部上拉 | ✅ |
| **KEY2** | GPIO4 | 低电平有效，内部上拉 | ✅ |
| **SW1** | — | 物理电源开关，未接 MCU GPIO | ❌ |

> SW1 为纯硬件断电开关，无法被软件检测，不参与业务逻辑。

### 1.2 当前按键驱动（待重构）

当前使用 GPIO ISR + 手动去抖（200ms 时间戳），**只支持"按下"一种事件**：

```c
// 当前驱动（button.c）
gpio_isr_handler() → xQueueSend → btn_task() → 200ms去抖 → 回调
```

**问题**：2 个按键 × 1 种事件 = **只有 2 种操作**，无法覆盖宝石角的全部功能。

---

## 二、宝石角（3 键）功能分析

宝石角按键版有 3 个按键：**BOOT**、**KEY1**、**KEY2**。通过 `iot_button` 组件支持单击/双击/长按，加上 key_nav 导航管理器，实现了完整的无触摸交互。

### 2.1 宝石角各界面按键行为

| 界面 | BOOT 单击 | KEY 单击 | BOOT 双击 | KEY 长按 |
|------|----------|---------|----------|---------|
| **Home** | → Set | → Img | — | — |
| **Img（浏览）** | → Home | 下一张图片 | → Home | — |
| **Img（删除确认）** | 确认删除 | 取消（退出删除模式） | → Home | — |
| **Set（无焦点）** | → Home | 选中第一个图标 | → Home | — |
| **Set（有焦点）** | 执行选中功能 | 切换到下一个焦点 | → Home | — |
| **Set（亮度调节）** | 亮度 +10% | 亮度 -10% | → Home | 退出调节模式 |
| **应援灯** | 切换颜色 | 退出 → Set | → Home | — |

### 2.2 宝石角 Set 界面焦点系统

焦点通过蓝色边框高亮（`#2196F3`，3px），KEY 单击循环切换：

```
节能 → 应援灯 → 删除 → 亮度 → 节能 → ...
```

BOOT 单击执行当前焦点对应的功能（进入应援灯、进入亮度调节、进入删除模式等）。

---

## 三、两键方案设计

### 3.1 核心思路

用 `iot_button` 组件替换当前 GPIO ISR 驱动，为 2 个按键获得 **6 种事件**：

| 按键 | 单击 | 双击 | 长按 |
|------|------|------|------|
| **BOOT (GPIO0)** | ✅ | ✅ | ✅ |
| **KEY2 (GPIO4)** | ✅ | ✅ | ✅ |

2 键 × 3 种事件 = **6 种操作**，完全覆盖宝石角 3 键的功能需求。

### 3.2 按键角色定义

| 按键 | 角色 | 助记 |
|------|------|------|
| **BOOT** | 确认 / 执行 / 返回 | 右手拇指（主操作） |
| **KEY2** | 导航 / 切换 / 浏览 | 左手拇指（辅助操作） |

### 3.3 iot_button 时间参数

```c
button_config_t btn_cfg = {
    .long_press_time = 2000,    // 长按阈值 2 秒
    .short_press_time = 0,      // 双击检测窗口 180ms（默认值）
};
```

| 事件 | 触发条件 |
|------|---------|
| 单击 | 按下后 180ms 内无第二次按下 |
| 双击 | 180ms 内连续按下两次 |
| 长按 | 持续按住超过 2 秒 |

---

## 四、完整按键映射表

### 4.1 屏幕关闭（低功耗模式）

| 按键 | 任何操作 | 行为 |
|------|---------|------|
| BOOT | 按下 | 唤醒屏幕，恢复亮度，不触发业务 |
| KEY2 | 按下 | 唤醒屏幕，恢复亮度，不触发业务 |

> 唤醒后回到休眠前的界面。

### 4.2 Home 界面

| 按键 | 事件 | 行为 | 说明 |
|------|------|------|------|
| BOOT | 单击 | → **Set** 界面 | 进入设置 |
| KEY2 | 单击 | → **Img** 界面 | 进入图片浏览 |
| BOOT | 长按 | — | 预留（可用于关机提示/重置等） |
| KEY2 | 长按 | — | 预留 |

### 4.3 Img 界面（图片浏览）

| 按键 | 事件 | 行为 | 说明 |
|------|------|------|------|
| KEY2 | 单击 | **下一张**图片 | 自动跳过解码失败的无效图片 |
| KEY2 | 双击 | **上一张**图片 | 自动跳过无效图片 |
| BOOT | 单击 | → **Home** 界面 | 返回主界面 |
| BOOT | 长按 | 进入 **删除确认** 模式 | 替代宝石角的 Set→删除→Img 路径 |

> 宝石角用左右两个按键分别切换上/下一张。本方案用 KEY2 单击=下一张、双击=上一张，一个按键两个方向。

### 4.4 Img 界面（删除确认模式）

BOOT 长按触发后，显示删除确认 UI（ContainerDle）：

| 按键 | 事件 | 行为 | 说明 |
|------|------|------|------|
| BOOT | 单击 | **确认删除**当前图片 | 删除后自动显示下一张 |
| KEY2 | 单击 | **取消**，退出删除模式 | 隐藏 ContainerDle |

### 4.5 Set 界面（焦点导航模式）

进入 Set 界面后，默认无焦点。KEY2 单击开始焦点循环：

```
无焦点 → 节能 → 应援灯 → 亮度 → (无焦点) → 节能 → ...
```

> 注：相比宝石角去掉了"删除"焦点项。删除功能已移到 Img 界面的 BOOT 长按中，避免 Set→Img 的复杂跳转。

| 按键 | 事件 | 行为 | 说明 |
|------|------|------|------|
| KEY2 | 单击 | **切换焦点**到下一个图标 | 蓝色边框高亮 |
| BOOT | 单击（无焦点时） | → **Home** 界面 | 返回 |
| BOOT | 单击（有焦点时） | **执行**选中功能 | 进入节能/应援灯/亮度调节 |
| BOOT | 单击（在焦点项上） | → 对应功能 | 详见下方 |

**各焦点项的执行行为**：

| 焦点项 | BOOT 单击执行 |
|--------|-------------|
| 节能 | 切换节能模式开/关（亮度降至 10% + 10s 超时熄屏） |
| 应援灯 | 进入应援灯全屏模式 |
| 亮度 | 进入亮度调节模式 |

### 4.6 Set 界面（亮度调节模式）

从焦点"亮度"按 BOOT 进入，滑块高亮显示：

| 按键 | 事件 | 行为 | 说明 |
|------|------|------|------|
| BOOT | 单击 | 亮度 **+10%** | 上限 100% |
| KEY2 | 单击 | 亮度 **-10%** | 下限 10% |
| KEY2 | 长按 | **退出**亮度调节模式 | 回到焦点导航 |

### 4.7 应援灯全屏模式

从焦点"应援灯"按 BOOT 进入：

| 按键 | 事件 | 行为 | 说明 |
|------|------|------|------|
| BOOT | 单击 | **切换颜色**（红→绿→蓝→循环） | LCD 硬件级写 GRAM |
| KEY2 | 单击 | **退出** → Set 界面 | 恢复原亮度 |

### 4.8 ScreenChar 角色动画界面（如有）

| 按键 | 事件 | 行为 | 说明 |
|------|------|------|------|
| BOOT | 单击 | 切换动画播放/暂停 | 保持原有逻辑 |

---

## 五、与宝石角功能对照

### 5.1 功能覆盖对比

| 功能 | 宝石角（3 键） | 本方案（2 键） | 实现方式 |
|------|:---:|:---:|------|
| 界面导航（Home/Img/Set） | ✅ | ✅ | BOOT=确认/返回，KEY2=切换 |
| 下一张图片 | ✅ KEY 单击 | ✅ KEY2 单击 | 相同 |
| 上一张图片 | ✅ 另一个KEY | ✅ KEY2 双击 | 双击替代第三键 |
| 删除图片 | ✅ Set→删除→Img | ✅ Img 中 BOOT 长按 | 更直接，少一步跳转 |
| Set 焦点导航 | ✅ | ✅ | 相同 |
| 亮度调节 | ✅ | ✅ | 相同 |
| 应援灯 | ✅ | ✅ | 相同 |
| 节能开关 | ✅ | ✅ | 相同 |
| 低功耗唤醒 | ✅ | ✅ | 两键均可唤醒 |
| 跳过无效图片 | ✅ | ✅ | 自动跳过（已实现） |

### 5.2 操作差异

| 操作 | 宝石角 | 本方案 | 差异说明 |
|------|--------|--------|---------|
| 上一张图片 | 独立按键单击 | KEY2 双击 | 需要快速双击，操作稍慢 |
| 删除图片入口 | Set 界面焦点 | Img 界面 BOOT 长按 | 更直接，浏览时即可删除 |
| 退出亮度调节 | KEY 长按 | KEY2 长按 | 相同 |

---

## 六、界面导航图

```
                    ┌──────────────────┐
                    │   Home（主界面）   │
                    │                  │
                    │  BOOT→Set        │
                    │  KEY2→Img        │
                    └───┬──────────┬───┘
                        │          │
              BOOT单击   │          │  KEY2单击
                        ▼          ▼
          ┌─────────────────┐  ┌──────────────────────┐
          │  Set（设置界面）  │  │  Img（图片浏览界面）   │
          │                 │  │                      │
          │  KEY2：焦点切换  │  │  KEY2单击：下一张      │
          │  BOOT：执行/返回 │  │  KEY2双击：上一张      │
          │                 │  │  BOOT单击：返回Home    │
          │  ┌────────────┐ │  │  BOOT长按：删除确认    │
          │  │焦点项：     │ │  │                      │
          │  │ 节能       │ │  │  ┌──────────────────┐ │
          │  │ 应援灯 ──┐ │ │  │  │  删除确认模式     │ │
          │  │ 亮度 ──┐ │ │ │  │  │  BOOT：确认删除   │ │
          │  └───────┼─┼─┘ │  │  │  KEY2：取消        │ │
          └──────────┼─┼───┘  │  └──────────────────┘ │
                     │ │      └──────────────────────┘
                     │ │
           ┌─────────┘ └──────────┐
           ▼                      ▼
  ┌─────────────────┐   ┌──────────────────┐
  │  亮度调节模式     │   │  应援灯全屏模式   │
  │                 │   │                  │
  │  BOOT：亮度+10% │   │  BOOT：切换颜色   │
  │  KEY2：亮度-10%  │   │  KEY2：退出→Set  │
  │  KEY2长按：退出   │   │                  │
  └─────────────────┘   └──────────────────┘
```

---

## 七、技术实现要点

### 7.1 按键驱动重构：GPIO ISR → iot_button

**当前**（button.c）：
```c
// GPIO 中断 + 手动 200ms 去抖，只支持"按下"
gpio_isr_handler_add(PIN_BTN_BOOT, gpio_isr_handler, ...);
```

**目标**（使用 iot_button 组件）：
```c
#include "iot_button.h"

button_config_t btn_cfg = {
    .long_press_time = 2000,
    .short_press_time = 0,  // 默认 180ms 双击窗口
};
button_gpio_config_t gpio_cfg = {
    .gpio_num = PIN_BTN_BOOT,
    .active_level = 0,  // 低电平有效
};

button_handle_t boot_handle;
iot_button_new_gpio_device(&btn_cfg, &gpio_cfg, &boot_handle);

// 注册三种事件
iot_button_register_cb(boot_handle, BUTTON_SINGLE_CLICK, NULL, boot_click_cb, NULL);
iot_button_register_cb(boot_handle, BUTTON_DOUBLE_CLICK, NULL, boot_dblclick_cb, NULL);
iot_button_register_cb(boot_handle, BUTTON_LONG_PRESS_START, NULL, boot_longpress_cb, NULL);
```

**依赖添加**（idf_component.yml）：
```yaml
dependencies:
  button: ">=3.2.0"
```

### 7.2 key_nav 导航管理器

从宝石角移植 key_nav 模块，核心结构：

```c
// 导航上下文（根据当前界面和模式决定按键行为）
typedef enum {
    NAV_CTX_HOME,           // Home 界面
    NAV_CTX_IMG,            // Img 浏览
    NAV_CTX_IMG_DELETE,     // Img 删除确认
    NAV_CTX_SET,            // Set 焦点导航
    NAV_CTX_SET_BRIGHTNESS, // 亮度调节
    NAV_CTX_FLASHLIGHT,     // 应援灯
} nav_context_t;

// Set 焦点项（比宝石角少了"删除"项）
typedef enum {
    SET_FOCUS_NONE = -1,
    SET_FOCUS_LOW_POWER = 0,
    SET_FOCUS_FLASHLIGHT,
    SET_FOCUS_BRIGHTNESS,
    SET_FOCUS_COUNT,
} set_focus_item_t;
```

### 7.3 回调中禁止 vTaskDelay（关键约束）

`iot_button` 回调在 `esp_timer` 任务中执行，必须派发到独立任务：

```c
static void boot_click_cb(void *arg, void *data) {
    // ❌ 禁止：vTaskDelay(pdMS_TO_TICKS(100));
    // ✅ 正确：派发到独立任务
    xTaskCreate(nav_boot_click_task, "nav_boot", 3072, NULL, 5, NULL);
}

static void nav_boot_click_task(void *arg) {
    // 这里可以安全地 vTaskDelay、修改 LVGL 等
    if (sleep_mgr_is_screen_off()) {
        sleep_mgr_notify_activity();
    } else {
        // 根据当前 nav_context 执行对应操作
        handle_boot_click();
    }
    vTaskDelete(NULL);
}
```

### 7.4 焦点高亮样式

```c
#define FOCUS_BORDER_COLOR  0x2196F3  // Material Blue
#define FOCUS_BORDER_WIDTH  3

static void set_focus_border(lv_obj_t *obj, bool active) {
    lvgl_port_lock(0);
    if (active) {
        lv_obj_set_style_border_color(obj, lv_color_hex(FOCUS_BORDER_COLOR), LV_PART_MAIN);
        lv_obj_set_style_border_width(obj, FOCUS_BORDER_WIDTH, LV_PART_MAIN);
        lv_obj_set_style_border_opa(obj, LV_OPA_COVER, LV_PART_MAIN);
    } else {
        lv_obj_set_style_border_opa(obj, LV_OPA_TRANSP, LV_PART_MAIN);
    }
    lvgl_port_unlock();
}
```

### 7.5 应援灯颜色切换优化

绕过 LVGL 分band渲染，直接写 LCD GRAM（瞬间切换，无从上到下刷新感）：

```c
void flashlight_switch_color(void) {
    lcd_disp_on_off(false);     // DISPOFF：LCD 停止输出
    lcd_fill_color(new_color);  // 直接写 GRAM（~35ms）
    lcd_disp_on_off(true);      // DISPON：瞬间恢复，画面已完整
}
```

---

## 八、文件变更清单

### 8.1 需要重构的文件

| 文件 | 变更内容 |
|------|---------|
| `main/button/button.c` | GPIO ISR → iot_button 组件，支持单击/双击/长按 |
| `main/button/include/button.h` | 新增事件类型枚举，回调签名变更 |
| `main/main.c` | 移除 `boot_btn_handler()`（~65 行），集成 `key_nav_init()` |
| `main/ui/screens/ui_ScreenHome.c` | 移除手势事件函数 |
| `main/ui/screens/ui_ScreenImg.c` | 移除手势/点击事件，保留 SCREEN_LOADED |
| `main/ui/screens/ui_ScreenSet.c` | 移除手势/滑块/点击事件 |
| `main/sleep_mgr/sleep_mgr.c` | 移除按键回调注册，由 key_nav 统一处理 |
| `main/CMakeLists.txt` | 添加 key_nav 源文件和头文件路径 |
| `main/idf_component.yml` | 添加 `button: ">=3.2.0"` 依赖 |

### 8.2 需要新增的文件

| 文件 | 功能 |
|------|------|
| `main/key_nav/key_nav.c` | 按键导航管理器（从宝石角移植，适配两键方案） |
| `main/key_nav/include/key_nav.h` | 导航上下文枚举 + 焦点状态 |

### 8.3 需要新增的函数

| 函数 | 文件 | 功能 |
|------|------|------|
| `lcd_fill_color(uint32_t color)` | `main/lcd/lcd.c` | 直接写 GRAM 填充颜色（应援灯优化） |
| `flashlight_switch_color()` | `ui_ScreenSet.c` | 应援灯颜色切换（调用 lcd_fill_color） |

---

## 九、实施步骤

1. **添加 iot_button 依赖** → `idf_component.yml`
2. **重构 button 模块** → 替换 GPIO ISR 为 iot_button，支持 6 种事件
3. **新建 key_nav 模块** → 从宝石角移植，调整为两键方案（KEY2 双击=上一张，BOOT 长按=删除）
4. **移除触摸事件** → 各 Screen 文件中删除手势/点击回调和 `lv_obj_add_event_cb` 注册
5. **适配 main.c** → 删除旧的 `boot_btn_handler()`，添加 `key_nav_init()`
6. **适配 sleep_mgr** → 唤醒逻辑由 key_nav 统一处理
7. **添加 lcd_fill_color()** → 应援灯硬件级颜色切换
8. **更新 CMakeLists.txt** → 添加新源文件和头文件路径
9. **编译测试** → `idf.py build`
10. **逐功能验证** → 按第十章清单测试

---

## 十、测试验证清单

### 10.1 基础功能

- [ ] BOOT 单击 / 双击 / 长按 事件均可正确触发
- [ ] KEY2 单击 / 双击 / 长按 事件均可正确触发
- [ ] 屏幕关闭时任意按键唤醒，不触发业务

### 10.2 Home 界面

- [ ] BOOT 单击 → 进入 Set
- [ ] KEY2 单击 → 进入 Img

### 10.3 Img 界面

- [ ] KEY2 单击 → 下一张图片（自动跳过无效图片）
- [ ] KEY2 双击 → 上一张图片（自动跳过无效图片）
- [ ] BOOT 单击 → 返回 Home
- [ ] BOOT 长按 → 显示删除确认
- [ ] 删除确认中 BOOT 单击 → 删除成功
- [ ] 删除确认中 KEY2 单击 → 取消

### 10.4 Set 界面

- [ ] KEY2 单击 → 焦点循环切换（蓝色边框）
- [ ] BOOT 单击（无焦点）→ 返回 Home
- [ ] BOOT 单击（节能焦点）→ 切换节能模式
- [ ] BOOT 单击（应援灯焦点）→ 进入应援灯
- [ ] BOOT 单击（亮度焦点）→ 进入亮度调节

### 10.5 亮度调节

- [ ] BOOT 单击 → 亮度 +10%（上限 100%）
- [ ] KEY2 单击 → 亮度 -10%（下限 10%）
- [ ] KEY2 长按 → 退出亮度调节模式

### 10.6 应援灯

- [ ] BOOT 单击 → 颜色切换（红→绿→蓝→红）
- [ ] KEY2 单击 → 退出回到 Set

### 10.7 低功耗

- [ ] 10 秒无操作 → 屏幕熄灭
- [ ] 任意按键 → 唤醒，显示熄屏前的界面
- [ ] 唤醒时不闪烁（先渲染再恢复亮度）

### 10.8 稳定性

- [ ] 快速连续按键不崩溃（防抖正常）
- [ ] 连续切换 20+ 张图片无内存泄漏
- [ ] 长时间运行 1 小时无崩溃
