Compare commits
11 Commits
68dd7e95ab
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 92bead72b9 | |||
| 5ef6c20862 | |||
| b154c3fa20 | |||
| efb3c8f304 | |||
| ff7d82286a | |||
| e5e1d5b85e | |||
| 6a6ee41a07 | |||
| 9a355e540c | |||
| ab6fcefea0 | |||
| 8254bc7d50 | |||
| e056a113a2 |
@@ -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(触摸)** 与 **QMI8658A(IMU)** 挂载在同一条 I2C 总线(`I2C_NUM_0`,SDA=IO47,SCL=IO48)。固件在 `main/bsp/board_config.h` 中定义引脚,触摸与 IMU 驱动共用该总线句柄。
|
||||||
|
|
||||||
|
| 设备 | I2C 地址(7-bit) | 说明 |
|
||||||
|
| :------- | :---------------- | :---------------------------- |
|
||||||
|
| CST816S | 由驱动自动探测 | 电容触摸 |
|
||||||
|
| QMI8658A | `0x6A` 或 `0x6B` | 由 SDO 电平决定;本板实测多为 `0x6B` |
|
||||||
|
|
||||||
|
IMU 中断引脚(IO8 / IO7)当前固件采用轮询采样,Phase 4 可改为中断驱动。
|
||||||
|
|
||||||
## Wi-Fi Station Example
|
## 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.)
|
||||||
|
|||||||
@@ -0,0 +1,765 @@
|
|||||||
|
# Settings 横屏页面实现规格
|
||||||
|
|
||||||
|
> 项目:HeavenlyHeroStar
|
||||||
|
> 目标芯片:ESP32-S3
|
||||||
|
> ESP-IDF:6.0.1
|
||||||
|
> LVGL:9.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(0–1023)` 已实现 |
|
||||||
|
| UI 屏幕 | `main/ui/screens/ui_Screen{1,2,3}.c` | Screen1 日历 170×320;Screen3 为默认启动页 |
|
||||||
|
| 事件处理 | `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(主从)** 布局:
|
||||||
|
|
||||||
|
- 左侧 Sidebar(90px):System / Display / Audio / Network 导航 + Save 按钮
|
||||||
|
- 右侧 Content(230px):随 Tab 切换动态刷新的设置项
|
||||||
|
- **全局横屏**:启动后 LVGL 逻辑分辨率为 **320×170**,所有页面按横屏坐标设计
|
||||||
|
|
||||||
|
首版(MVP)交付:
|
||||||
|
|
||||||
|
- BSP 横屏基础设施(display 旋转 + touch 校准)
|
||||||
|
- Settings 屏幕骨架与 Tab 切换机制
|
||||||
|
- System Tab 完整功能(恢复出厂)
|
||||||
|
- Display Tab 亮度滑块(实时预览 + NVS 持久化)
|
||||||
|
- Audio / Network Tab 占位 UI + NVS 存根
|
||||||
|
- Save 按钮写入 NVS;Factory Reset 恢复默认并重启
|
||||||
|
|
||||||
|
### 1.3 方案选型结论
|
||||||
|
|
||||||
|
| 方案 | 说明 | 结论 |
|
||||||
|
|------|------|------|
|
||||||
|
| A | SquareLine Studio 导出静态 Settings 屏 | 不利于 Tab 动态刷新,且易被重新导出覆盖 |
|
||||||
|
| **B** | **手写 C + `lv_obj_clean` 动态加载** | **采用**:模块化、可扩展、与 SquareLine 解耦 |
|
||||||
|
| C | 每个 Tab 独立 Screen,lv_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 | 相对 list,y 递增 | `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 行高 | 32–36px |
|
||||||
|
| 字体 | 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 = (240−170)/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 切换前 collect;Save 时 commit |
|
||||||
|
| Factory Reset | `lv_button` | — | 确认后 `settings_store_factory_reset()` + `esp_restart()` |
|
||||||
|
|
||||||
|
StatusBar 标题:`"System Settings"`
|
||||||
|
|
||||||
|
#### Display Tab
|
||||||
|
|
||||||
|
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|
||||||
|
|------|-----------|----------|------|
|
||||||
|
| Brightness | `lv_slider` (0–100) + `%` label | `brightness_pct` | `VALUE_CHANGED` 时:`settings_store` 更新内存 + `settings_store_apply()` 实时调背光;Save 时 commit |
|
||||||
|
|
||||||
|
StatusBar 标题:`"Display Settings"`
|
||||||
|
|
||||||
|
#### Audio Tab
|
||||||
|
|
||||||
|
| 控件 | LVGL 类型 | 数据字段 | 行为 |
|
||||||
|
|------|-----------|----------|------|
|
||||||
|
| Volume | `lv_slider` (0–100) | `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 控件回读到 out(out 应先拷贝 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 | 宽 | 100–120px |
|
||||||
|
| 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; /* 0–100, default 70 */
|
||||||
|
uint8_t volume_pct; /* 0–100, 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` | 0–100 |
|
||||||
|
| `"vol"` | `uint8_t` | `volume_pct` | 0–100 |
|
||||||
|
| `"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 结果更新内存 struct(collect 前应先 `*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 范围 | 待确认 |
|
||||||
|
|
||||||
|
### 阶段 1:BSP 横屏 + 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-1~AC-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×170,Sidebar 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` 整个 content;static style 不可绑定到会被删除的对象上。
|
||||||
|
9. **跨 Tab 数据**:Tab 切换前必须 collect;slider/switch 的 `VALUE_CHANGED` 应同步内存,避免 UI 与硬件不一致。
|
||||||
|
10. **Factory Reset 范围**:Phase 1 仅擦除 `"settings"` namespace;BLE 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 约束 |
|
||||||
@@ -0,0 +1,668 @@
|
|||||||
|
# 摇一摇切换页面实现规格
|
||||||
|
|
||||||
|
> 项目:HeavenlyHeroStar
|
||||||
|
> 目标芯片:ESP32-S3
|
||||||
|
> ESP-IDF:6.0.1
|
||||||
|
> LVGL:9.2.2
|
||||||
|
> 逻辑分辨率:320×170(全局横屏)
|
||||||
|
> IMU:QMI8658(3 轴加速度 + 3 轴陀螺仪)
|
||||||
|
> 状态:待实现(v1.0 初稿)
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
### 1.1 现状
|
||||||
|
|
||||||
|
板载 QMI8658 六轴 IMU 已在硬件文档中定义,固件侧尚未接入;UI 层四屏已创建但无页面路由机制。
|
||||||
|
|
||||||
|
| 模块 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| IMU 硬件 | `README.md` | QMI8658,I2C SDA=IO8 / SCL=IO7,INT1=IO6 / INT2=IO15 |
|
||||||
|
| 板级配置 | `main/bsp/board_config.h` | **无** IMU 引脚宏;触摸使用 `I2C_NUM_0`(IO47/48) |
|
||||||
|
| 触摸 I2C | `main/bsp/touch.c` | CST816S,`i2c_master_bus` 独占 `I2C_NUM_0` |
|
||||||
|
| UI 屏幕 | `main/ui/ui.c` | 启动时创建 Screen1/2/3 + Settings,默认 `lv_disp_load_scr(ui_Settings)` |
|
||||||
|
| 页面路由 | — | **不存在**;`_ui_screen_change()` 在 `ui_helpers.c` 中已实现但未被调用 |
|
||||||
|
| LVGL 线程 | `main/bsp/lvgl_port.c` | 独立 task + `_lock`;外部改 UI 须 `lvgl_port_lock()` |
|
||||||
|
| Settings | `main/ui/settings/` | `settings_data_t` **无** motion 字段;NVS namespace `"settings"` |
|
||||||
|
| 事件处理 | `main/app/ui_handlers.c` | HID / Settings 回调;无 IMU / 导航逻辑 |
|
||||||
|
| IMU 驱动 | — | 仓库内无 QMI8658 组件或 `main/bsp/imu.c` |
|
||||||
|
|
||||||
|
### 1.2 目标
|
||||||
|
|
||||||
|
通过 **摇一摇** 在四屏之间循环切换,作为触摸与物理按键之外的第三种导航方式。
|
||||||
|
|
||||||
|
**已确认产品决策:**
|
||||||
|
|
||||||
|
| 决策项 | 结论 |
|
||||||
|
|--------|------|
|
||||||
|
| 参与轮播的页面 | **Settings + Screen1 + Screen2 + Screen3** 四屏循环 |
|
||||||
|
| 手势映射 | **任意方向摇一次 → 下一页**(MVP 不做左/右方向区分) |
|
||||||
|
| 默认轮播顺序 | `Settings → Screen1 → Screen2 → Screen3 → Settings` |
|
||||||
|
| Settings 页行为 | 在 Settings 时摇一摇同样切到 Screen1(不因当前在设置页而禁用) |
|
||||||
|
|
||||||
|
**首版(MVP)交付:**
|
||||||
|
|
||||||
|
- QMI8658 BSP 驱动 + 100 Hz 加速度采样(轮询)
|
||||||
|
- 摇一摇检测:去重力合成幅值 + 阈值 + 时间窗 + cooldown
|
||||||
|
- `ui_nav` 模块:循环切下一屏,fade 动画
|
||||||
|
- System Tab:「Shake Navigation」开关 + 灵敏度档位(低/中/高)
|
||||||
|
- NVS 持久化;Factory Reset 恢复 motion 默认值
|
||||||
|
- 线程安全:IMU task → Queue → `ui_nav_next()`(内部 `lvgl_port_lock`)
|
||||||
|
|
||||||
|
**后续阶段(非 MVP 阻塞):**
|
||||||
|
|
||||||
|
- INT1 Data Ready 中断,降低 CPU 轮询与功耗
|
||||||
|
- `ui_nav_prev()` 与方向性摇一摇
|
||||||
|
- 切页页码指示(dot indicator)
|
||||||
|
- Screen1/2/3 横屏布局完善(见 `settings-screen-landscape.md` Phase 3)
|
||||||
|
|
||||||
|
### 1.3 方案选型结论
|
||||||
|
|
||||||
|
| 方案 | 说明 | 结论 |
|
||||||
|
|------|------|------|
|
||||||
|
| A | 仅陀螺仪积分判摇 | 零偏漂移大,不采用 |
|
||||||
|
| **B** | **加速度去重力 + 合成幅值峰值** | **MVP 采用** |
|
||||||
|
| C | QMI8658 内置 Wake-on-Motion 硬件判摇 | Phase 4 省电优化 |
|
||||||
|
| D | IMU task 内直接调用 `lv_screen_load_anim()` | 线程不安全,**禁止** |
|
||||||
|
| **E** | **FreeRTOS Queue → nav task → `lvgl_port_lock` 切屏** | **采用**(模式参考 `boot_button.c`) |
|
||||||
|
| F | esp_event 总线投递 `UI_NAV_NEXT` | 可选,与 Queue 等价;MVP 用 Queue 即可 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 硬件与 BSP
|
||||||
|
|
||||||
|
### 2.1 引脚与总线
|
||||||
|
|
||||||
|
来源:`README.md` 外围接口表。
|
||||||
|
|
||||||
|
| 信号 | GPIO | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| IMU_SDA | IO8 | I2C 数据线 |
|
||||||
|
| IMU_SCL | IO7 | I2C 时钟线 |
|
||||||
|
| IMU_INT1 | IO6 | 中断 1(Phase 4:Data Ready / Motion) |
|
||||||
|
| IMU_INT2 | IO15 | 中断 2(Phase 4:备用) |
|
||||||
|
|
||||||
|
**I2C 总线隔离:** 触摸已占用 `I2C_NUM_0`(`main/bsp/touch.c`)。IMU **必须**使用独立总线,建议 `I2C_NUM_1`,避免与 CST816 争用或地址冲突时的 bus 锁问题。
|
||||||
|
|
||||||
|
### 2.2 board_config.h 新增宏
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* IMU I2C (QMI8658) */
|
||||||
|
#define BOARD_IMU_I2C_NUM I2C_NUM_1
|
||||||
|
#define BOARD_PIN_IMU_SDA 8
|
||||||
|
#define BOARD_PIN_IMU_SCL 7
|
||||||
|
#define BOARD_PIN_IMU_INT1 6
|
||||||
|
#define BOARD_PIN_IMU_INT2 15
|
||||||
|
#define BOARD_IMU_I2C_FREQ_HZ 400000
|
||||||
|
#define BOARD_IMU_ENABLED 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 imu.c / imu.h 职责
|
||||||
|
|
||||||
|
新建 `main/bsp/imu.c`、`main/bsp/imu.h`:
|
||||||
|
|
||||||
|
| API | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `esp_err_t imu_init(void)` | 初始化 I2C bus + QMI8658 芯片 |
|
||||||
|
| `esp_err_t imu_read_accel(float *ax, *ay, *az)` | 读取加速度,单位 **g**(±8g 量程) |
|
||||||
|
| `esp_err_t imu_read_gyro(...)` | Phase 4 可选;MVP 可不实现 |
|
||||||
|
| `void imu_start_task(imu_sample_cb_t cb)` | 100 Hz 采样循环,回调 `motion_gesture` |
|
||||||
|
|
||||||
|
**初始化序列(MVP):**
|
||||||
|
|
||||||
|
1. `i2c_new_master_bus()` — 模式参考 `touch.c`
|
||||||
|
2. 读 WHO_AM_I,期望值 **`0x05`**(实现前对照 QMI8658A 数据手册确认)
|
||||||
|
3. 软复位 → 配置 acc:±8g,ODR **100 Hz**
|
||||||
|
4. 启动 `imu_task`(10 ms 周期读数,或按 ODR 对齐)
|
||||||
|
|
||||||
|
Phase 1 采用 **轮询**;INT1/INT2 在 Phase 4 启用。
|
||||||
|
|
||||||
|
### 2.4 QMI8658 寄存器(MVP 最小集)
|
||||||
|
|
||||||
|
> 完整寄存器表以实现阶段对照厂商数据手册为准;下表为 spec 占位,便于 Phase 1 编码。
|
||||||
|
|
||||||
|
| 寄存器 | 地址 | MVP 用途 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| WHO_AM_I | 0x00 | 设备识别 |
|
||||||
|
| CTRL1 | 0x02 | 软复位 / 使能 |
|
||||||
|
| CTRL2 | 0x03 | acc 量程、ODR |
|
||||||
|
| ACC_X_L … ACC_Z_H | 0x35–0x3A | 6 字节 burst 读 |
|
||||||
|
|
||||||
|
I2C 7-bit 地址:**`0x6B`**(SDO 默认;若硬件拉低则为 `0x6A`,真机确认)。
|
||||||
|
|
||||||
|
### 2.5 坐标系与轴映射
|
||||||
|
|
||||||
|
IMU 芯片坐标系与 PCB 安装方向、LVGL 横屏逻辑坐标 **不一定一致**。MVP 算法使用 **合成幅值**(与轴无关),不依赖单轴方向;Phase 4 若做方向性摇一摇再建立映射表。
|
||||||
|
|
||||||
|
**真机标定步骤(Phase 2 验收):**
|
||||||
|
|
||||||
|
1. 设备平放静止 2 s,串口打印 `ax, ay, az`,确认合成幅值 ≈ 1g
|
||||||
|
2. 分别沿 X/Y/Z 快速平移,确认对应轴响应最大
|
||||||
|
3. 记录 `g_ref` baseline,写入 `motion_gesture` 启动校准
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 系统架构
|
||||||
|
|
||||||
|
### 3.1 模块划分
|
||||||
|
|
||||||
|
```text
|
||||||
|
main/bsp/
|
||||||
|
├── board_config.h # IMU 引脚 / I2C 宏
|
||||||
|
├── imu.c / imu.h # I2C + QMI8658 驱动 + imu_task
|
||||||
|
|
||||||
|
main/app/
|
||||||
|
├── ui_nav.c / ui_nav.h # 屏幕轮播状态机
|
||||||
|
├── motion_gesture.c / .h # 摇一摇检测、cooldown、配置同步
|
||||||
|
└── ui_handlers.c # 可选:nav 事件注册
|
||||||
|
|
||||||
|
main/ui/settings/
|
||||||
|
├── settings_types.h # shake_nav_enabled, shake_sensitivity
|
||||||
|
├── settings_store.c # NVS shake_en / shake_sens
|
||||||
|
└── settings_tabs.c # System Tab 新增控件
|
||||||
|
|
||||||
|
main/app/app_main.c # 初始化顺序扩展
|
||||||
|
main/CMakeLists.txt # 追加源文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 架构图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph bsp [BSP Layer]
|
||||||
|
IMU["imu.c"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph app [App Layer]
|
||||||
|
Gesture["motion_gesture.c"]
|
||||||
|
Nav["ui_nav.c"]
|
||||||
|
Queue["nav_queue"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ui [UI Layer]
|
||||||
|
Screens["ui_Settings / Screen1/2/3"]
|
||||||
|
Store["settings_store.c"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph lvgl [LVGL Port]
|
||||||
|
Lock["lvgl_port_lock"]
|
||||||
|
LVGLTask["lvgl_port_task"]
|
||||||
|
end
|
||||||
|
|
||||||
|
IMU -->|"100Hz samples"| Gesture
|
||||||
|
Store -->|"apply enabled/sens"| Gesture
|
||||||
|
Gesture -->|"UI_NAV_NEXT"| Queue
|
||||||
|
Queue --> Nav
|
||||||
|
Nav --> Lock
|
||||||
|
Lock --> Screens
|
||||||
|
Screens --> LVGLTask
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 摇一摇切页时序
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 用户
|
||||||
|
participant IMU as imu_task
|
||||||
|
participant Gesture as motion_gesture
|
||||||
|
participant Queue as nav_queue
|
||||||
|
participant Nav as ui_nav
|
||||||
|
participant LVGL as lvgl_port_task
|
||||||
|
|
||||||
|
User->>IMU: 摇晃设备
|
||||||
|
IMU->>Gesture: accel sample 100Hz
|
||||||
|
Gesture->>Gesture: mag > threshold + min_samples
|
||||||
|
Gesture->>Gesture: cooldown check + enabled check
|
||||||
|
Gesture->>Queue: UI_NAV_NEXT
|
||||||
|
Queue->>Nav: ui_nav_next()
|
||||||
|
Nav->>LVGL: lvgl_port_lock()
|
||||||
|
Nav->>LVGL: lv_screen_load_anim next screen
|
||||||
|
Nav->>LVGL: lvgl_port_unlock()
|
||||||
|
LVGL->>User: 显示下一页
|
||||||
|
```
|
||||||
|
|
||||||
|
**线程间通信:** 参考 `main/bsp/boot_button.c` 的 `GPIO ISR → xQueueSend → dedicated task` 模式;`motion_gesture` 检测到 shake 后向 `nav_queue` 投递事件,由 `nav_handler_task` 调用 `ui_nav_next()`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 摇一摇检测算法
|
||||||
|
|
||||||
|
### 4.1 信号处理
|
||||||
|
|
||||||
|
1. **Baseline 校准:** 启动后 500 ms 内设备保持静止,采集加速度均值 `g_ref`(3 轴)
|
||||||
|
2. **动态量:** `a_dyn = a_raw - g_ref`
|
||||||
|
3. **合成幅值:** `mag = sqrt(ax² + ay² + az²)`(`ax/ay/az` 为 `a_dyn` 分量)
|
||||||
|
|
||||||
|
可选替代(Phase 4):一阶低通 `a_lp` + 高通 `a_hp = a_raw - a_lp`,对缓慢倾斜更不敏感。
|
||||||
|
|
||||||
|
### 4.2 判定状态机
|
||||||
|
|
||||||
|
```text
|
||||||
|
mag > threshold
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
v │
|
||||||
|
┌──────┐ 连续 ≥ min_samples ┌─────────────────┐
|
||||||
|
│ IDLE │ ──────────────────────> │ SHAKE_DETECTED │
|
||||||
|
└──────┘ └────────┬────────┘
|
||||||
|
^ │
|
||||||
|
│ cooldown_ms 到期 │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
v
|
||||||
|
post UI_NAV_NEXT(若 enabled)
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
|
||||||
|
- 仅在 `SHAKE_DETECTED` 上升沿投递 **一次** `UI_NAV_NEXT`
|
||||||
|
- `cooldown_ms` 内忽略后续峰值,防止一次猛摇连切多页
|
||||||
|
- `shake_nav_enabled == false` 时状态机仍可运行,但不投递事件
|
||||||
|
|
||||||
|
### 4.3 默认参数
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `sample_rate` | 100 Hz | acc ODR,与 imu_task 一致 |
|
||||||
|
| `threshold` | 0.8 g | 合成幅值阈值 |
|
||||||
|
| `threshold_low` | 0.6 g | 灵敏度 Low |
|
||||||
|
| `threshold_med` | 0.8 g | 灵敏度 Med(默认) |
|
||||||
|
| `threshold_high` | 1.0 g | 灵敏度 High |
|
||||||
|
| `window_ms` | 300 ms | 超阈值须在此窗口内形成有效峰值 |
|
||||||
|
| `min_samples` | 5 | 连续超阈值样本数(100 Hz 下 ≈ 50 ms) |
|
||||||
|
| `cooldown_ms` | 800 ms | 两次有效摇一摇最小间隔 |
|
||||||
|
| `calibration_ms` | 500 ms | 启动 baseline 采集时长 |
|
||||||
|
|
||||||
|
### 4.4 motion_gesture API
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
MOTION_NAV_NEXT = 0,
|
||||||
|
} motion_nav_event_t;
|
||||||
|
|
||||||
|
typedef void (*motion_nav_cb_t)(motion_nav_event_t event);
|
||||||
|
|
||||||
|
void motion_gesture_init(motion_nav_cb_t cb);
|
||||||
|
void motion_gesture_set_enabled(bool enabled);
|
||||||
|
void motion_gesture_set_sensitivity(shake_sensitivity_t sens);
|
||||||
|
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g);
|
||||||
|
```
|
||||||
|
|
||||||
|
`motion_gesture_on_sample()` 由 `imu_task` 每样本调用;内部维护状态机与 cooldown 计时。
|
||||||
|
|
||||||
|
### 4.5 误触与边界
|
||||||
|
|
||||||
|
| 场景 | MVP 策略 |
|
||||||
|
|------|----------|
|
||||||
|
| 敲桌面 | 提高 threshold 或选 Med/High 灵敏度 |
|
||||||
|
| 线缆拉扯 | cooldown 防止连切 |
|
||||||
|
| 手持走路 | 可能误触;Phase 4 可加「须从 IDLE 静止 ≥ 200 ms 才武装」 |
|
||||||
|
| Settings 调 slider 时轻微抖动 | 合成幅值阈值通常高于手指微动 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI 页面导航(ui_nav)
|
||||||
|
|
||||||
|
### 5.1 屏幕注册表
|
||||||
|
|
||||||
|
轮播顺序与 §1.2 一致:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "screens/ui_Settings.h"
|
||||||
|
#include "screens/ui_Screen1.h"
|
||||||
|
#include "screens/ui_Screen2.h"
|
||||||
|
#include "screens/ui_Screen3.h"
|
||||||
|
|
||||||
|
static lv_obj_t * const s_nav_screens[] = {
|
||||||
|
ui_Settings,
|
||||||
|
ui_Screen1,
|
||||||
|
ui_Screen2,
|
||||||
|
ui_Screen3,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define NAV_SCREEN_COUNT (sizeof(s_nav_screens) / sizeof(s_nav_screens[0]))
|
||||||
|
```
|
||||||
|
|
||||||
|
索引:0=Settings,1=Screen1,2=Screen2,3=Screen3。
|
||||||
|
|
||||||
|
### 5.2 公开 API
|
||||||
|
|
||||||
|
```c
|
||||||
|
void ui_nav_init(void);
|
||||||
|
void ui_nav_next(void); /* 线程安全:内部 lvgl_port_lock */
|
||||||
|
void ui_nav_prev(void); /* Phase 4 预留;MVP 不实现 */
|
||||||
|
void ui_nav_goto(uint8_t index); /* 可选:调试 / 恢复索引 */
|
||||||
|
|
||||||
|
lv_obj_t *ui_nav_current(void);
|
||||||
|
uint8_t ui_nav_current_index(void);
|
||||||
|
const char *ui_nav_current_name(void); /* 日志用:"Settings"/"Screen1"/... */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 切屏实现
|
||||||
|
|
||||||
|
- 使用现有 `_ui_screen_change()`(`main/ui/ui_helpers.c`)或等价 `lv_screen_load_anim()`
|
||||||
|
- 动画:`LV_SCR_LOAD_ANIM_FADE_IN`,时长 **300 ms**,delay **0**
|
||||||
|
- `ui_nav_next()`:`index = (index + 1) % NAV_SCREEN_COUNT`
|
||||||
|
- 内部流程:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void ui_nav_next(void)
|
||||||
|
{
|
||||||
|
lvgl_port_lock();
|
||||||
|
s_nav_index = (s_nav_index + 1) % NAV_SCREEN_COUNT;
|
||||||
|
lv_obj_t **target = (lv_obj_t **)&s_nav_screens[s_nav_index];
|
||||||
|
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
|
||||||
|
lvgl_port_unlock();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:`s_nav_screens` 元素在 `ui_init()` 后已非 NULL,`_ui_screen_change` 的 `target_init` 传 `NULL` 即可。
|
||||||
|
|
||||||
|
### 5.4 与 Settings Tab 的边界
|
||||||
|
|
||||||
|
| 行为 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 摇一摇切屏 | 切换 **LVGL Screen**(整屏),不切换 Settings 内 Sidebar Tab |
|
||||||
|
| Settings 内 Tab | 仍仅由 Sidebar 点击切换;遵循 `settings-screen-landscape.md` §3.3「切换 Tab 前先 collect」 |
|
||||||
|
| 摇离 Settings | **不**自动 `settings_tabs_collect()`;内存 struct 保留未 Save 的改动(与现有跨 Tab 行为一致) |
|
||||||
|
| 从 Settings 摇一摇 | 下一屏为 **Screen1** |
|
||||||
|
|
||||||
|
### 5.5 Phase 1 无 IMU 时的验证路径
|
||||||
|
|
||||||
|
在 `ui_nav` 就绪后、IMU 驱动完成前,将 **BOOT 键** 或 **KEY_IO(GPIO4)** 短按映射为 `ui_nav_next()`,验证四屏轮播与 fade 动画,不阻塞 Phase 2。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 数据模型与 NVS
|
||||||
|
|
||||||
|
### 6.1 类型扩展
|
||||||
|
|
||||||
|
在 `main/ui/settings/settings_types.h` 追加:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
SHAKE_SENS_LOW = 0,
|
||||||
|
SHAKE_SENS_MED,
|
||||||
|
SHAKE_SENS_HIGH,
|
||||||
|
} shake_sensitivity_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool notification_enabled;
|
||||||
|
uint8_t brightness_pct;
|
||||||
|
uint8_t volume_pct;
|
||||||
|
bool mute;
|
||||||
|
bool shake_nav_enabled; /* 默认 true */
|
||||||
|
shake_sensitivity_t shake_sensitivity; /* 默认 SHAKE_SENS_MED */
|
||||||
|
} settings_data_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `settings_tabs.h` 的 `settings_field_t` 追加:
|
||||||
|
|
||||||
|
```c
|
||||||
|
SETTINGS_FIELD_SHAKE_NAV,
|
||||||
|
SETTINGS_FIELD_SHAKE_SENS,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 默认值
|
||||||
|
|
||||||
|
```c
|
||||||
|
static const settings_data_t SETTINGS_DEFAULTS = {
|
||||||
|
.notification_enabled = false,
|
||||||
|
.brightness_pct = 70,
|
||||||
|
.volume_pct = 50,
|
||||||
|
.mute = false,
|
||||||
|
.shake_nav_enabled = true,
|
||||||
|
.shake_sensitivity = SHAKE_SENS_MED,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 NVS Schema
|
||||||
|
|
||||||
|
Namespace:`'settings'`(与现有字段共用)。
|
||||||
|
|
||||||
|
| Key | 类型 | 字段 | 默认 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| `shake_en` | u8 | `shake_nav_enabled` | 1 |
|
||||||
|
| `shake_sens` | u8 | `shake_sensitivity` | 1(Med) |
|
||||||
|
|
||||||
|
`settings_store_load()` / `commit()` / `factory_reset()` 同步读写上述 key。
|
||||||
|
|
||||||
|
### 6.4 settings_store_apply 扩展
|
||||||
|
|
||||||
|
```c
|
||||||
|
void settings_store_apply(void)
|
||||||
|
{
|
||||||
|
/* 现有背光逻辑 */
|
||||||
|
uint16_t duty = (uint16_t)((s_data.brightness_pct * 1023U) / 100U);
|
||||||
|
display_backlight_set_brightness(duty);
|
||||||
|
|
||||||
|
/* 新增 */
|
||||||
|
motion_gesture_set_enabled(s_data.shake_nav_enabled);
|
||||||
|
motion_gesture_set_sensitivity(s_data.shake_sensitivity);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**生效时机:**
|
||||||
|
|
||||||
|
- 启动:`settings_store_load()` 后 `settings_store_apply()`
|
||||||
|
- Save 按钮:`settings_store_commit()` 后 `settings_store_apply()`
|
||||||
|
- Factory Reset:恢复默认后 `apply()`
|
||||||
|
|
||||||
|
Switch / Roller 的 `VALUE_CHANGED` **仅更新内存**;与现有 notification / mute 行为一致,Save 后持久化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Settings UI 变更
|
||||||
|
|
||||||
|
### 7.1 System Tab 新增控件
|
||||||
|
|
||||||
|
在 `load_system_settings()` 中,于 Factory Reset 按钮 **上方** 追加一行(y 续排,避免与 y=112 的 Reset 重叠):
|
||||||
|
|
||||||
|
| 控件 | x | y | 宽 | 高 | NVS 字段 | 行为 |
|
||||||
|
|------|---|---|----|----|----------|------|
|
||||||
|
| Label "Shake Navigation" | 4 | 40 | 120 | 28 | — | 静态文本 |
|
||||||
|
| Switch | 180 | 40 | 40 | 28 | `shake_nav_enabled` | `SETTINGS_FIELD_SHAKE_NAV` |
|
||||||
|
| Label "Shake Sensitivity" | 4 | 76 | 120 | 28 | — | 静态文本 |
|
||||||
|
| Dropdown | 120 | 76 | 100 | 28 | `shake_sensitivity` | 选项 `"Low\nMed\nHigh"`,`SETTINGS_FIELD_SHAKE_SENS` |
|
||||||
|
|
||||||
|
Factory Reset 按钮保持 **y=112** 不变(与 `settings-screen-landscape.md` §5.4 兼容,新增项插入中间行)。
|
||||||
|
|
||||||
|
### 7.2 settings_tabs_collect 扩展
|
||||||
|
|
||||||
|
`SETTINGS_TAB_SYSTEM` 分支增加:
|
||||||
|
|
||||||
|
```c
|
||||||
|
if (s_shake_nav_switch != NULL) {
|
||||||
|
out->shake_nav_enabled = lv_obj_has_state(s_shake_nav_switch, LV_STATE_CHECKED);
|
||||||
|
}
|
||||||
|
if (s_shake_sens_dropdown != NULL) {
|
||||||
|
out->shake_sensitivity = (shake_sensitivity_t)lv_dropdown_get_selected(s_shake_sens_dropdown);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 ui_handlers 扩展
|
||||||
|
|
||||||
|
`settings_on_value_changed()` 增加 `SETTINGS_FIELD_SHAKE_NAV` / `SETTINGS_FIELD_SHAKE_SENS` case,更新 `settings_store` 内存 struct。
|
||||||
|
|
||||||
|
**不在 VALUE_CHANGED 时调用 `motion_gesture_set_*`:** 与 brightness 类似,Save + `apply()` 后统一生效;若需即时预览开关,可在 Save 规范外额外 `apply()`(Phase 2 可选)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 事件与集成
|
||||||
|
|
||||||
|
### 8.1 初始化顺序
|
||||||
|
|
||||||
|
扩展 `main/app/app_main.c`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. nvs_flash_init()
|
||||||
|
2. display_init()
|
||||||
|
3. lvgl_port_init()
|
||||||
|
4. touch_init() [若 BOARD_TOUCH_ENABLED]
|
||||||
|
5. lvgl_port_lock → ui_init() → ui_nav_init() → lvgl_port_unlock
|
||||||
|
6. settings_store_load()
|
||||||
|
7. settings_store_apply() [背光 + motion 配置]
|
||||||
|
8. ui_handlers_init()
|
||||||
|
9. motion_gesture_init(on_motion_nav) [注册 ui_nav_next 回调]
|
||||||
|
10. imu_init() [启动 imu_task]
|
||||||
|
11. ble_manager_init()
|
||||||
|
```
|
||||||
|
|
||||||
|
`ui_nav_init()` 须将 `s_nav_index` 与当前 loaded screen 同步(默认 Settings → index 0)。
|
||||||
|
|
||||||
|
### 8.2 nav_handler_task
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void nav_handler_task(void *arg)
|
||||||
|
{
|
||||||
|
motion_nav_event_t ev;
|
||||||
|
while (xQueueReceive(s_nav_queue, &ev, portMAX_DELAY) == pdTRUE) {
|
||||||
|
if (ev == MOTION_NAV_NEXT) {
|
||||||
|
ui_nav_next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Queue 深度建议 **4**;`motion_gesture` 在 cooldown 内不应重复投递。
|
||||||
|
|
||||||
|
### 8.3 CMakeLists.txt 变更
|
||||||
|
|
||||||
|
在 `main/CMakeLists.txt` 的 `SRCS` 追加:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"bsp/imu.c"
|
||||||
|
"app/ui_nav.c"
|
||||||
|
"app/motion_gesture.c"
|
||||||
|
```
|
||||||
|
|
||||||
|
`INCLUDE_DIRS` 已含 `app`、`bsp`,无需新增。
|
||||||
|
|
||||||
|
### 8.4 任务优先级与栈
|
||||||
|
|
||||||
|
| Task | 优先级 | 栈 | 说明 |
|
||||||
|
|------|--------|-----|------|
|
||||||
|
| `LVGL` | 2 | 8 KB | 已有 |
|
||||||
|
| `imu_task` | 4 | 3 KB | I2C 读 + 回调 gesture |
|
||||||
|
| `nav_handler` | 5 | 2 KB | Queue 消费,调用 ui_nav |
|
||||||
|
| `boot_btn` | 5 | 2 KB | 已有,Phase 1 可复用测 nav |
|
||||||
|
|
||||||
|
IMU 优先级低于 nav_handler,避免 I2C 阻塞切页响应。
|
||||||
|
|
||||||
|
### 8.5 调试日志
|
||||||
|
|
||||||
|
| Tag | 内容 |
|
||||||
|
|-----|------|
|
||||||
|
| `imu` | WHO_AM_I、init 失败 |
|
||||||
|
| `motion` | shake 触发、mag 峰值(`CONFIG_LOG_DEFAULT_LEVEL` DEBUG 下) |
|
||||||
|
| `ui_nav` | index 变化、screen 名称 |
|
||||||
|
|
||||||
|
Phase 1 可启用 mag 峰值串口输出,便于真机调 threshold。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 实现步骤
|
||||||
|
|
||||||
|
### 阶段 0:规格审核
|
||||||
|
|
||||||
|
| 步骤 | 内容 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| 0.1 | 审核本文档 v1.0 | 待确认 |
|
||||||
|
| 0.2 | 确认四屏轮播顺序与「任意摇→下一页」 | 已确认 |
|
||||||
|
|
||||||
|
### 阶段 1:ui_nav + IMU 驱动基础
|
||||||
|
|
||||||
|
| 步骤 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 1.1 | `board_config.h` 增加 IMU 宏 |
|
||||||
|
| 1.2 | 新建 `ui_nav.c`:注册表、init、next、lock 包装 |
|
||||||
|
| 1.3 | BOOT 或 KEY 短按 → `ui_nav_next()`,真机验证四屏 fade 轮播 |
|
||||||
|
| 1.4 | 新建 `imu.c`:I2C + WHO_AM_I + acc 读数 |
|
||||||
|
| 1.5 | 串口打印 100 Hz acc;确认 WHO_AM_I 与 1g 静止读数 |
|
||||||
|
|
||||||
|
**Phase 1 验收:** AC-2 用手动按键代替摇一摇通过;AC-1 通过。
|
||||||
|
|
||||||
|
### 阶段 2:motion_gesture + Queue 切页
|
||||||
|
|
||||||
|
| 步骤 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 2.1 | 新建 `motion_gesture.c`:baseline、状态机、cooldown |
|
||||||
|
| 2.2 | `nav_queue` + `nav_handler_task` → `ui_nav_next()` |
|
||||||
|
| 2.3 | `imu_task` 调用 `motion_gesture_on_sample()` |
|
||||||
|
| 2.4 | 真机调 threshold / cooldown,满足 AC-3 |
|
||||||
|
|
||||||
|
**Phase 2 验收:** AC-2、AC-3、AC-7、AC-8。
|
||||||
|
|
||||||
|
### 阶段 3:Settings + NVS
|
||||||
|
|
||||||
|
| 步骤 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 3.1 | 扩展 `settings_types.h`、`settings_store.c` |
|
||||||
|
| 3.2 | System Tab UI + collect + ui_handlers |
|
||||||
|
| 3.3 | `settings_store_apply()` 同步 motion 配置 |
|
||||||
|
| 3.4 | Factory Reset 验证 motion 默认值 |
|
||||||
|
|
||||||
|
**Phase 3 验收:** AC-4、AC-5、AC-6。
|
||||||
|
|
||||||
|
### 阶段 4:增强(可选)
|
||||||
|
|
||||||
|
| 步骤 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 4.1 | INT1 Data Ready,降低轮询频率 |
|
||||||
|
| 4.2 | `ui_nav_prev()` + 方向性摇一摇 |
|
||||||
|
| 4.3 | 屏幕底部页码 dot(4 屏指示) |
|
||||||
|
| 4.4 | 静止武装窗口,减少走路误触 |
|
||||||
|
| 4.5 | VALUE_CHANGED 即时 apply shake 开关(免 Save 预览) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 验收标准
|
||||||
|
|
||||||
|
| 编号 | 验收项 | 标准 |
|
||||||
|
|------|--------|------|
|
||||||
|
| AC-1 | IMU 通信 | WHO_AM_I = `0x05`;静止时合成幅值 ≈ 0.05–0.15g(去 baseline 后) |
|
||||||
|
| AC-2 | 轮播顺序 | 连续 4 次有效摇一摇:Settings → Screen1 → Screen2 → Screen3 → Settings |
|
||||||
|
| AC-3 | Cooldown | 单次持续摇晃只触发 **1 次** 切页 |
|
||||||
|
| AC-4 | 禁用开关 | Settings 中关闭 Shake Navigation 并 Save 后,摇一摇不切页 |
|
||||||
|
| AC-5 | 灵敏度 | 相同力度下 High 比 Low 更易触发(至少能感知差异) |
|
||||||
|
| AC-6 | NVS | 开关与灵敏度 Save 后重启仍保持 |
|
||||||
|
| AC-7 | 线程安全 | 连续摇切 50 次无 crash、无 LVGL assert |
|
||||||
|
| AC-8 | HID 无干扰 | 在 Screen2/3 摇切页时,不发送 BLE HID 报告 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 风险与约束
|
||||||
|
|
||||||
|
1. **I2C 冲突:** IMU 与触摸必须分 bus;禁止将 QMI8658 挂到 `I2C_NUM_0` 与 CST816 共用。
|
||||||
|
2. **WHO_AM_I / 地址:** 硬件 SDO 接法可能导致 `0x6A`;init 失败时优先排查。
|
||||||
|
3. **轴映射:** MVP 用合成幅值,方向错误仅影响 Phase 4 方向性手势,不影响 MVP。
|
||||||
|
4. **Settings 未 Save 切屏:** 摇离 Settings 不 collect Tab;用户未 Save 的改动仍在内存,重启丢失——与现有 Settings 行为一致,需在 UI 文案或后续 Save 提示中考虑(非 MVP)。
|
||||||
|
5. **CPU / 功耗:** 100 Hz 轮询 + I2C 读增加功耗;电池场景 Phase 4 改 INT 驱动。
|
||||||
|
6. **Screen 布局:** Screen1/2/3 可能仍为 SquareLine 竖屏坐标,切页后布局可能错位;属 `settings-screen-landscape.md` Phase 3,不阻塞本 spec MVP。
|
||||||
|
7. **误触:** 敲桌、走路仍可能触发;MVP 靠 threshold + cooldown,不做机器学习。
|
||||||
|
8. **Factory Reset 范围:** 与 Settings spec 一致,Phase 1 仅擦除 `"settings"` namespace;BLE bonding 等保留。
|
||||||
|
9. **陀螺仪:** MVP 不读 gyro;减少驱动复杂度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 参考
|
||||||
|
|
||||||
|
- 硬件引脚:`README.md` IMU 表
|
||||||
|
- 板级配置:`main/bsp/board_config.h`
|
||||||
|
- 触摸 I2C 范例:`main/bsp/touch.c`
|
||||||
|
- LVGL 线程锁:`main/bsp/lvgl_port.c`(`lvgl_port_lock` / `unlock`)
|
||||||
|
- 按键 Queue 范例:`main/bsp/boot_button.c`
|
||||||
|
- UI 入口与默认屏:`main/ui/ui.c`
|
||||||
|
- 切屏辅助: `main/ui/ui_helpers.c`(`_ui_screen_change`)
|
||||||
|
- Settings 数据:`main/ui/settings/settings_store.c`
|
||||||
|
- 姊妹规格:`docs/spec/settings-screen-landscape.md`(Phase 3 多屏导航由本 spec `ui_nav` 承接)
|
||||||
|
- 姊妹规格:`docs/spec/ble-hid-keyboard.md`(HID 与切页独立)
|
||||||
|
- LVGL 9.2:`lv_screen_load_anim()`、`LV_SCR_LOAD_ANIM_FADE_IN`
|
||||||
|
- QMI8658A 数据手册(寄存器与 WHO_AM_I,实现 Phase 1 前必读)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修订记录
|
||||||
|
|
||||||
|
| 版本 | 日期 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1.0 | 2026-07-05 | 初稿:四屏循环、任意摇→下一页、QMI8658 BSP、motion_gesture、ui_nav、Settings/NVS、分阶段实现与 AC-1~AC-8 |
|
||||||
+10
-1
@@ -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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
#include "motion_gesture.h"
|
||||||
|
|
||||||
|
#include "ui_nav.h"
|
||||||
|
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
static const char *TAG = "motion";
|
||||||
|
|
||||||
|
#define SAMPLE_PERIOD_MS 10
|
||||||
|
#define SAMPLE_RATE_HZ 100
|
||||||
|
|
||||||
|
/* Calibration: keep a sliding window of the last 500 ms and finish only when
|
||||||
|
* the window is stable (low variance). This avoids calibrating while the user
|
||||||
|
* is picking up or moving the device. */
|
||||||
|
#define CALIBRATION_MS 500
|
||||||
|
#define CALIBRATION_WINDOW_SAMPLES (CALIBRATION_MS / SAMPLE_PERIOD_MS)
|
||||||
|
#define CALIBRATION_MAX_VAR_G2 0.010f
|
||||||
|
|
||||||
|
/* Shake is detected on the horizontal plane (X/Y) so that picking the device
|
||||||
|
* up or putting it down (Z-axis jolt) does not trigger navigation. */
|
||||||
|
#define THRESHOLD_LOW_G 0.6f
|
||||||
|
#define THRESHOLD_MED_G 0.8f
|
||||||
|
#define THRESHOLD_HIGH_G 1.0f
|
||||||
|
|
||||||
|
/* A valid shake must produce enough strong samples within a short window. */
|
||||||
|
#define WINDOW_MS 300
|
||||||
|
#define MIN_SAMPLES 5
|
||||||
|
|
||||||
|
/* A real shake oscillates; require at least two zero-crossings on the
|
||||||
|
* dominant horizontal axis to distinguish it from a single jolt. */
|
||||||
|
#define MIN_ZERO_CROSSINGS 2
|
||||||
|
|
||||||
|
/* Cooldown prevents one physical shake from generating multiple events. */
|
||||||
|
#define COOLDOWN_MS 350
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GESTURE_IDLE = 0,
|
||||||
|
GESTURE_COOLDOWN,
|
||||||
|
} gesture_state_t;
|
||||||
|
|
||||||
|
static QueueHandle_t s_nav_queue;
|
||||||
|
static bool s_enabled = true;
|
||||||
|
static float s_threshold = THRESHOLD_MED_G;
|
||||||
|
static gesture_state_t s_state = GESTURE_IDLE;
|
||||||
|
|
||||||
|
/* Baseline calibration */
|
||||||
|
static float s_ref_x;
|
||||||
|
static float s_ref_y;
|
||||||
|
static float s_ref_z;
|
||||||
|
static bool s_calibrated;
|
||||||
|
|
||||||
|
static float s_cal_buf_x[CALIBRATION_WINDOW_SAMPLES];
|
||||||
|
static float s_cal_buf_y[CALIBRATION_WINDOW_SAMPLES];
|
||||||
|
static float s_cal_buf_z[CALIBRATION_WINDOW_SAMPLES];
|
||||||
|
static int s_cal_head;
|
||||||
|
static int s_cal_count;
|
||||||
|
|
||||||
|
/* Detection window state */
|
||||||
|
static int s_over_count;
|
||||||
|
static int64_t s_window_start_us;
|
||||||
|
static int s_zero_crossings;
|
||||||
|
static int s_last_x_sign;
|
||||||
|
static int s_last_y_sign;
|
||||||
|
static int64_t s_cooldown_until_us;
|
||||||
|
|
||||||
|
/* Last horizontal magnitude, exposed for UI/debug. */
|
||||||
|
static float s_last_horizontal_mag;
|
||||||
|
|
||||||
|
static void nav_handler_task(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
motion_nav_event_t ev;
|
||||||
|
|
||||||
|
while (xQueueReceive(s_nav_queue, &ev, portMAX_DELAY) == pdTRUE) {
|
||||||
|
if (ev == MOTION_NAV_NEXT) {
|
||||||
|
ui_nav_next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void post_nav_next(void)
|
||||||
|
{
|
||||||
|
motion_nav_event_t ev = MOTION_NAV_NEXT;
|
||||||
|
if (xQueueSend(s_nav_queue, &ev, 0) != pdTRUE) {
|
||||||
|
ESP_LOGW(TAG, "nav queue full");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Shake detected -> next screen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset_window(void)
|
||||||
|
{
|
||||||
|
s_over_count = 0;
|
||||||
|
s_window_start_us = 0;
|
||||||
|
s_zero_crossings = 0;
|
||||||
|
s_last_x_sign = 0;
|
||||||
|
s_last_y_sign = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run_calibration(float ax, float ay, float az)
|
||||||
|
{
|
||||||
|
s_cal_buf_x[s_cal_head] = ax;
|
||||||
|
s_cal_buf_y[s_cal_head] = ay;
|
||||||
|
s_cal_buf_z[s_cal_head] = az;
|
||||||
|
s_cal_head = (s_cal_head + 1) % CALIBRATION_WINDOW_SAMPLES;
|
||||||
|
if (s_cal_count < CALIBRATION_WINDOW_SAMPLES) {
|
||||||
|
s_cal_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_cal_count < CALIBRATION_WINDOW_SAMPLES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float sum_x = 0.0f;
|
||||||
|
float sum_y = 0.0f;
|
||||||
|
float sum_z = 0.0f;
|
||||||
|
float sum_sq_x = 0.0f;
|
||||||
|
float sum_sq_y = 0.0f;
|
||||||
|
float sum_sq_z = 0.0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < CALIBRATION_WINDOW_SAMPLES; i++) {
|
||||||
|
sum_x += s_cal_buf_x[i];
|
||||||
|
sum_y += s_cal_buf_y[i];
|
||||||
|
sum_z += s_cal_buf_z[i];
|
||||||
|
sum_sq_x += s_cal_buf_x[i] * s_cal_buf_x[i];
|
||||||
|
sum_sq_y += s_cal_buf_y[i] * s_cal_buf_y[i];
|
||||||
|
sum_sq_z += s_cal_buf_z[i] * s_cal_buf_z[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const int n = CALIBRATION_WINDOW_SAMPLES;
|
||||||
|
float var_x = (sum_sq_x / n) - (sum_x / n) * (sum_x / n);
|
||||||
|
float var_y = (sum_sq_y / n) - (sum_y / n) * (sum_y / n);
|
||||||
|
float var_z = (sum_sq_z / n) - (sum_z / n) * (sum_z / n);
|
||||||
|
float max_var = fmaxf(var_x, fmaxf(var_y, var_z));
|
||||||
|
|
||||||
|
if (max_var > CALIBRATION_MAX_VAR_G2) {
|
||||||
|
static int64_t s_last_cal_warn_us;
|
||||||
|
int64_t now_us = esp_timer_get_time();
|
||||||
|
if ((now_us - s_last_cal_warn_us) >= 1000 * 1000) {
|
||||||
|
s_last_cal_warn_us = now_us;
|
||||||
|
ESP_LOGW(TAG, "Calibration: motion detected (var=%.3f), waiting", max_var);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ref_x = sum_x / n;
|
||||||
|
s_ref_y = sum_y / n;
|
||||||
|
s_ref_z = sum_z / n;
|
||||||
|
s_calibrated = true;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Baseline calibrated (ref=%.3f,%.3f,%.3f g)", s_ref_x, s_ref_y, s_ref_z);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int axis_sign(float value)
|
||||||
|
{
|
||||||
|
if (value > 0.0f) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (value < 0.0f) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_sample(float ax, float ay, float az)
|
||||||
|
{
|
||||||
|
int64_t now_us = esp_timer_get_time();
|
||||||
|
|
||||||
|
if (!s_calibrated) {
|
||||||
|
run_calibration(ax, ay, az);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dx = ax - s_ref_x;
|
||||||
|
float dy = ay - s_ref_y;
|
||||||
|
float dz = az - s_ref_z;
|
||||||
|
|
||||||
|
/* Use only horizontal magnitude for shake detection. Z-axis is dominated by
|
||||||
|
* gravity and is the main axis for pick-up/put-down false triggers. */
|
||||||
|
float horizontal_mag = sqrtf(dx * dx + dy * dy);
|
||||||
|
s_last_horizontal_mag = horizontal_mag;
|
||||||
|
|
||||||
|
if (s_state == GESTURE_COOLDOWN) {
|
||||||
|
if (now_us >= s_cooldown_until_us) {
|
||||||
|
s_state = GESTURE_IDLE;
|
||||||
|
reset_window();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (horizontal_mag > s_threshold) {
|
||||||
|
if (s_over_count == 0) {
|
||||||
|
s_window_start_us = now_us;
|
||||||
|
}
|
||||||
|
s_over_count++;
|
||||||
|
|
||||||
|
int x_sign = axis_sign(dx);
|
||||||
|
int y_sign = axis_sign(dy);
|
||||||
|
if (s_last_x_sign != 0 && x_sign != s_last_x_sign) {
|
||||||
|
s_zero_crossings++;
|
||||||
|
}
|
||||||
|
if (s_last_y_sign != 0 && y_sign != s_last_y_sign) {
|
||||||
|
s_zero_crossings++;
|
||||||
|
}
|
||||||
|
s_last_x_sign = x_sign;
|
||||||
|
s_last_y_sign = y_sign;
|
||||||
|
|
||||||
|
int64_t window_us = (int64_t)WINDOW_MS * 1000;
|
||||||
|
if ((now_us - s_window_start_us) > window_us) {
|
||||||
|
/* Window expired: start fresh with the current sample. */
|
||||||
|
reset_window();
|
||||||
|
s_window_start_us = now_us;
|
||||||
|
s_over_count = 1;
|
||||||
|
s_last_x_sign = axis_sign(dx);
|
||||||
|
s_last_y_sign = axis_sign(dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_over_count >= MIN_SAMPLES && s_zero_crossings >= MIN_ZERO_CROSSINGS) {
|
||||||
|
if (s_enabled) {
|
||||||
|
post_nav_next();
|
||||||
|
}
|
||||||
|
s_state = GESTURE_COOLDOWN;
|
||||||
|
s_cooldown_until_us = now_us + (int64_t)COOLDOWN_MS * 1000;
|
||||||
|
reset_window();
|
||||||
|
}
|
||||||
|
} else if (s_over_count > 0) {
|
||||||
|
int64_t window_us = (int64_t)WINDOW_MS * 1000;
|
||||||
|
if ((now_us - s_window_start_us) > window_us) {
|
||||||
|
reset_window();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)dz;
|
||||||
|
}
|
||||||
|
|
||||||
|
void motion_gesture_init(motion_nav_cb_t cb)
|
||||||
|
{
|
||||||
|
(void)cb;
|
||||||
|
|
||||||
|
s_nav_queue = xQueueCreate(4, sizeof(motion_nav_event_t));
|
||||||
|
xTaskCreate(nav_handler_task, "nav_handler", 2048, NULL, 5, NULL);
|
||||||
|
|
||||||
|
s_calibrated = false;
|
||||||
|
s_cal_head = 0;
|
||||||
|
s_cal_count = 0;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Motion gesture ready (calibrating %d ms)", CALIBRATION_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void motion_gesture_set_enabled(bool enabled)
|
||||||
|
{
|
||||||
|
s_enabled = enabled;
|
||||||
|
ESP_LOGI(TAG, "Shake navigation %s", enabled ? "enabled" : "disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void motion_gesture_set_sensitivity(shake_sensitivity_t sens)
|
||||||
|
{
|
||||||
|
switch (sens) {
|
||||||
|
case SHAKE_SENS_LOW:
|
||||||
|
s_threshold = THRESHOLD_LOW_G;
|
||||||
|
break;
|
||||||
|
case SHAKE_SENS_MED:
|
||||||
|
s_threshold = THRESHOLD_MED_G;
|
||||||
|
break;
|
||||||
|
case SHAKE_SENS_HIGH:
|
||||||
|
s_threshold = THRESHOLD_HIGH_G;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(false && "unexpected shake_sensitivity_t");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Sensitivity set (threshold=%.2f g)", s_threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g)
|
||||||
|
{
|
||||||
|
process_sample(ax_g, ay_g, az_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
float motion_gesture_get_last_magnitude(void)
|
||||||
|
{
|
||||||
|
return s_last_horizontal_mag;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool motion_gesture_is_calibrated(void)
|
||||||
|
{
|
||||||
|
return s_calibrated;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "settings_types.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MOTION_NAV_NEXT = 0,
|
||||||
|
} motion_nav_event_t;
|
||||||
|
|
||||||
|
typedef void (*motion_nav_cb_t)(motion_nav_event_t event);
|
||||||
|
|
||||||
|
void motion_gesture_init(motion_nav_cb_t cb);
|
||||||
|
void motion_gesture_set_enabled(bool enabled);
|
||||||
|
void motion_gesture_set_sensitivity(shake_sensitivity_t sens);
|
||||||
|
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g);
|
||||||
|
|
||||||
|
float motion_gesture_get_last_magnitude(void);
|
||||||
|
bool motion_gesture_is_calibrated(void);
|
||||||
+143
-2
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#include "ui_nav.h"
|
||||||
|
|
||||||
|
#include "lvgl_port.h"
|
||||||
|
#include "screens/ui_Screen1.h"
|
||||||
|
#include "screens/ui_Screen2.h"
|
||||||
|
#include "screens/ui_Screen3.h"
|
||||||
|
#include "screens/ui_Settings.h"
|
||||||
|
#include "ui_helpers.h"
|
||||||
|
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
static const char *TAG = "ui_nav";
|
||||||
|
|
||||||
|
static lv_obj_t *s_nav_screens[4];
|
||||||
|
static const char *const s_nav_names[] = {
|
||||||
|
"Settings",
|
||||||
|
"Screen1",
|
||||||
|
"Screen2",
|
||||||
|
"Screen3",
|
||||||
|
};
|
||||||
|
|
||||||
|
#define NAV_SCREEN_COUNT (sizeof(s_nav_screens) / sizeof(s_nav_screens[0]))
|
||||||
|
|
||||||
|
static uint8_t s_nav_index;
|
||||||
|
|
||||||
|
void ui_nav_init(void)
|
||||||
|
{
|
||||||
|
s_nav_screens[0] = ui_Settings;
|
||||||
|
s_nav_screens[1] = ui_Screen1;
|
||||||
|
s_nav_screens[2] = ui_Screen2;
|
||||||
|
s_nav_screens[3] = ui_Screen3;
|
||||||
|
s_nav_index = 0;
|
||||||
|
ESP_LOGI(TAG, "Navigation ready (%u screens, start=%s)", (unsigned)NAV_SCREEN_COUNT,
|
||||||
|
s_nav_names[s_nav_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_nav_goto(uint8_t index)
|
||||||
|
{
|
||||||
|
if (index >= NAV_SCREEN_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lvgl_port_lock();
|
||||||
|
s_nav_index = index;
|
||||||
|
lv_obj_t **target = &s_nav_screens[s_nav_index];
|
||||||
|
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
|
||||||
|
lvgl_port_unlock();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Goto %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_nav_next(void)
|
||||||
|
{
|
||||||
|
lvgl_port_lock();
|
||||||
|
s_nav_index = (s_nav_index + 1) % NAV_SCREEN_COUNT;
|
||||||
|
lv_obj_t **target = &s_nav_screens[s_nav_index];
|
||||||
|
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
|
||||||
|
lvgl_port_unlock();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Next -> %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t *ui_nav_current(void)
|
||||||
|
{
|
||||||
|
return s_nav_screens[s_nav_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t ui_nav_current_index(void)
|
||||||
|
{
|
||||||
|
return s_nav_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ui_nav_current_name(void)
|
||||||
|
{
|
||||||
|
return s_nav_names[s_nav_index];
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "lvgl.h"
|
||||||
|
|
||||||
|
void ui_nav_init(void);
|
||||||
|
void ui_nav_next(void);
|
||||||
|
void ui_nav_goto(uint8_t index);
|
||||||
|
|
||||||
|
lv_obj_t *ui_nav_current(void);
|
||||||
|
uint8_t ui_nav_current_index(void);
|
||||||
|
const char *ui_nav_current_name(void);
|
||||||
@@ -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
@@ -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
@@ -0,0 +1,308 @@
|
|||||||
|
#include "imu.h"
|
||||||
|
|
||||||
|
#include "board_config.h"
|
||||||
|
#include "touch.h"
|
||||||
|
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
static const char *TAG = "imu";
|
||||||
|
|
||||||
|
#if BOARD_IMU_ENABLED
|
||||||
|
|
||||||
|
#define QMI8658_ADDR_SDO_LOW 0x6A
|
||||||
|
#define QMI8658_ADDR_SDO_HIGH 0x6B
|
||||||
|
#define QMI8658_WHO_AM_I 0x00
|
||||||
|
#define QMI8658_WHO_AM_I_ID 0x05
|
||||||
|
#define QMI8658_CTRL1 0x02
|
||||||
|
#define QMI8658_CTRL2 0x03
|
||||||
|
#define QMI8658_CTRL3 0x04
|
||||||
|
#define QMI8658_CTRL5 0x06
|
||||||
|
#define QMI8658_CTRL6 0x07
|
||||||
|
#define QMI8658_CTRL7 0x08
|
||||||
|
#define QMI8658_RESET 0x60
|
||||||
|
#define QMI8658_ACC_X_L 0x35
|
||||||
|
|
||||||
|
#define QMI8658_RESET_VALUE 0xB0
|
||||||
|
/* Little-endian + address auto-increment (matches int16 parse below) */
|
||||||
|
#define QMI8658_CTRL1_VALUE 0x40
|
||||||
|
/* ±8g, 1000 Hz ODR */
|
||||||
|
#define QMI8658_CTRL2_VALUE 0x23
|
||||||
|
/* Gyro ±512 dps, 1000 Hz — required even when only reading accel */
|
||||||
|
#define QMI8658_CTRL3_VALUE 0x53
|
||||||
|
#define QMI8658_CTRL5_VALUE 0x11
|
||||||
|
#define QMI8658_CTRL6_VALUE 0x00
|
||||||
|
#define QMI8658_CTRL7_VALUE 0x03
|
||||||
|
|
||||||
|
#define QMI8658_ACCEL_SENS_8G 4096.0f
|
||||||
|
|
||||||
|
#define IMU_SAMPLE_PERIOD_MS 10
|
||||||
|
#define IMU_DEBUG_LOG_INTERVAL_MS 1000
|
||||||
|
|
||||||
|
static i2c_master_bus_handle_t s_imu_bus;
|
||||||
|
static i2c_master_dev_handle_t s_imu_dev;
|
||||||
|
static imu_sample_cb_t s_sample_cb;
|
||||||
|
|
||||||
|
static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz);
|
||||||
|
|
||||||
|
static int16_t imu_raw16_le(const uint8_t *lo, const uint8_t *hi)
|
||||||
|
{
|
||||||
|
return (int16_t)((*hi << 8) | *lo);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t imu_write_u8(uint8_t reg, uint8_t val)
|
||||||
|
{
|
||||||
|
uint8_t buf[2] = {reg, val};
|
||||||
|
return i2c_master_transmit(s_imu_dev, buf, sizeof(buf), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t imu_read_u8(uint8_t reg, uint8_t *val)
|
||||||
|
{
|
||||||
|
return i2c_master_transmit_receive(s_imu_dev, ®, 1, val, 1, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t imu_read_bytes(uint8_t reg, uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
return i2c_master_transmit_receive(s_imu_dev, ®, 1, data, len, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t imu_probe_device(uint8_t addr)
|
||||||
|
{
|
||||||
|
i2c_device_config_t dev_cfg = {
|
||||||
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||||
|
.device_address = addr,
|
||||||
|
.scl_speed_hz = BOARD_IMU_I2C_FREQ_HZ,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t err = i2c_master_bus_add_device(s_imu_bus, &dev_cfg, &s_imu_dev);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t who_am_i = 0;
|
||||||
|
err = imu_read_u8(QMI8658_WHO_AM_I, &who_am_i);
|
||||||
|
if (err != ESP_OK || who_am_i != QMI8658_WHO_AM_I_ID) {
|
||||||
|
i2c_master_bus_rm_device(s_imu_dev);
|
||||||
|
s_imu_dev = NULL;
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "WHO_AM_I mismatch at 0x%02X: 0x%02X", addr, who_am_i);
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "QMI8658 found at 0x%02X (WHO_AM_I=0x%02X)", addr, who_am_i);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t imu_configure(void)
|
||||||
|
{
|
||||||
|
esp_err_t err = imu_write_u8(QMI8658_RESET, QMI8658_RESET_VALUE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
|
||||||
|
err = imu_write_u8(QMI8658_CTRL1, QMI8658_CTRL1_VALUE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imu_write_u8(QMI8658_CTRL2, QMI8658_CTRL2_VALUE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imu_write_u8(QMI8658_CTRL3, QMI8658_CTRL3_VALUE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imu_write_u8(QMI8658_CTRL5, QMI8658_CTRL5_VALUE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imu_write_u8(QMI8658_CTRL6, QMI8658_CTRL6_VALUE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imu_write_u8(QMI8658_CTRL7, QMI8658_CTRL7_VALUE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void imu_log_sample(const char *label, float ax, float ay, float az,
|
||||||
|
int16_t rx, int16_t ry, int16_t rz)
|
||||||
|
{
|
||||||
|
float mag = sqrtf(ax * ax + ay * ay + az * az);
|
||||||
|
ESP_LOGI(TAG, "%s raw=(%d,%d,%d) g=(%.3f,%.3f,%.3f) |mag|=%.3f",
|
||||||
|
label, rx, ry, rz, ax, ay, az, mag);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void imu_task(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
|
||||||
|
int64_t last_log_us = esp_timer_get_time();
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
float ax = 0.0f;
|
||||||
|
float ay = 0.0f;
|
||||||
|
float az = 0.0f;
|
||||||
|
int16_t rx = 0;
|
||||||
|
int16_t ry = 0;
|
||||||
|
int16_t rz = 0;
|
||||||
|
|
||||||
|
esp_err_t err = imu_read_raw(&rx, &ry, &rz);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ax = (float)rx / QMI8658_ACCEL_SENS_8G;
|
||||||
|
ay = (float)ry / QMI8658_ACCEL_SENS_8G;
|
||||||
|
az = (float)rz / QMI8658_ACCEL_SENS_8G;
|
||||||
|
|
||||||
|
int64_t now_us = esp_timer_get_time();
|
||||||
|
if ((now_us - last_log_us) >= (int64_t)IMU_DEBUG_LOG_INTERVAL_MS * 1000) {
|
||||||
|
imu_log_sample("sample", ax, ay, az, rx, ry, rz);
|
||||||
|
last_log_us = now_us;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_sample_cb != NULL) {
|
||||||
|
s_sample_cb(ax, ay, az);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(IMU_SAMPLE_PERIOD_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t imu_init(void)
|
||||||
|
{
|
||||||
|
#if BOARD_TOUCH_ENABLED
|
||||||
|
s_imu_bus = touch_get_i2c_bus();
|
||||||
|
if (s_imu_bus == NULL) {
|
||||||
|
ESP_LOGE(TAG, "Shared I2C bus not ready (touch not initialized?)");
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Using shared I2C bus (SDA=%d SCL=%d)", BOARD_PIN_IMU_SDA,
|
||||||
|
BOARD_PIN_IMU_SCL);
|
||||||
|
#else
|
||||||
|
ESP_LOGI(TAG, "Initialize dedicated I2C for IMU");
|
||||||
|
i2c_master_bus_config_t bus_config = {
|
||||||
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||||
|
.i2c_port = BOARD_IMU_I2C_NUM,
|
||||||
|
.sda_io_num = BOARD_PIN_IMU_SDA,
|
||||||
|
.scl_io_num = BOARD_PIN_IMU_SCL,
|
||||||
|
.glitch_ignore_cnt = 7,
|
||||||
|
.flags.enable_internal_pullup = true,
|
||||||
|
};
|
||||||
|
esp_err_t bus_err = i2c_new_master_bus(&bus_config, &s_imu_bus);
|
||||||
|
if (bus_err != ESP_OK) {
|
||||||
|
return bus_err;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
esp_err_t err = imu_probe_device(QMI8658_ADDR_SDO_LOW);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
err = imu_probe_device(QMI8658_ADDR_SDO_HIGH);
|
||||||
|
}
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "QMI8658 not found");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imu_configure();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "IMU configure failed: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ax = 0.0f;
|
||||||
|
float ay = 0.0f;
|
||||||
|
float az = 0.0f;
|
||||||
|
int16_t rx = 0;
|
||||||
|
int16_t ry = 0;
|
||||||
|
int16_t rz = 0;
|
||||||
|
if (imu_read_accel(&ax, &ay, &az) == ESP_OK) {
|
||||||
|
imu_read_raw(&rx, &ry, &rz);
|
||||||
|
imu_log_sample("init check", ax, ay, az, rx, ry, rz);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Init check read failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "IMU initialized (±8g, 1000 Hz ODR)");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz)
|
||||||
|
{
|
||||||
|
if (s_imu_dev == NULL || rx == NULL || ry == NULL || rz == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t raw[6];
|
||||||
|
esp_err_t err = imu_read_bytes(QMI8658_ACC_X_L, raw, sizeof(raw));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
*rx = imu_raw16_le(&raw[0], &raw[1]);
|
||||||
|
*ry = imu_raw16_le(&raw[2], &raw[3]);
|
||||||
|
*rz = imu_raw16_le(&raw[4], &raw[5]);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t imu_read_accel(float *ax, float *ay, float *az)
|
||||||
|
{
|
||||||
|
int16_t rx = 0;
|
||||||
|
int16_t ry = 0;
|
||||||
|
int16_t rz = 0;
|
||||||
|
|
||||||
|
esp_err_t err = imu_read_raw(&rx, &ry, &rz);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
*ax = (float)rx / QMI8658_ACCEL_SENS_8G;
|
||||||
|
*ay = (float)ry / QMI8658_ACCEL_SENS_8G;
|
||||||
|
*az = (float)rz / QMI8658_ACCEL_SENS_8G;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void imu_start_task(imu_sample_cb_t cb)
|
||||||
|
{
|
||||||
|
s_sample_cb = cb;
|
||||||
|
xTaskCreate(imu_task, "imu_task", 3072, NULL, 4, NULL);
|
||||||
|
ESP_LOGI(TAG, "IMU sample task started");
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
esp_err_t imu_init(void)
|
||||||
|
{
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t imu_read_accel(float *ax, float *ay, float *az)
|
||||||
|
{
|
||||||
|
(void)ax;
|
||||||
|
(void)ay;
|
||||||
|
(void)az;
|
||||||
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void imu_start_task(imu_sample_cb_t cb)
|
||||||
|
{
|
||||||
|
(void)cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
typedef void (*imu_sample_cb_t)(float ax_g, float ay_g, float az_g);
|
||||||
|
|
||||||
|
esp_err_t imu_init(void);
|
||||||
|
esp_err_t imu_read_accel(float *ax, float *ay, float *az);
|
||||||
|
void imu_start_task(imu_sample_cb_t cb);
|
||||||
+34
-6
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+242
-32
@@ -16,22 +16,127 @@
|
|||||||
|
|
||||||
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 */
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ////////////////////
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"*/
|
||||||
|
|||||||
Reference in New Issue
Block a user