From 99d7e910f1fb0925e1ca9f130b52f7d4fc1ccbc6 Mon Sep 17 00:00:00 2001 From: Rdzleo Date: Fri, 27 Mar 2026 10:59:33 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=8F=90=E4=BA=A4=EF=BC=9A?= =?UTF-8?q?=E6=97=A0=E5=85=B3=E7=B4=A7=E8=A6=81=E7=9A=84=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=EF=BC=8C=E4=B8=8D=E7=94=A8=E7=90=86=E4=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 +- docs/电子吧唧按键功能规划文档.md | 476 +++++++++++++++++++++++++++++++ spiffs_image/03.jpg | Bin 8805 -> 0 bytes 3 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 docs/电子吧唧按键功能规划文档.md delete mode 100644 spiffs_image/03.jpg diff --git a/.vscode/settings.json b/.vscode/settings.json index 73e8293..e60f9b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -109,7 +109,7 @@ "random": "cpp", "*.obj": "cpp" }, - "idf.port": "/dev/tty.usbmodem834401", + "idf.port": "/dev/tty.usbmodem834101", "idf.espIdfPath": "/Users/rdzleo/esp/esp-idf/v5.4.2/esp-idf", "idf.toolsPath": "/Users/rdzleo/.espressif", "idf.pythonInstallPath": "/opt/homebrew/bin/python3", diff --git a/docs/电子吧唧按键功能规划文档.md b/docs/电子吧唧按键功能规划文档.md new file mode 100644 index 0000000..bea13af --- /dev/null +++ b/docs/电子吧唧按键功能规划文档.md @@ -0,0 +1,476 @@ +# 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 小时无崩溃 diff --git a/spiffs_image/03.jpg b/spiffs_image/03.jpg deleted file mode 100644 index da663a6a36e1d0c6c5d1b02d80c58ac3a755e290..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8805 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 Vd;c9hmY+YTf6giYhnNK#`7iO~Y@q-E