Compare commits

...

6 Commits

Author SHA1 Message Date
rsgltzyd 92bead72b9 feat: 更新 display.c,添加反转输出标志并移除不必要的日志和 GPIO 设置 2026-07-08 22:46:44 +08:00
rsgltzyd 5ef6c20862 feat: 更新ui_Screen1.c、ui_Screen2.c 和 ui_Screen3.c ,引入 board_config.h 以获取屏幕分辨率 2026-07-08 22:38:53 +08:00
rsgltzyd b154c3fa20 feat: 增强运动手势的校准与检测逻辑 2026-07-08 22:18:42 +08:00
rsgltzyd efb3c8f304 docs: 更新 README.md,添加触摸控制芯片与 IMU 传感器的 I2C 总线共享信息
- 修改触摸控制芯片 CST816 和六轴惯性测量单元 QMI8658A 的描述,说明它们共用 I2C 总线(IO47/IO48)
- 更新触摸屏和 IMU 传感器的引脚信息,确保文档准确反映硬件配置
- 添加 I2C 设备与固件配置部分,提供设备地址和说明
2026-07-05 21:29:47 +08:00
rsgltzyd ff7d82286a feat: 实现摇一摇导航功能 2026-07-05 21:28:54 +08:00
rsgltzyd e5e1d5b85e feat: 添加摇一摇切换页面实现规格文档
- 新建 shake-nav-screen.md 文件,详细描述摇一摇功能的背景、目标、硬件与BSP配置、系统架构及摇一摇检测算法
- 包含IMU驱动、页面路由机制及后续开发计划,为实现摇一摇导航提供清晰的指导
2026-07-05 21:05:14 +08:00
22 changed files with 1550 additions and 30 deletions
+19 -8
View File
@@ -13,12 +13,12 @@
| | 叠封 PSRAM | **8MB** | 用于运行代码,性能优异 | | | 叠封 PSRAM | **8MB** | 用于运行代码,性能优异 |
| | 外接 Flash | **16MB** (W25Q128) | 存储程序和数据 | | | 外接 Flash | **16MB** (W25Q128) | 存储程序和数据 |
| **显示与触摸** | LCD 屏幕 | 1.9英寸,170×320分辨率,262K彩色 | 驱动IC为ST7789V2 | | **显示与触摸** | LCD 屏幕 | 1.9英寸,170×320分辨率,262K彩色 | 驱动IC为ST7789V2 |
| | 触摸控制芯片 | **CST816** (电容式) | **触控版特有**通过I2C接口 | | | 触摸控制芯片 | **CST816** (电容式) | **触控版特有**I2C 与 IMU 共用 IO47/IO48 |
| **无线通信** | Wi-Fi | 2.4GHz, 802.11 b/g/n | | | **无线通信** | Wi-Fi | 2.4GHz, 802.11 b/g/n | |
| | 蓝牙 | Bluetooth 5 (BLE) | | | | 蓝牙 | Bluetooth 5 (BLE) | |
| | 板载天线 | 贴片陶瓷天线 | | | | 板载天线 | 贴片陶瓷天线 | |
| | 外部天线接口 | **IPEX-1 连接器** | **触控版特有**,需修改焊盘启用 | | | 外部天线接口 | **IPEX-1 连接器** | **触控版特有**,需修改焊盘启用 |
| **传感器** | 六轴惯性测量单元 | **QMI8658** | 包含3轴加速度计和3轴陀螺仪 | | **传感器** | 六轴惯性测量单元 | **QMI8658A** | 与 CST816 共用 I2C 总线(IO47/IO48 |
| **存储扩展** | Micro SD 卡槽 | 支持Micro SD卡 | 位于PCB背面 | | **存储扩展** | Micro SD 卡槽 | 支持Micro SD卡 | 位于PCB背面 |
| **电源管理** | 电池接口 | MX1.25 连接器 | 支持3.7V锂电池**充放电** | | **电源管理** | 电池接口 | MX1.25 连接器 | 支持3.7V锂电池**充放电** |
| | USB供电 | USB Type-C 接口 | 同时用于下载调试 | | | USB供电 | USB Type-C 接口 | 同时用于下载调试 |
@@ -37,18 +37,18 @@
| | | IO12 | LCD\_CS(片选) | | | | IO12 | LCD\_CS(片选) |
| | | IO13 | LCD\_DIN(数据输入) | | | | IO13 | LCD\_DIN(数据输入) |
| | | IO14 | LCD\_BL(背光控制,PWM | | | | IO14 | LCD\_BL(背光控制,PWM |
| **触摸屏** | I2C | IO47 | TP\_SDA(数据线) | | **触摸屏 / I2C 总线** | I2C | IO47 | TP\_SDA / IMU\_SDA(与 IMU 共用) |
| | | IO48 | TP\_SCL(时钟线) | | (CST816 + QMI8658) | | IO48 | TP\_SCL / IMU\_SCL(与 IMU 共用) |
| | GPIO | IO17 | TP\_RESET(复位) | | | GPIO | IO17 | TP\_RESET(复位) |
| | | IO21 | TP\_INT(中断) | | | | IO21 | TP\_INT(中断) |
| **SD 卡** | SPI | IO5 | SD\_CS(片选) | | **SD 卡** | SPI | IO5 | SD\_CS(片选) |
| | | IO39 | SD\_CLK(时钟) | | | | IO39 | SD\_CLK(时钟) |
| | | IO40 | SD\_MISO(主入从出) | | | | IO40 | SD\_MISO(主入从出) |
| | | IO41 | SD\_MOSI(主出从入) | | | | IO41 | SD\_MOSI(主出从入) |
| **IMU 传感器** | I2C | IO8 | IMU\_SDA数据线) | | **IMU 传感器** | I2C | IO47 | IMU\_SDA与 TP 共用,见上) |
| (QMI8658A) | | IO7 | IMU\_SCL时钟线) | | (QMI8658A) | | IO48 | IMU\_SCL与 TP 共用,见上) |
| | GPIO | IO6 | IMU\_INT1(中断1 | | | GPIO | IO8 | IMU\_INT1(中断1 |
| | | IO15 | IMU\_INT2(中断2 | | | | IO7 | IMU\_INT2(中断2 |
| **UART0 串口** | UART | IO43 | U0\_TX(发送) | | **UART0 串口** | UART | IO43 | U0\_TX(发送) |
| | | IO44 | U0\_RX(接收) | | | | IO44 | U0\_RX(接收) |
| **用户按键** | GPIO | IO0 | BOOT0(启动/下载模式) | | **用户按键** | GPIO | IO0 | BOOT0(启动/下载模式) |
@@ -63,6 +63,17 @@
| **扩展排针 J3** | 混合 | IO0~IO14 | 20Pin 排针,引出主要 GPIO | | **扩展排针 J3** | 混合 | IO0~IO14 | 20Pin 排针,引出主要 GPIO |
| **扩展排针 J4** | 混合 | IO14~IO48等 | 20Pin 排针,含 USB/3V3/地 | | **扩展排针 J4** | 混合 | IO14~IO48等 | 20Pin 排针,含 USB/3V3/地 |
### I2C 设备与固件配置
触控版 PCB 上 **CST816(触摸)****QMI8658AIMU** 挂载在同一条 I2C 总线(`I2C_NUM_0`SDA=IO47SCL=IO48)。固件在 `main/bsp/board_config.h` 中定义引脚,触摸与 IMU 驱动共用该总线句柄。
| 设备 | I2C 地址(7-bit | 说明 |
| :------- | :---------------- | :---------------------------- |
| CST816S | 由驱动自动探测 | 电容触摸 |
| QMI8658A | `0x6A``0x6B` | 由 SDO 电平决定;本板实测多为 `0x6B` |
IMU 中断引脚(IO8 / IO7)当前固件采用轮询采样,Phase 4 可改为中断驱动。
## Wi-Fi Station Example ## Wi-Fi Station Example
(See the README.md file in the upper level 'examples' directory for more information about examples.) (See the README.md file in the upper level 'examples' directory for more information about examples.)
+668
View File
@@ -0,0 +1,668 @@
# 摇一摇切换页面实现规格
> 项目: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 |
+3
View File
@@ -1,10 +1,13 @@
idf_component_register(SRCS idf_component_register(SRCS
"app/app_main.c" "app/app_main.c"
"app/ui_handlers.c" "app/ui_handlers.c"
"app/ui_nav.c"
"app/motion_gesture.c"
"bsp/display.c" "bsp/display.c"
"bsp/touch.c" "bsp/touch.c"
"bsp/lvgl_port.c" "bsp/lvgl_port.c"
"bsp/boot_button.c" "bsp/boot_button.c"
"bsp/imu.c"
"services/wifi_manager.c" "services/wifi_manager.c"
"services/ble_manager.c" "services/ble_manager.c"
"services/ble_hid.c" "services/ble_hid.c"
+19
View File
@@ -3,11 +3,15 @@
#include "board_config.h" #include "board_config.h"
#include "display.h" #include "display.h"
#include "imu.h"
#include "lvgl_port.h" #include "lvgl_port.h"
#include "motion_gesture.h"
#include "touch.h" #include "touch.h"
#include "ui.h" #include "ui.h"
#include "ui_handlers.h" #include "ui_handlers.h"
#include "ui_nav.h"
#include "ble_manager.h" #include "ble_manager.h"
#include "settings/settings_store.h"
#if BOARD_WIFI_ENABLED #if BOARD_WIFI_ENABLED
#include "wifi_manager.h" #include "wifi_manager.h"
@@ -38,10 +42,25 @@ void app_main(void)
lvgl_port_lock(); lvgl_port_lock();
ui_init(); ui_init();
ui_nav_init();
lvgl_port_unlock(); lvgl_port_unlock();
settings_store_load();
settings_store_apply();
ui_handlers_init(); ui_handlers_init();
motion_gesture_init(NULL);
#if BOARD_IMU_ENABLED
ret = imu_init();
if (ret == ESP_OK) {
imu_start_task(motion_gesture_on_sample);
} else {
ESP_LOGW(TAG, "IMU init failed, shake navigation unavailable");
}
#endif
#if BOARD_WIFI_ENABLED #if BOARD_WIFI_ENABLED
ESP_LOGI(TAG, "Starting WiFi"); ESP_LOGI(TAG, "Starting WiFi");
wifi_manager_init(); wifi_manager_init();
+295
View File
@@ -0,0 +1,295 @@
#include "motion_gesture.h"
#include "ui_nav.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include <assert.h>
#include <math.h>
static const char *TAG = "motion";
#define SAMPLE_PERIOD_MS 10
#define SAMPLE_RATE_HZ 100
/* Calibration: keep a sliding window of the last 500 ms and finish only when
* the window is stable (low variance). This avoids calibrating while the user
* is picking up or moving the device. */
#define CALIBRATION_MS 500
#define CALIBRATION_WINDOW_SAMPLES (CALIBRATION_MS / SAMPLE_PERIOD_MS)
#define CALIBRATION_MAX_VAR_G2 0.010f
/* Shake is detected on the horizontal plane (X/Y) so that picking the device
* up or putting it down (Z-axis jolt) does not trigger navigation. */
#define THRESHOLD_LOW_G 0.6f
#define THRESHOLD_MED_G 0.8f
#define THRESHOLD_HIGH_G 1.0f
/* A valid shake must produce enough strong samples within a short window. */
#define WINDOW_MS 300
#define MIN_SAMPLES 5
/* A real shake oscillates; require at least two zero-crossings on the
* dominant horizontal axis to distinguish it from a single jolt. */
#define MIN_ZERO_CROSSINGS 2
/* Cooldown prevents one physical shake from generating multiple events. */
#define COOLDOWN_MS 350
typedef enum {
GESTURE_IDLE = 0,
GESTURE_COOLDOWN,
} gesture_state_t;
static QueueHandle_t s_nav_queue;
static bool s_enabled = true;
static float s_threshold = THRESHOLD_MED_G;
static gesture_state_t s_state = GESTURE_IDLE;
/* Baseline calibration */
static float s_ref_x;
static float s_ref_y;
static float s_ref_z;
static bool s_calibrated;
static float s_cal_buf_x[CALIBRATION_WINDOW_SAMPLES];
static float s_cal_buf_y[CALIBRATION_WINDOW_SAMPLES];
static float s_cal_buf_z[CALIBRATION_WINDOW_SAMPLES];
static int s_cal_head;
static int s_cal_count;
/* Detection window state */
static int s_over_count;
static int64_t s_window_start_us;
static int s_zero_crossings;
static int s_last_x_sign;
static int s_last_y_sign;
static int64_t s_cooldown_until_us;
/* Last horizontal magnitude, exposed for UI/debug. */
static float s_last_horizontal_mag;
static void nav_handler_task(void *arg)
{
(void)arg;
motion_nav_event_t ev;
while (xQueueReceive(s_nav_queue, &ev, portMAX_DELAY) == pdTRUE) {
if (ev == MOTION_NAV_NEXT) {
ui_nav_next();
}
}
}
static void post_nav_next(void)
{
motion_nav_event_t ev = MOTION_NAV_NEXT;
if (xQueueSend(s_nav_queue, &ev, 0) != pdTRUE) {
ESP_LOGW(TAG, "nav queue full");
} else {
ESP_LOGI(TAG, "Shake detected -> next screen");
}
}
static void reset_window(void)
{
s_over_count = 0;
s_window_start_us = 0;
s_zero_crossings = 0;
s_last_x_sign = 0;
s_last_y_sign = 0;
}
static void run_calibration(float ax, float ay, float az)
{
s_cal_buf_x[s_cal_head] = ax;
s_cal_buf_y[s_cal_head] = ay;
s_cal_buf_z[s_cal_head] = az;
s_cal_head = (s_cal_head + 1) % CALIBRATION_WINDOW_SAMPLES;
if (s_cal_count < CALIBRATION_WINDOW_SAMPLES) {
s_cal_count++;
}
if (s_cal_count < CALIBRATION_WINDOW_SAMPLES) {
return;
}
float sum_x = 0.0f;
float sum_y = 0.0f;
float sum_z = 0.0f;
float sum_sq_x = 0.0f;
float sum_sq_y = 0.0f;
float sum_sq_z = 0.0f;
for (int i = 0; i < CALIBRATION_WINDOW_SAMPLES; i++) {
sum_x += s_cal_buf_x[i];
sum_y += s_cal_buf_y[i];
sum_z += s_cal_buf_z[i];
sum_sq_x += s_cal_buf_x[i] * s_cal_buf_x[i];
sum_sq_y += s_cal_buf_y[i] * s_cal_buf_y[i];
sum_sq_z += s_cal_buf_z[i] * s_cal_buf_z[i];
}
const int n = CALIBRATION_WINDOW_SAMPLES;
float var_x = (sum_sq_x / n) - (sum_x / n) * (sum_x / n);
float var_y = (sum_sq_y / n) - (sum_y / n) * (sum_y / n);
float var_z = (sum_sq_z / n) - (sum_z / n) * (sum_z / n);
float max_var = fmaxf(var_x, fmaxf(var_y, var_z));
if (max_var > CALIBRATION_MAX_VAR_G2) {
static int64_t s_last_cal_warn_us;
int64_t now_us = esp_timer_get_time();
if ((now_us - s_last_cal_warn_us) >= 1000 * 1000) {
s_last_cal_warn_us = now_us;
ESP_LOGW(TAG, "Calibration: motion detected (var=%.3f), waiting", max_var);
}
return;
}
s_ref_x = sum_x / n;
s_ref_y = sum_y / n;
s_ref_z = sum_z / n;
s_calibrated = true;
ESP_LOGI(TAG, "Baseline calibrated (ref=%.3f,%.3f,%.3f g)", s_ref_x, s_ref_y, s_ref_z);
}
static int axis_sign(float value)
{
if (value > 0.0f) {
return 1;
}
if (value < 0.0f) {
return -1;
}
return 0;
}
static void process_sample(float ax, float ay, float az)
{
int64_t now_us = esp_timer_get_time();
if (!s_calibrated) {
run_calibration(ax, ay, az);
return;
}
float dx = ax - s_ref_x;
float dy = ay - s_ref_y;
float dz = az - s_ref_z;
/* Use only horizontal magnitude for shake detection. Z-axis is dominated by
* gravity and is the main axis for pick-up/put-down false triggers. */
float horizontal_mag = sqrtf(dx * dx + dy * dy);
s_last_horizontal_mag = horizontal_mag;
if (s_state == GESTURE_COOLDOWN) {
if (now_us >= s_cooldown_until_us) {
s_state = GESTURE_IDLE;
reset_window();
}
return;
}
if (horizontal_mag > s_threshold) {
if (s_over_count == 0) {
s_window_start_us = now_us;
}
s_over_count++;
int x_sign = axis_sign(dx);
int y_sign = axis_sign(dy);
if (s_last_x_sign != 0 && x_sign != s_last_x_sign) {
s_zero_crossings++;
}
if (s_last_y_sign != 0 && y_sign != s_last_y_sign) {
s_zero_crossings++;
}
s_last_x_sign = x_sign;
s_last_y_sign = y_sign;
int64_t window_us = (int64_t)WINDOW_MS * 1000;
if ((now_us - s_window_start_us) > window_us) {
/* Window expired: start fresh with the current sample. */
reset_window();
s_window_start_us = now_us;
s_over_count = 1;
s_last_x_sign = axis_sign(dx);
s_last_y_sign = axis_sign(dy);
}
if (s_over_count >= MIN_SAMPLES && s_zero_crossings >= MIN_ZERO_CROSSINGS) {
if (s_enabled) {
post_nav_next();
}
s_state = GESTURE_COOLDOWN;
s_cooldown_until_us = now_us + (int64_t)COOLDOWN_MS * 1000;
reset_window();
}
} else if (s_over_count > 0) {
int64_t window_us = (int64_t)WINDOW_MS * 1000;
if ((now_us - s_window_start_us) > window_us) {
reset_window();
}
}
(void)dz;
}
void motion_gesture_init(motion_nav_cb_t cb)
{
(void)cb;
s_nav_queue = xQueueCreate(4, sizeof(motion_nav_event_t));
xTaskCreate(nav_handler_task, "nav_handler", 2048, NULL, 5, NULL);
s_calibrated = false;
s_cal_head = 0;
s_cal_count = 0;
ESP_LOGI(TAG, "Motion gesture ready (calibrating %d ms)", CALIBRATION_MS);
}
void motion_gesture_set_enabled(bool enabled)
{
s_enabled = enabled;
ESP_LOGI(TAG, "Shake navigation %s", enabled ? "enabled" : "disabled");
}
void motion_gesture_set_sensitivity(shake_sensitivity_t sens)
{
switch (sens) {
case SHAKE_SENS_LOW:
s_threshold = THRESHOLD_LOW_G;
break;
case SHAKE_SENS_MED:
s_threshold = THRESHOLD_MED_G;
break;
case SHAKE_SENS_HIGH:
s_threshold = THRESHOLD_HIGH_G;
break;
default:
assert(false && "unexpected shake_sensitivity_t");
break;
}
ESP_LOGI(TAG, "Sensitivity set (threshold=%.2f g)", s_threshold);
}
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g)
{
process_sample(ax_g, ay_g, az_g);
}
float motion_gesture_get_last_magnitude(void)
{
return s_last_horizontal_mag;
}
bool motion_gesture_is_calibrated(void)
{
return s_calibrated;
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <stdbool.h>
#include "settings_types.h"
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);
float motion_gesture_get_last_magnitude(void);
bool motion_gesture_is_calibrated(void);
+6
View File
@@ -105,6 +105,12 @@ void settings_on_value_changed(lv_event_t *e)
case SETTINGS_FIELD_MUTE: case SETTINGS_FIELD_MUTE:
buf.mute = lv_obj_has_state(target, LV_STATE_CHECKED); buf.mute = lv_obj_has_state(target, LV_STATE_CHECKED);
break; break;
case SETTINGS_FIELD_SHAKE_NAV:
buf.shake_nav_enabled = lv_obj_has_state(target, LV_STATE_CHECKED);
break;
case SETTINGS_FIELD_SHAKE_SENS:
buf.shake_sensitivity = (shake_sensitivity_t)lv_dropdown_get_selected(target);
break;
default: default:
LV_ASSERT_MSG(false, "unexpected settings_field_t"); LV_ASSERT_MSG(false, "unexpected settings_field_t");
break; break;
+76
View File
@@ -0,0 +1,76 @@
#include "ui_nav.h"
#include "lvgl_port.h"
#include "screens/ui_Screen1.h"
#include "screens/ui_Screen2.h"
#include "screens/ui_Screen3.h"
#include "screens/ui_Settings.h"
#include "ui_helpers.h"
#include "esp_log.h"
static const char *TAG = "ui_nav";
static lv_obj_t *s_nav_screens[4];
static const char *const s_nav_names[] = {
"Settings",
"Screen1",
"Screen2",
"Screen3",
};
#define NAV_SCREEN_COUNT (sizeof(s_nav_screens) / sizeof(s_nav_screens[0]))
static uint8_t s_nav_index;
void ui_nav_init(void)
{
s_nav_screens[0] = ui_Settings;
s_nav_screens[1] = ui_Screen1;
s_nav_screens[2] = ui_Screen2;
s_nav_screens[3] = ui_Screen3;
s_nav_index = 0;
ESP_LOGI(TAG, "Navigation ready (%u screens, start=%s)", (unsigned)NAV_SCREEN_COUNT,
s_nav_names[s_nav_index]);
}
void ui_nav_goto(uint8_t index)
{
if (index >= NAV_SCREEN_COUNT) {
return;
}
lvgl_port_lock();
s_nav_index = index;
lv_obj_t **target = &s_nav_screens[s_nav_index];
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
lvgl_port_unlock();
ESP_LOGI(TAG, "Goto %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
}
void ui_nav_next(void)
{
lvgl_port_lock();
s_nav_index = (s_nav_index + 1) % NAV_SCREEN_COUNT;
lv_obj_t **target = &s_nav_screens[s_nav_index];
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
lvgl_port_unlock();
ESP_LOGI(TAG, "Next -> %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
}
lv_obj_t *ui_nav_current(void)
{
return s_nav_screens[s_nav_index];
}
uint8_t ui_nav_current_index(void)
{
return s_nav_index;
}
const char *ui_nav_current_name(void)
{
return s_nav_names[s_nav_index];
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "lvgl.h"
void ui_nav_init(void);
void ui_nav_next(void);
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);
+9
View File
@@ -49,6 +49,15 @@
#define BOARD_TOUCH_I2C_FREQ_HZ 400000 #define BOARD_TOUCH_I2C_FREQ_HZ 400000
#define BOARD_TOUCH_ENABLED 1 #define BOARD_TOUCH_ENABLED 1
/* IMU (QMI8658) — shares I2C bus with touch on IO47/IO48 */
#define BOARD_IMU_I2C_NUM I2C_NUM_0
#define BOARD_PIN_IMU_SDA 47
#define BOARD_PIN_IMU_SCL 48
#define BOARD_PIN_IMU_INT1 8
#define BOARD_PIN_IMU_INT2 7
#define BOARD_IMU_I2C_FREQ_HZ 400000
#define BOARD_IMU_ENABLED 1
/* User IO */ /* User IO */
#define BOARD_PIN_BOOT 0 #define BOARD_PIN_BOOT 0
#define BOARD_PIN_KEY 4 #define BOARD_PIN_KEY 4
+1 -4
View File
@@ -2,7 +2,6 @@
#include "board_config.h" #include "board_config.h"
#include "driver/gpio.h"
#include "driver/ledc.h" #include "driver/ledc.h"
#include "driver/spi_master.h" #include "driver/spi_master.h"
#include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_ops.h"
@@ -49,6 +48,7 @@ esp_err_t display_init(esp_lcd_panel_io_handle_t *io_handle_out,
.gpio_num = BOARD_PIN_BK_LIGHT, .gpio_num = BOARD_PIN_BK_LIGHT,
.duty = 0, .duty = 0,
.hpoint = 0, .hpoint = 0,
.flags.output_invert = 1,
}; };
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
@@ -90,9 +90,6 @@ esp_err_t display_init(esp_lcd_panel_io_handle_t *io_handle_out,
ESP_ERROR_CHECK(esp_lcd_panel_set_gap(panel_handle, BOARD_LCD_GAP_X, BOARD_LCD_GAP_Y)); ESP_ERROR_CHECK(esp_lcd_panel_set_gap(panel_handle, BOARD_LCD_GAP_X, BOARD_LCD_GAP_Y));
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
ESP_LOGI(TAG, "Turn on LCD backlight");
gpio_set_level(BOARD_PIN_BK_LIGHT, BOARD_LCD_BK_LIGHT_ON_LEVEL);
*io_handle_out = io_handle; *io_handle_out = io_handle;
*panel_handle_out = panel_handle; *panel_handle_out = panel_handle;
return ESP_OK; return ESP_OK;
+308
View File
@@ -0,0 +1,308 @@
#include "imu.h"
#include "board_config.h"
#include "touch.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <math.h>
static const char *TAG = "imu";
#if BOARD_IMU_ENABLED
#define QMI8658_ADDR_SDO_LOW 0x6A
#define QMI8658_ADDR_SDO_HIGH 0x6B
#define QMI8658_WHO_AM_I 0x00
#define QMI8658_WHO_AM_I_ID 0x05
#define QMI8658_CTRL1 0x02
#define QMI8658_CTRL2 0x03
#define QMI8658_CTRL3 0x04
#define QMI8658_CTRL5 0x06
#define QMI8658_CTRL6 0x07
#define QMI8658_CTRL7 0x08
#define QMI8658_RESET 0x60
#define QMI8658_ACC_X_L 0x35
#define QMI8658_RESET_VALUE 0xB0
/* Little-endian + address auto-increment (matches int16 parse below) */
#define QMI8658_CTRL1_VALUE 0x40
/* ±8g, 1000 Hz ODR */
#define QMI8658_CTRL2_VALUE 0x23
/* Gyro ±512 dps, 1000 Hz — required even when only reading accel */
#define QMI8658_CTRL3_VALUE 0x53
#define QMI8658_CTRL5_VALUE 0x11
#define QMI8658_CTRL6_VALUE 0x00
#define QMI8658_CTRL7_VALUE 0x03
#define QMI8658_ACCEL_SENS_8G 4096.0f
#define IMU_SAMPLE_PERIOD_MS 10
#define IMU_DEBUG_LOG_INTERVAL_MS 1000
static i2c_master_bus_handle_t s_imu_bus;
static i2c_master_dev_handle_t s_imu_dev;
static imu_sample_cb_t s_sample_cb;
static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz);
static int16_t imu_raw16_le(const uint8_t *lo, const uint8_t *hi)
{
return (int16_t)((*hi << 8) | *lo);
}
static esp_err_t imu_write_u8(uint8_t reg, uint8_t val)
{
uint8_t buf[2] = {reg, val};
return i2c_master_transmit(s_imu_dev, buf, sizeof(buf), 100);
}
static esp_err_t imu_read_u8(uint8_t reg, uint8_t *val)
{
return i2c_master_transmit_receive(s_imu_dev, &reg, 1, val, 1, 100);
}
static esp_err_t imu_read_bytes(uint8_t reg, uint8_t *data, size_t len)
{
return i2c_master_transmit_receive(s_imu_dev, &reg, 1, data, len, 100);
}
static esp_err_t imu_probe_device(uint8_t addr)
{
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = addr,
.scl_speed_hz = BOARD_IMU_I2C_FREQ_HZ,
};
esp_err_t err = i2c_master_bus_add_device(s_imu_bus, &dev_cfg, &s_imu_dev);
if (err != ESP_OK) {
return err;
}
uint8_t who_am_i = 0;
err = imu_read_u8(QMI8658_WHO_AM_I, &who_am_i);
if (err != ESP_OK || who_am_i != QMI8658_WHO_AM_I_ID) {
i2c_master_bus_rm_device(s_imu_dev);
s_imu_dev = NULL;
if (err == ESP_OK) {
ESP_LOGW(TAG, "WHO_AM_I mismatch at 0x%02X: 0x%02X", addr, who_am_i);
return ESP_ERR_NOT_FOUND;
}
return err;
}
ESP_LOGI(TAG, "QMI8658 found at 0x%02X (WHO_AM_I=0x%02X)", addr, who_am_i);
return ESP_OK;
}
static esp_err_t imu_configure(void)
{
esp_err_t err = imu_write_u8(QMI8658_RESET, QMI8658_RESET_VALUE);
if (err != ESP_OK) {
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
err = imu_write_u8(QMI8658_CTRL1, QMI8658_CTRL1_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL2, QMI8658_CTRL2_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL3, QMI8658_CTRL3_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL5, QMI8658_CTRL5_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL6, QMI8658_CTRL6_VALUE);
if (err != ESP_OK) {
return err;
}
err = imu_write_u8(QMI8658_CTRL7, QMI8658_CTRL7_VALUE);
if (err != ESP_OK) {
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
return ESP_OK;
}
static void imu_log_sample(const char *label, float ax, float ay, float az,
int16_t rx, int16_t ry, int16_t rz)
{
float mag = sqrtf(ax * ax + ay * ay + az * az);
ESP_LOGI(TAG, "%s raw=(%d,%d,%d) g=(%.3f,%.3f,%.3f) |mag|=%.3f",
label, rx, ry, rz, ax, ay, az, mag);
}
static void imu_task(void *arg)
{
(void)arg;
int64_t last_log_us = esp_timer_get_time();
while (1) {
float ax = 0.0f;
float ay = 0.0f;
float az = 0.0f;
int16_t rx = 0;
int16_t ry = 0;
int16_t rz = 0;
esp_err_t err = imu_read_raw(&rx, &ry, &rz);
if (err == ESP_OK) {
ax = (float)rx / QMI8658_ACCEL_SENS_8G;
ay = (float)ry / QMI8658_ACCEL_SENS_8G;
az = (float)rz / QMI8658_ACCEL_SENS_8G;
int64_t now_us = esp_timer_get_time();
if ((now_us - last_log_us) >= (int64_t)IMU_DEBUG_LOG_INTERVAL_MS * 1000) {
imu_log_sample("sample", ax, ay, az, rx, ry, rz);
last_log_us = now_us;
}
if (s_sample_cb != NULL) {
s_sample_cb(ax, ay, az);
}
}
vTaskDelay(pdMS_TO_TICKS(IMU_SAMPLE_PERIOD_MS));
}
}
esp_err_t imu_init(void)
{
#if BOARD_TOUCH_ENABLED
s_imu_bus = touch_get_i2c_bus();
if (s_imu_bus == NULL) {
ESP_LOGE(TAG, "Shared I2C bus not ready (touch not initialized?)");
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Using shared I2C bus (SDA=%d SCL=%d)", BOARD_PIN_IMU_SDA,
BOARD_PIN_IMU_SCL);
#else
ESP_LOGI(TAG, "Initialize dedicated I2C for IMU");
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = BOARD_IMU_I2C_NUM,
.sda_io_num = BOARD_PIN_IMU_SDA,
.scl_io_num = BOARD_PIN_IMU_SCL,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
esp_err_t bus_err = i2c_new_master_bus(&bus_config, &s_imu_bus);
if (bus_err != ESP_OK) {
return bus_err;
}
#endif
esp_err_t err = imu_probe_device(QMI8658_ADDR_SDO_LOW);
if (err != ESP_OK) {
err = imu_probe_device(QMI8658_ADDR_SDO_HIGH);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "QMI8658 not found");
return err;
}
err = imu_configure();
if (err != ESP_OK) {
ESP_LOGE(TAG, "IMU configure failed: %s", esp_err_to_name(err));
return err;
}
float ax = 0.0f;
float ay = 0.0f;
float az = 0.0f;
int16_t rx = 0;
int16_t ry = 0;
int16_t rz = 0;
if (imu_read_accel(&ax, &ay, &az) == ESP_OK) {
imu_read_raw(&rx, &ry, &rz);
imu_log_sample("init check", ax, ay, az, rx, ry, rz);
} else {
ESP_LOGW(TAG, "Init check read failed");
}
ESP_LOGI(TAG, "IMU initialized (±8g, 1000 Hz ODR)");
return ESP_OK;
}
static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz)
{
if (s_imu_dev == NULL || rx == NULL || ry == NULL || rz == NULL) {
return ESP_ERR_INVALID_STATE;
}
uint8_t raw[6];
esp_err_t err = imu_read_bytes(QMI8658_ACC_X_L, raw, sizeof(raw));
if (err != ESP_OK) {
return err;
}
*rx = imu_raw16_le(&raw[0], &raw[1]);
*ry = imu_raw16_le(&raw[2], &raw[3]);
*rz = imu_raw16_le(&raw[4], &raw[5]);
return ESP_OK;
}
esp_err_t imu_read_accel(float *ax, float *ay, float *az)
{
int16_t rx = 0;
int16_t ry = 0;
int16_t rz = 0;
esp_err_t err = imu_read_raw(&rx, &ry, &rz);
if (err != ESP_OK) {
return err;
}
*ax = (float)rx / QMI8658_ACCEL_SENS_8G;
*ay = (float)ry / QMI8658_ACCEL_SENS_8G;
*az = (float)rz / QMI8658_ACCEL_SENS_8G;
return ESP_OK;
}
void imu_start_task(imu_sample_cb_t cb)
{
s_sample_cb = cb;
xTaskCreate(imu_task, "imu_task", 3072, NULL, 4, NULL);
ESP_LOGI(TAG, "IMU sample task started");
}
#else
esp_err_t imu_init(void)
{
return ESP_OK;
}
esp_err_t imu_read_accel(float *ax, float *ay, float *az)
{
(void)ax;
(void)ay;
(void)az;
return ESP_ERR_NOT_SUPPORTED;
}
void imu_start_task(imu_sample_cb_t cb)
{
(void)cb;
}
#endif
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "esp_err.h"
typedef void (*imu_sample_cb_t)(float ax_g, float ay_g, float az_g);
esp_err_t imu_init(void);
esp_err_t imu_read_accel(float *ax, float *ay, float *az);
void imu_start_task(imu_sample_cb_t cb);
+10
View File
@@ -103,6 +103,11 @@ esp_err_t touch_init(lv_display_t *display)
return ESP_OK; return ESP_OK;
} }
i2c_master_bus_handle_t touch_get_i2c_bus(void)
{
return s_touch_i2c_bus;
}
#else #else
esp_err_t touch_init(lv_display_t *display) esp_err_t touch_init(lv_display_t *display)
@@ -111,4 +116,9 @@ esp_err_t touch_init(lv_display_t *display)
return ESP_OK; return ESP_OK;
} }
i2c_master_bus_handle_t touch_get_i2c_bus(void)
{
return NULL;
}
#endif #endif
+2
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include "driver/i2c_master.h"
#include "esp_err.h" #include "esp_err.h"
#include "lvgl.h" #include "lvgl.h"
esp_err_t touch_init(lv_display_t *display); esp_err_t touch_init(lv_display_t *display);
i2c_master_bus_handle_t touch_get_i2c_bus(void);
+4 -2
View File
@@ -4,6 +4,7 @@
// Project name: HeavenlyHeroStar // Project name: HeavenlyHeroStar
#include "../ui.h" #include "../ui.h"
#include "board_config.h"
lv_obj_t * uic_Calendar1; lv_obj_t * uic_Calendar1;
lv_obj_t * ui_Screen1 = NULL; lv_obj_t * ui_Screen1 = NULL;
@@ -15,12 +16,13 @@ lv_obj_t * ui_Calendar1 = NULL;
void ui_Screen1_screen_init(void) void ui_Screen1_screen_init(void)
{ {
ui_Screen1 = lv_obj_create(NULL); ui_Screen1 = lv_obj_create(NULL);
lv_obj_set_size(ui_Screen1, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); /// Flags lv_obj_remove_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); /// Flags
ui_Calendar1 = lv_calendar_create(ui_Screen1); ui_Calendar1 = lv_calendar_create(ui_Screen1);
lv_obj_t * ui_Calendar1_header = lv_calendar_header_arrow_create(ui_Calendar1); lv_obj_t * ui_Calendar1_header = lv_calendar_header_arrow_create(ui_Calendar1);
lv_obj_set_width(ui_Calendar1, 170); lv_obj_set_width(ui_Calendar1, BOARD_LCD_LOGIC_H_RES);
lv_obj_set_height(ui_Calendar1, 320); lv_obj_set_height(ui_Calendar1, BOARD_LCD_LOGIC_V_RES);
lv_obj_set_align(ui_Calendar1, LV_ALIGN_CENTER); lv_obj_set_align(ui_Calendar1, LV_ALIGN_CENTER);
uic_Calendar1 = ui_Calendar1; uic_Calendar1 = ui_Calendar1;
+12 -6
View File
@@ -4,6 +4,7 @@
// Project name: HeavenlyHeroStar // Project name: HeavenlyHeroStar
#include "../ui.h" #include "../ui.h"
#include "board_config.h"
lv_obj_t * ui_Screen2 = NULL; lv_obj_t * ui_Screen2 = NULL;
lv_obj_t * ui_Button1 = NULL; lv_obj_t * ui_Button1 = NULL;
@@ -45,11 +46,14 @@ void ui_event_Button3(lv_event_t * e)
void ui_Screen2_screen_init(void) void ui_Screen2_screen_init(void)
{ {
ui_Screen2 = lv_obj_create(NULL); ui_Screen2 = lv_obj_create(NULL);
lv_obj_set_size(ui_Screen2, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Screen2, LV_OBJ_FLAG_SCROLLABLE); /// Flags lv_obj_remove_flag(ui_Screen2, LV_OBJ_FLAG_SCROLLABLE); /// Flags
ui_Button1 = lv_button_create(ui_Screen2); ui_Button1 = lv_button_create(ui_Screen2);
lv_obj_set_width(ui_Button1, 100); lv_obj_set_width(ui_Button1, 100);
lv_obj_set_height(ui_Button1, 80); lv_obj_set_height(ui_Button1, 80);
lv_obj_set_x(ui_Button1, -100);
lv_obj_set_y(ui_Button1, 0);
lv_obj_set_align(ui_Button1, LV_ALIGN_CENTER); lv_obj_set_align(ui_Button1, LV_ALIGN_CENTER);
lv_obj_add_flag(ui_Button1, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags lv_obj_add_flag(ui_Button1, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags
lv_obj_remove_flag(ui_Button1, LV_OBJ_FLAG_SCROLLABLE); /// Flags lv_obj_remove_flag(ui_Button1, LV_OBJ_FLAG_SCROLLABLE); /// Flags
@@ -58,7 +62,7 @@ void ui_Screen2_screen_init(void)
lv_obj_set_width(ui_Button2, 100); lv_obj_set_width(ui_Button2, 100);
lv_obj_set_height(ui_Button2, 80); lv_obj_set_height(ui_Button2, 80);
lv_obj_set_x(ui_Button2, 0); lv_obj_set_x(ui_Button2, 0);
lv_obj_set_y(ui_Button2, 100); lv_obj_set_y(ui_Button2, 0);
lv_obj_set_align(ui_Button2, LV_ALIGN_CENTER); lv_obj_set_align(ui_Button2, LV_ALIGN_CENTER);
lv_obj_add_flag(ui_Button2, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags lv_obj_add_flag(ui_Button2, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags
lv_obj_remove_flag(ui_Button2, LV_OBJ_FLAG_SCROLLABLE); /// Flags lv_obj_remove_flag(ui_Button2, LV_OBJ_FLAG_SCROLLABLE); /// Flags
@@ -66,8 +70,8 @@ void ui_Screen2_screen_init(void)
ui_Button3 = lv_button_create(ui_Screen2); ui_Button3 = lv_button_create(ui_Screen2);
lv_obj_set_width(ui_Button3, 100); lv_obj_set_width(ui_Button3, 100);
lv_obj_set_height(ui_Button3, 80); lv_obj_set_height(ui_Button3, 80);
lv_obj_set_x(ui_Button3, 0); lv_obj_set_x(ui_Button3, 100);
lv_obj_set_y(ui_Button3, -100); lv_obj_set_y(ui_Button3, 0);
lv_obj_set_align(ui_Button3, LV_ALIGN_CENTER); lv_obj_set_align(ui_Button3, LV_ALIGN_CENTER);
lv_obj_add_flag(ui_Button3, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags lv_obj_add_flag(ui_Button3, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags
lv_obj_remove_flag(ui_Button3, LV_OBJ_FLAG_SCROLLABLE); /// Flags lv_obj_remove_flag(ui_Button3, LV_OBJ_FLAG_SCROLLABLE); /// Flags
@@ -75,6 +79,8 @@ void ui_Screen2_screen_init(void)
ui_Label2 = lv_label_create(ui_Screen2); ui_Label2 = lv_label_create(ui_Screen2);
lv_obj_set_width(ui_Label2, LV_SIZE_CONTENT); /// 1 lv_obj_set_width(ui_Label2, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_Label2, LV_SIZE_CONTENT); /// 1 lv_obj_set_height(ui_Label2, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_Label2, -100);
lv_obj_set_y(ui_Label2, 0);
lv_obj_set_align(ui_Label2, LV_ALIGN_CENTER); lv_obj_set_align(ui_Label2, LV_ALIGN_CENTER);
lv_label_set_text(ui_Label2, "Ctrl+C"); lv_label_set_text(ui_Label2, "Ctrl+C");
@@ -82,15 +88,15 @@ void ui_Screen2_screen_init(void)
lv_obj_set_width(ui_Label4, LV_SIZE_CONTENT); /// 1 lv_obj_set_width(ui_Label4, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_Label4, LV_SIZE_CONTENT); /// 1 lv_obj_set_height(ui_Label4, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_Label4, 0); lv_obj_set_x(ui_Label4, 0);
lv_obj_set_y(ui_Label4, 100); lv_obj_set_y(ui_Label4, 0);
lv_obj_set_align(ui_Label4, LV_ALIGN_CENTER); lv_obj_set_align(ui_Label4, LV_ALIGN_CENTER);
lv_label_set_text(ui_Label4, "Ctrl+V"); lv_label_set_text(ui_Label4, "Ctrl+V");
ui_Label5 = lv_label_create(ui_Screen2); ui_Label5 = lv_label_create(ui_Screen2);
lv_obj_set_width(ui_Label5, LV_SIZE_CONTENT); /// 1 lv_obj_set_width(ui_Label5, LV_SIZE_CONTENT); /// 1
lv_obj_set_height(ui_Label5, LV_SIZE_CONTENT); /// 1 lv_obj_set_height(ui_Label5, LV_SIZE_CONTENT); /// 1
lv_obj_set_x(ui_Label5, 0); lv_obj_set_x(ui_Label5, 100);
lv_obj_set_y(ui_Label5, -100); lv_obj_set_y(ui_Label5, 0);
lv_obj_set_align(ui_Label5, LV_ALIGN_CENTER); lv_obj_set_align(ui_Label5, LV_ALIGN_CENTER);
lv_label_set_text(ui_Label5, "Ctrl+X"); lv_label_set_text(ui_Label5, "Ctrl+X");
+10 -8
View File
@@ -4,6 +4,7 @@
// Project name: HeavenlyHeroStar // Project name: HeavenlyHeroStar
#include "../ui.h" #include "../ui.h"
#include "board_config.h"
lv_obj_t * ui_Screen3 = NULL; lv_obj_t * ui_Screen3 = NULL;
lv_obj_t * ui_BtnVolDown = NULL; lv_obj_t * ui_BtnVolDown = NULL;
@@ -23,8 +24,8 @@ static void ui_screen3_create_button(lv_obj_t **btn, lv_obj_t **label, int32_t x
const char *text) const char *text)
{ {
*btn = lv_button_create(ui_Screen3); *btn = lv_button_create(ui_Screen3);
lv_obj_set_width(*btn, 75); lv_obj_set_width(*btn, 90);
lv_obj_set_height(*btn, 90); lv_obj_set_height(*btn, 45);
lv_obj_set_x(*btn, x); lv_obj_set_x(*btn, x);
lv_obj_set_y(*btn, y); lv_obj_set_y(*btn, y);
lv_obj_set_align(*btn, LV_ALIGN_CENTER); lv_obj_set_align(*btn, LV_ALIGN_CENTER);
@@ -100,14 +101,15 @@ void ui_event_BtnNext(lv_event_t * e)
void ui_Screen3_screen_init(void) void ui_Screen3_screen_init(void)
{ {
ui_Screen3 = lv_obj_create(NULL); ui_Screen3 = lv_obj_create(NULL);
lv_obj_set_size(ui_Screen3, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Screen3, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(ui_Screen3, LV_OBJ_FLAG_SCROLLABLE);
ui_screen3_create_button(&ui_BtnVolDown, &ui_LabelVolDown, -42, -107, "Vol-"); ui_screen3_create_button(&ui_BtnVolDown, &ui_LabelVolDown, -50, -55, "Vol-");
ui_screen3_create_button(&ui_BtnVolUp, &ui_LabelVolUp, 42, -107, "Vol+"); ui_screen3_create_button(&ui_BtnVolUp, &ui_LabelVolUp, 50, -55, "Vol+");
ui_screen3_create_button(&ui_BtnMute, &ui_LabelMute, -42, 0, "Mute"); ui_screen3_create_button(&ui_BtnMute, &ui_LabelMute, -50, 0, "Mute");
ui_screen3_create_button(&ui_BtnPlayPause, &ui_LabelPlayPause, 42, 0, "Play"); ui_screen3_create_button(&ui_BtnPlayPause, &ui_LabelPlayPause, 50, 0, "Play");
ui_screen3_create_button(&ui_BtnPrev, &ui_LabelPrev, -42, 107, "Prev"); ui_screen3_create_button(&ui_BtnPrev, &ui_LabelPrev, -50, 55, "Prev");
ui_screen3_create_button(&ui_BtnNext, &ui_LabelNext, 42, 107, "Next"); ui_screen3_create_button(&ui_BtnNext, &ui_LabelNext, 50, 55, "Next");
lv_obj_add_event_cb(ui_BtnVolDown, ui_event_BtnVolDown, LV_EVENT_ALL, NULL); lv_obj_add_event_cb(ui_BtnVolDown, ui_event_BtnVolDown, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_BtnVolUp, ui_event_BtnVolUp, LV_EVENT_ALL, NULL); lv_obj_add_event_cb(ui_BtnVolUp, ui_event_BtnVolUp, LV_EVENT_ALL, NULL);
+16
View File
@@ -1,6 +1,7 @@
#include "settings_store.h" #include "settings_store.h"
#include "display.h" #include "display.h"
#include "motion_gesture.h"
#include "esp_log.h" #include "esp_log.h"
#include "nvs.h" #include "nvs.h"
@@ -15,6 +16,8 @@ static const settings_data_t SETTINGS_DEFAULTS = {
.brightness_pct = 70, .brightness_pct = 70,
.volume_pct = 50, .volume_pct = 50,
.mute = false, .mute = false,
.shake_nav_enabled = true,
.shake_sensitivity = SHAKE_SENS_MED,
}; };
static settings_data_t s_data; static settings_data_t s_data;
@@ -48,6 +51,10 @@ void settings_store_load(void)
load_key_u8(handle, "vol", &s_data.volume_pct, SETTINGS_DEFAULTS.volume_pct); load_key_u8(handle, "vol", &s_data.volume_pct, SETTINGS_DEFAULTS.volume_pct);
load_key_u8(handle, "mute", &val, SETTINGS_DEFAULTS.mute); load_key_u8(handle, "mute", &val, SETTINGS_DEFAULTS.mute);
s_data.mute = val != 0; s_data.mute = val != 0;
load_key_u8(handle, "shake_en", &val, SETTINGS_DEFAULTS.shake_nav_enabled);
s_data.shake_nav_enabled = val != 0;
load_key_u8(handle, "shake_sens", &val, SETTINGS_DEFAULTS.shake_sensitivity);
s_data.shake_sensitivity = (shake_sensitivity_t)val;
nvs_close(handle); nvs_close(handle);
ESP_LOGI(TAG, "Settings loaded (bright=%u vol=%u)", s_data.brightness_pct, ESP_LOGI(TAG, "Settings loaded (bright=%u vol=%u)", s_data.brightness_pct,
@@ -83,6 +90,12 @@ esp_err_t settings_store_commit(void)
if (err == ESP_OK) { if (err == ESP_OK) {
err = nvs_set_u8(handle, "mute", s_data.mute ? 1 : 0); err = nvs_set_u8(handle, "mute", s_data.mute ? 1 : 0);
} }
if (err == ESP_OK) {
err = nvs_set_u8(handle, "shake_en", s_data.shake_nav_enabled ? 1 : 0);
}
if (err == ESP_OK) {
err = nvs_set_u8(handle, "shake_sens", (uint8_t)s_data.shake_sensitivity);
}
if (err == ESP_OK) { if (err == ESP_OK) {
err = nvs_commit(handle); err = nvs_commit(handle);
} }
@@ -118,4 +131,7 @@ void settings_store_apply(void)
{ {
uint16_t duty = (uint16_t)((s_data.brightness_pct * 1023U) / 100U); uint16_t duty = (uint16_t)((s_data.brightness_pct * 1023U) / 100U);
display_backlight_set_brightness(duty); display_backlight_set_brightness(duty);
motion_gesture_set_enabled(s_data.shake_nav_enabled);
motion_gesture_set_sensitivity(s_data.shake_sensitivity);
} }
+41
View File
@@ -3,6 +3,8 @@
#include "ui_handlers.h" #include "ui_handlers.h"
static lv_obj_t *s_notif_switch; static lv_obj_t *s_notif_switch;
static lv_obj_t *s_shake_nav_switch;
static lv_obj_t *s_shake_sens_dropdown;
static lv_obj_t *s_brightness_slider; static lv_obj_t *s_brightness_slider;
static lv_obj_t *s_brightness_pct_label; static lv_obj_t *s_brightness_pct_label;
static lv_obj_t *s_volume_slider; static lv_obj_t *s_volume_slider;
@@ -43,6 +45,8 @@ static lv_obj_t *create_settings_body(lv_obj_t *content)
void load_system_settings(lv_obj_t *content, const settings_data_t *data) void load_system_settings(lv_obj_t *content, const settings_data_t *data)
{ {
s_notif_switch = NULL; s_notif_switch = NULL;
s_shake_nav_switch = NULL;
s_shake_sens_dropdown = NULL;
create_status_bar(content, "System Settings"); create_status_bar(content, "System Settings");
lv_obj_t *body = create_settings_body(content); lv_obj_t *body = create_settings_body(content);
@@ -62,6 +66,35 @@ void load_system_settings(lv_obj_t *content, const settings_data_t *data)
LV_EVENT_VALUE_CHANGED, LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_NOTIFICATION); (void *)(intptr_t)SETTINGS_FIELD_NOTIFICATION);
lv_obj_t *shake_nav_label = lv_label_create(body);
lv_label_set_text(shake_nav_label, "Shake Navigation");
lv_obj_set_pos(shake_nav_label, 4, 40);
lv_obj_set_size(shake_nav_label, 120, 28);
s_shake_nav_switch = lv_switch_create(body);
lv_obj_set_pos(s_shake_nav_switch, 180, 40);
lv_obj_set_size(s_shake_nav_switch, 40, 28);
if (data->shake_nav_enabled) {
lv_obj_add_state(s_shake_nav_switch, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(s_shake_nav_switch, settings_on_value_changed,
LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_SHAKE_NAV);
lv_obj_t *shake_sens_label = lv_label_create(body);
lv_label_set_text(shake_sens_label, "Shake Sensitivity");
lv_obj_set_pos(shake_sens_label, 4, 76);
lv_obj_set_size(shake_sens_label, 120, 28);
s_shake_sens_dropdown = lv_dropdown_create(body);
lv_obj_set_pos(s_shake_sens_dropdown, 120, 76);
lv_obj_set_size(s_shake_sens_dropdown, 100, 28);
lv_dropdown_set_options(s_shake_sens_dropdown, "Low\nMed\nHigh");
lv_dropdown_set_selected(s_shake_sens_dropdown, data->shake_sensitivity);
lv_obj_add_event_cb(s_shake_sens_dropdown, settings_on_value_changed,
LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_SHAKE_SENS);
lv_obj_t *reset_btn = lv_button_create(body); lv_obj_t *reset_btn = lv_button_create(body);
lv_obj_set_pos(reset_btn, 4, 112); lv_obj_set_pos(reset_btn, 4, 112);
lv_obj_set_size(reset_btn, 100, 28); lv_obj_set_size(reset_btn, 100, 28);
@@ -170,6 +203,14 @@ void settings_tabs_collect(settings_tab_t tab, settings_data_t *out)
out->notification_enabled = out->notification_enabled =
lv_obj_has_state(s_notif_switch, LV_STATE_CHECKED); lv_obj_has_state(s_notif_switch, LV_STATE_CHECKED);
} }
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);
}
break; break;
case SETTINGS_TAB_DISPLAY: case SETTINGS_TAB_DISPLAY:
if (s_brightness_slider != NULL) { if (s_brightness_slider != NULL) {
+2
View File
@@ -7,6 +7,8 @@ typedef enum {
SETTINGS_FIELD_NOTIFICATION = 0, SETTINGS_FIELD_NOTIFICATION = 0,
SETTINGS_FIELD_VOLUME, SETTINGS_FIELD_VOLUME,
SETTINGS_FIELD_MUTE, SETTINGS_FIELD_MUTE,
SETTINGS_FIELD_SHAKE_NAV,
SETTINGS_FIELD_SHAKE_SENS,
} settings_field_t; } settings_field_t;
void load_system_settings(lv_obj_t *content, const settings_data_t *data); void load_system_settings(lv_obj_t *content, const settings_data_t *data);
+8
View File
@@ -10,9 +10,17 @@ typedef enum {
SETTINGS_TAB_NETWORK, SETTINGS_TAB_NETWORK,
} settings_tab_t; } settings_tab_t;
typedef enum {
SHAKE_SENS_LOW = 0,
SHAKE_SENS_MED,
SHAKE_SENS_HIGH,
} shake_sensitivity_t;
typedef struct { typedef struct {
bool notification_enabled; bool notification_enabled;
uint8_t brightness_pct; uint8_t brightness_pct;
uint8_t volume_pct; uint8_t volume_pct;
bool mute; bool mute;
bool shake_nav_enabled;
shake_sensitivity_t shake_sensitivity;
} settings_data_t; } settings_data_t;