Baji_Rtc_Toy/main/ui/battery_ui.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

191 lines
6.1 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.

// 电池指示器模块
// 屏幕顶部药丸容器 + 电池图标LVGL基元绘制
// 仅 Home/Img 界面使用每次进入界面或切图时显示2秒后渐隐消失
#include "ui.h"
#include "battery_ui.h"
// === 刘海容器参数(小药丸形,悬浮) ===
#define NOTCH_W 50
#define NOTCH_H 20
#define NOTCH_Y 8
#define NOTCH_RADIUS 10
#define NOTCH_BG_COLOR 0x000000
#define NOTCH_BG_OPA 255
// === 电池图标参数 ===
#define BAT_W 24
#define BAT_H 12
#define BAT_BORDER 2
#define BAT_RADIUS 3
#define BAT_OUTLINE_CLR 0xBBBBBB
#define BAT_CAP_W 3
#define BAT_CAP_H 6
#define BAT_CAP_RADIUS 1
#define BAT_PAD 1
// === 填充条参数 ===
#define FILL_MAX_W (BAT_W - 2 * BAT_BORDER - 2 * BAT_PAD)
#define FILL_H (BAT_H - 2 * BAT_BORDER - 2 * BAT_PAD)
#define FILL_RADIUS 1
// === 颜色 ===
#define BAT_COLOR_GREEN 0x4CAF50
#define BAT_COLOR_RED 0xF44336
#define BAT_LOW_THRESH 20
// === 显示时间和动画参数 ===
#define SHOW_DURATION_MS 2000
#define FADE_DURATION_MS 500
// === 容器跟踪 ===
#define MAX_SLOTS 2
typedef struct {
lv_obj_t *notch;
lv_obj_t *fill;
int fill_x;
int fill_y;
} bat_slot_t;
static bat_slot_t slots[MAX_SLOTS];
static int slot_count = 0;
static lv_timer_t *hide_timer = NULL;
static int cached_level = 100;
// === 渐隐动画回调 ===
static void fade_anim_cb(void *obj, int32_t value) {
lv_obj_set_style_opa((lv_obj_t *)obj, (lv_opa_t)value, 0);
}
static void start_fade_out(lv_obj_t *notch) {
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, notch);
lv_anim_set_values(&a, LV_OPA_COVER, LV_OPA_TRANSP);
lv_anim_set_time(&a, FADE_DURATION_MS);
lv_anim_set_exec_cb(&a, fade_anim_cb);
lv_anim_start(&a);
}
static void hide_timer_cb(lv_timer_t *timer) {
for (int i = 0; i < slot_count; i++) {
if (lv_obj_is_valid(slots[i].notch)) {
start_fade_out(slots[i].notch);
}
}
lv_timer_del(timer);
hide_timer = NULL;
}
void battery_ui_add_to_screen(lv_obj_t *screen, int level) {
if (level < 0) level = 0;
if (level > 100) level = 100;
cached_level = level;
if (slot_count >= MAX_SLOTS) return;
// 1. 药丸容器(初始隐藏)
lv_obj_t *notch = lv_obj_create(screen);
lv_obj_remove_style_all(notch);
lv_obj_set_size(notch, NOTCH_W, NOTCH_H);
lv_obj_set_align(notch, LV_ALIGN_TOP_MID);
lv_obj_set_y(notch, NOTCH_Y);
lv_obj_set_style_bg_color(notch, lv_color_hex(NOTCH_BG_COLOR), 0);
lv_obj_set_style_bg_opa(notch, NOTCH_BG_OPA, 0);
lv_obj_set_style_radius(notch, NOTCH_RADIUS, 0);
lv_obj_set_style_border_width(notch, 0, 0);
lv_obj_clear_flag(notch, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_opa(notch, LV_OPA_TRANSP, 0);
// 2. 电池主体
int bat_total_w = BAT_W + BAT_CAP_W;
int bat_x = (NOTCH_W - bat_total_w) / 2;
int bat_y = (NOTCH_H - BAT_H) / 2;
lv_obj_t *body = lv_obj_create(notch);
lv_obj_remove_style_all(body);
lv_obj_set_size(body, BAT_W, BAT_H);
lv_obj_set_pos(body, bat_x, bat_y);
lv_obj_set_style_bg_opa(body, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_color(body, lv_color_hex(BAT_OUTLINE_CLR), 0);
lv_obj_set_style_border_width(body, BAT_BORDER, 0);
lv_obj_set_style_radius(body, BAT_RADIUS, 0);
lv_obj_clear_flag(body, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
// 3. 电池帽
lv_obj_t *cap = lv_obj_create(notch);
lv_obj_remove_style_all(cap);
lv_obj_set_size(cap, BAT_CAP_W, BAT_CAP_H);
lv_obj_set_pos(cap, bat_x + BAT_W, bat_y + (BAT_H - BAT_CAP_H) / 2);
lv_obj_set_style_bg_color(cap, lv_color_hex(BAT_OUTLINE_CLR), 0);
lv_obj_set_style_bg_opa(cap, LV_OPA_COVER, 0);
lv_obj_set_style_radius(cap, BAT_CAP_RADIUS, 0);
lv_obj_set_style_border_width(cap, 0, 0);
lv_obj_clear_flag(cap, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
// 4. 电量填充条
int fill_x = bat_x + BAT_BORDER + BAT_PAD;
int fill_y = bat_y + BAT_BORDER + BAT_PAD;
int fill_w = FILL_MAX_W * level / 100;
uint32_t fill_color = (level <= BAT_LOW_THRESH) ? BAT_COLOR_RED : BAT_COLOR_GREEN;
lv_obj_t *fill = lv_obj_create(notch);
lv_obj_remove_style_all(fill);
lv_obj_set_size(fill, (fill_w > 0) ? fill_w : 0, FILL_H);
lv_obj_set_pos(fill, fill_x, fill_y);
lv_obj_set_style_bg_color(fill, lv_color_hex(fill_color), 0);
lv_obj_set_style_bg_opa(fill, (fill_w > 0) ? LV_OPA_COVER : LV_OPA_TRANSP, 0);
lv_obj_set_style_radius(fill, FILL_RADIUS, 0);
lv_obj_set_style_border_width(fill, 0, 0);
lv_obj_clear_flag(fill, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
slots[slot_count].notch = notch;
slots[slot_count].fill = fill;
slots[slot_count].fill_x = fill_x;
slots[slot_count].fill_y = fill_y;
slot_count++;
}
void battery_ui_show_briefly(void) {
int fill_w = FILL_MAX_W * cached_level / 100;
uint32_t fill_color = (cached_level <= BAT_LOW_THRESH) ? BAT_COLOR_RED : BAT_COLOR_GREEN;
// 清理已销毁的slot
int valid = 0;
for (int i = 0; i < slot_count; i++) {
if (lv_obj_is_valid(slots[i].notch)) {
slots[valid] = slots[i];
valid++;
}
}
slot_count = valid;
// 取消渐隐动画并立即显示
for (int i = 0; i < slot_count; i++) {
lv_obj_t *notch = slots[i].notch;
lv_anim_del(notch, fade_anim_cb);
lv_obj_set_style_opa(notch, LV_OPA_COVER, 0);
lv_obj_t *fill = slots[i].fill;
if (lv_obj_is_valid(fill)) {
lv_obj_set_size(fill, (fill_w > 0) ? fill_w : 0, FILL_H);
lv_obj_set_style_bg_color(fill, lv_color_hex(fill_color), 0);
lv_obj_set_style_bg_opa(fill, (fill_w > 0) ? LV_OPA_COVER : LV_OPA_TRANSP, 0);
}
}
// 重置定时器
if (hide_timer) {
lv_timer_del(hide_timer);
}
hide_timer = lv_timer_create(hide_timer_cb, SHOW_DURATION_MS, NULL);
lv_timer_set_repeat_count(hide_timer, 1);
}
void battery_ui_update_level(int level) {
if (level < 0) level = 0;
if (level > 100) level = 100;
cached_level = level;
}