Compare commits
6 Commits
6a6ee41a07
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 92bead72b9 | |||
| 5ef6c20862 | |||
| b154c3fa20 | |||
| efb3c8f304 | |||
| ff7d82286a | |||
| e5e1d5b85e |
@@ -13,12 +13,12 @@
|
||||
| | 叠封 PSRAM | **8MB** | 用于运行代码,性能优异 |
|
||||
| | 外接 Flash | **16MB** (W25Q128) | 存储程序和数据 |
|
||||
| **显示与触摸** | LCD 屏幕 | 1.9英寸,170×320分辨率,262K彩色 | 驱动IC为ST7789V2 |
|
||||
| | 触摸控制芯片 | **CST816** (电容式) | **触控版特有**,通过I2C接口 |
|
||||
| | 触摸控制芯片 | **CST816** (电容式) | **触控版特有**,I2C 与 IMU 共用 IO47/IO48 |
|
||||
| **无线通信** | Wi-Fi | 2.4GHz, 802.11 b/g/n | |
|
||||
| | 蓝牙 | Bluetooth 5 (BLE) | |
|
||||
| | 板载天线 | 贴片陶瓷天线 | |
|
||||
| | 外部天线接口 | **IPEX-1 连接器** | **触控版特有**,需修改焊盘启用 |
|
||||
| **传感器** | 六轴惯性测量单元 | **QMI8658** | 包含3轴加速度计和3轴陀螺仪 |
|
||||
| **传感器** | 六轴惯性测量单元 | **QMI8658A** | 与 CST816 共用 I2C 总线(IO47/IO48) |
|
||||
| **存储扩展** | Micro SD 卡槽 | 支持Micro SD卡 | 位于PCB背面 |
|
||||
| **电源管理** | 电池接口 | MX1.25 连接器 | 支持3.7V锂电池**充放电** |
|
||||
| | USB供电 | USB Type-C 接口 | 同时用于下载调试 |
|
||||
@@ -37,18 +37,18 @@
|
||||
| | | IO12 | LCD\_CS(片选) |
|
||||
| | | IO13 | LCD\_DIN(数据输入) |
|
||||
| | | IO14 | LCD\_BL(背光控制,PWM) |
|
||||
| **触摸屏** | I2C | IO47 | TP\_SDA(数据线) |
|
||||
| | | IO48 | TP\_SCL(时钟线) |
|
||||
| **触摸屏 / I2C 总线** | I2C | IO47 | TP\_SDA / IMU\_SDA(与 IMU 共用) |
|
||||
| (CST816 + QMI8658) | | IO48 | TP\_SCL / IMU\_SCL(与 IMU 共用) |
|
||||
| | GPIO | IO17 | TP\_RESET(复位) |
|
||||
| | | IO21 | TP\_INT(中断) |
|
||||
| **SD 卡** | SPI | IO5 | SD\_CS(片选) |
|
||||
| | | IO39 | SD\_CLK(时钟) |
|
||||
| | | IO40 | SD\_MISO(主入从出) |
|
||||
| | | IO41 | SD\_MOSI(主出从入) |
|
||||
| **IMU 传感器** | I2C | IO8 | IMU\_SDA(数据线) |
|
||||
| (QMI8658A) | | IO7 | IMU\_SCL(时钟线) |
|
||||
| | GPIO | IO6 | IMU\_INT1(中断1) |
|
||||
| | | IO15 | IMU\_INT2(中断2) |
|
||||
| **IMU 传感器** | I2C | IO47 | IMU\_SDA(与 TP 共用,见上) |
|
||||
| (QMI8658A) | | IO48 | IMU\_SCL(与 TP 共用,见上) |
|
||||
| | GPIO | IO8 | IMU\_INT1(中断1) |
|
||||
| | | IO7 | IMU\_INT2(中断2) |
|
||||
| **UART0 串口** | UART | IO43 | U0\_TX(发送) |
|
||||
| | | IO44 | U0\_RX(接收) |
|
||||
| **用户按键** | GPIO | IO0 | BOOT0(启动/下载模式) |
|
||||
@@ -63,6 +63,17 @@
|
||||
| **扩展排针 J3** | 混合 | IO0~IO14 | 20Pin 排针,引出主要 GPIO |
|
||||
| **扩展排针 J4** | 混合 | IO14~IO48等 | 20Pin 排针,含 USB/3V3/地 |
|
||||
|
||||
### I2C 设备与固件配置
|
||||
|
||||
触控版 PCB 上 **CST816(触摸)** 与 **QMI8658A(IMU)** 挂载在同一条 I2C 总线(`I2C_NUM_0`,SDA=IO47,SCL=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
|
||||
|
||||
(See the README.md file in the upper level 'examples' directory for more information about examples.)
|
||||
|
||||
@@ -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 |
|
||||
@@ -1,10 +1,13 @@
|
||||
idf_component_register(SRCS
|
||||
"app/app_main.c"
|
||||
"app/ui_handlers.c"
|
||||
"app/ui_nav.c"
|
||||
"app/motion_gesture.c"
|
||||
"bsp/display.c"
|
||||
"bsp/touch.c"
|
||||
"bsp/lvgl_port.c"
|
||||
"bsp/boot_button.c"
|
||||
"bsp/imu.c"
|
||||
"services/wifi_manager.c"
|
||||
"services/ble_manager.c"
|
||||
"services/ble_hid.c"
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
|
||||
#include "board_config.h"
|
||||
#include "display.h"
|
||||
#include "imu.h"
|
||||
#include "lvgl_port.h"
|
||||
#include "motion_gesture.h"
|
||||
#include "touch.h"
|
||||
#include "ui.h"
|
||||
#include "ui_handlers.h"
|
||||
#include "ui_nav.h"
|
||||
#include "ble_manager.h"
|
||||
#include "settings/settings_store.h"
|
||||
|
||||
#if BOARD_WIFI_ENABLED
|
||||
#include "wifi_manager.h"
|
||||
@@ -38,10 +42,25 @@ void app_main(void)
|
||||
|
||||
lvgl_port_lock();
|
||||
ui_init();
|
||||
ui_nav_init();
|
||||
lvgl_port_unlock();
|
||||
|
||||
settings_store_load();
|
||||
settings_store_apply();
|
||||
|
||||
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
|
||||
ESP_LOGI(TAG, "Starting WiFi");
|
||||
wifi_manager_init();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -105,6 +105,12 @@ void settings_on_value_changed(lv_event_t *e)
|
||||
case SETTINGS_FIELD_MUTE:
|
||||
buf.mute = lv_obj_has_state(target, LV_STATE_CHECKED);
|
||||
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:
|
||||
LV_ASSERT_MSG(false, "unexpected settings_field_t");
|
||||
break;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
@@ -49,6 +49,15 @@
|
||||
#define BOARD_TOUCH_I2C_FREQ_HZ 400000
|
||||
#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 */
|
||||
#define BOARD_PIN_BOOT 0
|
||||
#define BOARD_PIN_KEY 4
|
||||
|
||||
+1
-4
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "board_config.h"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/ledc.h"
|
||||
#include "driver/spi_master.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,
|
||||
.duty = 0,
|
||||
.hpoint = 0,
|
||||
.flags.output_invert = 1,
|
||||
};
|
||||
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_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;
|
||||
*panel_handle_out = panel_handle;
|
||||
return ESP_OK;
|
||||
|
||||
+308
@@ -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, ®, 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, ®, 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
|
||||
@@ -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);
|
||||
@@ -103,6 +103,11 @@ esp_err_t touch_init(lv_display_t *display)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
i2c_master_bus_handle_t touch_get_i2c_bus(void)
|
||||
{
|
||||
return s_touch_i2c_bus;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
i2c_master_bus_handle_t touch_get_i2c_bus(void)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_err.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
esp_err_t touch_init(lv_display_t *display);
|
||||
i2c_master_bus_handle_t touch_get_i2c_bus(void);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Project name: HeavenlyHeroStar
|
||||
|
||||
#include "../ui.h"
|
||||
#include "board_config.h"
|
||||
|
||||
lv_obj_t * uic_Calendar1;
|
||||
lv_obj_t * ui_Screen1 = NULL;
|
||||
@@ -15,12 +16,13 @@ lv_obj_t * ui_Calendar1 = NULL;
|
||||
void ui_Screen1_screen_init(void)
|
||||
{
|
||||
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
|
||||
|
||||
ui_Calendar1 = lv_calendar_create(ui_Screen1);
|
||||
lv_obj_t * ui_Calendar1_header = lv_calendar_header_arrow_create(ui_Calendar1);
|
||||
lv_obj_set_width(ui_Calendar1, 170);
|
||||
lv_obj_set_height(ui_Calendar1, 320);
|
||||
lv_obj_set_width(ui_Calendar1, BOARD_LCD_LOGIC_H_RES);
|
||||
lv_obj_set_height(ui_Calendar1, BOARD_LCD_LOGIC_V_RES);
|
||||
lv_obj_set_align(ui_Calendar1, LV_ALIGN_CENTER);
|
||||
|
||||
uic_Calendar1 = ui_Calendar1;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Project name: HeavenlyHeroStar
|
||||
|
||||
#include "../ui.h"
|
||||
#include "board_config.h"
|
||||
|
||||
lv_obj_t * ui_Screen2 = 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)
|
||||
{
|
||||
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
|
||||
|
||||
ui_Button1 = lv_button_create(ui_Screen2);
|
||||
lv_obj_set_width(ui_Button1, 100);
|
||||
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_add_flag(ui_Button1, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// 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_height(ui_Button2, 80);
|
||||
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_add_flag(ui_Button2, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// 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);
|
||||
lv_obj_set_width(ui_Button3, 100);
|
||||
lv_obj_set_height(ui_Button3, 80);
|
||||
lv_obj_set_x(ui_Button3, 0);
|
||||
lv_obj_set_y(ui_Button3, -100);
|
||||
lv_obj_set_x(ui_Button3, 100);
|
||||
lv_obj_set_y(ui_Button3, 0);
|
||||
lv_obj_set_align(ui_Button3, LV_ALIGN_CENTER);
|
||||
lv_obj_add_flag(ui_Button3, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// 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);
|
||||
lv_obj_set_width(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_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_height(ui_Label4, LV_SIZE_CONTENT); /// 1
|
||||
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_label_set_text(ui_Label4, "Ctrl+V");
|
||||
|
||||
ui_Label5 = lv_label_create(ui_Screen2);
|
||||
lv_obj_set_width(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_y(ui_Label5, -100);
|
||||
lv_obj_set_x(ui_Label5, 100);
|
||||
lv_obj_set_y(ui_Label5, 0);
|
||||
lv_obj_set_align(ui_Label5, LV_ALIGN_CENTER);
|
||||
lv_label_set_text(ui_Label5, "Ctrl+X");
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Project name: HeavenlyHeroStar
|
||||
|
||||
#include "../ui.h"
|
||||
#include "board_config.h"
|
||||
|
||||
lv_obj_t * ui_Screen3 = 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)
|
||||
{
|
||||
*btn = lv_button_create(ui_Screen3);
|
||||
lv_obj_set_width(*btn, 75);
|
||||
lv_obj_set_height(*btn, 90);
|
||||
lv_obj_set_width(*btn, 90);
|
||||
lv_obj_set_height(*btn, 45);
|
||||
lv_obj_set_x(*btn, x);
|
||||
lv_obj_set_y(*btn, y);
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
ui_screen3_create_button(&ui_BtnVolDown, &ui_LabelVolDown, -42, -107, "Vol-");
|
||||
ui_screen3_create_button(&ui_BtnVolUp, &ui_LabelVolUp, 42, -107, "Vol+");
|
||||
ui_screen3_create_button(&ui_BtnMute, &ui_LabelMute, -42, 0, "Mute");
|
||||
ui_screen3_create_button(&ui_BtnPlayPause, &ui_LabelPlayPause, 42, 0, "Play");
|
||||
ui_screen3_create_button(&ui_BtnPrev, &ui_LabelPrev, -42, 107, "Prev");
|
||||
ui_screen3_create_button(&ui_BtnNext, &ui_LabelNext, 42, 107, "Next");
|
||||
ui_screen3_create_button(&ui_BtnVolDown, &ui_LabelVolDown, -50, -55, "Vol-");
|
||||
ui_screen3_create_button(&ui_BtnVolUp, &ui_LabelVolUp, 50, -55, "Vol+");
|
||||
ui_screen3_create_button(&ui_BtnMute, &ui_LabelMute, -50, 0, "Mute");
|
||||
ui_screen3_create_button(&ui_BtnPlayPause, &ui_LabelPlayPause, 50, 0, "Play");
|
||||
ui_screen3_create_button(&ui_BtnPrev, &ui_LabelPrev, -50, 55, "Prev");
|
||||
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_BtnVolUp, ui_event_BtnVolUp, LV_EVENT_ALL, NULL);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "settings_store.h"
|
||||
|
||||
#include "display.h"
|
||||
#include "motion_gesture.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
@@ -15,6 +16,8 @@ static const settings_data_t SETTINGS_DEFAULTS = {
|
||||
.brightness_pct = 70,
|
||||
.volume_pct = 50,
|
||||
.mute = false,
|
||||
.shake_nav_enabled = true,
|
||||
.shake_sensitivity = SHAKE_SENS_MED,
|
||||
};
|
||||
|
||||
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, "mute", &val, SETTINGS_DEFAULTS.mute);
|
||||
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);
|
||||
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) {
|
||||
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) {
|
||||
err = nvs_commit(handle);
|
||||
}
|
||||
@@ -118,4 +131,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "ui_handlers.h"
|
||||
|
||||
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_pct_label;
|
||||
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)
|
||||
{
|
||||
s_notif_switch = NULL;
|
||||
s_shake_nav_switch = NULL;
|
||||
s_shake_sens_dropdown = NULL;
|
||||
|
||||
create_status_bar(content, "System Settings");
|
||||
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,
|
||||
(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_set_pos(reset_btn, 4, 112);
|
||||
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 =
|
||||
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;
|
||||
case SETTINGS_TAB_DISPLAY:
|
||||
if (s_brightness_slider != NULL) {
|
||||
|
||||
@@ -7,6 +7,8 @@ typedef enum {
|
||||
SETTINGS_FIELD_NOTIFICATION = 0,
|
||||
SETTINGS_FIELD_VOLUME,
|
||||
SETTINGS_FIELD_MUTE,
|
||||
SETTINGS_FIELD_SHAKE_NAV,
|
||||
SETTINGS_FIELD_SHAKE_SENS,
|
||||
} settings_field_t;
|
||||
|
||||
void load_system_settings(lv_obj_t *content, const settings_data_t *data);
|
||||
|
||||
@@ -10,9 +10,17 @@ typedef enum {
|
||||
SETTINGS_TAB_NETWORK,
|
||||
} settings_tab_t;
|
||||
|
||||
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;
|
||||
shake_sensitivity_t shake_sensitivity;
|
||||
} settings_data_t;
|
||||
|
||||
Reference in New Issue
Block a user