diff --git a/docs/spec/settings-screen-landscape.md b/docs/spec/settings-screen-landscape.md new file mode 100644 index 0000000..31b2592 --- /dev/null +++ b/docs/spec/settings-screen-landscape.md @@ -0,0 +1,765 @@ +# 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 选中时:** + +```text ++---------------------------------------------------------------+ +| Sidebar 90px | Content 230px | +| [System] * | System Settings --:-- | +| [Display] | ----------------------------------- | +| [Audio] | Enable Notification [switch OFF] | +| [Network] | | +| [Save] | [Factory Reset] | ++---------------------------------------------------------------+ +|<-- 90px -->|<-------------------- 230px -------------------->| +``` + +**Display Tab 选中时(Content 区域示意):** + +```text +| 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 模块划分 + +```text +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 架构图 + +```mermaid +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 切换数据流 + +```mermaid +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` 新增: + +```c +/* 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` 之后添加: + +```c +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](https://docs.lvgl.io/9.2/porting/display.html)): + +- `lv_display_set_rotation()` **交换逻辑分辨率**(320×170),并变换 **触摸坐标**;**不会自动旋转像素** +- 本项目无 ST7789 硬件 scan rotation,须走 **软件旋转** 路径 +- 当前为 `LV_DISPLAY_RENDER_MODE_PARTIAL`:LVGL 可能在内部按块旋转后再调用 `flush_cb`,**但必须在真机验证**;若画面仍呈竖屏或区域错位,在 `flush_cb` 中补充: + +```c +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`,须同步验证: + +1. 画面是否整体偏移或裁切 +2. `lv_display_rotate_area` 输出坐标加上 gap offset 是否正确 +3. 四角触控是否与像素对齐 + +**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` 一致,待真机验证):** + +```c +.flags = { + .swap_xy = 0, + .mirror_x = 0, + .mirror_y = 0, +}, +``` + +**校准步骤:** + +1. **先** 设 `lv_display_set_rotation(90°)`,**保持** touch flags 全 0,加载 Settings 页 +2. 在四个逻辑角点击,观察触控是否落在对应 UI 区域 +3. 若整体镜像或轴互换,**仅调整物理对齐 flags**(swap × mirror 共 8 种),每次改一项并记录 +4. 确认 Sidebar 左侧按钮可准确触控后,写入 `board_config.h`: + +```c +#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`** + +```c +#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); +``` + +**初始化伪代码:** + +```c +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`** + +```c +#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**): + +```c +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`** + +```c +#pragma once +#include +#include + +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` | — | + +```c +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`** + +```c +#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 流程 + +```mermaid +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 流程 + +1. 用户点击 Factory Reset 按钮 +2. Phase 1 无确认对话框,直接执行(Phase 2 可选加 `lv_msgbox` 确认,见 §8) +3. 调用 `settings_store_factory_reset()`:擦除 NVS namespace,恢复 `SETTINGS_DEFAULTS` +4. 调用 `settings_store_apply()` 恢复背光 +5. `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` 中添加: + +```text +"ui/screens/ui_Settings.c" +"ui/settings/settings_store.c" +"ui/settings/settings_tabs.c" +``` + +### 7.3 ui.h / ui.c 变更 + +```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 无额外要求: + +```text +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. 风险与约束 + +1. **flush 软件旋转**:`lv_display_set_rotation` 不保证自动旋转像素;须真机验证,必要时改 `flush_cb`(§4.2)。 +2. **触摸校准**:区分物理对齐与 LVGL rotation;错误地在 touch 驱动层做 rotation 补偿会导致双重变换(§4.4)。 +3. **Panel gap**:240 宽面板 170 有效区居中,旋转后须验证 offset(§4.3)。 +4. **Partial 模式性能**:逻辑宽 320 + 软件旋转增加 CPU 负担,若卡顿可考虑增大 `BOARD_LVGL_DRAW_BUF_LINES` 或后续评估 DIRECT 模式。 +5. **SquareLine 冲突**:Settings 手写 C,不纳入 SquareLine 工程;重新导出 UI 时不应覆盖 `ui_Settings.c` 与 `ui_handlers.c` 中 Settings 部分。 +6. **WiFi 默认关闭**:Network Tab Phase 1 仅为占位,避免用户误解为已连网。 +7. **170px 高度**:控件过多时需合并 Tab 或减小行高,禁止启用纵向滚动。 +8. **Style 生命周期**:Tab 切换使用 `lv_obj_clean` 而非 `lv_obj_del` 整个 content;static style 不可绑定到会被删除的对象上。 +9. **跨 Tab 数据**:Tab 切换前必须 collect;slider/switch 的 `VALUE_CHANGED` 应同步内存,避免 UI 与硬件不一致。 +10. **Factory Reset 范围**:Phase 1 仅擦除 `"settings"` namespace;BLE bonding、快捷键等其它 NVS namespace(见 `ble-hid-keyboard.md` 规划)保留。 +11. **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](https://docs.lvgl.io/9.2/porting/display.html) +- 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 约束 |