1、按键驱动重构:从GPIO中断改为iot_button组件,支持BOOT/KEY2的单击/双击/长按6种事件; 2、新增按键导航管理器(key_nav):9种上下文状态机,统一分发按键事件到对应界面业务逻辑; 3、BLE模块改造:广播默认关闭由按键触发,新增设备间图片传输(GATT Client),支持扫描配对→MTU协商→分包发送; 4、新增6个UI界面:配对(Peiwang)、更新(Update)、发送等待(ImageShar)、接收等待(ImageReception)、发送中(Sharing)、接收中(Receiving); 5、新增电池指示器组件(battery_ui):支持多界面真实电量显示,3秒渐隐效果; 6、Home界面重构:移除手势事件,改为airhub背景图+电池指示器; 7、Img界面重构:移除触摸事件,新增删除二次确认边框机制; 8、禁用ScreenSet/ScreenChar界面(保留文件),禁用触摸初始化(保留代码可恢复); 9、sleep_mgr简化:移除ScreenSet亮度UI依赖,按键唤醒由key_nav统一处理; 10、新增9张图片资源(airhub背景、配对、传输状态、电池图标等); Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17 KiB
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 时间戳),只支持"按下"一种事件:
// 当前驱动(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 时间参数
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):
// GPIO 中断 + 手动 200ms 去抖,只支持"按下"
gpio_isr_handler_add(PIN_BTN_BOOT, gpio_isr_handler, ...);
目标(使用 iot_button 组件):
#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):
dependencies:
button: ">=3.2.0"
7.2 key_nav 导航管理器
从宝石角移植 key_nav 模块,核心结构:
// 导航上下文(根据当前界面和模式决定按键行为)
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 任务中执行,必须派发到独立任务:
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 焦点高亮样式
#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(瞬间切换,无从上到下刷新感):
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) |
九、实施步骤
- 添加 iot_button 依赖 →
idf_component.yml - 重构 button 模块 → 替换 GPIO ISR 为 iot_button,支持 6 种事件
- 新建 key_nav 模块 → 从宝石角移植,调整为两键方案(KEY2 双击=上一张,BOOT 长按=删除)
- 移除触摸事件 → 各 Screen 文件中删除手势/点击回调和
lv_obj_add_event_cb注册 - 适配 main.c → 删除旧的
boot_btn_handler(),添加key_nav_init() - 适配 sleep_mgr → 唤醒逻辑由 key_nav 统一处理
- 添加 lcd_fill_color() → 应援灯硬件级颜色切换
- 更新 CMakeLists.txt → 添加新源文件和头文件路径
- 编译测试 →
idf.py build - 逐功能验证 → 按第十章清单测试
十、测试验证清单
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 小时无崩溃