diff --git a/docs/spec/shake-nav-screen.md b/docs/spec/shake-nav-screen.md new file mode 100644 index 0000000..4abdc4c --- /dev/null +++ b/docs/spec/shake-nav-screen.md @@ -0,0 +1,668 @@ +# 摇一摇切换页面实现规格 + +> 项目:HeavenlyHeroStar +> 目标芯片:ESP32-S3 +> ESP-IDF:6.0.1 +> LVGL:9.2.2 +> 逻辑分辨率:320×170(全局横屏) +> IMU:QMI8658(3 轴加速度 + 3 轴陀螺仪) +> 状态:待实现(v1.0 初稿) + +## 1. 背景与目标 + +### 1.1 现状 + +板载 QMI8658 六轴 IMU 已在硬件文档中定义,固件侧尚未接入;UI 层四屏已创建但无页面路由机制。 + +| 模块 | 路径 | 说明 | +|------|------|------| +| IMU 硬件 | `README.md` | QMI8658,I2C SDA=IO8 / SCL=IO7,INT1=IO6 / INT2=IO15 | +| 板级配置 | `main/bsp/board_config.h` | **无** IMU 引脚宏;触摸使用 `I2C_NUM_0`(IO47/48) | +| 触摸 I2C | `main/bsp/touch.c` | CST816S,`i2c_master_bus` 独占 `I2C_NUM_0` | +| UI 屏幕 | `main/ui/ui.c` | 启动时创建 Screen1/2/3 + Settings,默认 `lv_disp_load_scr(ui_Settings)` | +| 页面路由 | — | **不存在**;`_ui_screen_change()` 在 `ui_helpers.c` 中已实现但未被调用 | +| LVGL 线程 | `main/bsp/lvgl_port.c` | 独立 task + `_lock`;外部改 UI 须 `lvgl_port_lock()` | +| Settings | `main/ui/settings/` | `settings_data_t` **无** motion 字段;NVS namespace `"settings"` | +| 事件处理 | `main/app/ui_handlers.c` | HID / Settings 回调;无 IMU / 导航逻辑 | +| IMU 驱动 | — | 仓库内无 QMI8658 组件或 `main/bsp/imu.c` | + +### 1.2 目标 + +通过 **摇一摇** 在四屏之间循环切换,作为触摸与物理按键之外的第三种导航方式。 + +**已确认产品决策:** + +| 决策项 | 结论 | +|--------|------| +| 参与轮播的页面 | **Settings + Screen1 + Screen2 + Screen3** 四屏循环 | +| 手势映射 | **任意方向摇一次 → 下一页**(MVP 不做左/右方向区分) | +| 默认轮播顺序 | `Settings → Screen1 → Screen2 → Screen3 → Settings` | +| Settings 页行为 | 在 Settings 时摇一摇同样切到 Screen1(不因当前在设置页而禁用) | + +**首版(MVP)交付:** + +- QMI8658 BSP 驱动 + 100 Hz 加速度采样(轮询) +- 摇一摇检测:去重力合成幅值 + 阈值 + 时间窗 + cooldown +- `ui_nav` 模块:循环切下一屏,fade 动画 +- System Tab:「Shake Navigation」开关 + 灵敏度档位(低/中/高) +- NVS 持久化;Factory Reset 恢复 motion 默认值 +- 线程安全:IMU task → Queue → `ui_nav_next()`(内部 `lvgl_port_lock`) + +**后续阶段(非 MVP 阻塞):** + +- INT1 Data Ready 中断,降低 CPU 轮询与功耗 +- `ui_nav_prev()` 与方向性摇一摇 +- 切页页码指示(dot indicator) +- Screen1/2/3 横屏布局完善(见 `settings-screen-landscape.md` Phase 3) + +### 1.3 方案选型结论 + +| 方案 | 说明 | 结论 | +|------|------|------| +| A | 仅陀螺仪积分判摇 | 零偏漂移大,不采用 | +| **B** | **加速度去重力 + 合成幅值峰值** | **MVP 采用** | +| C | QMI8658 内置 Wake-on-Motion 硬件判摇 | Phase 4 省电优化 | +| D | IMU task 内直接调用 `lv_screen_load_anim()` | 线程不安全,**禁止** | +| **E** | **FreeRTOS Queue → nav task → `lvgl_port_lock` 切屏** | **采用**(模式参考 `boot_button.c`) | +| F | esp_event 总线投递 `UI_NAV_NEXT` | 可选,与 Queue 等价;MVP 用 Queue 即可 | + +--- + +## 2. 硬件与 BSP + +### 2.1 引脚与总线 + +来源:`README.md` 外围接口表。 + +| 信号 | GPIO | 说明 | +|------|------|------| +| IMU_SDA | IO8 | I2C 数据线 | +| IMU_SCL | IO7 | I2C 时钟线 | +| IMU_INT1 | IO6 | 中断 1(Phase 4:Data Ready / Motion) | +| IMU_INT2 | IO15 | 中断 2(Phase 4:备用) | + +**I2C 总线隔离:** 触摸已占用 `I2C_NUM_0`(`main/bsp/touch.c`)。IMU **必须**使用独立总线,建议 `I2C_NUM_1`,避免与 CST816 争用或地址冲突时的 bus 锁问题。 + +### 2.2 board_config.h 新增宏 + +```c +/* IMU I2C (QMI8658) */ +#define BOARD_IMU_I2C_NUM I2C_NUM_1 +#define BOARD_PIN_IMU_SDA 8 +#define BOARD_PIN_IMU_SCL 7 +#define BOARD_PIN_IMU_INT1 6 +#define BOARD_PIN_IMU_INT2 15 +#define BOARD_IMU_I2C_FREQ_HZ 400000 +#define BOARD_IMU_ENABLED 1 +``` + +### 2.3 imu.c / imu.h 职责 + +新建 `main/bsp/imu.c`、`main/bsp/imu.h`: + +| API | 说明 | +|-----|------| +| `esp_err_t imu_init(void)` | 初始化 I2C bus + QMI8658 芯片 | +| `esp_err_t imu_read_accel(float *ax, *ay, *az)` | 读取加速度,单位 **g**(±8g 量程) | +| `esp_err_t imu_read_gyro(...)` | Phase 4 可选;MVP 可不实现 | +| `void imu_start_task(imu_sample_cb_t cb)` | 100 Hz 采样循环,回调 `motion_gesture` | + +**初始化序列(MVP):** + +1. `i2c_new_master_bus()` — 模式参考 `touch.c` +2. 读 WHO_AM_I,期望值 **`0x05`**(实现前对照 QMI8658A 数据手册确认) +3. 软复位 → 配置 acc:±8g,ODR **100 Hz** +4. 启动 `imu_task`(10 ms 周期读数,或按 ODR 对齐) + +Phase 1 采用 **轮询**;INT1/INT2 在 Phase 4 启用。 + +### 2.4 QMI8658 寄存器(MVP 最小集) + +> 完整寄存器表以实现阶段对照厂商数据手册为准;下表为 spec 占位,便于 Phase 1 编码。 + +| 寄存器 | 地址 | MVP 用途 | +|--------|------|----------| +| WHO_AM_I | 0x00 | 设备识别 | +| CTRL1 | 0x02 | 软复位 / 使能 | +| CTRL2 | 0x03 | acc 量程、ODR | +| ACC_X_L … ACC_Z_H | 0x35–0x3A | 6 字节 burst 读 | + +I2C 7-bit 地址:**`0x6B`**(SDO 默认;若硬件拉低则为 `0x6A`,真机确认)。 + +### 2.5 坐标系与轴映射 + +IMU 芯片坐标系与 PCB 安装方向、LVGL 横屏逻辑坐标 **不一定一致**。MVP 算法使用 **合成幅值**(与轴无关),不依赖单轴方向;Phase 4 若做方向性摇一摇再建立映射表。 + +**真机标定步骤(Phase 2 验收):** + +1. 设备平放静止 2 s,串口打印 `ax, ay, az`,确认合成幅值 ≈ 1g +2. 分别沿 X/Y/Z 快速平移,确认对应轴响应最大 +3. 记录 `g_ref` baseline,写入 `motion_gesture` 启动校准 + +--- + +## 3. 系统架构 + +### 3.1 模块划分 + +```text +main/bsp/ +├── board_config.h # IMU 引脚 / I2C 宏 +├── imu.c / imu.h # I2C + QMI8658 驱动 + imu_task + +main/app/ +├── ui_nav.c / ui_nav.h # 屏幕轮播状态机 +├── motion_gesture.c / .h # 摇一摇检测、cooldown、配置同步 +└── ui_handlers.c # 可选:nav 事件注册 + +main/ui/settings/ +├── settings_types.h # shake_nav_enabled, shake_sensitivity +├── settings_store.c # NVS shake_en / shake_sens +└── settings_tabs.c # System Tab 新增控件 + +main/app/app_main.c # 初始化顺序扩展 +main/CMakeLists.txt # 追加源文件 +``` + +### 3.2 架构图 + +```mermaid +flowchart TB + subgraph bsp [BSP Layer] + IMU["imu.c"] + end + + subgraph app [App Layer] + Gesture["motion_gesture.c"] + Nav["ui_nav.c"] + Queue["nav_queue"] + end + + subgraph ui [UI Layer] + Screens["ui_Settings / Screen1/2/3"] + Store["settings_store.c"] + end + + subgraph lvgl [LVGL Port] + Lock["lvgl_port_lock"] + LVGLTask["lvgl_port_task"] + end + + IMU -->|"100Hz samples"| Gesture + Store -->|"apply enabled/sens"| Gesture + Gesture -->|"UI_NAV_NEXT"| Queue + Queue --> Nav + Nav --> Lock + Lock --> Screens + Screens --> LVGLTask +``` + +### 3.3 摇一摇切页时序 + +```mermaid +sequenceDiagram + participant User as 用户 + participant IMU as imu_task + participant Gesture as motion_gesture + participant Queue as nav_queue + participant Nav as ui_nav + participant LVGL as lvgl_port_task + + User->>IMU: 摇晃设备 + IMU->>Gesture: accel sample 100Hz + Gesture->>Gesture: mag > threshold + min_samples + Gesture->>Gesture: cooldown check + enabled check + Gesture->>Queue: UI_NAV_NEXT + Queue->>Nav: ui_nav_next() + Nav->>LVGL: lvgl_port_lock() + Nav->>LVGL: lv_screen_load_anim next screen + Nav->>LVGL: lvgl_port_unlock() + LVGL->>User: 显示下一页 +``` + +**线程间通信:** 参考 `main/bsp/boot_button.c` 的 `GPIO ISR → xQueueSend → dedicated task` 模式;`motion_gesture` 检测到 shake 后向 `nav_queue` 投递事件,由 `nav_handler_task` 调用 `ui_nav_next()`。 + +--- + +## 4. 摇一摇检测算法 + +### 4.1 信号处理 + +1. **Baseline 校准:** 启动后 500 ms 内设备保持静止,采集加速度均值 `g_ref`(3 轴) +2. **动态量:** `a_dyn = a_raw - g_ref` +3. **合成幅值:** `mag = sqrt(ax² + ay² + az²)`(`ax/ay/az` 为 `a_dyn` 分量) + +可选替代(Phase 4):一阶低通 `a_lp` + 高通 `a_hp = a_raw - a_lp`,对缓慢倾斜更不敏感。 + +### 4.2 判定状态机 + +```text + mag > threshold + ┌──────────────────────────────┐ + │ │ + v │ + ┌──────┐ 连续 ≥ min_samples ┌─────────────────┐ + │ IDLE │ ──────────────────────> │ SHAKE_DETECTED │ + └──────┘ └────────┬────────┘ + ^ │ + │ cooldown_ms 到期 │ + └────────────────────────────────────────┘ + │ + v + post UI_NAV_NEXT(若 enabled) +``` + +**规则:** + +- 仅在 `SHAKE_DETECTED` 上升沿投递 **一次** `UI_NAV_NEXT` +- `cooldown_ms` 内忽略后续峰值,防止一次猛摇连切多页 +- `shake_nav_enabled == false` 时状态机仍可运行,但不投递事件 + +### 4.3 默认参数 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `sample_rate` | 100 Hz | acc ODR,与 imu_task 一致 | +| `threshold` | 0.8 g | 合成幅值阈值 | +| `threshold_low` | 0.6 g | 灵敏度 Low | +| `threshold_med` | 0.8 g | 灵敏度 Med(默认) | +| `threshold_high` | 1.0 g | 灵敏度 High | +| `window_ms` | 300 ms | 超阈值须在此窗口内形成有效峰值 | +| `min_samples` | 5 | 连续超阈值样本数(100 Hz 下 ≈ 50 ms) | +| `cooldown_ms` | 800 ms | 两次有效摇一摇最小间隔 | +| `calibration_ms` | 500 ms | 启动 baseline 采集时长 | + +### 4.4 motion_gesture API + +```c +typedef enum { + MOTION_NAV_NEXT = 0, +} motion_nav_event_t; + +typedef void (*motion_nav_cb_t)(motion_nav_event_t event); + +void motion_gesture_init(motion_nav_cb_t cb); +void motion_gesture_set_enabled(bool enabled); +void motion_gesture_set_sensitivity(shake_sensitivity_t sens); +void motion_gesture_on_sample(float ax_g, float ay_g, float az_g); +``` + +`motion_gesture_on_sample()` 由 `imu_task` 每样本调用;内部维护状态机与 cooldown 计时。 + +### 4.5 误触与边界 + +| 场景 | MVP 策略 | +|------|----------| +| 敲桌面 | 提高 threshold 或选 Med/High 灵敏度 | +| 线缆拉扯 | cooldown 防止连切 | +| 手持走路 | 可能误触;Phase 4 可加「须从 IDLE 静止 ≥ 200 ms 才武装」 | +| Settings 调 slider 时轻微抖动 | 合成幅值阈值通常高于手指微动 | + +--- + +## 5. UI 页面导航(ui_nav) + +### 5.1 屏幕注册表 + +轮播顺序与 §1.2 一致: + +```c +#include "screens/ui_Settings.h" +#include "screens/ui_Screen1.h" +#include "screens/ui_Screen2.h" +#include "screens/ui_Screen3.h" + +static lv_obj_t * const s_nav_screens[] = { + ui_Settings, + ui_Screen1, + ui_Screen2, + ui_Screen3, +}; + +#define NAV_SCREEN_COUNT (sizeof(s_nav_screens) / sizeof(s_nav_screens[0])) +``` + +索引:0=Settings,1=Screen1,2=Screen2,3=Screen3。 + +### 5.2 公开 API + +```c +void ui_nav_init(void); +void ui_nav_next(void); /* 线程安全:内部 lvgl_port_lock */ +void ui_nav_prev(void); /* Phase 4 预留;MVP 不实现 */ +void ui_nav_goto(uint8_t index); /* 可选:调试 / 恢复索引 */ + +lv_obj_t *ui_nav_current(void); +uint8_t ui_nav_current_index(void); +const char *ui_nav_current_name(void); /* 日志用:"Settings"/"Screen1"/... */ +``` + +### 5.3 切屏实现 + +- 使用现有 `_ui_screen_change()`(`main/ui/ui_helpers.c`)或等价 `lv_screen_load_anim()` +- 动画:`LV_SCR_LOAD_ANIM_FADE_IN`,时长 **300 ms**,delay **0** +- `ui_nav_next()`:`index = (index + 1) % NAV_SCREEN_COUNT` +- 内部流程: + +```c +void ui_nav_next(void) +{ + lvgl_port_lock(); + s_nav_index = (s_nav_index + 1) % NAV_SCREEN_COUNT; + lv_obj_t **target = (lv_obj_t **)&s_nav_screens[s_nav_index]; + _ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL); + lvgl_port_unlock(); +} +``` + +> 注:`s_nav_screens` 元素在 `ui_init()` 后已非 NULL,`_ui_screen_change` 的 `target_init` 传 `NULL` 即可。 + +### 5.4 与 Settings Tab 的边界 + +| 行为 | 说明 | +|------|------| +| 摇一摇切屏 | 切换 **LVGL Screen**(整屏),不切换 Settings 内 Sidebar Tab | +| Settings 内 Tab | 仍仅由 Sidebar 点击切换;遵循 `settings-screen-landscape.md` §3.3「切换 Tab 前先 collect」 | +| 摇离 Settings | **不**自动 `settings_tabs_collect()`;内存 struct 保留未 Save 的改动(与现有跨 Tab 行为一致) | +| 从 Settings 摇一摇 | 下一屏为 **Screen1** | + +### 5.5 Phase 1 无 IMU 时的验证路径 + +在 `ui_nav` 就绪后、IMU 驱动完成前,将 **BOOT 键** 或 **KEY_IO(GPIO4)** 短按映射为 `ui_nav_next()`,验证四屏轮播与 fade 动画,不阻塞 Phase 2。 + +--- + +## 6. 数据模型与 NVS + +### 6.1 类型扩展 + +在 `main/ui/settings/settings_types.h` 追加: + +```c +typedef enum { + SHAKE_SENS_LOW = 0, + SHAKE_SENS_MED, + SHAKE_SENS_HIGH, +} shake_sensitivity_t; + +typedef struct { + bool notification_enabled; + uint8_t brightness_pct; + uint8_t volume_pct; + bool mute; + bool shake_nav_enabled; /* 默认 true */ + shake_sensitivity_t shake_sensitivity; /* 默认 SHAKE_SENS_MED */ +} settings_data_t; +``` + +在 `settings_tabs.h` 的 `settings_field_t` 追加: + +```c + SETTINGS_FIELD_SHAKE_NAV, + SETTINGS_FIELD_SHAKE_SENS, +``` + +### 6.2 默认值 + +```c +static const settings_data_t SETTINGS_DEFAULTS = { + .notification_enabled = false, + .brightness_pct = 70, + .volume_pct = 50, + .mute = false, + .shake_nav_enabled = true, + .shake_sensitivity = SHAKE_SENS_MED, +}; +``` + +### 6.3 NVS Schema + +Namespace:`'settings'`(与现有字段共用)。 + +| Key | 类型 | 字段 | 默认 | +|-----|------|------|------| +| `shake_en` | u8 | `shake_nav_enabled` | 1 | +| `shake_sens` | u8 | `shake_sensitivity` | 1(Med) | + +`settings_store_load()` / `commit()` / `factory_reset()` 同步读写上述 key。 + +### 6.4 settings_store_apply 扩展 + +```c +void settings_store_apply(void) +{ + /* 现有背光逻辑 */ + uint16_t duty = (uint16_t)((s_data.brightness_pct * 1023U) / 100U); + display_backlight_set_brightness(duty); + + /* 新增 */ + motion_gesture_set_enabled(s_data.shake_nav_enabled); + motion_gesture_set_sensitivity(s_data.shake_sensitivity); +} +``` + +**生效时机:** + +- 启动:`settings_store_load()` 后 `settings_store_apply()` +- Save 按钮:`settings_store_commit()` 后 `settings_store_apply()` +- Factory Reset:恢复默认后 `apply()` + +Switch / Roller 的 `VALUE_CHANGED` **仅更新内存**;与现有 notification / mute 行为一致,Save 后持久化。 + +--- + +## 7. Settings UI 变更 + +### 7.1 System Tab 新增控件 + +在 `load_system_settings()` 中,于 Factory Reset 按钮 **上方** 追加一行(y 续排,避免与 y=112 的 Reset 重叠): + +| 控件 | x | y | 宽 | 高 | NVS 字段 | 行为 | +|------|---|---|----|----|----------|------| +| Label "Shake Navigation" | 4 | 40 | 120 | 28 | — | 静态文本 | +| Switch | 180 | 40 | 40 | 28 | `shake_nav_enabled` | `SETTINGS_FIELD_SHAKE_NAV` | +| Label "Shake Sensitivity" | 4 | 76 | 120 | 28 | — | 静态文本 | +| Dropdown | 120 | 76 | 100 | 28 | `shake_sensitivity` | 选项 `"Low\nMed\nHigh"`,`SETTINGS_FIELD_SHAKE_SENS` | + +Factory Reset 按钮保持 **y=112** 不变(与 `settings-screen-landscape.md` §5.4 兼容,新增项插入中间行)。 + +### 7.2 settings_tabs_collect 扩展 + +`SETTINGS_TAB_SYSTEM` 分支增加: + +```c +if (s_shake_nav_switch != NULL) { + out->shake_nav_enabled = lv_obj_has_state(s_shake_nav_switch, LV_STATE_CHECKED); +} +if (s_shake_sens_dropdown != NULL) { + out->shake_sensitivity = (shake_sensitivity_t)lv_dropdown_get_selected(s_shake_sens_dropdown); +} +``` + +### 7.3 ui_handlers 扩展 + +`settings_on_value_changed()` 增加 `SETTINGS_FIELD_SHAKE_NAV` / `SETTINGS_FIELD_SHAKE_SENS` case,更新 `settings_store` 内存 struct。 + +**不在 VALUE_CHANGED 时调用 `motion_gesture_set_*`:** 与 brightness 类似,Save + `apply()` 后统一生效;若需即时预览开关,可在 Save 规范外额外 `apply()`(Phase 2 可选)。 + +--- + +## 8. 事件与集成 + +### 8.1 初始化顺序 + +扩展 `main/app/app_main.c`: + +```text +1. nvs_flash_init() +2. display_init() +3. lvgl_port_init() +4. touch_init() [若 BOARD_TOUCH_ENABLED] +5. lvgl_port_lock → ui_init() → ui_nav_init() → lvgl_port_unlock +6. settings_store_load() +7. settings_store_apply() [背光 + motion 配置] +8. ui_handlers_init() +9. motion_gesture_init(on_motion_nav) [注册 ui_nav_next 回调] +10. imu_init() [启动 imu_task] +11. ble_manager_init() +``` + +`ui_nav_init()` 须将 `s_nav_index` 与当前 loaded screen 同步(默认 Settings → index 0)。 + +### 8.2 nav_handler_task + +```c +static void nav_handler_task(void *arg) +{ + motion_nav_event_t ev; + while (xQueueReceive(s_nav_queue, &ev, portMAX_DELAY) == pdTRUE) { + if (ev == MOTION_NAV_NEXT) { + ui_nav_next(); + } + } +} +``` + +Queue 深度建议 **4**;`motion_gesture` 在 cooldown 内不应重复投递。 + +### 8.3 CMakeLists.txt 变更 + +在 `main/CMakeLists.txt` 的 `SRCS` 追加: + +```text +"bsp/imu.c" +"app/ui_nav.c" +"app/motion_gesture.c" +``` + +`INCLUDE_DIRS` 已含 `app`、`bsp`,无需新增。 + +### 8.4 任务优先级与栈 + +| Task | 优先级 | 栈 | 说明 | +|------|--------|-----|------| +| `LVGL` | 2 | 8 KB | 已有 | +| `imu_task` | 4 | 3 KB | I2C 读 + 回调 gesture | +| `nav_handler` | 5 | 2 KB | Queue 消费,调用 ui_nav | +| `boot_btn` | 5 | 2 KB | 已有,Phase 1 可复用测 nav | + +IMU 优先级低于 nav_handler,避免 I2C 阻塞切页响应。 + +### 8.5 调试日志 + +| Tag | 内容 | +|-----|------| +| `imu` | WHO_AM_I、init 失败 | +| `motion` | shake 触发、mag 峰值(`CONFIG_LOG_DEFAULT_LEVEL` DEBUG 下) | +| `ui_nav` | index 变化、screen 名称 | + +Phase 1 可启用 mag 峰值串口输出,便于真机调 threshold。 + +--- + +## 9. 实现步骤 + +### 阶段 0:规格审核 + +| 步骤 | 内容 | 状态 | +|------|------|------| +| 0.1 | 审核本文档 v1.0 | 待确认 | +| 0.2 | 确认四屏轮播顺序与「任意摇→下一页」 | 已确认 | + +### 阶段 1:ui_nav + IMU 驱动基础 + +| 步骤 | 内容 | +|------|------| +| 1.1 | `board_config.h` 增加 IMU 宏 | +| 1.2 | 新建 `ui_nav.c`:注册表、init、next、lock 包装 | +| 1.3 | BOOT 或 KEY 短按 → `ui_nav_next()`,真机验证四屏 fade 轮播 | +| 1.4 | 新建 `imu.c`:I2C + WHO_AM_I + acc 读数 | +| 1.5 | 串口打印 100 Hz acc;确认 WHO_AM_I 与 1g 静止读数 | + +**Phase 1 验收:** AC-2 用手动按键代替摇一摇通过;AC-1 通过。 + +### 阶段 2:motion_gesture + Queue 切页 + +| 步骤 | 内容 | +|------|------| +| 2.1 | 新建 `motion_gesture.c`:baseline、状态机、cooldown | +| 2.2 | `nav_queue` + `nav_handler_task` → `ui_nav_next()` | +| 2.3 | `imu_task` 调用 `motion_gesture_on_sample()` | +| 2.4 | 真机调 threshold / cooldown,满足 AC-3 | + +**Phase 2 验收:** AC-2、AC-3、AC-7、AC-8。 + +### 阶段 3:Settings + NVS + +| 步骤 | 内容 | +|------|------| +| 3.1 | 扩展 `settings_types.h`、`settings_store.c` | +| 3.2 | System Tab UI + collect + ui_handlers | +| 3.3 | `settings_store_apply()` 同步 motion 配置 | +| 3.4 | Factory Reset 验证 motion 默认值 | + +**Phase 3 验收:** AC-4、AC-5、AC-6。 + +### 阶段 4:增强(可选) + +| 步骤 | 内容 | +|------|------| +| 4.1 | INT1 Data Ready,降低轮询频率 | +| 4.2 | `ui_nav_prev()` + 方向性摇一摇 | +| 4.3 | 屏幕底部页码 dot(4 屏指示) | +| 4.4 | 静止武装窗口,减少走路误触 | +| 4.5 | VALUE_CHANGED 即时 apply shake 开关(免 Save 预览) | + +--- + +## 10. 验收标准 + +| 编号 | 验收项 | 标准 | +|------|--------|------| +| AC-1 | IMU 通信 | WHO_AM_I = `0x05`;静止时合成幅值 ≈ 0.05–0.15g(去 baseline 后) | +| AC-2 | 轮播顺序 | 连续 4 次有效摇一摇:Settings → Screen1 → Screen2 → Screen3 → Settings | +| AC-3 | Cooldown | 单次持续摇晃只触发 **1 次** 切页 | +| AC-4 | 禁用开关 | Settings 中关闭 Shake Navigation 并 Save 后,摇一摇不切页 | +| AC-5 | 灵敏度 | 相同力度下 High 比 Low 更易触发(至少能感知差异) | +| AC-6 | NVS | 开关与灵敏度 Save 后重启仍保持 | +| AC-7 | 线程安全 | 连续摇切 50 次无 crash、无 LVGL assert | +| AC-8 | HID 无干扰 | 在 Screen2/3 摇切页时,不发送 BLE HID 报告 | + +--- + +## 11. 风险与约束 + +1. **I2C 冲突:** IMU 与触摸必须分 bus;禁止将 QMI8658 挂到 `I2C_NUM_0` 与 CST816 共用。 +2. **WHO_AM_I / 地址:** 硬件 SDO 接法可能导致 `0x6A`;init 失败时优先排查。 +3. **轴映射:** MVP 用合成幅值,方向错误仅影响 Phase 4 方向性手势,不影响 MVP。 +4. **Settings 未 Save 切屏:** 摇离 Settings 不 collect Tab;用户未 Save 的改动仍在内存,重启丢失——与现有 Settings 行为一致,需在 UI 文案或后续 Save 提示中考虑(非 MVP)。 +5. **CPU / 功耗:** 100 Hz 轮询 + I2C 读增加功耗;电池场景 Phase 4 改 INT 驱动。 +6. **Screen 布局:** Screen1/2/3 可能仍为 SquareLine 竖屏坐标,切页后布局可能错位;属 `settings-screen-landscape.md` Phase 3,不阻塞本 spec MVP。 +7. **误触:** 敲桌、走路仍可能触发;MVP 靠 threshold + cooldown,不做机器学习。 +8. **Factory Reset 范围:** 与 Settings spec 一致,Phase 1 仅擦除 `"settings"` namespace;BLE bonding 等保留。 +9. **陀螺仪:** MVP 不读 gyro;减少驱动复杂度。 + +--- + +## 12. 参考 + +- 硬件引脚:`README.md` IMU 表 +- 板级配置:`main/bsp/board_config.h` +- 触摸 I2C 范例:`main/bsp/touch.c` +- LVGL 线程锁:`main/bsp/lvgl_port.c`(`lvgl_port_lock` / `unlock`) +- 按键 Queue 范例:`main/bsp/boot_button.c` +- UI 入口与默认屏:`main/ui/ui.c` +- 切屏辅助: `main/ui/ui_helpers.c`(`_ui_screen_change`) +- Settings 数据:`main/ui/settings/settings_store.c` +- 姊妹规格:`docs/spec/settings-screen-landscape.md`(Phase 3 多屏导航由本 spec `ui_nav` 承接) +- 姊妹规格:`docs/spec/ble-hid-keyboard.md`(HID 与切页独立) +- LVGL 9.2:`lv_screen_load_anim()`、`LV_SCR_LOAD_ANIM_FADE_IN` +- QMI8658A 数据手册(寄存器与 WHO_AM_I,实现 Phase 1 前必读) + +--- + +## 修订记录 + +| 版本 | 日期 | 说明 | +|------|------|------| +| 1.0 | 2026-07-05 | 初稿:四屏循环、任意摇→下一页、QMI8658 BSP、motion_gesture、ui_nav、Settings/NVS、分阶段实现与 AC-1~AC-8 |