28 KiB
Settings 横屏页面实现规格
项目:HeavenlyHeroStar
目标芯片:ESP32-S3
ESP-IDF:6.0.1
LVGL:9.2.2
逻辑分辨率:320×170(全局横屏)
状态:待实现(v1.2 修订稿)
1. 背景与目标
1.1 现状
当前 UI 层由 SquareLine Studio 生成,尚无 Settings 页面:
| 模块 | 路径 | 说明 |
|---|---|---|
| 物理分辨率 | main/bsp/board_config.h |
BOARD_LCD_H_RES=170, BOARD_LCD_V_RES=320(竖屏面板) |
| LVGL Display | main/bsp/lvgl_port.c |
以 170×320 创建 display,无旋转 |
| 触摸 | main/bsp/touch.c |
CST816S,swap_xy/mirror_x/mirror_y 均为 0 |
| 背光 | main/bsp/display.c |
display_backlight_set_brightness(0–1023) 已实现 |
| UI 屏幕 | main/ui/screens/ui_Screen{1,2,3}.c |
Screen1 日历 170×320;Screen3 为默认启动页 |
| 事件处理 | main/app/ui_handlers.c |
HID 相关回调,与 SquareLine 生成的 ui_events.c 分离 |
| NVS | main/app/app_main.c |
已初始化 nvs_flash,Settings 尚未使用 |
| WiFi | main/services/wifi_manager.c |
BOARD_WIFI_ENABLED=0,Network Tab 首版为占位 |
1.2 目标
实现 Settings 设置页,采用 Master-Detail(主从) 布局:
- 左侧 Sidebar(90px):System / Display / Audio / Network 导航 + Save 按钮
- 右侧 Content(230px):随 Tab 切换动态刷新的设置项
- 全局横屏:启动后 LVGL 逻辑分辨率为 320×170,所有页面按横屏坐标设计
首版(MVP)交付:
- BSP 横屏基础设施(display 旋转 + touch 校准)
- Settings 屏幕骨架与 Tab 切换机制
- System Tab 完整功能(恢复出厂)
- Display Tab 亮度滑块(实时预览 + NVS 持久化)
- Audio / Network Tab 占位 UI + NVS 存根
- Save 按钮写入 NVS;Factory Reset 恢复默认并重启
1.3 方案选型结论
| 方案 | 说明 | 结论 |
|---|---|---|
| A | SquareLine Studio 导出静态 Settings 屏 | 不利于 Tab 动态刷新,且易被重新导出覆盖 |
| B | 手写 C + lv_obj_clean 动态加载 |
采用:模块化、可扩展、与 SquareLine 解耦 |
| C | 每个 Tab 独立 Screen,lv_scr_load 切换 | 内存占用高,Sidebar 需重复创建 |
| 横屏-硬件 | 修改 ST7789 scan direction | 不采用:需改 panel 驱动,touch 仍须软件映射 |
| 横屏-软件 | lv_display_set_rotation(90°) |
采用:改动集中在 BSP,UI 直接用 320×170 坐标 |
2. 页面布局
2.1 整体结构(320×170)
System Tab 选中时:
+---------------------------------------------------------------+
| Sidebar 90px | Content 230px |
| [System] * | System Settings --:-- |
| [Display] | ----------------------------------- |
| [Audio] | Enable Notification [switch OFF] |
| [Network] | |
| [Save] | [Factory Reset] |
+---------------------------------------------------------------+
|<-- 90px -->|<-------------------- 230px -------------------->|
Display Tab 选中时(Content 区域示意):
| Display Settings --:-- |
| ----------------------------------- |
| Brightness: [=======> ] 70% |
2.2 区域参数表
| 区域名称 | 宽度 | 高度 | 位置 (x, y) | LVGL 控件 | 说明 |
|---|---|---|---|---|---|
| Screen 根 | 320 | 170 | (0, 0) | lv_obj_create(NULL) |
禁用滚动 |
| Sidebar | 90 | 170 | (0, 0) | lv_list |
导航容器;显式设 pad_all=2、item_gap=2 |
| Sidebar 导航按钮 ×4 | 86 | 28 | 相对 list,y 递增 | lv_list_add_button |
System/Display/Audio/Network |
| Save 按钮 | 86 | 28 | (2, 138) | lv_button |
固定底部,不在 list 内滚动 |
| Content 容器 | 230 | 170 | (90, 0) | lv_obj |
动态刷新区域 |
| StatusBar | 230 | 25 | (0, 0) 相对 content | lv_obj + lv_label ×2 |
标题左、时间右 |
| SettingsBody | 230 | 145 | (0, 25) 相对 content | switch/slider/button | 各 Tab 内容 |
2.3 紧凑布局约束
170px 高度下必须严格控制间距,避免纵向滚动:
| 约束项 | 值 |
|---|---|
Screen / Sidebar / Content pad_all |
≤ 2px |
| 导航按钮间距 | 2px |
| SettingsBody 行高 | 32–36px |
| 字体 | Phase 1 使用 LV_FONT_DEFAULT;Phase 3 可换 12px 定制字体 |
| 滚动 | Screen、Sidebar、Content 均移除 LV_OBJ_FLAG_SCROLLABLE |
2.4 System Tab 控件坐标(相对 SettingsBody)
| 控件 | x | y | 宽 | 高 | 类型 |
|---|---|---|---|---|---|
| Label "Enable Notification" | 4 | 4 | 120 | 28 | lv_label |
| Switch 通知 | 180 | 4 | 40 | 28 | lv_switch |
| Button "Factory Reset" | 4 | 112 | 100 | 28 | lv_button |
2.5 Display Tab 控件坐标(相对 SettingsBody)
| 控件 | x | y | 宽 | 高 | 类型 |
|---|---|---|---|---|---|
| Label "Brightness" | 4 | 4 | 80 | 28 | lv_label |
| Slider 亮度 | 88 | 8 | 100 | 12 | lv_slider |
| Label "70%" | 196 | 4 | 30 | 28 | lv_label |
Audio / Network Tab 坐标见 §5.2;布局模式一致(label 左 80px + 控件区 140px)。
3. 系统架构
3.1 模块划分
main/bsp/
├── board_config.h # 新增 BOARD_LCD_LOGIC_H_RES/V_RES
├── lvgl_port.c # rotation + flush_cb 软件旋转(真机验证)
├── touch.c # 物理面板 touch 对齐(非 LVGL 旋转)
└── display.c # 背光 API(已有)
main/ui/
├── screens/
│ └── ui_Settings.c/.h # 屏幕骨架:sidebar + content + Save
├── settings/
│ ├── settings_types.h # settings_data_t、Tab 枚举
│ ├── settings_store.c/.h # NVS 读写、默认值、commit/reset
│ └── settings_tabs.c/.h # load_xxx_settings() 动态构建控件
├── ui.c / ui.h # 注册 Settings 屏幕
└── ui_events.c # SquareLine 生成,不修改
main/app/
└── ui_handlers.c # Settings 事件回调(Save/Tab/Slider 等)
3.2 架构图
flowchart TB
subgraph bsp [BSP Layer]
Display["display.c"]
LVGLPort["lvgl_port.c"]
Touch["touch.c"]
end
subgraph ui [UI Layer]
SettingsScreen["ui_Settings.c"]
SettingsTabs["settings_tabs.c"]
SettingsStore["settings_store.c"]
end
subgraph app [App Layer]
UIHandlers["ui_handlers.c"]
end
Display --> LVGLPort
LVGLPort --> SettingsScreen
Touch --> LVGLPort
SettingsScreen --> SettingsTabs
SettingsScreen --> SettingsStore
SettingsTabs --> UIHandlers
UIHandlers --> SettingsStore
SettingsStore --> NVS["nvs_flash"]
SettingsStore --> Display
3.3 Tab 切换数据流
sequenceDiagram
participant User as 用户
participant Sidebar as Sidebar
participant Content as content_obj
participant Tabs as settings_tabs
participant Store as settings_store
User->>Sidebar: 点击 Display(当前在 System)
Sidebar->>Tabs: settings_tabs_collect(SYSTEM, buf)
Tabs->>Store: settings_store_update(buf)
Sidebar->>Sidebar: 更新选中高亮
Sidebar->>Content: lv_obj_clean(content_obj)
Sidebar->>Tabs: load_display_settings(content, store)
Tabs->>Content: 创建 slider/label
Tabs->>Store: 读取 brightness_pct 填充控件
User->>Sidebar: 点击 Save
Sidebar->>Tabs: settings_tabs_collect(active_tab, buf)
Tabs->>Store: settings_store_update(buf)
Store->>Store: settings_store_commit()
Store->>Store: settings_store_apply()
Tab 切换前必须先 collect 当前 Tab 到内存,否则未 Save 的控件改动会在 lv_obj_clean 后丢失。Save 仅 collect 当前 active Tab,但 settings_store_commit() 持久化 内存 struct 全部字段(含其它 Tab 切换前已 collect 的值)。
4. 全局横屏基础设施(BSP)
4.1 逻辑分辨率宏
在 main/bsp/board_config.h 新增:
/* Physical panel pixels (portrait wiring) */
#define BOARD_LCD_H_RES 170
#define BOARD_LCD_V_RES 320
/* LVGL logical resolution after 90° rotation (landscape) */
#define BOARD_LCD_LOGIC_H_RES 320
#define BOARD_LCD_LOGIC_V_RES 170
UI 代码 统一使用 BOARD_LCD_LOGIC_H_RES / BOARD_LCD_LOGIC_V_RES 作为屏幕尺寸。
4.2 Display 旋转与 flush_cb
在 main/bsp/lvgl_port.c 的 lvgl_port_init() 中,lv_display_create 之后添加:
lv_display_t *display =
lv_display_create(BOARD_LCD_H_RES, BOARD_LCD_V_RES);
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
/* 逻辑分辨率:320×170 */
说明(依据 LVGL 9.2 Display porting — Rotation):
lv_display_set_rotation()交换逻辑分辨率(320×170),并变换 触摸坐标;不会自动旋转像素- 本项目无 ST7789 硬件 scan rotation,须走 软件旋转 路径
- 当前为
LV_DISPLAY_RENDER_MODE_PARTIAL:LVGL 可能在内部按块旋转后再调用flush_cb,但必须在真机验证;若画面仍呈竖屏或区域错位,在flush_cb中补充:
lv_display_rotate_area(disp, (lv_area_t *)&rotated_area);
lv_draw_sw_rotate(px_map, rotated_buf, src_w, src_h, src_stride, dst_stride,
lv_display_get_rotation(disp), cf);
/* 再将 rotated_buf / rotated_area 传给 esp_lcd_panel_draw_bitmap */
- 绘制 buffer 大小仍基于物理
BOARD_LCD_H_RES × BOARD_LVGL_DRAW_BUF_LINES;逻辑宽变为 320 后 partial 分块与软件旋转 CPU 开销上升,需关注刷新流畅度(见 §11)
Phase 1 步骤 1.2 验收: 加载 Settings 页后,Sidebar 在左、Content 在右,文字可读,无镜像/错位。
4.3 Panel Gap 与 flush 坐标
物理面板为 240×320,有效显示区 170×320 居中,display.c 已通过 esp_lcd_panel_set_gap(BOARD_LCD_GAP_X, 0) 设置 35px 水平偏移(BOARD_LCD_GAP_X = (240−170)/2)。
旋转 90° 后 gap 方向可能变化;若 flush_cb 需手动 lv_draw_sw_rotate,须同步验证:
- 画面是否整体偏移或裁切
lv_display_rotate_area输出坐标加上 gap offset 是否正确- 四角触控是否与像素对齐
Phase 1 步骤 1.2/1.3 一并验收,必要时在 flush 路径对 area 做 gap 补偿。
4.4 Touch 坐标映射
原则:触摸驱动只做「物理竖屏面板」对齐;LVGL 负责 rotation 后的逻辑坐标变换。
esp_lcd_touch_config_t的x_max/y_max保持 物理尺寸 170×320,不随逻辑横屏改变lv_indev_set_display(indev, display)关联 display 后,LVGL 9 会根据lv_display_set_rotation()在内部变换触摸点- 因此
swap_xy/mirror_x/mirror_y仅用于 触摸 IC 原始坐标 → 物理面板(170×320、原点左上角)的对齐,不得 为了 90° 逻辑横屏在驱动层做 rotation 补偿,否则 双重变换
初始值(与当前 touch.c 一致,待真机验证):
.flags = {
.swap_xy = 0,
.mirror_x = 0,
.mirror_y = 0,
},
校准步骤:
- 先 设
lv_display_set_rotation(90°),保持 touch flags 全 0,加载 Settings 页 - 在四个逻辑角点击,观察触控是否落在对应 UI 区域
- 若整体镜像或轴互换,仅调整物理对齐 flags(swap × mirror 共 8 种),每次改一项并记录
- 确认 Sidebar 左侧按钮可准确触控后,写入
board_config.h:
#define BOARD_TOUCH_SWAP_XY 0 /* 真机确认值 */
#define BOARD_TOUCH_MIRROR_X 0
#define BOARD_TOUCH_MIRROR_Y 0
注释须标明:物理面板对齐,非 LVGL 逻辑旋转。
4.5 现有 Screen 兼容
| 屏幕 | 现状 | 横屏影响 | 处理阶段 |
|---|---|---|---|
| Screen1 日历 | 170×320 居中 | 布局错位 | Phase 3 重排或替换 |
| Screen2 | SquareLine 生成 | 需验证 | Phase 3 |
| Screen3 HID 控制 | 绝对坐标按钮 | 需重排 | Phase 3 |
| Settings | 新建 | 按 320×170 设计 | Phase 1 |
Phase 1 建议将 ui.c 中 lv_disp_load_scr() 默认屏改为 ui_Settings,便于验收横屏与 Settings 功能。
5. Settings UI 规格
5.1 屏幕骨架 API
main/ui/screens/ui_Settings.h
#pragma once
#include "lvgl.h"
#include "settings_types.h"
extern lv_obj_t *ui_Settings;
extern lv_obj_t *ui_Settings_sidebar;
extern lv_obj_t *ui_Settings_content;
void ui_Settings_screen_init(void);
void ui_Settings_screen_destroy(void);
/* 切换 Tab 时由 ui_handlers 调用 */
void ui_Settings_switch_tab(settings_tab_t tab);
settings_tab_t ui_Settings_get_active_tab(void);
初始化伪代码:
void ui_Settings_screen_init(void)
{
ui_Settings = lv_obj_create(NULL);
lv_obj_set_size(ui_Settings, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Settings, LV_OBJ_FLAG_SCROLLABLE);
ui_Settings_sidebar = lv_list_create(ui_Settings);
lv_obj_set_size(ui_Settings_sidebar, 90, 170);
lv_obj_set_pos(ui_Settings_sidebar, 0, 0);
lv_obj_remove_flag(ui_Settings_sidebar, LV_OBJ_FLAG_SCROLLABLE);
/* 4 个导航 button + 事件 */
/* Save button 固定底部 */
ui_Settings_content = lv_obj_create(ui_Settings);
lv_obj_set_size(ui_Settings_content, 230, 170);
lv_obj_set_pos(ui_Settings_content, 90, 0);
lv_obj_remove_flag(ui_Settings_content, LV_OBJ_FLAG_SCROLLABLE);
settings_store_load();
settings_store_apply(); /* 启动时恢复 NVS 背光等到硬件 */
load_system_settings(ui_Settings_content, settings_store_get());
}
5.2 各 Tab MVP 内容
System Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|---|---|---|---|
| Enable Notification | lv_switch |
notification_enabled |
VALUE_CHANGED 时更新内存;Tab 切换前 collect;Save 时 commit |
| Factory Reset | lv_button |
— | 确认后 settings_store_factory_reset() + esp_restart() |
StatusBar 标题:"System Settings"
Display Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|---|---|---|---|
| Brightness | lv_slider (0–100) + % label |
brightness_pct |
VALUE_CHANGED 时:settings_store 更新内存 + settings_store_apply() 实时调背光;Save 时 commit |
StatusBar 标题:"Display Settings"
Audio Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|---|---|---|---|
| Volume | lv_slider (0–100) |
volume_pct |
VALUE_CHANGED 更新内存;Phase 1 无 codec 输出 |
| Mute | lv_switch |
mute |
VALUE_CHANGED 更新内存;Phase 1 无 codec 输出 |
StatusBar 标题:"Audio Settings"
Network Tab
| 控件 | LVGL 类型 | 行为 |
|---|---|---|
| WiFi Status | lv_label |
显示 "WiFi: Disabled"(BOARD_WIFI_ENABLED=0) |
| Scan | lv_button |
Phase 1 点击显示 "Not available" 日志;Phase 4 接 wifi_manager |
StatusBar 标题:"Network Settings"
5.3 Tab 加载函数
main/ui/settings/settings_tabs.h
#pragma once
#include "lvgl.h"
#include "settings_types.h"
void load_system_settings(lv_obj_t *content, const settings_data_t *data);
void load_display_settings(lv_obj_t *content, const settings_data_t *data);
void load_audio_settings(lv_obj_t *content, const settings_data_t *data);
void load_network_settings(lv_obj_t *content, const settings_data_t *data);
/* Save / Tab 切换前:从指定 Tab 控件回读到 out(out 应先拷贝 settings_store_get()) */
void settings_tabs_collect(settings_tab_t tab, settings_data_t *out);
每次切换 Tab(先 collect 再 clean):
void ui_Settings_switch_tab(settings_tab_t tab)
{
settings_data_t buf = *settings_store_get();
settings_tabs_collect(s_active_tab, &buf);
settings_store_update(&buf);
s_active_tab = tab;
sidebar_update_highlight(tab);
lv_obj_clean(ui_Settings_content);
switch (tab) {
case SETTINGS_TAB_SYSTEM:
load_system_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_DISPLAY:
load_display_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_AUDIO:
load_audio_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_NETWORK:
load_network_settings(ui_Settings_content, settings_store_get());
break;
default:
LV_ASSERT_MSG(false, "unexpected settings_tab_t");
break;
}
}
5.4 样式规范
在 ui_Settings.c 内使用 static lv_style_t(或 settings_style.c):
| 元素 | 属性 | 值 |
|---|---|---|
| Sidebar 按钮(默认) | 宽×高 | 86×28 |
| Sidebar 按钮(选中) | 背景色 | lv_palette_main(LV_PALETTE_BLUE) |
| Content 背景 | 背景色 | lv_palette_lighten(LV_PALETTE_GREY, 2) |
| StatusBar 分隔线 | 高度 1px | bottom border 或 lv_obj 线 |
| Slider | 宽 | 100–120px |
| Factory Reset 按钮 | 文字色 | lv_palette_main(LV_PALETTE_RED) |
Style 生命周期: 使用文件级 static style,在 ui_Settings_screen_init() 初始化一次;lv_obj_clean(content) 仅删除子对象,不删除 static style,避免泄漏。
5.5 StatusBar 时间
| 阶段 | 行为 |
|---|---|
| Phase 1 | 静态占位 "--:--" |
| Phase 4 | SNTP 同步后 lv_label_set_text_fmt() 每分钟更新 |
6. 数据模型与 NVS
6.1 类型定义
main/ui/settings/settings_types.h
#pragma once
#include <stdint.h>
#include <stdbool.h>
typedef enum {
SETTINGS_TAB_SYSTEM = 0,
SETTINGS_TAB_DISPLAY,
SETTINGS_TAB_AUDIO,
SETTINGS_TAB_NETWORK,
} settings_tab_t;
typedef struct {
bool notification_enabled;
uint8_t brightness_pct; /* 0–100, default 70 */
uint8_t volume_pct; /* 0–100, default 50 */
bool mute;
} settings_data_t;
6.2 默认值
| 字段 | 默认值 | 说明 |
|---|---|---|
notification_enabled |
false |
与 mockup 一致,默认关闭 |
brightness_pct |
70 |
与 mockup 一致 |
volume_pct |
50 |
居中 |
mute |
false |
— |
static const settings_data_t SETTINGS_DEFAULTS = {
.notification_enabled = false,
.brightness_pct = 70,
.volume_pct = 50,
.mute = false,
};
6.3 NVS Schema
| 属性 | 值 |
|---|---|
| Namespace | "settings" |
| 存储格式 | 各 key 独立 u8/blob,便于单项迁移 |
| Key | 类型 | 字段 | 说明 |
|---|---|---|---|
"notif" |
uint8_t |
notification_enabled |
0/1 |
"bright" |
uint8_t |
brightness_pct |
0–100 |
"vol" |
uint8_t |
volume_pct |
0–100 |
"mute" |
uint8_t |
mute |
0/1 |
6.4 Store API
main/ui/settings/settings_store.h
#pragma once
#include "settings_types.h"
void settings_store_load(void);
const settings_data_t *settings_store_get(void);
void settings_store_update(const settings_data_t *data);
esp_err_t settings_store_commit(void);
esp_err_t settings_store_factory_reset(void);
void settings_store_apply(void); /* 将内存值应用到硬件(背光等) */
| 函数 | 说明 |
|---|---|
settings_store_load |
启动时从 NVS 读取;key 不存在则用 SETTINGS_DEFAULTS |
settings_store_get |
返回内存中当前 struct 指针(只读) |
settings_store_update |
用 collect 结果更新内存 struct(collect 前应先 *out = *settings_store_get()) |
settings_store_commit |
将全部字段写入 NVS |
settings_store_factory_reset |
擦除 "settings" namespace,恢复默认值,commit |
settings_store_apply |
调用 display_backlight_set_brightness() 等即时生效项 |
6.5 Save 流程
sequenceDiagram
participant User
participant SaveBtn
participant Tabs as settings_tabs
participant Store as settings_store
participant NVS
participant HW as display.c
User->>SaveBtn: 点击 Save
SaveBtn->>Tabs: settings_tabs_collect(active_tab, buf)
Tabs->>Store: settings_store_update(buf)
Store->>NVS: settings_store_commit()
Store->>HW: settings_store_apply()
SaveBtn->>User: 可选:短暂 toast "Saved"
注意:
- Display Tab 亮度在
VALUE_CHANGED时已通过settings_store_apply()实时调背光并更新内存;Save 负责写入 NVS - 跨 Tab 修改:切换 Tab 时 collect 当前 Tab 到内存;最终 Save 一次 commit 全部字段
- 背光等硬件调用统一经
settings_store_apply(),不 在settings_tabs.c直接调用display.c(见 §7.1)
6.6 Factory Reset 流程
- 用户点击 Factory Reset 按钮
- Phase 1 无确认对话框,直接执行(Phase 2 可选加
lv_msgbox确认,见 §8) - 调用
settings_store_factory_reset():擦除 NVS namespace,恢复SETTINGS_DEFAULTS - 调用
settings_store_apply()恢复背光 esp_restart()
7. 事件与集成
7.1 事件归属
| 事件 | 注册位置 | 处理函数(ui_handlers.c) |
|---|---|---|
| Sidebar Tab 点击 | ui_Settings.c init |
settings_on_tab_clicked() |
| Save 点击 | ui_Settings.c init |
settings_on_save_clicked() |
| Factory Reset | settings_tabs.c 创建时 |
settings_on_factory_reset() |
| Brightness slider | settings_tabs.c 创建时 |
settings_on_brightness_changed() → 更新 store + settings_store_apply() |
| 其他 slider/switch | 各 load 函数 | settings_on_value_changed() → 更新 store 内存 |
原则: Settings 相关回调一律放在 main/app/ui_handlers.c,不修改 SquareLine 生成的 ui_events.c。硬件侧效应(背光)经 settings_store_apply(),UI 层不直接 #include "display.h"。
7.2 CMakeLists.txt 变更
在 main/CMakeLists.txt 的 SRCS 中添加:
"ui/screens/ui_Settings.c"
"ui/settings/settings_store.c"
"ui/settings/settings_tabs.c"
7.3 ui.h / ui.c 变更
/* ui.h */
#include "screens/ui_Settings.h"
/* ui_init() */
ui_Settings_screen_init();
/* Phase 1 默认屏(LVGL 9 亦可写 lv_screen_load(ui_Settings)) */
lv_disp_load_scr(ui_Settings);
/* ui_destroy() */
ui_Settings_screen_destroy();
7.4 初始化顺序
在 app_main.c 中保持现有顺序,Settings 无额外要求:
nvs_flash_init → display_init → lvgl_port_init → touch_init → ui_init → ui_handlers_init
settings_store_load() 与 settings_store_apply() 在 ui_Settings_screen_init() 内调用(NVS 已就绪),确保重启后背光等与 NVS 一致。
8. 实现步骤
阶段 0:规格审核
| 步骤 | 内容 | 状态 |
|---|---|---|
| 0.1 | 审核本文档 v1.2 | 完成 |
| 0.2 | 确认横屏全局策略与 Tab MVP 范围 | 待确认 |
阶段 1:BSP 横屏 + Settings 全量 MVP
| 步骤 | 内容 |
|---|---|
| 1.1 | board_config.h 增加逻辑分辨率宏、touch 对齐宏 |
| 1.2 | lvgl_port.c:lv_display_set_rotation(90°);真机验证 flush,必要时 lv_draw_sw_rotate + gap 补偿 |
| 1.3 | touch.c:物理面板对齐 flags 真机验证(勿与 LVGL rotation 双重变换) |
| 1.4 | 新建 settings_types.h、settings_store.c(含 apply) |
| 1.5 | 新建 ui_Settings.c:Sidebar + Content + Save |
| 1.6 | 新建 settings_tabs.c:四个 load_xxx_settings() + settings_tabs_collect() |
| 1.7 | ui_handlers.c:Tab 切换(先 collect)、Save、Factory Reset、slider/switch 回调 |
| 1.8 | 更新 CMakeLists.txt、ui.c、ui.h |
| 1.9 | 默认加载 Settings 页,真机验收 §10 全部 AC |
Phase 1 验收: 与 §1.2 MVP 及 §10 验收标准 AC-1~AC-8 一致(含四个 Tab、Display 亮度、跨 Tab Save、Factory Reset)。
阶段 2:体验增强(可选,非 MVP 阻塞)
| 步骤 | 内容 |
|---|---|
| 2.1 | Factory Reset lv_msgbox 确认对话框 |
| 2.2 | Save 成功 toast / 视觉反馈 |
| 2.3 | Sidebar / Tab 切换过渡动画 |
阶段 3:现有 Screen 横屏适配
| 步骤 | 内容 |
|---|---|
| 3.1 | Screen3 HID 控制页按 320×170 重排 |
| 3.2 | Screen1/2 适配或废弃 |
| 3.3 | 多 Screen 导航入口(如 Settings 返回主屏) |
阶段 4:增强功能(后续)
| 步骤 | 内容 |
|---|---|
| 4.1 | SNTP 时间显示 |
| 4.2 | 通知功能真实实现 |
| 4.3 | WiFi 扫描/连接 UI(BOARD_WIFI_ENABLED=1) |
| 4.4 | Audio codec 对接 |
| 4.5 | Factory Reset 确认对话框(若未在 Phase 2 完成) |
9. 与现有代码对应关系
| 现有 | Settings 实施后 |
|---|---|
lv_display_create(170, 320) |
不变物理尺寸 + set_rotation(90°);flush 可能需软件旋转 |
ui_Screen3 默认启动 |
Phase 1 改为 ui_Settings;Phase 3 增加导航 |
ui_events.c SquareLine 回调 |
不修改;Settings 事件放 ui_handlers.c |
display_backlight_set_brightness() |
经 settings_store_apply() 调用,不在 UI Tab 层直接调用 |
nvs_flash 已初始化 |
Settings 使用独立 namespace "settings" |
wifi_manager.c |
Network Tab Phase 4 对接 |
| SquareLine Screen1/2/3 | Phase 3 适配,Settings 不纳入 SquareLine |
10. 验收标准
| 编号 | 验收项 | 标准 |
|---|---|---|
| AC-1 | 逻辑分辨率 | LVGL 屏幕 320×170,Sidebar 90px + Content 230px |
| AC-2 | 触摸 | 四角及 Sidebar 按钮触控准确,无误触 |
| AC-3 | Tab 切换 | 4 个 Tab 可切换,右侧内容正确刷新,无滚动条 |
| AC-4 | System Tab | Switch/Factory Reset 可交互 |
| AC-5 | Display Tab | 亮度 slider 实时改变背光 |
| AC-6 | Save | 跨 Tab 修改后 Save,重启后全部设置值从 NVS 恢复 |
| AC-7 | Factory Reset | 恢复默认值、背光恢复 70%、设备重启 |
| AC-8 | 内存 | Tab 反复切换 50 次无 crash、无显著内存增长 |
11. 风险与约束
- flush 软件旋转:
lv_display_set_rotation不保证自动旋转像素;须真机验证,必要时改flush_cb(§4.2)。 - 触摸校准:区分物理对齐与 LVGL rotation;错误地在 touch 驱动层做 rotation 补偿会导致双重变换(§4.4)。
- Panel gap:240 宽面板 170 有效区居中,旋转后须验证 offset(§4.3)。
- Partial 模式性能:逻辑宽 320 + 软件旋转增加 CPU 负担,若卡顿可考虑增大
BOARD_LVGL_DRAW_BUF_LINES或后续评估 DIRECT 模式。 - SquareLine 冲突:Settings 手写 C,不纳入 SquareLine 工程;重新导出 UI 时不应覆盖
ui_Settings.c与ui_handlers.c中 Settings 部分。 - WiFi 默认关闭:Network Tab Phase 1 仅为占位,避免用户误解为已连网。
- 170px 高度:控件过多时需合并 Tab 或减小行高,禁止启用纵向滚动。
- Style 生命周期:Tab 切换使用
lv_obj_clean而非lv_obj_del整个 content;static style 不可绑定到会被删除的对象上。 - 跨 Tab 数据:Tab 切换前必须 collect;slider/switch 的
VALUE_CHANGED应同步内存,避免 UI 与硬件不一致。 - Factory Reset 范围:Phase 1 仅擦除
"settings"namespace;BLE bonding、快捷键等其它 NVS namespace(见ble-hid-keyboard.md规划)保留。 - Screen 兼容:Phase 1 默认 Settings 页会导致 Screen1/2/3 暂不可直接验收,属预期行为。
12. 参考
- 板级配置:
main/bsp/board_config.h - LVGL Port:
main/bsp/lvgl_port.c - 背光 API:
main/bsp/display.h - 现有 UI 入口:
main/ui/ui.c - 事件处理范例:
main/app/ui_handlers.c - 姊妹规格:
docs/spec/ble-hid-keyboard.md - LVGL 9.2 Display Rotation:porting/display.html — Rotation
- LVGL API:
lv_display_set_rotation()、lv_display_rotate_area()、lv_draw_sw_rotate()
修订记录
| 版本 | 日期 | 说明 |
|---|---|---|
| 1.0 | 2026-07-05 | 初版:Settings 横屏 320×170 布局、BSP、NVS、分阶段实现规格 |
| 1.1 | 2026-07-05 | 移除语言选择(dropdown / NVS lang / settings_language_t) |
| 1.2 | 2026-07-05 | 技术修订:统一 Tab 线框与 MVP 范围;修正 LVGL 9 flush/touch 旋转说明;Tab 切换先 collect;启动 apply 背光;补 gap/分层/跨 Tab Save 约束 |