## 功能迁移清单(从 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>
191 lines
6.1 KiB
C
191 lines
6.1 KiB
C
// 电池指示器模块
|
||
// 屏幕顶部药丸容器 + 电池图标(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;
|
||
}
|