Files
HeavenlyHeroStar/docs/spec/settings-screen-landscape.md
T

766 lines
28 KiB
Markdown
Raw 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.
# Settings 横屏页面实现规格
> 项目:HeavenlyHeroStar
> 目标芯片:ESP32-S3
> ESP-IDF6.0.1
> LVGL9.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(01023)` 已实现 |
| UI 屏幕 | `main/ui/screens/ui_Screen{1,2,3}.c` | Screen1 日历 170×320Screen3 为默认启动页 |
| 事件处理 | `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(主从)** 布局:
- 左侧 Sidebar90px):System / Display / Audio / Network 导航 + Save 按钮
- 右侧 Content230px):随 Tab 切换动态刷新的设置项
- **全局横屏**:启动后 LVGL 逻辑分辨率为 **320×170**,所有页面按横屏坐标设计
首版(MVP)交付:
- BSP 横屏基础设施(display 旋转 + touch 校准)
- Settings 屏幕骨架与 Tab 切换机制
- System Tab 完整功能(恢复出厂)
- Display Tab 亮度滑块(实时预览 + NVS 持久化)
- Audio / Network Tab 占位 UI + NVS 存根
- Save 按钮写入 NVSFactory Reset 恢复默认并重启
### 1.3 方案选型结论
| 方案 | 说明 | 结论 |
|------|------|------|
| A | SquareLine Studio 导出静态 Settings 屏 | 不利于 Tab 动态刷新,且易被重新导出覆盖 |
| **B** | **手写 C + `lv_obj_clean` 动态加载** | **采用**:模块化、可扩展、与 SquareLine 解耦 |
| C | 每个 Tab 独立 Screenlv_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 | 相对 listy 递增 | `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 行高 | 3236px |
| 字体 | 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 = (240170)/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 切换前 collectSave 时 commit |
| Factory Reset | `lv_button` | — | 确认后 `settings_store_factory_reset()` + `esp_restart()` |
StatusBar 标题:`"System Settings"`
#### Display Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|------|-----------|----------|------|
| Brightness | `lv_slider` (0100) + `%` label | `brightness_pct` | `VALUE_CHANGED` 时:`settings_store` 更新内存 + `settings_store_apply()` 实时调背光;Save 时 commit |
StatusBar 标题:`"Display Settings"`
#### Audio Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|------|-----------|----------|------|
| Volume | `lv_slider` (0100) | `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 控件回读到 outout 应先拷贝 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 | 宽 | 100120px |
| 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 <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; /* 0100, default 70 */
uint8_t volume_pct; /* 0100, 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` | 0100 |
| `"vol"` | `uint8_t` | `volume_pct` | 0100 |
| `"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 结果更新内存 structcollect 前应先 `*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 范围 | 待确认 |
### 阶段 1BSP 横屏 + 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-1AC-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×170Sidebar 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` 整个 contentstatic style 不可绑定到会被删除的对象上。
9. **跨 Tab 数据**Tab 切换前必须 collectslider/switch 的 `VALUE_CHANGED` 应同步内存,避免 UI 与硬件不一致。
10. **Factory Reset 范围**Phase 1 仅擦除 `"settings"` namespaceBLE 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 约束 |