Baji_Rtc_Toy/main/dzbj/dzbj_button.c
Rdzleo 0735d45e52 feat: 从按键版迁移APP传图、设备间图片分享/接收、组合键模式切换功能
## 功能迁移清单(从 Dzbj_ESP32_S3_Key → Baji_Rtc_Toy)

### 1. 设备间BLE图片传输(GATT Client + 协议)
- 新增 ble_transfer.c/h:发送方 GATT Client 扫描→连接→MTU协商→分包写入
- 接收方复用现有 GATT Server(IMAGE_WRITE 0x0B01),协议完全兼容
- 发送完成/失败自动跳转 Img 界面并关闭蓝牙

### 2. APP传图显示 Update 界面
- 新增 ui_ScreenUpdate.c/h:更新进度界面(Gengxin背景 + Update_GIF动画)
- dzbj_ble.c WRITE_EVT 中通过 ble_transfer_is_receiving() 区分 APP传图 vs 设备间传输
- APP传图 → ScreenUpdate,设备间传输 → ScreenReceiving

### 3. KEY2 按键功能入口(iot_button 单击/双击/长按)
- KEY2 单击:开蓝牙 → Peiwang 配对界面(APP传图)
- KEY2 双击:接收模式 → ScreenImageReception(等待配对)
- KEY2 长按:发送模式 → ScreenImageShar(等待配对)
- 按键参数与按键版对齐:long_press_time=1200ms, short_press_time=300ms

### 4. BOOT+KEY2 组合键模式切换(替代 BOOT 长按3秒)
- BOOT 2秒长按 + KEY2 同时按下 → 触发模式切换
- 消除单键长按的误触发问题
- AI模式和吧唧模式均注册组合键

### 5. 按键上下文状态机
- btn_context_t 枚举:HOME/IMG/SET/PEIWANG/IMAGE_SHAR/IMAGE_RECEPTION/SHARING/RECEIVING/UPDATE
- 所有界面切换点(手势/按键/BLE自动跳转)同步设置 context
- BOOT 单击按 context 分发:Home无操作、Img/Set返回Home、配对退出蓝牙、传输等待取消

### 6. 新增 UI 界面(6个Screen + 7张图片)
- ScreenPeiwang:蓝牙配对等待
- ScreenUpdate:APP传图更新中
- ScreenImageShar:发送方等待配对
- ScreenImageReception:接收方等待配对
- ScreenSharing:发送方传输中
- ScreenReceiving:接收方接收中

### 7. 其他适配
- BLE 广播改为按需启动(dzbj_ble_start/stop/is_active)
- sleep_mgr 移除 KEY2 唤醒(仅 BOOT 唤醒屏幕)
- device_mode 新增模式切换按键抑制(防止重启后立即触发)
- battery_ui 电池指示器组件
- sdkconfig 启用 BLE GATTC 支持

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 10:55:17 +08:00

328 lines
9.9 KiB
C
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.

