Compare commits

..

11 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
rsgltzyd 6a6ee41a07 feat: 实现设置页面及其功能 2026-07-05 20:52:04 +08:00
rsgltzyd 9a355e540c feat: 添加 Settings 横屏页面实现规格文档 2026-07-05 20:33:26 +08:00
rsgltzyd ab6fcefea0 fix: 更新消费者控制报告以支持播放/暂停功能
- 修改 HID 报告定义,将播放按钮的 ID 更新为播放/暂停
- 更新消费者报告映射,添加播放/暂停的使用情况
- 调整逻辑以确保播放/暂停功能正确映射到新的报告 ID
2026-07-02 22:30:06 +08:00
rsgltzyd 8254bc7d50 feat: 添加第三屏幕及其音量控制功能
- 在 CMakeLists.txt 中添加 ui_Screen3.c 源文件以支持新屏幕
- 新增 ui_events.c 和 ui_events.h,定义音量控制按钮的点击事件处理函数
- 实现 ui_Screen3.c,构建第三屏幕的 UI 组件和事件回调
- 更新 ui.c 以初始化和加载第三屏幕
- 修改 ble_hid.c 和 ble_hid.h,添加消费者控制功能以支持音量调节
- 更新相关头文件,确保新屏幕与现有结构兼容
2026-07-02 22:23:20 +08:00
rsgltzyd e056a113a2 refactor: 提升 HID 按键发送逻辑的可重用性
- 将发送 HID 按键的逻辑提取到 send_hid_combo_or_warn 函数中
- 更新 on_boot_press 函数以使用新的发送逻辑
- 在按钮点击事件处理函数中调用 send_hid_combo_or_warn,简化代码并增强可读性
2026-07-02 08:57:55 +08:00
35 changed files with 3508 additions and 67 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.)
+765
View File
@@ -0,0 +1,765 @@
# Settings 横屏页面实现规格
> 项目:HeavenlyHeroStar
> 目标芯片:ESP32-S3
> ESP-IDF6.0.1
> LVGL9.2.2
> 逻辑分辨率:320×170(全局横屏)
> 状态:待实现(v1.2 修订稿)
## 1. 背景与目标
### 1.1 现状
当前 UI 层由 SquareLine Studio 生成,尚无 Settings 页面:
| 模块 | 路径 | 说明 |
|------|------|------|
| 物理分辨率 | `main/bsp/board_config.h` | `BOARD_LCD_H_RES=170`, `BOARD_LCD_V_RES=320`(竖屏面板) |
| LVGL Display | `main/bsp/lvgl_port.c` | 以 170×320 创建 display,无旋转 |
| 触摸 | `main/bsp/touch.c` | CST816S`swap_xy/mirror_x/mirror_y` 均为 0 |
| 背光 | `main/bsp/display.c` | `display_backlight_set_brightness(01023)` 已实现 |
| UI 屏幕 | `main/ui/screens/ui_Screen{1,2,3}.c` | Screen1 日历 170×320Screen3 为默认启动页 |
| 事件处理 | `main/app/ui_handlers.c` | HID 相关回调,与 SquareLine 生成的 `ui_events.c` 分离 |
| NVS | `main/app/app_main.c` | 已初始化 `nvs_flash`Settings 尚未使用 |
| WiFi | `main/services/wifi_manager.c` | `BOARD_WIFI_ENABLED=0`Network Tab 首版为占位 |
### 1.2 目标
实现 **Settings 设置页**,采用 **Master-Detail(主从)** 布局:
- 左侧 Sidebar90px):System / Display / Audio / Network 导航 + Save 按钮
- 右侧 Content230px):随 Tab 切换动态刷新的设置项
- **全局横屏**:启动后 LVGL 逻辑分辨率为 **320×170**,所有页面按横屏坐标设计
首版(MVP)交付:
- BSP 横屏基础设施(display 旋转 + touch 校准)
- Settings 屏幕骨架与 Tab 切换机制
- System Tab 完整功能(恢复出厂)
- Display Tab 亮度滑块(实时预览 + NVS 持久化)
- Audio / Network Tab 占位 UI + NVS 存根
- Save 按钮写入 NVSFactory Reset 恢复默认并重启
### 1.3 方案选型结论
| 方案 | 说明 | 结论 |
|------|------|------|
| A | SquareLine Studio 导出静态 Settings 屏 | 不利于 Tab 动态刷新,且易被重新导出覆盖 |
| **B** | **手写 C + `lv_obj_clean` 动态加载** | **采用**:模块化、可扩展、与 SquareLine 解耦 |
| C | 每个 Tab 独立 Screenlv_scr_load 切换 | 内存占用高,Sidebar 需重复创建 |
| 横屏-硬件 | 修改 ST7789 scan direction | 不采用:需改 panel 驱动,touch 仍须软件映射 |
| **横屏-软件** | **`lv_display_set_rotation(90°)`** | **采用**:改动集中在 BSP,UI 直接用 320×170 坐标 |
---
## 2. 页面布局
### 2.1 整体结构(320×170
**System Tab 选中时:**
```text
+---------------------------------------------------------------+
| Sidebar 90px | Content 230px |
| [System] * | System Settings --:-- |
| [Display] | ----------------------------------- |
| [Audio] | Enable Notification [switch OFF] |
| [Network] | |
| [Save] | [Factory Reset] |
+---------------------------------------------------------------+
|<-- 90px -->|<-------------------- 230px -------------------->|
```
**Display Tab 选中时(Content 区域示意):**
```text
| Display Settings --:-- |
| ----------------------------------- |
| Brightness: [=======> ] 70% |
```
### 2.2 区域参数表
| 区域名称 | 宽度 | 高度 | 位置 (x, y) | LVGL 控件 | 说明 |
|----------|------|------|-------------|-----------|------|
| Screen 根 | 320 | 170 | (0, 0) | `lv_obj_create(NULL)` | 禁用滚动 |
| Sidebar | 90 | 170 | (0, 0) | `lv_list` | 导航容器;显式设 `pad_all=2``item_gap=2` |
| Sidebar 导航按钮 ×4 | 86 | 28 | 相对 listy 递增 | `lv_list_add_button` | System/Display/Audio/Network |
| Save 按钮 | 86 | 28 | (2, 138) | `lv_button` | 固定底部,不在 list 内滚动 |
| Content 容器 | 230 | 170 | (90, 0) | `lv_obj` | 动态刷新区域 |
| StatusBar | 230 | 25 | (0, 0) 相对 content | `lv_obj` + `lv_label` ×2 | 标题左、时间右 |
| SettingsBody | 230 | 145 | (0, 25) 相对 content | switch/slider/button | 各 Tab 内容 |
### 2.3 紧凑布局约束
170px 高度下必须严格控制间距,避免纵向滚动:
| 约束项 | 值 |
|--------|-----|
| Screen / Sidebar / Content `pad_all` | ≤ 2px |
| 导航按钮间距 | 2px |
| SettingsBody 行高 | 3236px |
| 字体 | Phase 1 使用 `LV_FONT_DEFAULT`Phase 3 可换 12px 定制字体 |
| 滚动 | Screen、Sidebar、Content 均移除 `LV_OBJ_FLAG_SCROLLABLE` |
### 2.4 System Tab 控件坐标(相对 SettingsBody
| 控件 | x | y | 宽 | 高 | 类型 |
|------|---|---|----|----|------|
| Label "Enable Notification" | 4 | 4 | 120 | 28 | `lv_label` |
| Switch 通知 | 180 | 4 | 40 | 28 | `lv_switch` |
| Button "Factory Reset" | 4 | 112 | 100 | 28 | `lv_button` |
### 2.5 Display Tab 控件坐标(相对 SettingsBody
| 控件 | x | y | 宽 | 高 | 类型 |
|------|---|---|----|----|------|
| Label "Brightness" | 4 | 4 | 80 | 28 | `lv_label` |
| Slider 亮度 | 88 | 8 | 100 | 12 | `lv_slider` |
| Label "70%" | 196 | 4 | 30 | 28 | `lv_label` |
> Audio / Network Tab 坐标见 §5.2;布局模式一致(label 左 80px + 控件区 140px)。
---
## 3. 系统架构
### 3.1 模块划分
```text
main/bsp/
├── board_config.h # 新增 BOARD_LCD_LOGIC_H_RES/V_RES
├── lvgl_port.c # rotation + flush_cb 软件旋转(真机验证)
├── touch.c # 物理面板 touch 对齐(非 LVGL 旋转)
└── display.c # 背光 API(已有)
main/ui/
├── screens/
│ └── ui_Settings.c/.h # 屏幕骨架:sidebar + content + Save
├── settings/
│ ├── settings_types.h # settings_data_t、Tab 枚举
│ ├── settings_store.c/.h # NVS 读写、默认值、commit/reset
│ └── settings_tabs.c/.h # load_xxx_settings() 动态构建控件
├── ui.c / ui.h # 注册 Settings 屏幕
└── ui_events.c # SquareLine 生成,不修改
main/app/
└── ui_handlers.c # Settings 事件回调(Save/Tab/Slider 等)
```
### 3.2 架构图
```mermaid
flowchart TB
subgraph bsp [BSP Layer]
Display["display.c"]
LVGLPort["lvgl_port.c"]
Touch["touch.c"]
end
subgraph ui [UI Layer]
SettingsScreen["ui_Settings.c"]
SettingsTabs["settings_tabs.c"]
SettingsStore["settings_store.c"]
end
subgraph app [App Layer]
UIHandlers["ui_handlers.c"]
end
Display --> LVGLPort
LVGLPort --> SettingsScreen
Touch --> LVGLPort
SettingsScreen --> SettingsTabs
SettingsScreen --> SettingsStore
SettingsTabs --> UIHandlers
UIHandlers --> SettingsStore
SettingsStore --> NVS["nvs_flash"]
SettingsStore --> Display
```
### 3.3 Tab 切换数据流
```mermaid
sequenceDiagram
participant User as 用户
participant Sidebar as Sidebar
participant Content as content_obj
participant Tabs as settings_tabs
participant Store as settings_store
User->>Sidebar: 点击 Display(当前在 System
Sidebar->>Tabs: settings_tabs_collect(SYSTEM, buf)
Tabs->>Store: settings_store_update(buf)
Sidebar->>Sidebar: 更新选中高亮
Sidebar->>Content: lv_obj_clean(content_obj)
Sidebar->>Tabs: load_display_settings(content, store)
Tabs->>Content: 创建 slider/label
Tabs->>Store: 读取 brightness_pct 填充控件
User->>Sidebar: 点击 Save
Sidebar->>Tabs: settings_tabs_collect(active_tab, buf)
Tabs->>Store: settings_store_update(buf)
Store->>Store: settings_store_commit()
Store->>Store: settings_store_apply()
```
**Tab 切换前必须先 collect 当前 Tab 到内存**,否则未 Save 的控件改动会在 `lv_obj_clean` 后丢失。Save 仅 collect **当前 active Tab**,但 `settings_store_commit()` 持久化 **内存 struct 全部字段**(含其它 Tab 切换前已 collect 的值)。
---
## 4. 全局横屏基础设施(BSP)
### 4.1 逻辑分辨率宏
`main/bsp/board_config.h` 新增:
```c
/* Physical panel pixels (portrait wiring) */
#define BOARD_LCD_H_RES 170
#define BOARD_LCD_V_RES 320
/* LVGL logical resolution after 90° rotation (landscape) */
#define BOARD_LCD_LOGIC_H_RES 320
#define BOARD_LCD_LOGIC_V_RES 170
```
UI 代码 **统一使用** `BOARD_LCD_LOGIC_H_RES` / `BOARD_LCD_LOGIC_V_RES` 作为屏幕尺寸。
### 4.2 Display 旋转与 flush_cb
`main/bsp/lvgl_port.c``lvgl_port_init()` 中,`lv_display_create` 之后添加:
```c
lv_display_t *display =
lv_display_create(BOARD_LCD_H_RES, BOARD_LCD_V_RES);
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
/* 逻辑分辨率:320×170 */
```
说明(依据 [LVGL 9.2 Display porting — Rotation](https://docs.lvgl.io/9.2/porting/display.html)):
- `lv_display_set_rotation()` **交换逻辑分辨率**320×170),并变换 **触摸坐标****不会自动旋转像素**
- 本项目无 ST7789 硬件 scan rotation,须走 **软件旋转** 路径
- 当前为 `LV_DISPLAY_RENDER_MODE_PARTIAL`:LVGL 可能在内部按块旋转后再调用 `flush_cb`,**但必须在真机验证**;若画面仍呈竖屏或区域错位,在 `flush_cb` 中补充:
```c
lv_display_rotate_area(disp, (lv_area_t *)&rotated_area);
lv_draw_sw_rotate(px_map, rotated_buf, src_w, src_h, src_stride, dst_stride,
lv_display_get_rotation(disp), cf);
/* 再将 rotated_buf / rotated_area 传给 esp_lcd_panel_draw_bitmap */
```
- 绘制 buffer 大小仍基于物理 `BOARD_LCD_H_RES × BOARD_LVGL_DRAW_BUF_LINES`;逻辑宽变为 320 后 partial 分块与软件旋转 **CPU 开销上升**,需关注刷新流畅度(见 §11
**Phase 1 步骤 1.2 验收:** 加载 Settings 页后,Sidebar 在左、Content 在右,文字可读,无镜像/错位。
### 4.3 Panel Gap 与 flush 坐标
物理面板为 240×320,有效显示区 170×320 居中,`display.c` 已通过 `esp_lcd_panel_set_gap(BOARD_LCD_GAP_X, 0)` 设置 **35px 水平偏移**`BOARD_LCD_GAP_X = (240170)/2`)。
旋转 90° 后 gap 方向可能变化;若 `flush_cb` 需手动 `lv_draw_sw_rotate`,须同步验证:
1. 画面是否整体偏移或裁切
2. `lv_display_rotate_area` 输出坐标加上 gap offset 是否正确
3. 四角触控是否与像素对齐
**Phase 1 步骤 1.2/1.3 一并验收**,必要时在 flush 路径对 area 做 gap 补偿。
### 4.4 Touch 坐标映射
**原则:触摸驱动只做「物理竖屏面板」对齐;LVGL 负责 rotation 后的逻辑坐标变换。**
- `esp_lcd_touch_config_t``x_max` / `y_max` **保持** 物理尺寸 170×320,不随逻辑横屏改变
- `lv_indev_set_display(indev, display)` 关联 display 后,LVGL 9 会根据 `lv_display_set_rotation()` **在内部变换触摸点**
- 因此 `swap_xy` / `mirror_x` / `mirror_y` **仅用于** 触摸 IC 原始坐标 → 物理面板(170×320、原点左上角)的对齐,**不得** 为了 90° 逻辑横屏在驱动层做 rotation 补偿,否则 **双重变换**
**初始值(与当前 `touch.c` 一致,待真机验证):**
```c
.flags = {
.swap_xy = 0,
.mirror_x = 0,
.mirror_y = 0,
},
```
**校准步骤:**
1. **先**`lv_display_set_rotation(90°)`**保持** touch flags 全 0,加载 Settings 页
2. 在四个逻辑角点击,观察触控是否落在对应 UI 区域
3. 若整体镜像或轴互换,**仅调整物理对齐 flags**swap × mirror 共 8 种),每次改一项并记录
4. 确认 Sidebar 左侧按钮可准确触控后,写入 `board_config.h`
```c
#define BOARD_TOUCH_SWAP_XY 0 /* 真机确认值 */
#define BOARD_TOUCH_MIRROR_X 0
#define BOARD_TOUCH_MIRROR_Y 0
```
> 注释须标明:**物理面板对齐,非 LVGL 逻辑旋转**。
### 4.5 现有 Screen 兼容
| 屏幕 | 现状 | 横屏影响 | 处理阶段 |
|------|------|----------|----------|
| Screen1 日历 | 170×320 居中 | 布局错位 | Phase 3 重排或替换 |
| Screen2 | SquareLine 生成 | 需验证 | Phase 3 |
| Screen3 HID 控制 | 绝对坐标按钮 | 需重排 | Phase 3 |
| **Settings** | 新建 | 按 320×170 设计 | **Phase 1** |
Phase 1 建议将 `ui.c``lv_disp_load_scr()` 默认屏改为 `ui_Settings`,便于验收横屏与 Settings 功能。
---
## 5. Settings UI 规格
### 5.1 屏幕骨架 API
**`main/ui/screens/ui_Settings.h`**
```c
#pragma once
#include "lvgl.h"
#include "settings_types.h"
extern lv_obj_t *ui_Settings;
extern lv_obj_t *ui_Settings_sidebar;
extern lv_obj_t *ui_Settings_content;
void ui_Settings_screen_init(void);
void ui_Settings_screen_destroy(void);
/* 切换 Tab 时由 ui_handlers 调用 */
void ui_Settings_switch_tab(settings_tab_t tab);
settings_tab_t ui_Settings_get_active_tab(void);
```
**初始化伪代码:**
```c
void ui_Settings_screen_init(void)
{
ui_Settings = lv_obj_create(NULL);
lv_obj_set_size(ui_Settings, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_remove_flag(ui_Settings, LV_OBJ_FLAG_SCROLLABLE);
ui_Settings_sidebar = lv_list_create(ui_Settings);
lv_obj_set_size(ui_Settings_sidebar, 90, 170);
lv_obj_set_pos(ui_Settings_sidebar, 0, 0);
lv_obj_remove_flag(ui_Settings_sidebar, LV_OBJ_FLAG_SCROLLABLE);
/* 4 个导航 button + 事件 */
/* Save button 固定底部 */
ui_Settings_content = lv_obj_create(ui_Settings);
lv_obj_set_size(ui_Settings_content, 230, 170);
lv_obj_set_pos(ui_Settings_content, 90, 0);
lv_obj_remove_flag(ui_Settings_content, LV_OBJ_FLAG_SCROLLABLE);
settings_store_load();
settings_store_apply(); /* 启动时恢复 NVS 背光等到硬件 */
load_system_settings(ui_Settings_content, settings_store_get());
}
```
### 5.2 各 Tab MVP 内容
#### System Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|------|-----------|----------|------|
| Enable Notification | `lv_switch` | `notification_enabled` | `VALUE_CHANGED` 时更新内存;Tab 切换前 collectSave 时 commit |
| Factory Reset | `lv_button` | — | 确认后 `settings_store_factory_reset()` + `esp_restart()` |
StatusBar 标题:`"System Settings"`
#### Display Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|------|-----------|----------|------|
| Brightness | `lv_slider` (0100) + `%` label | `brightness_pct` | `VALUE_CHANGED` 时:`settings_store` 更新内存 + `settings_store_apply()` 实时调背光;Save 时 commit |
StatusBar 标题:`"Display Settings"`
#### Audio Tab
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|------|-----------|----------|------|
| Volume | `lv_slider` (0100) | `volume_pct` | `VALUE_CHANGED` 更新内存;Phase 1 无 codec 输出 |
| Mute | `lv_switch` | `mute` | `VALUE_CHANGED` 更新内存;Phase 1 无 codec 输出 |
StatusBar 标题:`"Audio Settings"`
#### Network Tab
| 控件 | LVGL 类型 | 行为 |
|------|-----------|------|
| WiFi Status | `lv_label` | 显示 `"WiFi: Disabled"``BOARD_WIFI_ENABLED=0` |
| Scan | `lv_button` | Phase 1 点击显示 `"Not available"` 日志;Phase 4 接 `wifi_manager` |
StatusBar 标题:`"Network Settings"`
### 5.3 Tab 加载函数
**`main/ui/settings/settings_tabs.h`**
```c
#pragma once
#include "lvgl.h"
#include "settings_types.h"
void load_system_settings(lv_obj_t *content, const settings_data_t *data);
void load_display_settings(lv_obj_t *content, const settings_data_t *data);
void load_audio_settings(lv_obj_t *content, const settings_data_t *data);
void load_network_settings(lv_obj_t *content, const settings_data_t *data);
/* Save / Tab 切换前:从指定 Tab 控件回读到 outout 应先拷贝 settings_store_get() */
void settings_tabs_collect(settings_tab_t tab, settings_data_t *out);
```
每次切换 Tab**先 collect 再 clean**):
```c
void ui_Settings_switch_tab(settings_tab_t tab)
{
settings_data_t buf = *settings_store_get();
settings_tabs_collect(s_active_tab, &buf);
settings_store_update(&buf);
s_active_tab = tab;
sidebar_update_highlight(tab);
lv_obj_clean(ui_Settings_content);
switch (tab) {
case SETTINGS_TAB_SYSTEM:
load_system_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_DISPLAY:
load_display_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_AUDIO:
load_audio_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_NETWORK:
load_network_settings(ui_Settings_content, settings_store_get());
break;
default:
LV_ASSERT_MSG(false, "unexpected settings_tab_t");
break;
}
}
```
### 5.4 样式规范
`ui_Settings.c` 内使用 static `lv_style_t`(或 `settings_style.c`):
| 元素 | 属性 | 值 |
|------|------|-----|
| Sidebar 按钮(默认) | 宽×高 | 86×28 |
| Sidebar 按钮(选中) | 背景色 | `lv_palette_main(LV_PALETTE_BLUE)` |
| Content 背景 | 背景色 | `lv_palette_lighten(LV_PALETTE_GREY, 2)` |
| StatusBar 分隔线 | 高度 1px | bottom border 或 `lv_obj` 线 |
| Slider | 宽 | 100120px |
| Factory Reset 按钮 | 文字色 | `lv_palette_main(LV_PALETTE_RED)` |
**Style 生命周期:** 使用文件级 static style,在 `ui_Settings_screen_init()` 初始化一次;`lv_obj_clean(content)` 仅删除子对象,不删除 static style,避免泄漏。
### 5.5 StatusBar 时间
| 阶段 | 行为 |
|------|------|
| Phase 1 | 静态占位 `"--:--"` |
| Phase 4 | SNTP 同步后 `lv_label_set_text_fmt()` 每分钟更新 |
---
## 6. 数据模型与 NVS
### 6.1 类型定义
**`main/ui/settings/settings_types.h`**
```c
#pragma once
#include <stdint.h>
#include <stdbool.h>
typedef enum {
SETTINGS_TAB_SYSTEM = 0,
SETTINGS_TAB_DISPLAY,
SETTINGS_TAB_AUDIO,
SETTINGS_TAB_NETWORK,
} settings_tab_t;
typedef struct {
bool notification_enabled;
uint8_t brightness_pct; /* 0100, default 70 */
uint8_t volume_pct; /* 0100, default 50 */
bool mute;
} settings_data_t;
```
### 6.2 默认值
| 字段 | 默认值 | 说明 |
|------|--------|------|
| `notification_enabled` | `false` | 与 mockup 一致,默认关闭 |
| `brightness_pct` | `70` | 与 mockup 一致 |
| `volume_pct` | `50` | 居中 |
| `mute` | `false` | — |
```c
static const settings_data_t SETTINGS_DEFAULTS = {
.notification_enabled = false,
.brightness_pct = 70,
.volume_pct = 50,
.mute = false,
};
```
### 6.3 NVS Schema
| 属性 | 值 |
|------|-----|
| Namespace | `"settings"` |
| 存储格式 | 各 key 独立 u8/blob,便于单项迁移 |
| Key | 类型 | 字段 | 说明 |
|-----|------|------|------|
| `"notif"` | `uint8_t` | `notification_enabled` | 0/1 |
| `"bright"` | `uint8_t` | `brightness_pct` | 0100 |
| `"vol"` | `uint8_t` | `volume_pct` | 0100 |
| `"mute"` | `uint8_t` | `mute` | 0/1 |
### 6.4 Store API
**`main/ui/settings/settings_store.h`**
```c
#pragma once
#include "settings_types.h"
void settings_store_load(void);
const settings_data_t *settings_store_get(void);
void settings_store_update(const settings_data_t *data);
esp_err_t settings_store_commit(void);
esp_err_t settings_store_factory_reset(void);
void settings_store_apply(void); /* 将内存值应用到硬件(背光等) */
```
| 函数 | 说明 |
|------|------|
| `settings_store_load` | 启动时从 NVS 读取;key 不存在则用 `SETTINGS_DEFAULTS` |
| `settings_store_get` | 返回内存中当前 struct 指针(只读) |
| `settings_store_update` | 用 collect 结果更新内存 structcollect 前应先 `*out = *settings_store_get()` |
| `settings_store_commit` | 将全部字段写入 NVS |
| `settings_store_factory_reset` | 擦除 `"settings"` namespace,恢复默认值,commit |
| `settings_store_apply` | 调用 `display_backlight_set_brightness()` 等即时生效项 |
### 6.5 Save 流程
```mermaid
sequenceDiagram
participant User
participant SaveBtn
participant Tabs as settings_tabs
participant Store as settings_store
participant NVS
participant HW as display.c
User->>SaveBtn: 点击 Save
SaveBtn->>Tabs: settings_tabs_collect(active_tab, buf)
Tabs->>Store: settings_store_update(buf)
Store->>NVS: settings_store_commit()
Store->>HW: settings_store_apply()
SaveBtn->>User: 可选:短暂 toast "Saved"
```
**注意:**
- Display Tab 亮度在 `VALUE_CHANGED` 时已通过 `settings_store_apply()` 实时调背光并更新内存;Save 负责写入 NVS
- 跨 Tab 修改:切换 Tab 时 collect 当前 Tab 到内存;最终 Save 一次 commit 全部字段
- 背光等硬件调用统一经 `settings_store_apply()`**不** 在 `settings_tabs.c` 直接调用 `display.c`(见 §7.1
### 6.6 Factory Reset 流程
1. 用户点击 Factory Reset 按钮
2. Phase 1 无确认对话框,直接执行(Phase 2 可选加 `lv_msgbox` 确认,见 §8
3. 调用 `settings_store_factory_reset()`:擦除 NVS namespace,恢复 `SETTINGS_DEFAULTS`
4. 调用 `settings_store_apply()` 恢复背光
5. `esp_restart()`
---
## 7. 事件与集成
### 7.1 事件归属
| 事件 | 注册位置 | 处理函数(`ui_handlers.c` |
|------|----------|----------------------------|
| Sidebar Tab 点击 | `ui_Settings.c` init | `settings_on_tab_clicked()` |
| Save 点击 | `ui_Settings.c` init | `settings_on_save_clicked()` |
| Factory Reset | `settings_tabs.c` 创建时 | `settings_on_factory_reset()` |
| Brightness slider | `settings_tabs.c` 创建时 | `settings_on_brightness_changed()` → 更新 store + `settings_store_apply()` |
| 其他 slider/switch | 各 load 函数 | `settings_on_value_changed()` → 更新 store 内存 |
**原则:** Settings 相关回调一律放在 `main/app/ui_handlers.c`**不修改** SquareLine 生成的 `ui_events.c`。硬件侧效应(背光)经 `settings_store_apply()`UI 层不直接 `#include "display.h"`
### 7.2 CMakeLists.txt 变更
`main/CMakeLists.txt``SRCS` 中添加:
```text
"ui/screens/ui_Settings.c"
"ui/settings/settings_store.c"
"ui/settings/settings_tabs.c"
```
### 7.3 ui.h / ui.c 变更
```c
/* ui.h */
#include "screens/ui_Settings.h"
/* ui_init() */
ui_Settings_screen_init();
/* Phase 1 默认屏(LVGL 9 亦可写 lv_screen_load(ui_Settings) */
lv_disp_load_scr(ui_Settings);
/* ui_destroy() */
ui_Settings_screen_destroy();
```
### 7.4 初始化顺序
`app_main.c` 中保持现有顺序,Settings 无额外要求:
```text
nvs_flash_init → display_init → lvgl_port_init → touch_init → ui_init → ui_handlers_init
```
`settings_store_load()``settings_store_apply()``ui_Settings_screen_init()` 内调用(NVS 已就绪),确保重启后背光等与 NVS 一致。
---
## 8. 实现步骤
### 阶段 0:规格审核
| 步骤 | 内容 | 状态 |
|------|------|------|
| 0.1 | 审核本文档 v1.2 | 完成 |
| 0.2 | 确认横屏全局策略与 Tab MVP 范围 | 待确认 |
### 阶段 1BSP 横屏 + Settings 全量 MVP
| 步骤 | 内容 |
|------|------|
| 1.1 | `board_config.h` 增加逻辑分辨率宏、touch 对齐宏 |
| 1.2 | `lvgl_port.c``lv_display_set_rotation(90°)`;真机验证 flush,必要时 `lv_draw_sw_rotate` + gap 补偿 |
| 1.3 | `touch.c`:物理面板对齐 flags 真机验证(勿与 LVGL rotation 双重变换) |
| 1.4 | 新建 `settings_types.h``settings_store.c`(含 `apply` |
| 1.5 | 新建 `ui_Settings.c`Sidebar + Content + Save |
| 1.6 | 新建 `settings_tabs.c`:四个 `load_xxx_settings()` + `settings_tabs_collect()` |
| 1.7 | `ui_handlers.c`Tab 切换(先 collect)、Save、Factory Reset、slider/switch 回调 |
| 1.8 | 更新 `CMakeLists.txt``ui.c``ui.h` |
| 1.9 | 默认加载 Settings 页,真机验收 §10 全部 AC |
**Phase 1 验收:** 与 §1.2 MVP 及 §10 验收标准 AC-1AC-8 一致(含四个 Tab、Display 亮度、跨 Tab Save、Factory Reset)。
### 阶段 2:体验增强(可选,非 MVP 阻塞)
| 步骤 | 内容 |
|------|------|
| 2.1 | Factory Reset `lv_msgbox` 确认对话框 |
| 2.2 | Save 成功 toast / 视觉反馈 |
| 2.3 | Sidebar / Tab 切换过渡动画 |
### 阶段 3:现有 Screen 横屏适配
| 步骤 | 内容 |
|------|------|
| 3.1 | Screen3 HID 控制页按 320×170 重排 |
| 3.2 | Screen1/2 适配或废弃 |
| 3.3 | 多 Screen 导航入口(如 Settings 返回主屏) |
### 阶段 4:增强功能(后续)
| 步骤 | 内容 |
|------|------|
| 4.1 | SNTP 时间显示 |
| 4.2 | 通知功能真实实现 |
| 4.3 | WiFi 扫描/连接 UI`BOARD_WIFI_ENABLED=1` |
| 4.4 | Audio codec 对接 |
| 4.5 | Factory Reset 确认对话框(若未在 Phase 2 完成) |
---
## 9. 与现有代码对应关系
| 现有 | Settings 实施后 |
|------|-----------------|
| `lv_display_create(170, 320)` | 不变物理尺寸 + `set_rotation(90°)`flush 可能需软件旋转 |
| `ui_Screen3` 默认启动 | Phase 1 改为 `ui_Settings`Phase 3 增加导航 |
| `ui_events.c` SquareLine 回调 | 不修改;Settings 事件放 `ui_handlers.c` |
| `display_backlight_set_brightness()` | 经 `settings_store_apply()` 调用,不在 UI Tab 层直接调用 |
| `nvs_flash` 已初始化 | Settings 使用独立 namespace `"settings"` |
| `wifi_manager.c` | Network Tab Phase 4 对接 |
| SquareLine Screen1/2/3 | Phase 3 适配,Settings 不纳入 SquareLine |
---
## 10. 验收标准
| 编号 | 验收项 | 标准 |
|------|--------|------|
| AC-1 | 逻辑分辨率 | LVGL 屏幕 320×170Sidebar 90px + Content 230px |
| AC-2 | 触摸 | 四角及 Sidebar 按钮触控准确,无误触 |
| AC-3 | Tab 切换 | 4 个 Tab 可切换,右侧内容正确刷新,无滚动条 |
| AC-4 | System Tab | Switch/Factory Reset 可交互 |
| AC-5 | Display Tab | 亮度 slider 实时改变背光 |
| AC-6 | Save | 跨 Tab 修改后 Save,重启后全部设置值从 NVS 恢复 |
| AC-7 | Factory Reset | 恢复默认值、背光恢复 70%、设备重启 |
| AC-8 | 内存 | Tab 反复切换 50 次无 crash、无显著内存增长 |
---
## 11. 风险与约束
1. **flush 软件旋转**`lv_display_set_rotation` 不保证自动旋转像素;须真机验证,必要时改 `flush_cb`(§4.2)。
2. **触摸校准**:区分物理对齐与 LVGL rotation;错误地在 touch 驱动层做 rotation 补偿会导致双重变换(§4.4)。
3. **Panel gap**:240 宽面板 170 有效区居中,旋转后须验证 offset(§4.3)。
4. **Partial 模式性能**:逻辑宽 320 + 软件旋转增加 CPU 负担,若卡顿可考虑增大 `BOARD_LVGL_DRAW_BUF_LINES` 或后续评估 DIRECT 模式。
5. **SquareLine 冲突**Settings 手写 C,不纳入 SquareLine 工程;重新导出 UI 时不应覆盖 `ui_Settings.c``ui_handlers.c` 中 Settings 部分。
6. **WiFi 默认关闭**Network Tab Phase 1 仅为占位,避免用户误解为已连网。
7. **170px 高度**:控件过多时需合并 Tab 或减小行高,禁止启用纵向滚动。
8. **Style 生命周期**Tab 切换使用 `lv_obj_clean` 而非 `lv_obj_del` 整个 contentstatic style 不可绑定到会被删除的对象上。
9. **跨 Tab 数据**Tab 切换前必须 collectslider/switch 的 `VALUE_CHANGED` 应同步内存,避免 UI 与硬件不一致。
10. **Factory Reset 范围**Phase 1 仅擦除 `"settings"` namespaceBLE bonding、快捷键等其它 NVS namespace(见 `ble-hid-keyboard.md` 规划)保留。
11. **Screen 兼容**Phase 1 默认 Settings 页会导致 Screen1/2/3 暂不可直接验收,属预期行为。
---
## 12. 参考
- 板级配置:`main/bsp/board_config.h`
- LVGL Port`main/bsp/lvgl_port.c`
- 背光 API`main/bsp/display.h`
- 现有 UI 入口:`main/ui/ui.c`
- 事件处理范例:`main/app/ui_handlers.c`
- 姊妹规格:`docs/spec/ble-hid-keyboard.md`
- LVGL 9.2 Display Rotation[porting/display.html — Rotation](https://docs.lvgl.io/9.2/porting/display.html)
- LVGL API`lv_display_set_rotation()``lv_display_rotate_area()``lv_draw_sw_rotate()`
---
## 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0 | 2026-07-05 | 初版:Settings 横屏 320×170 布局、BSP、NVS、分阶段实现规格 |
| 1.1 | 2026-07-05 | 移除语言选择(dropdown / NVS lang / settings_language_t |
| 1.2 | 2026-07-05 | 技术修订:统一 Tab 线框与 MVP 范围;修正 LVGL 9 flush/touch 旋转说明;Tab 切换先 collect;启动 apply 背光;补 gap/分层/跨 Tab Save 约束 |
+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 |
+10 -1
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"
@@ -13,6 +16,10 @@ idf_component_register(SRCS
"ui/ui_helpers.c" "ui/ui_helpers.c"
"ui/screens/ui_Screen1.c" "ui/screens/ui_Screen1.c"
"ui/screens/ui_Screen2.c" "ui/screens/ui_Screen2.c"
"ui/screens/ui_Screen3.c"
"ui/screens/ui_Settings.c"
"ui/settings/settings_store.c"
"ui/settings/settings_tabs.c"
"ui/widgets/ui_comp_hook.c" "ui/widgets/ui_comp_hook.c"
PRIV_REQUIRES PRIV_REQUIRES
bt bt
@@ -29,4 +36,6 @@ idf_component_register(SRCS
"app" "app"
"bsp" "bsp"
"services" "services"
"ui") "ui"
"ui/settings"
)
+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);
+143 -2
View File
@@ -1,28 +1,130 @@
#include "ui_handlers.h" #include "ui_handlers.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h>
#include "ble_hid.h" #include "ble_hid.h"
#include "boot_button.h" #include "boot_button.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_system.h"
#include "screens/ui_Settings.h"
#include "settings/settings_store.h"
#include "settings/settings_tabs.h"
#include "ui_events.h" #include "ui_events.h"
static const char *TAG = "ui_handlers"; static const char *TAG = "ui_handlers";
static void on_boot_press(void) static void send_hid_combo_or_warn(uint8_t modifier, uint8_t keycode)
{ {
if (ble_hid_ready()) { if (ble_hid_ready()) {
ble_hid_send_combo(0, HID_KEY_C); ble_hid_send_combo(modifier, keycode);
} else { } else {
ESP_LOGW(TAG, "HID not ready"); ESP_LOGW(TAG, "HID not ready");
} }
} }
static void send_consumer_or_warn(uint16_t usage)
{
if (ble_hid_ready()) {
ble_hid_send_consumer(usage);
} else {
ESP_LOGW(TAG, "HID not ready");
}
}
static void on_boot_press(void)
{
send_hid_combo_or_warn(0, HID_KEY_C);
}
void ui_handlers_init(void) void ui_handlers_init(void)
{ {
boot_button_init(on_boot_press); boot_button_init(on_boot_press);
} }
void settings_on_tab_clicked(lv_event_t *e)
{
settings_tab_t tab = (settings_tab_t)(intptr_t)lv_event_get_user_data(e);
ui_Settings_switch_tab(tab);
}
void settings_on_save_clicked(lv_event_t *e)
{
(void)e;
settings_data_t buf = *settings_store_get();
settings_tabs_collect(ui_Settings_get_active_tab(), &buf);
settings_store_update(&buf);
esp_err_t err = settings_store_commit();
if (err == ESP_OK) {
settings_store_apply();
ESP_LOGI(TAG, "Settings saved");
} else {
ESP_LOGW(TAG, "Settings save failed");
}
}
void settings_on_factory_reset(lv_event_t *e)
{
(void)e;
ESP_LOGW(TAG, "Factory reset");
settings_store_factory_reset();
settings_store_apply();
esp_restart();
}
void settings_on_brightness_changed(lv_event_t *e)
{
lv_obj_t *slider = lv_event_get_target(e);
uint8_t pct = (uint8_t)lv_slider_get_value(slider);
settings_data_t buf = *settings_store_get();
buf.brightness_pct = pct;
settings_store_update(&buf);
settings_store_apply();
lv_obj_t *pct_label = lv_obj_get_user_data(slider);
if (pct_label != NULL) {
lv_label_set_text_fmt(pct_label, "%u%%", pct);
}
}
void settings_on_value_changed(lv_event_t *e)
{
settings_field_t field =
(settings_field_t)(intptr_t)lv_event_get_user_data(e);
lv_obj_t *target = lv_event_get_target(e);
settings_data_t buf = *settings_store_get();
switch (field) {
case SETTINGS_FIELD_NOTIFICATION:
buf.notification_enabled = lv_obj_has_state(target, LV_STATE_CHECKED);
break;
case SETTINGS_FIELD_VOLUME:
buf.volume_pct = (uint8_t)lv_slider_get_value(target);
break;
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;
}
settings_store_update(&buf);
}
void settings_on_network_scan_clicked(lv_event_t *e)
{
(void)e;
ESP_LOGI(TAG, "Not available");
}
void update_year_action(lv_event_t * e) void update_year_action(lv_event_t * e)
{ {
(void)e; (void)e;
@@ -31,14 +133,53 @@ void update_year_action(lv_event_t * e)
void click_button_two(lv_event_t * e) void click_button_two(lv_event_t * e)
{ {
(void)e; (void)e;
send_hid_combo_or_warn(HID_MOD_LCTRL, HID_KEY_C);
} }
void click_button_three(lv_event_t * e) void click_button_three(lv_event_t * e)
{ {
(void)e; (void)e;
send_hid_combo_or_warn(HID_MOD_LCTRL, HID_KEY_V);
} }
void click_button_one(lv_event_t * e) void click_button_one(lv_event_t * e)
{ {
(void)e; (void)e;
send_hid_combo_or_warn(HID_MOD_LCTRL, HID_KEY_X);
}
void click_consumer_vol_up(lv_event_t * e)
{
(void)e;
send_consumer_or_warn(HID_CONSUMER_VOLUME_UP);
}
void click_consumer_vol_down(lv_event_t * e)
{
(void)e;
send_consumer_or_warn(HID_CONSUMER_VOLUME_DOWN);
}
void click_consumer_mute(lv_event_t * e)
{
(void)e;
send_consumer_or_warn(HID_CONSUMER_MUTE);
}
void click_consumer_play_pause(lv_event_t * e)
{
(void)e;
send_consumer_or_warn(HID_CONSUMER_PLAY_PAUSE);
}
void click_consumer_prev(lv_event_t * e)
{
(void)e;
send_consumer_or_warn(HID_CONSUMER_SCAN_PREVIOUS);
}
void click_consumer_next(lv_event_t * e)
{
(void)e;
send_consumer_or_warn(HID_CONSUMER_SCAN_NEXT);
} }
+9
View File
@@ -1,4 +1,13 @@
#pragma once #pragma once
#include "lvgl.h"
/* UI event callbacks declared in ui_events.h are implemented in ui_handlers.c */ /* UI event callbacks declared in ui_events.h are implemented in ui_handlers.c */
void ui_handlers_init(void); void ui_handlers_init(void);
void settings_on_tab_clicked(lv_event_t *e);
void settings_on_save_clicked(lv_event_t *e);
void settings_on_factory_reset(lv_event_t *e);
void settings_on_brightness_changed(lv_event_t *e);
void settings_on_value_changed(lv_event_t *e);
void settings_on_network_scan_clicked(lv_event_t *e);
+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);
+18
View File
@@ -9,6 +9,15 @@
#define BOARD_LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000) #define BOARD_LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000)
#define BOARD_LCD_H_RES 170 #define BOARD_LCD_H_RES 170
#define BOARD_LCD_V_RES 320 #define BOARD_LCD_V_RES 320
/* LVGL logical resolution after 90° rotation (landscape) */
#define BOARD_LCD_LOGIC_H_RES 320
#define BOARD_LCD_LOGIC_V_RES 170
/* Touch IC → physical panel alignment (not LVGL logical rotation) */
#define BOARD_TOUCH_SWAP_XY 0
#define BOARD_TOUCH_MIRROR_X 0
#define BOARD_TOUCH_MIRROR_Y 0
#define BOARD_LCD_PANEL_H_RES 240 #define BOARD_LCD_PANEL_H_RES 240
#define BOARD_LCD_GAP_X ((BOARD_LCD_PANEL_H_RES - BOARD_LCD_H_RES) / 2) #define BOARD_LCD_GAP_X ((BOARD_LCD_PANEL_H_RES - BOARD_LCD_H_RES) / 2)
#define BOARD_LCD_GAP_Y 0 #define BOARD_LCD_GAP_Y 0
@@ -40,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);
+34 -6
View File
@@ -2,6 +2,7 @@
#include "board_config.h" #include "board_config.h"
#include "draw/sw/lv_draw_sw.h"
#include "driver/spi_master.h" #include "driver/spi_master.h"
#include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_ops.h"
#include "esp_log.h" #include "esp_log.h"
@@ -31,14 +32,40 @@ static bool notify_flush_ready(esp_lcd_panel_io_handle_t panel_io,
static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{ {
esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data(disp); esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data(disp);
int offsetx1 = area->x1; const lv_area_t *draw_area = area;
int offsetx2 = area->x2; uint8_t *draw_buf = px_map;
int offsety1 = area->y1;
int offsety2 = area->y2; static uint8_t rotated_buf[BOARD_LCD_H_RES * BOARD_LVGL_DRAW_BUF_LINES *
lv_draw_sw_rgb565_swap(px_map, sizeof(lv_color16_t)];
lv_display_rotation_t rotation = lv_display_get_rotation(disp);
lv_area_t rotated_area;
if (rotation != LV_DISPLAY_ROTATION_0) {
lv_color_format_t cf = lv_display_get_color_format(disp);
rotated_area = *area;
lv_display_rotate_area(disp, &rotated_area);
int32_t src_w = lv_area_get_width(area);
int32_t src_h = lv_area_get_height(area);
uint32_t src_stride = lv_draw_buf_width_to_stride(src_w, cf);
uint32_t dest_stride =
lv_draw_buf_width_to_stride(lv_area_get_width(&rotated_area), cf);
lv_draw_sw_rotate(px_map, rotated_buf, src_w, src_h, src_stride,
dest_stride, rotation, cf);
draw_area = &rotated_area;
draw_buf = rotated_buf;
}
int offsetx1 = draw_area->x1;
int offsetx2 = draw_area->x2;
int offsety1 = draw_area->y1;
int offsety2 = draw_area->y2;
lv_draw_sw_rgb565_swap(draw_buf,
(offsetx2 + 1 - offsetx1) * (offsety2 + 1 - offsety1)); (offsetx2 + 1 - offsetx1) * (offsety2 + 1 - offsety1));
esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1,
offsety2 + 1, px_map); offsety2 + 1, draw_buf);
} }
static void increase_lvgl_tick(void *arg) static void increase_lvgl_tick(void *arg)
@@ -80,6 +107,7 @@ lv_display_t *lvgl_port_init(esp_lcd_panel_io_handle_t io_handle,
lv_display_t *display = lv_display_t *display =
lv_display_create(BOARD_LCD_H_RES, BOARD_LCD_V_RES); lv_display_create(BOARD_LCD_H_RES, BOARD_LCD_V_RES);
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_270);
size_t draw_buffer_sz = BOARD_LCD_H_RES * BOARD_LVGL_DRAW_BUF_LINES * size_t draw_buffer_sz = BOARD_LCD_H_RES * BOARD_LVGL_DRAW_BUF_LINES *
sizeof(lv_color16_t); sizeof(lv_color16_t);
+13 -3
View File
@@ -86,9 +86,9 @@ esp_err_t touch_init(lv_display_t *display)
.interrupt = 0, .interrupt = 0,
}, },
.flags = { .flags = {
.swap_xy = 0, .swap_xy = BOARD_TOUCH_SWAP_XY,
.mirror_x = 0, .mirror_x = BOARD_TOUCH_MIRROR_X,
.mirror_y = 0, .mirror_y = BOARD_TOUCH_MIRROR_Y,
}, },
.interrupt_callback = touch_isr_cb, .interrupt_callback = touch_isr_cb,
}; };
@@ -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);
+241 -31
View File
@@ -17,21 +17,126 @@
static const char *TAG = "ble_hid"; static const char *TAG = "ble_hid";
#define HID_KEYBOARD_REPORT_ID 1 #define HID_KEYBOARD_REPORT_ID 1
#define HID_KEYBOARD_REPORT_LEN 7 #define HID_CONSUMER_REPORT_ID 2
#define HID_KEYBOARD_REPORT_LEN 8
#define HID_CONSUMER_REPORT_LEN 2
static const unsigned char s_keyboard_report_map[] = { /* Bit positions for ESP-IDF mediaReportMap2 (Windows-compatible). */
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xE0, #define HID_CC_RPT_VOLUME_BITS 0x3F
0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, #define HID_CC_RPT_BUTTON_BITS 0xF0
0x95, 0x01, 0x75, 0x08, 0x81, 0x03, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, #define HID_CC_RPT_VOLUME_UP 0x40
0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x03, #define HID_CC_RPT_VOLUME_DOWN 0x80
0x95, 0x05, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, #define HID_CC_RPT_MUTE 1
0x29, 0x65, 0x81, 0x00, 0xC0, #define HID_CC_RPT_PLAY_PAUSE 13
#define HID_CC_RPT_SCAN_NEXT_TRK 10
#define HID_CC_RPT_SCAN_PREV_TRK 11
/*
* Keyboard (Report ID 1) + Consumer Control (Report ID 2, mediaReportMap2).
* Windows 11 expects the bit-packed consumer report from ESP-IDF examples,
* not a raw 16-bit HID usage in the input report.
*/
static const unsigned char s_hid_report_map[] = {
/* Keyboard - Report ID 1 */
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x85, HID_KEYBOARD_REPORT_ID,
0x05, 0x07, // Usage Page (Keyboard/Keypad)
0x19, 0xE0, // Usage Minimum (0xE0)
0x29, 0xE7, // Usage Maximum (0xE7)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x03, // Input (Constant, Variable, Absolute)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x05, 0x08, // Usage Page (LEDs)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x05, // Usage Maximum (0x05)
0x91, 0x02, // Output (Data, Variable, Absolute)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x03, // Output (Constant, Variable, Absolute)
0x95, 0x05, // Report Count (5)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Keyboard/Keypad)
0x19, 0x00, // Usage Minimum (0x00)
0x29, 0x65, // Usage Maximum (0x65)
0x81, 0x00, // Input (Data, Array, Absolute)
0xC0, // End Collection
/* Consumer Control - Report ID 2 (mediaReportMap2 from ESP-IDF) */
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, HID_CONSUMER_REPORT_ID,
0x09, 0x02, // Usage (Numeric Key Pad)
0xA1, 0x02, // Collection (Logical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x0A, // Usage Maximum (0x0A)
0x15, 0x01, // Logical Minimum (1)
0x25, 0x0A, // Logical Maximum (10)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x81, 0x00, // Input (Data, Array, Absolute)
0xC0, // End Collection
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x86, // Usage (Channel)
0x15, 0xFF, // Logical Minimum (-1)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x02, // Report Size (2)
0x95, 0x01, // Report Count (1)
0x81, 0x46, // Input (Data, Variable, Relative)
0x09, 0xE9, // Usage (Volume Increment)
0x09, 0xEA, // Usage (Volume Decrement)
0x15, 0x00, // Logical Minimum (0)
0x75, 0x01, // Report Size (1)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x09, 0xE2, // Usage (Mute)
0x09, 0x30, // Usage (Power)
0x09, 0x83, // Usage (Recall Last)
0x09, 0x81, // Usage (Assign Selection)
0x09, 0xB0, // Usage (Play)
0x09, 0xB1, // Usage (Pause)
0x09, 0xB2, // Usage (Record)
0x09, 0xB3, // Usage (Fast Forward)
0x09, 0xB4, // Usage (Rewind)
0x09, 0xB5, // Usage (Scan Next Track)
0x09, 0xB6, // Usage (Scan Previous Track)
0x09, 0xB7, // Usage (Stop)
0x09, 0xCD, // Usage (Play/Pause)
0x15, 0x01, // Logical Minimum (1)
0x25, 0x0D, // Logical Maximum (13)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x81, 0x00, // Input (Data, Array, Absolute)
0x09, 0x80, // Usage (Selection)
0xA1, 0x02, // Collection (Logical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x03, // Usage Maximum (0x03)
0x15, 0x01, // Logical Minimum (1)
0x25, 0x03, // Logical Maximum (3)
0x75, 0x02, // Report Size (2)
0x81, 0x00, // Input (Data, Array, Absolute)
0xC0, // End Collection
0x81, 0x03, // Input (Constant, Variable, Absolute)
0xC0, // End Collection
}; };
static esp_hid_raw_report_map_t s_report_maps[] = { static esp_hid_raw_report_map_t s_report_maps[] = {
{ {
.data = s_keyboard_report_map, .data = s_hid_report_map,
.len = sizeof(s_keyboard_report_map), .len = sizeof(s_hid_report_map),
}, },
}; };
@@ -50,9 +155,56 @@ static esp_hidd_dev_t *s_hid_dev;
static ble_hid_status_cb_t s_status_cb; static ble_hid_status_cb_t s_status_cb;
static bool s_connected; static bool s_connected;
static void ble_hid_cc_set_volume_up(uint8_t *buffer)
{
buffer[0] &= HID_CC_RPT_VOLUME_BITS;
buffer[0] |= HID_CC_RPT_VOLUME_UP;
}
static void ble_hid_cc_set_volume_down(uint8_t *buffer)
{
buffer[0] &= HID_CC_RPT_VOLUME_BITS;
buffer[0] |= HID_CC_RPT_VOLUME_DOWN;
}
static void ble_hid_cc_set_button(uint8_t *buffer, uint8_t button)
{
buffer[1] &= HID_CC_RPT_BUTTON_BITS;
buffer[1] |= button;
}
static bool ble_hid_encode_consumer_report(uint16_t usage, uint8_t buffer[HID_CONSUMER_REPORT_LEN])
{
memset(buffer, 0, HID_CONSUMER_REPORT_LEN);
switch (usage) {
case HID_CONSUMER_VOLUME_UP:
ble_hid_cc_set_volume_up(buffer);
return true;
case HID_CONSUMER_VOLUME_DOWN:
ble_hid_cc_set_volume_down(buffer);
return true;
case HID_CONSUMER_MUTE:
ble_hid_cc_set_button(buffer, HID_CC_RPT_MUTE);
return true;
case HID_CONSUMER_PLAY_PAUSE:
ble_hid_cc_set_button(buffer, HID_CC_RPT_PLAY_PAUSE);
return true;
case HID_CONSUMER_SCAN_NEXT:
ble_hid_cc_set_button(buffer, HID_CC_RPT_SCAN_NEXT_TRK);
return true;
case HID_CONSUMER_SCAN_PREVIOUS:
ble_hid_cc_set_button(buffer, HID_CC_RPT_SCAN_PREV_TRK);
return true;
default:
ESP_LOGW(TAG, "unsupported consumer usage 0x%04X", usage);
return false;
}
}
static void ble_hid_log_buffer(const char *label, const uint8_t *data, size_t len) static void ble_hid_log_buffer(const char *label, const uint8_t *data, size_t len)
{ {
char hex[32]; char hex[40];
size_t pos = 0; size_t pos = 0;
for (size_t i = 0; i < len && pos + 3 < sizeof(hex); i++) { for (size_t i = 0; i < len && pos + 3 < sizeof(hex); i++) {
@@ -62,17 +214,33 @@ static void ble_hid_log_buffer(const char *label, const uint8_t *data, size_t le
ESP_LOGI(TAG, "%s len=%u: %s", label, (unsigned)len, hex); ESP_LOGI(TAG, "%s len=%u: %s", label, (unsigned)len, hex);
} }
static void ble_hid_send_idle_report(void) static int ble_hid_send_input_report(uint8_t report_id, const uint8_t *buffer, size_t len,
const char *label)
{ {
uint8_t buffer[HID_KEYBOARD_REPORT_LEN] = {0}; esp_err_t ret;
if (!ble_hid_ready()) {
return ESP_ERR_INVALID_STATE;
}
ble_hid_log_buffer(label, buffer, len);
ret = esp_hidd_dev_input_set(s_hid_dev, 0, report_id, (uint8_t *)buffer, len);
return (ret == ESP_OK) ? 0 : ret;
}
static void ble_hid_send_idle_reports(void)
{
uint8_t keyboard[HID_KEYBOARD_REPORT_LEN] = {0};
uint8_t consumer[HID_CONSUMER_REPORT_LEN] = {0};
if (!ble_hid_ready()) { if (!ble_hid_ready()) {
return; return;
} }
ble_hid_log_buffer("idle", buffer, sizeof(buffer)); (void)ble_hid_send_input_report(HID_KEYBOARD_REPORT_ID, keyboard, sizeof(keyboard),
(void)esp_hidd_dev_input_set(s_hid_dev, 0, HID_KEYBOARD_REPORT_ID, buffer, "keyboard idle");
sizeof(buffer)); (void)ble_hid_send_input_report(HID_CONSUMER_REPORT_ID, consumer, sizeof(consumer),
"consumer idle");
} }
static void ble_hid_idle_task(void *arg) static void ble_hid_idle_task(void *arg)
@@ -80,7 +248,7 @@ static void ble_hid_idle_task(void *arg)
(void)arg; (void)arg;
/* Windows often enables notify a few seconds after connect. */ /* Windows often enables notify a few seconds after connect. */
vTaskDelay(pdMS_TO_TICKS(3500)); vTaskDelay(pdMS_TO_TICKS(3500));
ble_hid_send_idle_report(); ble_hid_send_idle_reports();
vTaskDelete(NULL); vTaskDelete(NULL);
} }
@@ -157,15 +325,15 @@ int ble_hid_init(ble_hid_status_cb_t status_cb)
return ret; return ret;
} }
ESP_LOGI(TAG, "esp_hidd keyboard ready (report payload=%u bytes)", ESP_LOGI(TAG, "esp_hidd ready (keyboard=%u bytes, consumer=%u bytes)",
(unsigned)HID_KEYBOARD_REPORT_LEN); (unsigned)HID_KEYBOARD_REPORT_LEN, (unsigned)HID_CONSUMER_REPORT_LEN);
return ESP_OK; return ESP_OK;
} }
void ble_hid_on_input_notify(bool enabled) void ble_hid_on_input_notify(bool enabled)
{ {
if (enabled) { if (enabled) {
ble_hid_send_idle_report(); ble_hid_send_idle_reports();
} }
} }
@@ -177,7 +345,6 @@ bool ble_hid_ready(void)
int ble_hid_key_press(uint8_t modifier, uint8_t keycode) int ble_hid_key_press(uint8_t modifier, uint8_t keycode)
{ {
uint8_t buffer[HID_KEYBOARD_REPORT_LEN] = {0}; uint8_t buffer[HID_KEYBOARD_REPORT_LEN] = {0};
esp_err_t ret;
if (!ble_hid_ready()) { if (!ble_hid_ready()) {
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
@@ -185,26 +352,18 @@ int ble_hid_key_press(uint8_t modifier, uint8_t keycode)
buffer[0] = modifier; buffer[0] = modifier;
buffer[2] = keycode; buffer[2] = keycode;
ble_hid_log_buffer("press", buffer, sizeof(buffer)); return ble_hid_send_input_report(HID_KEYBOARD_REPORT_ID, buffer, sizeof(buffer), "press");
ret = esp_hidd_dev_input_set(s_hid_dev, 0, HID_KEYBOARD_REPORT_ID, buffer,
sizeof(buffer));
return (ret == ESP_OK) ? 0 : ret;
} }
int ble_hid_key_release(void) int ble_hid_key_release(void)
{ {
uint8_t buffer[HID_KEYBOARD_REPORT_LEN] = {0}; uint8_t buffer[HID_KEYBOARD_REPORT_LEN] = {0};
esp_err_t ret;
if (!ble_hid_ready()) { if (!ble_hid_ready()) {
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
ble_hid_log_buffer("release", buffer, sizeof(buffer)); return ble_hid_send_input_report(HID_KEYBOARD_REPORT_ID, buffer, sizeof(buffer), "release");
ret = esp_hidd_dev_input_set(s_hid_dev, 0, HID_KEYBOARD_REPORT_ID, buffer,
sizeof(buffer));
return (ret == ESP_OK) ? 0 : ret;
} }
int ble_hid_send_combo(uint8_t modifier, uint8_t keycode) int ble_hid_send_combo(uint8_t modifier, uint8_t keycode)
@@ -220,6 +379,51 @@ int ble_hid_send_combo(uint8_t modifier, uint8_t keycode)
return ble_hid_key_release(); return ble_hid_key_release();
} }
int ble_hid_consumer_press(uint16_t usage)
{
uint8_t buffer[HID_CONSUMER_REPORT_LEN] = {0};
if (!ble_hid_ready()) {
return ESP_ERR_INVALID_STATE;
}
if (usage == 0) {
return ESP_ERR_INVALID_ARG;
}
if (!ble_hid_encode_consumer_report(usage, buffer)) {
return ESP_ERR_NOT_SUPPORTED;
}
return ble_hid_send_input_report(HID_CONSUMER_REPORT_ID, buffer, sizeof(buffer),
"consumer press");
}
int ble_hid_consumer_release(void)
{
uint8_t buffer[HID_CONSUMER_REPORT_LEN] = {0};
if (!ble_hid_ready()) {
return ESP_ERR_INVALID_STATE;
}
return ble_hid_send_input_report(HID_CONSUMER_REPORT_ID, buffer, sizeof(buffer),
"consumer release");
}
int ble_hid_send_consumer(uint16_t usage)
{
int rc;
rc = ble_hid_consumer_press(usage);
if (rc != 0) {
return rc;
}
vTaskDelay(pdMS_TO_TICKS(100));
return ble_hid_consumer_release();
}
#else #else
int ble_hid_init(ble_hid_status_cb_t status_cb) int ble_hid_init(ble_hid_status_cb_t status_cb)
@@ -252,4 +456,10 @@ int ble_hid_send_combo(uint8_t modifier, uint8_t keycode)
return -1; return -1;
} }
int ble_hid_send_consumer(uint16_t usage)
{
(void)usage;
return -1;
}
#endif /* BOARD_BLE_ENABLED */ #endif /* BOARD_BLE_ENABLED */
+10
View File
@@ -9,8 +9,17 @@
#define HID_MOD_LGUI 0x08 #define HID_MOD_LGUI 0x08
#define HID_KEY_C 0x06 #define HID_KEY_C 0x06
#define HID_KEY_V 0x19
#define HID_KEY_X 0x1B
#define HID_KEY_F5 0x3E #define HID_KEY_F5 0x3E
#define HID_CONSUMER_VOLUME_UP 0x00E9
#define HID_CONSUMER_VOLUME_DOWN 0x00EA
#define HID_CONSUMER_MUTE 0x00E2
#define HID_CONSUMER_PLAY_PAUSE 0x00CD
#define HID_CONSUMER_SCAN_NEXT 0x00B5
#define HID_CONSUMER_SCAN_PREVIOUS 0x00B6
typedef void (*ble_hid_status_cb_t)(bool connected); typedef void (*ble_hid_status_cb_t)(bool connected);
int ble_hid_init(ble_hid_status_cb_t status_cb); int ble_hid_init(ble_hid_status_cb_t status_cb);
@@ -19,6 +28,7 @@ bool ble_hid_ready(void);
int ble_hid_key_press(uint8_t modifier, uint8_t keycode); int ble_hid_key_press(uint8_t modifier, uint8_t keycode);
int ble_hid_key_release(void); int ble_hid_key_release(void);
int ble_hid_send_combo(uint8_t modifier, uint8_t keycode); int ble_hid_send_combo(uint8_t modifier, uint8_t keycode);
int ble_hid_send_consumer(uint16_t usage);
#if defined(BOARD_BLE_ENABLED) && BOARD_BLE_ENABLED #if defined(BOARD_BLE_ENABLED) && BOARD_BLE_ENABLED
void ble_hid_on_input_notify(bool enabled); void ble_hid_on_input_notify(bool enabled);
+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");
+139
View File
@@ -0,0 +1,139 @@
// This file was generated by SquareLine Studio
// SquareLine Studio version: SquareLine Studio 1.6.1
// LVGL version: 9.2.2
// Project name: HeavenlyHeroStar
#include "../ui.h"
#include "board_config.h"
lv_obj_t * ui_Screen3 = NULL;
lv_obj_t * ui_BtnVolDown = NULL;
lv_obj_t * ui_BtnVolUp = NULL;
lv_obj_t * ui_BtnMute = NULL;
lv_obj_t * ui_BtnPlayPause = NULL;
lv_obj_t * ui_BtnPrev = NULL;
lv_obj_t * ui_BtnNext = NULL;
lv_obj_t * ui_LabelVolDown = NULL;
lv_obj_t * ui_LabelVolUp = NULL;
lv_obj_t * ui_LabelMute = NULL;
lv_obj_t * ui_LabelPlayPause = NULL;
lv_obj_t * ui_LabelPrev = NULL;
lv_obj_t * ui_LabelNext = NULL;
static void ui_screen3_create_button(lv_obj_t **btn, lv_obj_t **label, int32_t x, int32_t y,
const char *text)
{
*btn = lv_button_create(ui_Screen3);
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);
lv_obj_add_flag(*btn, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
lv_obj_remove_flag(*btn, LV_OBJ_FLAG_SCROLLABLE);
*label = lv_label_create(ui_Screen3);
lv_obj_set_width(*label, LV_SIZE_CONTENT);
lv_obj_set_height(*label, LV_SIZE_CONTENT);
lv_obj_set_x(*label, x);
lv_obj_set_y(*label, y);
lv_obj_set_align(*label, LV_ALIGN_CENTER);
lv_label_set_text(*label, text);
}
// event funtions
void ui_event_BtnVolDown(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
click_consumer_vol_down(e);
}
}
void ui_event_BtnVolUp(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
click_consumer_vol_up(e);
}
}
void ui_event_BtnMute(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
click_consumer_mute(e);
}
}
void ui_event_BtnPlayPause(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
click_consumer_play_pause(e);
}
}
void ui_event_BtnPrev(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
click_consumer_prev(e);
}
}
void ui_event_BtnNext(lv_event_t * e)
{
lv_event_code_t event_code = lv_event_get_code(e);
if(event_code == LV_EVENT_CLICKED) {
click_consumer_next(e);
}
}
// build funtions
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, -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);
lv_obj_add_event_cb(ui_BtnMute, ui_event_BtnMute, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_BtnPlayPause, ui_event_BtnPlayPause, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_BtnPrev, ui_event_BtnPrev, LV_EVENT_ALL, NULL);
lv_obj_add_event_cb(ui_BtnNext, ui_event_BtnNext, LV_EVENT_ALL, NULL);
}
void ui_Screen3_screen_destroy(void)
{
if(ui_Screen3) lv_obj_del(ui_Screen3);
ui_Screen3 = NULL;
ui_BtnVolDown = NULL;
ui_BtnVolUp = NULL;
ui_BtnMute = NULL;
ui_BtnPlayPause = NULL;
ui_BtnPrev = NULL;
ui_BtnNext = NULL;
ui_LabelVolDown = NULL;
ui_LabelVolUp = NULL;
ui_LabelMute = NULL;
ui_LabelPlayPause = NULL;
ui_LabelPrev = NULL;
ui_LabelNext = NULL;
}
+41
View File
@@ -0,0 +1,41 @@
// This file was generated by SquareLine Studio
// SquareLine Studio version: SquareLine Studio 1.6.1
// LVGL version: 9.2.2
// Project name: HeavenlyHeroStar
#ifndef UI_SCREEN3_H
#define UI_SCREEN3_H
#ifdef __cplusplus
extern "C" {
#endif
// SCREEN: ui_Screen3
extern void ui_Screen3_screen_init(void);
extern void ui_Screen3_screen_destroy(void);
extern lv_obj_t * ui_Screen3;
extern void ui_event_BtnVolDown(lv_event_t * e);
extern lv_obj_t * ui_BtnVolDown;
extern void ui_event_BtnVolUp(lv_event_t * e);
extern lv_obj_t * ui_BtnVolUp;
extern void ui_event_BtnMute(lv_event_t * e);
extern lv_obj_t * ui_BtnMute;
extern void ui_event_BtnPlayPause(lv_event_t * e);
extern lv_obj_t * ui_BtnPlayPause;
extern void ui_event_BtnPrev(lv_event_t * e);
extern lv_obj_t * ui_BtnPrev;
extern void ui_event_BtnNext(lv_event_t * e);
extern lv_obj_t * ui_BtnNext;
extern lv_obj_t * ui_LabelVolDown;
extern lv_obj_t * ui_LabelVolUp;
extern lv_obj_t * ui_LabelMute;
extern lv_obj_t * ui_LabelPlayPause;
extern lv_obj_t * ui_LabelPrev;
extern lv_obj_t * ui_LabelNext;
// CUSTOM VARIABLES
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif
+154
View File
@@ -0,0 +1,154 @@
#include "ui_Settings.h"
#include "board_config.h"
#include "settings/settings_store.h"
#include "settings/settings_tabs.h"
#include "ui_handlers.h"
#include <stddef.h>
lv_obj_t *ui_Settings;
lv_obj_t *ui_Settings_sidebar;
lv_obj_t *ui_Settings_content;
static settings_tab_t s_active_tab = SETTINGS_TAB_SYSTEM;
static lv_obj_t *s_nav_buttons[4];
static lv_style_t s_style_sidebar_btn_active;
static lv_style_t s_style_content_bg;
static bool s_styles_inited;
static const char *s_tab_labels[] = {
"System",
"Display",
"Audio",
"Network",
};
static void init_styles(void)
{
if (s_styles_inited) {
return;
}
lv_style_init(&s_style_sidebar_btn_active);
lv_style_set_bg_color(&s_style_sidebar_btn_active,
lv_palette_main(LV_PALETTE_BLUE));
lv_style_set_bg_opa(&s_style_sidebar_btn_active, LV_OPA_COVER);
lv_style_init(&s_style_content_bg);
lv_style_set_bg_color(&s_style_content_bg,
lv_palette_lighten(LV_PALETTE_GREY, 2));
lv_style_set_bg_opa(&s_style_content_bg, LV_OPA_COVER);
s_styles_inited = true;
}
static void sidebar_update_highlight(settings_tab_t tab)
{
for (size_t i = 0; i < 4; i++) {
if (s_nav_buttons[i] == NULL) {
continue;
}
if ((settings_tab_t)i == tab) {
lv_obj_add_style(s_nav_buttons[i], &s_style_sidebar_btn_active, 0);
} else {
lv_obj_remove_style(s_nav_buttons[i], &s_style_sidebar_btn_active, 0);
}
}
}
settings_tab_t ui_Settings_get_active_tab(void)
{
return s_active_tab;
}
void ui_Settings_switch_tab(settings_tab_t tab)
{
settings_data_t buf = *settings_store_get();
settings_tabs_collect(s_active_tab, &buf);
settings_store_update(&buf);
s_active_tab = tab;
sidebar_update_highlight(tab);
lv_obj_clean(ui_Settings_content);
switch (tab) {
case SETTINGS_TAB_SYSTEM:
load_system_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_DISPLAY:
load_display_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_AUDIO:
load_audio_settings(ui_Settings_content, settings_store_get());
break;
case SETTINGS_TAB_NETWORK:
load_network_settings(ui_Settings_content, settings_store_get());
break;
default:
LV_ASSERT_MSG(false, "unexpected settings_tab_t");
break;
}
}
void ui_Settings_screen_init(void)
{
init_styles();
ui_Settings = lv_obj_create(NULL);
lv_obj_set_size(ui_Settings, BOARD_LCD_LOGIC_H_RES, BOARD_LCD_LOGIC_V_RES);
lv_obj_set_style_pad_all(ui_Settings, 2, 0);
lv_obj_remove_flag(ui_Settings, LV_OBJ_FLAG_SCROLLABLE);
ui_Settings_sidebar = lv_list_create(ui_Settings);
lv_obj_set_size(ui_Settings_sidebar, 90, 170);
lv_obj_set_pos(ui_Settings_sidebar, 0, 0);
lv_obj_set_style_pad_all(ui_Settings_sidebar, 2, 0);
lv_obj_set_style_pad_row(ui_Settings_sidebar, 2, 0);
lv_obj_remove_flag(ui_Settings_sidebar, LV_OBJ_FLAG_SCROLLABLE);
for (size_t i = 0; i < 4; i++) {
s_nav_buttons[i] =
lv_list_add_button(ui_Settings_sidebar, NULL, s_tab_labels[i]);
lv_obj_set_size(s_nav_buttons[i], 86, 28);
lv_obj_add_event_cb(s_nav_buttons[i], settings_on_tab_clicked,
LV_EVENT_CLICKED, (void *)(intptr_t)i);
}
lv_obj_t *save_btn = lv_button_create(ui_Settings);
lv_obj_set_size(save_btn, 86, 28);
lv_obj_set_pos(save_btn, 2, 138);
lv_obj_t *save_label = lv_label_create(save_btn);
lv_label_set_text(save_label, "Save");
lv_obj_center(save_label);
lv_obj_add_event_cb(save_btn, settings_on_save_clicked, LV_EVENT_CLICKED,
NULL);
ui_Settings_content = lv_obj_create(ui_Settings);
lv_obj_set_size(ui_Settings_content, 230, 170);
lv_obj_set_pos(ui_Settings_content, 90, 0);
lv_obj_set_style_pad_all(ui_Settings_content, 2, 0);
lv_obj_add_style(ui_Settings_content, &s_style_content_bg, 0);
lv_obj_remove_flag(ui_Settings_content, LV_OBJ_FLAG_SCROLLABLE);
settings_store_load();
settings_store_apply();
s_active_tab = SETTINGS_TAB_SYSTEM;
sidebar_update_highlight(s_active_tab);
load_system_settings(ui_Settings_content, settings_store_get());
}
void ui_Settings_screen_destroy(void)
{
if (ui_Settings != NULL) {
lv_obj_del(ui_Settings);
ui_Settings = NULL;
ui_Settings_sidebar = NULL;
ui_Settings_content = NULL;
}
for (size_t i = 0; i < 4; i++) {
s_nav_buttons[i] = NULL;
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "lvgl.h"
#include "settings_types.h"
extern lv_obj_t *ui_Settings;
extern lv_obj_t *ui_Settings_sidebar;
extern lv_obj_t *ui_Settings_content;
void ui_Settings_screen_init(void);
void ui_Settings_screen_destroy(void);
void ui_Settings_switch_tab(settings_tab_t tab);
settings_tab_t ui_Settings_get_active_tab(void);
+137
View File
@@ -0,0 +1,137 @@
#include "settings_store.h"
#include "display.h"
#include "motion_gesture.h"
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
static const char *TAG = "settings_store";
static const char *SETTINGS_NAMESPACE = "settings";
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,
};
static settings_data_t s_data;
static void load_key_u8(nvs_handle_t handle, const char *key, uint8_t *out,
uint8_t default_val)
{
uint8_t val = default_val;
if (nvs_get_u8(handle, key, &val) != ESP_OK) {
val = default_val;
}
*out = val;
}
void settings_store_load(void)
{
s_data = SETTINGS_DEFAULTS;
nvs_handle_t handle;
esp_err_t err = nvs_open(SETTINGS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK) {
ESP_LOGI(TAG, "No saved settings, using defaults");
return;
}
uint8_t val;
load_key_u8(handle, "notif", &val, SETTINGS_DEFAULTS.notification_enabled);
s_data.notification_enabled = val != 0;
load_key_u8(handle, "bright", &s_data.brightness_pct,
SETTINGS_DEFAULTS.brightness_pct);
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,
s_data.volume_pct);
}
const settings_data_t *settings_store_get(void)
{
return &s_data;
}
void settings_store_update(const settings_data_t *data)
{
s_data = *data;
}
esp_err_t settings_store_commit(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(SETTINGS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err));
return err;
}
err = nvs_set_u8(handle, "notif", s_data.notification_enabled ? 1 : 0);
if (err == ESP_OK) {
err = nvs_set_u8(handle, "bright", s_data.brightness_pct);
}
if (err == ESP_OK) {
err = nvs_set_u8(handle, "vol", s_data.volume_pct);
}
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);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "commit failed: %s", esp_err_to_name(err));
}
return err;
}
esp_err_t settings_store_factory_reset(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(SETTINGS_NAMESPACE, NVS_READWRITE, &handle);
if (err == ESP_OK) {
err = nvs_erase_all(handle);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
}
s_data = SETTINGS_DEFAULTS;
esp_err_t commit_err = settings_store_commit();
if (err != ESP_OK) {
return err;
}
return commit_err;
}
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);
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "esp_err.h"
#include "settings_types.h"
void settings_store_load(void);
const settings_data_t *settings_store_get(void);
void settings_store_update(const settings_data_t *data);
esp_err_t settings_store_commit(void);
esp_err_t settings_store_factory_reset(void);
void settings_store_apply(void);
+235
View File
@@ -0,0 +1,235 @@
#include "settings_tabs.h"
#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;
static lv_obj_t *s_mute_switch;
static lv_obj_t *create_status_bar(lv_obj_t *content, const char *title)
{
lv_obj_t *status_bar = lv_obj_create(content);
lv_obj_set_size(status_bar, 230, 25);
lv_obj_set_pos(status_bar, 0, 0);
lv_obj_set_style_pad_all(status_bar, 2, 0);
lv_obj_set_style_border_width(status_bar, 1, 0);
lv_obj_set_style_border_side(status_bar, LV_BORDER_SIDE_BOTTOM, 0);
lv_obj_remove_flag(status_bar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *title_label = lv_label_create(status_bar);
lv_label_set_text(title_label, title);
lv_obj_align(title_label, LV_ALIGN_LEFT_MID, 2, 0);
lv_obj_t *time_label = lv_label_create(status_bar);
lv_label_set_text(time_label, "--:--");
lv_obj_align(time_label, LV_ALIGN_RIGHT_MID, -2, 0);
return status_bar;
}
static lv_obj_t *create_settings_body(lv_obj_t *content)
{
lv_obj_t *body = lv_obj_create(content);
lv_obj_set_size(body, 230, 145);
lv_obj_set_pos(body, 0, 25);
lv_obj_set_style_pad_all(body, 2, 0);
lv_obj_set_style_border_width(body, 0, 0);
lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE);
return body;
}
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);
lv_obj_t *notif_label = lv_label_create(body);
lv_label_set_text(notif_label, "Enable Notification");
lv_obj_set_pos(notif_label, 4, 4);
lv_obj_set_size(notif_label, 120, 28);
s_notif_switch = lv_switch_create(body);
lv_obj_set_pos(s_notif_switch, 180, 4);
lv_obj_set_size(s_notif_switch, 40, 28);
if (data->notification_enabled) {
lv_obj_add_state(s_notif_switch, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(s_notif_switch, settings_on_value_changed,
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);
lv_obj_set_style_text_color(reset_btn, lv_palette_main(LV_PALETTE_RED), 0);
lv_obj_t *reset_label = lv_label_create(reset_btn);
lv_label_set_text(reset_label, "Factory Reset");
lv_obj_center(reset_label);
lv_obj_add_event_cb(reset_btn, settings_on_factory_reset, LV_EVENT_CLICKED,
NULL);
}
void load_display_settings(lv_obj_t *content, const settings_data_t *data)
{
s_brightness_slider = NULL;
s_brightness_pct_label = NULL;
create_status_bar(content, "Display Settings");
lv_obj_t *body = create_settings_body(content);
lv_obj_t *bright_label = lv_label_create(body);
lv_label_set_text(bright_label, "Brightness");
lv_obj_set_pos(bright_label, 4, 4);
lv_obj_set_size(bright_label, 80, 28);
s_brightness_slider = lv_slider_create(body);
lv_obj_set_pos(s_brightness_slider, 88, 8);
lv_obj_set_size(s_brightness_slider, 100, 12);
lv_slider_set_range(s_brightness_slider, 0, 100);
lv_slider_set_value(s_brightness_slider, data->brightness_pct, LV_ANIM_OFF);
s_brightness_pct_label = lv_label_create(body);
lv_label_set_text_fmt(s_brightness_pct_label, "%u%%", data->brightness_pct);
lv_obj_set_pos(s_brightness_pct_label, 196, 4);
lv_obj_set_size(s_brightness_pct_label, 30, 28);
lv_obj_set_user_data(s_brightness_slider, s_brightness_pct_label);
lv_obj_add_event_cb(s_brightness_slider, settings_on_brightness_changed,
LV_EVENT_VALUE_CHANGED, NULL);
}
void load_audio_settings(lv_obj_t *content, const settings_data_t *data)
{
s_volume_slider = NULL;
s_mute_switch = NULL;
create_status_bar(content, "Audio Settings");
lv_obj_t *body = create_settings_body(content);
lv_obj_t *vol_label = lv_label_create(body);
lv_label_set_text(vol_label, "Volume");
lv_obj_set_pos(vol_label, 4, 4);
lv_obj_set_size(vol_label, 80, 28);
s_volume_slider = lv_slider_create(body);
lv_obj_set_pos(s_volume_slider, 88, 8);
lv_obj_set_size(s_volume_slider, 100, 12);
lv_slider_set_range(s_volume_slider, 0, 100);
lv_slider_set_value(s_volume_slider, data->volume_pct, LV_ANIM_OFF);
lv_obj_add_event_cb(s_volume_slider, settings_on_value_changed,
LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_VOLUME);
lv_obj_t *mute_label = lv_label_create(body);
lv_label_set_text(mute_label, "Mute");
lv_obj_set_pos(mute_label, 4, 40);
lv_obj_set_size(mute_label, 80, 28);
s_mute_switch = lv_switch_create(body);
lv_obj_set_pos(s_mute_switch, 180, 40);
lv_obj_set_size(s_mute_switch, 40, 28);
if (data->mute) {
lv_obj_add_state(s_mute_switch, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(s_mute_switch, settings_on_value_changed,
LV_EVENT_VALUE_CHANGED,
(void *)(intptr_t)SETTINGS_FIELD_MUTE);
}
void load_network_settings(lv_obj_t *content, const settings_data_t *data)
{
(void)data;
create_status_bar(content, "Network Settings");
lv_obj_t *body = create_settings_body(content);
lv_obj_t *wifi_label = lv_label_create(body);
lv_label_set_text(wifi_label, "WiFi: Disabled");
lv_obj_set_pos(wifi_label, 4, 4);
lv_obj_set_size(wifi_label, 140, 28);
lv_obj_t *scan_btn = lv_button_create(body);
lv_obj_set_pos(scan_btn, 4, 40);
lv_obj_set_size(scan_btn, 80, 28);
lv_obj_t *scan_label = lv_label_create(scan_btn);
lv_label_set_text(scan_label, "Scan");
lv_obj_center(scan_label);
lv_obj_add_event_cb(scan_btn, settings_on_network_scan_clicked,
LV_EVENT_CLICKED, NULL);
}
void settings_tabs_collect(settings_tab_t tab, settings_data_t *out)
{
switch (tab) {
case SETTINGS_TAB_SYSTEM:
if (s_notif_switch != NULL) {
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) {
out->brightness_pct =
(uint8_t)lv_slider_get_value(s_brightness_slider);
}
break;
case SETTINGS_TAB_AUDIO:
if (s_volume_slider != NULL) {
out->volume_pct = (uint8_t)lv_slider_get_value(s_volume_slider);
}
if (s_mute_switch != NULL) {
out->mute = lv_obj_has_state(s_mute_switch, LV_STATE_CHECKED);
}
break;
case SETTINGS_TAB_NETWORK:
break;
default:
LV_ASSERT_MSG(false, "unexpected settings_tab_t");
break;
}
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "lvgl.h"
#include "settings_types.h"
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);
void load_display_settings(lv_obj_t *content, const settings_data_t *data);
void load_audio_settings(lv_obj_t *content, const settings_data_t *data);
void load_network_settings(lv_obj_t *content, const settings_data_t *data);
void settings_tabs_collect(settings_tab_t tab, settings_data_t *out);
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef enum {
SETTINGS_TAB_SYSTEM = 0,
SETTINGS_TAB_DISPLAY,
SETTINGS_TAB_AUDIO,
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;
+5 -1
View File
@@ -32,12 +32,16 @@ void ui_init(void)
lv_disp_set_theme(dispp, theme); lv_disp_set_theme(dispp, theme);
ui_Screen1_screen_init(); ui_Screen1_screen_init();
ui_Screen2_screen_init(); ui_Screen2_screen_init();
ui_Screen3_screen_init();
ui_Settings_screen_init();
ui____initial_actions0 = lv_obj_create(NULL); ui____initial_actions0 = lv_obj_create(NULL);
lv_disp_load_scr(ui_Screen2); lv_disp_load_scr(ui_Settings);
} }
void ui_destroy(void) void ui_destroy(void)
{ {
ui_Screen1_screen_destroy(); ui_Screen1_screen_destroy();
ui_Screen2_screen_destroy(); ui_Screen2_screen_destroy();
ui_Screen3_screen_destroy();
ui_Settings_screen_destroy();
} }
+2
View File
@@ -20,6 +20,8 @@ extern "C" {
#include "screens/ui_Screen1.h" #include "screens/ui_Screen1.h"
#include "screens/ui_Screen2.h" #include "screens/ui_Screen2.h"
#include "screens/ui_Screen3.h"
#include "screens/ui_Settings.h"
///////////////////// VARIABLES //////////////////// ///////////////////// VARIABLES ////////////////////
+30
View File
@@ -24,3 +24,33 @@ void click_button_one(lv_event_t * e)
{ {
// Your code here // Your code here
} }
void click_consumer_vol_up(lv_event_t * e)
{
// Your code here
}
void click_consumer_vol_down(lv_event_t * e)
{
// Your code here
}
void click_consumer_mute(lv_event_t * e)
{
// Your code here
}
void click_consumer_play_pause(lv_event_t * e)
{
// Your code here
}
void click_consumer_prev(lv_event_t * e)
{
// Your code here
}
void click_consumer_next(lv_event_t * e)
{
// Your code here
}
+6
View File
@@ -16,6 +16,12 @@ void update_year_action(lv_event_t * e);
void click_button_two(lv_event_t * e); void click_button_two(lv_event_t * e);
void click_button_three(lv_event_t * e); void click_button_three(lv_event_t * e);
void click_button_one(lv_event_t * e); void click_button_one(lv_event_t * e);
void click_consumer_vol_up(lv_event_t * e);
void click_consumer_vol_down(lv_event_t * e);
void click_consumer_mute(lv_event_t * e);
void click_consumer_play_pause(lv_event_t * e);
void click_consumer_prev(lv_event_t * e);
void click_consumer_next(lv_event_t * e);
#ifdef __cplusplus #ifdef __cplusplus
} /*extern "C"*/ } /*extern "C"*/