Files
rsgltzyd e5e1d5b85e feat: 添加摇一摇切换页面实现规格文档
- 新建 shake-nav-screen.md 文件,详细描述摇一摇功能的背景、目标、硬件与BSP配置、系统架构及摇一摇检测算法
- 包含IMU驱动、页面路由机制及后续开发计划,为实现摇一摇导航提供清晰的指导
2026-07-05 21:05:14 +08:00

669 lines
23 KiB
Markdown
Raw Permalink 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.
# 摇一摇切换页面实现规格
> 项目:HeavenlyHeroStar
> 目标芯片:ESP32-S3
> ESP-IDF6.0.1
> LVGL9.2.2
> 逻辑分辨率:320×170(全局横屏)
> IMUQMI86583 轴加速度 + 3 轴陀螺仪)
> 状态:待实现(v1.0 初稿)
## 1. 背景与目标
### 1.1 现状
板载 QMI8658 六轴 IMU 已在硬件文档中定义,固件侧尚未接入;UI 层四屏已创建但无页面路由机制。
| 模块 | 路径 | 说明 |
|------|------|------|
| IMU 硬件 | `README.md` | QMI8658I2C SDA=IO8 / SCL=IO7INT1=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 | 中断 1Phase 4Data Ready / Motion |
| IMU_INT2 | IO15 | 中断 2Phase 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 | 0x350x3A | 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=Settings1=Screen12=Screen23=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_IOGPIO4** 短按映射为 `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` | 1Med |
`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 | 确认四屏轮播顺序与「任意摇→下一页」 | 已确认 |
### 阶段 1ui_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 通过。
### 阶段 2motion_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。
### 阶段 3Settings + 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.050.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"` namespaceBLE 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-1AC-8 |