// 吧唧模式按键驱动
// BOOT(GPIO0): iot_button由board类管理此模块处理业务分发
// KEY2(GPIO4): iot_button此模块管理支持单击/双击/长按
// 界面上下文状态机决定按键行为
#include "dzbj_button.h"
#include "device_mode.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "iot_button.h"
#include "lvgl.h"
#include "esp_lvgl_port.h"
#include "sleep_mgr/include/sleep_mgr.h"
#include "dzbj_ble.h"
#include "ble_transfer.h"
#include "pages.h"
#include "ui/ui.h"
#include "ui/screens/ui_ScreenHome.h"
#include "ui/screens/ui_ScreenImg.h"
#include "ui/screens/ui_ScreenPeiwang.h"
#include "ui/screens/ui_ScreenImageShar.h"
#include "ui/screens/ui_ScreenImageReception.h"
static const char *TAG = "DZBJ_BTN";
// === 界面上下文状态 ===
static btn_context_t current_ctx = BTN_CTX_HOME;
// === KEY2 iot_button 实例 ===
static button_handle_t key2_handle = NULL;
// === 外部UI函数声明 ===
extern void ui_ScreenImg_hide_delete_container(void);
extern bool flashlight_is_active(void);
extern uint8_t flashlight_get_saved_brightness(void);
extern void flashlight_exit(void);
extern void pwm_set_brightness(uint8_t percent);
// === 上下文管理 ===
void dzbj_button_set_context(btn_context_t ctx)
{
current_ctx = ctx;
ESP_LOGI(TAG, "按键上下文切换: %d", ctx);
}
btn_context_t dzbj_button_get_context(void)
{
return current_ctx;
}
// === 退出到Home的通用逻辑在独立任务中执行可调用vTaskDelay ===
static void go_home_task(void *arg) {
sleep_mgr_notify_activity();
// 退出手电筒
bool was_flashlight = flashlight_is_active();
uint8_t saved_brightness = 0;
if (was_flashlight) {
saved_brightness = flashlight_get_saved_brightness();
flashlight_exit();
vTaskDelay(pdMS_TO_TICKS(80));
}
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
lvgl_port_unlock();
}
current_ctx = BTN_CTX_HOME;
if (was_flashlight) {
vTaskDelay(pdMS_TO_TICKS(150));
pwm_set_brightness(saved_brightness);
}
vTaskDelete(NULL);
}
// === 退出配对/等待界面 → Home ===
static void exit_peiwang_task(void *arg) {
ESP_LOGI(TAG, "退出配对界面,关闭蓝牙");
dzbj_ble_stop();
sleep_mgr_notify_activity();
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
lvgl_port_unlock();
}
current_ctx = BTN_CTX_HOME;
vTaskDelete(NULL);
}
static void exit_transfer_wait_task(void *arg) {
ESP_LOGI(TAG, "退出传输等待,取消传输");
ble_transfer_cancel();
sleep_mgr_notify_activity();
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
lvgl_port_unlock();
}
current_ctx = BTN_CTX_HOME;
vTaskDelete(NULL);
}
// === KEY2 进入功能界面的任务 ===
static void key2_enter_peiwang_task(void *arg) {
ESP_LOGI(TAG, "KEY2单击开蓝牙 → 配对界面");
sleep_mgr_notify_activity();
dzbj_ble_start();
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenPeiwang, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenPeiwang_screen_init);
lvgl_port_unlock();
}
current_ctx = BTN_CTX_PEIWANG;
vTaskDelete(NULL);
}
static void key2_enter_reception_task(void *arg) {
ESP_LOGI(TAG, "KEY2双击接收模式 → 等待配对");
sleep_mgr_notify_activity();
ble_transfer_start_receive();
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenImageReception, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImageReception_screen_init);
lvgl_port_unlock();
}
current_ctx = BTN_CTX_IMAGE_RECEPTION;
vTaskDelete(NULL);
}
static void key2_enter_share_task(void *arg) {
ESP_LOGI(TAG, "KEY2长按发送模式 → 等待配对");
sleep_mgr_notify_activity();
init_spiffs_image_list();
ble_transfer_start_send();
if (lvgl_port_lock(100)) {
_ui_screen_change(&ui_ScreenImageShar, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImageShar_screen_init);
lvgl_port_unlock();
}
current_ctx = BTN_CTX_IMAGE_SHAR;
vTaskDelete(NULL);
}
// === KEY2 事件回调在esp_timer中执行不能vTaskDelay派发到独立任务 ===
static void key2_click_cb(void *handle, void *usr_data) {
// 模式切换重启后按键抑制
if (device_mode_in_switch_suppress()) {
ESP_LOGI(TAG, "模式切换按键抑制期忽略KEY2单击");
return;
}
switch (current_ctx) {
case BTN_CTX_HOME:
case BTN_CTX_IMG:
// 开蓝牙 → 配对界面
xTaskCreate(key2_enter_peiwang_task, "k2_pw", 3072, NULL, 5, NULL);
break;
case BTN_CTX_PEIWANG:
// 退出配对 → Home
xTaskCreate(exit_peiwang_task, "k2_exit", 3072, NULL, 5, NULL);
break;
case BTN_CTX_IMAGE_SHAR:
case BTN_CTX_IMAGE_RECEPTION:
// 取消传输 → Home
xTaskCreate(exit_transfer_wait_task, "k2_exit", 3072, NULL, 5, NULL);
break;
default:
// SHARING/RECEIVING/UPDATE 不响应
break;
}
}
static void key2_double_click_cb(void *handle, void *usr_data) {
if (device_mode_in_switch_suppress()) return;
switch (current_ctx) {
case BTN_CTX_HOME:
case BTN_CTX_IMG:
// 接收模式
xTaskCreate(key2_enter_reception_task, "k2_recv", 3072, NULL, 5, NULL);
break;
default:
break;
}
}
static void key2_long_press_cb(void *handle, void *usr_data) {
if (device_mode_in_switch_suppress()) return;
// 组合键保护如果BOOT同时按下说明是模式切换组合键跳过
if (gpio_get_level(PIN_BTN_BOOT) == 0) {
ESP_LOGI(TAG, "BOOT+KEY2组合键检测跳过KEY2长按业务");
return;
}
switch (current_ctx) {
case BTN_CTX_HOME:
case BTN_CTX_IMG:
// 发送模式
xTaskCreate(key2_enter_share_task, "k2_shar", 3072, NULL, 5, NULL);
break;
default:
break;
}
}
// === 初始化 ===
esp_err_t dzbj_button_init(void)
{
// KEY2 使用 iot_button支持单击/双击/长按)
button_config_t key2_cfg = {
.type = BUTTON_TYPE_GPIO,
.long_press_time = 1200,
.short_press_time = 300, // 双击检测窗口300ms与按键版对齐
.gpio_button_config = {
.gpio_num = PIN_BTN_KEY2,
.active_level = 0, // 低电平有效
},
};
key2_handle = iot_button_create(&key2_cfg);
if (!key2_handle) {
ESP_LOGE(TAG, "KEY2 iot_button创建失败");
return ESP_FAIL;
}
// 注册KEY2事件
iot_button_register_cb(key2_handle, BUTTON_SINGLE_CLICK, key2_click_cb, NULL);
iot_button_register_cb(key2_handle, BUTTON_DOUBLE_CLICK, key2_double_click_cb, NULL);
iot_button_register_cb(key2_handle, BUTTON_LONG_PRESS_START, key2_long_press_cb, NULL);
ESP_LOGI(TAG, "按键初始化完成 (KEY2=GPIO%d iot_buttonBOOT由board类管理)", PIN_BTN_KEY2);
return ESP_OK;
}
// === BOOT+KEY2 组合键模式切换 ===
static void mode_switch_task(void *arg) {
ESP_LOGI(TAG, "执行模式切换...");
if (device_mode_is_badge()) {
ESP_LOGI(TAG, "吧唧模式 → AI模式");
device_mode_set(DEVICE_MODE_AI);
} else {
ESP_LOGI(TAG, "AI模式 → 吧唧模式");
device_mode_set(DEVICE_MODE_BADGE);
}
vTaskDelete(NULL);
}
static void mode_switch_combo_cb(void *handle, void *data) {
if (gpio_get_level(PIN_BTN_KEY2) == 0) {
ESP_LOGI(TAG, "BOOT+KEY2组合键2秒触发模式切换");
xTaskCreate(mode_switch_task, "mode_sw", 2048, NULL, 5, NULL);
}
}
void dzbj_button_register_mode_switch_combo(void *boot_handle)
{
if (!boot_handle) return;
button_event_config_t evt_cfg = {
.event = BUTTON_LONG_PRESS_START,
.event_data = {
.long_press = {
.press_time = 2000,
},
},
};
esp_err_t ret = iot_button_register_event_cb((button_handle_t)boot_handle, evt_cfg, mode_switch_combo_cb, NULL);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "BOOT+KEY2组合键模式切换注册成功(2000ms)");
} else {
ESP_LOGE(TAG, "组合键注册失败: %s", esp_err_to_name(ret));
}
}
// === BOOT 单击处理(根据上下文分发) ===
void dzbj_boot_click_handler(void)
{
if (device_mode_in_switch_suppress()) {
ESP_LOGI(TAG, "模式切换按键抑制期忽略BOOT单击");
return;
}
bool screen_was_off = sleep_mgr_is_screen_off();
if (screen_was_off) {
ESP_LOGI(TAG, "BOOT低功耗唤醒");
sleep_mgr_notify_activity();
return;
}
switch (current_ctx) {
case BTN_CTX_HOME:
// Home界面 BOOT单击无额外操作已在Home
sleep_mgr_notify_activity();
break;
case BTN_CTX_IMG:
case BTN_CTX_SET:
// Img/Set界面 BOOT单击返回Home
xTaskCreate(go_home_task, "boot_home", 4096, NULL, 5, NULL);
break;
case BTN_CTX_PEIWANG:
// 配对界面 BOOT单击关蓝牙 → Home
xTaskCreate(exit_peiwang_task, "boot_exit", 3072, NULL, 5, NULL);
break;
case BTN_CTX_IMAGE_SHAR:
case BTN_CTX_IMAGE_RECEPTION:
// 传输等待界面 BOOT单击取消 → Home
xTaskCreate(exit_transfer_wait_task, "boot_exit", 3072, NULL, 5, NULL);
break;
case BTN_CTX_SHARING:
case BTN_CTX_RECEIVING:
case BTN_CTX_UPDATE:
// 传输中/更新中不响应
break;
}
}