Compare commits
6 Commits
4b3206dca3
...
4de9f2ba61
| Author | SHA1 | Date | |
|---|---|---|---|
| 4de9f2ba61 | |||
| 0d0bc33192 | |||
| 86200f5e3a | |||
| 31982ba7b9 | |||
| 3a1111e99d | |||
| 3dc6cadf49 |
42
.planning/STATE.md
Normal file
42
.planning/STATE.md
Normal file
@ -0,0 +1,42 @@
|
||||
# STATE — 项目状态追踪
|
||||
|
||||
> 跨 session 的项目状态、roadmap 演化、关键决策记录。本文件用于让新 session 快速恢复项目上下文。
|
||||
|
||||
## Current Milestone
|
||||
|
||||
- **Milestone**: `digital_human_rtc` (数字人 RTC 项目)
|
||||
- **Branch**: `Rtc_AIavatar`
|
||||
- **ROADMAP**: `.planning/milestones/digital_human_rtc/ROADMAP.md`
|
||||
- **总阶段数**: 10
|
||||
- **当前进度**: Phase 1-6 完成,Phase 7 (battery_psm) 进行中
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Roadmap Evolution
|
||||
|
||||
记录 roadmap 在项目过程中的演化(按时间倒序,最新在前):
|
||||
|
||||
- **2026-05-15** — Phase 7 ROADMAP 同步矫正:原 Phase 7 "集成测试 + 推送" 因发现文件系统已有 `phase_07_battery_psm`(电量保护重构)而被重编号为 Phase 10。Phase 7 在 ROADMAP 中改写为指向 `phase_07_battery_psm/README.md`。
|
||||
- **2026-05-15** — Phase 8 added: 数字人 RTC 音频卡顿根因诊断。通过 4 类 ESP_LOGW 埋点(audio queue 深度 / PCM write 耗时 / WiFi RSSI / heap)定位卡顿真因(CPU/PSRAM/DMA/WiFi/Opus/碎片 6 选 1+ 组合),产出 DIAG_REPORT.md 决定 Phase 9 实施策略。
|
||||
- **2026-05-15** — Phase 9 added (占位): 音频卡顿实施优化。具体方案待 Phase 8 数据决策,分支预案 A=EAF 旁路 / B=WiFi 扩容 / C=完整切 EAF+资源再分配 / D=DMA 排查 已列入 ROADMAP。
|
||||
|
||||
### Key Technical Decisions
|
||||
|
||||
- **方案 A vs 方案 B 路线分歧已用 Phase 8 数据驱动决策化解**:
|
||||
- 方案 A = `eaf_dec_*` 旁路替换 lv_gif(保留 LVGL)
|
||||
- 方案 B = 数字人模式完整切 esp_emote_gfx(弃用 LVGL)
|
||||
- 两者选择不靠拍脑袋,靠 Phase 8 诊断报告
|
||||
- **EAF 集成边界确认**:
|
||||
- 数字人模式 LVGL 实际范围只有 `main/dzbj/ai_chat_ui.c` (458 行) + `main/display/lcd_display.cc` 数字人分支 (~300 行)
|
||||
- 不涉及任何 `ui/screens/` 下 SquareLine 界面(属吧唧模式,`CONFIG_BAJI_BADGE_MODE` 编译排除)
|
||||
- `ui_ScreenUpdate` 已确认是吧唧模式 BLE 收图 UI,**非 OTA**,不影响数字人模式
|
||||
- **方案 B 完整切若启用,资源账本**:
|
||||
- 释放:~40KB DRAM + ~80KB PSRAM(LVGL 框架本体)
|
||||
- 投入:WiFi RX 缓冲扩容 ~15KB DRAM + Opus jitter buffer ~10KB PSRAM + RTC SDK jitter ~40KB PSRAM
|
||||
- 净结余:~20KB DRAM + ~10KB PSRAM 仍可备用
|
||||
|
||||
### Open Risks (Phase 9 实施时验证)
|
||||
|
||||
- `gfx_label` 是否支持中文自动换行 + 双行居中(方案 B/C 阻塞点)
|
||||
- `font_puhui_20_4.c`(8.5MB LVGL bitmap font)能否被 EAF 直接复用
|
||||
- `cst816s` 触摸路径在弃用 LVGL 后如何接驳(需确认数字人模式是否需要触摸交互)
|
||||
@ -1,6 +1,6 @@
|
||||
# ROADMAP — 数字人 RTC 项目
|
||||
|
||||
7 个阶段,按依赖关系串行。每个阶段产生原子 commit,可独立 revert。
|
||||
10 个阶段,按依赖关系串行。每个阶段产生原子 commit,可独立 revert。
|
||||
|
||||
## 阶段总览
|
||||
|
||||
@ -14,7 +14,13 @@ Phase 2 (分区表调整) ──┘ │
|
||||
│
|
||||
Phase 6 (RTC 空闲超时联动)
|
||||
│
|
||||
Phase 7 (集成测试 + 推送)
|
||||
Phase 7 (电量保护 + 低功耗重构)
|
||||
│
|
||||
Phase 8 (音频卡顿根因诊断)
|
||||
│
|
||||
Phase 9 (音频卡顿实施优化 - 待定)
|
||||
│
|
||||
Phase 10 (集成测试 + 推送)
|
||||
```
|
||||
|
||||
---
|
||||
@ -341,7 +347,110 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: 集成测试 + 推送
|
||||
## Phase 7: 电量保护 + 低功耗管理重构
|
||||
|
||||
**目标**:把开机电量保护异步化 + 屏幕低电 UI + PowerSaveTimer 状态机重写 + esp_pm_configure 收口受守卫保护,重构成连贯系统而非局部打补丁。
|
||||
|
||||
**详细规格**:见 [phases/phase_07_battery_psm/README.md](phases/phase_07_battery_psm/README.md)
|
||||
|
||||
**完成标志**:
|
||||
- ✅ 开机不再被电池采样 6 秒阻塞
|
||||
- ✅ 屏幕分级低电 UI 提示(>25% / 15-25% / <15% / <5%)
|
||||
- ✅ PowerSaveTimer `in_sleep_mode_` 状态机无边角
|
||||
- ✅ esp_pm_configure 调用统一收口到 callback 内部
|
||||
|
||||
**产出 commit**:`refactor(power): Phase 7 - 电量保护异步化 + 低功耗状态机重写`
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: 数字人 RTC 音频卡顿根因诊断
|
||||
|
||||
**目标**:通过 4 类 ESP_LOGW 日志埋点采集运行时数据,定位 RTC 音频卡顿真实根因(CPU 争抢 / PSRAM 带宽 / DMA / WiFi / Opus 抖动 / 内存碎片),让数据驱动 Phase 9 的实施策略决策。
|
||||
|
||||
**详细规格**:见 [phases/phase_08_audio_glitch_diag/README.md](phases/phase_08_audio_glitch_diag/README.md)
|
||||
|
||||
**完成标志**:
|
||||
- ✅ 4 处日志埋点编译通过并正常输出
|
||||
- ✅ 实际复现一次卡顿,采集到包含卡顿瞬间的日志
|
||||
- ✅ 产出 `DIAG_REPORT.md` 明确根因判定
|
||||
- ✅ 给出 Phase 9 实施分支推荐(A/B/C/D 之一)
|
||||
|
||||
**产出 commit**:`diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: ❌ 已取消 — 音频卡顿增量优化尝试
|
||||
|
||||
**取消原因**:用户决策(2026-05-15)— 增量修补效果不明显(v1 引入 ES8311 dev 30 regression,v2 jitter buffer 工作但仍卡,v3 未来得及实测),改为方案 C 完整切 EAF(见 Phase 10)。
|
||||
|
||||
**保留**:[phase_09_audio_jitter_codecinit/CANCELLED.md](phases/phase_09_audio_jitter_codecinit/CANCELLED.md) 记录 v1/v2/v3 实验教训供未来参考。
|
||||
|
||||
**回滚**:Phase 9 所有代码改动通过 `git restore` 已回滚,Phase 8 DIAG 埋点保留作为 Phase 10 验证基准。
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: 数字人模式 LVGL → esp_emote_gfx 完整切换
|
||||
|
||||
**目标**:仅在 `CONFIG_BAJI_BADGE_MODE=n` 分支下弃用 LVGL,采用乐鑫 esp_emote_gfx 框架 + EAF 动画格式。Phase 8 数据排除了"调度"问题(codec write 0 慢),用户决策直接验证 EAF 显示效果。释放 ~40KB DRAM + ~80KB PSRAM 留作 Phase 11 资源再分配。
|
||||
|
||||
**驱动**:用户假设 "GIF 与 RTC 抢占资源 + WiFi 缓冲不够",方案 C 是该假设的最彻底验证 —— LVGL/GIF 完全消失后看显示效果与音频感知。
|
||||
|
||||
**关键改动范围**:
|
||||
1. 添加 `esp_emote_gfx` 组件依赖(idf_component.yml)
|
||||
2. EAF Packer 工具链:hiyori_m{03,06,07}.gif → .eaf(4-bit + Heatshrink + 透明)
|
||||
3. 重写 `main/display/lcd_display.cc` 数字人分支:接管 display 改为 `gfx_emote_add_disp`
|
||||
4. 重写 `main/dzbj/ai_chat_ui.c`(458 行):`lv_obj` → `gfx_obj`,`lv_gif` → `gfx_anim`,`lv_label` → `gfx_label`
|
||||
5. CMakeLists 条件编译切换(仅 `CONFIG_BAJI_BADGE_MODE=n` 走 EAF)
|
||||
6. 字体接驳:`font_puhui_20_4.c` 复用(EAF 官方支持 LVGL bitmap font)
|
||||
7. 触摸路径:cst816s → `gfx_touch`(如数字人模式需要)
|
||||
|
||||
**预估工时**:3-5 天
|
||||
|
||||
**完成标志**:
|
||||
- ✅ `CONFIG_BAJI_BADGE_MODE=n` 编译通过
|
||||
- ✅ 烧录后数字人 GIF 动画正常显示(hiyori 三个表情切换)
|
||||
- ✅ 字幕显示(中文 + 自动换行 + 双行居中)
|
||||
- ✅ RTC 对话听感主观验证:扬声器卡顿是否消失(这是核心 PoC 目的)
|
||||
- ✅ `idf.py size` 对比:Flash/DRAM/PSRAM 变化
|
||||
- ✅ DRAM 净结余 ≥ 30KB 用于 Phase 11
|
||||
|
||||
**产出 commit**:`feat(ui): Phase 10 - 数字人模式 LVGL → esp_emote_gfx 完整迁移`
|
||||
|
||||
**关键风险**:
|
||||
- `gfx_label` 中文自动换行 + 双行居中支持验证(实施前先跑 PoC)
|
||||
- 数字人模式如有触摸交互,需重写
|
||||
- 资源转 EAF 工具链稳定性
|
||||
- `ui_ScreenUpdate`(吧唧模式 BLE 收图 UI)共用 lv_gif,方案 C 仅影响数字人模式,吧唧模式保持 LVGL(双轨编译)
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: 内存优化 + WiFi 缓冲扩容
|
||||
|
||||
**目标**:把 Phase 10 释放的 ~40KB DRAM + ~80KB PSRAM 重新分配到 WiFi RX/TX 缓冲、Opus jitter buffer、RTC SDK jitter buffer,最大化 RTC 体验。
|
||||
|
||||
**关键改动**:
|
||||
1. `sdkconfig.defaults`:
|
||||
- `CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM`: 10 → 16
|
||||
- `CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM`: 32 → 48
|
||||
- `CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM`: 32 → 48
|
||||
- `CONFIG_ESP_WIFI_RX_BA_WIN`: 6 → 16(AMPDU 窗口扩大减少重传)
|
||||
2. Opus decode pool 上限扩容(如 SDK 暴露)
|
||||
3. 火山 RTC SDK jitter buffer 配置扩容(如 SDK 暴露)
|
||||
4. `heap_caps_print_heap_info` 前后对比
|
||||
|
||||
**预估工时**:1 天
|
||||
|
||||
**完成标志**:
|
||||
- ✅ sdkconfig 改动编译通过且烧录稳定
|
||||
- ✅ DRAM 投入 ≤ 15KB 后剩余可用 ≥ Phase 8 baseline
|
||||
- ✅ Phase 8 DIAG 埋点对比:queue=0 次数下降、WiFi 重传减少
|
||||
- ✅ 用户主观:扬声器流畅度提升
|
||||
|
||||
**产出 commit**:`perf(rtc-only): Phase 11 - WiFi 缓冲扩容 + jitter buffer 强化`
|
||||
|
||||
---
|
||||
|
||||
## Phase 12: 集成测试 + 推送
|
||||
|
||||
**目标**:端到端验证 MILESTONE.md 第 6 节全部验收项,推送到 gitea + GitHub。
|
||||
|
||||
@ -359,6 +468,7 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
|
||||
**完成标志**:
|
||||
- ✅ MILESTONE.md 第 6 节成功标准全部 ✓
|
||||
- ✅ Phase 10/11 音频卡顿问题已解决
|
||||
- ✅ gitea + GitHub 远程已同步
|
||||
- ✅ 文档更新完成
|
||||
|
||||
@ -373,9 +483,14 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
- Phase 4/5 依赖 Phase 1(dzbj 模块清理完成)+ Phase 3(GIF 资源就位)
|
||||
- Phase 4 ⊥ Phase 5(情绪映射和字幕显示独立,可并行)
|
||||
- Phase 6 依赖 Phase 1(清理 sleep_mgr 调用点)
|
||||
- Phase 7 必须最后
|
||||
- Phase 7 依赖 Phase 6(PowerSaveTimer 状态机重写需 Phase 6 守卫到位)
|
||||
- **Phase 8 依赖 Phase 6(卡顿症状在 Phase 6 收尾发现,需要 RTC 链路稳定)**
|
||||
- **Phase 9 已取消**(增量优化效果不明显,改走 Phase 10 完整切 EAF)
|
||||
- **Phase 10 依赖 Phase 8**(数据排除调度问题后用户决策完整切 EAF 验证显示效果)
|
||||
- **Phase 11 依赖 Phase 10**(用 Phase 10 释放的 DRAM/PSRAM 做资源再分配)
|
||||
- Phase 12 必须最后(依赖 Phase 11 完成)
|
||||
|
||||
**建议串行执行顺序**:1 → 2 → 3 → 4 → 5 → 6 → 7(最稳)
|
||||
**建议串行执行顺序**:1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → ~~9~~ → 10 → 11 → 12
|
||||
|
||||
---
|
||||
|
||||
@ -388,5 +503,10 @@ static const emotion_gif_map_t emotion_gif_table[] = {
|
||||
| Phase 3 | ✅ 完成(commit `7d1c7dc`) |
|
||||
| Phase 4 | ✅ 完成(commit `497c1b4`) |
|
||||
| Phase 5 | ✅ 完成(commit `f2be992`) |
|
||||
| Phase 6 | 🔄 进行中(B+C 双源 + 软退房 + Light Sleep 防护,最新方案见 PLAN 头部"实施变更记录") |
|
||||
| Phase 7 | ⏳ 待启动 |
|
||||
| Phase 6 | ✅ 完成(commit `b8a5fe9` + `4b7b194` 收尾) |
|
||||
| Phase 7 | 🔄 进行中([phase_07_battery_psm](phases/phase_07_battery_psm/README.md) 规格已写,实施待启动) |
|
||||
| Phase 8 | ✅ 完成(commit `3dc6cad`,4 类 DIAG 埋点 + 根因报告) |
|
||||
| **Phase 9** | **❌ 已取消**(v1/v2/v3 增量优化未解决,CANCELLED.md 记录教训) |
|
||||
| **Phase 10** | **⏳ 待启动**(数字人模式 LVGL→EAF 完整切换,新增) |
|
||||
| **Phase 11** | **⏸️ 阻塞中**(内存优化 + WiFi 缓冲扩容,依赖 Phase 10 完成) |
|
||||
| Phase 12 | ⏳ 待启动(集成测试 + 推送,原 Phase 10 重编号) |
|
||||
|
||||
@ -0,0 +1,138 @@
|
||||
# Phase 8 — 音频卡顿诊断报告
|
||||
|
||||
> 状态:✅ 已完成
|
||||
> 实测日志:[../../../../05-最新日志.txt](../../../../05-最新日志.txt)(1716 行,777 行 DIAG)
|
||||
|
||||
---
|
||||
|
||||
## 1. 测试环境
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 日期 | 2026-05-15 11:22 |
|
||||
| 设备 | ESP32-S3-N16R8 (Kapi) |
|
||||
| 板子 | movecall-moji-esp32s3 |
|
||||
| 串口 | `/dev/cu.usbmodem834401` |
|
||||
| RSSI 基线 | -24 ~ -33 dBm(极强信号) |
|
||||
| WiFi | airhub, BW20, ch1 |
|
||||
| RTC | 火山 RTC(VolcEngineRTCLite v1.57.207.001) |
|
||||
| 测试时长 | ~70 秒(开机 → RTC 对话 ~45 秒) |
|
||||
| 主观感受 | 开机播报"卡卡在呢"声音抖 + RTC 对话过程中扬声器断续/不连贯 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户主观感知
|
||||
|
||||
**用户报告**:两段都有卡顿
|
||||
- **A. 开机播报阶段**:"卡卡在呢"语音听感抖
|
||||
- **B. RTC 对话阶段**:AI 回答声音不连贯/断续
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键时段数据对照
|
||||
|
||||
### 3.1 write_slow 时段分布
|
||||
|
||||
| 时段 | write_slow 次数 | 主要场景 |
|
||||
|---|---|---|
|
||||
| 0-2s | 0 | 系统启动 |
|
||||
| **2-13s** | **126** ⚠️ | **开机播报 + standby 播报(PlaySound 路径)** |
|
||||
| 13-21s | 4 | 待机 |
|
||||
| **21-25s** | **16** ⚠️ | **BOOT 按键 → "卡卡在呢"播报** |
|
||||
| **25s+ (RTC 对话期)** | **0 ✓** | **codec 写入完全健康** |
|
||||
|
||||
**write_slow 耗时分布**:50-58ms/帧(PCM 帧时长 60ms 的 83-97%),最大 102ms 单帧
|
||||
|
||||
### 3.2 queue 深度时段分布
|
||||
|
||||
| 时段 | 平均深度 | 最大 | queue=0 次数 | queue≥5 次数 |
|
||||
|---|---|---|---|---|
|
||||
| 0-5s 开机 | 28.4 | 50 | 0 | 29 |
|
||||
| 5-13s 播报 | 21.2 | 48 | 2 | 31 |
|
||||
| 21-25s 连接 | 8.0 | 16 | 1 | 6 |
|
||||
| **25-40s 对话** | **2.4** | 7 | 7 | 21 |
|
||||
| **40s+ 对话** | **2.7** | **19** ⚠️ | **58** ⚠️ | 62 |
|
||||
|
||||
### 3.3 关键突发事件(40s+ 对话期)
|
||||
|
||||
```
|
||||
ts= 44979ms DIAG: queue=12 enq_ws ← WebSocket 入队突发
|
||||
ts= 45039ms DIAG: queue=17 enq_ws ← 50ms 内入队 +5 帧
|
||||
ts= 45089ms DIAG: queue=19 enq_ws ← 100ms 内堆积顶峰
|
||||
ts= 45139ms DIAG: queue=17 deq ← 出队开始消化
|
||||
ts= 45199ms DIAG: queue=19 enq_ws ← 又一波入队
|
||||
ts= 45249ms DIAG: queue=18 deq
|
||||
ts= 45309ms DIAG: queue=16 deq ← 消化中
|
||||
ts= 45369ms DIAG: queue=17 deq
|
||||
ts= 45429ms DIAG: queue=15 deq
|
||||
ts= 45479ms DIAG: queue=15 enq_ws
|
||||
...
|
||||
(在此期间无任何 write_slow,codec 写入正常)
|
||||
```
|
||||
|
||||
### 3.4 ES7210 初始化错误(影响段 A)
|
||||
|
||||
```
|
||||
I (2520) ES8311: Work in Slave mode
|
||||
E (2690) I2C_If: Fail to write to dev 80 ← I2C 0x80 写入失败
|
||||
E (2770) ES7210: Write register fail ← 接连失败
|
||||
E (2770) ES7210: Open fail ← ES7210 输入侧 Open 失败
|
||||
I (2009) ES7210: Enable ES7210_INPUT_MIC1 ← 但随后又"成功"
|
||||
W (2509) DIAG: write_slow 57519us samples=960 ← 同时刻 codec 写入开始持续慢
|
||||
```
|
||||
|
||||
**推测**:ES7210 I2C 初始化报错后虽然继续,但 codec 整体 ready 状态有缺陷 → 开机播报期间每帧 PCM 写入要等 50-58ms。
|
||||
|
||||
### 3.5 RSSI / heap 全程
|
||||
|
||||
- RSSI 全程 **-24 ~ -33 dBm**(极强)→ 物理层 WiFi 完全健康
|
||||
- free_int ~60KB / PSRAM 6.7MB 全程稳定,frag 60% 但不上涨 → 无内存碎片问题
|
||||
|
||||
---
|
||||
|
||||
## 4. 根因判定
|
||||
|
||||
| 候选根因 | 成立? | 证据 |
|
||||
|---|---|---|
|
||||
| ① CPU 争抢 (LZW vs Opus) | ❌ 排除 | 对话期 0 次 write_slow,codec 时间预算充足 |
|
||||
| ② PSRAM 带宽争抢 | ❌ 排除 | 同上 |
|
||||
| ③ I2S DMA 延迟(通用)| ❌ 排除(对话期)| RTC 对话期写入完全 normal |
|
||||
| **③' codec init 时序缺陷(开机阶段专属)** | **✅ 成立(A)** | ES7210 I2C 失败 + 2-13s 集中 126 次 write_slow |
|
||||
| ④ WiFi 丢包 | ❌ 排除 | RSSI -24 ~ -33 dBm 极强 |
|
||||
| **⑤ Opus/WebSocket 帧到达抖动** | **✅ 成立(B)** | queue 突发堆积 19 + queue=0 出现 58 次(40s+ 对话期) |
|
||||
| ⑥ 内存碎片 | ❌ 排除 | heap 全程稳定 |
|
||||
|
||||
**最终判定**:**两个独立根因 ③'(开机 codec 时序)+ ⑤(应用层网络抖动)**,分别对应用户主观感知的 A 和 B 段。
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 9 分支推荐
|
||||
|
||||
> 原 PLAN 预案 A/B/C/D 都不完全匹配。本案需要**双线并行修复**。
|
||||
|
||||
### 推荐:分支 B' —— 双线修复(精准化版)
|
||||
|
||||
| 子任务 | 解决问题 | 实施 | 估时 |
|
||||
|---|---|---|---|
|
||||
| **9.1 应用层 jitter buffer**(主线) | ⑤ Opus 帧到达抖动 → 用户感知 B 段卡顿 | 在 `audio_decode_queue_` 消费侧加 "fill-threshold + drain":队列首次填到 N=4 帧才开始消费;下面阈值若降到 1 帧暂停消费等再次蓄水;上限阈值 12 帧触发丢包/降级(避免无限堆积)| 0.5 天 |
|
||||
| **9.2 开机 codec init 时序修复** | ③' ES7210 init 失败 → 用户感知 A 段卡顿 | 排查 `BoxAudioCodec::Initialize` 路径:①ES7210 reset 时序(拉低/拉高/等待)②`Adev_Codec` open 重试 ③播报前等真正 ready | 0.5 天 |
|
||||
|
||||
### 不做的事(基于数据)
|
||||
|
||||
- ❌ 不引入 esp_emote_gfx(CPU 完全够用,原 A/C 分支无效)
|
||||
- ❌ 不扩 `CONFIG_ESP_WIFI_*_BUFFER_NUM`(物理层 RX 健康)
|
||||
- ❌ 不优化 Opus 解码(耗时未浮现成瓶颈)
|
||||
|
||||
### 实施估时合计:1 天
|
||||
|
||||
---
|
||||
|
||||
## 6. 附录
|
||||
|
||||
- 实测日志:`/Users/rdzleo/Desktop/Baji_Rtc_Toy/05-最新日志.txt`
|
||||
- 关键代码位置:
|
||||
- 入队点:[application.cc:351](../../../../main/application.cc#L351) / [:801](../../../../main/application.cc#L801) / [:2899](../../../../main/application.cc#L2899)
|
||||
- 出队点:[application.cc:2197](../../../../main/application.cc#L2197) 附近
|
||||
- codec 写入:[application.cc:2290](../../../../main/application.cc#L2290)
|
||||
- codec init:`main/audio_codecs/box_audio_codec.cc`(待 Phase 9 改)
|
||||
- PHASE8_DIAG_ENABLE 关闭:`#define PHASE8_DIAG_ENABLE 0`([main/application.cc](../../../../main/application.cc) 顶部)
|
||||
@ -0,0 +1,367 @@
|
||||
# Phase 8 PLAN — 数字人 RTC 音频卡顿根因诊断
|
||||
|
||||
> 里程碑: `digital_human_rtc`
|
||||
> 阶段目标: 通过 4 类 ESP_LOGW 埋点,定位 RTC 音频卡顿真因(6 候选根因 → 1+ 组合),产出 DIAG_REPORT.md 决定 Phase 9 实施分支(A/B/C/D)。
|
||||
> 性质: **诊断 phase,零业务逻辑改动**,可一键 revert。
|
||||
|
||||
---
|
||||
|
||||
## 0. 调研结论
|
||||
|
||||
### 0.1 关键代码位置
|
||||
|
||||
| 位置 | 用途 | 埋点策略 |
|
||||
|---|---|---|
|
||||
| [main/application.cc:339](../../../../main/application.cc#L339) | 第一处 `audio_decode_queue_.emplace_back(opus)` 入队 | 入队后采样队列深度 |
|
||||
| [main/application.cc:789](../../../../main/application.cc#L789) | RTC 下行数据入队 `audio_decode_queue_.emplace_back(data)` | 入队后采样队列深度 |
|
||||
| [main/application.cc:2155-2156](../../../../main/application.cc#L2155) | `OnAudioOutput()` 中出队 `pop_front()` | 出队后采样队列深度 |
|
||||
| [main/application.cc:2258](../../../../main/application.cc#L2258) | `codec->OutputData(pcm)` 唯一 PCM 写入点 | 写入前后包 timer,>15ms 告警 |
|
||||
| [main/application.cc:1957](../../../../main/application.cc#L1957) | `OnClockTimer()` 周期任务(`clock_ticks_++` 节流,已有 `% 10 == 0` 分支 ≈ 1Hz)| 1Hz 分支挂 RSSI + heap 采样 |
|
||||
| [main/audio_codecs/audio_codec.cc:17](../../../../main/audio_codecs/audio_codec.cc#L17) | `AudioCodec::OutputData` → 内部 `Write` | 不动,timer 包在调用方 |
|
||||
|
||||
### 0.2 已掌握的事实
|
||||
|
||||
- `OnAudioOutput` 在 `AudioLoop` 中每 ~10ms 循环调用([application.cc:2019](../../../../main/application.cc#L2019)),处理一个完整 PCM 帧(~20ms)
|
||||
- 队列 push/pop 都在 `mutex_` 保护下,size() 调用安全
|
||||
- `background_task_->Schedule()` 异步执行解码 + `codec->OutputData()`,Core 1 上跑
|
||||
- `clock_ticks_` 在 [application.cc:1958](../../../../main/application.cc#L1958) 自增,`% 10 == 0` 分支已存在 → 复用为 1Hz 触发点
|
||||
- `codec->OutputData` 内部 `Write(data, samples)` 是纯虚 → 不同板子(ES7210/ES8311)实现不同,最好在 application.cc 端测耗时,覆盖所有板子
|
||||
|
||||
### 0.3 ESP_LOGW 性能影响评估
|
||||
|
||||
- ESP_LOGW 走 UART0/USB CDC(取决于 `CONFIG_ESP_CONSOLE_*`),单次输出 ~100-500us
|
||||
- 高频打印会拖慢观察对象本身 → 加节流(50ms / 1Hz / 阈值告警)
|
||||
- 不需要切到 USB CDC:当前项目已用 UART0 输出日志且实测可承受 ESP_LOGI 高频打印(参考 phase_06 `🔍 中止后音频[N]` 等密集 INFO 日志)
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计方案
|
||||
|
||||
### 1.1 埋点 4 处(节流策略)
|
||||
|
||||
| 编号 | 位置 | 触发条件 | 输出格式 | 节流 |
|
||||
|---|---|---|---|---|
|
||||
| **DIAG-1** | OnAudioOutput 入口 + 入队点 | 队列深度变化 | `ESP_LOGW("DIAG", "queue=%d ts=%lld", size, esp_timer_get_time())` | 50ms 节流(last_log_us 全局) |
|
||||
| **DIAG-2** | `codec->OutputData(pcm)` 前后 | 写入耗时 > 15ms | `ESP_LOGW("DIAG", "write_slow %lldus samples=%zu", cost, pcm.size())` | 阈值告警(>15000us 才打印) |
|
||||
| **DIAG-3** | OnClockTimer 1Hz 分支 | 1Hz 周期 | `ESP_LOGW("DIAG", "rssi=%d ch=%d", rssi, channel)` | 复用 `clock_ticks_ % 10 == 0` |
|
||||
| **DIAG-4** | OnClockTimer 1Hz 分支 | 1Hz 周期 | `ESP_LOGW("DIAG", "free_int=%d psram=%d largest_int=%d frag=%.1f%%", ...)` | 同 DIAG-3 |
|
||||
|
||||
### 1.2 实现位置统一
|
||||
|
||||
所有埋点集中在 [main/application.cc](../../../../main/application.cc)。**不修改 audio_codecs/ 或其他模块**。便于一次性 revert:
|
||||
|
||||
```cpp
|
||||
// 顶部加包裹宏(编译期开关)
|
||||
#ifndef PHASE8_DIAG_ENABLE
|
||||
#define PHASE8_DIAG_ENABLE 1
|
||||
#endif
|
||||
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
#define DIAG_LOG(fmt, ...) ESP_LOGW("DIAG", fmt, ##__VA_ARGS__)
|
||||
static int64_t g_diag_queue_last_us = 0;
|
||||
#else
|
||||
#define DIAG_LOG(fmt, ...) ((void)0)
|
||||
#endif
|
||||
```
|
||||
|
||||
Phase 8 完成后做 Phase 9 时,可以 `#define PHASE8_DIAG_ENABLE 0` 一键关掉所有埋点。
|
||||
|
||||
### 1.3 复现卡顿的测试场景
|
||||
|
||||
1. **基线对话**(5 分钟):连续 RTC 对话,每分钟主动说话 2-3 次
|
||||
2. **情绪触发**(5 分钟):让 AI 说带情绪标签的回复("(happy)..."、"(sad)..."),触发 GIF 切换瞬间观察 queue + write_slow
|
||||
3. **WiFi 抗扰**(可选):手机开热点 + 远离路由器,观察 rssi 下降时 queue 是否变空
|
||||
4. **长时压测**(10 分钟):连续无停顿对话,看是否 free_int / largest_int 持续下降
|
||||
|
||||
日志收集:`idf.py monitor 2>&1 | tee phase_08_diag.log`
|
||||
|
||||
### 1.4 数据分析方法
|
||||
|
||||
筛选 DIAG 日志:`grep "DIAG" phase_08_diag.log > diag_only.log`
|
||||
|
||||
按时间序对照表格:
|
||||
|
||||
| 时间窗口 | queue 序列 | write_slow 次数 | rssi 趋势 | free_int 趋势 | 推断根因 |
|
||||
|---|---|---|---|---|---|
|
||||
| 卡顿前 5s | ? | ? | ? | ? | — |
|
||||
| 卡顿瞬间 | ? | ? | ? | ? | ?(对照表 1) |
|
||||
| 卡顿后 5s | ? | ? | ? | ? | — |
|
||||
|
||||
参照 [README.md §5 卡顿日志特征对照表](README.md#5-卡顿日志特征对照表) 给出根因判定 + Phase 9 分支推荐。
|
||||
|
||||
---
|
||||
|
||||
## 2. 任务清单
|
||||
|
||||
### 任务 2.1 — 顶部包裹宏 + 全局节流变量
|
||||
|
||||
**文件**: `main/application.cc`
|
||||
|
||||
**读取参考**:
|
||||
- 现有 include 顺序(不影响)
|
||||
- 现有静态全局变量风格(参考 `last_subtitle_emotion` 等)
|
||||
|
||||
**改动**(加在 file 顶部 include 之后、其他静态变量附近):
|
||||
|
||||
```cpp
|
||||
// ============================================================
|
||||
// Phase 8: 音频卡顿诊断埋点(一键开关,关闭后零运行时开销)
|
||||
// ============================================================
|
||||
#ifndef PHASE8_DIAG_ENABLE
|
||||
#define PHASE8_DIAG_ENABLE 1
|
||||
#endif
|
||||
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
static int64_t g_diag_queue_last_us = 0; // queue 深度日志节流(50ms)
|
||||
#endif
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `grep "PHASE8_DIAG_ENABLE" main/application.cc` 返回至少 1 处
|
||||
- 编译通过:`idf.py build`
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.2 — DIAG-1: queue 深度埋点(3 处)
|
||||
|
||||
**文件**: `main/application.cc`
|
||||
|
||||
**读取参考**:
|
||||
- 第 339 行 `audio_decode_queue_.emplace_back(std::move(opus))`(入队点 1)
|
||||
- 第 789 行 `audio_decode_queue_.emplace_back(std::move(data))`(入队点 2,RTC 下行)
|
||||
- 第 2156 行 `audio_decode_queue_.pop_front()`(出队点)
|
||||
|
||||
**改动**: 在 3 处之后插入节流采样块(共用宏,统一节流):
|
||||
|
||||
```cpp
|
||||
// 在 339 行 emplace_back 之后立即插入
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
{
|
||||
int64_t now_us = esp_timer_get_time();
|
||||
if (now_us - g_diag_queue_last_us > 50000) { // 50ms 节流
|
||||
g_diag_queue_last_us = now_us;
|
||||
ESP_LOGW("DIAG", "queue=%zu enq1", audio_decode_queue_.size());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
入队点 2(line 789 后)用 tag `enq2`,出队点(line 2156 后)用 tag `deq`。
|
||||
|
||||
**验收**:
|
||||
- `grep -c 'ESP_LOGW("DIAG", "queue=' main/application.cc` 返回 ≥ 3
|
||||
- 编译通过
|
||||
- 烧录后 `idf.py monitor` 能看到 `DIAG: queue=N enqX` / `queue=N deq` 日志
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.3 — DIAG-2: codec OutputData 写入耗时
|
||||
|
||||
**文件**: `main/application.cc`
|
||||
|
||||
**读取参考**:
|
||||
- 第 2258 行 `codec->OutputData(pcm)`
|
||||
|
||||
**改动**: 将该行替换为耗时统计块:
|
||||
|
||||
```cpp
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
int64_t _diag_t = esp_timer_get_time();
|
||||
codec->OutputData(pcm);
|
||||
int64_t _diag_cost = esp_timer_get_time() - _diag_t;
|
||||
if (_diag_cost > 15000) {
|
||||
ESP_LOGW("DIAG", "write_slow %lldus samples=%zu", _diag_cost, pcm.size());
|
||||
}
|
||||
#else
|
||||
codec->OutputData(pcm);// 直接输出PCM数据
|
||||
#endif
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `grep -n "write_slow" main/application.cc` 返回 1 处(在 codec->OutputData 上下文)
|
||||
- 编译通过
|
||||
- 主动制造卡顿(如同时启动多个 RTC GIF 切换)能在日志中看到至少 1 次 `write_slow` 输出
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.4 — DIAG-3 + DIAG-4: OnClockTimer 1Hz 采样
|
||||
|
||||
**文件**: `main/application.cc`
|
||||
|
||||
**读取参考**:
|
||||
- 第 1957-1985 行 `Application::OnClockTimer()` 完整函数
|
||||
- 现有 `if (clock_ticks_ % 10 == 0)` 分支
|
||||
- ESP-IDF `esp_wifi_sta_get_ap_info` API 签名(`<esp_wifi.h>`)
|
||||
- `heap_caps_get_free_size` / `heap_caps_get_largest_free_block` API(`<esp_heap_caps.h>`)
|
||||
|
||||
**改动**: 在 OnClockTimer 现有的 `% 10 == 0` 分支末尾追加:
|
||||
|
||||
```cpp
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
// DIAG-3: WiFi RSSI(每 ~1s 一次)
|
||||
wifi_ap_record_t _diag_ap;
|
||||
if (esp_wifi_sta_get_ap_info(&_diag_ap) == ESP_OK) {
|
||||
ESP_LOGW("DIAG", "rssi=%d ch=%d", _diag_ap.rssi, _diag_ap.primary);
|
||||
}
|
||||
// DIAG-4: heap snapshot
|
||||
int _diag_free_int = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
int _diag_free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||
int _diag_largest_int = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
|
||||
float _diag_frag = _diag_free_int > 0 ? (1.0f - (float)_diag_largest_int / (float)_diag_free_int) * 100.0f : 0.0f;
|
||||
ESP_LOGW("DIAG", "free_int=%d psram=%d largest_int=%d frag=%.1f%%",
|
||||
_diag_free_int, _diag_free_psram, _diag_largest_int, _diag_frag);
|
||||
#endif
|
||||
```
|
||||
|
||||
确保 [application.cc](../../../../main/application.cc) 顶部 include 已包含:
|
||||
- `<esp_wifi.h>`(如缺则 add)
|
||||
- `<esp_heap_caps.h>`(如缺则 add)
|
||||
|
||||
**验收**:
|
||||
- `grep -c 'ESP_LOGW("DIAG", "rssi=' main/application.cc` = 1
|
||||
- `grep -c 'ESP_LOGW("DIAG", "free_int=' main/application.cc` = 1
|
||||
- `grep -nE "esp_wifi.h|esp_heap_caps.h" main/application.cc` 至少能找到 esp_wifi.h
|
||||
- 编译通过
|
||||
- 烧录后 1Hz 频率持续输出 `DIAG: rssi=-XX` 和 `DIAG: free_int=...` 日志
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.5 — 编译 + 烧录 + 启动 RTC 对话
|
||||
|
||||
**前置**:
|
||||
- `idf.py set-target esp32s3`(如未设置)
|
||||
- `idf.py menuconfig` 确认 `CONFIG_LOG_DEFAULT_LEVEL >= 3 (WARN)` 否则 ESP_LOGW 不输出
|
||||
|
||||
**步骤**:
|
||||
```bash
|
||||
idf.py build flash monitor 2>&1 | tee .planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/phase_08_diag.log
|
||||
```
|
||||
|
||||
启动后:
|
||||
1. 等待数字人 RTC 连接成功
|
||||
2. 开始连续对话至少 5 分钟(按 §1.3 测试场景)
|
||||
3. 至少触发一次明确卡顿(抖动/嗡嗡感)
|
||||
4. 触发后再保持采样 30 秒
|
||||
5. Ctrl+] 退出 monitor
|
||||
|
||||
**验收**:
|
||||
- 日志文件 `phase_08_diag.log` 已生成且 > 100KB
|
||||
- `grep -c "DIAG:" phase_08_diag.log` ≥ 500(5 分钟 × 多类日志)
|
||||
- 日志中至少有 1 处 `write_slow` 或 `queue=0` 频次密集出现
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.6 — 数据分析 + DIAG_REPORT.md
|
||||
|
||||
**文件**: 新建 `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/DIAG_REPORT.md`
|
||||
|
||||
**结构**:
|
||||
|
||||
```markdown
|
||||
# Phase 8 — 音频卡顿诊断报告
|
||||
|
||||
## 1. 测试环境
|
||||
- 日期 / 固件 commit / 路由器型号 / 房间距离
|
||||
|
||||
## 2. 卡顿瞬间日志片段
|
||||
(卡顿前 5s + 卡顿瞬间 + 卡顿后 5s 的 DIAG 行)
|
||||
|
||||
## 3. 指标统计
|
||||
| 指标 | 卡顿前 60s 平均 | 卡顿瞬间 | 卡顿后 60s 平均 |
|
||||
| queue | ? | ? | ? |
|
||||
| write_slow 次数 | ? | ? | ? |
|
||||
| rssi | ? | ? | ? |
|
||||
| free_int | ? | ? | ? |
|
||||
| largest_int / free_int | ? | ? | ? |
|
||||
|
||||
## 4. 根因判定
|
||||
对照 README §5 表格:__(① / ② / ③ / ④ / ⑤ / ⑥ 中的一个或多个)__
|
||||
|
||||
## 5. Phase 9 分支推荐
|
||||
推荐分支:__(A / B / C / D)__
|
||||
理由:...
|
||||
|
||||
## 6. 附录
|
||||
- 完整日志文件路径
|
||||
- 分析脚本(可选)
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `DIAG_REPORT.md` 存在
|
||||
- 第 4 节明确指出 ①-⑥ 中的一个或多个
|
||||
- 第 5 节明确给出 A/B/C/D 之一
|
||||
- 第 2 节附实际日志片段(不是占位符)
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.7 — 提交 commit
|
||||
|
||||
**前置**: 任务 2.1-2.6 全部验收通过
|
||||
|
||||
**改动文件清单**:
|
||||
- `main/application.cc`(埋点)
|
||||
- `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/PLAN.md`(本文件,已存在)
|
||||
- `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/DIAG_REPORT.md`
|
||||
- `.planning/milestones/digital_human_rtc/phases/phase_08_audio_glitch_diag/phase_08_diag.log`(实测日志)
|
||||
|
||||
**commit message**:
|
||||
```
|
||||
diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告
|
||||
|
||||
- 加 4 处 DIAG ESP_LOGW 埋点:queue 深度 / write_slow / rssi / heap
|
||||
- PHASE8_DIAG_ENABLE 一键开关,关闭后零运行时开销
|
||||
- 实测复现卡顿 + DIAG_REPORT.md 输出根因判定
|
||||
- Phase 9 实施分支推荐:{A/B/C/D}(待 DIAG_REPORT 填)
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `git log -1 --format=%s` 包含 "Phase 8"
|
||||
- `git show --stat HEAD` 列出预期 4 个文件
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务顺序
|
||||
|
||||
```
|
||||
2.1 顶部宏 → 2.2 queue 埋点 ─┐
|
||||
├→ 2.5 编译烧录 → 2.6 DIAG_REPORT → 2.7 commit
|
||||
2.3 write 埋点 ─┤
|
||||
2.4 1Hz 采样 ─┘
|
||||
```
|
||||
|
||||
2.2 / 2.3 / 2.4 可并行编辑,但都依赖 2.1 的宏定义。
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与回滚
|
||||
|
||||
| 风险 | 触发条件 | 缓解 / 回滚 |
|
||||
|---|---|---|
|
||||
| ESP_LOGW 自身耗时干扰测量 | 卡顿瞬间日志被自身拖累 | 50ms / 阈值 / 1Hz 已做节流;如仍不准,可改用 ringbuffer + 离线 dump |
|
||||
| `esp_wifi_sta_get_ap_info` 在 STA 未连接时 ENOENT | RTC 连接前调用 | 已用 `== ESP_OK` 守卫 |
|
||||
| heap fragmentation 计算有除零风险 | free_int = 0(极端情况)| 已加 `> 0` 守卫 |
|
||||
| 卡顿无法复现 | 测试场景不足 | 至少跑完 §1.3 三种场景;若仍不复现,扩展到 30 分钟压测 |
|
||||
| 编译期开关失效 | `#ifndef` 被覆盖 | 用 `grep "PHASE8_DIAG_ENABLE" main/application.cc` 确认且没有冲突 |
|
||||
| 一键 revert | Phase 9 完成后需要去除埋点 | 直接 `git revert <Phase8_commit>` 或 `#define PHASE8_DIAG_ENABLE 0` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 8 完成验收清单
|
||||
|
||||
- [ ] 任务 2.1-2.7 全部验收
|
||||
- [ ] `PHASE8_DIAG_ENABLE` 宏一键开关已就位
|
||||
- [ ] 实测日志 `phase_08_diag.log` 已采集(> 5 分钟 RTC 对话 + 至少 1 次卡顿)
|
||||
- [ ] `DIAG_REPORT.md` 明确根因判定(①-⑥ 之一或组合)
|
||||
- [ ] `DIAG_REPORT.md` 明确 Phase 9 实施分支推荐(A/B/C/D 之一)
|
||||
- [ ] commit 推送到 `Rtc_AIavatar` 分支
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 8 不做的事
|
||||
|
||||
- ❌ 不修改业务逻辑(pure observability)
|
||||
- ❌ 不引入 esp_emote_gfx 依赖
|
||||
- ❌ 不调整 sdkconfig(WiFi 缓冲、PSM、jitter buffer 都不动)
|
||||
- ❌ 不优化 lv_gif / LZW 解码(那是 Phase 9 的事)
|
||||
- ❌ 不写 audio_codecs/ 层埋点(写在 application.cc 端覆盖所有板子足够)
|
||||
- ❌ 不删除已有日志(DIAG 是新增 tag,不冲突)
|
||||
@ -0,0 +1,197 @@
|
||||
# Phase 8:数字人 RTC 音频卡顿根因诊断
|
||||
|
||||
**性质**:诊断 phase(仅埋点 + 数据采集,零业务逻辑改动)
|
||||
**预估工时**:半天
|
||||
**Depends on**:Phase 6(卡顿症状在 Phase 6 收尾发现)
|
||||
|
||||
> ⚠️ Phase 7 在 ROADMAP 旧版中标注为"集成测试 + 推送",实际已被 [phase_07_battery_psm](../phase_07_battery_psm/README.md)(电量保护 + 低功耗管理重构)占用。原"集成测试 + 推送" phase 后挪到 Phase 10。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景
|
||||
|
||||
Phase 6 收尾时观察到 RTC 数字人对话期间扬声器音频抖动/卡顿。已采取的对策**未能消除**卡顿:
|
||||
|
||||
- ✅ LVGL 任务绑 Core 0、音频循环绑 Core 1
|
||||
- ✅ GIF 定时器周期从 10ms 拉到 20ms
|
||||
- ✅ GIF 颜色压到 8 色减少 LZW CPU 开销
|
||||
- ✅ DMA flush 串行化到 `background_task_`
|
||||
- ✅ HTTPS 播放参数恢复通过 `background_task_->Schedule()` 串行
|
||||
|
||||
继续盲投优化(如切换 esp_emote_gfx EAF 框架或 WiFi 缓冲扩容)有 3-5 天工时风险,**方向选错就浪费**。
|
||||
|
||||
本 phase 通过**最小日志埋点**采集运行时数据,让根因数据驱动 Phase 9 的实施策略决策。
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标
|
||||
|
||||
通过 4 类 ESP_LOGW 日志埋点采集运行时数据,定位 RTC 音频卡顿的真实主要责任方,从下列候选根因中确认(或确认是组合):
|
||||
|
||||
| ID | 候选根因 | 验证手段 |
|
||||
|---|---|---|
|
||||
| ① | CPU 争抢(LVGL/GIF LZW 解码 vs Opus 解码) | 单帧 GIF 解码耗时 + audio queue 堆积 |
|
||||
| ② | PSRAM 带宽争抢 | free heap 变化 + cache miss 推断 |
|
||||
| ③ | I2S DMA flush 延迟 | `codec->Write()` 单次耗时 |
|
||||
| ④ | WiFi RX 丢包/重传 | RSSI + queue 空缺 |
|
||||
| ⑤ | Opus 帧到达抖动(网络层) | queue 长期空 + RSSI 正常 |
|
||||
| ⑥ | 内存碎片导致 malloc 卡顿 | free heap 抖动 + largest free block |
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务
|
||||
|
||||
### 任务 1 — 埋点:Opus 解码队列深度
|
||||
|
||||
**位置**:[main/application.cc](../../../../main/application.cc) 的 `OnAudioOutput` 入队/出队点
|
||||
|
||||
**实现**:
|
||||
```cpp
|
||||
// 每次入队或出队后
|
||||
if (esp_log_timestamp() - last_log_ms > 50) { // 50ms 节流
|
||||
last_log_ms = esp_log_timestamp();
|
||||
ESP_LOGW("DIAG", "queue=%d", audio_decode_queue_.size());
|
||||
}
|
||||
```
|
||||
|
||||
**目的**:观察 audio queue 在卡顿瞬间是空(网络/抖动)还是堆积(CPU/带宽)。
|
||||
|
||||
---
|
||||
|
||||
### 任务 2 — 埋点:PCM 写入耗时
|
||||
|
||||
**位置**:[main/application.cc](../../../../main/application.cc) 调用 `codec->Write(pcm, samples)` 前后
|
||||
|
||||
**实现**:
|
||||
```cpp
|
||||
int64_t t = esp_timer_get_time();
|
||||
codec->Write(pcm, samples);
|
||||
int64_t cost = esp_timer_get_time() - t;
|
||||
if (cost > 15000) { // > 15ms 才告警
|
||||
ESP_LOGW("DIAG", "write_slow %lldus", cost);
|
||||
}
|
||||
```
|
||||
|
||||
**目的**:识别 I2S/DMA 路径上的写入阻塞(根因 ③)。
|
||||
|
||||
---
|
||||
|
||||
### 任务 3 — 埋点:WiFi RSSI
|
||||
|
||||
**位置**:周期性任务(建议 1Hz),可挂在现有 `OnClockTimer` 上
|
||||
|
||||
**实现**:
|
||||
```cpp
|
||||
wifi_ap_record_t ap;
|
||||
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
|
||||
ESP_LOGW("DIAG", "rssi=%d", ap.rssi);
|
||||
}
|
||||
```
|
||||
|
||||
**目的**:判断 WiFi 信号是否参与卡顿(根因 ④⑤)。
|
||||
|
||||
---
|
||||
|
||||
### 任务 4 — 埋点:Internal/PSRAM 可用堆
|
||||
|
||||
**位置**:周期性任务(建议 1Hz),同 OnClockTimer
|
||||
|
||||
**实现**:
|
||||
```cpp
|
||||
ESP_LOGW("DIAG", "free_int=%d free_psram=%d largest_int=%d",
|
||||
heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
|
||||
heap_caps_get_free_size(MALLOC_CAP_SPIRAM),
|
||||
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
|
||||
```
|
||||
|
||||
**目的**:判断是否有内存碎片(根因 ⑥)或 PSRAM 带宽压力。
|
||||
|
||||
---
|
||||
|
||||
### 任务 5 — 触发卡顿采集
|
||||
|
||||
1. `idf.py build flash monitor` 烧录埋点版本
|
||||
2. 启动数字人 RTC 对话至少 **5 分钟**
|
||||
3. 主动触发数字人 GIF 切换(让 AI 说带情绪标签的话,如"(happy)你好"、"(sad)抱歉"等)观察切换瞬间
|
||||
4. 复现至少一次明确卡顿(抖动/嗡嗡感)
|
||||
5. 用 `idf.py monitor` 保存日志到文件:`monitor 2>&1 | tee phase_08_diag.log`
|
||||
|
||||
---
|
||||
|
||||
### 任务 6 — 数据分析报告
|
||||
|
||||
在本目录下产出 **`DIAG_REPORT.md`**,包含:
|
||||
|
||||
- **卡顿瞬间日志片段**:摘取卡顿前后 5 秒的 DIAG 日志
|
||||
- **根因判定**:对照「卡顿日志特征对照表」(见 §5),明确指认 ①-⑥ 哪一类或组合
|
||||
- **Phase 9 分支推荐**:A / B / C / D 之一(见 §6)
|
||||
- **附录**:完整日志文件或链接
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- ✅ 4 处日志埋点编译通过,正常输出 `DIAG` tag 日志
|
||||
- ✅ 实际复现一次卡顿,采集到包含卡顿瞬间的日志
|
||||
- ✅ 写出 `DIAG_REPORT.md`,明确判定根因属于 ①-⑥ 哪一类(或组合)
|
||||
- ✅ 给出 Phase 9 实施分支推荐(A/B/C/D 之一)
|
||||
- ✅ 不引入业务逻辑改动,可一键 revert 埋点代码
|
||||
|
||||
---
|
||||
|
||||
## 5. 卡顿日志特征对照表
|
||||
|
||||
| 日志特征 | 推断根因 | Phase 9 分支 |
|
||||
|---|---|---|
|
||||
| `queue=0` 频繁出现 + `rssi < -70` | WiFi 丢包 | B |
|
||||
| `queue=0` 频繁出现 + `rssi 正常` | Opus 帧抖动 | B |
|
||||
| `write_slow` 频繁 | DMA/I2S 问题 | D |
|
||||
| `queue` 堆积(>5)+ `write_slow` 偶发 | CPU/带宽争抢 | A 或 C |
|
||||
| `largest_int` 持续下降 | 内存碎片 | 单独修复 |
|
||||
| 全都正常但听感差 | 心理学/采样率,与代码无关 | 取消 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 9 实施分支预案
|
||||
|
||||
| 分支 | 根因 | 实施动作 |
|
||||
|---|---|---|
|
||||
| **A** | 仅 CPU 争抢 ① | 用 esp_emote_gfx 的 `eaf_dec_*` 解码器替换 `lv_gif`(LZW),保留 LVGL 框架,旁路渲染到 `lv_canvas`。预估 1-2 天 |
|
||||
| **B** | 仅 WiFi/网络 ④⑤ | WiFi 缓冲扩容(`CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM` 10→16、`DYNAMIC_RX/TX_BUFFER_NUM` 32→48、`RX_BA_WIN` 6→16)。预估 0.5 天 |
|
||||
| **C** | 组合 ①+④⑤ | 数字人模式完整切 EAF(仅 `CONFIG_BAJI_BADGE_MODE=n` 分支):弃用 LVGL,重写 `ai_chat_ui.c` 用 `gfx_obj/gfx_anim/gfx_label`,释放 ~40KB DRAM + ~80KB PSRAM,资源投到 WiFi RX + Opus jitter + RTC SDK jitter 扩容。预估 3-5 天 |
|
||||
| **D** | DMA/I2S ③ | 取消 EAF 方案,转 DMA 路径排查(与 EAF/WiFi 无关)。预估视具体问题 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 不在范围
|
||||
|
||||
- 不修改业务逻辑(纯埋点)
|
||||
- 不引入 esp_emote_gfx 依赖
|
||||
- 不调整 sdkconfig(WiFi 缓冲、jitter buffer 都不动)
|
||||
- 不删除已有日志(埋点是新增)
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险点
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| ESP_LOGW 走 UART 输出本身有耗时(~100us),可能影响时序观察 | 50ms 节流,避免高频打印;必要时切到 USB CDC 高速通道 |
|
||||
| WiFi 信号采样频率过高拖累性能 | 限制 1Hz 即可,足以观察 RSSI 趋势 |
|
||||
| 复现卡顿需要特定网络/对话场景 | 至少 5 分钟对话 + 主动触发 GIF 切换 + 让 AI 说情绪标签句 |
|
||||
| 日志埋点本身改变测量结果(观察者效应) | 必要时改用 ringbuffer + 离线 dump,减少在线 IO 开销 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 产出 commit
|
||||
|
||||
```
|
||||
diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 后续 phase
|
||||
|
||||
- **Phase 9**:根据本 phase `DIAG_REPORT.md` 的根因结论,按 §6 分支预案选定实施方案(A/B/C/D)
|
||||
- **Phase 10**(原 ROADMAP 中的 Phase 7):集成测试 + 推送
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,71 @@
|
||||
# Phase 9 — 已取消
|
||||
|
||||
**取消日期**:2026-05-15
|
||||
**取消决策方**:用户
|
||||
**取消原因**:增量修补策略(jitter buffer + codec init 调整)效果不明显,用户改为方案 C 完整切 EAF(Phase 10)以验证 GIF/LVGL 是否真是元凶。
|
||||
|
||||
---
|
||||
|
||||
## 三轮尝试教训
|
||||
|
||||
### v1(初版)
|
||||
**改动**:
|
||||
- 9.1 jitter buffer:`OnAudioOutput` 加 FILLING/DRAINING 状态机(PREBUF=4 / OVERFLOW=12)
|
||||
- 9.2.1 ES7210 init 3 次重试 + 失败降级
|
||||
- 9.2.2 PlaySound 前 vTaskDelay(150ms)
|
||||
|
||||
**实测问题**:
|
||||
1. **jitter buffer 完全没生效** — 日志中 0 处 "Jitter buffer 蓄水完成"
|
||||
- 根因:项目 RTC 对话状态用 `kDeviceStateDialog`(不是 Speaking/Listening)
|
||||
- 我的判定漏了 Dialog → 在对话期完全不进 jitter 分支
|
||||
2. **开机播报听不到声音** — 新 regression
|
||||
- 根因:ES7210 重试逻辑改变 I2C 总线时序 → ES8311 (`dev 30`) 写入失败
|
||||
- Phase 8 baseline 时 ES7210 失败会触发 assert reboot 自愈;v1 让 ES7210 不失败反而破坏 ES8311 init
|
||||
|
||||
### v2(hotfix)
|
||||
**改动**:
|
||||
- jitter buffer 判定加 `kDeviceStateDialog`
|
||||
- 回退 9.2.1(恢复 Phase 8 baseline 的 assert 模式)
|
||||
- 保留 9.2.2 (150ms 延迟)
|
||||
|
||||
**实测**:
|
||||
- jitter buffer 工作了(15+ 次 "蓄水完成开始消费 (q=4)")✓
|
||||
- 对话期 `write_slow=0` ✓
|
||||
- 开机播报恢复正常 ✓
|
||||
- **但用户主观仍然非常卡顿** ✗
|
||||
|
||||
**核心数据**:
|
||||
- 对话期 (30s+) queue 采样 859 次
|
||||
- 平均 3.6 / 最大 22 / queue=0 出现 52 次 / queue≥12 出现 28 次(远超 OVERFLOW)
|
||||
|
||||
### v3(深挖)
|
||||
**新假设**:`background_task` 使用 `xTaskCreate` **未绑核** → 可能跑 Core 0 与 LVGL/GIF 抢调度。
|
||||
|
||||
**改动**:
|
||||
- `background_task.cc` 改用 `xTaskCreatePinnedToCore(..., 1)` 强制 Core 1
|
||||
- 加 DIAG-5 `bg_lag` 埋点测调度延迟
|
||||
- jitter buffer 调大 PREBUF 4→6, OVERFLOW 12→24
|
||||
|
||||
**结果**:未来得及硬件实测 — 用户决策放弃增量修补改走 Phase 10。
|
||||
|
||||
---
|
||||
|
||||
## 关键教训
|
||||
|
||||
1. **device_state 名字不能凭印象写**,务必先 grep 项目实际使用
|
||||
2. **修改一个 codec init 链路时,要预想对邻近 codec 的连锁影响**(共享 I2C 总线 / 时钟)
|
||||
3. **DIAG-2 测 codec write 耗时是不够的** — 测不到 background_task schedule 延迟
|
||||
4. **增量优化策略容易越改越乱**,当假设需要双线/三线修复时,应考虑是否方向本身不对
|
||||
5. **用户的物理直觉值得尊重** — "kapi 项目底座 RTC 流畅 → 数字人模式卡顿" 这个对比本身就是强证据,指向新增的 GIF/LVGL
|
||||
|
||||
---
|
||||
|
||||
## 回滚记录
|
||||
|
||||
- 所有代码改动通过 `git restore` 回滚(无 commit)
|
||||
- 影响文件:
|
||||
- `main/application.cc/h`
|
||||
- `main/audio_codecs/box_audio_codec.cc`
|
||||
- `main/background_task.cc`
|
||||
- Phase 8 commit `3dc6cad` 保留(DIAG 埋点继续作为 Phase 10 验证基准)
|
||||
- 本目录 `phase_09_audio_jitter_codecinit/` 全部文档(PLAN.md / IMPL_REPORT 模板 / 多版本日志)保留供回溯
|
||||
@ -0,0 +1,361 @@
|
||||
# Phase 9 PLAN — 音频卡顿双线修复(jitter buffer + codec init 时序)
|
||||
|
||||
> 里程碑: `digital_human_rtc`
|
||||
> 阶段目标: 基于 Phase 8 [DIAG_REPORT.md](../phase_08_audio_glitch_diag/DIAG_REPORT.md) 双根因判定 ③'+⑤,双线修复 RTC 对话期 jitter + 开机 codec init 时序。
|
||||
> 性质: **实施 phase**,纯软件改动,不引入 esp_emote_gfx / 不调整 sdkconfig。
|
||||
|
||||
---
|
||||
|
||||
## 0. 决策依据(DIAG_REPORT 摘要)
|
||||
|
||||
- 主观感知:A. 开机"卡卡在呢"声音抖 + B. RTC 对话期 AI 回答断续
|
||||
- **A 根因 ③'**:ES7210 I2C 0x80 写入失败 → 2-13s 集中 126 次 `write_slow`(50-58ms/帧)
|
||||
- **B 根因 ⑤**:WebSocket Opus 帧应用层突发到达(queue=19 突发 + queue=0 出现 58 次 @ 40s+)
|
||||
- 排除:CPU 争抢 / PSRAM 带宽 / WiFi 物理层 / 内存碎片
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计方案
|
||||
|
||||
### 1.1 子任务 9.1 — 应用层 jitter buffer(解 B)
|
||||
|
||||
**位置**: [main/application.cc](../../../../main/application.cc) `Application::OnAudioOutput()`
|
||||
|
||||
**机制**(fill-threshold + drain + overflow drop):
|
||||
```
|
||||
状态机:
|
||||
┌─ FILLING ──q ≥ PREBUF ───▶ DRAINING ─q = 0─▶ FILLING ─┐
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
参数:
|
||||
PREBUF = 4 帧 (60ms × 4 = 240ms 预蓄水)
|
||||
OVERFLOW = 12 帧 (60ms × 12 = 720ms 上限,超过丢最老 1 帧)
|
||||
|
||||
行为:
|
||||
FILLING: audio_decode_queue_.empty() 一直 return; 等填满 PREBUF 帧
|
||||
DRAINING: 正常 pop_front + 解码 + 输出
|
||||
DRAINING + size > OVERFLOW: 主动 pop_front 丢老帧(防止永久堆积)
|
||||
DRAINING + queue empty: 切回 FILLING(重新蓄水)
|
||||
```
|
||||
|
||||
**仅在 RTC 对话期生效**:device_state == kDeviceStateSpeaking 或 kDeviceStateListening
|
||||
- 开机播报 PlaySound 路径 **不走** jitter buffer(一次性灌入数据,预蓄水反而拖累首帧)
|
||||
- 通过 protocol 状态或 device_state 判断
|
||||
|
||||
**关键参数推导**(基于 Phase 8 DIAG 数据):
|
||||
- queue=19 突发观察到 4 帧(45039/45089/45199 enq 集中)
|
||||
- PREBUF=4 留出 240ms 抗抖,能吞下一次"突发-消化"周期
|
||||
- OVERFLOW=12 防止极端情况(≥720ms 堆积明显延迟,丢帧重新对齐)
|
||||
|
||||
### 1.2 子任务 9.2 — 开机 codec init 时序加固(解 A)
|
||||
|
||||
**位置**:
|
||||
- [main/audio_codecs/box_audio_codec.cc](../../../../main/audio_codecs/box_audio_codec.cc) ES7210 init 重试 + 错误降级
|
||||
- [main/application.cc](../../../../main/application.cc) `Application::Start()` 中 PlaySound 前等待 codec 稳定
|
||||
|
||||
**改动 1 — ES7210 init 失败重试**(BoxAudioCodec 构造函数 line 73-82):
|
||||
```cpp
|
||||
// 当前:
|
||||
in_codec_if_ = es7210_codec_new(&es7210_cfg);
|
||||
assert(in_codec_if_ != NULL); // ✗ assert 失败直接 abort
|
||||
|
||||
// 改造为:3 次重试 + 失败降级
|
||||
for (int retry = 0; retry < 3; retry++) {
|
||||
in_codec_if_ = es7210_codec_new(&es7210_cfg);
|
||||
if (in_codec_if_ != NULL) break;
|
||||
ESP_LOGW(TAG, "ES7210 init failed, retry %d/3", retry + 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
if (in_codec_if_ == NULL) {
|
||||
ESP_LOGE(TAG, "ES7210 init permanently failed, fallback to output-only");
|
||||
// 不 abort,置为 output_only 等价状态
|
||||
}
|
||||
```
|
||||
|
||||
**改动 2 — Application::Start 中 codec ready 等待**(PlaySound "开机播报语音" 之前):
|
||||
```cpp
|
||||
// 当前:
|
||||
codec->EnableInput(true);
|
||||
codec->EnableOutput(true);
|
||||
PlaySound(...); // ✗ 立即播报,可能 codec 仍在 init
|
||||
|
||||
// 改造为:等 codec 稳定后再播报
|
||||
codec->EnableInput(true);
|
||||
codec->EnableOutput(true);
|
||||
vTaskDelay(pdMS_TO_TICKS(150)); // 给 I2S DMA + codec 稳定时间
|
||||
PlaySound(...);
|
||||
```
|
||||
|
||||
**改动 3 - 不引入新的 codec ready 标志**(不动 audio_codec.h 接口):
|
||||
- 150ms 是经验值,足以让 ES8311 → I2S DMA → ES7210 stabilize
|
||||
- 如果用户测下来仍卡,再升级到 codec ready callback 机制
|
||||
|
||||
---
|
||||
|
||||
## 2. 任务清单
|
||||
|
||||
### 任务 9.1.1 — 在 Application 类中加 jitter buffer 状态成员
|
||||
|
||||
**文件**: [main/application.h](../../../../main/application.h)
|
||||
|
||||
**读取参考**:
|
||||
- 现有 private 成员变量布局
|
||||
- audio_decode_queue_ 声明
|
||||
|
||||
**改动**: 在 audio_decode_queue_ 附近加:
|
||||
```cpp
|
||||
// Phase 9.1: 应用层 jitter buffer(解 RTC 对话期 Opus 帧到达抖动)
|
||||
enum class JitterState : uint8_t { FILLING = 0, DRAINING = 1 };
|
||||
JitterState jitter_state_ = JitterState::FILLING;
|
||||
static constexpr int kJitterPrebufFrames = 4; // 预蓄水阈值
|
||||
static constexpr int kJitterOverflowFrames = 12; // 上限丢帧阈值
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `grep -nE "jitter_state_|kJitterPrebufFrames|kJitterOverflowFrames" main/application.h` 各返回 ≥ 1
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.1.2 — OnAudioOutput 中插入 jitter buffer 状态机
|
||||
|
||||
**文件**: [main/application.cc](../../../../main/application.cc) 的 `OnAudioOutput()`
|
||||
|
||||
**读取参考**:
|
||||
- 当前 `if (audio_decode_queue_.empty()) return;` 分支(约 2126 行)
|
||||
- 当前 `auto opus = std::move(audio_decode_queue_.front());` 出队点(约 2167 行)
|
||||
- DIAG-1 出队埋点位置
|
||||
|
||||
**改动**(在出队前插入状态机判定):
|
||||
|
||||
```cpp
|
||||
// 在 audio_decode_queue_.empty() 处理之后、device_state 检查之前插入
|
||||
|
||||
// Phase 9.1: 应用层 jitter buffer 状态机(仅对话期生效,开机播报不走)
|
||||
bool is_rtc_audio = (device_state_ == kDeviceStateSpeaking ||
|
||||
device_state_ == kDeviceStateListening) &&
|
||||
opus_playback_active_.load() == false; // 不与 HTTPS 抢
|
||||
if (is_rtc_audio) {
|
||||
int q = (int)audio_decode_queue_.size();
|
||||
if (jitter_state_ == JitterState::FILLING) {
|
||||
if (q < kJitterPrebufFrames) {
|
||||
return; // 蓄水中,不消费
|
||||
}
|
||||
jitter_state_ = JitterState::DRAINING;
|
||||
ESP_LOGI(TAG, "Jitter buffer 蓄水完成开始消费 (q=%d)", q);
|
||||
} else { // DRAINING
|
||||
if (q > kJitterOverflowFrames) {
|
||||
// 上限保护:丢最老 1 帧
|
||||
audio_decode_queue_.pop_front();
|
||||
ESP_LOGW(TAG, "Jitter buffer 超限丢帧 (q=%d)", q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 出队(原有逻辑)
|
||||
auto opus = std::move(audio_decode_queue_.front());
|
||||
audio_decode_queue_.pop_front();
|
||||
// ...
|
||||
```
|
||||
|
||||
**重置点**(empty 分支末尾,准备返回前):
|
||||
```cpp
|
||||
if (audio_decode_queue_.empty()) {
|
||||
// ... 原有逻辑 ...
|
||||
|
||||
// Phase 9.1: 队列变空 → 切回 FILLING 重新蓄水
|
||||
if (jitter_state_ == JitterState::DRAINING) {
|
||||
jitter_state_ = JitterState::FILLING;
|
||||
ESP_LOGD(TAG, "Jitter buffer 重置为 FILLING");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `grep -c "Jitter buffer" main/application.cc` ≥ 3
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.1.3 — 状态切换时复位 jitter(避免转入对话即受历史影响)
|
||||
|
||||
**文件**: [main/application.cc](../../../../main/application.cc)
|
||||
|
||||
**读取参考**:
|
||||
- `SetDeviceState(kDeviceStateSpeaking)` / `SetDeviceState(kDeviceStateListening)` 等转换点
|
||||
|
||||
**改动**: 在转入 Listening 或 Speaking 状态时显式置 FILLING:
|
||||
- 找 `void Application::SetDeviceState(DeviceState state)` 入口
|
||||
- 在状态切换时若新状态是 Speaking/Listening,置 `jitter_state_ = JitterState::FILLING`
|
||||
|
||||
**验收**:
|
||||
- 切换 RTC 对话开始时日志能看到一次 "Jitter buffer 蓄水完成开始消费"
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.2.1 — ES7210 init 重试 + 不 abort
|
||||
|
||||
**文件**: [main/audio_codecs/box_audio_codec.cc](../../../../main/audio_codecs/box_audio_codec.cc)
|
||||
|
||||
**读取参考**:
|
||||
- line 73-82: 当前 ES7210 init 路径
|
||||
- `assert(in_codec_if_ != NULL)` 失败行为
|
||||
|
||||
**改动**: 将 line 76 `in_codec_if_ = es7210_codec_new(&es7210_cfg);` + `assert` 替换为重试循环(详细代码见 §1.2 改动 1)
|
||||
|
||||
**注意**:
|
||||
- 重试 3 次仍失败时**不 abort**,但应该把 `input_dev_` 置为 nullptr 避免后续 `esp_codec_dev_open(input_dev_)` 崩
|
||||
- 同样把 line 80 `dev_cfg.codec_if = in_codec_if_;` + line 82 `assert(input_dev_ != NULL);` 也要相应处理(in_codec_if_ 为 null 时不创建 input_dev_)
|
||||
|
||||
**验收**:
|
||||
- `grep -c "ES7210 init failed" main/audio_codecs/box_audio_codec.cc` = 1
|
||||
- 编译通过
|
||||
- 烧录后即使 ES7210 init 失败也不会 reboot 循环(保留向后兼容)
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.2.2 — PlaySound 前等 codec 稳定
|
||||
|
||||
**文件**: [main/application.cc](../../../../main/application.cc) `Application::Start()`
|
||||
|
||||
**读取参考**:
|
||||
- `AudioCodec: 将运行时输出音量设置为:80` 之后到 `PlaySound` 之前
|
||||
- 当前路径:"设备启动完成,播放开机播报语音"
|
||||
|
||||
**改动**: 在 PlaySound 调用之前添加 150ms 延迟。具体定位关键字 "设备启动完成" 附近。
|
||||
|
||||
**验收**:
|
||||
- `grep -nE "vTaskDelay.*150.*codec|codec 稳定等待" main/application.cc` 返回 1 行
|
||||
- 编译通过
|
||||
- 烧录后日志:开机播报阶段 `write_slow` 次数应显著减少(从 126 次降到 <20 次)
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.3 — 编译 + 烧录 + 验证(用 Phase 8 DIAG 埋点做对比)
|
||||
|
||||
**前置**: 9.1.x + 9.2.x 全部任务完成
|
||||
|
||||
**步骤**:
|
||||
```bash
|
||||
source ~/esp/esp-idf/export.sh
|
||||
idf.py build flash monitor 2>&1 | tee .planning/milestones/digital_human_rtc/phases/phase_09_audio_jitter_codecinit/phase_09_diag.log
|
||||
```
|
||||
|
||||
测试场景与 Phase 8 一致:5 分钟 RTC 对话 + 主动 GIF 切换。
|
||||
|
||||
**关键对比指标**(vs Phase 8 baseline):
|
||||
|
||||
| 指标 | Phase 8 baseline | Phase 9 目标 |
|
||||
|---|---|---|
|
||||
| 开机播报 write_slow (2-13s) | 126 次 | **< 20 次** |
|
||||
| 对话期 queue=0 次数 | 58 (40s+) | **< 15** |
|
||||
| 对话期 queue max | 19 | **< 8** |
|
||||
| 对话期 write_slow | 0 | 保持 0 |
|
||||
| 用户主观:开机"卡卡在呢"听感 | 抖 | **不抖** |
|
||||
| 用户主观:对话期 AI 声音 | 断续 | **连贯** |
|
||||
|
||||
**Jitter buffer 工作日志样本**(应能看到):
|
||||
```
|
||||
I Jitter buffer 蓄水完成开始消费 (q=4)
|
||||
... 几十秒正常消费 ...
|
||||
W Jitter buffer 超限丢帧 (q=13) ← 偶发但合理
|
||||
D Jitter buffer 重置为 FILLING ← AI 说话间隙
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 9.4 — 产出 IMPL_REPORT.md + commit
|
||||
|
||||
**文件**: 新建 `.planning/milestones/digital_human_rtc/phases/phase_09_audio_jitter_codecinit/IMPL_REPORT.md`
|
||||
|
||||
**结构**:
|
||||
```markdown
|
||||
# Phase 9 — 实施报告
|
||||
|
||||
## 1. 改动概览
|
||||
- 9.1 jitter buffer:3 处代码 + N 行
|
||||
- 9.2 codec init:2 处代码 + N 行
|
||||
|
||||
## 2. 实测对比表(Phase 8 vs Phase 9)
|
||||
| 指标 | Phase 8 | Phase 9 | 改善 |
|
||||
|
||||
## 3. 用户主观验证
|
||||
- 开机播报听感:☐ 不抖 / ☐ 仍抖
|
||||
- 对话期 AI 声音:☐ 连贯 / ☐ 仍断续
|
||||
|
||||
## 4. 残留问题(若有)
|
||||
...
|
||||
|
||||
## 5. PHASE8_DIAG_ENABLE 是否关闭
|
||||
☐ 已关 / ☐ 保留待 Phase 10 集成测试
|
||||
```
|
||||
|
||||
**commit message**:
|
||||
```
|
||||
perf(rtc-only): Phase 9 - 音频卡顿双线修复(jitter buffer + codec init)
|
||||
|
||||
9.1 应用层 jitter buffer(解决 ⑤ Opus 帧到达抖动):
|
||||
- OnAudioOutput 加 FILLING/DRAINING 状态机
|
||||
- PREBUF=4 帧(240ms 预蓄水)+ OVERFLOW=12 帧(720ms 上限丢帧)
|
||||
- 仅 RTC 对话期生效,开机播报路径绕过
|
||||
|
||||
9.2 开机 codec init 时序加固(解决 ③' ES7210 I2C 失败):
|
||||
- ES7210 init 失败 3 次重试 + 失败降级不 abort
|
||||
- PlaySound 前 150ms codec 稳定等待
|
||||
|
||||
实测对比(详 IMPL_REPORT.md):
|
||||
- 开机 write_slow 126→{N}
|
||||
- 对话期 queue=0 出现 58→{M}
|
||||
- 对话期 queue max 19→{X}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务顺序
|
||||
|
||||
```
|
||||
9.1.1 (header 成员) → 9.1.2 (OnAudioOutput) → 9.1.3 (状态切换重置)
|
||||
↓
|
||||
9.2.1 (ES7210 重试) → 9.2.2 (PlaySound 等待) ──┴→ 9.3 (build+flash 验证) → 9.4 (commit)
|
||||
```
|
||||
|
||||
9.1 / 9.2 可并行编辑,统一在 9.3 build。
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与回滚
|
||||
|
||||
| 风险 | 触发 | 缓解 |
|
||||
|---|---|---|
|
||||
| PREBUF=4 蓄水延迟首字 240ms | 用户感觉 AI"反应慢" | 这是设计权衡:换抖动减少。240ms 远小于 AI 思考延迟(秒级),不会显著感知 |
|
||||
| OVERFLOW=12 丢帧导致句子缺词 | 网络长时间堆积 | 720ms 才丢,且只丢老帧(用户最早感知的"音频已过期"),不丢最新 |
|
||||
| ES7210 重试 150ms 拖慢开机 | 用户感觉开机变慢 | 50ms × 3 = 最多 150ms,可接受 |
|
||||
| PlaySound 等 150ms 让开机播报变慢 | 用户感觉开机变慢 | 150ms 不可感知 |
|
||||
| jitter buffer 干扰 HTTPS 音频播放 | HTTPS(故事/音乐)走 audio_decode_queue_ 同一通道 | `is_rtc_audio` 判定加 `opus_playback_active_ == false` 排除 HTTPS |
|
||||
| 回滚 | 9.1 或 9.2 任一引入 regression | 单独 `git revert` 即可(独立子任务) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 9 完成验收清单
|
||||
|
||||
- [ ] 9.1.1-9.2.2 五个子任务全部代码就位
|
||||
- [ ] `idf.py build` 通过
|
||||
- [ ] 烧录 + RTC 对话 ≥ 5 分钟
|
||||
- [ ] 开机 `write_slow` 次数 < 20(vs baseline 126)
|
||||
- [ ] 对话期 `queue=0` 次数 < 15(vs baseline 58)
|
||||
- [ ] 用户主观:开机"卡卡在呢"不抖 + 对话 AI 声音连贯
|
||||
- [ ] IMPL_REPORT.md 填实数据
|
||||
- [ ] commit 推送到 Rtc_AIavatar
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 9 不做的事
|
||||
|
||||
- ❌ 不引入 esp_emote_gfx(Phase 8 数据否决了 CPU 假设)
|
||||
- ❌ 不动 `CONFIG_ESP_WIFI_*_BUFFER_NUM`(物理层 OK)
|
||||
- ❌ 不深挖 ES7210 寄存器级 init 问题(重试 + 降级足够,深层修复留待后续)
|
||||
- ❌ 不关闭 PHASE8_DIAG_ENABLE(保留埋点便于 Phase 9 验证)
|
||||
- ❌ 不动 LVGL/GIF 相关代码
|
||||
@ -0,0 +1,351 @@
|
||||
# Phase 10 PLAN — 数字人模式 LVGL → esp_emote_gfx 完整切换
|
||||
|
||||
> 里程碑: `digital_human_rtc`
|
||||
> 阶段目标: `CONFIG_BAJI_BADGE_MODE=n` 时数字人模式完全弃用 LVGL,采用乐鑫 esp_emote_gfx + EAF 动画格式,验证 GIF 抢资源假设是否成立。吧唧模式保持 LVGL。
|
||||
> 预估工时: 3-5 天(含 PoC 显示效果验证 + 完整 UI 切换)
|
||||
|
||||
---
|
||||
|
||||
## 0. 调研结论
|
||||
|
||||
### 0.1 esp_emote_gfx 核心 API(已实地调查)
|
||||
|
||||
- 组件名: `espressif2022/esp_emote_gfx` v3.0.5
|
||||
- 入口: `gfx_emote_init() / gfx_emote_add_disp()` 接管显示
|
||||
- 控件: `gfx_obj` 基类 + `gfx_anim / gfx_img / gfx_label / gfx_button / gfx_qrcode`
|
||||
- EAF 格式: magic `0x5A5A`, 4/8/24-bit 调色板, RLE/Huffman/Heatshrink/RAW 多种编码,block-based decode,原生透明(调色板 idx 0)
|
||||
- 文档: `espressif2022.github.io/esp_emote_gfx/zh_CN/index.html`
|
||||
- 工具: ESP Emote GFX Packer `emote-gfx-gen-tool-dev.pages.dev`
|
||||
|
||||
### 0.2 项目数字人模式 UI 边界(已确认)
|
||||
|
||||
数字人模式下 UI 只有 [main/dzbj/ai_chat_ui.c](../../../../main/dzbj/ai_chat_ui.c) (458 行) 和 [main/display/lcd_display.cc](../../../../main/display/lcd_display.cc) 数字人分支,**7 个 LVGL 对象**:
|
||||
|
||||
| 控件 | EAF 对应 | 难度 |
|
||||
|---|---|---|
|
||||
| `ai_screen` (lv_obj 根容器) | `gfx_obj`(disp 根)| 低 |
|
||||
| `gif_emotion` (lv_gif 主数字人) | `gfx_anim` + EAF 资源 | 低 |
|
||||
| `gif_icon` (lv_gif 叠加图标) | `gfx_anim` + EAF 资源 | 低 |
|
||||
| `emoji_img` (lv_img 静态) | `gfx_img` + RGB565A8 | 低 |
|
||||
| `status_label` (lv_label) | `gfx_label` | 低 |
|
||||
| `chat_container` (lv_obj 字幕背景) | `gfx_obj` 容器 | 低 |
|
||||
| `chat_label` (lv_label 字幕 312×48 + 自动换行 + 居中) | `gfx_label` | **⚠️ 中文换行风险点** |
|
||||
|
||||
### 0.3 关键风险(必须先 PoC 验证)
|
||||
|
||||
1. **gfx_label 中文自动换行 + 双行居中**:未在 esp_emote_gfx 公开文档中明确说明,需 PoC
|
||||
2. **font_puhui_20_4.c 复用**:8.5MB LVGL bitmap font,EAF 文档说支持 LVGL bitmap font,但实际兼容性需验证
|
||||
3. **display 接管冲突**:数字人模式下 LCD panel 必须只由 esp_emote_gfx 接管(移除 lvgl_port)
|
||||
|
||||
### 0.4 双轨架构
|
||||
|
||||
| 编译条件 | UI 框架 | 涉及文件 |
|
||||
|---|---|---|
|
||||
| `CONFIG_BAJI_BADGE_MODE=y` | LVGL(原有) | `lcd_display.cc` + `ui/screens/*` + `ai_chat_ui.c` (LVGL 版本) |
|
||||
| `CONFIG_BAJI_BADGE_MODE=n` | **esp_emote_gfx** | 新增 `eaf_display.cc/h` + 新版 ai_chat_ui (EAF) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计方案
|
||||
|
||||
### 1.1 文件结构(新增)
|
||||
|
||||
```
|
||||
main/display/
|
||||
display.h 保留(虚基类,接口不变)
|
||||
lcd_display.cc/h 保留(LVGL,吧唧模式专用)
|
||||
eaf_display.cc/h ← 新增(esp_emote_gfx,数字人模式专用)
|
||||
|
||||
main/dzbj/
|
||||
ai_chat_ui.c 保留(LVGL 版本,吧唧模式用)
|
||||
ai_chat_ui_eaf.c ← 新增(EAF 版本,数字人模式用)
|
||||
|
||||
main/boards/movecall-moji-esp32s3/
|
||||
movecall_moji_esp32s3.cc 修改:根据 BAJI_BADGE_MODE 实例化 LcdDisplay 或 EafDisplay
|
||||
|
||||
spiffs_image/
|
||||
hiyori_m03.gif → hiyori_m03.eaf ← 离线工具转换
|
||||
hiyori_m06.gif → hiyori_m06.eaf
|
||||
hiyori_m07.gif → hiyori_m07.eaf
|
||||
```
|
||||
|
||||
### 1.2 CMakeLists 条件编译
|
||||
|
||||
```cmake
|
||||
# main/CMakeLists.txt
|
||||
if(CONFIG_BAJI_BADGE_MODE)
|
||||
list(APPEND srcs
|
||||
"display/lcd_display.cc"
|
||||
"dzbj/ai_chat_ui.c"
|
||||
# ... LVGL UI screens ...
|
||||
)
|
||||
else()
|
||||
# 数字人模式:EAF 路径
|
||||
list(APPEND srcs
|
||||
"display/eaf_display.cc"
|
||||
"dzbj/ai_chat_ui_eaf.c"
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
### 1.3 Display 接口适配
|
||||
|
||||
`display.h` 现有虚函数接口(`SetStatus / SetEmotion / SetChatMessage / ...`)**不动**。
|
||||
`EafDisplay : public Display` 实现这些函数,内部用 gfx_label / gfx_anim 等。
|
||||
|
||||
### 1.4 实施策略:分阶段 PoC
|
||||
|
||||
为了快速看到显示效果,分两步:
|
||||
- **PoC 阶段(10.1-10.3)**:最小可行 —— 加依赖 + 转一个 GIF + 写最小 EafDisplay 只显示 hiyori GIF。**烧录看显示**。
|
||||
- **完整阶段(10.4-10.8)**:扩展字幕、状态、emoji、CMakeLists 切换。
|
||||
|
||||
---
|
||||
|
||||
## 2. 任务清单
|
||||
|
||||
### 任务 10.1 — 添加 esp_emote_gfx 组件依赖
|
||||
|
||||
**文件**: `main/idf_component.yml`
|
||||
|
||||
**改动**: 在 dependencies 中追加:
|
||||
```yaml
|
||||
## Phase 10: 数字人模式 EAF UI(替代 LVGL)
|
||||
espressif2022/esp_emote_gfx: "~3.0.5"
|
||||
```
|
||||
|
||||
**验收**:
|
||||
- `idf.py reconfigure` 成功拉取组件
|
||||
- `managed_components/espressif2022__esp_emote_gfx/` 目录存在
|
||||
- 头文件 `core/gfx_emote.h` 可被引用
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.2 — gfx_label 中文换行 PoC
|
||||
|
||||
**目的**: 验证最大风险点 —— gfx_label 是否支持中文自动换行 + 双行居中。**如失败需重新设计字幕方案**。
|
||||
|
||||
**手段**: 不动主代码,单独写小测试:
|
||||
1. 拉 `managed_components/espressif2022__esp_emote_gfx/test_apps/` 看官方测试
|
||||
2. 找 `gfx_label_set_long_mode` / `gfx_label_set_text_align` 等 API
|
||||
3. 短上下文 PoC:创建 gfx_label,文本 "(happy)今天天气真好这是一段需要换行测试的字幕",宽度 312px,看是否自动换行 + 居中
|
||||
|
||||
**验收**:
|
||||
- 找到 gfx_label 的换行 API(如 `gfx_label_set_long_mode(obj, GFX_LABEL_LONG_WRAP)`)
|
||||
- 文本能自动换行(至少 2 行)
|
||||
- 文本居中显示
|
||||
|
||||
**降级方案**(如换行不支持): 主代码端手动按字符宽度切分 + 创建两个 gfx_label 分别显示上下行
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.3 — EAF Packer 转换 hiyori_m06.gif(PoC 阶段)
|
||||
|
||||
**前置**: ESP Emote GFX Packer 工具
|
||||
- 在线: `emote-gfx-gen-tool-dev.pages.dev`
|
||||
- 或 GitHub Releases 下载 CLI
|
||||
|
||||
**步骤**:
|
||||
1. 上传 `spiffs_image/hiyori_m06.gif`(PoC 先转一个)
|
||||
2. 配置: 4-bit palette + Heatshrink + 保留透明
|
||||
3. 下载 `hiyori_m06.eaf`,放到 `spiffs_image/`
|
||||
|
||||
**验收**:
|
||||
- `spiffs_image/hiyori_m06.eaf` 存在
|
||||
- 文件大小 < 原 gif(应该明显更小)
|
||||
- `head -c 4 hiyori_m06.eaf` 显示 magic `0x5A 0x5A`
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.4 — 最小 EafDisplay PoC(只显示 hiyori_m06.eaf)
|
||||
|
||||
**新建**: `main/display/eaf_display.h` + `main/display/eaf_display.cc`
|
||||
|
||||
**最小内容**:
|
||||
```cpp
|
||||
class EafDisplay : public Display {
|
||||
public:
|
||||
EafDisplay(esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_handle_t panel,
|
||||
int width, int height,
|
||||
int offset_x, int offset_y,
|
||||
bool mirror_x, bool mirror_y, bool swap_xy);
|
||||
~EafDisplay() override;
|
||||
|
||||
// 暂时只实现最小接口
|
||||
void SetStatus(const char* status) override { /* TODO */ }
|
||||
void SetEmotion(const char* emotion) override; // 切换 EAF 动画
|
||||
|
||||
private:
|
||||
gfx_handle_t gfx_handle_;
|
||||
gfx_disp_t* disp_;
|
||||
gfx_obj_t* anim_obj_;
|
||||
uint8_t* eaf_data_ = nullptr;
|
||||
size_t eaf_size_ = 0;
|
||||
};
|
||||
```
|
||||
|
||||
构造时:
|
||||
1. `gfx_emote_init(&gfx_handle_)`
|
||||
2. `gfx_emote_add_disp(gfx_handle_, &disp_cfg)` —— disp_cfg 用 LCD panel 信息
|
||||
3. 加载 `/spiflash/hiyori_m06.eaf` 到 `eaf_data_`(PSRAM)
|
||||
4. `anim_obj_ = gfx_anim_create(disp_)`
|
||||
5. `gfx_anim_set_src(anim_obj_, eaf_data_, eaf_size_)`
|
||||
6. `gfx_anim_set_segment(anim_obj_, 0, total_frames - 1, 20, true)`(20fps 循环)
|
||||
7. `gfx_anim_start(anim_obj_)`
|
||||
|
||||
**验收**:
|
||||
- 编译通过
|
||||
- 烧录后 LCD 显示 hiyori_m06 动画循环播放
|
||||
- **听 RTC 对话扬声器卡顿是否改善**(核心 PoC 目的)
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.5 — board 工厂条件编译
|
||||
|
||||
**文件**: `main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc`
|
||||
|
||||
**改动**: 找到 LcdDisplay 实例化位置,加 #ifdef 分支:
|
||||
```cpp
|
||||
#ifdef CONFIG_BAJI_BADGE_MODE
|
||||
display_ = new LcdDisplay(...);
|
||||
#else
|
||||
display_ = new EafDisplay(...);
|
||||
#endif
|
||||
```
|
||||
|
||||
**CMakeLists 改动**: 数字人模式下移除 `lcd_display.cc` 编译,加入 `eaf_display.cc`
|
||||
|
||||
**验收**:
|
||||
- 双向编译都通过(`=y` 和 `=n`)
|
||||
- 数字人模式固件不含 LVGL 符号(`nm` 检查无 lv_obj_create)
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.6 — EafDisplay 完整接口实现
|
||||
|
||||
扩展 EafDisplay 实现剩余 `Display` 虚函数:
|
||||
|
||||
- `SetStatus(const char*)` — gfx_label 显示连接状态
|
||||
- `SetChatMessage(role, content)` — gfx_label 显示字幕(中文换行)
|
||||
- `SetEmotion(emotion)` — 切换 EAF 动画(emotion → eaf 路径映射)
|
||||
- `SetIcon(icon)` — gfx_img 显示叠加图标
|
||||
- `Lock / Unlock` — esp_emote_gfx 锁机制接驳
|
||||
- `Update()` — 强制刷新
|
||||
|
||||
复用 `ai_chat_ui.c` 的 emotion → gif 映射表逻辑,改为 emotion → eaf 路径。
|
||||
|
||||
**验收**:
|
||||
- 数字人模式下 application.cc 调用 Display::SetXxx 都正常工作
|
||||
- 编译通过
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.7 — 字体接驳
|
||||
|
||||
**前置**: `main/fonts/font_puhui_20_4.c`(8.5MB CJK bitmap font)
|
||||
|
||||
**步骤**:
|
||||
1. 查 esp_emote_gfx 字体 API:`gfx_font_t*` / `gfx_label_set_font`
|
||||
2. 验证是否能直接传 `&font_puhui_20_4` (lv_font_t) → 应该不行,需要适配
|
||||
3. 如不兼容,用 esp_emote_gfx 工具重新生成同字符集 EAF font 资源
|
||||
|
||||
**降级方案**: 如果字体重新生成太麻烦,用内置默认字体或英文字体先 PoC
|
||||
|
||||
**验收**:
|
||||
- 字幕中文显示正常
|
||||
- 字幕单字符宽度合理(非乱码)
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.8 — 触摸路径接驳
|
||||
|
||||
**前提确认**: 数字人模式是否需要触摸交互?
|
||||
- 看 [main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc](../../../../main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc) cst816s 是否在 `CONFIG_BAJI_BADGE_MODE=n` 下注册了 LVGL indev
|
||||
|
||||
**如不需要触摸**: 跳过此任务
|
||||
**如需要**: 用 `gfx_touch` 接驳 cst816s,重写 LVGL_INDEV 路径
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.9 — 编译 + 烧录 + 显示效果验证
|
||||
|
||||
```bash
|
||||
source ~/esp/esp-idf/export.sh
|
||||
idf.py build
|
||||
idf.py flash monitor 2>&1 | tee .planning/milestones/digital_human_rtc/phases/phase_10_lvgl_to_eaf/phase_10_diag.log
|
||||
```
|
||||
|
||||
**测试场景**:
|
||||
1. 开机:是否正常显示数字人 GIF?
|
||||
2. RTC 连接:状态文字是否显示?
|
||||
3. 对话:字幕中文换行 + 居中是否正确?
|
||||
4. **听感**:扬声器卡顿是否消失?(**核心 PoC 验证目的**)
|
||||
5. 情绪切换:AI 说带情绪标签的话时 GIF 是否切换?
|
||||
|
||||
**指标对比**(vs Phase 8 baseline):
|
||||
|
||||
| 指标 | Phase 8 baseline | Phase 10 目标 |
|
||||
|---|---|---|
|
||||
| `idf.py size` DRAM | baseline | -30~40 KB |
|
||||
| `idf.py size` Flash | baseline | -80 KB |
|
||||
| `heap_caps_get_free_size(INTERNAL)` | baseline | +30 KB |
|
||||
| `heap_caps_get_free_size(SPIRAM)` | baseline | +80 KB |
|
||||
| 用户主观 RTC 对话听感 | 卡 | **不卡(核心目标)** |
|
||||
|
||||
---
|
||||
|
||||
### 任务 10.10 — 产出 IMPL_REPORT.md + commit
|
||||
|
||||
报告核心: PoC 显示效果 + 听感主观验证 + 资源对比,决定 Phase 11 是否启动。
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务顺序
|
||||
|
||||
```
|
||||
10.1 加依赖 → 10.2 gfx_label 中文换行 PoC(关键风险点)
|
||||
↓ 通过
|
||||
10.3 EAF Packer 转 hiyori_m06 → 10.4 最小 EafDisplay PoC(一个 GIF 显示)→ 编译烧录看效果
|
||||
↓ 显示效果 OK
|
||||
10.5 board 工厂 → 10.6 EafDisplay 完整接口 → 10.7 字体 → 10.8 触摸 → 10.9 编译烧录验证 → 10.10 commit
|
||||
```
|
||||
|
||||
**关键 GO/NO-GO 决策点**:
|
||||
- 10.2 后: gfx_label 中文换行不支持 → 评估降级方案
|
||||
- 10.4 后: 显示效果 OK + 听感改善 → 继续完整切换;显示坏 → 中止并回退
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与回滚
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| esp_emote_gfx 文档不全 / API 不稳定 | 参考 `managed_components/.../test_apps/` 官方示例 |
|
||||
| 字体兼容失败 | 降级英文字体先 PoC,字幕中文文档化为已知限制 |
|
||||
| 中文换行不支持 | 手动切分两个 gfx_label |
|
||||
| 显示效果坏 | 任何阶段 commit 前都可 `git reset --hard HEAD` 回滚 |
|
||||
| 听感未改善 | 数据证明假设 1 错误,需重新审视 → 继续 Phase 11 直接 WiFi 缓冲扩容验证假设 2 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 10 完成验收清单
|
||||
|
||||
- [ ] esp_emote_gfx 组件已添加且可拉取
|
||||
- [ ] gfx_label 中文换行已验证(或降级方案就绪)
|
||||
- [ ] 至少一个 EAF 动画文件已生成且能在设备显示
|
||||
- [ ] EafDisplay 实现 Display 全部虚函数
|
||||
- [ ] CMakeLists 双轨编译双向通过
|
||||
- [ ] 烧录数字人模式 + RTC 对话 5 分钟
|
||||
- [ ] 听感主观:扬声器卡顿是否消失(PoC 核心目标)
|
||||
- [ ] 资源对比 `idf.py size` + heap 数据已记录
|
||||
- [ ] IMPL_REPORT.md 给出 Phase 11 GO/NO-GO 决策
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 10 不做的事
|
||||
|
||||
- ❌ 不动吧唧模式 LVGL UI(`CONFIG_BAJI_BADGE_MODE=y` 路径完全不变)
|
||||
- ❌ 不删除 LVGL/lvgl_port 组件(吧唧模式仍需要)
|
||||
- ❌ 不动 audio_codec / RTC 协议(这是上游问题)
|
||||
- ❌ 不做内存优化和 WiFi 扩容(留给 Phase 11)
|
||||
- ❌ 不动 ScreenUpdate / ui/screens(吧唧模式专属)
|
||||
@ -60,7 +60,7 @@ dependencies:
|
||||
type: service
|
||||
version: 0.5.3
|
||||
espressif/dl_fft:
|
||||
component_hash: 7dadbd644c0d7ba4733cc3726ec4cff6edf27b043725e1115861dec1609a3d28
|
||||
component_hash: ced3cf28cc70452b7859c06f4e5059215167254a2047e34c893d6f501ccd6ea2
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
@ -68,7 +68,7 @@ dependencies:
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 0.3.1
|
||||
version: 0.4.0
|
||||
espressif/esp-dsp:
|
||||
component_hash: 619639efc18cfa361a9e423739b9b0ffc14991effc6c027f955c2f2c3bf1754b
|
||||
dependencies:
|
||||
@ -169,6 +169,49 @@ dependencies:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 2.5.0
|
||||
espressif/esp_mmap_assets:
|
||||
component_hash: b7c559238d9f4c11048b1d7302f5474e4f4f590902433efd792bd0cbf5324f2a
|
||||
dependencies:
|
||||
- name: espressif/cmake_utilities
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: 0.*
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 2.0.0
|
||||
espressif/esp_new_jpeg:
|
||||
component_hash: 98823384f51ca298e2c9bebacd1c629148e528ed0902d18b16556df151519e68
|
||||
dependencies: []
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
targets:
|
||||
- esp32
|
||||
- esp32s2
|
||||
- esp32s3
|
||||
- esp32s31
|
||||
- esp32p4
|
||||
- esp32c2
|
||||
- esp32c3
|
||||
- esp32c5
|
||||
- esp32c6
|
||||
- esp32c61
|
||||
- esp32h4
|
||||
version: 1.0.1
|
||||
espressif/freetype:
|
||||
component_hash: a4169cdd22b3572342b2d640d7082405b8895e3214539283601c03412589b65d
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.4'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 2.14.2
|
||||
espressif/knob:
|
||||
component_hash: a389d980693ad195b2160de22a72f3391694230188ab16b8f3c7ec4410a7c417
|
||||
dependencies:
|
||||
@ -193,10 +236,54 @@ dependencies:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 2.5.5
|
||||
espressif2022/esp_emote_gfx:
|
||||
component_hash: a06a58c74f7deb4186460f27f5e6db52fda4c254d7e03c3e05e7987aaf73de1a
|
||||
dependencies:
|
||||
- name: espressif/cmake_utilities
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: 0.*
|
||||
- name: espressif/esp_lcd_touch
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: '>=1.0'
|
||||
- name: espressif/esp_new_jpeg
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: 1.*
|
||||
- name: espressif/freetype
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: 2.*
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
- name: laride/heatshrink
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: ^0.4.1
|
||||
- name: lvgl/lvgl
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: '*'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 3.0.5
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.4.2
|
||||
laride/heatshrink:
|
||||
component_hash: 0828b0fea3f0754f8404a5279e883c52fe27494bbe1762e38d5cd96c99229e47
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 0.4.1
|
||||
lvgl/lvgl:
|
||||
component_hash: 948bff879a345149b83065535bbc4a026ce9f47498a22881e432a264b9098015
|
||||
dependencies: []
|
||||
@ -217,10 +304,12 @@ direct_dependencies:
|
||||
- espressif/esp_lcd_touch
|
||||
- espressif/esp_lcd_touch_cst816s
|
||||
- espressif/esp_lvgl_port
|
||||
- espressif/esp_mmap_assets
|
||||
- espressif/knob
|
||||
- espressif/led_strip
|
||||
- espressif2022/esp_emote_gfx
|
||||
- idf
|
||||
- lvgl/lvgl
|
||||
manifest_hash: 567fb06fed7b7df9c9bbd2a0615df5b600cd13d08df4b38a71d28971feaec792
|
||||
manifest_hash: 56465d60ff0a813df7f9be998612a4c2bc61e6d560c2f56fd585445d05b25456
|
||||
target: esp32s3
|
||||
version: 2.0.0
|
||||
|
||||
@ -28,10 +28,10 @@ set(SOURCES "audio_codecs/audio_codec.cc"
|
||||
"dzbj/pages_pwm.c"
|
||||
"dzbj/dzbj_init.c" # 含 dzbj_hw_display_init(公共硬件初始化);dzbj_display_init 函数体内部 #ifdef 包裹
|
||||
"dzbj/fatfs.c" # DecodeImg 公共(AI 模式 BG GIF PoC 也用);吧唧专用函数 fatfs_init/list 等无副作用
|
||||
"dzbj/ai_chat_ui.c" # AI 对话模式 LVGL 屏幕
|
||||
"dzbj/sprite_demo.c" # Sprite Sheet PoC(RGB565 raw 替代 GIF)
|
||||
"dzbj/dual_gif_demo.c" # 双 GIF 循环播放 PoC
|
||||
"dzbj/bg_gif_demo.c" # 背景 + 透明 GIF 叠加(方案 C)
|
||||
# Phase 10: ai_chat_ui 双轨编译
|
||||
# CONFIG_BAJI_BADGE_MODE=y → ai_chat_ui.c (LVGL 版)
|
||||
# CONFIG_BAJI_BADGE_MODE=n → ai_chat_ui_eaf.c (esp_emote_gfx 版)
|
||||
# 这两个在下方 if(CONFIG_BAJI_BADGE_MODE) 块中条件编译
|
||||
"fonts/font_puhui_20_4.c" # 阿里巴巴普惠体 20px 4bpp(GB2312 简体中文)
|
||||
# SquareLine Studio UI 公共文件(AI 模式也使用)
|
||||
"ui/ui.c"
|
||||
@ -221,6 +221,11 @@ if(CONFIG_BAJI_BADGE_MODE)
|
||||
"dzbj/dzbj_button.c"
|
||||
"dzbj/dzbj_battery.c"
|
||||
"dzbj/ble_transfer.c"
|
||||
# 吧唧模式 LVGL UI(AI 对话屏幕 + 各种 PoC)
|
||||
"dzbj/ai_chat_ui.c"
|
||||
"dzbj/sprite_demo.c"
|
||||
"dzbj/dual_gif_demo.c"
|
||||
"dzbj/bg_gif_demo.c"
|
||||
# SquareLine Studio 吧唧专属 UI 屏幕(9 个)
|
||||
"ui/screens/ui_ScreenHome.c"
|
||||
"ui/screens/ui_ScreenImg.c"
|
||||
@ -232,6 +237,11 @@ if(CONFIG_BAJI_BADGE_MODE)
|
||||
"ui/screens/ui_ScreenSharing.c"
|
||||
"ui/screens/ui_ScreenReceiving.c"
|
||||
)
|
||||
else()
|
||||
# Phase 10: 数字人模式 EAF UI(替代 LVGL 版 ai_chat_ui.c + bg_gif_demo.c)
|
||||
list(APPEND SOURCES
|
||||
"dzbj/ai_chat_ui_eaf.c"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(CONFIG_CONNECTION_TYPE_MQTT_UDP)
|
||||
|
||||
@ -49,6 +49,18 @@ extern "C" void ai_chat_resume_animation(void);
|
||||
// 取消注释下行宏可恢复方案 A 作为兜底(双源刷新)。
|
||||
// #define PHASE6_ENABLE_AUDIO_FALLBACK
|
||||
|
||||
// ============================================================
|
||||
// Phase 8: 音频卡顿诊断埋点(一键开关,关闭后零运行时开销)
|
||||
// 完成根因定位后改为 0 或 git revert 即可移除全部埋点。
|
||||
// ============================================================
|
||||
#ifndef PHASE8_DIAG_ENABLE
|
||||
#define PHASE8_DIAG_ENABLE 1
|
||||
#endif
|
||||
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
#include <esp_heap_caps.h>
|
||||
static int64_t g_diag_queue_last_us = 0; // queue 深度日志节流(50ms)
|
||||
#endif
|
||||
|
||||
// 定义设备状态字符串
|
||||
static const char* const STATE_STRINGS[] = {
|
||||
@ -787,6 +799,16 @@ void Application::Start() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
size_t len = data.size();
|
||||
audio_decode_queue_.emplace_back(std::move(data));
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
// Phase 8 DIAG-1: WebSocket 入队后队列深度(50ms 节流)
|
||||
{
|
||||
int64_t _diag_now = esp_timer_get_time();
|
||||
if (_diag_now - g_diag_queue_last_us > 50000) {
|
||||
g_diag_queue_last_us = _diag_now;
|
||||
ESP_LOGW("DIAG", "queue=%zu enq_ws", audio_decode_queue_.size());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
static bool first_enqueue_logged = false;
|
||||
if (!first_enqueue_logged && len > 0) {
|
||||
ESP_LOGI(TAG, "收到下行音频首包入队: 字节=%zu", len);
|
||||
@ -1957,6 +1979,27 @@ void Application::Start() {
|
||||
void Application::OnClockTimer() {
|
||||
clock_ticks_++;
|
||||
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
// Phase 8 DIAG-3: WiFi RSSI(1Hz,clock_timer 周期 1s)
|
||||
{
|
||||
wifi_ap_record_t _diag_ap;
|
||||
if (esp_wifi_sta_get_ap_info(&_diag_ap) == ESP_OK) {
|
||||
ESP_LOGW("DIAG", "rssi=%d ch=%d", _diag_ap.rssi, _diag_ap.primary);
|
||||
}
|
||||
}
|
||||
// Phase 8 DIAG-4: heap 快照 + 碎片率(1Hz)
|
||||
{
|
||||
int _diag_free_int = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
int _diag_free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||
int _diag_largest_int = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
|
||||
float _diag_frag = _diag_free_int > 0
|
||||
? (1.0f - (float)_diag_largest_int / (float)_diag_free_int) * 100.0f
|
||||
: 0.0f;
|
||||
ESP_LOGW("DIAG", "free_int=%d psram=%d largest_int=%d frag=%.1f%%",
|
||||
_diag_free_int, _diag_free_psram, _diag_largest_int, _diag_frag);
|
||||
}
|
||||
#endif
|
||||
|
||||
// 每10秒打印一次调试信息
|
||||
if (clock_ticks_ % 10 == 0) {
|
||||
int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
@ -2154,6 +2197,16 @@ void Application::OnAudioOutput() {
|
||||
|
||||
auto opus = std::move(audio_decode_queue_.front());
|
||||
audio_decode_queue_.pop_front();
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
// Phase 8 DIAG-1: 出队后队列深度(50ms 节流)
|
||||
{
|
||||
int64_t _diag_now = esp_timer_get_time();
|
||||
if (_diag_now - g_diag_queue_last_us > 50000) {
|
||||
g_diag_queue_last_us = _diag_now;
|
||||
ESP_LOGW("DIAG", "queue=%zu deq", audio_decode_queue_.size());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// 在出队时捕获opus解码标志,避免background_task异步执行时标志已变化
|
||||
// 导致残留的Opus帧被当作PCM播放(产生杂音)
|
||||
bool is_opus_frame = opus_playback_active_.load();
|
||||
@ -2255,7 +2308,17 @@ void Application::OnAudioOutput() {
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "直接输出PCM到编解码器: 样本=%zu", pcm.size());
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
// Phase 8 DIAG-2: codec PCM 写入耗时(>15ms 阈值告警)
|
||||
int64_t _diag_t = esp_timer_get_time();
|
||||
codec->OutputData(pcm);// 直接输出PCM数据
|
||||
int64_t _diag_cost = esp_timer_get_time() - _diag_t;
|
||||
if (_diag_cost > 15000) {
|
||||
ESP_LOGW("DIAG", "write_slow %lldus samples=%zu", _diag_cost, pcm.size());
|
||||
}
|
||||
#else
|
||||
codec->OutputData(pcm);// 直接输出PCM数据
|
||||
#endif
|
||||
#ifdef PHASE6_ENABLE_AUDIO_FALLBACK
|
||||
if (!pcm.empty()) {
|
||||
this->last_audible_output_time_ = std::chrono::steady_clock::now();
|
||||
@ -2827,6 +2890,16 @@ void Application::HttpsPlaybackFromUrl(const std::string& url) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.mutex_);
|
||||
app.audio_decode_queue_.emplace_back(std::move(opus_frame));
|
||||
#if PHASE8_DIAG_ENABLE
|
||||
// Phase 8 DIAG-1: RTC/HTTPS 入队后队列深度(50ms 节流)
|
||||
{
|
||||
int64_t _diag_now = esp_timer_get_time();
|
||||
if (_diag_now - g_diag_queue_last_us > 50000) {
|
||||
g_diag_queue_last_us = _diag_now;
|
||||
ESP_LOGW("DIAG", "queue=%zu enq_rtc", app.audio_decode_queue_.size());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
enqueued++;
|
||||
|
||||
|
||||
404
main/dzbj/ai_chat_ui_eaf.c
Normal file
404
main/dzbj/ai_chat_ui_eaf.c
Normal file
@ -0,0 +1,404 @@
|
||||
/*
|
||||
* Phase 10: 数字人 RTC 模式 EAF UI 实现
|
||||
*
|
||||
* 完全替代 ai_chat_ui.c (LVGL 版),提供相同的 C API 签名让 AiChatDisplay 桥接层无需改动。
|
||||
*
|
||||
* 架构:
|
||||
* esp_emote_gfx (gfx_emote_init + gfx_disp_add + gfx_anim)
|
||||
* ↓
|
||||
* mmap_assets (use_fs 模式,从 /spiflash/hiyori-assets.bin 加载)
|
||||
* ↓
|
||||
* panel_handle (lcd.c 暴露,已由 lcd_init 完成硬件初始化)
|
||||
*
|
||||
* PoC 阶段说明:
|
||||
* - 只显示数字人动画(核心目的:验证显示 + 听感效果)
|
||||
* - 字幕/状态文字: 仅日志输出(字体接驳留待后续,需要打包 .bin 字体到 mmap_assets)
|
||||
* - 资源限制: 当前 hiyori-assets.bin 只含 m06 + m07(用户在线工具未导入 m03)
|
||||
* sad/angry 等负面情绪暂时降级到 m07
|
||||
*/
|
||||
|
||||
#include "ai_chat_ui.h"
|
||||
#include "lcd.h" // 引用 panel_handle / lcd_io_handle
|
||||
|
||||
#include "gfx.h"
|
||||
#include "esp_mmap_assets.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "esp_spiffs.h" // Phase 10: SPIFFS 自动挂载
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// Phase 10 v2 修复:
|
||||
// esp_mmap_assets v2.0.0 在 use_fs=true 模式下,mmap_assets_get_mem() 返回的是文件内偏移量
|
||||
// 而不是 RAM 指针(看 esp_mmap_assets.c line 523 + line 353 的 fseek 用法)。
|
||||
// 把 offset 当指针 dereference 会导致 LoadProhibited panic。
|
||||
// 修复:开机时用 mmap_assets_copy_by_index 把所有 EAF 数据 fread 到 PSRAM buffer,
|
||||
// 运行时直接用 buffer 指针给 gfx_anim 使用。
|
||||
typedef struct {
|
||||
uint8_t *data; // EAF 数据 PSRAM 指针(malloc 出来)
|
||||
size_t size; // EAF 大小
|
||||
char name[40]; // 文件名
|
||||
} eaf_cache_entry_t;
|
||||
|
||||
static eaf_cache_entry_t s_eaf_cache[8]; // 预留 8 个表情槽位
|
||||
static int s_eaf_cache_count = 0;
|
||||
|
||||
static const char *TAG = "AI_CHAT_EAF";
|
||||
|
||||
// ==========================================================
|
||||
// 配置常量
|
||||
// ==========================================================
|
||||
#define EAF_ASSETS_PATH "/spiflash/hiyori-assets.bin"
|
||||
#define EAF_MAX_FILES 3 // index.json + 2 个 EAF (m06 + m07)
|
||||
#define EAF_DEFAULT_FPS 14 // 与工具配置一致
|
||||
#define LCD_W 360
|
||||
#define LCD_H 360
|
||||
|
||||
// ==========================================================
|
||||
// 全局 EAF 上下文
|
||||
// ==========================================================
|
||||
static gfx_handle_t s_emote_handle = NULL;
|
||||
static gfx_disp_t *s_disp = NULL;
|
||||
static gfx_obj_t *s_anim_obj = NULL;
|
||||
static mmap_assets_handle_t s_assets = NULL;
|
||||
static int s_current_emotion_idx = -1;
|
||||
static bool s_initialized = false;
|
||||
|
||||
// ==========================================================
|
||||
// 情绪 → asset 名字 映射表
|
||||
// ==========================================================
|
||||
typedef struct {
|
||||
const char *emotion;
|
||||
const char *asset_name;
|
||||
} eaf_emotion_map_t;
|
||||
|
||||
static const eaf_emotion_map_t s_emotion_map[] = {
|
||||
// 默认/积极 → m06
|
||||
{"neutral", "hiyori_m06.eaf"},
|
||||
{"happy", "hiyori_m06.eaf"},
|
||||
{"laughing", "hiyori_m06.eaf"},
|
||||
{"funny", "hiyori_m06.eaf"},
|
||||
{"loving", "hiyori_m06.eaf"},
|
||||
{"relaxed", "hiyori_m06.eaf"},
|
||||
{"delicious", "hiyori_m06.eaf"},
|
||||
{"kissy", "hiyori_m06.eaf"},
|
||||
{"confident", "hiyori_m06.eaf"},
|
||||
{"silly", "hiyori_m06.eaf"},
|
||||
{"blink", "hiyori_m06.eaf"},
|
||||
{"curious", "hiyori_m06.eaf"},
|
||||
// 思考/疲倦 → m07
|
||||
{"sleepy", "hiyori_m07.eaf"},
|
||||
{"thinking", "hiyori_m07.eaf"},
|
||||
{"confused", "hiyori_m07.eaf"},
|
||||
{"embarrassed", "hiyori_m07.eaf"},
|
||||
{"dizzy", "hiyori_m07.eaf"},
|
||||
// 负面/严肃 → 暂用 m07(m03 未导入)
|
||||
{"sad", "hiyori_m07.eaf"},
|
||||
{"crying", "hiyori_m07.eaf"},
|
||||
{"angry", "hiyori_m07.eaf"},
|
||||
{"surprised", "hiyori_m07.eaf"},
|
||||
{"shocked", "hiyori_m07.eaf"},
|
||||
};
|
||||
#define EMOTION_MAP_SIZE (sizeof(s_emotion_map) / sizeof(s_emotion_map[0]))
|
||||
|
||||
// ==========================================================
|
||||
// LCD flush 回调 (gfx → esp_lcd_panel_draw_bitmap)
|
||||
// ==========================================================
|
||||
static void eaf_disp_flush_cb(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data) {
|
||||
esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)gfx_disp_get_user_data(disp);
|
||||
esp_lcd_panel_draw_bitmap(panel, x1, y1, x2, y2, data);
|
||||
}
|
||||
|
||||
// panel IO 完成回调,通知 gfx flush 完毕
|
||||
static bool eaf_flush_io_ready(esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_io_event_data_t *edata,
|
||||
void *user_ctx) {
|
||||
gfx_disp_t *disp = (gfx_disp_t *)user_ctx;
|
||||
if (disp) {
|
||||
gfx_disp_flush_ready(disp, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// 按名字查找 cache 中的 EAF entry
|
||||
// ==========================================================
|
||||
static int find_cache_index_by_name(const char *name) {
|
||||
if (!name) return -1;
|
||||
for (int i = 0; i < s_eaf_cache_count; i++) {
|
||||
if (strcmp(s_eaf_cache[i].name, name) == 0 && s_eaf_cache[i].data) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// 切换表情到指定 asset (用 PSRAM 中 cache 的 EAF 数据)
|
||||
// ==========================================================
|
||||
static esp_err_t switch_emotion_by_asset(const char *asset_name) {
|
||||
if (!s_initialized || !s_anim_obj) return ESP_ERR_INVALID_STATE;
|
||||
|
||||
int idx = find_cache_index_by_name(asset_name);
|
||||
if (idx < 0) {
|
||||
ESP_LOGW(TAG, "Asset 未在 cache: %s", asset_name);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
if (idx == s_current_emotion_idx) {
|
||||
return ESP_OK; // 已是当前表情
|
||||
}
|
||||
|
||||
uint8_t *eaf_data = s_eaf_cache[idx].data;
|
||||
size_t eaf_size = s_eaf_cache[idx].size;
|
||||
|
||||
esp_err_t ret = gfx_emote_lock(s_emote_handle);
|
||||
if (ret != ESP_OK) return ret;
|
||||
|
||||
gfx_anim_stop(s_anim_obj);
|
||||
|
||||
gfx_anim_src_t src = {
|
||||
.type = GFX_ANIM_SRC_TYPE_MEMORY,
|
||||
.data = eaf_data,
|
||||
.data_len = eaf_size,
|
||||
};
|
||||
gfx_anim_set_src_desc(s_anim_obj, &src);
|
||||
|
||||
// 居中显示,hiyori 209×360 居中放 360×360 屏
|
||||
gfx_obj_align(s_anim_obj, GFX_ALIGN_CENTER, 0, 0);
|
||||
|
||||
// 全部帧 + EAF_DEFAULT_FPS + 永远循环
|
||||
gfx_anim_set_segment(s_anim_obj, 0, 0xFFFFFFFF, EAF_DEFAULT_FPS, true);
|
||||
gfx_anim_start(s_anim_obj);
|
||||
|
||||
gfx_emote_unlock(s_emote_handle);
|
||||
|
||||
s_current_emotion_idx = idx;
|
||||
ESP_LOGI(TAG, "切换表情: %s (idx=%d, size=%d)", asset_name, idx, eaf_size);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// 公开 C API(与 ai_chat_ui.c 完全相同的签名)
|
||||
// ==========================================================
|
||||
|
||||
void ai_chat_screen_init(void) {
|
||||
if (s_initialized) {
|
||||
ESP_LOGW(TAG, "已初始化,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "============================");
|
||||
ESP_LOGI(TAG, "=== EAF 数字人 UI 初始化 ===");
|
||||
ESP_LOGI(TAG, "============================");
|
||||
|
||||
// 0. 确保 SPIFFS 挂载(mmap_assets_new with use_fs=true 需要 vfs 路径可访问)
|
||||
size_t spiffs_total = 0, spiffs_used = 0;
|
||||
esp_err_t mount_ret = esp_spiffs_info("storage", &spiffs_total, &spiffs_used);
|
||||
if (mount_ret != ESP_OK) {
|
||||
ESP_LOGI(TAG, "SPIFFS 未挂载,自动挂载到 /spiflash...");
|
||||
esp_vfs_spiffs_conf_t spiffs_cfg = {
|
||||
.base_path = "/spiflash",
|
||||
.partition_label = "storage",
|
||||
.max_files = 5,
|
||||
.format_if_mount_failed = false,
|
||||
};
|
||||
mount_ret = esp_vfs_spiffs_register(&spiffs_cfg);
|
||||
if (mount_ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "SPIFFS 挂载失败: %s", esp_err_to_name(mount_ret));
|
||||
return;
|
||||
}
|
||||
esp_spiffs_info("storage", &spiffs_total, &spiffs_used);
|
||||
}
|
||||
ESP_LOGI(TAG, "SPIFFS 已就绪: total=%u KB, used=%u KB",
|
||||
(unsigned)(spiffs_total / 1024), (unsigned)(spiffs_used / 1024));
|
||||
|
||||
// 1. 自己解析 hiyori-assets.bin(绕过 esp_mmap_assets v2.0.0 use_fs 模式的严重 offset bug)
|
||||
//
|
||||
// MMAP bin 实际 layout(hex 反推得出):
|
||||
// [0x00-0x03] "MMAP" magic
|
||||
// [0x04-0x07] version + checksum (2B + 2B)
|
||||
// [0x08-0x0B] header_size = 16
|
||||
// [0x0C-0x0F] file_count
|
||||
// [0x10-0x1F] reserved (16B)
|
||||
// [0x20+] file entry table,每 entry = 28B (16B name + 4B size + 4B offset + 4B pad)
|
||||
// [data] table 后是数据段。每个文件: 2B 0x5A 0x5A magic prefix + size 字节数据。
|
||||
// entry.offset 是相对数据段起点的偏移(指向文件的 magic prefix 起点)
|
||||
ESP_LOGI(TAG, "解析 hiyori-assets.bin:");
|
||||
FILE *f = fopen(EAF_ASSETS_PATH, "rb");
|
||||
if (!f) {
|
||||
ESP_LOGE(TAG, "打开 %s 失败", EAF_ASSETS_PATH);
|
||||
return;
|
||||
}
|
||||
uint8_t header[16];
|
||||
if (fread(header, 1, 16, f) != 16 || memcmp(header, "MMAP", 4) != 0) {
|
||||
ESP_LOGE(TAG, "MMAP 头解析失败");
|
||||
fclose(f);
|
||||
return;
|
||||
}
|
||||
uint32_t file_count = header[12] | (header[13] << 8) | (header[14] << 16) | (header[15] << 24);
|
||||
ESP_LOGI(TAG, " MMAP file_count=%u", (unsigned)file_count);
|
||||
// 跳过 reserved 16B 到 entry table 起点 (0x20)
|
||||
fseek(f, 0x20, SEEK_SET);
|
||||
const size_t ENTRY_SIZE = 28; // 16 + 4 + 4 + 4
|
||||
const size_t DATA_START = 0x20 + file_count * ENTRY_SIZE;
|
||||
|
||||
s_eaf_cache_count = 0;
|
||||
for (uint32_t i = 0; i < file_count; i++) {
|
||||
uint8_t entry[28];
|
||||
fseek(f, 0x20 + i * ENTRY_SIZE, SEEK_SET);
|
||||
if (fread(entry, 1, ENTRY_SIZE, f) != ENTRY_SIZE) {
|
||||
ESP_LOGE(TAG, " entry[%u] 读取失败", (unsigned)i);
|
||||
continue;
|
||||
}
|
||||
char name[17] = {0};
|
||||
memcpy(name, entry, 16);
|
||||
name[16] = '\0';
|
||||
uint32_t fsize = entry[16] | (entry[17] << 8) | (entry[18] << 16) | (entry[19] << 24);
|
||||
uint32_t foffset = entry[20] | (entry[21] << 8) | (entry[22] << 16) | (entry[23] << 24);
|
||||
|
||||
// 只缓存 .eaf 文件
|
||||
size_t nlen = strlen(name);
|
||||
if (nlen < 4 || strcmp(name + nlen - 4, ".eaf") != 0) {
|
||||
ESP_LOGI(TAG, " 跳过非 EAF: %s (size=%u)", name, (unsigned)fsize);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 真实文件位置 = data_section_start + entry.offset + 2 (跳过 0x5A 0x5A magic prefix)
|
||||
size_t real_offset = DATA_START + foffset + 2;
|
||||
|
||||
uint8_t *buf = heap_caps_malloc(fsize, MALLOC_CAP_SPIRAM);
|
||||
if (!buf) {
|
||||
ESP_LOGE(TAG, " PSRAM malloc 失败: %s (size=%u)", name, (unsigned)fsize);
|
||||
continue;
|
||||
}
|
||||
if (fseek(f, real_offset, SEEK_SET) != 0 || fread(buf, 1, fsize, f) != fsize) {
|
||||
ESP_LOGE(TAG, " fread 失败: %s @ offset %zu", name, real_offset);
|
||||
heap_caps_free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 验证 EAF format magic
|
||||
if (buf[0] != 0x89 || buf[1] != 'E' || buf[2] != 'A' || buf[3] != 'F') {
|
||||
ESP_LOGE(TAG, " EAF magic 失败: %s (got %02x %02x %02x %02x)",
|
||||
name, buf[0], buf[1], buf[2], buf[3]);
|
||||
heap_caps_free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s_eaf_cache_count >= (int)(sizeof(s_eaf_cache)/sizeof(s_eaf_cache[0]))) {
|
||||
ESP_LOGW(TAG, " cache 已满,丢弃: %s", name);
|
||||
heap_caps_free(buf);
|
||||
break;
|
||||
}
|
||||
s_eaf_cache[s_eaf_cache_count].data = buf;
|
||||
s_eaf_cache[s_eaf_cache_count].size = fsize;
|
||||
strncpy(s_eaf_cache[s_eaf_cache_count].name, name, sizeof(s_eaf_cache[0].name) - 1);
|
||||
s_eaf_cache[s_eaf_cache_count].name[sizeof(s_eaf_cache[0].name) - 1] = '\0';
|
||||
ESP_LOGI(TAG, " ✓ Cached [%d] %s (%u bytes) @ %p (file_offset=%zu)",
|
||||
s_eaf_cache_count, name, (unsigned)fsize, buf, real_offset);
|
||||
s_eaf_cache_count++;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (s_eaf_cache_count == 0) {
|
||||
ESP_LOGE(TAG, "没有 EAF 资源被加载,初始化中止");
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "EAF 预加载完成,共 %d 个表情可用", s_eaf_cache_count);
|
||||
|
||||
// 2. 初始化 gfx 核心(绑 Core 0,与原 LVGL 一致避免抢音频 Core 1)
|
||||
gfx_core_config_t gfx_cfg = {
|
||||
.fps = 25,
|
||||
.task = GFX_EMOTE_INIT_CONFIG(),
|
||||
};
|
||||
gfx_cfg.task.task_priority = 4;
|
||||
gfx_cfg.task.task_affinity = 0; // Core 0
|
||||
gfx_cfg.task.task_stack = 8 * 1024;
|
||||
s_emote_handle = gfx_emote_init(&gfx_cfg);
|
||||
if (!s_emote_handle) {
|
||||
ESP_LOGE(TAG, "gfx_emote_init 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 添加 display(接管 panel_handle)
|
||||
gfx_disp_config_t disp_cfg = {
|
||||
.h_res = LCD_W,
|
||||
.v_res = LCD_H,
|
||||
.flush_cb = eaf_disp_flush_cb,
|
||||
.update_cb = NULL,
|
||||
.user_data = (void *)panel_handle,
|
||||
.flags = {
|
||||
.swap = true, // RGB565 字节序(与 LVGL 配置一致)
|
||||
.buff_dma = true,
|
||||
.buff_spiram = false,
|
||||
.double_buffer = true,
|
||||
},
|
||||
.buffers = { .buf1 = NULL, .buf2 = NULL, .buf_pixels = LCD_W * 20 },
|
||||
};
|
||||
s_disp = gfx_disp_add(s_emote_handle, &disp_cfg);
|
||||
if (!s_disp) {
|
||||
ESP_LOGE(TAG, "gfx_disp_add 失败");
|
||||
gfx_emote_deinit(s_emote_handle);
|
||||
s_emote_handle = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册 panel IO 完成回调
|
||||
const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = eaf_flush_io_ready };
|
||||
esp_lcd_panel_io_register_event_callbacks(lcd_io_handle, &cbs, s_disp);
|
||||
|
||||
// 4. 设置背景色 = BG_COLOR (0x000000 黑色,与 LVGL 版一致)
|
||||
gfx_disp_set_bg_color(s_disp, GFX_COLOR_HEX(0x000000));
|
||||
|
||||
// 5. 创建动画对象 + 加载默认表情 m06
|
||||
s_anim_obj = gfx_anim_create(s_disp);
|
||||
if (!s_anim_obj) {
|
||||
ESP_LOGE(TAG, "gfx_anim_create 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
s_initialized = true;
|
||||
|
||||
// 默认表情 = neutral → m06
|
||||
switch_emotion_by_asset("hiyori_m06.eaf");
|
||||
|
||||
ESP_LOGI(TAG, "=== EAF 数字人 UI 初始化完成 ===");
|
||||
}
|
||||
|
||||
void ai_chat_set_status(const char* status) {
|
||||
// PoC 阶段不显示状态文字(gfx_label 需要字体资源接驳,留待后续)
|
||||
if (status) {
|
||||
ESP_LOGI(TAG, "状态: %s(PoC 阶段暂不显示)", status);
|
||||
}
|
||||
}
|
||||
|
||||
void ai_chat_set_emotion(const char* emotion) {
|
||||
if (!emotion || !s_initialized) return;
|
||||
|
||||
// 查映射表
|
||||
const char *asset_name = "hiyori_m06.eaf"; // 默认 fallback
|
||||
for (size_t i = 0; i < EMOTION_MAP_SIZE; i++) {
|
||||
if (strcmp(emotion, s_emotion_map[i].emotion) == 0) {
|
||||
asset_name = s_emotion_map[i].asset_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch_emotion_by_asset(asset_name);
|
||||
}
|
||||
|
||||
void ai_chat_set_chat_message(const char* role, const char* content) {
|
||||
(void)role;
|
||||
// PoC 阶段不显示字幕(gfx_label 需要字体资源接驳,留待后续)
|
||||
if (content && content[0]) {
|
||||
ESP_LOGI(TAG, "字幕: %s(PoC 阶段暂不显示)", content);
|
||||
}
|
||||
}
|
||||
|
||||
void ai_chat_resume_animation(void) {
|
||||
// EAF 动画由 gfx_anim_start 持续播放,无需手动 resume
|
||||
ESP_LOGD(TAG, "resume_animation(EAF 模式下自动循环,无需操作)");
|
||||
}
|
||||
@ -13,10 +13,13 @@
|
||||
#define TAG "DZBJ"
|
||||
|
||||
// 仅硬件+LVGL 初始化(不加载 SquareLine UI,不点亮背光)
|
||||
//
|
||||
// Phase 10: 数字人 EAF 模式(CONFIG_BAJI_BADGE_MODE=n)下跳过 LVGL 初始化
|
||||
// 让 esp_emote_gfx 接管 panel_handle,避免双框架冲突
|
||||
void dzbj_hw_display_init(i2c_master_bus_handle_t i2c_bus) {
|
||||
ESP_LOGI(TAG, "开始初始化显示硬件...");
|
||||
|
||||
// 1. LCD 硬件初始化(QSPI ST77916)
|
||||
// 1. LCD 硬件初始化(QSPI ST77916)—— 共享
|
||||
lcd_init();
|
||||
ESP_LOGI(TAG, "LCD 硬件初始化完成");
|
||||
|
||||
@ -31,9 +34,15 @@ void dzbj_hw_display_init(i2c_master_bus_handle_t i2c_bus) {
|
||||
ESP_LOGI(TAG, "屏幕触摸已禁用 (DZBJ_ENABLE_TOUCH=0)");
|
||||
#endif
|
||||
|
||||
// 4. LVGL 初始化(显示)
|
||||
#ifdef CONFIG_BAJI_BADGE_MODE
|
||||
// 4. LVGL 初始化(仅吧唧模式)
|
||||
lvgl_lcd_init();
|
||||
ESP_LOGI(TAG, "LVGL 初始化完成");
|
||||
#else
|
||||
// Phase 10: 数字人 EAF 模式下不初始化 LVGL
|
||||
// esp_emote_gfx 会在 ai_chat_screen_init 中接管 panel_handle
|
||||
ESP_LOGI(TAG, "数字人 EAF 模式: 跳过 LVGL 初始化,等待 esp_emote_gfx 接管");
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef CONFIG_BAJI_BADGE_MODE
|
||||
|
||||
@ -209,7 +209,8 @@ static const st77916_lcd_init_cmd_t lcd_init_cmds[] = {
|
||||
|
||||
static lv_disp_t * disp_handle = NULL;
|
||||
esp_lcd_panel_handle_t panel_handle = NULL; // 暴露给 sprite_demo 等模块直接 DMA 写 LCD
|
||||
static esp_lcd_panel_io_handle_t io_handle = NULL;
|
||||
static esp_lcd_panel_io_handle_t io_handle = NULL; // 仅文件内使用
|
||||
esp_lcd_panel_io_handle_t lcd_io_handle = NULL; // Phase 10: 暴露给 EAF UI 注册 IO 完成回调(lcd_init 后赋值)
|
||||
#if DZBJ_ENABLE_TOUCH
|
||||
static esp_lcd_touch_handle_t touch_handle = NULL;
|
||||
static esp_lcd_panel_io_handle_t tp_io_handle = NULL;
|
||||
@ -239,6 +240,7 @@ void lcd_init(){
|
||||
io_config.pclk_hz = 80 * 1000 * 1000;
|
||||
io_config.trans_queue_depth = 64; // 默认 10 太小,sprite 分条传输需要更大队列
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI_LCD_HOST, &io_config, &io_handle));
|
||||
lcd_io_handle = io_handle; // Phase 10: 同步给 EAF UI 使用
|
||||
const st77916_vendor_config_t vendor_config = {
|
||||
.init_cmds = lcd_init_cmds,
|
||||
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st77916_lcd_init_cmd_t),
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
#include "esp_lcd_st77916.h"
|
||||
#include <driver/i2c_master.h>
|
||||
|
||||
// 全局 LCD 句柄(lcd_init 后可用)
|
||||
extern esp_lcd_panel_handle_t panel_handle;
|
||||
extern esp_lcd_panel_io_handle_t lcd_io_handle; // Phase 10: 给 EAF 注册 IO 完成回调
|
||||
|
||||
void lcd_init(void);
|
||||
void lvgl_lcd_init(void);
|
||||
void lcd_clear_screen_black(void);
|
||||
|
||||
@ -17,6 +17,9 @@ dependencies:
|
||||
esp_lcd_touch_cst816s: "1.1.0"
|
||||
## JPEG 解码(dzbj 图片显示)
|
||||
esp_jpeg: "*"
|
||||
## Phase 10: 数字人模式 UI 框架(替代 LVGL,仅 CONFIG_BAJI_BADGE_MODE=n 时使用)
|
||||
espressif2022/esp_emote_gfx: "~3.0.5"
|
||||
espressif/esp_mmap_assets: "*"
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: ">=5.3"
|
||||
|
||||
@ -0,0 +1 @@
|
||||
a06a58c74f7deb4186460f27f5e6db52fda4c254d7e03c3e05e7987aaf73de1a
|
||||
116
managed_components/espressif2022__esp_emote_gfx/.gitignore
vendored
Normal file
116
managed_components/espressif2022__esp_emote_gfx/.gitignore
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
|
||||
# =========================
|
||||
# Operating System Files
|
||||
# =========================
|
||||
|
||||
# Linux
|
||||
# =========================
|
||||
|
||||
# Vim temporary files
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OSX
|
||||
# =========================
|
||||
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Windows
|
||||
# =========================
|
||||
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# ESP32
|
||||
build/
|
||||
sdkconfig
|
||||
managed_components/
|
||||
*.lock
|
||||
dist/
|
||||
|
||||
#Vscoe
|
||||
.vscode/
|
||||
.sdkconfig
|
||||
.sbmp
|
||||
.bmp
|
||||
.gif
|
||||
sdkconfig.old
|
||||
|
||||
# Generated documentation
|
||||
docs/_build/
|
||||
# Babel/gettext catalogs built by docs/scripts/sync_locale_zh.py
|
||||
docs/locale/zh_CN/LC_MESSAGES/
|
||||
docs/doxygen_output/
|
||||
Doxyfile
|
||||
API_REFERENCE.md
|
||||
|
||||
# Python cache (docs scripts / locale imports)
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
@ -0,0 +1,98 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
default_install_hook_types:
|
||||
- pre-commit
|
||||
- commit-msg
|
||||
|
||||
exclude: |
|
||||
(?x)(
|
||||
.*/assets/.*
|
||||
| mmap_generate_*.h
|
||||
| font_puhui_16_4.c
|
||||
| docs/.*
|
||||
| png_to_rgb565a8.py
|
||||
| qrcodegen.*
|
||||
)
|
||||
repos:
|
||||
- repo: https://github.com/igrr/astyle_py.git
|
||||
rev: v1.0.5
|
||||
hooks:
|
||||
- id: astyle_py
|
||||
args: ['--style=otbs', '--attach-namespaces', '--attach-classes', '--indent=spaces=4', '--convert-tabs', '--align-pointer=name', '--align-reference=name', '--keep-one-line-statements', '--pad-header', '--pad-oper']
|
||||
|
||||
- repo: https://github.com/espressif/check-copyright/
|
||||
rev: v1.0.3
|
||||
hooks:
|
||||
- id: check-copyright
|
||||
args: ['--config', 'check_copyright_config.yaml']
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
types: [python]
|
||||
args: ['--config=.flake8', '--tee', '--benchmark']
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
# note: whitespace exclusions use multiline regex, see https://pre-commit.com/#regular-expressions
|
||||
# items are:
|
||||
# 1 - some file extensions
|
||||
# 2 - any file matching *test*/*expected* (for host tests, if possible use this naming pattern always)
|
||||
# 3 - any file with known-warnings in the name
|
||||
# 4 - any directory named 'testdata'
|
||||
# 5 - protobuf auto-generated files
|
||||
exclude: &whitespace_excludes |
|
||||
(?x)^(
|
||||
.+\.(md|rst|map|bin)|
|
||||
.+test.*\/.*expected.*|
|
||||
.+known-warnings.*|
|
||||
.+\/testdata\/.+|
|
||||
.*_pb2.py|
|
||||
.*.pb-c.h|
|
||||
.*.pb-c.c|
|
||||
.*.yuv
|
||||
)$
|
||||
- id: end-of-file-fixer
|
||||
exclude: *whitespace_excludes
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: mixed-line-ending
|
||||
args: ['-f=lf']
|
||||
- id: double-quote-string-fixer
|
||||
- id: no-commit-to-branch
|
||||
name: Do not use more than one slash in the branch name
|
||||
args: ['--pattern', '^[^/]*/[^/]*/']
|
||||
- id: no-commit-to-branch
|
||||
name: Do not use uppercase letters in the branch name
|
||||
args: ['--pattern', '^[^A-Z]*[A-Z]']
|
||||
# - repo: local
|
||||
# hooks:
|
||||
# - id: check-executables
|
||||
# name: Check File Permissions
|
||||
# entry: .gitlab/tools/check_executables.py --action executables
|
||||
# language: python
|
||||
# types: [executable]
|
||||
# exclude: '\.pre-commit/.+'
|
||||
# - id: check-executable-list
|
||||
# name: Validate executable-list.txt
|
||||
# entry: .gitlab/tools/check_executables.py --action list
|
||||
# language: python
|
||||
# pass_filenames: false
|
||||
# always_run: true
|
||||
- repo: https://github.com/espressif/conventional-precommit-linter
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: conventional-precommit-linter
|
||||
stages: [commit-msg]
|
||||
args:
|
||||
- --subject-min-length=15
|
||||
- --body-max-line-length=200
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
args: [-w]
|
||||
88
managed_components/espressif2022__esp_emote_gfx/CHANGELOG.md
Normal file
88
managed_components/espressif2022__esp_emote_gfx/CHANGELOG.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the ESP Emote GFX component will be documented in this file.
|
||||
|
||||
## [3.0.5] - 2026-04-30
|
||||
- Add motion scene widget documentation covering `gfx_motion`, `gfx_motion_scene`, asset layout, and runtime usage
|
||||
- Add motion widget example references to README and Sphinx docs
|
||||
- Simplify the motion rendering path by removing NanoVG and libtess2 dependencies
|
||||
- Keep polygon fill on the internal scanline fallback path for a leaner release footprint
|
||||
|
||||
## [3.0.4] - 2026-04-21
|
||||
- restore gfx_disp_event_t
|
||||
- Render loop: sleep `GFX_RENDER_TASK_IDLE_SLEEP_MS` once before the main loop so the first frame is not driven until the caller can finish setup after `add_disp()` (avoids a startup deadlock)
|
||||
|
||||
## [3.0.3] - 2026-04-20
|
||||
- Add `gfx_button` widget (text, font, normal/pressed colors, border)
|
||||
- Add `gfx_log` API for log level configuration
|
||||
- Documentation: separate English and Simplified Chinese HTML builds (gettext), language switcher, unified `postprocess_docs.sh` pipeline (API RST, Sphinx, Doxygen)
|
||||
- Simplify GitHub Actions documentation job to a single build step
|
||||
|
||||
## [3.0.2] - 2026-04-17
|
||||
- Update version of esp_new_jpeg
|
||||
|
||||
## [3.0.1] - 2026-02-13
|
||||
- Add CI build action for P4
|
||||
- Optimize multi-buffer switching logic
|
||||
- Fix crash when text is NULL
|
||||
- Fix missing API documentation (e.g. gfx_touch_add)
|
||||
|
||||
## [3.0.0] - 2026-01-22
|
||||
- Add documentation build action
|
||||
- Optimize EAF 8-bit render
|
||||
- Fix FreeType parsing performance
|
||||
- Remove duplicated label-related APIs
|
||||
|
||||
## [2.1.0] - 2026-01-28
|
||||
- Support for decoding Heatshrink-compressed image slices
|
||||
|
||||
## [2.0.4] - 2026-01-22
|
||||
- Fix Huffman+RLE decoding buffer sizing to prevent oversized output errors (Issue [#18](https://github.com/espressif2022/esp_emote_gfx/issues/18))
|
||||
|
||||
## [2.0.3] - 2026-01-08
|
||||
- Delete local assets
|
||||
- Build acion for ['release-v5.2', 'release-v5.3', 'release-v5.4', 'release-v5.5']
|
||||
- Fix ESP-IDF version compatibility issues
|
||||
- Change flush_callback timeout from 20 ms to wait forever
|
||||
|
||||
## [2.0.2] - 2025-12-26
|
||||
- Add optional JPEG decoding support for EAF animations
|
||||
- Center QR code rendering in UI layout
|
||||
- Add alpha channel support for animations
|
||||
|
||||
## [2.0.1] - 2025-12-05
|
||||
- Add Touch event
|
||||
|
||||
## [2.0.0] - 2025-12-01
|
||||
- Added partial refresh mode support
|
||||
- Added QR code widget (gfx_qrcode)
|
||||
|
||||
## [1.2.0] - 2025-09-0
|
||||
- use eaf as a lib
|
||||
|
||||
## [1.1.2] - 2025-09-29
|
||||
|
||||
### Upgrade dependencies
|
||||
- Update `espressif/esp_new_jpeg` to 0.6.x by @Kevincoooool. [#8](https://github.com/espressif2022/esp_emote_gfx/pull/8)
|
||||
|
||||
## [1.1.1] - 2025-09-23
|
||||
|
||||
### Fixed
|
||||
- Resolve image block decoding failure in specific cases. [#6](https://github.com/espressif2022/esp_emote_gfx/issues/6)
|
||||
|
||||
## [1.0.0] - 2025-08-01
|
||||
|
||||
### Added
|
||||
- Initial release of ESP Emote GFX framework
|
||||
- Core graphics rendering engine
|
||||
- Object system for images and labels
|
||||
- Basic drawing functions and color utilities
|
||||
- Software blending capabilities
|
||||
- Timer system for animations
|
||||
- Support for ESP-IDF 5.0+
|
||||
- FreeType font rendering integration
|
||||
- JPEG image decoding support
|
||||
|
||||
### Features
|
||||
- Lightweight graphics framework optimized for embedded systems
|
||||
- Memory-efficient design for resource-constrained environments
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,23 @@
|
||||
# Keep source discovery automatic while ensuring newly added files trigger
|
||||
# CMake regeneration in normal configure mode. ESP-IDF also evaluates component
|
||||
# CMakeLists in script mode while collecting requirements, where
|
||||
# CONFIGURE_DEPENDS is not accepted.
|
||||
if(CMAKE_SCRIPT_MODE_FILE)
|
||||
file(GLOB_RECURSE SRC_FILES "src/*.c")
|
||||
else()
|
||||
file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS "src/*.c")
|
||||
endif()
|
||||
|
||||
set(PRIV_INCLUDE_DIRS
|
||||
"src"
|
||||
)
|
||||
|
||||
idf_component_register(
|
||||
SRCS
|
||||
${SRC_FILES}
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
PRIV_INCLUDE_DIRS
|
||||
${PRIV_INCLUDE_DIRS}
|
||||
REQUIRES esp_timer
|
||||
)
|
||||
152
managed_components/espressif2022__esp_emote_gfx/Kconfig
Normal file
152
managed_components/espressif2022__esp_emote_gfx/Kconfig
Normal file
@ -0,0 +1,152 @@
|
||||
menu "ESP Emote Graphics Framework"
|
||||
|
||||
config GFX_FONT_FREETYPE_SUPPORT
|
||||
bool "Enable FreeType font support"
|
||||
default n
|
||||
help
|
||||
Enable support for FreeType fonts (TTF/OTF).
|
||||
This requires the FreeType library to be enabled.
|
||||
|
||||
When enabled, you can use TrueType and OpenType fonts.
|
||||
When disabled, only LVGL C format fonts are supported.
|
||||
|
||||
config GFX_EAF_JPEG_DECODE_SUPPORT
|
||||
bool "Enable JPEG decoding support in EAF"
|
||||
default y
|
||||
help
|
||||
Enable support for JPEG decoding in EAF (Emote Animation Format).
|
||||
This requires the ESP JPEG decoder component to be enabled.
|
||||
|
||||
When enabled, EAF files can use JPEG encoding for image blocks.
|
||||
When disabled, JPEG encoding will not be available, reducing code size.
|
||||
|
||||
config GFX_EAF_HEATSHRINK_SUPPORT
|
||||
bool "Enable Heatshrink support in EAF"
|
||||
default y
|
||||
depends on HEATSHRINK_DYNAMIC_ALLOC || (HEATSHRINK_STATIC_WINDOW_BITS = 8 && HEATSHRINK_STATIC_LOOKAHEAD_BITS = 4)
|
||||
help
|
||||
Enable support for Heatshrink decoding in EAF (Emote Animation Format).
|
||||
|
||||
Note: Only supports 8-bit window and 4-bit lookahead if dynamic allocation is disabled.
|
||||
|
||||
comment "Heatshrink support is unavailable due to static bit mismatch"
|
||||
depends on !HEATSHRINK_DYNAMIC_ALLOC && (HEATSHRINK_STATIC_WINDOW_BITS != 8 || HEATSHRINK_STATIC_LOOKAHEAD_BITS != 4)
|
||||
|
||||
menu "Software Blend"
|
||||
|
||||
config GFX_BLEND_TRI_EDGE_AA_RANGE
|
||||
int "Triangle edge AA range"
|
||||
range 0 4096
|
||||
default 0
|
||||
help
|
||||
Edge anti-aliasing distance threshold for the software
|
||||
triangle rasterizer, in mesh sub-pixel units. 0 means one
|
||||
logical pixel in the mesh fixed-point format (256 for Q8).
|
||||
|
||||
config GFX_MESH_IMG_SCANLINE_MAX_VERTS
|
||||
int "Mesh image scanline fill max vertices"
|
||||
range 16 2048
|
||||
default 512
|
||||
help
|
||||
Maximum polygon vertices accepted by mesh_img scanline fill.
|
||||
Higher values support more complex filled motion paths, but
|
||||
increase per-object scratch memory when scanline fill is used.
|
||||
|
||||
config GFX_BLEND_POLYGON_MAX_INTERSECTIONS
|
||||
int "Polygon fill max scanline intersections"
|
||||
range 16 256
|
||||
default 64
|
||||
help
|
||||
Maximum number of edge intersections stored per polygon scanline
|
||||
sample. 32 is faster and enough for simple convex-ish shapes.
|
||||
Increase to 64/128 for complex filled Bezier loops to avoid
|
||||
clipped or broken scanline spans.
|
||||
|
||||
config GFX_BLEND_POLYGON_SUB_SAMPLES
|
||||
int "Polygon fill vertical AA sub-samples"
|
||||
range 1 16
|
||||
default 8
|
||||
help
|
||||
Number of vertical sub-samples used by software polygon fill.
|
||||
Higher values produce smoother coverage but cost more CPU.
|
||||
|
||||
config GFX_BLEND_POLYGON_COVERAGE_MAX_WIDTH
|
||||
int "Polygon fill coverage buffer max width"
|
||||
range 64 2048
|
||||
default 512
|
||||
help
|
||||
Maximum pixel width of a polygon fill coverage row. Larger
|
||||
values support wider dirty chunks at the cost of stack memory.
|
||||
|
||||
config GFX_BLEND_POLYGON_INWARD_AA
|
||||
bool "Keep polygon AA inside filled shapes"
|
||||
default y
|
||||
help
|
||||
Do not blend partially covered pixels whose centre is outside
|
||||
the polygon. This avoids bright/dark halos when solid filled
|
||||
Bezier parts are drawn over changing framebuffer contents.
|
||||
|
||||
config GFX_BLEND_POLYGON_SOLID_HARD_EDGE
|
||||
bool "Use hard edge for fully opaque polygon fills"
|
||||
default y
|
||||
depends on GFX_BLEND_POLYGON_INWARD_AA
|
||||
help
|
||||
For opacity=255 polygon fills, write the fill colour directly
|
||||
for inward edge pixels instead of blending coverage with the
|
||||
destination framebuffer. This is faster and prevents colour
|
||||
contamination, but the edge is less smooth.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "Motion Widget"
|
||||
|
||||
config GFX_MOTION_BEZIER_STROKE_SEGS_PER_SEG
|
||||
int "Bezier stroke samples per cubic segment"
|
||||
range 2 24
|
||||
default 6
|
||||
help
|
||||
Tessellation samples per cubic Bezier segment for motion
|
||||
BEZIER_LOOP and BEZIER_STRIP strokes.
|
||||
|
||||
config GFX_MOTION_BEZIER_FILL_LOOP_SEGS_PER_SEG
|
||||
int "Generic Bezier fill samples per cubic segment"
|
||||
range 2 24
|
||||
default 12
|
||||
help
|
||||
Tessellation samples per cubic Bezier segment for generic
|
||||
filled closed loops. Higher values produce smoother fill
|
||||
outlines but increase setup and raster cost.
|
||||
|
||||
config GFX_MOTION_BEZIER_FILL_SEGS
|
||||
int "Preset Bezier fill segments"
|
||||
range 4 64
|
||||
default 24
|
||||
help
|
||||
Segment count for special 7/13-point eye/ellipse fills.
|
||||
|
||||
config GFX_MOTION_HUB_FILL_MAX_POINTS
|
||||
int "Generic Bezier fill max hub mesh points"
|
||||
range 64 1024
|
||||
default 512
|
||||
help
|
||||
Scratch point budget for generic filled Bezier loops.
|
||||
|
||||
choice GFX_MOTION_BEZIER_FILL_RASTERIZER
|
||||
prompt "Bezier fill rasterizer"
|
||||
default GFX_MOTION_BEZIER_FILL_RASTERIZER_SCANLINE
|
||||
help
|
||||
Select how BEZIER_FILL mesh objects are rasterized.
|
||||
Scanline is stable for arbitrary filled paths and avoids
|
||||
triangle fan artifacts. Mesh triangles use the regular mesh
|
||||
triangle rasterizer and can produce smoother AA on some simple
|
||||
cartoon shapes, but may show artifacts on concave paths.
|
||||
|
||||
config GFX_MOTION_BEZIER_FILL_RASTERIZER_SCANLINE
|
||||
bool "Scanline polygon fill"
|
||||
|
||||
config GFX_MOTION_BEZIER_FILL_RASTERIZER_TRIANGLE
|
||||
bool "Mesh triangle fill"
|
||||
endchoice
|
||||
|
||||
endmenu
|
||||
endmenu
|
||||
202
managed_components/espressif2022__esp_emote_gfx/LICENSE
Normal file
202
managed_components/espressif2022__esp_emote_gfx/LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
111
managed_components/espressif2022__esp_emote_gfx/README.md
Normal file
111
managed_components/espressif2022__esp_emote_gfx/README.md
Normal file
@ -0,0 +1,111 @@
|
||||
<p align="center">
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<h1 align="center">ESP Emote GFX</h1>
|
||||
|
||||
<p align="center">
|
||||
<span>面向嵌入式小屏设备的轻量 UI 图形库</span>
|
||||
<br>
|
||||
<sub>Widgets · Text · Images · QR Codes · Animation · Motion Scenes</sub>
|
||||
<br>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://components.espressif.com/components/espressif2022/esp_emote_gfx">
|
||||
<img src="https://components.espressif.com/components/espressif2022/esp_emote_gfx/badge.svg" alt="Component Registry">
|
||||
</a>
|
||||
<a href="https://github.com/espressif2022/esp_emote_gfx/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/ESP--IDF-5.0%2B-red" alt="ESP-IDF 5.0+">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/rendering-software-2f855a" alt="Software Rendering">
|
||||
<img src="https://img.shields.io/badge/target-small%20displays-0f766e" alt="Small Displays">
|
||||
<img src="https://img.shields.io/badge/motion-path%20driven-d97706" alt="Path Driven Motion">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://espressif2022.github.io/esp_emote_gfx/zh_CN/index.html">中文文档</a> |
|
||||
<a href="https://espressif2022.github.io/esp_emote_gfx/en/index.html">English Docs</a> |
|
||||
<a href="https://components.espressif.com/components/espressif2022/esp_emote_gfx">Component Registry</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>把嵌入式小屏 UI 里常见的显示对象、图像、文本、动画、二维码和 Motion 场景,收进一套轻量图形库。</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
适合资源受限但仍需要流畅动效、清晰文字和轻量交互的小屏产品。
|
||||
</p>
|
||||
|
||||
## 功能框架
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/_static/esp_emote_gfx_framework.svg" alt="ESP Emote GFX framework" width="920">
|
||||
</p>
|
||||
|
||||
## 模块说明
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%">
|
||||
<strong>基础控件</strong>
|
||||
<br>
|
||||
提供图片、文本、按钮、二维码、动画和 Motion 场景等常用 UI 元素。
|
||||
</td>
|
||||
<td width="33%">
|
||||
<strong>渲染与图像</strong>
|
||||
<br>
|
||||
覆盖软件绘制、图像资源、RGB565 / RGB565A8 数据,以及基于控制点的 mesh image 形变。
|
||||
</td>
|
||||
<td width="33%">
|
||||
<strong>文本与字体</strong>
|
||||
<br>
|
||||
支持 LVGL bitmap font 和 FreeType TTF/OTF 字体渲染。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="33%">
|
||||
<strong>动画播放</strong>
|
||||
<br>
|
||||
负责 EAF 播放、分段控制、循环模式,以及 timer-driven 的状态更新。
|
||||
</td>
|
||||
<td width="33%">
|
||||
<strong>Motion 场景</strong>
|
||||
<br>
|
||||
面向路径驱动的角色、表情和交互动效,支持生成式 asset、pose/action 切换和颜色/纹理绑定。
|
||||
</td>
|
||||
<td width="33%">
|
||||
<strong>嵌入式集成</strong>
|
||||
<br>
|
||||
作为组件接入工程,连接显示刷新、输入、内存 buffer 和线程安全对象访问。
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 文档
|
||||
|
||||
详细安装、API、示例、Motion 架构和测试工程说明都放在在线文档里:
|
||||
|
||||
- 中文文档:<https://espressif2022.github.io/esp_emote_gfx/zh_CN/index.html>
|
||||
- English docs: <https://espressif2022.github.io/esp_emote_gfx/en/index.html>
|
||||
- Component Registry: <https://components.espressif.com/components/espressif2022/esp_emote_gfx>
|
||||
|
||||
## English
|
||||
|
||||
ESP Emote GFX is a lightweight software-rendered graphics library for compact embedded displays that need expressive UI elements without pulling in a heavy graphics stack.
|
||||
|
||||
For installation, API references, examples, and motion architecture notes, please visit the online documentation:
|
||||
|
||||
- Documentation: <https://espressif2022.github.io/esp_emote_gfx/en/index.html>
|
||||
- Component Registry: <https://components.espressif.com/components/espressif2022/esp_emote_gfx>
|
||||
|
||||
## License
|
||||
|
||||
ESP Emote GFX is licensed under the Apache License 2.0. See [LICENSE](LICENSE).
|
||||
@ -0,0 +1,41 @@
|
||||
DEFAULT:
|
||||
perform_check: yes # should the check be performed?
|
||||
# Sections setting this to 'no' don't need to include any other options as they are ignored
|
||||
# When a file is using a section with the option set to 'no', no checks are performed.
|
||||
|
||||
# what licenses (or license expressions) are allowed for files in this section
|
||||
# when setting this option in a section, you need to list all the allowed licenses
|
||||
allowed_licenses:
|
||||
- Apache-2.0
|
||||
license_for_new_files: Apache-2.0 # license to be used when inserting a new copyright notice
|
||||
new_notice_c: | # notice for new C, CPP, H, HPP and LD files
|
||||
/*
|
||||
* SPDX-FileCopyrightText: {years} Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: {license}
|
||||
*/
|
||||
new_notice_python: | # notice for new python files
|
||||
# SPDX-FileCopyrightText: {years} Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: {license}
|
||||
|
||||
# comment lines matching:
|
||||
# SPDX-FileCopyrightText: year[-year] Espressif Systems
|
||||
# or
|
||||
# SPDX-FileContributor: year[-year] Espressif Systems
|
||||
# are replaced with this template prefixed with the correct comment notation (# or // or *) and SPDX- notation
|
||||
espressif_copyright: '{years} Espressif Systems (Shanghai) CO LTD'
|
||||
|
||||
# You can create your own rules for files or group of files
|
||||
examples_and_unit_tests:
|
||||
include:
|
||||
- '**/**/test_apps/**'
|
||||
- 'products/'
|
||||
allowed_licenses:
|
||||
- Apache-2.0
|
||||
- Unlicense
|
||||
- CC0-1.0
|
||||
license_for_new_files: CC0-1.0
|
||||
|
||||
# ignore: # You can also select ignoring files here
|
||||
# perform_check: no # Don't check files from that block
|
||||
# include:
|
||||
18
managed_components/espressif2022__esp_emote_gfx/docs/.gitignore
vendored
Normal file
18
managed_components/espressif2022__esp_emote_gfx/docs/.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
_build/
|
||||
doxygen_output/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.tox/
|
||||
.cache/
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
.eggs/
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
# Default HTML output matches postprocess_docs.sh (English under html/en/)
|
||||
BUILDDIR = _build/html/en
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
56
managed_components/espressif2022__esp_emote_gfx/docs/_static/esp_emote_gfx.css
vendored
Normal file
56
managed_components/espressif2022__esp_emote_gfx/docs/_static/esp_emote_gfx.css
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Minimal overrides: EN/中文 switcher only. Theme colors come from sphinx_idf_theme (light).
|
||||
*/
|
||||
|
||||
.gfx-langbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
z-index: 400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: rgba(252, 252, 252, 0.95);
|
||||
color: #404040;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 0 0 6px 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.gfx-lang-btn {
|
||||
color: #2980b9 !important;
|
||||
text-decoration: none !important;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gfx-lang-btn:hover {
|
||||
color: #2e8bcc !important;
|
||||
background: rgba(41, 128, 185, 0.08);
|
||||
}
|
||||
|
||||
.gfx-lang-btn.is-active {
|
||||
color: #2b2b2b !important;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.gfx-lang-btn--muted {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gfx-lang-sep {
|
||||
opacity: 0.45;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wy-nav-content {
|
||||
padding-top: 2.25rem;
|
||||
}
|
||||
}
|
||||
88
managed_components/espressif2022__esp_emote_gfx/docs/_static/esp_emote_gfx_framework.svg
vendored
Normal file
88
managed_components/espressif2022__esp_emote_gfx/docs/_static/esp_emote_gfx_framework.svg
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
<svg width="1040" height="520" viewBox="0 0 1040 520" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>
|
||||
.bg { fill: #ffffff; }
|
||||
.frame { fill: #f8fafc; stroke: #cbd5e1; stroke-width: 1.2; }
|
||||
.group { fill: #ffffff; stroke: #d8dee8; stroke-width: 1.2; }
|
||||
.group-soft { fill: #f9fbfd; stroke: #d8dee8; stroke-width: 1.2; }
|
||||
.accent { fill: #ecfdf5; stroke: #0f766e; stroke-width: 1.4; }
|
||||
.asset { fill: #fff7ed; stroke: #d97706; stroke-width: 1.4; }
|
||||
text { font-family: Arial, sans-serif; }
|
||||
.title { font-size: 28px; font-weight: 700; fill: #111827; }
|
||||
.caption { font-size: 14px; font-weight: 500; fill: #64748b; }
|
||||
.section { font-size: 16px; font-weight: 700; fill: #334155; }
|
||||
.label { font-size: 14px; font-weight: 700; fill: #111827; }
|
||||
.small { font-size: 12px; font-weight: 500; fill: #64748b; }
|
||||
.line { stroke: #94a3b8; stroke-width: 1.5; stroke-linecap: round; }
|
||||
.arrow { fill: #94a3b8; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<rect class="bg" width="1040" height="520" rx="20"/>
|
||||
<rect class="frame" x="24" y="24" width="992" height="472" rx="18"/>
|
||||
|
||||
<text class="title" x="520" y="62" text-anchor="middle">Functional Framework</text>
|
||||
<text class="caption" x="520" y="88" text-anchor="middle">Widgets, runtime services, software rendering, and embedded display integration</text>
|
||||
|
||||
<rect class="group" x="72" y="120" width="896" height="96" rx="14"/>
|
||||
<text class="section" x="96" y="150">Widget Layer</text>
|
||||
|
||||
<g>
|
||||
<rect class="accent" x="244" y="138" width="92" height="42" rx="9"/>
|
||||
<text class="label" x="290" y="164" text-anchor="middle">Label</text>
|
||||
<rect class="accent" x="352" y="138" width="92" height="42" rx="9"/>
|
||||
<text class="label" x="398" y="164" text-anchor="middle">Image</text>
|
||||
<rect class="accent" x="460" y="138" width="92" height="42" rx="9"/>
|
||||
<text class="label" x="506" y="164" text-anchor="middle">Button</text>
|
||||
<rect class="accent" x="568" y="138" width="92" height="42" rx="9"/>
|
||||
<text class="label" x="614" y="164" text-anchor="middle">QR Code</text>
|
||||
<rect class="accent" x="676" y="138" width="112" height="42" rx="9"/>
|
||||
<text class="label" x="732" y="164" text-anchor="middle">Animation</text>
|
||||
<rect class="accent" x="804" y="138" width="126" height="42" rx="9"/>
|
||||
<text class="label" x="867" y="164" text-anchor="middle">Motion Scene</text>
|
||||
</g>
|
||||
|
||||
<text class="small" x="96" y="184">User-facing UI objects</text>
|
||||
<text class="small" x="244" y="200">Object properties, layout, invalidation, and drawing callbacks</text>
|
||||
|
||||
<line class="line" x1="520" y1="216" x2="520" y2="244"/>
|
||||
<path class="arrow" d="M520 252L514 242H526L520 252Z"/>
|
||||
|
||||
<rect class="group" x="72" y="252" width="896" height="104" rx="14"/>
|
||||
<text class="section" x="96" y="284">Core Runtime</text>
|
||||
|
||||
<g>
|
||||
<rect class="group-soft" x="244" y="274" width="126" height="48" rx="9"/>
|
||||
<text class="label" x="307" y="301" text-anchor="middle">Object Tree</text>
|
||||
<text class="small" x="307" y="316" text-anchor="middle">state + hierarchy</text>
|
||||
|
||||
<rect class="group-soft" x="392" y="274" width="136" height="48" rx="9"/>
|
||||
<text class="label" x="460" y="301" text-anchor="middle">Display Route</text>
|
||||
<text class="small" x="460" y="316" text-anchor="middle">refresh + flush</text>
|
||||
|
||||
<rect class="group-soft" x="550" y="274" width="112" height="48" rx="9"/>
|
||||
<text class="label" x="606" y="301" text-anchor="middle">Timer</text>
|
||||
<text class="small" x="606" y="316" text-anchor="middle">animation ticks</text>
|
||||
|
||||
<rect class="group-soft" x="684" y="274" width="112" height="48" rx="9"/>
|
||||
<text class="label" x="740" y="301" text-anchor="middle">Touch</text>
|
||||
<text class="small" x="740" y="316" text-anchor="middle">input events</text>
|
||||
</g>
|
||||
|
||||
<text class="small" x="96" y="318">Shared services</text>
|
||||
<text class="small" x="244" y="342">Coordinates widget lifecycle, refresh scheduling, input dispatch, and thread-safe access.</text>
|
||||
|
||||
<line class="line" x1="520" y1="356" x2="520" y2="382"/>
|
||||
<path class="arrow" d="M520 390L514 380H526L520 390Z"/>
|
||||
|
||||
<rect class="group" x="72" y="390" width="428" height="72" rx="14"/>
|
||||
<text class="section" x="96" y="422">Rendering & Assets</text>
|
||||
<text class="small" x="96" y="446">Software draw · blend · mesh image · fonts · EAF · image converter</text>
|
||||
|
||||
<rect class="asset" x="540" y="390" width="428" height="72" rx="14"/>
|
||||
<text class="section" x="564" y="422">Platform Integration</text>
|
||||
<text class="small" x="564" y="446">Display buffer · flush callback · input source · component packaging</text>
|
||||
|
||||
<line class="line" x1="500" y1="426" x2="540" y2="426"/>
|
||||
<path class="arrow" d="M548 426L538 420V432L548 426Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
52
managed_components/espressif2022__esp_emote_gfx/docs/_static/lang_switch.js
vendored
Normal file
52
managed_components/espressif2022__esp_emote_gfx/docs/_static/lang_switch.js
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* ZH / EN switcher: same path under /en/ or /zh_CN/ (GitHub Pages subpath safe).
|
||||
*/
|
||||
(function () {
|
||||
function swapLang(pathname, targetLang) {
|
||||
var trimmed = pathname.replace(/^\/+|\/+$/g, '');
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
var segments = trimmed.split('/');
|
||||
var i = segments.indexOf('en');
|
||||
if (i === -1) {
|
||||
i = segments.indexOf('zh_CN');
|
||||
}
|
||||
if (i === -1) {
|
||||
return null;
|
||||
}
|
||||
segments[i] = targetLang;
|
||||
return '/' + segments.join('/');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var enBtn = document.getElementById('gfx-lang-en');
|
||||
var zhBtn = document.getElementById('gfx-lang-zh');
|
||||
if (!enBtn || !zhBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
var path = window.location.pathname;
|
||||
var enHref = swapLang(path, 'en');
|
||||
var zhHref = swapLang(path, 'zh_CN');
|
||||
|
||||
if (!enHref || !zhHref) {
|
||||
enBtn.classList.add('gfx-lang-btn--muted');
|
||||
zhBtn.classList.add('gfx-lang-btn--muted');
|
||||
enBtn.setAttribute('aria-disabled', 'true');
|
||||
zhBtn.setAttribute('aria-disabled', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
enBtn.href = enHref;
|
||||
zhBtn.href = zhHref;
|
||||
|
||||
if (path.indexOf('/zh_CN/') !== -1) {
|
||||
zhBtn.classList.add('is-active');
|
||||
zhBtn.setAttribute('aria-current', 'true');
|
||||
} else {
|
||||
enBtn.classList.add('is-active');
|
||||
enBtn.setAttribute('aria-current', 'true');
|
||||
}
|
||||
});
|
||||
})();
|
||||
11
managed_components/espressif2022__esp_emote_gfx/docs/_templates/layout.html
vendored
Normal file
11
managed_components/espressif2022__esp_emote_gfx/docs/_templates/layout.html
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{# Extends sphinx_idf_theme: ZH/EN bar + theme CSS (see conf.py html_css_files). #}
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block extrabody %}
|
||||
{{ super() }}
|
||||
<div id="gfx-langbar" class="gfx-langbar" role="navigation" aria-label="Language">
|
||||
<a id="gfx-lang-en" class="gfx-lang-btn" href="#">EN</a>
|
||||
<span class="gfx-lang-sep" aria-hidden="true">|</span>
|
||||
<a id="gfx-lang-zh" class="gfx-lang-btn" href="#">中文</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,104 @@
|
||||
Core System (gfx_core)
|
||||
======================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_core_config_t
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
uint32_t fps; /**< Target FPS (frames per second) */
|
||||
struct {
|
||||
int task_priority; /**< Render task priority (1–20) */
|
||||
int task_stack; /**< Render task stack size (bytes) */
|
||||
int task_affinity; /**< CPU core (-1: any, 0/1: pinned) */
|
||||
unsigned task_stack_caps; /**< Stack heap caps (see esp_heap_caps.h) */
|
||||
} task;
|
||||
} gfx_core_config_t;
|
||||
|
||||
Macros
|
||||
------
|
||||
|
||||
GFX_EMOTE_INIT_CONFIG()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#define GFX_EMOTE_INIT_CONFIG() \
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_emote_init()
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_handle_t gfx_emote_init(const gfx_core_config_t *cfg);
|
||||
|
||||
gfx_emote_deinit()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Deinitialize graphics context
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_emote_deinit(gfx_handle_t handle);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``handle`` - Graphics handle
|
||||
|
||||
gfx_emote_lock()
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Lock the recursive render mutex to prevent rendering during external operations
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_emote_lock(gfx_handle_t handle);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``handle`` - Graphics handle
|
||||
|
||||
**Returns:**
|
||||
|
||||
* esp_err_t ESP_OK on success, otherwise an error code
|
||||
|
||||
gfx_emote_unlock()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Unlock the recursive render mutex after external operations
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_emote_unlock(gfx_handle_t handle);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``handle`` - Graphics handle
|
||||
|
||||
**Returns:**
|
||||
|
||||
* esp_err_t ESP_OK on success, otherwise an error code
|
||||
|
||||
gfx_refr_now()
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Perform one synchronous refresh (render and flush) immediately. Holds the render mutex for the duration; safe to call from any task.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_refr_now(gfx_handle_t handle);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``handle`` - Graphics handle
|
||||
|
||||
**Returns:**
|
||||
|
||||
* esp_err_t ESP_OK on success, otherwise an error code
|
||||
@ -0,0 +1,271 @@
|
||||
Display (gfx_disp)
|
||||
==================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_disp_flush_cb_t
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void (*gfx_disp_flush_cb_t)(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data);
|
||||
|
||||
gfx_disp_update_cb_t
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void (*gfx_disp_update_cb_t)(gfx_disp_t *disp, gfx_disp_event_t event, const void *obj);
|
||||
|
||||
gfx_disp_event_t
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_DISP_EVENT_IDLE = 0,
|
||||
GFX_DISP_EVENT_ONE_FRAME_DONE,
|
||||
GFX_DISP_EVENT_PART_FRAME_DONE,
|
||||
GFX_DISP_EVENT_ALL_FRAME_DONE,
|
||||
} gfx_disp_event_t;
|
||||
|
||||
gfx_perf_counter_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
uint64_t calls; /**< Number of API calls */
|
||||
uint64_t pixels; /**< Processed pixels */
|
||||
uint64_t time_us; /**< Elapsed time in microseconds */
|
||||
} gfx_perf_counter_t;
|
||||
|
||||
gfx_blend_perf_stats_t
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
gfx_perf_counter_t fill; /**< gfx_sw_blend_fill_area */
|
||||
gfx_perf_counter_t color_draw; /**< gfx_sw_blend_draw */
|
||||
gfx_perf_counter_t image_draw; /**< gfx_sw_blend_img_draw */
|
||||
gfx_perf_counter_t triangle_draw; /**< gfx_sw_blend_img_triangle_draw */
|
||||
uint64_t triangle_covered_pixels; /**< Triangle pixels blended (inside + AA) */
|
||||
uint64_t triangle_aa_pixels; /**< Triangle edge-AA blended pixels */
|
||||
} gfx_blend_perf_stats_t;
|
||||
|
||||
gfx_disp_perf_stats_t
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
uint32_t dirty_pixels; /**< Dirty pixels in the latest rendered frame */
|
||||
uint64_t frame_time_us; /**< Total frame time */
|
||||
uint64_t render_time_us; /**< Time spent in render phase */
|
||||
uint64_t flush_time_us; /**< Time spent in flush callbacks */
|
||||
uint32_t flush_count; /**< Number of flush calls */
|
||||
gfx_blend_perf_stats_t blend; /**< Blend-stage details */
|
||||
} gfx_disp_perf_stats_t;
|
||||
|
||||
gfx_disp_config_t
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
uint32_t h_res; /**< Screen width in pixels */
|
||||
uint32_t v_res; /**< Screen height in pixels */
|
||||
gfx_disp_flush_cb_t flush_cb; /**< Flush callback for this display */
|
||||
gfx_disp_update_cb_t update_cb; /**< Update callback (frame/playback events) */
|
||||
void *user_data; /**< User data for this display */
|
||||
struct {
|
||||
unsigned char swap : 1; /**< Color swap flag */
|
||||
unsigned char buff_dma : 1; /**< Alloc buffer with MALLOC_CAP_DMA (internal alloc only) */
|
||||
unsigned char buff_spiram : 1; /**< Alloc buffer in PSRAM (internal alloc only) */
|
||||
unsigned char double_buffer : 1; /**< Alloc second buffer for double buffering (internal alloc only) */
|
||||
unsigned char full_frame : 1; /**< 1 = buf1/buf2 are full-screen framebuffers (e.g. RGB); draw at chunk region. 0 = partition buffer; draw from start. */
|
||||
} flags;
|
||||
struct {
|
||||
void *buf1; /**< Frame buffer 1 (NULL = internal alloc) */
|
||||
void *buf2; /**< Frame buffer 2 (NULL = internal alloc) */
|
||||
size_t buf_pixels; /**< Size per buffer in pixels (0 = auto) */
|
||||
} buffers;
|
||||
} gfx_disp_config_t;
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_disp_add()
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_disp_t * gfx_disp_add(gfx_handle_t handle, const gfx_disp_config_t *cfg);
|
||||
|
||||
gfx_disp_del()
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Remove a display from the list and release its resources (child list nodes, event group, buffers). Does not free the gfx_disp_t; caller must free(disp) after.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_disp_del(gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add; safe to pass NULL
|
||||
|
||||
gfx_disp_refresh_all()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Invalidate full screen of a display to trigger refresh
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_disp_refresh_all(gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add
|
||||
|
||||
gfx_disp_flush_ready()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Notify that flush is done (e.g. from panel IO callback)
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
bool gfx_disp_flush_ready(gfx_disp_t *disp, bool swap_act_buf);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add
|
||||
* ``swap_act_buf`` - Whether to swap the active buffer
|
||||
|
||||
**Returns:**
|
||||
|
||||
* bool True on success
|
||||
|
||||
gfx_disp_get_user_data()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get user data for a display
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void * gfx_disp_get_user_data(gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add
|
||||
|
||||
**Returns:**
|
||||
|
||||
* void* User data, or NULL
|
||||
|
||||
gfx_disp_get_hor_res()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get display horizontal resolution in pixels
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
uint32_t gfx_disp_get_hor_res(gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add (NULL allowed; returns default width)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* uint32_t Width in pixels
|
||||
|
||||
gfx_disp_get_ver_res()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get display vertical resolution in pixels
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
uint32_t gfx_disp_get_ver_res(gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add (NULL allowed; returns default height)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* uint32_t Height in pixels
|
||||
|
||||
gfx_disp_is_flushing_last()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Check if display is currently flushing the last block
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
bool gfx_disp_is_flushing_last(gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add
|
||||
|
||||
**Returns:**
|
||||
|
||||
* true if flushing last block, false otherwise
|
||||
|
||||
gfx_disp_get_perf_stats()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get latest per-display performance statistics
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_disp_get_perf_stats(gfx_disp_t *disp, gfx_disp_perf_stats_t *out_stats);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display handle
|
||||
* ``out_stats`` - Output stats structure
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success
|
||||
|
||||
gfx_disp_set_bg_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set default background color for a display
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_disp_set_bg_color(gfx_disp_t *disp, gfx_color_t color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add
|
||||
* ``color`` - Background color (e.g. RGB565)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* esp_err_t ESP_OK on success
|
||||
|
||||
gfx_disp_set_bg_enable()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Enable or disable drawing the background (fill with bg_color before widgets)
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_disp_set_bg_enable(gfx_disp_t *disp, bool enable);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add
|
||||
* ``enable`` - true to enable background (default), false to disable background
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success
|
||||
@ -0,0 +1,58 @@
|
||||
Log (gfx_log)
|
||||
=============
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_log_level_t
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_LOG_LEVEL_NONE = 0,
|
||||
GFX_LOG_LEVEL_ERROR,
|
||||
GFX_LOG_LEVEL_WARN,
|
||||
GFX_LOG_LEVEL_INFO,
|
||||
GFX_LOG_LEVEL_DEBUG,
|
||||
GFX_LOG_LEVEL_VERBOSE,
|
||||
} gfx_log_level_t;
|
||||
|
||||
gfx_log_module_t
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_LOG_MODULE_CORE = 0,
|
||||
GFX_LOG_MODULE_DISP,
|
||||
GFX_LOG_MODULE_OBJ,
|
||||
GFX_LOG_MODULE_REFR,
|
||||
GFX_LOG_MODULE_RENDER,
|
||||
GFX_LOG_MODULE_TIMER,
|
||||
GFX_LOG_MODULE_TOUCH,
|
||||
GFX_LOG_MODULE_IMG_DEC,
|
||||
GFX_LOG_MODULE_LABEL,
|
||||
GFX_LOG_MODULE_LABEL_OBJ,
|
||||
GFX_LOG_MODULE_DRAW_LABEL,
|
||||
GFX_LOG_MODULE_FONT_LV,
|
||||
GFX_LOG_MODULE_FONT_FT,
|
||||
GFX_LOG_MODULE_IMG,
|
||||
GFX_LOG_MODULE_QRCODE,
|
||||
GFX_LOG_MODULE_BUTTON,
|
||||
GFX_LOG_MODULE_ANIM,
|
||||
GFX_LOG_MODULE_ANIM_DEC,
|
||||
GFX_LOG_MODULE_EAF_DEC,
|
||||
GFX_LOG_MODULE_QRCODE_LIB,
|
||||
GFX_LOG_MODULE_COUNT,
|
||||
} gfx_log_module_t;
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_log_set_level()
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_log_set_level(gfx_log_module_t module, gfx_log_level_t level);
|
||||
@ -0,0 +1,203 @@
|
||||
Object (gfx_obj)
|
||||
================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_obj_touch_cb_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Application-level touch callback (register with gfx_obj_set_touch_cb)
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void (*gfx_obj_touch_cb_t)(gfx_obj_t *obj, const gfx_touch_event_t *event, void *user_data);
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_obj_set_pos()
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_set_pos(gfx_obj_t *obj, gfx_coord_t x, gfx_coord_t y);
|
||||
|
||||
gfx_obj_set_size()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the size of an object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_set_size(gfx_obj_t *obj, uint16_t w, uint16_t h);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the object
|
||||
* ``w`` - Width
|
||||
* ``h`` - Height
|
||||
|
||||
gfx_obj_align()
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Align an object relative to the screen or another object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_align(gfx_obj_t *obj, uint8_t align, gfx_coord_t x_ofs, gfx_coord_t y_ofs);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the object to align
|
||||
* ``align`` - Alignment type (see GFX_ALIGN_* constants)
|
||||
* ``x_ofs`` - X offset from the alignment position
|
||||
* ``y_ofs`` - Y offset from the alignment position
|
||||
|
||||
gfx_obj_align_to()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Align an object relative to another object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_align_to(gfx_obj_t *obj, gfx_obj_t *base, uint8_t align, gfx_coord_t x_ofs, gfx_coord_t y_ofs);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the object to align
|
||||
* ``base`` - Reference object; NULL means align to the display
|
||||
* ``align`` - Alignment type (see GFX_ALIGN_* constants)
|
||||
* ``x_ofs`` - X offset from the alignment position
|
||||
* ``y_ofs`` - Y offset from the alignment position
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success
|
||||
|
||||
gfx_obj_set_visible()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set object visibility
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_set_visible(gfx_obj_t *obj, bool visible);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Object to set visibility for
|
||||
* ``visible`` - True to make object visible, false to hide
|
||||
|
||||
gfx_obj_get_visible()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get object visibility
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
bool gfx_obj_get_visible(gfx_obj_t *obj);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Object to check visibility for
|
||||
|
||||
**Returns:**
|
||||
|
||||
* True if object is visible, false if hidden
|
||||
|
||||
gfx_obj_update_layout()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Update object's layout (mark for recalculation before rendering)
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_obj_update_layout(gfx_obj_t *obj);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Object to update layout
|
||||
|
||||
**Note:**
|
||||
|
||||
This is used when object properties that affect layout have changed, but the actual position calculation needs to be deferred until rendering
|
||||
|
||||
gfx_obj_get_pos()
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get the position of an object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_get_pos(gfx_obj_t *obj, gfx_coord_t *x, gfx_coord_t *y);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the object
|
||||
* ``x`` - Pointer to store X coordinate
|
||||
* ``y`` - Pointer to store Y coordinate
|
||||
|
||||
gfx_obj_get_size()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get the size of an object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_get_size(gfx_obj_t *obj, uint16_t *w, uint16_t *h);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the object
|
||||
* ``w`` - Pointer to store width
|
||||
* ``h`` - Pointer to store height
|
||||
|
||||
gfx_obj_delete()
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Delete an object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_delete(gfx_obj_t *obj);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the object to delete
|
||||
|
||||
gfx_obj_set_touch_cb()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Register application touch callback for an object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_obj_set_touch_cb(gfx_obj_t *obj, gfx_obj_touch_cb_t cb, void *user_data);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Object to listen on
|
||||
* ``cb`` - Callback (NULL to clear)
|
||||
* ``user_data`` - Passed to cb
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success
|
||||
|
||||
gfx_obj_get_trace_id()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get object creation sequence id (monotonic per process lifetime)
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
uint32_t gfx_obj_get_trace_id(gfx_obj_t *obj);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Object pointer
|
||||
|
||||
**Returns:**
|
||||
|
||||
* uint32_t Sequence id, 0 if obj is NULL
|
||||
@ -0,0 +1,157 @@
|
||||
Timer (gfx_timer)
|
||||
=================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_timer_handle_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void *gfx_timer_handle_t;
|
||||
|
||||
gfx_timer_cb_t
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void (*gfx_timer_cb_t)(void *);
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_timer_create()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_timer_handle_t gfx_timer_create(void *handle, gfx_timer_cb_t timer_cb, uint32_t period, void *user_data);
|
||||
|
||||
gfx_timer_delete()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Delete a timer
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_timer_delete(void *handle, gfx_timer_handle_t timer);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``handle`` - Player handle
|
||||
* ``timer`` - Timer handle to delete
|
||||
|
||||
gfx_timer_pause()
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Pause a timer
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_timer_pause(gfx_timer_handle_t timer);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``timer`` - Timer handle to pause
|
||||
|
||||
gfx_timer_resume()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Resume a timer
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_timer_resume(gfx_timer_handle_t timer);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``timer`` - Timer handle to resume
|
||||
|
||||
gfx_timer_is_running()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Check if a timer is running
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
bool gfx_timer_is_running(gfx_timer_handle_t timer_handle);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``timer_handle`` - Timer handle to check
|
||||
|
||||
**Returns:**
|
||||
|
||||
* true if timer is running, false otherwise
|
||||
|
||||
gfx_timer_set_repeat_count()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set timer repeat count
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_timer_set_repeat_count(gfx_timer_handle_t timer, int32_t repeat_count);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``timer`` - Timer handle to modify
|
||||
* ``repeat_count`` - Number of times to repeat (-1 for infinite)
|
||||
|
||||
gfx_timer_set_period()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set timer period
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_timer_set_period(gfx_timer_handle_t timer, uint32_t period);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``timer`` - Timer handle to modify
|
||||
* ``period`` - New period in milliseconds
|
||||
|
||||
gfx_timer_reset()
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Reset a timer
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_timer_reset(gfx_timer_handle_t timer);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``timer`` - Timer handle to reset
|
||||
|
||||
gfx_timer_tick_get()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get current system tick
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
uint32_t gfx_timer_tick_get(void);
|
||||
|
||||
**Returns:**
|
||||
|
||||
* Current tick value in milliseconds
|
||||
|
||||
gfx_timer_get_actual_fps()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Get actual FPS from timer manager
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
uint32_t gfx_timer_get_actual_fps(void *handle);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``handle`` - Player handle
|
||||
|
||||
**Returns:**
|
||||
|
||||
* Actual FPS value, 0 if handle is invalid
|
||||
@ -0,0 +1,77 @@
|
||||
Touch (gfx_touch)
|
||||
=================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_touch_event_cb_t
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void (*gfx_touch_event_cb_t)(gfx_touch_t *touch, const gfx_touch_event_t *event, void *user_data);
|
||||
|
||||
gfx_touch_event_type_t
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_TOUCH_EVENT_PRESS = 0,
|
||||
GFX_TOUCH_EVENT_RELEASE,
|
||||
GFX_TOUCH_EVENT_MOVE, /**< Finger moved while pressed (slide) */
|
||||
} gfx_touch_event_type_t;
|
||||
|
||||
gfx_touch_config_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
esp_lcd_touch_handle_t handle; /**< LCD touch driver handle */
|
||||
gfx_touch_event_cb_t event_cb; /**< Event callback */
|
||||
uint32_t poll_ms; /**< Poll interval ms (0 = default) */
|
||||
gfx_disp_t *disp; /**< Display handle */
|
||||
void *user_data; /**< User data for callback */
|
||||
} gfx_touch_config_t;
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_touch_add()
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_touch_t * gfx_touch_add(gfx_handle_t handle, const gfx_touch_config_t *cfg);
|
||||
|
||||
gfx_touch_set_disp()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bind a display to a touch device
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_touch_set_disp(gfx_touch_t *touch, gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``touch`` - Touch pointer returned from gfx_touch_add
|
||||
* ``disp`` - Display to receive touch hit-testing and dispatch
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, ESP_ERR_INVALID_ARG if touch is NULL
|
||||
|
||||
gfx_touch_del()
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Remove a touch device from the list and release resources (stops polling, disables IRQ). Does not free the gfx_touch_t; caller must free(touch) after.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_touch_del(gfx_touch_t *touch);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``touch`` - Touch pointer returned from gfx_touch_add; safe to pass NULL
|
||||
@ -0,0 +1,85 @@
|
||||
Types (gfx_types)
|
||||
=================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_opa_t
|
||||
~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef uint8_t gfx_opa_t;
|
||||
|
||||
gfx_coord_t
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef int16_t gfx_coord_t;
|
||||
|
||||
gfx_handle_t
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void *gfx_handle_t;
|
||||
|
||||
gfx_area_t
|
||||
~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
gfx_coord_t x1;
|
||||
gfx_coord_t y1;
|
||||
gfx_coord_t x2;
|
||||
gfx_coord_t y2;
|
||||
} gfx_area_t;
|
||||
|
||||
Macros
|
||||
------
|
||||
|
||||
GFX_BUFFER_OFFSET_16BPP()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Calculate buffer pointer with offset for 16-bit format (RGB565)
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#define GFX_BUFFER_OFFSET_16BPP(buffer, y_offset, stride, x_offset) \
|
||||
|
||||
GFX_BUFFER_OFFSET_8BPP()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Calculate buffer pointer with offset for 8-bit format
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#define GFX_BUFFER_OFFSET_8BPP(buffer, y_offset, stride, x_offset) \
|
||||
|
||||
GFX_BUFFER_OFFSET_4BPP()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Calculate buffer pointer with offset for 4-bit format (2 pixels per byte)
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#define GFX_BUFFER_OFFSET_4BPP(buffer, y_offset, stride, x_offset) \
|
||||
|
||||
GFX_COLOR_HEX()
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#define GFX_COLOR_HEX(color) ((gfx_color_t)gfx_color_hex(color))
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_color_hex()
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_color_t gfx_color_hex(uint32_t c);
|
||||
@ -0,0 +1,26 @@
|
||||
Core API Reference
|
||||
==================
|
||||
|
||||
The core API provides the foundation for the graphics framework, including initialization, object management, and basic types.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
gfx_core
|
||||
gfx_disp
|
||||
gfx_log
|
||||
gfx_obj
|
||||
gfx_timer
|
||||
gfx_touch
|
||||
gfx_types
|
||||
|
||||
Core Modules
|
||||
------------
|
||||
|
||||
* :doc:`gfx_core` - Core System (gfx_core)
|
||||
* :doc:`gfx_disp` - Display (gfx_disp)
|
||||
* :doc:`gfx_log` - Log (gfx_log)
|
||||
* :doc:`gfx_obj` - Object (gfx_obj)
|
||||
* :doc:`gfx_timer` - Timer (gfx_timer)
|
||||
* :doc:`gfx_touch` - Touch (gfx_touch)
|
||||
* :doc:`gfx_types` - Types (gfx_types)
|
||||
@ -0,0 +1,229 @@
|
||||
Animation (gfx_anim)
|
||||
====================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_anim_segment_action_t
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_ANIM_SEGMENT_ACTION_CONTINUE = 0,
|
||||
GFX_ANIM_SEGMENT_ACTION_PAUSE,
|
||||
} gfx_anim_segment_action_t;
|
||||
|
||||
gfx_anim_src_type_t
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Public animation source type.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_ANIM_SRC_TYPE_MEMORY = 0, /**< In-memory animation payload */
|
||||
} gfx_anim_src_type_t;
|
||||
|
||||
gfx_anim_segment_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Playback description for one animation segment.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
uint32_t start; /* inclusive start frame */
|
||||
uint32_t end; /* inclusive end frame */
|
||||
uint32_t fps; /* playback fps for this segment */
|
||||
uint32_t play_count; /* total plays for this segment, 0 means forever */
|
||||
gfx_anim_segment_action_t end_action; /* action after the last play finishes */
|
||||
} gfx_anim_segment_t;
|
||||
|
||||
gfx_anim_src_t
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Typed animation source descriptor.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
gfx_anim_src_type_t type; /**< Source payload type */
|
||||
const void *data; /**< Type-specific payload pointer */
|
||||
size_t data_len; /**< Payload length in bytes */
|
||||
} gfx_anim_src_t;
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_anim_create()
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t * gfx_anim_create(gfx_disp_t *disp);
|
||||
|
||||
gfx_anim_set_src_desc()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the typed source descriptor for an animation object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_set_src_desc(gfx_obj_t *obj, const gfx_anim_src_t *src);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
* ``src`` - Pointer to the typed source descriptor
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_anim_set_src()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the source data for an animation object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_set_src(gfx_obj_t *obj, const void *src_data, size_t src_len);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
* ``src_data`` - Source data
|
||||
* ``src_len`` - Source data length
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_anim_set_segment()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the segment for an animation object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_set_segment(gfx_obj_t *obj, uint32_t start, uint32_t end, uint32_t fps, bool repeat);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
* ``start`` - Start frame index
|
||||
* ``end`` - End frame index
|
||||
* ``fps`` - Frames per second
|
||||
* ``repeat`` - Whether to repeat the animation
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_anim_set_segments()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set a segment playback plan for an animation object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_set_segments(gfx_obj_t *obj, const gfx_anim_segment_t *segments, size_t segment_count);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
* ``segments`` - Segment plan array
|
||||
* ``segment_count`` - Number of segment entries in the array
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_anim_play_left_to_tail()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Drain the remaining segment plan and block until playback finishes
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_play_left_to_tail(gfx_obj_t *obj);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, ESP_ERR_NOT_FOUND if there is no remaining work, or another ESP_ERR_* code on failure
|
||||
|
||||
gfx_anim_start()
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Start the animation
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_start(gfx_obj_t *obj);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_anim_stop()
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Stop the animation
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_stop(gfx_obj_t *obj);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_anim_set_mirror()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set mirror display for an animation object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_set_mirror(gfx_obj_t *obj, bool enabled, int16_t offset);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the animation object
|
||||
* ``enabled`` - Whether to enable mirror display
|
||||
* ``offset`` - Mirror offset in pixels
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_anim_set_auto_mirror()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set auto mirror alignment for animation object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_anim_set_auto_mirror(gfx_obj_t *obj, bool enabled);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Animation object
|
||||
* ``enabled`` - Whether to enable auto mirror alignment
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, ESP_ERR_* otherwise
|
||||
@ -0,0 +1,184 @@
|
||||
Button (gfx_button)
|
||||
===================
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_button_create()
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create a button object on a display
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t * gfx_button_create(gfx_disp_t *disp);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``disp`` - Display from gfx_disp_add()
|
||||
|
||||
**Returns:**
|
||||
|
||||
* Pointer to the created button object
|
||||
|
||||
gfx_button_set_text()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the label text for a button
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_text(gfx_obj_t *obj, const char *text);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``text`` - Text string; NULL is treated as an empty string
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_text_fmt()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the label text for a button using printf-style formatting
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_text_fmt(gfx_obj_t *obj, const char *fmt, ...);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``fmt`` - Format string
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_font()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the font used by the button label
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_font(gfx_obj_t *obj, gfx_font_t font);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``font`` - Font handle
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_text_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the label text color for a button
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_text_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``color`` - Text color
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_bg_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the normal background color for a button
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_bg_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``color`` - Background color
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_bg_color_pressed()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the pressed background color for a button
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_bg_color_pressed(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``color`` - Pressed background color
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_border_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the border color for a button
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_border_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``color`` - Border color
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_border_width()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the border width for a button
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_border_width(gfx_obj_t *obj, uint16_t width);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``width`` - Border width in pixels; 0 disables the border
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_button_set_text_align()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the text alignment for a button label
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_button_set_text_align(gfx_obj_t *obj, gfx_text_align_t align);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Button object
|
||||
* ``align`` - Text alignment
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
@ -0,0 +1,25 @@
|
||||
LVGL Font Compatibility (gfx_font_lvgl)
|
||||
=======================================
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_font_lv_load_from_binary()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
lv_font_t * gfx_font_lv_load_from_binary(uint8_t *bin_addr);
|
||||
|
||||
gfx_font_lv_delete()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Delete an LVGL font created from binary data
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_font_lv_delete(lv_font_t *font);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``font`` - Pointer to lv_font_t to delete
|
||||
@ -0,0 +1,112 @@
|
||||
Image (gfx_img)
|
||||
===============
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_color_format_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_COLOR_FORMAT_RGB565 = 0x04, /**< RGB565 format without alpha channel */
|
||||
GFX_COLOR_FORMAT_RGB565A8 = 0x0A, /**< RGB565 format with separate alpha channel */
|
||||
} gfx_color_format_t;
|
||||
|
||||
gfx_img_src_type_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Public image source type.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_IMG_SRC_TYPE_IMAGE_DSC = 0, /**< In-memory gfx_image_dsc_t payload */
|
||||
} gfx_img_src_type_t;
|
||||
|
||||
gfx_image_header_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic: 8; /**< Magic number. Must be GFX_IMAGE_HEADER_MAGIC */
|
||||
uint32_t cf : 8; /**< Color format: See `gfx_color_format_t` */
|
||||
uint32_t flags: 16; /**< Image flags */
|
||||
uint32_t w: 16; /**< Width of the image */
|
||||
uint32_t h: 16; /**< Height of the image */
|
||||
uint32_t stride: 16; /**< Number of bytes in a row */
|
||||
uint32_t reserved: 16; /**< Reserved for future use */
|
||||
} gfx_image_header_t;
|
||||
|
||||
gfx_image_dsc_t
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
gfx_image_header_t header; /**< A header describing the basics of the image */
|
||||
uint32_t data_size; /**< Size of the image in bytes */
|
||||
const uint8_t *data; /**< Pointer to the data of the image */
|
||||
const void *reserved; /**< Reserved field for future use */
|
||||
const void *reserved_2; /**< Reserved field for future use */
|
||||
} gfx_image_dsc_t;
|
||||
|
||||
gfx_img_src_t
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Typed image source descriptor.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
gfx_img_src_type_t type; /**< Source payload type */
|
||||
const void *data; /**< Type-specific payload pointer */
|
||||
} gfx_img_src_t;
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_img_create()
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t * gfx_img_create(gfx_disp_t *disp);
|
||||
|
||||
gfx_img_set_src_desc()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the typed source descriptor for an image object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_img_set_src_desc(gfx_obj_t *obj, const gfx_img_src_t *src);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the image object
|
||||
* ``src`` - Pointer to the typed source descriptor
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, ESP_ERR_* otherwise
|
||||
|
||||
gfx_img_set_src()
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the source data for an image object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_img_set_src(gfx_obj_t *obj, void *src);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the image object
|
||||
* ``src`` - Pointer to the image source data
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, ESP_ERR_* otherwise
|
||||
@ -0,0 +1,387 @@
|
||||
Label (gfx_label)
|
||||
=================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_font_t
|
||||
~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef void *gfx_font_t;
|
||||
|
||||
gfx_text_align_t
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_TEXT_ALIGN_AUTO, /**< Align text auto */
|
||||
GFX_TEXT_ALIGN_LEFT, /**< Align text to left */
|
||||
GFX_TEXT_ALIGN_CENTER, /**< Align text to center */
|
||||
GFX_TEXT_ALIGN_RIGHT, /**< Align text to right */
|
||||
} gfx_text_align_t;
|
||||
|
||||
gfx_label_long_mode_t
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_LABEL_LONG_WRAP, /**< Break the long lines (word wrap) */
|
||||
GFX_LABEL_LONG_SCROLL, /**< Make the text scrolling horizontally smoothly */
|
||||
GFX_LABEL_LONG_CLIP, /**< Simply clip the parts which don't fit */
|
||||
GFX_LABEL_LONG_SCROLL_SNAP, /**< Jump to next section after interval (horizontal paging) */
|
||||
} gfx_label_long_mode_t;
|
||||
|
||||
gfx_label_cfg_t
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct {
|
||||
const char *name; /**< The name of the font file */
|
||||
const void *mem; /**< The pointer to the font file */
|
||||
size_t mem_size; /**< The size of the memory */
|
||||
uint16_t font_size; /**< The size of the font */
|
||||
} gfx_label_cfg_t;
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_label_create()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t * gfx_label_create(gfx_disp_t *disp);
|
||||
|
||||
gfx_label_new_font()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create a new font
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_new_font(const gfx_label_cfg_t *cfg, gfx_font_t *ret_font);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``cfg`` - Font configuration
|
||||
* ``ret_font`` - Pointer to store the font handle
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_delete_font()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Delete a font and free its resources
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_delete_font(gfx_font_t font);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``font`` - Font handle to delete
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_text()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the text for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_text(gfx_obj_t *obj, const char *text);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``text`` - Text string to display
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_text_fmt()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the text for a label object with format
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_text_fmt(gfx_obj_t *obj, const char *fmt, ...);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``fmt`` - Format string
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the color for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``color`` - Color value
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_bg_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the background color for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_bg_color(gfx_obj_t *obj, gfx_color_t bg_color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``bg_color`` - Background color value
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_bg_enable()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Enable or disable background for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_bg_enable(gfx_obj_t *obj, bool enable);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``enable`` - True to enable background, false to disable
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_opa()
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the opacity for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_opa(gfx_obj_t *obj, gfx_opa_t opa);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``opa`` - Opacity value (0-255)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_font()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the font for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_font(gfx_obj_t *obj, gfx_font_t font);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``font`` - Font handle
|
||||
|
||||
gfx_label_set_text_align()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the text alignment for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_text_align(gfx_obj_t *obj, gfx_text_align_t align);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``align`` - Text alignment value
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_long_mode()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the long text mode for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_long_mode(gfx_obj_t *obj, gfx_label_long_mode_t long_mode);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``long_mode`` - Long text handling mode (wrap, scroll, or clip)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_line_spacing()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the line spacing for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_line_spacing(gfx_obj_t *obj, uint16_t spacing);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``spacing`` - Line spacing in pixels
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_label_set_scroll_speed()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the horizontal scrolling speed for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_scroll_speed(gfx_obj_t *obj, uint32_t speed_ms);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``speed_ms`` - Scrolling speed in milliseconds per pixel
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
**Note:**
|
||||
|
||||
Only effective when long_mode is GFX_LABEL_LONG_SCROLL
|
||||
|
||||
gfx_label_set_scroll_loop()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set whether scrolling should loop continuously
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_scroll_loop(gfx_obj_t *obj, bool loop);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``loop`` - True to enable continuous looping, false for one-time scroll
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
**Note:**
|
||||
|
||||
Only effective when long_mode is GFX_LABEL_LONG_SCROLL
|
||||
|
||||
gfx_label_set_scroll_step()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the scroll step size for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_scroll_step(gfx_obj_t *obj, int32_t step);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``step`` - Scroll step size in pixels per timer tick (default: 1, can be negative)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
**Note:**
|
||||
|
||||
Only effective when long_mode is GFX_LABEL_LONG_SCROLL
|
||||
|
||||
**Note:**
|
||||
|
||||
Step cannot be zero
|
||||
|
||||
gfx_label_set_snap_interval()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the snap scroll interval time for a label object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_snap_interval(gfx_obj_t *obj, uint32_t interval_ms);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``interval_ms`` - Interval time in milliseconds to stay on each section before jumping
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
**Note:**
|
||||
|
||||
Only effective when long_mode is GFX_LABEL_LONG_SCROLL_SNAP
|
||||
|
||||
**Note:**
|
||||
|
||||
The jump offset is automatically calculated as the label width
|
||||
|
||||
gfx_label_set_snap_loop()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set whether snap scrolling should loop continuously
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_label_set_snap_loop(gfx_obj_t *obj, bool loop);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the label object
|
||||
* ``loop`` - True to enable continuous looping, false to stop at end
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
**Note:**
|
||||
|
||||
Only effective when long_mode is GFX_LABEL_LONG_SCROLL_SNAP
|
||||
@ -0,0 +1,96 @@
|
||||
Motion Driver (gfx_motion)
|
||||
==========================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_motion_cfg_t
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef struct gfx_motion_cfg_t {
|
||||
uint16_t timer_period_ms;
|
||||
int16_t damping_div;
|
||||
} gfx_motion_cfg_t;
|
||||
|
||||
gfx_motion_tick_cb_t
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Callback executed on each motion timer tick. Return ``true`` when state changed and an apply pass is needed.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef bool (*gfx_motion_tick_cb_t)(gfx_motion_t *motion, void *user_data);
|
||||
|
||||
gfx_motion_apply_cb_t
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Callback that pushes the current motion state into display objects.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef esp_err_t (*gfx_motion_apply_cb_t)(gfx_motion_t *motion, void *user_data, bool force_apply);
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_motion_cfg_init()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Initialize a motion config with timer period and damping divisor.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_motion_cfg_init(gfx_motion_cfg_t *cfg, uint16_t timer_period_ms, int16_t damping_div);
|
||||
|
||||
gfx_motion_init()
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Create and start a motion driver bound to a display and anchor object.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_init(gfx_motion_t *motion,
|
||||
gfx_disp_t *disp,
|
||||
gfx_obj_t *anchor,
|
||||
const gfx_motion_cfg_t *cfg,
|
||||
gfx_motion_tick_cb_t tick_cb,
|
||||
gfx_motion_apply_cb_t apply_cb,
|
||||
void *user_data);
|
||||
|
||||
gfx_motion_deinit()
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Stop and destroy the motion driver.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_motion_deinit(gfx_motion_t *motion);
|
||||
|
||||
gfx_motion_set_period()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Change the timer period of a running motion driver.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_set_period(gfx_motion_t *motion, uint16_t period_ms);
|
||||
|
||||
gfx_motion_step()
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Run one motion tick immediately.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_step(gfx_motion_t *motion, bool force_apply);
|
||||
|
||||
gfx_motion_ease_i16()
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Utility helper for damped integer interpolation.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
int16_t gfx_motion_ease_i16(int16_t cur, int16_t tgt, int16_t div);
|
||||
@ -0,0 +1,198 @@
|
||||
Scene Asset and Runtime (gfx_motion_scene)
|
||||
======================================
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
``gfx_motion_scene.h`` defines both the ROM-side scene asset format and the runtime used to play it back on a display.
|
||||
|
||||
The asset model is built around:
|
||||
|
||||
* joints
|
||||
* segments
|
||||
* poses
|
||||
* actions
|
||||
* layout metadata
|
||||
|
||||
Important Types
|
||||
---------------
|
||||
|
||||
gfx_motion_segment_kind_t
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Segment primitive kind.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_MOTION_SEG_CAPSULE = 0,
|
||||
GFX_MOTION_SEG_RING = 1,
|
||||
GFX_MOTION_SEG_BEZIER_STRIP = 2,
|
||||
GFX_MOTION_SEG_BEZIER_LOOP = 3,
|
||||
GFX_MOTION_SEG_BEZIER_FILL = 4,
|
||||
} gfx_motion_segment_kind_t;
|
||||
|
||||
gfx_motion_segment_t
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
One visual primitive wired to joints and optional style/resource metadata.
|
||||
|
||||
gfx_motion_pose_t
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
One pose containing a flat coordinate array for all joints.
|
||||
|
||||
gfx_motion_action_step_t and gfx_motion_action_t
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These describe action playback: pose target, hold time, interpolation mode, facing, and loop behavior.
|
||||
|
||||
gfx_motion_asset_t
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Top-level scene asset bundle exported into ROM and consumed by the runtime.
|
||||
|
||||
gfx_motion_player_t
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Unified display runtime that owns:
|
||||
|
||||
* one ``gfx_motion_scene_t`` parser state
|
||||
* one ``gfx_motion_t`` timer/runtime driver
|
||||
* one ``gfx_mesh_img`` object per segment
|
||||
|
||||
Scene Functions
|
||||
---------------
|
||||
|
||||
gfx_motion_scene_init()
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Validate and initialize a parser scene state.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_scene_init(gfx_motion_scene_t *scene, const gfx_motion_asset_t *asset);
|
||||
|
||||
gfx_motion_scene_set_action()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Switch the active action by index.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_scene_set_action(gfx_motion_scene_t *scene, uint16_t action_index, bool snap_now);
|
||||
|
||||
gfx_motion_scene_set_action_loop()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Override the current action loop behavior.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_scene_set_action_loop(gfx_motion_scene_t *scene, bool loop);
|
||||
|
||||
gfx_motion_scene_clear_action_loop_override()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Clear the loop override and restore the asset-defined loop flag.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_scene_clear_action_loop_override(gfx_motion_scene_t *scene);
|
||||
|
||||
gfx_motion_scene_tick()
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Advance the current pose toward its target pose.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
bool gfx_motion_scene_tick(gfx_motion_scene_t *scene);
|
||||
|
||||
gfx_motion_scene_advance()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Advance the action timeline.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_motion_scene_advance(gfx_motion_scene_t *scene);
|
||||
|
||||
Runtime Functions
|
||||
-----------------
|
||||
|
||||
gfx_motion_player_init()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create display objects for all scene segments and start the motion timer.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_player_init(gfx_motion_player_t *player,
|
||||
gfx_disp_t *disp,
|
||||
const gfx_motion_asset_t *asset);
|
||||
|
||||
gfx_motion_player_deinit()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Destroy all segment objects and stop the runtime.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void gfx_motion_player_deinit(gfx_motion_player_t *player);
|
||||
|
||||
gfx_motion_player_set_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the default runtime color used by non-palette, non-textured segments.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_player_set_color(gfx_motion_player_t *player, gfx_color_t color);
|
||||
|
||||
gfx_motion_player_set_canvas()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the destination canvas rectangle the scene is scaled into.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_player_set_canvas(gfx_motion_player_t *player,
|
||||
gfx_coord_t x, gfx_coord_t y,
|
||||
uint16_t w, uint16_t h);
|
||||
|
||||
gfx_motion_player_sync()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Force the current player state to be pushed to display objects immediately without advancing the action timeline.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_player_sync(gfx_motion_player_t *player);
|
||||
|
||||
gfx_motion_player_set_action()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Switch the active runtime action.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_player_set_action(gfx_motion_player_t *player, uint16_t action_idx, bool snap);
|
||||
|
||||
gfx_motion_player_set_action_loop()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Override action loop behavior at runtime.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_player_set_action_loop(gfx_motion_player_t *player, bool loop);
|
||||
|
||||
gfx_motion_player_clear_action_loop_override()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Clear the runtime loop override.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_motion_player_clear_action_loop_override(gfx_motion_player_t *player);
|
||||
@ -0,0 +1,121 @@
|
||||
QR Code (gfx_qrcode)
|
||||
====================
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
gfx_qrcode_ecc_t
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
typedef enum {
|
||||
GFX_QRCODE_ECC_LOW = 0, /**< The QR Code can tolerate about 7% erroneous codewords */
|
||||
GFX_QRCODE_ECC_MEDIUM, /**< The QR Code can tolerate about 15% erroneous codewords */
|
||||
GFX_QRCODE_ECC_QUARTILE, /**< The QR Code can tolerate about 25% erroneous codewords */
|
||||
GFX_QRCODE_ECC_HIGH /**< The QR Code can tolerate about 30% erroneous codewords */
|
||||
} gfx_qrcode_ecc_t;
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
gfx_qrcode_create()
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t * gfx_qrcode_create(gfx_disp_t *disp);
|
||||
|
||||
gfx_qrcode_set_data()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the data/text for a QR Code object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_qrcode_set_data(gfx_obj_t *obj, const char *data);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the QR Code object
|
||||
* ``data`` - Pointer to the null-terminated string to encode
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
**Note:**
|
||||
|
||||
The length is automatically calculated using strlen()
|
||||
|
||||
gfx_qrcode_set_size()
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the size for a QR Code object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_qrcode_set_size(gfx_obj_t *obj, uint16_t size);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the QR Code object
|
||||
* ``size`` - Size in pixels (both width and height)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_qrcode_set_ecc()
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the error correction level for a QR Code object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_qrcode_set_ecc(gfx_obj_t *obj, gfx_qrcode_ecc_t ecc);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the QR Code object
|
||||
* ``ecc`` - Error correction level
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_qrcode_set_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the foreground color for a QR Code object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_qrcode_set_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the QR Code object
|
||||
* ``color`` - Foreground color (QR modules color)
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
|
||||
gfx_qrcode_set_bg_color()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set the background color for a QR Code object
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_err_t gfx_qrcode_set_bg_color(gfx_obj_t *obj, gfx_color_t bg_color);
|
||||
|
||||
**Parameters:**
|
||||
|
||||
* ``obj`` - Pointer to the QR Code object
|
||||
* ``bg_color`` - Background color
|
||||
|
||||
**Returns:**
|
||||
|
||||
* ESP_OK on success, error code otherwise
|
||||
@ -0,0 +1,28 @@
|
||||
Widget API Reference
|
||||
====================
|
||||
|
||||
The widget API provides specialized functionality for different types of graphical elements.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
gfx_anim
|
||||
gfx_button
|
||||
gfx_font_lvgl
|
||||
gfx_img
|
||||
gfx_label
|
||||
gfx_qrcode
|
||||
gfx_motion
|
||||
gfx_motion_scene
|
||||
|
||||
Widget Modules
|
||||
--------------
|
||||
|
||||
* :doc:`gfx_anim` - Animation (gfx_anim)
|
||||
* :doc:`gfx_button` - Button (gfx_button)
|
||||
* :doc:`gfx_font_lvgl` - LVGL Font Compatibility (gfx_font_lvgl)
|
||||
* :doc:`gfx_img` - Image (gfx_img)
|
||||
* :doc:`gfx_label` - Label (gfx_label)
|
||||
* :doc:`gfx_qrcode` - QR Code (gfx_qrcode)
|
||||
* :doc:`gfx_motion` - Motion Driver (gfx_motion)
|
||||
* :doc:`gfx_motion_scene` - Motion Scene and Player (gfx_motion_scene)
|
||||
@ -0,0 +1,109 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
All notable changes to the ESP Emote GFX component will be documented in this file.
|
||||
|
||||
[3.0.5]
|
||||
------------
|
||||
* Add motion scene widget documentation covering ``gfx_motion``, ``gfx_motion_scene``, asset layout, and runtime usage
|
||||
* Add motion widget example references to README and Sphinx docs
|
||||
* Simplify the motion rendering path by removing NanoVG and libtess2 dependencies
|
||||
* Keep polygon fill on the internal scanline fallback path for a leaner release footprint
|
||||
|
||||
[3.0.4] - 2026-04-21
|
||||
--------------------
|
||||
* restore ``gfx_disp_event_t``
|
||||
* Render loop: sleep ``GFX_RENDER_TASK_IDLE_SLEEP_MS`` once before the main loop so the first frame is not driven until the caller can finish setup after ``add_disp()`` (avoids a startup deadlock)
|
||||
|
||||
[3.0.3] - 2026-04-20
|
||||
--------------------
|
||||
* Add `gfx_button` widget (text, font, normal/pressed colors, border)
|
||||
* Add `gfx_log` API for log level configuration
|
||||
* Documentation: separate English and Simplified Chinese HTML builds (gettext), language switcher, unified `postprocess_docs.sh` pipeline (API RST, Sphinx, Doxygen)
|
||||
* Simplify GitHub Actions documentation job to a single build step
|
||||
|
||||
[3.0.2] - 2026-04-17
|
||||
--------------------
|
||||
* Update version of esp_new_jpeg
|
||||
|
||||
[3.0.1] - 2026-02-13
|
||||
--------------------
|
||||
* Add CI build action for P4
|
||||
* Optimize multi-buffer switching logic
|
||||
* Fix crash when text is NULL
|
||||
* Fix missing API documentation (e.g. gfx_touch_add)
|
||||
|
||||
[3.0.0] - 2026-01-22
|
||||
--------------------
|
||||
* Add documentation build action
|
||||
* Optimize EAF 8-bit render
|
||||
* Fix FreeType parsing performance
|
||||
* Remove duplicated label-related APIs
|
||||
|
||||
[2.1.0] - 2026-01-28
|
||||
--------------------
|
||||
* Support for decoding Heatshrink-compressed image slices
|
||||
|
||||
[2.0.4] - 2026-01-22
|
||||
--------------------
|
||||
* Fix Huffman+RLE decoding buffer sizing to prevent oversized output errors (Issue `#18 <https://github.com/espressif2022/esp_emote_gfx/issues/18>`_)
|
||||
|
||||
[2.0.3] - 2026-01-08
|
||||
--------------------
|
||||
* Delete local assets
|
||||
* Build acion for ['release-v5.2', 'release-v5.3', 'release-v5.4', 'release-v5.5']
|
||||
* Fix ESP-IDF version compatibility issues
|
||||
* Change flush_callback timeout from 20 ms to wait forever
|
||||
|
||||
[2.0.2] - 2025-12-26
|
||||
--------------------
|
||||
* Add optional JPEG decoding support for EAF animations
|
||||
* Center QR code rendering in UI layout
|
||||
* Add alpha channel support for animations
|
||||
|
||||
[2.0.1] - 2025-12-05
|
||||
--------------------
|
||||
* Add Touch event
|
||||
|
||||
[2.0.0] - 2025-12-01
|
||||
--------------------
|
||||
* Added partial refresh mode support
|
||||
* Added QR code widget (gfx_qrcode)
|
||||
|
||||
[1.2.0] - 2025-09-0
|
||||
-------------------
|
||||
* use eaf as a lib
|
||||
|
||||
[1.1.2] - 2025-09-29
|
||||
--------------------
|
||||
|
||||
Upgrade dependencies
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
* Update `espressif/esp_new_jpeg` to 0.6.x by @Kevincoooool. `#8 <https://github.com/espressif2022/esp_emote_gfx/pull/8>`_
|
||||
|
||||
[1.1.1] - 2025-09-23
|
||||
--------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
* Resolve image block decoding failure in specific cases. `#6 <https://github.com/espressif2022/esp_emote_gfx/issues/6>`_
|
||||
|
||||
[1.0.0] - 2025-08-01
|
||||
--------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
* Initial release of ESP Emote GFX framework
|
||||
* Core graphics rendering engine
|
||||
* Object system for images and labels
|
||||
* Basic drawing functions and color utilities
|
||||
* Software blending capabilities
|
||||
* Timer system for animations
|
||||
* Support for ESP-IDF 5.0+
|
||||
* FreeType font rendering integration
|
||||
* JPEG image decoding support
|
||||
|
||||
Features
|
||||
~~~~~~~~
|
||||
* Lightweight graphics framework optimized for embedded systems
|
||||
* Memory-efficient design for resource-constrained environments
|
||||
76
managed_components/espressif2022__esp_emote_gfx/docs/conf.py
Normal file
76
managed_components/espressif2022__esp_emote_gfx/docs/conf.py
Normal file
@ -0,0 +1,76 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'ESP Emote GFX'
|
||||
copyright = '2024-2025, Espressif Systems (Shanghai) CO LTD'
|
||||
author = 'Espressif Systems'
|
||||
release = '1.0.0'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.intersphinx',
|
||||
'breathe', # For Doxygen integration (optional)
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# gettext / Sphinx i18n: translations live in docs/locale/<lang>/LC_MESSAGES/*.mo
|
||||
locale_dirs = ['locale']
|
||||
gettext_compact = False
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'sphinx_idf_theme'
|
||||
html_static_path = ['_static']
|
||||
html_logo = None
|
||||
html_favicon = None
|
||||
html_theme_options = {
|
||||
'display_version': True,
|
||||
}
|
||||
html_css_files = ['esp_emote_gfx.css']
|
||||
html_js_files = ['lang_switch.js']
|
||||
|
||||
project_slug = 'esp-emote-gfx'
|
||||
project_homepage = 'https://github.com/espressif2022/esp_emote_gfx'
|
||||
language = 'en'
|
||||
languages = ['en', 'zh_CN']
|
||||
idf_target = 'esp32'
|
||||
idf_targets = ['esp32']
|
||||
idf_target_title_dict = {
|
||||
'esp32': 'ESP32',
|
||||
}
|
||||
versions_url = ''
|
||||
pdf_file = ''
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# Breathe configuration (if using Doxygen)
|
||||
breathe_projects = {
|
||||
"esp_emote_gfx": "../doxygen/xml"
|
||||
}
|
||||
breathe_default_project = "esp_emote_gfx"
|
||||
|
||||
# Intersphinx mapping
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3', None),
|
||||
}
|
||||
|
||||
# -- Options for autodoc ----------------------------------------------------
|
||||
autodoc_mock_imports = ['esp_err', 'esp_log', 'lvgl', 'freetype']
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_config_value('pdf_file', pdf_file, 'html')
|
||||
app.add_config_value('idf_target_title_dict', idf_target_title_dict, 'html')
|
||||
|
||||
@ -0,0 +1,338 @@
|
||||
Examples
|
||||
========
|
||||
|
||||
This section provides comprehensive examples demonstrating various features of ESP Emote GFX.
|
||||
|
||||
Initialization (core + display + optional touch)
|
||||
------------------------------------------------
|
||||
|
||||
Initialize the graphics core, add a display with flush callback, and optionally add touch. Widgets are created on the display (``gfx_disp_t *disp``).
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "gfx_app";
|
||||
static gfx_handle_t gfx_handle = NULL;
|
||||
static gfx_disp_t *gfx_disp = NULL;
|
||||
|
||||
static void disp_flush_callback(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data)
|
||||
{
|
||||
esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)gfx_disp_get_user_data(disp);
|
||||
esp_lcd_panel_draw_bitmap(panel, x1, y1, x2, y2, data);
|
||||
}
|
||||
|
||||
static void touch_event_cb(gfx_touch_t *touch, const gfx_touch_event_t *event, void *user_data)
|
||||
{
|
||||
ESP_LOGD(TAG, "touch type %d at (%d, %d)", event->type, event->x, event->y);
|
||||
}
|
||||
|
||||
esp_err_t init_gfx(esp_lcd_panel_handle_t panel_handle, esp_lcd_touch_handle_t touch_handle)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
gfx_core_config_t gfx_cfg = {
|
||||
.fps = 30,
|
||||
.task = GFX_EMOTE_INIT_CONFIG(),
|
||||
};
|
||||
gfx_handle = gfx_emote_init(&gfx_cfg);
|
||||
ESP_GOTO_ON_FALSE(gfx_handle != NULL, ESP_FAIL, err_out, TAG, "Failed to init GFX");
|
||||
|
||||
gfx_disp_config_t disp_cfg = {
|
||||
.h_res = 320,
|
||||
.v_res = 240,
|
||||
.flush_cb = disp_flush_callback,
|
||||
.update_cb = NULL,
|
||||
.user_data = (void *)panel_handle,
|
||||
.flags = { .swap = true },
|
||||
.buffers = { .buf1 = NULL, .buf2 = NULL, .buf_pixels = 320 * 16 },
|
||||
};
|
||||
gfx_disp = gfx_disp_add(gfx_handle, &disp_cfg);
|
||||
ESP_GOTO_ON_FALSE(gfx_disp != NULL, ESP_FAIL, err_gfx, TAG, "Failed to add display");
|
||||
|
||||
if (touch_handle) {
|
||||
gfx_touch_config_t touch_cfg = {
|
||||
.handle = touch_handle,
|
||||
.event_cb = touch_event_cb,
|
||||
.disp = gfx_disp,
|
||||
.poll_ms = 50,
|
||||
.user_data = NULL,
|
||||
};
|
||||
if (gfx_touch_add(gfx_handle, &touch_cfg) == NULL) {
|
||||
ESP_LOGW(TAG, "Touch add failed");
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
err_gfx:
|
||||
gfx_emote_deinit(gfx_handle);
|
||||
gfx_handle = NULL;
|
||||
err_out:
|
||||
return ret;
|
||||
}
|
||||
|
||||
Basic Examples
|
||||
--------------
|
||||
|
||||
Simple Label
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Create and display a simple text label on a display (``disp`` from ``gfx_disp_add``):
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
void setup_label(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_obj_t *label = gfx_label_create(disp);
|
||||
gfx_label_set_text(label, "Hello, World!");
|
||||
gfx_obj_set_pos(label, 50, 50);
|
||||
gfx_label_set_color(label, GFX_COLOR_HEX(0xFF0000));
|
||||
gfx_disp_refresh_all(disp);
|
||||
}
|
||||
|
||||
Image Display
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Display an image:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
void setup_image(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_obj_t *img = gfx_img_create(disp);
|
||||
extern const gfx_image_dsc_t my_image;
|
||||
gfx_img_set_src(img, (void *)&my_image);
|
||||
gfx_obj_align(img, GFX_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
|
||||
Advanced Examples
|
||||
-----------------
|
||||
|
||||
Multiple Widgets
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Create and manage multiple widgets on the same display:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
void setup_widgets(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_obj_t *label = gfx_label_create(disp);
|
||||
gfx_label_set_text(label, "Status: OK");
|
||||
gfx_obj_set_pos(label, 10, 10);
|
||||
|
||||
gfx_obj_t *img = gfx_img_create(disp);
|
||||
extern const gfx_image_dsc_t icon;
|
||||
gfx_img_set_src(img, (void *)&icon);
|
||||
gfx_obj_set_pos(img, 10, 50);
|
||||
|
||||
gfx_obj_t *anim = gfx_anim_create(disp);
|
||||
gfx_anim_set_src(anim, anim_data, anim_size);
|
||||
gfx_obj_set_size(anim, 100, 100);
|
||||
gfx_obj_set_pos(anim, 150, 50);
|
||||
gfx_anim_set_segment(anim, 0, 10, 30, true);
|
||||
gfx_anim_start(anim);
|
||||
}
|
||||
|
||||
Touch and object callback (e.g. drag)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Register a per-object touch callback so the object receives PRESS/MOVE/RELEASE (e.g. for dragging):
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
static int32_t drag_off_x, drag_off_y;
|
||||
|
||||
static void obj_touch_cb(gfx_obj_t *obj, const gfx_touch_event_t *event, void *user_data)
|
||||
{
|
||||
gfx_coord_t ox, oy;
|
||||
gfx_obj_get_pos(obj, &ox, &oy);
|
||||
if (event->type == GFX_TOUCH_EVENT_PRESS) {
|
||||
drag_off_x = (int32_t)event->x - ox;
|
||||
drag_off_y = (int32_t)event->y - oy;
|
||||
}
|
||||
if (event->type == GFX_TOUCH_EVENT_MOVE) {
|
||||
gfx_obj_set_pos(obj, (int32_t)event->x - drag_off_x, (int32_t)event->y - drag_off_y);
|
||||
}
|
||||
}
|
||||
|
||||
void make_draggable_label(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_obj_t *label = gfx_label_create(disp);
|
||||
gfx_label_set_text(label, "Drag me");
|
||||
gfx_obj_set_pos(label, 50, 50);
|
||||
gfx_obj_set_touch_cb(label, obj_touch_cb, NULL);
|
||||
}
|
||||
|
||||
Text Scrolling
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Create a scrolling text label (see widget API for ``gfx_label_set_long_mode``, ``gfx_label_set_scroll_speed``, etc.):
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
void setup_scroll_label(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_obj_t *label = gfx_label_create(disp);
|
||||
gfx_label_set_text(label, "This is a very long text that will scroll horizontally");
|
||||
gfx_obj_set_size(label, 200, 30);
|
||||
gfx_obj_set_pos(label, 10, 100);
|
||||
gfx_label_set_long_mode(label, GFX_LABEL_LONG_SCROLL);
|
||||
gfx_label_set_scroll_speed(label, 30);
|
||||
gfx_label_set_scroll_loop(label, true);
|
||||
}
|
||||
|
||||
Timer-Based Updates
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use the graphics timer to update widgets periodically. Timers are created on the **handle**:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
static gfx_obj_t *label = NULL;
|
||||
static int counter = 0;
|
||||
|
||||
static void timer_callback(void *user_data)
|
||||
{
|
||||
gfx_handle_t handle = (gfx_handle_t)user_data;
|
||||
gfx_emote_lock(handle);
|
||||
if (label) {
|
||||
gfx_label_set_text_fmt(label, "Counter: %d", counter++);
|
||||
}
|
||||
gfx_emote_unlock(handle);
|
||||
}
|
||||
|
||||
void setup_timer_label(gfx_handle_t handle, gfx_disp_t *disp)
|
||||
{
|
||||
label = gfx_label_create(disp);
|
||||
gfx_obj_set_pos(label, 50, 50);
|
||||
gfx_timer_create(handle, timer_callback, 1000, handle);
|
||||
}
|
||||
|
||||
QR Code Generation
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Generate and display a QR code:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
void setup_qrcode(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_obj_t *qrcode = gfx_qrcode_create(disp);
|
||||
gfx_qrcode_set_data(qrcode, "https://www.espressif.com");
|
||||
gfx_qrcode_set_size(qrcode, 200);
|
||||
gfx_qrcode_set_ecc(qrcode, GFX_QRCODE_ECC_MEDIUM);
|
||||
gfx_obj_align(qrcode, GFX_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
|
||||
Motion Scene Playback
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Load a generated scene asset and start an action:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
#include "rig_active.inc"
|
||||
|
||||
static gfx_motion_player_t motion_player;
|
||||
|
||||
void setup_motion_scene(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_motion_player_init(&motion_player, disp, &s_motion_scene_asset);
|
||||
gfx_motion_player_set_canvas(&motion_player, 0, 0, 360, 360);
|
||||
gfx_motion_player_set_action(&motion_player, 0, true);
|
||||
}
|
||||
|
||||
For a complete interactive example with touch-driven movement and action switching, see ``test_apps/main/test_motion.c``.
|
||||
|
||||
Thread-Safe Operations
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When modifying widgets from another task, always use the graphics lock (on the **handle**):
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
void update_widgets_from_task(gfx_handle_t handle)
|
||||
{
|
||||
gfx_emote_lock(handle);
|
||||
gfx_label_set_text(label, "Updated from task");
|
||||
gfx_obj_set_pos(img, new_x, new_y);
|
||||
gfx_emote_unlock(handle);
|
||||
}
|
||||
|
||||
Complete Application Example
|
||||
-----------------------------
|
||||
|
||||
Initialization (core + one display), then create a label and refresh:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "gfx_app";
|
||||
static gfx_handle_t gfx_handle = NULL;
|
||||
static gfx_disp_t *gfx_disp = NULL;
|
||||
|
||||
static void disp_flush_callback(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data)
|
||||
{
|
||||
esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)gfx_disp_get_user_data(disp);
|
||||
esp_lcd_panel_draw_bitmap(panel, x1, y1, x2, y2, data);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
gfx_core_config_t gfx_cfg = {
|
||||
.fps = 30,
|
||||
.task = GFX_EMOTE_INIT_CONFIG(),
|
||||
};
|
||||
gfx_handle = gfx_emote_init(&gfx_cfg);
|
||||
if (gfx_handle == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to initialize GFX");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_disp_config_t disp_cfg = {
|
||||
.h_res = 320,
|
||||
.v_res = 240,
|
||||
.flush_cb = disp_flush_callback,
|
||||
.update_cb = NULL,
|
||||
.user_data = panel_handle, // your esp_lcd_panel_handle_t
|
||||
.flags = { .swap = true },
|
||||
.buffers = { .buf1 = NULL, .buf2 = NULL, .buf_pixels = 320 * 16 },
|
||||
};
|
||||
gfx_disp = gfx_disp_add(gfx_handle, &disp_cfg);
|
||||
if (gfx_disp == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to add display");
|
||||
gfx_emote_deinit(gfx_handle);
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_obj_t *title = gfx_label_create(gfx_disp);
|
||||
gfx_label_set_text(title, "ESP Emote GFX");
|
||||
gfx_obj_align(title, GFX_ALIGN_TOP_MID, 0, 10);
|
||||
gfx_label_set_color(title, GFX_COLOR_HEX(0x0000FF));
|
||||
|
||||
gfx_disp_refresh_all(gfx_disp);
|
||||
ESP_LOGI(TAG, "GFX application started");
|
||||
}
|
||||
|
||||
For more examples, see the test applications in the ``test_apps/`` directory.
|
||||
@ -0,0 +1,57 @@
|
||||
ESP Emote GFX Documentation
|
||||
===========================
|
||||
|
||||
Welcome to the ESP Emote GFX API documentation. This is a lightweight graphics framework for ESP-IDF with support for images, labels, animations, buttons, QR codes, fonts, and path-driven motion scenes.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
overview
|
||||
quickstart
|
||||
motion_widget
|
||||
api/core/index
|
||||
api/widgets/index
|
||||
examples
|
||||
changelog
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
ESP Emote GFX is a graphics framework designed for embedded systems, providing:
|
||||
|
||||
* **Images**: Display images in RGB565A8 format with alpha transparency
|
||||
* **Animations**: GIF animations with ESP32 tools (EAF format)
|
||||
* **Buttons**: Interactive button widgets with text, border, and pressed-state styling
|
||||
* **Motion Scenes**: Path-based articulated widgets using joints, poses, actions, and mesh segments
|
||||
* **Fonts**: LVGL fonts and FreeType TTF/OTF support
|
||||
* **Timers**: Built-in timing system for smooth animations
|
||||
* **Memory Optimized**: Designed for embedded systems with limited resources
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Lightweight and memory-efficient
|
||||
* Thread-safe operations with mutex locking
|
||||
* Support for multiple object types (images, labels, animations, buttons, QR codes, motion scenes)
|
||||
* Flexible buffer management (internal or external buffers)
|
||||
* Rich text rendering with scrolling and wrapping
|
||||
* Animation playback control with segments and loops
|
||||
* Path-driven character playback with touch-friendly scene runtime
|
||||
|
||||
Quick Links
|
||||
-----------
|
||||
|
||||
* :doc:`Quick Start Guide <quickstart>`
|
||||
* :doc:`Motion Widget Guide <motion_widget>`
|
||||
* :doc:`Core API Reference <api/core/index>`
|
||||
* :doc:`Widget API Reference <api/widgets/index>`
|
||||
* :doc:`Examples <examples>`
|
||||
* `Doxygen API Reference <../doxygen/index.html>`_ - Auto-generated C/C++ API documentation
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
@ -0,0 +1,280 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Chinese (zh_CN) msgstr for Sphinx gettext catalogs. Keys must match .pot msgid exactly."""
|
||||
|
||||
# api: leave untranslated (empty dict) — HTML shows English for reference pages.
|
||||
TRANSLATIONS_API: dict[str, str] = {}
|
||||
|
||||
TRANSLATIONS_INDEX: dict[str, str] = {
|
||||
"Contents:": "目录:",
|
||||
"ESP Emote GFX Documentation": "ESP Emote GFX 文档",
|
||||
"Welcome to the ESP Emote GFX API documentation. This is a lightweight graphics framework for ESP-IDF with support for images, labels, animations, buttons, QR codes, and fonts.":
|
||||
"欢迎使用 ESP Emote GFX API 文档。这是一个面向 ESP-IDF 的轻量级图形框架,支持图像、标签、动画、按钮、二维码与字体。",
|
||||
"Overview": "概览",
|
||||
"ESP Emote GFX is a graphics framework designed for embedded systems, providing:":
|
||||
"ESP Emote GFX 面向嵌入式系统,提供以下能力:",
|
||||
"**Images**: Display images in RGB565A8 format with alpha transparency":
|
||||
"**图像**:以 RGB565A8 格式显示图像并支持 Alpha 透明",
|
||||
"**Animations**: GIF animations with ESP32 tools (EAF format)":
|
||||
"**动画**:配合 ESP32 工具使用 EAF 等格式",
|
||||
"**Buttons**: Interactive button widgets with text, border, and pressed-state styling":
|
||||
"**按钮**:可交互按钮组件,支持文本、边框与按下态样式",
|
||||
"**Fonts**: LVGL fonts and FreeType TTF/OTF support":
|
||||
"**字体**:LVGL 字体与 FreeType TTF/OTF",
|
||||
"**Timers**: Built-in timing system for smooth animations":
|
||||
"**定时器**:内置时序系统,用于平滑动画",
|
||||
"**Memory Optimized**: Designed for embedded systems with limited resources":
|
||||
"**内存优化**:面向资源受限的嵌入式场景",
|
||||
"Features": "特性",
|
||||
"Lightweight and memory-efficient": "轻量且节省内存",
|
||||
"Thread-safe operations with mutex locking": "通过互斥锁实现线程安全操作",
|
||||
"Support for multiple object types (images, labels, animations, buttons, QR codes)":
|
||||
"支持多种对象类型(图像、标签、动画、按钮、二维码)",
|
||||
"Flexible buffer management (internal or external buffers)": "灵活的缓冲区管理(内部或外部)",
|
||||
"Rich text rendering with scrolling and wrapping": "富文本渲染,支持滚动与换行",
|
||||
"Animation playback control with segments and loops": "动画分段与循环播放控制",
|
||||
"Quick Links": "快速链接",
|
||||
":doc:`Quick Start Guide <quickstart>`": ":doc:`快速入门 <quickstart>`",
|
||||
":doc:`Core API Reference <api/core/index>`": ":doc:`Core API 参考 <api/core/index>`",
|
||||
":doc:`Widget API Reference <api/widgets/index>`": ":doc:`Widget API 参考 <api/widgets/index>`",
|
||||
":doc:`Examples <examples>`": ":doc:`示例 <examples>`",
|
||||
"`Doxygen API Reference <../doxygen/index.html>`_ - Auto-generated C/C++ API documentation":
|
||||
"`Doxygen API 参考 <../doxygen/index.html>`_ — 自动生成的 C/C++ API 文档",
|
||||
"Indices and tables": "索引与表格",
|
||||
":ref:`genindex`": ":ref:`genindex`",
|
||||
":ref:`modindex`": ":ref:`modindex`",
|
||||
":ref:`search`": ":ref:`search`",
|
||||
}
|
||||
|
||||
TRANSLATIONS_QUICKSTART: dict[str, str] = {
|
||||
"Quick Start Guide": "快速入门",
|
||||
"This guide will help you get started with ESP Emote GFX in just a few steps.":
|
||||
"本指南帮助你在几个步骤内上手 ESP Emote GFX。",
|
||||
"Installation": "安装",
|
||||
"Add ESP Emote GFX to your ESP-IDF project by including it as a component. The component is available through the ESP Component Registry.":
|
||||
"将 ESP Emote GFX 作为组件加入 ESP-IDF 工程;组件可在 ESP 组件注册表中获取。",
|
||||
"Basic Setup": "基础设置",
|
||||
"Include the main header:": "包含主头文件:",
|
||||
"Initialize the graphics core (no display yet):": "初始化图形核心(尚未添加显示):",
|
||||
"Add a display with a flush callback:": "添加显示设备并设置 flush 回调:",
|
||||
"(Optional) Register panel IO callback so the framework knows when flush is done:":
|
||||
"(可选)注册 panel IO 回调,以便框架获知 flush 完成:",
|
||||
"(Optional) Add touch input:": "(可选)添加触摸输入:",
|
||||
"Creating Your First Widget": "创建第一个组件",
|
||||
"Widgets are created on a **display** (``gfx_disp_t *``), not on the handle.":
|
||||
"组件创建在 display(``gfx_disp_t *``)上,而不是在 handle 上。",
|
||||
"Creating a Label": "创建标签",
|
||||
"Creating an Image": "创建图像",
|
||||
"Creating an Animation": "创建动画",
|
||||
"Object touch callback (e.g. drag)": "对象触摸回调(例如拖拽)",
|
||||
"Thread Safety": "线程安全",
|
||||
"When modifying objects from outside the graphics task, use the graphics lock:":
|
||||
"在图形任务之外修改对象时,请使用图形锁:",
|
||||
"Complete Example": "完整示例",
|
||||
"Next Steps": "下一步",
|
||||
"Read the :doc:`Core API Reference <api/core/index>` for detailed API documentation":
|
||||
"阅读 :doc:`Core API 参考 <api/core/index>` 获取完整 API 说明",
|
||||
"Check out the :doc:`Widget API Reference <api/widgets/index>` for widget-specific functions":
|
||||
"查看 :doc:`Widget API 参考 <api/widgets/index>` 了解各组件接口",
|
||||
"See :doc:`Examples <examples>` for more complex usage patterns":
|
||||
"参阅 :doc:`示例 <examples>` 获取更多用法",
|
||||
}
|
||||
|
||||
TRANSLATIONS_EXAMPLES: dict[str, str] = {
|
||||
"Examples": "示例",
|
||||
"This section provides comprehensive examples demonstrating various features of ESP Emote GFX.":
|
||||
"本节提供 ESP Emote GFX 各项能力的示例代码。",
|
||||
"Initialization (core + display + optional touch)": "初始化(核心 + 显示 + 可选触摸)",
|
||||
"Initialize the graphics core, add a display with flush callback, and optionally add touch. Widgets are created on the display (``gfx_disp_t *disp``).":
|
||||
"初始化图形核心,添加带 flush 回调的显示设备,并可选择添加触摸。组件创建在显示(``gfx_disp_t *disp``)上。",
|
||||
"Basic Examples": "基础示例",
|
||||
"Simple Label": "简单标签",
|
||||
"Create and display a simple text label on a display (``disp`` from ``gfx_disp_add``):":
|
||||
"在显示上创建并显示简单文本标签(``disp`` 来自 ``gfx_disp_add``):",
|
||||
"Image Display": "图像显示",
|
||||
"Display an image:": "显示图像:",
|
||||
"Advanced Examples": "进阶示例",
|
||||
"Multiple Widgets": "多组件",
|
||||
"Create and manage multiple widgets on the same display:":
|
||||
"在同一显示上创建并管理多个组件:",
|
||||
"Touch and object callback (e.g. drag)": "触摸与对象回调(例如拖拽)",
|
||||
"Register a per-object touch callback so the object receives PRESS/MOVE/RELEASE (e.g. for dragging):":
|
||||
"注册逐对象的触摸回调,使对象接收 PRESS/MOVE/RELEASE(用于拖拽等):",
|
||||
"Text Scrolling": "文本滚动",
|
||||
"Create a scrolling text label (see widget API for ``gfx_label_set_long_mode``, ``gfx_label_set_scroll_speed``, etc.):":
|
||||
"创建可滚动文本标签(详见 Widget API 中的 ``gfx_label_set_long_mode``、``gfx_label_set_scroll_speed`` 等):",
|
||||
"Timer-Based Updates": "基于定时器的更新",
|
||||
"Use the graphics timer to update widgets periodically. Timers are created on the **handle**:":
|
||||
"使用图形定时器周期性更新组件。定时器创建在 **handle** 上:",
|
||||
"QR Code Generation": "二维码生成",
|
||||
"Generate and display a QR code:": "生成并显示二维码:",
|
||||
"Thread-Safe Operations": "线程安全操作",
|
||||
"When modifying widgets from another task, always use the graphics lock (on the **handle**):":
|
||||
"从其他任务修改组件时,务必使用图形锁(在 **handle** 上):",
|
||||
"Complete Application Example": "完整应用示例",
|
||||
"Initialization (core + one display), then create a label and refresh:":
|
||||
"初始化(核心 + 单个显示),再创建标签并刷新:",
|
||||
"For more examples, see the test applications in the ``test_apps/`` directory.":
|
||||
"更多示例见 ``test_apps/`` 目录中的测试应用。",
|
||||
}
|
||||
|
||||
TRANSLATIONS_OVERVIEW: dict[str, str] = {
|
||||
"Overview": "概览",
|
||||
"ESP Emote GFX is a lightweight graphics framework for ESP-IDF that provides a simple yet powerful API for rendering graphics on embedded displays. It is designed with memory efficiency and performance in mind, making it ideal for resource-constrained embedded systems.":
|
||||
"ESP Emote GFX 是面向 ESP-IDF 的轻量级图形框架,提供简洁而强大的嵌入式显示渲染 API,在内存与性能之间取得平衡,适合资源受限场景。",
|
||||
"Architecture": "架构",
|
||||
"The framework is built around a core object system where all graphical elements (images, labels, animations, buttons, QR codes) are treated as objects. These objects share common properties like position, size, visibility, and alignment.":
|
||||
"框架以统一对象系统为核心:图像、标签、动画、按钮、二维码等元素均为对象,共享位置、尺寸、可见性与对齐等属性。",
|
||||
"Core Components": "核心组件",
|
||||
"Core System": "核心系统",
|
||||
"The core system (`gfx_core`) manages:": "核心系统(`gfx_core`)负责:",
|
||||
"Graphics context initialization and deinitialization": "图形上下文的初始化与反初始化",
|
||||
"Buffer management (internal or external)": "缓冲区管理(内部或外部)",
|
||||
"Rendering pipeline": "渲染管线",
|
||||
"Thread safety with mutex locking": "基于互斥锁的线程安全",
|
||||
"Screen refresh and invalidation": "屏幕刷新与脏区失效",
|
||||
"Object System": "对象系统",
|
||||
"The object system (`gfx_obj`) provides:": "对象系统(`gfx_obj`)提供:",
|
||||
"Base object structure for all graphical elements": "所有图形元素的基类结构",
|
||||
"Position and size management": "位置与尺寸管理",
|
||||
"Alignment system (similar to LVGL)": "对齐系统(类似 LVGL)",
|
||||
"Visibility control": "可见性控制",
|
||||
"Object lifecycle management": "对象生命周期管理",
|
||||
"Timer System": "定时器系统",
|
||||
"The timer system (`gfx_timer`) provides:": "定时器系统(`gfx_timer`)提供:",
|
||||
"High-resolution timers for animations": "用于动画的高分辨率定时器",
|
||||
"Callback-based timer events": "基于回调的定时事件",
|
||||
"Repeat count and period control": "重复次数与周期控制",
|
||||
"System tick management": "系统 tick 管理",
|
||||
"Widgets": "组件",
|
||||
"Image Widget": "图像组件",
|
||||
"The image widget supports:": "图像组件支持:",
|
||||
"RGB565 format (16-bit color)": "RGB565(16 位色)",
|
||||
"RGB565A8 format (16-bit color with 8-bit alpha)": "RGB565A8(16 位色 + 8 位 Alpha)",
|
||||
"C array and binary formats": "C 数组与二进制格式",
|
||||
"Automatic format detection": "自动识别格式",
|
||||
"Label Widget": "标签组件",
|
||||
"The label widget provides:": "标签组件提供:",
|
||||
"Text rendering with multiple font formats": "多种字体格式的文本渲染",
|
||||
"LVGL font support": "LVGL 字体支持",
|
||||
"FreeType TTF/OTF font support": "FreeType TTF/OTF 支持",
|
||||
"Text alignment (left, center, right)": "文本对齐(左、中、右)",
|
||||
"Long text handling (wrap, scroll, clip)": "长文本处理(换行、滚动、裁剪)",
|
||||
"Background colors and opacity": "背景色与透明度",
|
||||
"Button Widget": "按钮组件",
|
||||
"The button widget provides:": "按钮组件提供:",
|
||||
"Text label management": "文本标签管理",
|
||||
"Normal and pressed background colors": "常态与按下背景色",
|
||||
"Border color and width configuration": "边框颜色与宽度",
|
||||
"Font and text alignment control": "字体与文本对齐控制",
|
||||
"Animation Widget": "动画组件",
|
||||
"The animation widget supports:": "动画组件支持:",
|
||||
"EAF (ESP Animation Format) files": "EAF(ESP 动画格式)文件",
|
||||
"Frame-by-frame playback control": "逐帧播放控制",
|
||||
"Segment playback (start/end frames)": "分段播放(起止帧)",
|
||||
"FPS control": "帧率控制",
|
||||
"Loop and repeat options": "循环与重复选项",
|
||||
"Mirror effects": "镜像效果",
|
||||
"QR Code Widget": "二维码组件",
|
||||
"The QR code widget provides:": "二维码组件提供:",
|
||||
"Dynamic QR code generation": "动态生成二维码",
|
||||
"Configurable size and error correction": "可配置尺寸与纠错等级",
|
||||
"Custom foreground and background colors": "自定义前景与背景色",
|
||||
"Memory Management": "内存管理",
|
||||
"The framework supports two buffer management modes:": "框架支持两种缓冲区管理模式:",
|
||||
"Internal Buffers": "内部缓冲区",
|
||||
"The framework automatically allocates and manages frame buffers internally. This is the simplest mode but requires sufficient heap memory.":
|
||||
"由框架在内部分配并管理帧缓冲。最简单,但需要足够的堆内存。",
|
||||
"External Buffers": "外部缓冲区",
|
||||
"You can provide your own buffers, allowing you to:": "你可自行提供缓冲区,从而可以:",
|
||||
"Use memory-mapped regions": "使用内存映射区域",
|
||||
"Control buffer placement (SRAM, SPIRAM, etc.)": "控制缓冲区位置(SRAM、SPIRAM 等)",
|
||||
"Optimize for specific memory constraints": "针对内存约束优化",
|
||||
"Thread Safety": "线程安全",
|
||||
"All widget operations should be performed within a graphics lock to ensure thread safety:":
|
||||
"所有组件操作应在图形锁内执行以保证线程安全:",
|
||||
"Dependencies": "依赖",
|
||||
"ESP-IDF 5.0 or higher": "ESP-IDF 5.0 或更高版本",
|
||||
"FreeType (for TTF/OTF font support)": "FreeType(TTF/OTF 字体)",
|
||||
"ESP New JPEG (for JPEG decoding)": "ESP New JPEG(JPEG 解码)",
|
||||
"License": "许可证",
|
||||
"This project is licensed under the Apache License 2.0.": "本项目采用 Apache License 2.0 许可证。",
|
||||
}
|
||||
|
||||
TRANSLATIONS_CHANGELOG: dict[str, str] = {
|
||||
"Changelog": "更新日志",
|
||||
"All notable changes to the ESP Emote GFX component will be documented in this file.":
|
||||
"ESP Emote GFX 组件的重要变更将记录于此。",
|
||||
"[3.0.3] - 2026-04-20": "[3.0.3] - 2026-04-20",
|
||||
"Add `gfx_button` widget (text, font, normal/pressed colors, border)":
|
||||
"新增 `gfx_button` 组件(文本、字体、常态/按下背景色、边框)",
|
||||
"Add `gfx_log` API for log level configuration": "新增 `gfx_log` API,用于配置日志级别",
|
||||
"Documentation: separate English and Simplified Chinese HTML builds (gettext), language switcher, unified `postprocess_docs.sh` pipeline (API RST, Sphinx, Doxygen)":
|
||||
"文档:英文与简体中文独立 HTML 构建(gettext)、页顶语言切换、统一 `postprocess_docs.sh` 流程(API RST、Sphinx、Doxygen)",
|
||||
"Simplify GitHub Actions documentation job to a single build step":
|
||||
"精简 GitHub Actions 文档构建为单一步骤",
|
||||
"[3.0.2] - 2026-04-17": "[3.0.2] - 2026-04-17",
|
||||
"Update version of esp_new_jpeg": "更新 esp_new_jpeg 版本",
|
||||
"[3.0.1] - 2026-02-13": "[3.0.1] - 2026-02-13",
|
||||
"Add CI build action for P4": "为 P4 添加 CI 构建",
|
||||
"Optimize multi-buffer switching logic": "优化多缓冲切换逻辑",
|
||||
"Fix crash when text is NULL": "修复 text 为 NULL 时的崩溃",
|
||||
"Fix missing API documentation (e.g. gfx_touch_add)": "补全缺失的 API 文档(如 gfx_touch_add)",
|
||||
"[3.0.0] - 2026-01-22": "[3.0.0] - 2026-01-22",
|
||||
"Add documentation build action": "添加文档构建 Action",
|
||||
"Optimize EAF 8-bit render": "优化 EAF 8 位渲染",
|
||||
"Fix FreeType parsing performance": "修复 FreeType 解析性能",
|
||||
"Remove duplicated label-related APIs": "移除重复的标签相关 API",
|
||||
"[2.1.0] - 2026-01-28": "[2.1.0] - 2026-01-28",
|
||||
"Support for decoding Heatshrink-compressed image slices": "支持解码 Heatshrink 压缩的图像条带",
|
||||
"[2.0.4] - 2026-01-22": "[2.0.4] - 2026-01-22",
|
||||
"Fix Huffman+RLE decoding buffer sizing to prevent oversized output errors (Issue `#18 <https://github.com/espressif2022/esp_emote_gfx/issues/18>`_)":
|
||||
"修复 Huffman+RLE 解码缓冲区尺寸,避免输出过大错误(Issue `#18 <https://github.com/espressif2022/esp_emote_gfx/issues/18>`_)",
|
||||
"[2.0.3] - 2026-01-08": "[2.0.3] - 2026-01-08",
|
||||
"Delete local assets": "删除本地资源",
|
||||
"Build acion for ['release-v5.2', 'release-v5.3', 'release-v5.4', 'release-v5.5']":
|
||||
"为 ['release-v5.2', 'release-v5.3', 'release-v5.4', 'release-v5.5'] 构建 Action",
|
||||
"Fix ESP-IDF version compatibility issues": "修复 ESP-IDF 版本兼容问题",
|
||||
"Change flush_callback timeout from 20 ms to wait forever": "将 flush_callback 超时从 20 ms 改为无限等待",
|
||||
"[2.0.2] - 2025-12-26": "[2.0.2] - 2025-12-26",
|
||||
"Add optional JPEG decoding support for EAF animations": "为 EAF 动画增加可选 JPEG 解码",
|
||||
"Center QR code rendering in UI layout": "在界面布局中居中渲染二维码",
|
||||
"Add alpha channel support for animations": "为动画增加 Alpha 通道支持",
|
||||
"[2.0.1] - 2025-12-05": "[2.0.1] - 2025-12-05",
|
||||
"Add Touch event": "增加触摸事件",
|
||||
"[2.0.0] - 2025-12-01": "[2.0.0] - 2025-12-01",
|
||||
"Added partial refresh mode support": "增加局部刷新模式",
|
||||
"Added QR code widget (gfx_qrcode)": "增加二维码组件 (gfx_qrcode)",
|
||||
"[1.2.0] - 2025-09-0": "[1.2.0] - 2025-09-0",
|
||||
"use eaf as a lib": "将 eaf 作为库使用",
|
||||
"[1.1.2] - 2025-09-29": "[1.1.2] - 2025-09-29",
|
||||
"Upgrade dependencies": "升级依赖",
|
||||
"Update `espressif/esp_new_jpeg` to 0.6.x by @Kevincoooool. `#8 <https://github.com/espressif2022/esp_emote_gfx/pull/8>`_":
|
||||
"将 `espressif/esp_new_jpeg` 升级到 0.6.x(@Kevincoooool)。`#8 <https://github.com/espressif2022/esp_emote_gfx/pull/8>`_",
|
||||
"[1.1.1] - 2025-09-23": "[1.1.1] - 2025-09-23",
|
||||
"Fixed": "修复",
|
||||
"Resolve image block decoding failure in specific cases. `#6 <https://github.com/espressif2022/esp_emote_gfx/issues/6>`_":
|
||||
"解决特定场景下图块解码失败。`#6 <https://github.com/espressif2022/esp_emote_gfx/issues/6>`_",
|
||||
"[1.0.0] - 2025-08-01": "[1.0.0] - 2025-08-01",
|
||||
"Added": "新增",
|
||||
"Initial release of ESP Emote GFX framework": "ESP Emote GFX 框架首次发布",
|
||||
"Core graphics rendering engine": "核心图形渲染引擎",
|
||||
"Object system for images and labels": "图像与标签对象系统",
|
||||
"Basic drawing functions and color utilities": "基础绘制与颜色工具",
|
||||
"Software blending capabilities": "软件混合能力",
|
||||
"Timer system for animations": "用于动画的定时器系统",
|
||||
"Support for ESP-IDF 5.0+": "支持 ESP-IDF 5.0+",
|
||||
"FreeType font rendering integration": "集成 FreeType 字体渲染",
|
||||
"JPEG image decoding support": "JPEG 图像解码支持",
|
||||
"Features": "特性",
|
||||
"Lightweight graphics framework optimized for embedded systems": "面向嵌入式系统的轻量图形框架",
|
||||
"Memory-efficient design for resource-constrained environments": "面向资源受限环境的省内存设计",
|
||||
}
|
||||
|
||||
TRANSLATIONS_BY_CATALOG: dict[str, dict[str, str]] = {
|
||||
"index": TRANSLATIONS_INDEX,
|
||||
"overview": TRANSLATIONS_OVERVIEW,
|
||||
"quickstart": TRANSLATIONS_QUICKSTART,
|
||||
"examples": TRANSLATIONS_EXAMPLES,
|
||||
"changelog": TRANSLATIONS_CHANGELOG,
|
||||
"api": TRANSLATIONS_API,
|
||||
}
|
||||
@ -0,0 +1,386 @@
|
||||
Motion, Mesh Image, and Rendering Architecture
|
||||
==============================================
|
||||
|
||||
Purpose
|
||||
-------
|
||||
|
||||
This document explains how the motion scene stack is split today and where
|
||||
future optimization work should happen. It focuses on these files:
|
||||
|
||||
* ``src/widget/motion/gfx_motion_scene.c``
|
||||
* ``src/widget/motion/gfx_motion_player.c``
|
||||
* ``src/widget/motion/gfx_motion_primitives.c``
|
||||
* ``src/widget/motion/gfx_motion_style.c``
|
||||
* ``src/widget/img/gfx_mesh_img.c``
|
||||
* ``src/core/draw/gfx_blend.c``
|
||||
|
||||
The key design rule is that scene playback, segment-to-mesh conversion, mesh
|
||||
image drawing, and low-level blending are separate layers. Each layer owns one
|
||||
kind of state and should not reach across the boundary unless it is exposing a
|
||||
small, reusable API.
|
||||
|
||||
High-Level Flow
|
||||
---------------
|
||||
|
||||
The runtime path is:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
gfx_motion_asset_t
|
||||
|
|
||||
v
|
||||
gfx_motion_scene.c
|
||||
validate asset, own action timeline, update pose_cur/pose_tgt
|
||||
|
|
||||
v
|
||||
gfx_motion_player.c
|
||||
own mesh objects, callbacks, canvas mapping, segment dispatch
|
||||
|
|
||||
+--> gfx_motion_primitives.c
|
||||
| generate capsule/ring/bezier mesh geometry
|
||||
|
|
||||
+--> gfx_motion_style.c
|
||||
bind palette/resource/opacity/layer/UV style
|
||||
|
|
||||
v
|
||||
gfx_mesh_img.c
|
||||
own mesh state, source image, UV/rest points, bounds,
|
||||
draw each mesh cell or scanline-filled polygon
|
||||
|
|
||||
v
|
||||
gfx_blend.c
|
||||
triangle rasterization, image sampling, polygon fill, AA, clipping
|
||||
|
|
||||
v
|
||||
display buffer
|
||||
|
||||
Layer Ownership
|
||||
---------------
|
||||
|
||||
``gfx_motion_scene.c`` is the parser and timeline layer.
|
||||
|
||||
It owns:
|
||||
|
||||
* Asset validation.
|
||||
* Pose state: ``pose_cur`` and ``pose_tgt``.
|
||||
* Active action, active step, step ticks, loop override.
|
||||
* Interpolation policy such as ``HOLD`` and ``DAMPED``.
|
||||
* Facing/mirroring when loading a target pose.
|
||||
|
||||
It must not own:
|
||||
|
||||
* Display objects.
|
||||
* Mesh grids.
|
||||
* Pixel colors or image descriptors.
|
||||
* Rendering decisions such as scanline fill or triangle fallback.
|
||||
|
||||
``gfx_motion_player.c`` is the presentation adapter for motion scenes.
|
||||
|
||||
It owns:
|
||||
|
||||
* One ``gfx_mesh_img`` object per segment.
|
||||
* Mapping from design-space scene coordinates into the destination canvas.
|
||||
* Per-segment grid setup and cached grid size.
|
||||
* Motion driver callbacks that connect ``gfx_motion_t`` to the scene and mesh
|
||||
objects.
|
||||
* Dispatching each segment to the primitive and style helpers.
|
||||
|
||||
It must not own:
|
||||
|
||||
* Asset timeline rules beyond calling ``gfx_motion_scene_*``.
|
||||
* Generic mesh drawing.
|
||||
* Low-level triangle rasterization or polygon fill.
|
||||
* Primitive geometry algorithms or style/resource binding details.
|
||||
|
||||
``gfx_motion_primitives.c`` is the motion geometry algorithm layer.
|
||||
|
||||
It owns:
|
||||
|
||||
* Segment tessellation for capsule, ring, Bezier stroke, and Bezier fill.
|
||||
* Primitive-local scratch usage through ``gfx_motion_player_runtime_scratch_t``.
|
||||
* Cubic Bezier position/tangent evaluation.
|
||||
* Stroke extrusion and fill mesh generation.
|
||||
|
||||
It must not own:
|
||||
|
||||
* Action playback.
|
||||
* Display object lifetime.
|
||||
* Palette/resource binding.
|
||||
* Generic mesh drawing internals.
|
||||
|
||||
``gfx_motion_style.c`` is the motion style/resource binding layer.
|
||||
|
||||
It owns:
|
||||
|
||||
* Runtime solid color, palette color, opacity, texture source, UV crop, and
|
||||
layer visibility helpers.
|
||||
* Resource UV mapping into mesh ``rest_points``.
|
||||
* Binding the correct image source for each segment.
|
||||
|
||||
It must not own:
|
||||
|
||||
* Primitive geometry.
|
||||
* Action playback.
|
||||
* Mesh cell rasterization.
|
||||
|
||||
``gfx_mesh_img.c`` is the generic deformable image widget.
|
||||
|
||||
It owns:
|
||||
|
||||
* The current mesh grid size and point count.
|
||||
* ``points``: current object-local mesh geometry.
|
||||
* ``rest_points``: reference UV/sample coordinates for the source image.
|
||||
* Source image descriptor and decoded image header.
|
||||
* Object bounds derived from current points.
|
||||
* Mesh options such as column wrapping, inward AA, opacity, control-point debug
|
||||
drawing, and scanline fill.
|
||||
* Drawing by splitting each mesh cell into triangles, or by using scanline
|
||||
polygon fill for solid filled shapes.
|
||||
|
||||
It must not own:
|
||||
|
||||
* Motion scene semantics.
|
||||
* Segment kinds such as capsule or Bezier.
|
||||
* Action playback or pose interpolation.
|
||||
|
||||
``gfx_blend.c`` is the software raster backend.
|
||||
|
||||
It owns:
|
||||
|
||||
* Image triangle drawing with source UVs.
|
||||
* Polygon fill coverage.
|
||||
* Clipping against buffer and object clip areas.
|
||||
* Alpha blending and RGB565 byte swap handling.
|
||||
* Anti-aliasing policy for primitive edges.
|
||||
* Chunking wide polygon coverage so large filled shapes do not disappear.
|
||||
|
||||
It must not own:
|
||||
|
||||
* Widget state.
|
||||
* Mesh object layout.
|
||||
* Motion-specific assumptions.
|
||||
|
||||
Scene Asset Model
|
||||
-----------------
|
||||
|
||||
``gfx_motion_asset_t`` is the ROM-side bundle consumed by the runtime. It is
|
||||
defined in ``include/widget/gfx_motion_scene.h`` and contains:
|
||||
|
||||
* ``meta``: schema version and design-space viewbox.
|
||||
* ``joint_names`` and ``joint_count``: named control points.
|
||||
* ``segments``: visual primitives referencing joints.
|
||||
* ``poses``: complete joint coordinate snapshots.
|
||||
* ``actions``: step sequences that select target poses.
|
||||
* ``sequence``: optional default playback sequence.
|
||||
* ``layout``: default stroke, mirror axis, timing, and damping hints.
|
||||
* ``resources``: optional texture images with UV crop.
|
||||
* ``color_palette``: optional fixed segment colors.
|
||||
|
||||
The scene layer validates structural invariants early:
|
||||
|
||||
* Viewbox dimensions must be positive.
|
||||
* Joint, pose, action, and sequence pointers must match their counts.
|
||||
* Segment joint ranges must stay within ``joint_count``.
|
||||
* Bezier control counts must satisfy ``3k + 1``.
|
||||
* Resource and palette indices must resolve.
|
||||
* Resource UV crop must fit inside the image descriptor.
|
||||
* Layer bits must be within the 32-bit layer mask range.
|
||||
|
||||
Playback Model
|
||||
--------------
|
||||
|
||||
``gfx_motion_scene_init()`` validates the asset and initializes the first action
|
||||
step. The scene starts with ``pose_cur`` snapped to ``pose_tgt``.
|
||||
|
||||
``gfx_motion_scene_advance()`` advances the action timeline. It updates the
|
||||
active step when ``hold_ticks`` expires, loads the new target pose, and applies
|
||||
the step's interpolation policy.
|
||||
|
||||
``gfx_motion_scene_tick()`` eases ``pose_cur`` toward ``pose_tgt`` for damped
|
||||
steps. The function returns whether coordinates changed. The player uses that
|
||||
signal, plus dirty flags, to decide whether to update mesh objects.
|
||||
|
||||
``GFX_MOTION_INTERP_HOLD`` means snap immediately to the target pose. It is
|
||||
used both on action switch and on step advance.
|
||||
|
||||
Player Segment Pipeline
|
||||
-----------------------
|
||||
|
||||
``gfx_motion_player_init()`` creates one ``gfx_mesh_img`` object per segment.
|
||||
The initial grid is chosen from the segment kind:
|
||||
|
||||
* Capsule: ``1 x 1`` grid, four points.
|
||||
* Ring: ``N x 1`` wrapped grid, two point rows.
|
||||
* Bezier strip: sampled curve columns, non-wrapped grid.
|
||||
* Bezier loop: sampled curve columns, wrapped grid.
|
||||
* Bezier fill: preset eye/ellipse grid or generic closed-loop rim grid.
|
||||
|
||||
On each motion apply callback, the player:
|
||||
|
||||
1. Checks whether the scene or mesh is dirty.
|
||||
2. Converts needed joints from design space into screen space.
|
||||
3. Computes stroke width and radius in screen pixels.
|
||||
4. Applies the matching segment primitive into the mesh object.
|
||||
5. Sets object visibility from the layer mask.
|
||||
6. Clears dirty flags after all visible segments have been updated.
|
||||
|
||||
Primitive conversion details in ``gfx_motion_primitives.c``:
|
||||
|
||||
* Capsule computes a thick rectangle aligned with the segment direction.
|
||||
* Ring computes outer and inner circular point rows and enables wrapped columns.
|
||||
* Bezier stroke evaluates cubic position and analytic tangent, then extrudes
|
||||
left/right normals into two mesh rows.
|
||||
* Bezier fill either uses an eye/ellipse preset path or builds a hub/rim mesh
|
||||
for generic closed loops.
|
||||
|
||||
Styling and Resources
|
||||
---------------------
|
||||
|
||||
``gfx_motion_style.c`` binds image sources in this priority order:
|
||||
|
||||
1. ``resource_idx`` texture image.
|
||||
2. ``color_idx`` palette 1x1 image.
|
||||
3. Runtime solid 1x1 image.
|
||||
|
||||
For texture resources, ``uv_x``, ``uv_y``, ``uv_w``, and ``uv_h`` are mapped
|
||||
into mesh ``rest_points``. The mesh's current ``points`` still describe screen
|
||||
geometry; ``rest_points`` describe where to sample the source image. This keeps
|
||||
UV crop generic and lets the same mesh renderer draw both full-image and
|
||||
cropped-resource segments.
|
||||
|
||||
For palette and runtime solid colors, the source is a 1x1 RGB565 image. Filled
|
||||
Bezier segments may additionally enable scanline fill so solid closed shapes do
|
||||
not need to be rasterized through textured triangles.
|
||||
|
||||
Mesh Image Model
|
||||
----------------
|
||||
|
||||
``gfx_mesh_img`` stores two point arrays:
|
||||
|
||||
* ``points`` are object-local Q8 geometry coordinates.
|
||||
* ``rest_points`` are object-local Q8 source sampling coordinates.
|
||||
|
||||
For a plain image, both arrays start as a regular grid over the image. For
|
||||
motion segments, the player continuously updates ``points`` while ``rest_points``
|
||||
remain the source UV reference. Texture crop updates only ``rest_points``.
|
||||
|
||||
When ``points`` change, ``gfx_mesh_img_update_bounds()`` recalculates the object
|
||||
bounding box. The draw origin is derived from the object position minus the
|
||||
minimum mesh bound. This allows meshes with negative local coordinates while
|
||||
still drawing through the normal object geometry system.
|
||||
|
||||
Important mesh options:
|
||||
|
||||
* ``wrap_cols`` connects the last column back to the first. Rings and closed
|
||||
Bezier loops need this.
|
||||
* ``aa_inward`` makes edge AA fade inward to avoid halos on thin strokes.
|
||||
* ``scanline_fill`` bypasses textured triangle drawing for solid filled
|
||||
polygons when possible.
|
||||
* ``opacity`` applies uniform per-segment alpha.
|
||||
|
||||
Draw Pipeline
|
||||
-------------
|
||||
|
||||
``gfx_mesh_img_draw()`` opens the image decoder, resolves RGB565 or RGB565A8
|
||||
payloads, computes the clipped object area, and chooses one of two paths.
|
||||
|
||||
Scanline fill path:
|
||||
|
||||
* Used for selected solid filled polygons.
|
||||
* Builds a polygon from mesh points.
|
||||
* Calls ``gfx_sw_blend_polygon_fill()``.
|
||||
* Falls back to triangle drawing if the scanline scratch capacity is too small.
|
||||
|
||||
Triangle path:
|
||||
|
||||
* Iterates every mesh cell.
|
||||
* Builds four vertices with screen position and source UV.
|
||||
* Splits the quad into two triangles.
|
||||
* Chooses the shorter diagonal to reduce cracks on deformed quads.
|
||||
* Marks internal edges so AA does not darken shared seams.
|
||||
* Calls ``gfx_sw_blend_img_triangle_draw()`` twice per cell.
|
||||
|
||||
Low-Level Drawing
|
||||
-----------------
|
||||
|
||||
``gfx_sw_blend_img_triangle_draw()`` samples the source image inside a triangle
|
||||
and blends into the destination buffer. It handles:
|
||||
|
||||
* Screen clipping.
|
||||
* Source UV interpolation.
|
||||
* RGB565/RGB565A8 source alpha.
|
||||
* Uniform opacity.
|
||||
* Internal edge suppression.
|
||||
* Optional inward AA.
|
||||
|
||||
``gfx_sw_blend_polygon_fill()`` fills a polygon with a solid color. It clips to
|
||||
the destination buffer, computes per-pixel coverage, and chunks wide polygons
|
||||
across X so coverage scratch memory stays bounded.
|
||||
|
||||
Module Boundary Assessment
|
||||
--------------------------
|
||||
|
||||
The current module split is intentionally layered:
|
||||
|
||||
* ``scene.c`` is pure playback state.
|
||||
* ``player.c`` is motion-scene runtime orchestration.
|
||||
* ``primitives.c`` is motion geometry generation.
|
||||
* ``style.c`` is motion style/resource binding.
|
||||
* ``mesh_img.c`` is reusable deformable image infrastructure.
|
||||
* ``gfx_blend.c`` is low-level rasterization.
|
||||
|
||||
The main area to watch from here is whether primitive APIs stabilize. If more
|
||||
primitive families are added, keep them in ``gfx_motion_primitives.c`` until the
|
||||
file itself becomes too large; only then split by primitive family.
|
||||
|
||||
Optimization Guide
|
||||
------------------
|
||||
|
||||
Useful optimization entry points:
|
||||
|
||||
* Player dirty flags: avoid applying meshes when pose and canvas are unchanged.
|
||||
* Cached segment grids: avoid reallocating mesh points in hot paths.
|
||||
* Bezier sampling density: tune stroke and fill samples separately.
|
||||
* Resource UV updates: avoid recomputing rest points unless grid or resource
|
||||
crop changes.
|
||||
* Mesh bounds: keep clamping warnings visible because excessive bounds can hide
|
||||
real coordinate bugs.
|
||||
* Scanline fill: prefer it for solid large fills; keep triangle fallback for
|
||||
textured or unsupported cases.
|
||||
* Blend chunk width: increase only if stack/static scratch budget allows it.
|
||||
* Layer mask: hide inactive segment groups before tessellation if many layers
|
||||
become common.
|
||||
|
||||
Testing Checklist
|
||||
-----------------
|
||||
|
||||
When changing this stack, test these cases:
|
||||
|
||||
* Empty segment assets still initialize and deinitialize safely.
|
||||
* ``HOLD`` actions snap immediately on init, action switch, and step advance.
|
||||
* Palette segments are not overwritten by ``gfx_motion_player_set_color()``.
|
||||
* Texture segments respect resource UV crop.
|
||||
* Ring grid changes do not lose UV crop.
|
||||
* Layer mask hides and restores segment visibility.
|
||||
* Bezier strokes do not show dashed/bowtie artifacts on tight curves.
|
||||
* Oversized scanline fills fall back to triangle rendering instead of blanking.
|
||||
* Wide polygon fills render in chunks instead of returning early.
|
||||
* Mesh allocation failure preserves the previous grid when possible.
|
||||
* Bounds that exceed geometry range are clamped and logged.
|
||||
|
||||
Change Guidelines
|
||||
-----------------
|
||||
|
||||
Use these rules when iterating:
|
||||
|
||||
* Put timeline or action behavior in ``gfx_motion_scene.c``.
|
||||
* Put segment-to-mesh conversion in ``gfx_motion_primitives.c``.
|
||||
* Put palette/resource/layer/opacity handling in ``gfx_motion_style.c``.
|
||||
* Put generic mesh storage, UV, bounds, and draw dispatch in ``gfx_mesh_img.c``.
|
||||
* Put pixel coverage, sampling, AA, and blend math in ``gfx_blend.c``.
|
||||
* Keep public structs in ``include/widget/gfx_motion_scene.h`` stable where
|
||||
possible because generated assets depend on them.
|
||||
* Validate asset mistakes in the scene layer rather than letting the player or
|
||||
renderer fail later.
|
||||
* Keep renderer fallbacks visible through logs instead of silently drawing
|
||||
nothing.
|
||||
@ -0,0 +1,342 @@
|
||||
Motion、Mesh Image 与绘制架构说明
|
||||
==================================
|
||||
|
||||
文档目标
|
||||
--------
|
||||
|
||||
这份文档用于说明当前 motion scene、mesh image 和底层绘制链路的模块划分,方便后续做性能优化、功能迭代和代码 review。重点覆盖下面几个文件:
|
||||
|
||||
* ``src/widget/motion/gfx_motion_scene.c``
|
||||
* ``src/widget/motion/gfx_motion_player.c``
|
||||
* ``src/widget/motion/gfx_motion_primitives.c``
|
||||
* ``src/widget/motion/gfx_motion_style.c``
|
||||
* ``src/widget/img/gfx_mesh_img.c``
|
||||
* ``src/core/draw/gfx_blend.c``
|
||||
|
||||
当前架构的核心原则是:动作播放、segment 到 mesh 的转换、mesh image 绘制、底层像素混合是四个独立层次。每一层只维护自己负责的状态;如果需要跨层交互,应通过小而稳定的 API 完成,而不是让上层直接依赖下层内部实现。
|
||||
|
||||
整体数据流
|
||||
----------
|
||||
|
||||
运行时链路如下:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
gfx_motion_asset_t
|
||||
|
|
||||
v
|
||||
gfx_motion_scene.c
|
||||
校验 asset,维护 action 时间线,更新 pose_cur/pose_tgt
|
||||
|
|
||||
v
|
||||
gfx_motion_player.c
|
||||
管理 mesh 对象、callback、canvas 映射和 segment dispatch
|
||||
|
|
||||
+--> gfx_motion_primitives.c
|
||||
| 生成 capsule/ring/bezier 的 mesh 几何
|
||||
|
|
||||
+--> gfx_motion_style.c
|
||||
绑定 palette/resource/opacity/layer/UV 样式
|
||||
|
|
||||
v
|
||||
gfx_mesh_img.c
|
||||
维护 mesh 状态、图片源、UV/rest_points、bounds,
|
||||
绘制每个 mesh cell 或 scanline 填充多边形
|
||||
|
|
||||
v
|
||||
gfx_blend.c
|
||||
三角形光栅化、图片采样、多边形填充、AA、裁剪
|
||||
|
|
||||
v
|
||||
display buffer
|
||||
|
||||
模块职责边界
|
||||
------------
|
||||
|
||||
``gfx_motion_scene.c`` 是 parser 和 action timeline 层。
|
||||
|
||||
它负责:
|
||||
|
||||
* 校验 ``gfx_motion_asset_t`` 的结构合法性。
|
||||
* 维护 pose 状态:``pose_cur`` 和 ``pose_tgt``。
|
||||
* 维护当前 action、当前 step、step tick、loop override。
|
||||
* 处理 ``HOLD``、``DAMPED`` 等插值策略。
|
||||
* 加载 target pose 时处理 facing 和 mirror。
|
||||
|
||||
它不应该负责:
|
||||
|
||||
* display object 的创建或销毁。
|
||||
* mesh grid 的尺寸或点位。
|
||||
* 像素颜色、图片 descriptor、palette 图片。
|
||||
* scanline fill、triangle fallback 等绘制策略。
|
||||
|
||||
``gfx_motion_player.c`` 是 motion scene 到显示对象的适配层。
|
||||
|
||||
它负责:
|
||||
|
||||
* 每个 segment 创建并持有一个 ``gfx_mesh_img`` 对象。
|
||||
* 将设计空间坐标映射到目标 canvas 的屏幕坐标。
|
||||
* 设置每个 segment 的 mesh grid,并缓存 grid 尺寸以避免热路径重复分配。
|
||||
* 提供 ``gfx_motion_t`` 的 tick/apply callback,把 scene 状态同步到 mesh 对象。
|
||||
* 将 segment 分发给 primitive helper 和 style helper。
|
||||
|
||||
它不应该负责:
|
||||
|
||||
* action timeline 规则本身,除了调用 ``gfx_motion_scene_*``。
|
||||
* 通用 mesh 绘制逻辑。
|
||||
* 底层三角形光栅化、多边形填充或 alpha blend。
|
||||
* primitive 几何算法和资源样式绑定细节。
|
||||
|
||||
``gfx_motion_primitives.c`` 是 motion 几何算法层。
|
||||
|
||||
它负责:
|
||||
|
||||
* 将 capsule、ring、Bezier stroke、Bezier fill 转换成 mesh 点。
|
||||
* 通过 ``gfx_motion_player_runtime_scratch_t`` 使用 primitive 局部 scratch。
|
||||
* cubic Bezier 位置和 tangent 计算。
|
||||
* stroke extrusion 和 fill mesh generation。
|
||||
|
||||
它不应该负责:
|
||||
|
||||
* action 播放。
|
||||
* display object 生命周期。
|
||||
* palette/resource 绑定。
|
||||
* 通用 mesh 绘制内部细节。
|
||||
|
||||
``gfx_motion_style.c`` 是 motion 样式和资源绑定层。
|
||||
|
||||
它负责:
|
||||
|
||||
* runtime solid color、palette color、opacity、texture source、UV crop、layer visibility helper。
|
||||
* 将 resource UV 映射到 mesh ``rest_points``。
|
||||
* 为每个 segment 绑定正确的 image source。
|
||||
|
||||
它不应该负责:
|
||||
|
||||
* primitive 几何。
|
||||
* action 播放。
|
||||
* mesh cell 光栅化。
|
||||
|
||||
``gfx_mesh_img.c`` 是通用的可变形图片 widget。
|
||||
|
||||
它负责:
|
||||
|
||||
* 当前 mesh grid 尺寸和 point count。
|
||||
* ``points``:当前 object-local 的 mesh 几何坐标。
|
||||
* ``rest_points``:源图片采样坐标,也就是 UV/reference points。
|
||||
* source image descriptor 和解码后的 image header。
|
||||
* 根据当前 ``points`` 计算 object bounds。
|
||||
* mesh 选项:``wrap_cols``、``aa_inward``、``opacity``、control point debug drawing、``scanline_fill``。
|
||||
* 将 mesh cell 拆成三角形绘制,或对 solid fill 使用 scanline polygon fill。
|
||||
|
||||
它不应该负责:
|
||||
|
||||
* motion scene 的语义。
|
||||
* capsule、ring、Bezier 等 segment kind。
|
||||
* action 播放或 pose 插值。
|
||||
|
||||
``gfx_blend.c`` 是软件绘制后端。
|
||||
|
||||
它负责:
|
||||
|
||||
* 带 UV 的图片三角形绘制。
|
||||
* 多边形填充 coverage 计算。
|
||||
* buffer area 和 clip area 裁剪。
|
||||
* alpha blend 和 RGB565 byte swap。
|
||||
* primitive 边缘的抗锯齿策略。
|
||||
* 对超宽 polygon fill 按 X 方向分块,避免大形状因为 coverage buffer 上限直接不绘制。
|
||||
|
||||
它不应该负责:
|
||||
|
||||
* widget 状态。
|
||||
* mesh object layout。
|
||||
* motion 专用假设。
|
||||
|
||||
Scene Asset 模型
|
||||
----------------
|
||||
|
||||
``gfx_motion_asset_t`` 是 ROM 侧的 scene bundle,由运行时消费,定义在 ``include/widget/gfx_motion_scene.h``。它包含:
|
||||
|
||||
* ``meta``:schema version 和设计空间 viewbox。
|
||||
* ``joint_names`` / ``joint_count``:命名控制点。
|
||||
* ``segments``:引用 joints 的可视 primitive。
|
||||
* ``poses``:完整的 joint 坐标快照。
|
||||
* ``actions``:由多个 step 组成的动作序列,每个 step 指向一个 target pose。
|
||||
* ``sequence``:可选的默认播放序列。
|
||||
* ``layout``:默认 stroke、mirror axis、timer period、damping 等 hint。
|
||||
* ``resources``:可选纹理图片及 UV crop。
|
||||
* ``color_palette``:可选固定 segment 颜色。
|
||||
|
||||
scene 层会提前校验这些结构不变量:
|
||||
|
||||
* viewbox 宽高必须为正数。
|
||||
* joint、pose、action、sequence 的 pointer 必须和 count 匹配。
|
||||
* segment 引用的 joint 范围必须在 ``joint_count`` 内。
|
||||
* Bezier 控制点数量必须满足 ``3k + 1``。
|
||||
* resource index 和 palette index 必须能解析到有效条目。
|
||||
* resource UV crop 必须落在 image descriptor 范围内。
|
||||
* layer bit 必须在 32-bit layer mask 可表达范围内。
|
||||
|
||||
播放模型
|
||||
--------
|
||||
|
||||
``gfx_motion_scene_init()`` 负责校验 asset 并初始化第一个 action step。初始化后,``pose_cur`` 会直接 snap 到 ``pose_tgt``。
|
||||
|
||||
``gfx_motion_scene_advance()`` 负责推进 action timeline。当 ``hold_ticks`` 到期时,它切到下一个 step,加载新的 target pose,并应用该 step 的插值策略。
|
||||
|
||||
``gfx_motion_scene_tick()`` 负责将 ``pose_cur`` 向 ``pose_tgt`` 推进。对 ``DAMPED`` step,它会做 easing;函数返回坐标是否发生变化。player 会结合这个返回值和 dirty flag 判断是否需要更新 mesh 对象。
|
||||
|
||||
``GFX_MOTION_INTERP_HOLD`` 表示立即 snap 到 target pose。它在 init、action switch、step advance 时都应该生效。
|
||||
|
||||
Player Segment 管线
|
||||
-------------------
|
||||
|
||||
``gfx_motion_player_init()`` 会为每个 segment 创建一个 ``gfx_mesh_img`` 对象。初始 grid 由 segment kind 决定:
|
||||
|
||||
* Capsule:``1 x 1`` grid,共 4 个点。
|
||||
* Ring:``N x 1`` wrapped grid,两行点,分别表示外圈和内圈。
|
||||
* Bezier strip:按曲线采样生成列,不 wrap。
|
||||
* Bezier loop:按曲线采样生成列,并启用 wrap。
|
||||
* Bezier fill:使用 eye/ellipse preset grid,或 generic closed-loop rim grid。
|
||||
|
||||
每次 motion apply callback 中,player 会:
|
||||
|
||||
1. 检查 scene 或 mesh 是否 dirty。
|
||||
2. 将当前 segment 需要的 joints 从设计坐标转换到屏幕坐标。
|
||||
3. 根据 canvas scale 计算 stroke width 和 radius。
|
||||
4. 按 segment kind 调用对应 primitive apply helper,更新 mesh points。
|
||||
5. 根据 layer mask 设置 object visible。
|
||||
6. 所有可见 segment 更新完成后清理 dirty flag。
|
||||
|
||||
``gfx_motion_primitives.c`` 中的 primitive 转换细节:
|
||||
|
||||
* Capsule 根据两个端点和 stroke width 生成一个沿线段方向的厚矩形。
|
||||
* Ring 生成外圈和内圈两行圆形采样点,并启用 column wrap。
|
||||
* Bezier stroke 计算 cubic 位置和解析 tangent,再沿左右法线挤出成两行 mesh。
|
||||
* Bezier fill 对 eye/ellipse 使用 preset path;对 generic closed loop 构建 hub/rim mesh。
|
||||
|
||||
样式与资源
|
||||
----------
|
||||
|
||||
``gfx_motion_style.c`` 按下面优先级绑定 image source:
|
||||
|
||||
1. ``resource_idx``:纹理图片。
|
||||
2. ``color_idx``:palette 生成的 1x1 图片。
|
||||
3. runtime solid 1x1 图片。
|
||||
|
||||
对 texture resource,``uv_x``、``uv_y``、``uv_w``、``uv_h`` 会映射到 mesh 的 ``rest_points``。mesh 当前 ``points`` 仍然表示屏幕几何;``rest_points`` 表示源图片采样坐标。这样 UV crop 逻辑保持通用,同一个 mesh renderer 可以同时绘制整图纹理和裁剪后的 resource segment。
|
||||
|
||||
对 palette color 和 runtime solid color,source 是一个 1x1 RGB565 image。Bezier fill segment 还可以启用 scanline fill,让 solid closed shape 不必通过 textured triangles 光栅化。
|
||||
|
||||
Mesh Image 模型
|
||||
---------------
|
||||
|
||||
``gfx_mesh_img`` 维护两组点:
|
||||
|
||||
* ``points``:object-local Q8 几何坐标。
|
||||
* ``rest_points``:object-local Q8 源图片采样坐标。
|
||||
|
||||
普通图片中,这两组点初始都是覆盖整张图片的规则 grid。motion segment 中,player 会持续更新 ``points`` 来改变屏幕形状,而 ``rest_points`` 保持为源 UV reference。texture crop 只更新 ``rest_points``。
|
||||
|
||||
当 ``points`` 改变时,``gfx_mesh_img_update_bounds()`` 会重新计算 object bounding box。draw origin 由 object position 减去 mesh 最小 bound 得出,因此 mesh 可以有负的 local coordinate,同时仍然走正常 object geometry 系统绘制。
|
||||
|
||||
重要 mesh 选项:
|
||||
|
||||
* ``wrap_cols``:把最后一列和第一列连起来。ring 和 closed Bezier loop 需要它。
|
||||
* ``aa_inward``:让边缘 AA 向内衰减,避免细 stroke 外侧出现 halo。
|
||||
* ``scanline_fill``:在条件满足时绕过 textured triangle drawing,直接绘制 solid polygon。
|
||||
* ``opacity``:应用 segment 级整体透明度。
|
||||
|
||||
绘制管线
|
||||
--------
|
||||
|
||||
``gfx_mesh_img_draw()`` 会打开 image decoder,解析 RGB565 或 RGB565A8 payload,计算裁剪区域,然后选择两条路径之一。
|
||||
|
||||
Scanline fill 路径:
|
||||
|
||||
* 用于部分 solid filled polygon。
|
||||
* 从 mesh points 构造 polygon。
|
||||
* 调用 ``gfx_sw_blend_polygon_fill()``。
|
||||
* 如果 scanline scratch capacity 不够,会 fallback 到 triangle drawing,而不是直接空白。
|
||||
|
||||
Triangle 路径:
|
||||
|
||||
* 遍历每个 mesh cell。
|
||||
* 用屏幕坐标和 source UV 构造四个 vertex。
|
||||
* 将 quad 拆成两个 triangle。
|
||||
* 选择较短对角线,减少变形 quad 上的裂缝。
|
||||
* 标记内部边,避免 shared edge AA 产生深色缝。
|
||||
* 每个 cell 调用两次 ``gfx_sw_blend_img_triangle_draw()``。
|
||||
|
||||
底层绘制
|
||||
--------
|
||||
|
||||
``gfx_sw_blend_img_triangle_draw()`` 在 triangle 内采样源图片并混合到目标 buffer。它处理:
|
||||
|
||||
* 屏幕裁剪。
|
||||
* source UV 插值。
|
||||
* RGB565/RGB565A8 source alpha。
|
||||
* uniform opacity。
|
||||
* internal edge suppression。
|
||||
* 可选 inward AA。
|
||||
|
||||
``gfx_sw_blend_polygon_fill()`` 用 solid color 填充 polygon。它负责裁剪、per-pixel coverage 计算,并对超宽 polygon 按 X 方向分块,保证 coverage scratch memory 有上限。
|
||||
|
||||
模块划分评估
|
||||
------------
|
||||
|
||||
当前模块划分已经按层拆开:
|
||||
|
||||
* ``scene.c`` 是纯 playback state。
|
||||
* ``player.c`` 是 motion scene runtime orchestration。
|
||||
* ``primitives.c`` 是 motion geometry generation。
|
||||
* ``style.c`` 是 motion style/resource binding。
|
||||
* ``mesh_img.c`` 是可复用 deformable image 基础设施。
|
||||
* ``gfx_blend.c`` 是底层 rasterization。
|
||||
|
||||
后续主要观察 ``gfx_motion_primitives.c`` 的体积。如果继续增加新的 primitive family,先放在该文件中;等 primitive API 和边界稳定后,再按 primitive family 进一步拆分。
|
||||
|
||||
优化入口
|
||||
--------
|
||||
|
||||
后续优化可以优先看这些点:
|
||||
|
||||
* Player dirty flags:pose 和 canvas 未变化时避免 apply mesh。
|
||||
* Cached segment grids:热路径避免重复 realloc mesh points。
|
||||
* Bezier sampling density:stroke 和 fill 分别调采样密度。
|
||||
* Resource UV updates:只有 grid 或 resource crop 改变时才重算 rest points。
|
||||
* Mesh bounds:保留 clamp warning,因为过大 bounds 往往暴露坐标 bug。
|
||||
* Scanline fill:solid 大面积 fill 优先走 scanline;textured 或 unsupported 情况保留 triangle fallback。
|
||||
* Blend chunk width:只有在 stack/static scratch 预算允许时才增大。
|
||||
* Layer mask:如果未来 layer 很多,可以在 tessellation 前跳过隐藏 segment group。
|
||||
|
||||
测试 Checklist
|
||||
--------------
|
||||
|
||||
改动这条链路时建议覆盖这些 case:
|
||||
|
||||
* 空 segment asset 可以安全 init/deinit。
|
||||
* ``HOLD`` action 在 init、action switch、step advance 时都会立即 snap。
|
||||
* palette segment 不会被 ``gfx_motion_player_set_color()`` 覆盖。
|
||||
* texture segment 会尊重 resource UV crop。
|
||||
* ring grid 动态变化后不会丢失 UV crop。
|
||||
* layer mask 可以隐藏并恢复 segment。
|
||||
* Bezier stroke 在急弯或短曲线下不出现 dashed/bowtie artifact。
|
||||
* oversized scanline fill 会 fallback 到 triangle rendering,而不是绘制空白。
|
||||
* wide polygon fill 会按 chunk 绘制,而不是提前 return。
|
||||
* mesh allocation failure 尽量保留旧 grid 状态。
|
||||
* bounds 超过 geometry range 时会 clamp 并打 log。
|
||||
|
||||
迭代规则
|
||||
--------
|
||||
|
||||
后续修改建议遵守这些规则:
|
||||
|
||||
* action timeline 或 pose 行为放在 ``gfx_motion_scene.c``。
|
||||
* segment-to-mesh conversion 放在 ``gfx_motion_primitives.c``。
|
||||
* palette/resource/layer/opacity 逻辑放在 ``gfx_motion_style.c``。
|
||||
* 通用 mesh storage、UV、bounds、draw dispatch 放在 ``gfx_mesh_img.c``。
|
||||
* pixel coverage、sampling、AA、blend math 放在 ``gfx_blend.c``。
|
||||
* ``include/widget/gfx_motion_scene.h`` 里的 public struct 尽量保持稳定,因为生成的 asset 依赖它们。
|
||||
* asset 错误尽量在 scene 层校验,不要拖到 player 或 renderer 才失败。
|
||||
* renderer fallback 要打 log,避免静默绘制空白。
|
||||
@ -0,0 +1,103 @@
|
||||
Motion Scene Widget
|
||||
===================
|
||||
|
||||
The motion scene widget is the path-driven character and emote runtime in ESP Emote GFX. It is designed for assets exported as a ``gfx_motion_asset_t`` bundle and rendered through ``gfx_motion_player_t``.
|
||||
|
||||
When to Use It
|
||||
--------------
|
||||
|
||||
Use the motion scene path when you need:
|
||||
|
||||
* Character or emote playback built from vector-like paths instead of bitmap frames
|
||||
* Per-part styling with solid colors, palette colors, opacity, or texture binding
|
||||
* Small action sets such as idle, move, happy, thinking, or touch-reactive actions
|
||||
* Canvas-level movement where the whole character can swim, drift, or follow touch input
|
||||
|
||||
Core Model
|
||||
----------
|
||||
|
||||
The motion scene asset has four main layers:
|
||||
|
||||
* ``joint_names`` and joint coordinates: named control points in design space
|
||||
* ``segments``: visual primitives built from joints
|
||||
* ``poses``: complete joint-coordinate snapshots
|
||||
* ``actions``: sequences of pose steps with hold time, interpolation, and facing
|
||||
|
||||
The runtime owns a parser plus renderer:
|
||||
|
||||
* ``gfx_motion_scene_t`` manages pose interpolation and action state
|
||||
* ``gfx_motion_player_t`` creates one ``gfx_mesh_img`` object per segment and applies the current pose to screen space
|
||||
|
||||
Segment Types
|
||||
-------------
|
||||
|
||||
The current scene format supports these segment kinds:
|
||||
|
||||
* ``GFX_MOTION_SEG_CAPSULE``: thick limb/body stroke between two joints
|
||||
* ``GFX_MOTION_SEG_RING``: circular outline around a center joint
|
||||
* ``GFX_MOTION_SEG_BEZIER_STRIP``: open Bezier stroke
|
||||
* ``GFX_MOTION_SEG_BEZIER_LOOP``: closed Bezier stroke
|
||||
* ``GFX_MOTION_SEG_BEZIER_FILL``: closed filled Bezier region
|
||||
|
||||
Each segment can also carry:
|
||||
|
||||
* ``stroke_width`` override
|
||||
* ``resource_idx`` for texture/image binding
|
||||
* ``color_idx`` for palette-bound solid color
|
||||
* ``opacity`` for per-part alpha
|
||||
|
||||
Playback Flow
|
||||
-------------
|
||||
|
||||
Typical runtime usage:
|
||||
|
||||
1. Create or include a generated scene asset (for example from a designer/export pipeline).
|
||||
2. Call ``gfx_motion_player_init()`` with a display and the asset.
|
||||
3. Set the target canvas using ``gfx_motion_player_set_canvas()``.
|
||||
4. Optionally set the runtime color with ``gfx_motion_player_set_color()``.
|
||||
5. Select an initial action using ``gfx_motion_player_set_action()``.
|
||||
6. When finished, call ``gfx_motion_player_deinit()``.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
#include "rig_active.inc"
|
||||
|
||||
static gfx_motion_player_t motion_player;
|
||||
|
||||
void motion_scene_start(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_motion_player_init(&motion_player, disp, &s_motion_scene_asset);
|
||||
gfx_motion_player_set_canvas(&motion_player, 0, 0, 360, 360);
|
||||
gfx_motion_player_set_color(&motion_player, GFX_COLOR_HEX(0xFF7A00));
|
||||
gfx_motion_player_set_action(&motion_player, 0, true);
|
||||
}
|
||||
|
||||
void motion_scene_stop(void)
|
||||
{
|
||||
gfx_motion_player_deinit(&motion_player);
|
||||
}
|
||||
|
||||
Interactive Example
|
||||
-------------------
|
||||
|
||||
An end-to-end example is available in ``test_apps/main/test_motion.c``. It demonstrates:
|
||||
|
||||
* full-screen motion scene preview
|
||||
* tap-to-switch action
|
||||
* touch-guided movement by changing the runtime canvas
|
||||
* timer-driven autonomous movement between touch interactions
|
||||
|
||||
Current Notes
|
||||
-------------
|
||||
|
||||
The current implementation intentionally keeps the dependency chain small:
|
||||
|
||||
* no NanoVG dependency
|
||||
* no libtess2 dependency
|
||||
* filled polygon rendering uses the internal software path
|
||||
|
||||
This makes the widget easier to release and integrate into ESP-IDF projects, while keeping the scene model stable for designer/export tooling.
|
||||
@ -0,0 +1,158 @@
|
||||
Overview
|
||||
========
|
||||
|
||||
ESP Emote GFX is a lightweight graphics framework for ESP-IDF that provides a simple yet powerful API for rendering graphics on embedded displays. It is designed with memory efficiency and performance in mind, making it ideal for resource-constrained embedded systems.
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
The framework is built around a core object system where all graphical elements (images, labels, animations, buttons, QR codes, motion scenes) are treated as objects. These objects share common properties like position, size, visibility, and alignment.
|
||||
|
||||
Core Components
|
||||
---------------
|
||||
|
||||
Core System
|
||||
~~~~~~~~~~~
|
||||
|
||||
The core system (`gfx_core`) manages:
|
||||
|
||||
* Graphics context initialization and deinitialization
|
||||
* Buffer management (internal or external)
|
||||
* Rendering pipeline
|
||||
* Thread safety with mutex locking
|
||||
* Screen refresh and invalidation
|
||||
|
||||
Object System
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The object system (`gfx_obj`) provides:
|
||||
|
||||
* Base object structure for all graphical elements
|
||||
* Position and size management
|
||||
* Alignment system (similar to LVGL)
|
||||
* Visibility control
|
||||
* Object lifecycle management
|
||||
|
||||
Timer System
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The timer system (`gfx_timer`) provides:
|
||||
|
||||
* High-resolution timers for animations
|
||||
* Callback-based timer events
|
||||
* Repeat count and period control
|
||||
* System tick management
|
||||
|
||||
Widgets
|
||||
-------
|
||||
|
||||
Image Widget
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The image widget supports:
|
||||
|
||||
* RGB565 format (16-bit color)
|
||||
* RGB565A8 format (16-bit color with 8-bit alpha)
|
||||
* C array and binary formats
|
||||
* Automatic format detection
|
||||
|
||||
Label Widget
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The label widget provides:
|
||||
|
||||
* Text rendering with multiple font formats
|
||||
* LVGL font support
|
||||
* FreeType TTF/OTF font support
|
||||
* Text alignment (left, center, right)
|
||||
* Long text handling (wrap, scroll, clip)
|
||||
* Background colors and opacity
|
||||
|
||||
Button Widget
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The button widget provides:
|
||||
|
||||
* Text label management
|
||||
* Normal and pressed background colors
|
||||
* Border color and width configuration
|
||||
* Font and text alignment control
|
||||
|
||||
Animation Widget
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The animation widget supports:
|
||||
|
||||
* EAF (ESP Animation Format) files
|
||||
* Frame-by-frame playback control
|
||||
* Segment playback (start/end frames)
|
||||
* FPS control
|
||||
* Loop and repeat options
|
||||
* Mirror effects
|
||||
|
||||
QR Code Widget
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The QR code widget provides:
|
||||
|
||||
* Dynamic QR code generation
|
||||
* Configurable size and error correction
|
||||
* Custom foreground and background colors
|
||||
|
||||
Motion Scene Widget
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The motion scene runtime provides:
|
||||
|
||||
* Path-driven articulated animation built from joints, poses, and actions
|
||||
* Segment primitives for capsules, rings, open/closed Bezier strokes, and Bezier fills
|
||||
* Per-segment solid color, palette color, opacity, or texture binding
|
||||
* Display-space scaling through a configurable canvas and asset viewbox
|
||||
* Touch-friendly runtime usage for interactive characters and emotes
|
||||
|
||||
The public entry points are ``gfx_motion_player_init()``, ``gfx_motion_player_set_canvas()``, ``gfx_motion_player_set_action()``, and ``gfx_motion_player_set_color()``. The underlying asset format is described by ``gfx_motion_asset_t`` in ``widget/gfx_motion_scene.h``.
|
||||
|
||||
Memory Management
|
||||
-----------------
|
||||
|
||||
The framework supports two buffer management modes:
|
||||
|
||||
Internal Buffers
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The framework automatically allocates and manages frame buffers internally. This is the simplest mode but requires sufficient heap memory.
|
||||
|
||||
External Buffers
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
You can provide your own buffers, allowing you to:
|
||||
|
||||
* Use memory-mapped regions
|
||||
* Control buffer placement (SRAM, SPIRAM, etc.)
|
||||
* Optimize for specific memory constraints
|
||||
|
||||
Thread Safety
|
||||
-------------
|
||||
|
||||
All widget operations should be performed within a graphics lock to ensure thread safety:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_emote_lock(handle);
|
||||
// Perform operations
|
||||
gfx_obj_set_pos(obj, x, y);
|
||||
gfx_label_set_text(label, "New text");
|
||||
gfx_emote_unlock(handle);
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
* ESP-IDF 5.0 or higher
|
||||
* FreeType (for TTF/OTF font support)
|
||||
* ESP New JPEG (for JPEG decoding)
|
||||
* No NanoVG or libtess2 dependency is required for the current motion scene path
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
This project is licensed under the Apache License 2.0.
|
||||
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# ESP Emote GFX 文档本地预览脚本
|
||||
# 一键构建并预览文档
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PORT="${1:-8090}"
|
||||
# Bind all interfaces by default so LAN URLs (e.g. http://10.x.x.x:PORT/) work; use 127.0.0.1 for local-only.
|
||||
BIND_ADDR="${DOCS_PREVIEW_BIND:-0.0.0.0}"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
echo "=========================================="
|
||||
echo " ESP Emote GFX 文档本地预览"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 关闭已存在的 http.server 进程
|
||||
echo "[0/4] 检查并关闭已有服务..."
|
||||
OLD_PIDS=$(ps -ef | grep "python.*http.server.*$PORT" | grep -v grep | awk '{print $2}')
|
||||
if [ -n "$OLD_PIDS" ]; then
|
||||
echo " 关闭端口 $PORT 上的旧进程: $OLD_PIDS"
|
||||
echo "$OLD_PIDS" | xargs kill -9 2>/dev/null || true
|
||||
sleep 1
|
||||
else
|
||||
echo " ✓ 无旧进程"
|
||||
fi
|
||||
|
||||
# 检查并安装依赖
|
||||
echo "[1/4] 检查依赖..."
|
||||
if ! python3 -c "import sphinx" 2>/dev/null; then
|
||||
echo " 安装 Sphinx 依赖..."
|
||||
pip install -r docs/requirements.txt -q
|
||||
else
|
||||
echo " ✓ Sphinx 已安装"
|
||||
fi
|
||||
|
||||
# 自动生成 API RST + 构建 Sphinx + 后处理 Doxygen
|
||||
echo "[2/4] 自动生成并构建文档..."
|
||||
if command -v doxygen >/dev/null 2>&1; then
|
||||
bash docs/scripts/postprocess_docs.sh
|
||||
echo " ✓ API 文档、Sphinx、Doxygen 全部完成"
|
||||
else
|
||||
bash docs/scripts/postprocess_docs.sh --skip-doxygen
|
||||
echo " ✓ API 文档和 Sphinx 构建完成"
|
||||
echo " ⚠ Doxygen 未安装,跳过 C/C++ API 文档"
|
||||
echo " 安装方式: sudo apt-get install doxygen graphviz"
|
||||
fi
|
||||
|
||||
# 启动本地服务器
|
||||
echo "[3/4] 启动本地预览服务器..."
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 文档预览地址:"
|
||||
echo ""
|
||||
echo " http://127.0.0.1:$PORT (same host)"
|
||||
echo " http://<this-machine-LAN-ip>:$PORT (other devices; server binds $BIND_ADDR)"
|
||||
echo ""
|
||||
echo " 主要页面(EN / 中文 分目录;顶部可切换语言):"
|
||||
echo " - 语言选择: http://127.0.0.1:$PORT/index.html"
|
||||
echo " - English: http://127.0.0.1:$PORT/en/index.html"
|
||||
echo " - 中文: http://127.0.0.1:$PORT/zh_CN/index.html"
|
||||
echo " - Core API: http://127.0.0.1:$PORT/en/api/core/index.html"
|
||||
echo " - Widget API: http://127.0.0.1:$PORT/en/api/widgets/index.html"
|
||||
echo " - Doxygen: http://127.0.0.1:$PORT/doxygen/index.html"
|
||||
echo ""
|
||||
echo " 按 Ctrl+C 停止服务器"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
cd docs/_build/html
|
||||
python3 -m http.server "$PORT" --bind "$BIND_ADDR"
|
||||
|
||||
@ -0,0 +1,236 @@
|
||||
Quick Start Guide
|
||||
=================
|
||||
|
||||
This guide will help you get started with ESP Emote GFX in just a few steps.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Add ESP Emote GFX to your ESP-IDF project by including it as a component. The component is available through the ESP Component Registry.
|
||||
|
||||
Basic Setup
|
||||
-----------
|
||||
|
||||
1. Include the main header:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
|
||||
2. Initialize the graphics core (no display yet):
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_core_config_t gfx_cfg = {
|
||||
.fps = 30,
|
||||
.task = GFX_EMOTE_INIT_CONFIG()
|
||||
};
|
||||
gfx_handle_t handle = gfx_emote_init(&gfx_cfg);
|
||||
if (handle == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to initialize GFX");
|
||||
return;
|
||||
}
|
||||
|
||||
3. Add a display with a flush callback:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void disp_flush_callback(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data)
|
||||
{
|
||||
void *panel = gfx_disp_get_user_data(disp);
|
||||
// Send RGB565 data (x1,y1)-(x2,y2) to your panel, e.g. esp_lcd_panel_draw_bitmap(panel, x1, y1, x2, y2, data);
|
||||
}
|
||||
|
||||
gfx_disp_config_t disp_cfg = {
|
||||
.h_res = 320,
|
||||
.v_res = 240,
|
||||
.flush_cb = disp_flush_callback,
|
||||
.update_cb = NULL,
|
||||
.user_data = your_panel_handle, // e.g. esp_lcd_panel_handle_t
|
||||
.flags = { .swap = true },
|
||||
.buffers = { .buf1 = NULL, .buf2 = NULL, .buf_pixels = 320 * 16 },
|
||||
};
|
||||
gfx_disp_t *disp = gfx_disp_add(handle, &disp_cfg);
|
||||
if (disp == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to add display");
|
||||
gfx_emote_deinit(handle);
|
||||
return;
|
||||
}
|
||||
|
||||
4. (Optional) Register panel IO callback so the framework knows when flush is done:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
static bool flush_io_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
|
||||
{
|
||||
gfx_disp_t *disp = (gfx_disp_t *)user_ctx;
|
||||
if (disp) {
|
||||
gfx_disp_flush_ready(disp, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = flush_io_ready };
|
||||
esp_lcd_panel_io_register_event_callbacks(io_handle, &cbs, disp);
|
||||
|
||||
5. (Optional) Add touch input:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void touch_event_cb(gfx_touch_t *touch, const gfx_touch_event_t *event, void *user_data)
|
||||
{
|
||||
// Handle PRESS / MOVE / RELEASE; event->x, event->y, event->hit_obj
|
||||
}
|
||||
|
||||
gfx_touch_config_t touch_cfg = {
|
||||
.handle = esp_lcd_touch_handle, // from your BSP or esp_lcd_touch_new
|
||||
.event_cb = touch_event_cb,
|
||||
.disp = disp,
|
||||
.poll_ms = 50,
|
||||
.user_data = NULL,
|
||||
};
|
||||
gfx_touch_t *touch = gfx_touch_add(handle, &touch_cfg);
|
||||
|
||||
Creating Your First Widget
|
||||
--------------------------
|
||||
|
||||
Widgets are created on a **display** (``gfx_disp_t *``), not on the handle.
|
||||
|
||||
Creating a Label
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t *label = gfx_label_create(disp);
|
||||
gfx_label_set_text(label, "Hello, World!");
|
||||
gfx_obj_set_pos(label, 50, 50);
|
||||
gfx_label_set_color(label, GFX_COLOR_HEX(0xFF0000)); // Red
|
||||
|
||||
Creating an Image
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t *img = gfx_img_create(disp);
|
||||
extern const gfx_image_dsc_t my_image;
|
||||
gfx_img_set_src(img, (void *)&my_image);
|
||||
gfx_obj_set_pos(img, 100, 100);
|
||||
|
||||
Creating an Animation
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_obj_t *anim = gfx_anim_create(disp);
|
||||
gfx_anim_set_src(anim, anim_data, anim_size);
|
||||
gfx_obj_align(anim, GFX_ALIGN_CENTER, 0, 0);
|
||||
gfx_anim_set_segment(anim, 0, 0xFFFF, 15, true);
|
||||
gfx_anim_start(anim);
|
||||
|
||||
Creating a Rig Scene
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Rig scenes are created from a generated ``gfx_motion_asset_t`` and managed by ``gfx_motion_player_t``.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
#include "rig_active.inc"
|
||||
|
||||
static gfx_motion_player_t motion_player;
|
||||
|
||||
void setup_motion_scene(gfx_disp_t *disp)
|
||||
{
|
||||
gfx_motion_player_init(&motion_player, disp, &s_motion_scene_asset);
|
||||
gfx_motion_player_set_canvas(&motion_player, 0, 0, 320, 240);
|
||||
gfx_motion_player_set_color(&motion_player, GFX_COLOR_HEX(0xFF7A00));
|
||||
gfx_motion_player_set_action(&motion_player, 0, true);
|
||||
}
|
||||
|
||||
Use ``gfx_motion_player_deinit()`` when the scene is no longer needed.
|
||||
|
||||
Object touch callback (e.g. drag)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
void my_touch_cb(gfx_obj_t *obj, const gfx_touch_event_t *event, void *user_data)
|
||||
{
|
||||
if (event->type == GFX_TOUCH_EVENT_PRESS) { /* ... */ }
|
||||
if (event->type == GFX_TOUCH_EVENT_MOVE) { gfx_obj_set_pos(obj, event->x, event->y); }
|
||||
}
|
||||
gfx_obj_set_touch_cb(label, my_touch_cb, NULL);
|
||||
|
||||
Thread Safety
|
||||
-------------
|
||||
|
||||
When modifying objects from outside the graphics task, use the graphics lock:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
gfx_emote_lock(handle);
|
||||
gfx_label_set_text(label, "Updated text");
|
||||
gfx_obj_set_pos(img, new_x, new_y);
|
||||
gfx_emote_unlock(handle);
|
||||
|
||||
Complete Example
|
||||
----------------
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
#include "gfx.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "gfx_example";
|
||||
static gfx_handle_t gfx_handle = NULL;
|
||||
static gfx_disp_t *gfx_disp = NULL;
|
||||
|
||||
static void disp_flush_callback(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data)
|
||||
{
|
||||
esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)gfx_disp_get_user_data(disp);
|
||||
esp_lcd_panel_draw_bitmap(panel, x1, y1, x2, y2, data);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
gfx_core_config_t gfx_cfg = {
|
||||
.fps = 30,
|
||||
.task = GFX_EMOTE_INIT_CONFIG(),
|
||||
};
|
||||
gfx_handle = gfx_emote_init(&gfx_cfg);
|
||||
if (gfx_handle == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to initialize GFX");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_disp_config_t disp_cfg = {
|
||||
.h_res = 320,
|
||||
.v_res = 240,
|
||||
.flush_cb = disp_flush_callback,
|
||||
.update_cb = NULL,
|
||||
.user_data = panel_handle, // your esp_lcd_panel_handle_t
|
||||
.flags = { .swap = true },
|
||||
.buffers = { .buf1 = NULL, .buf2 = NULL, .buf_pixels = 320 * 16 },
|
||||
};
|
||||
gfx_disp = gfx_disp_add(gfx_handle, &disp_cfg);
|
||||
if (gfx_disp == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to add display");
|
||||
gfx_emote_deinit(gfx_handle);
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_obj_t *label = gfx_label_create(gfx_disp);
|
||||
gfx_label_set_text(label, "Hello, ESP Emote GFX!");
|
||||
gfx_obj_set_pos(label, 50, 50);
|
||||
gfx_label_set_color(label, GFX_COLOR_HEX(0x00FF00));
|
||||
|
||||
gfx_disp_refresh_all(gfx_disp);
|
||||
ESP_LOGI(TAG, "GFX application started");
|
||||
}
|
||||
|
||||
Next Steps
|
||||
----------
|
||||
|
||||
* Read the :doc:`Core API Reference <api/core/index>` for detailed API documentation
|
||||
* Read the :doc:`Rig Widget Guide <motion_widget>` for the scene asset model and playback flow
|
||||
* Check out the :doc:`Widget API Reference <api/widgets/index>` for widget-specific functions
|
||||
* See :doc:`Examples <examples>` for more complex usage patterns
|
||||
@ -0,0 +1,4 @@
|
||||
esp-docs>=2.1.5
|
||||
breathe>=4.35.0
|
||||
Babel>=2.12.0
|
||||
|
||||
@ -0,0 +1,645 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
自动从 C 头文件生成 RST API 文档
|
||||
|
||||
使用方法:
|
||||
python docs/scripts/generate_api_docs.py
|
||||
|
||||
功能:
|
||||
1. 解析头文件中的 Doxygen 注释
|
||||
2. 生成对应的 RST 文档
|
||||
3. 支持结构体、枚举、函数、宏等
|
||||
"""
|
||||
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
@dataclass
|
||||
class DocItem:
|
||||
"""文档项"""
|
||||
name: str
|
||||
kind: str # function, struct, enum, typedef, macro
|
||||
brief: str = ""
|
||||
description: str = ""
|
||||
params: List[Dict] = field(default_factory=list)
|
||||
returns: str = ""
|
||||
code: str = ""
|
||||
notes: List[str] = field(default_factory=list)
|
||||
examples: List[str] = field(default_factory=list)
|
||||
|
||||
class HeaderParser:
|
||||
"""解析 C 头文件"""
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
self.content = f.read()
|
||||
self.items: List[DocItem] = []
|
||||
|
||||
def parse(self) -> List[DocItem]:
|
||||
"""解析头文件"""
|
||||
self._parse_typedefs()
|
||||
self._parse_enums()
|
||||
self._parse_structs()
|
||||
self._parse_macros()
|
||||
self._parse_functions()
|
||||
return self.items
|
||||
|
||||
def _find_comment_before(self, pos: int) -> Optional[str]:
|
||||
"""查找位置前最近的 Doxygen 注释块"""
|
||||
before = self.content[:pos]
|
||||
|
||||
# 向前查找,跳过空白
|
||||
search_pos = len(before) - 1
|
||||
while search_pos >= 0 and before[search_pos] in ' \t\n':
|
||||
search_pos -= 1
|
||||
|
||||
if search_pos < 0:
|
||||
return None
|
||||
|
||||
# 检查是否以 */ 结尾(块注释)
|
||||
if search_pos >= 1 and before[search_pos-1:search_pos+1] == '*/':
|
||||
# 向前找 /**
|
||||
start = before.rfind('/**', 0, search_pos)
|
||||
if start != -1:
|
||||
return before[start:search_pos+1]
|
||||
|
||||
return None
|
||||
|
||||
def _parse_doxygen_block(self, comment: str) -> Dict:
|
||||
"""解析单个 Doxygen 注释块"""
|
||||
result = {
|
||||
'brief': '',
|
||||
'params': [],
|
||||
'returns': '',
|
||||
'notes': [],
|
||||
'examples': [],
|
||||
}
|
||||
|
||||
if not comment:
|
||||
return result
|
||||
|
||||
# 清理注释标记
|
||||
lines = []
|
||||
for line in comment.split('\n'):
|
||||
# 移除 /**, */, *, //
|
||||
line = re.sub(r'^\s*/?\*+/?', '', line)
|
||||
line = re.sub(r'\*/$', '', line)
|
||||
lines.append(line.strip())
|
||||
|
||||
text = '\n'.join(lines).strip()
|
||||
|
||||
# 提取 @brief
|
||||
match = re.search(r'@brief\s+(.+?)(?=\n\s*@|\n\s*\n|$)', text, re.DOTALL)
|
||||
if match:
|
||||
result['brief'] = ' '.join(match.group(1).split())
|
||||
|
||||
# 提取 @param
|
||||
for match in re.finditer(r'@param(?:\[(\w+)\])?\s+(\w+)\s+(.+?)(?=\n\s*@|\n\s*\n|$)', text, re.DOTALL):
|
||||
direction = match.group(1) or 'in'
|
||||
name = match.group(2)
|
||||
desc = ' '.join(match.group(3).split())
|
||||
result['params'].append({'name': name, 'direction': direction, 'desc': desc})
|
||||
|
||||
# 提取 @return
|
||||
match = re.search(r'@returns?\s+(.+?)(?=\n\s*@|\n\s*\n|$)', text, re.DOTALL)
|
||||
if match:
|
||||
result['returns'] = ' '.join(match.group(1).split())
|
||||
|
||||
# 提取 @note
|
||||
for match in re.finditer(r'@note\s+(.+?)(?=\n\s*@|\n\s*\n|$)', text, re.DOTALL):
|
||||
result['notes'].append(' '.join(match.group(1).split()))
|
||||
|
||||
# 提取 @code...@endcode
|
||||
for match in re.finditer(r'@code\s*(.+?)@endcode', text, re.DOTALL):
|
||||
code = match.group(1).strip()
|
||||
# 保持代码缩进
|
||||
result['examples'].append(code)
|
||||
|
||||
return result
|
||||
|
||||
def _parse_typedefs(self):
|
||||
"""解析简单 typedef"""
|
||||
# typedef void *gfx_handle_t;
|
||||
pattern = r'typedef\s+(\w+)\s*\*?\s*(\w+_t)\s*;'
|
||||
for match in re.finditer(pattern, self.content):
|
||||
name = match.group(2)
|
||||
comment = self._find_comment_before(match.start())
|
||||
parsed = self._parse_doxygen_block(comment)
|
||||
|
||||
self.items.append(DocItem(
|
||||
name=name,
|
||||
kind='typedef',
|
||||
brief=parsed['brief'],
|
||||
code=match.group(0).strip(),
|
||||
))
|
||||
|
||||
# 函数指针 typedef
|
||||
pattern = r'typedef\s+(\w+)\s*\(\s*\*\s*(\w+_t)\s*\)\s*\(([^)]*)\)\s*;'
|
||||
for match in re.finditer(pattern, self.content):
|
||||
name = match.group(2)
|
||||
comment = self._find_comment_before(match.start())
|
||||
parsed = self._parse_doxygen_block(comment)
|
||||
|
||||
self.items.append(DocItem(
|
||||
name=name,
|
||||
kind='typedef',
|
||||
brief=parsed['brief'],
|
||||
code=match.group(0).strip(),
|
||||
))
|
||||
|
||||
def _parse_enums(self):
|
||||
"""解析枚举"""
|
||||
pattern = r'typedef\s+enum\s*\{([^}]+)\}\s*(\w+)\s*;'
|
||||
for match in re.finditer(pattern, self.content):
|
||||
name = match.group(2)
|
||||
comment = self._find_comment_before(match.start())
|
||||
parsed = self._parse_doxygen_block(comment)
|
||||
|
||||
self.items.append(DocItem(
|
||||
name=name,
|
||||
kind='enum',
|
||||
brief=parsed['brief'],
|
||||
code=match.group(0).strip(),
|
||||
))
|
||||
|
||||
def _parse_structs(self):
|
||||
"""解析结构体"""
|
||||
pattern = r'typedef\s+struct\s*\{([\s\S]+?)\}\s*(\w+_t)\s*;'
|
||||
for match in re.finditer(pattern, self.content):
|
||||
name = match.group(2)
|
||||
comment = self._find_comment_before(match.start())
|
||||
parsed = self._parse_doxygen_block(comment)
|
||||
|
||||
self.items.append(DocItem(
|
||||
name=name,
|
||||
kind='struct',
|
||||
brief=parsed['brief'],
|
||||
code=match.group(0).strip(),
|
||||
))
|
||||
|
||||
def _parse_macros(self):
|
||||
"""解析宏定义"""
|
||||
pattern = r'#define\s+(\w+)\s*\([^)]*\)[^\n]*(?:\\\n[^\n]*)*'
|
||||
for match in re.finditer(pattern, self.content):
|
||||
name = match.group(1)
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
|
||||
comment = self._find_comment_before(match.start())
|
||||
parsed = self._parse_doxygen_block(comment)
|
||||
|
||||
self.items.append(DocItem(
|
||||
name=name,
|
||||
kind='macro',
|
||||
brief=parsed['brief'],
|
||||
code=match.group(0).strip(),
|
||||
))
|
||||
|
||||
def _parse_functions(self):
|
||||
"""解析函数声明"""
|
||||
# 匹配函数声明:返回类型 函数名(参数); 返回类型与函数名之间允许无空格(如 gfx_touch_t *gfx_touch_add)
|
||||
pattern = r'/\*\*[\s\S]*?\*/\s*\n\s*(\w+(?:\s*\*)?)\s*(\w+)\s*\(([^)]*)\)\s*;'
|
||||
|
||||
for match in re.finditer(pattern, self.content):
|
||||
full_match = match.group(0)
|
||||
ret_type = match.group(1).strip()
|
||||
name = match.group(2)
|
||||
|
||||
# 提取注释部分
|
||||
comment_end = full_match.find('*/')
|
||||
if comment_end != -1:
|
||||
comment = full_match[:comment_end+2]
|
||||
else:
|
||||
comment = None
|
||||
|
||||
parsed = self._parse_doxygen_block(comment)
|
||||
|
||||
# 构建函数签名
|
||||
func_sig = f"{ret_type} {name}({match.group(3).strip()});"
|
||||
|
||||
self.items.append(DocItem(
|
||||
name=name,
|
||||
kind='function',
|
||||
brief=parsed['brief'],
|
||||
params=parsed['params'],
|
||||
returns=parsed['returns'],
|
||||
code=func_sig,
|
||||
notes=parsed['notes'],
|
||||
examples=parsed['examples'],
|
||||
))
|
||||
|
||||
|
||||
class RstGenerator:
|
||||
"""生成 RST 文档"""
|
||||
|
||||
def __init__(self, module_name: str, title: str):
|
||||
self.module_name = module_name
|
||||
self.title = title
|
||||
self.items: List[DocItem] = []
|
||||
|
||||
def add_items(self, items: List[DocItem]):
|
||||
self.items.extend(items)
|
||||
|
||||
@staticmethod
|
||||
def _underline(text: str, char: str) -> str:
|
||||
return char * max(len(text), 3)
|
||||
|
||||
def generate(self) -> str:
|
||||
"""生成 RST 内容"""
|
||||
lines = []
|
||||
|
||||
# 标题
|
||||
lines.append(self.title)
|
||||
lines.append(self._underline(self.title, '='))
|
||||
lines.append('')
|
||||
|
||||
# 按类型分组
|
||||
macros = [i for i in self.items if i.kind == 'macro']
|
||||
typedefs = [i for i in self.items if i.kind == 'typedef']
|
||||
enums = [i for i in self.items if i.kind == 'enum']
|
||||
structs = [i for i in self.items if i.kind == 'struct']
|
||||
functions = [i for i in self.items if i.kind == 'function']
|
||||
|
||||
# 类型定义
|
||||
if typedefs or enums or structs:
|
||||
lines.append('Types')
|
||||
lines.append(self._underline('Types', '-'))
|
||||
lines.append('')
|
||||
|
||||
for item in typedefs:
|
||||
lines.extend(self._format_type(item))
|
||||
|
||||
for item in enums:
|
||||
lines.extend(self._format_type(item))
|
||||
|
||||
for item in structs:
|
||||
lines.extend(self._format_type(item))
|
||||
|
||||
# 宏
|
||||
if macros:
|
||||
lines.append('Macros')
|
||||
lines.append(self._underline('Macros', '-'))
|
||||
lines.append('')
|
||||
|
||||
for item in macros:
|
||||
lines.extend(self._format_macro(item))
|
||||
|
||||
# 函数
|
||||
if functions:
|
||||
lines.append('Functions')
|
||||
lines.append(self._underline('Functions', '-'))
|
||||
lines.append('')
|
||||
|
||||
for item in functions:
|
||||
lines.extend(self._format_function(item))
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _format_type(self, item: DocItem) -> List[str]:
|
||||
lines = []
|
||||
lines.append(f'{item.name}')
|
||||
lines.append('~' * len(item.name))
|
||||
lines.append('')
|
||||
if item.brief:
|
||||
lines.append(item.brief)
|
||||
lines.append('')
|
||||
lines.append('.. code-block:: c')
|
||||
lines.append('')
|
||||
for code_line in item.code.split('\n'):
|
||||
lines.append(f' {code_line}')
|
||||
lines.append('')
|
||||
return lines
|
||||
|
||||
def _format_macro(self, item: DocItem) -> List[str]:
|
||||
lines = []
|
||||
lines.append(f'{item.name}()')
|
||||
lines.append('~' * (len(item.name) + 2))
|
||||
lines.append('')
|
||||
if item.brief:
|
||||
lines.append(item.brief)
|
||||
lines.append('')
|
||||
lines.append('.. code-block:: c')
|
||||
lines.append('')
|
||||
for code_line in item.code.split('\n'):
|
||||
lines.append(f' {code_line}')
|
||||
lines.append('')
|
||||
return lines
|
||||
|
||||
def _format_function(self, item: DocItem) -> List[str]:
|
||||
lines = []
|
||||
lines.append(f'{item.name}()')
|
||||
lines.append('~' * (len(item.name) + 2))
|
||||
lines.append('')
|
||||
if item.brief:
|
||||
lines.append(item.brief)
|
||||
lines.append('')
|
||||
|
||||
lines.append('.. code-block:: c')
|
||||
lines.append('')
|
||||
lines.append(f' {item.code}')
|
||||
lines.append('')
|
||||
|
||||
if item.params:
|
||||
lines.append('**Parameters:**')
|
||||
lines.append('')
|
||||
for param in item.params:
|
||||
lines.append(f"* ``{param['name']}`` - {param['desc']}")
|
||||
lines.append('')
|
||||
|
||||
if item.returns:
|
||||
lines.append('**Returns:**')
|
||||
lines.append('')
|
||||
lines.append(f'* {item.returns}')
|
||||
lines.append('')
|
||||
|
||||
for note in item.notes:
|
||||
lines.append('**Note:**')
|
||||
lines.append('')
|
||||
lines.append(note)
|
||||
lines.append('')
|
||||
|
||||
for example in item.examples:
|
||||
lines.append('**Example:**')
|
||||
lines.append('')
|
||||
lines.append('.. code-block:: c')
|
||||
lines.append('')
|
||||
for code_line in example.split('\n'):
|
||||
lines.append(f' {code_line}')
|
||||
lines.append('')
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
TITLE_OVERRIDES = {
|
||||
'gfx_core': 'Core System',
|
||||
'gfx_types': 'Types',
|
||||
'gfx_disp': 'Display',
|
||||
'gfx_touch': 'Touch',
|
||||
'gfx_obj': 'Object',
|
||||
'gfx_timer': 'Timer',
|
||||
'gfx_img': 'Image',
|
||||
'gfx_label': 'Label',
|
||||
'gfx_anim': 'Animation',
|
||||
'gfx_qrcode': 'QR Code',
|
||||
'gfx_button': 'Button',
|
||||
'gfx_font_lvgl': 'LVGL Font Compatibility',
|
||||
}
|
||||
|
||||
# 各 API 子目录的 index 配置:(子目录名, 页面标题, 引言段落, “模块列表”小节标题)
|
||||
INDEX_CONFIG = [
|
||||
(
|
||||
'core',
|
||||
'Core API Reference',
|
||||
'The core API provides the foundation for the graphics framework, including initialization, object management, and basic types.',
|
||||
'Core Modules',
|
||||
),
|
||||
(
|
||||
'widgets',
|
||||
'Widget API Reference',
|
||||
'The widget API provides specialized functionality for different types of graphical elements.',
|
||||
'Widget Modules',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def stem_to_display_name(stem: str) -> str:
|
||||
"""Convert `gfx_button` to `Button` as a fallback title."""
|
||||
name = stem
|
||||
if name.startswith('gfx_'):
|
||||
name = name[4:]
|
||||
return name.replace('_', ' ').title()
|
||||
|
||||
|
||||
def title_for_header(stem: str) -> str:
|
||||
if stem in TITLE_OVERRIDES:
|
||||
return f"{TITLE_OVERRIDES[stem]} ({stem})"
|
||||
fallback = stem_to_display_name(stem)
|
||||
return f"{fallback} ({stem})"
|
||||
|
||||
|
||||
def discover_header_mapping(repo_root: Path) -> List[Tuple[str, str, str]]:
|
||||
mapping: List[Tuple[str, str, str]] = []
|
||||
search_roots = [
|
||||
('include/core', 'api/core'),
|
||||
('include/widget', 'api/widgets'),
|
||||
]
|
||||
|
||||
for header_dir, rst_dir in search_roots:
|
||||
full_dir = repo_root / header_dir
|
||||
if not full_dir.is_dir():
|
||||
continue
|
||||
|
||||
for header_path in sorted(full_dir.glob('*.h')):
|
||||
stem = header_path.stem
|
||||
rel_header = str(Path(header_dir) / header_path.name)
|
||||
rel_rst = str(Path(rst_dir) / f'{stem}.rst')
|
||||
mapping.append((rel_header, rel_rst, title_for_header(stem)))
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def md_to_rst_line(line: str) -> str:
|
||||
"""Convert a single Markdown line to RST (headers and list items)."""
|
||||
s = line.rstrip()
|
||||
# [text](url) -> `text <url>`_
|
||||
s = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'`\1 <\2>`_', s)
|
||||
if s.startswith('# '):
|
||||
title = s[2:].strip()
|
||||
return title + '\n' + '=' * len(title)
|
||||
if s.startswith('## '):
|
||||
title = s[3:].strip()
|
||||
return title + '\n' + '-' * len(title)
|
||||
if s.startswith('### '):
|
||||
title = s[4:].strip()
|
||||
return title + '\n' + '~' * len(title)
|
||||
if s.startswith('- ') and not s.startswith('- [ ]'):
|
||||
return '* ' + s[2:]
|
||||
return s
|
||||
|
||||
|
||||
def generate_changelog_rst(repo_root: Path, docs_dir: Path) -> bool:
|
||||
"""Read CHANGELOG.md and write docs/changelog.rst (MD to RST conversion)."""
|
||||
md_path = repo_root / 'CHANGELOG.md'
|
||||
rst_path = docs_dir / 'changelog.rst'
|
||||
if not md_path.is_file():
|
||||
return False
|
||||
with open(md_path, 'r', encoding='utf-8') as f:
|
||||
md_lines = f.readlines()
|
||||
rst_lines = []
|
||||
for line in md_lines:
|
||||
if not line.strip():
|
||||
rst_lines.append('')
|
||||
continue
|
||||
rst_lines.append(md_to_rst_line(line))
|
||||
rst_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(rst_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(rst_lines))
|
||||
f.write('\n')
|
||||
return True
|
||||
|
||||
|
||||
def update_index_rst(api_dir: Path, subdir: str, title: str, intro: str, modules_heading: str) -> bool:
|
||||
"""
|
||||
扫描 api/<subdir>/ 下的 .rst 文件(排除 index.rst),生成 toctree 和模块列表,写入 index.rst。
|
||||
每个条目的描述取自对应 .rst 文件的第一行(标题行)。
|
||||
返回是否写入了文件。
|
||||
"""
|
||||
def underline(text: str, char: str) -> str:
|
||||
return char * max(len(text), 3)
|
||||
|
||||
index_dir = api_dir / subdir
|
||||
if not index_dir.is_dir():
|
||||
return False
|
||||
|
||||
rst_files = sorted(
|
||||
f.stem for f in index_dir.glob('*.rst') if f.name != 'index.rst'
|
||||
)
|
||||
if not rst_files:
|
||||
return False
|
||||
|
||||
# 从每个 .rst 读取第一行作为描述
|
||||
descriptions = {}
|
||||
for stem in rst_files:
|
||||
rst_path = index_dir / f'{stem}.rst'
|
||||
try:
|
||||
with open(rst_path, 'r', encoding='utf-8') as f:
|
||||
first = f.readline()
|
||||
descriptions[stem] = first.strip() if first else stem
|
||||
except OSError:
|
||||
descriptions[stem] = stem
|
||||
|
||||
lines = [
|
||||
title,
|
||||
underline(title, '='),
|
||||
'',
|
||||
intro,
|
||||
'',
|
||||
'.. toctree::',
|
||||
' :maxdepth: 2',
|
||||
'',
|
||||
]
|
||||
for stem in rst_files:
|
||||
lines.append(f' {stem}')
|
||||
lines.extend(['', modules_heading, underline(modules_heading, '-'), ''])
|
||||
for stem in rst_files:
|
||||
desc = descriptions[stem]
|
||||
lines.append(f'* :doc:`{stem}` - {desc}')
|
||||
lines.append('')
|
||||
|
||||
index_path = index_dir / 'index.rst'
|
||||
with open(index_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
return True
|
||||
|
||||
|
||||
def run(args) -> int:
|
||||
# 确定项目根目录
|
||||
script_dir = Path(__file__).parent
|
||||
repo_root = script_dir.parent.parent
|
||||
|
||||
if not args.quiet:
|
||||
print("=" * 50)
|
||||
print(" ESP Emote GFX API 文档生成器")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
header_mapping = discover_header_mapping(repo_root)
|
||||
|
||||
generated = 0
|
||||
for header_path, rst_path, title in header_mapping:
|
||||
if args.header and args.header not in header_path:
|
||||
continue
|
||||
|
||||
full_header_path = repo_root / header_path
|
||||
full_rst_path = repo_root / args.output_dir / rst_path
|
||||
|
||||
if not full_header_path.exists():
|
||||
if not args.quiet:
|
||||
print(f"⚠ 跳过: {header_path} (文件不存在)")
|
||||
continue
|
||||
|
||||
if not args.quiet:
|
||||
print(f"处理: {header_path}")
|
||||
print(f" → {rst_path}")
|
||||
|
||||
# 解析头文件
|
||||
parser_obj = HeaderParser(str(full_header_path))
|
||||
items = parser_obj.parse()
|
||||
|
||||
# 统计各类型数量
|
||||
counts = {}
|
||||
for item in items:
|
||||
counts[item.kind] = counts.get(item.kind, 0) + 1
|
||||
|
||||
count_str = ', '.join(f"{v} {k}" for k, v in counts.items())
|
||||
if not args.quiet:
|
||||
print(f" 发现: {count_str}")
|
||||
|
||||
# 生成 RST
|
||||
generator = RstGenerator(header_path, title)
|
||||
generator.add_items(items)
|
||||
rst_content = generator.generate()
|
||||
|
||||
if args.dry_run:
|
||||
if not args.quiet:
|
||||
print(f" [dry-run] 将生成 {len(rst_content)} 字节")
|
||||
else:
|
||||
full_rst_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(full_rst_path, 'w', encoding='utf-8') as f:
|
||||
f.write(rst_content)
|
||||
if not args.quiet:
|
||||
print(f" ✓ 已生成")
|
||||
|
||||
generated += 1
|
||||
if not args.quiet:
|
||||
print()
|
||||
|
||||
# 根据 api/core 和 api/widgets 下的 .rst 自动更新 index.rst
|
||||
api_dir = repo_root / args.output_dir / 'api'
|
||||
if not args.dry_run and api_dir.is_dir():
|
||||
for subdir, title, intro, modules_heading in INDEX_CONFIG:
|
||||
index_dir = api_dir / subdir
|
||||
if index_dir.is_dir():
|
||||
if update_index_rst(api_dir, subdir, title, intro, modules_heading):
|
||||
if not args.quiet:
|
||||
print(f"更新索引: api/{subdir}/index.rst")
|
||||
if not args.quiet:
|
||||
print()
|
||||
|
||||
# 从 CHANGELOG.md 生成 docs/changelog.rst
|
||||
docs_dir = repo_root / args.output_dir
|
||||
if not args.dry_run:
|
||||
if generate_changelog_rst(repo_root, docs_dir):
|
||||
if not args.quiet:
|
||||
print("更新: changelog.rst (来自 CHANGELOG.md)")
|
||||
if not args.quiet:
|
||||
print()
|
||||
|
||||
if not args.quiet:
|
||||
print("=" * 50)
|
||||
print(f"完成! 共处理 {generated} 个文件")
|
||||
print("=" * 50)
|
||||
|
||||
return generated
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='从 C 头文件生成 RST 文档')
|
||||
parser.add_argument('--output-dir', '-o', default='docs',
|
||||
help='输出目录 (默认: docs)')
|
||||
parser.add_argument('--dry-run', '-n', action='store_true',
|
||||
help='只显示将要生成的文件,不实际写入')
|
||||
parser.add_argument('--header', '-H',
|
||||
help='只处理指定的头文件')
|
||||
parser.add_argument('--quiet', '-q', action='store_true',
|
||||
help='静默模式,仅在失败时输出')
|
||||
args = parser.parse_args()
|
||||
run(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
BUILD_SPHINX=1
|
||||
BUILD_API_RST=1
|
||||
BUILD_DOXYGEN=1
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-sphinx)
|
||||
BUILD_SPHINX=0
|
||||
;;
|
||||
--skip-api-rst)
|
||||
BUILD_API_RST=0
|
||||
;;
|
||||
--skip-doxygen)
|
||||
BUILD_DOXYGEN=0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
DOC_BUILD_ROOT="docs/_build/html"
|
||||
DOXYGEN_DIR="${DOC_BUILD_ROOT}/doxygen"
|
||||
ASSETS_DIR="${DOC_BUILD_ROOT}/assets"
|
||||
DOXYFILE_PATH="docs/_build/Doxyfile"
|
||||
mkdir -p "$DOC_BUILD_ROOT" "$ASSETS_DIR" "${DOC_BUILD_ROOT}/en" "${DOC_BUILD_ROOT}/zh_CN" "$(dirname "$DOXYFILE_PATH")"
|
||||
|
||||
if [[ "$BUILD_API_RST" -eq 1 ]]; then
|
||||
echo "[docs] Generating API RST sources..."
|
||||
python3 docs/scripts/generate_api_docs.py --output-dir docs --quiet
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_SPHINX" -eq 1 ]]; then
|
||||
echo "[docs] Extracting gettext messages..."
|
||||
python3 -m sphinx -b gettext -d docs/_build/doctrees-gettext docs docs/_build/gettext
|
||||
echo "[docs] Building zh_CN message catalogs (.po/.mo)..."
|
||||
python3 docs/scripts/sync_locale_zh.py
|
||||
|
||||
echo "[docs] Building Sphinx HTML (en)..."
|
||||
python3 -m sphinx -b html -d docs/_build/doctrees-en -D language=en docs "${DOC_BUILD_ROOT}/en"
|
||||
|
||||
echo "[docs] Building Sphinx HTML (zh_CN)..."
|
||||
python3 -m sphinx -b html -d docs/_build/doctrees-zh -D language=zh_CN docs "${DOC_BUILD_ROOT}/zh_CN"
|
||||
fi
|
||||
|
||||
# Root landing: language hub (no mixed-language pages; pick EN or 中文)
|
||||
cat <<'EOF' > "${DOC_BUILD_ROOT}/index.html"
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ESP Emote GFX Documentation</title>
|
||||
<meta http-equiv="refresh" content="0; url=en/index.html">
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
margin: 2rem; line-height: 1.6; background: #f7f7f8; color: #1a1a1a; }
|
||||
a { color: #c41e1a; text-decoration: none; font-weight: 600; }
|
||||
a:hover { text-decoration: underline; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p><strong>ESP Emote GFX</strong> — choose documentation language / 选择文档语言:</p>
|
||||
<ul>
|
||||
<li><a href="en/index.html">English (EN)</a></li>
|
||||
<li><a href="zh_CN/index.html">简体中文 (ZH)</a></li>
|
||||
</ul>
|
||||
<p>Redirecting to English… / 正在跳转至英文版…</p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
# Create a build-local Doxyfile so docs generation does not touch the repo root.
|
||||
cat <<'EOF' > "$DOXYFILE_PATH"
|
||||
PROJECT_NAME = esp_emote_gfx
|
||||
OUTPUT_DIRECTORY = docs/doxygen_output
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
INPUT = . src include components
|
||||
FILE_PATTERNS = *.h *.hpp *.c *.cpp
|
||||
RECURSIVE = YES
|
||||
EXTRACT_ALL = YES
|
||||
FULL_PATH_NAMES = NO
|
||||
GENERATE_LATEX = NO
|
||||
WARN_IF_UNDOCUMENTED = NO
|
||||
QUIET = YES
|
||||
EOF
|
||||
|
||||
if [[ "$BUILD_DOXYGEN" -eq 1 ]] && ! command -v doxygen >/dev/null 2>&1; then
|
||||
echo "Warning: doxygen not found, Doxygen API docs will be skipped"
|
||||
fi
|
||||
|
||||
rm -rf "$DOXYGEN_DIR"
|
||||
mkdir -p "$DOXYGEN_DIR"
|
||||
|
||||
if [[ "$BUILD_DOXYGEN" -eq 1 ]] && command -v doxygen >/dev/null 2>&1; then
|
||||
doxygen "$DOXYFILE_PATH"
|
||||
if [ -d docs/doxygen_output/html ]; then
|
||||
cp -r docs/doxygen_output/html/. "$DOXYGEN_DIR"/
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$DOXYGEN_DIR/index.html" ]; then
|
||||
cat <<'EOF' > "$DOXYGEN_DIR/index.html"
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Doxygen API Reference</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 2rem; line-height: 1.6; background: #f7f7f8; }
|
||||
a { color: #c41e1a; text-decoration: none; font-weight: 600; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Doxygen API Reference</h1>
|
||||
<p>Doxygen documentation was not generated. Please check the build logs.</p>
|
||||
<p><a href="../en/index.html">← English docs</a> · <a href="../zh_CN/index.html">← 中文文档</a></p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat <<'EOF' > "$ASSETS_DIR/espidf.css"
|
||||
:root { --bg:#f7f7f8; --text:#1a1a1a; --accent:#c41e1a; --muted:#6a737d; --border:#d0d4d8; --code-bg:#f0f2f4; }
|
||||
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,"Noto Sans",sans-serif; }
|
||||
a { color:var(--accent); text-decoration:none; font-weight:600; } a:hover { text-decoration:underline; }
|
||||
pre, code { background:var(--code-bg); border:1px solid var(--border); border-radius:4px; padding:.25rem .5rem; }
|
||||
.header,.headertitle,.navpath,.footer,.memitem,.memdoc,.memberdecls,.directory { border-color:var(--border)!important; }
|
||||
.memname { font-weight:600; } .mdescLeft,.mdescRight,.qindex { color:var(--muted); }
|
||||
EOF
|
||||
|
||||
if [ -d "$DOXYGEN_DIR" ]; then
|
||||
cp "$ASSETS_DIR/espidf.css" "$DOXYGEN_DIR/espidf.css"
|
||||
python3 <<'PY'
|
||||
import os, io
|
||||
root = os.path.join("docs", "_build", "html", "doxygen")
|
||||
css = '<link rel="stylesheet" href="espidf.css" />'
|
||||
if not os.path.isdir(root):
|
||||
raise SystemExit(0)
|
||||
for dirpath, _, files in os.walk(root):
|
||||
for name in files:
|
||||
if not name.endswith(".html"):
|
||||
continue
|
||||
path = os.path.join(dirpath, name)
|
||||
with io.open(path, "r", encoding="utf-8", errors="ignore") as fh:
|
||||
html = fh.read()
|
||||
if "espidf.css" in html:
|
||||
continue
|
||||
html = html.replace("</head>", css + "</head>", 1) if "</head>" in html else css + html
|
||||
with io.open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(html)
|
||||
PY
|
||||
fi
|
||||
|
||||
echo "Documentation post-processing complete."
|
||||
echo " - Sphinx EN: ${DOC_BUILD_ROOT}/en/"
|
||||
echo " - Sphinx ZH: ${DOC_BUILD_ROOT}/zh_CN/"
|
||||
echo " - Landing: ${DOC_BUILD_ROOT}/index.html"
|
||||
echo " - Doxygen: ${DOXYGEN_DIR}/"
|
||||
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build zh_CN .po/.mo from gettext .pot files + docs/locale/zh_CN/translation_data.py
|
||||
|
||||
Requires: Babel (usually installed with Sphinx). Requires docs/_build/gettext/*.pot
|
||||
(run: sphinx-build -b gettext docs docs/_build/gettext).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from babel.messages.mofile import write_mo
|
||||
from babel.messages.pofile import read_po, write_po
|
||||
except ImportError as e:
|
||||
print("Babel is required (pip install Babel).", file=sys.stderr)
|
||||
raise SystemExit(1) from e
|
||||
|
||||
|
||||
def main() -> int:
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
docs_dir = script_dir.parent
|
||||
repo_root = docs_dir.parent
|
||||
gettext_dir = docs_dir / "_build" / "gettext"
|
||||
locale_msgs = docs_dir / "locale" / "zh_CN" / "LC_MESSAGES"
|
||||
locale_msgs.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sys.path.insert(0, str(docs_dir / "locale" / "zh_CN"))
|
||||
from translation_data import TRANSLATIONS_BY_CATALOG # type: ignore
|
||||
|
||||
if not gettext_dir.is_dir():
|
||||
print(f"Missing {gettext_dir}; run sphinx-build -b gettext first.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
for pot_path in sorted(gettext_dir.glob("*.pot")):
|
||||
name = pot_path.stem
|
||||
catalog = read_po(io.open(pot_path, encoding="utf-8"))
|
||||
catalog.locale = "zh_CN"
|
||||
catalog.fuzzy = False
|
||||
|
||||
trans = TRANSLATIONS_BY_CATALOG.get(name, {})
|
||||
for msg in catalog:
|
||||
if not msg.id:
|
||||
continue
|
||||
if isinstance(msg.id, (list, tuple)):
|
||||
continue
|
||||
if msg.id in trans:
|
||||
msg.string = trans[msg.id]
|
||||
|
||||
out_po = locale_msgs / f"{name}.po"
|
||||
buf_po = io.BytesIO()
|
||||
write_po(buf_po, catalog, omit_header=False, width=79)
|
||||
out_po.write_text(buf_po.getvalue().decode("utf-8"), encoding="utf-8")
|
||||
|
||||
buf = io.BytesIO()
|
||||
write_mo(buf, catalog)
|
||||
mo_path = locale_msgs / f"{name}.mo"
|
||||
mo_path.write_bytes(buf.getvalue())
|
||||
print(f" wrote {out_po.relative_to(repo_root)} + .mo")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -0,0 +1,28 @@
|
||||
dependencies:
|
||||
cmake_utilities:
|
||||
version: 0.*
|
||||
espressif/esp_lcd_touch:
|
||||
public: true
|
||||
version: '>=1.0'
|
||||
espressif/esp_new_jpeg:
|
||||
public: true
|
||||
version: 1.*
|
||||
espressif/freetype:
|
||||
version: 2.*
|
||||
idf:
|
||||
version: '>=5.0'
|
||||
laride/heatshrink:
|
||||
version: ^0.4.1
|
||||
lvgl/lvgl:
|
||||
public: true
|
||||
version: '*'
|
||||
description: ESP Emote GFX - A lightweight UI graphics library for compact embedded
|
||||
displays.
|
||||
documentation: https://espressif2022.github.io/esp_emote_gfx/en/index.html
|
||||
issues: https://github.com/espressif2022/esp_emote_gfx/issues
|
||||
repository: git://github.com/espressif2022/esp_emote_gfx.git
|
||||
repository_info:
|
||||
commit_sha: 47f9fd04a44d5d2b5dba8c14f73e4fc0f76b21f9
|
||||
path: .
|
||||
url: https://github.com/espressif2022/esp_emote_gfx
|
||||
version: 3.0.5
|
||||
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/**
|
||||
* Fractional bits for mesh vertex coordinates (gfx_mesh_img_point_q8_t x_q8/y_q8)
|
||||
* and for the software triangle rasterizer.
|
||||
*/
|
||||
#define GFX_MESH_FRAC_SHIFT 8
|
||||
|
||||
#define GFX_MESH_FRAC_ONE (1 << GFX_MESH_FRAC_SHIFT)
|
||||
#define GFX_MESH_FRAC_HALF (1 << (GFX_MESH_FRAC_SHIFT - 1))
|
||||
#define GFX_MESH_FRAC_MASK (GFX_MESH_FRAC_ONE - 1)
|
||||
|
||||
/**
|
||||
* Triangle outer-edge AA distance threshold, same units as vertex coordinates.
|
||||
* Kconfig 0 means one logical pixel (GFX_MESH_FRAC_ONE).
|
||||
*/
|
||||
#if defined(CONFIG_GFX_BLEND_TRI_EDGE_AA_RANGE) && (CONFIG_GFX_BLEND_TRI_EDGE_AA_RANGE > 0)
|
||||
#define GFX_BLEND_TRI_EDGE_AA_RANGE (CONFIG_GFX_BLEND_TRI_EDGE_AA_RANGE)
|
||||
#else
|
||||
#define GFX_BLEND_TRI_EDGE_AA_RANGE (GFX_MESH_FRAC_ONE)
|
||||
#endif
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "gfx_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
/** Use as .task = GFX_EMOTE_INIT_CONFIG() when initializing gfx_core_config_t */
|
||||
#define GFX_EMOTE_INIT_CONFIG() \
|
||||
{ \
|
||||
.task_priority = 4, \
|
||||
.task_stack = 7168, \
|
||||
.task_affinity = -1, \
|
||||
.task_stack_caps = MALLOC_CAP_DEFAULT, \
|
||||
}
|
||||
|
||||
/*********************
|
||||
* TYPEDEFS
|
||||
*********************/
|
||||
/** Passed to gfx_emote_init(); add displays with gfx_disp_add() after init */
|
||||
typedef struct {
|
||||
uint32_t fps; /**< Target FPS (frames per second) */
|
||||
struct {
|
||||
int task_priority; /**< Render task priority (1–20) */
|
||||
int task_stack; /**< Render task stack size (bytes) */
|
||||
int task_affinity; /**< CPU core (-1: any, 0/1: pinned) */
|
||||
unsigned task_stack_caps; /**< Stack heap caps (see esp_heap_caps.h) */
|
||||
} task;
|
||||
} gfx_core_config_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Initialize graphics context
|
||||
*
|
||||
* @param cfg Core configuration (gfx_core_config_t): fps, task. Add displays with gfx_disp_add() and gfx_disp_config_t.
|
||||
* @return gfx_handle_t Graphics handle, NULL on error
|
||||
*
|
||||
* @note gfx_core_config_t fields: fps, task (priority, stack, affinity, stack_caps).
|
||||
* Resolution, buffers and flush callback are per-display; see gfx_disp_config_t and gfx_disp_add().
|
||||
*/
|
||||
gfx_handle_t gfx_emote_init(const gfx_core_config_t *cfg);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize graphics context
|
||||
*
|
||||
* @param handle Graphics handle
|
||||
*/
|
||||
void gfx_emote_deinit(gfx_handle_t handle);
|
||||
|
||||
/**
|
||||
* @brief Lock the recursive render mutex to prevent rendering during external operations
|
||||
*
|
||||
* @param handle Graphics handle
|
||||
* @return esp_err_t ESP_OK on success, otherwise an error code
|
||||
*/
|
||||
esp_err_t gfx_emote_lock(gfx_handle_t handle);
|
||||
|
||||
/**
|
||||
* @brief Unlock the recursive render mutex after external operations
|
||||
*
|
||||
* @param handle Graphics handle
|
||||
* @return esp_err_t ESP_OK on success, otherwise an error code
|
||||
*/
|
||||
esp_err_t gfx_emote_unlock(gfx_handle_t handle);
|
||||
|
||||
/**
|
||||
* @brief Perform one synchronous refresh (render and flush) immediately.
|
||||
* Holds the render mutex for the duration; safe to call from any task.
|
||||
*
|
||||
* @param handle Graphics handle
|
||||
* @return esp_err_t ESP_OK on success, otherwise an error code
|
||||
*/
|
||||
esp_err_t gfx_refr_now(gfx_handle_t handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "gfx_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* TYPEDEFS
|
||||
*********************/
|
||||
/** Display handle: one per screen; from gfx_disp_add(), use with all gfx_disp_* APIs */
|
||||
typedef struct gfx_disp gfx_disp_t;
|
||||
typedef enum {
|
||||
GFX_DISP_EVENT_IDLE = 0,
|
||||
GFX_DISP_EVENT_ONE_FRAME_DONE,
|
||||
GFX_DISP_EVENT_PART_FRAME_DONE,
|
||||
GFX_DISP_EVENT_ALL_FRAME_DONE,
|
||||
} gfx_disp_event_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t calls; /**< Number of API calls */
|
||||
uint64_t pixels; /**< Processed pixels */
|
||||
uint64_t time_us; /**< Elapsed time in microseconds */
|
||||
} gfx_perf_counter_t;
|
||||
|
||||
typedef struct {
|
||||
gfx_perf_counter_t fill; /**< gfx_sw_blend_fill_area */
|
||||
gfx_perf_counter_t color_draw; /**< gfx_sw_blend_draw */
|
||||
gfx_perf_counter_t image_draw; /**< gfx_sw_blend_img_draw */
|
||||
gfx_perf_counter_t triangle_draw; /**< gfx_sw_blend_img_triangle_draw */
|
||||
uint64_t triangle_covered_pixels; /**< Triangle pixels blended (inside + AA) */
|
||||
uint64_t triangle_aa_pixels; /**< Triangle edge-AA blended pixels */
|
||||
} gfx_blend_perf_stats_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t dirty_pixels; /**< Dirty pixels in the latest rendered frame */
|
||||
uint64_t frame_time_us; /**< Total frame time */
|
||||
uint64_t render_time_us; /**< Time spent in render phase */
|
||||
uint64_t flush_time_us; /**< Time spent in flush callbacks */
|
||||
uint32_t flush_count; /**< Number of flush calls */
|
||||
gfx_blend_perf_stats_t blend; /**< Blend-stage details */
|
||||
} gfx_disp_perf_stats_t;
|
||||
|
||||
typedef void (*gfx_disp_flush_cb_t)(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data);
|
||||
typedef void (*gfx_disp_update_cb_t)(gfx_disp_t *disp, gfx_disp_event_t event, const void *obj);
|
||||
|
||||
/*********************
|
||||
* CONFIG STRUCTS
|
||||
*********************/
|
||||
/** Passed to gfx_disp_add() for multi-screen setup */
|
||||
typedef struct {
|
||||
uint32_t h_res; /**< Screen width in pixels */
|
||||
uint32_t v_res; /**< Screen height in pixels */
|
||||
gfx_disp_flush_cb_t flush_cb; /**< Flush callback for this display */
|
||||
gfx_disp_update_cb_t update_cb; /**< Update callback (frame/playback events) */
|
||||
void *user_data; /**< User data for this display */
|
||||
struct {
|
||||
unsigned char swap : 1; /**< Color swap flag */
|
||||
unsigned char buff_dma : 1; /**< Alloc buffer with MALLOC_CAP_DMA (internal alloc only) */
|
||||
unsigned char buff_spiram : 1; /**< Alloc buffer in PSRAM (internal alloc only) */
|
||||
unsigned char double_buffer : 1; /**< Alloc second buffer for double buffering (internal alloc only) */
|
||||
unsigned char full_frame : 1; /**< 1 = buf1/buf2 are full-screen framebuffers (e.g. RGB); draw at chunk region. 0 = partition buffer; draw from start. */
|
||||
} flags;
|
||||
struct {
|
||||
void *buf1; /**< Frame buffer 1 (NULL = internal alloc) */
|
||||
void *buf2; /**< Frame buffer 2 (NULL = internal alloc) */
|
||||
size_t buf_pixels; /**< Size per buffer in pixels (0 = auto) */
|
||||
} buffers;
|
||||
} gfx_disp_config_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Add a display (multi-screen support)
|
||||
*
|
||||
* @param handle Graphics handle from gfx_emote_init
|
||||
* @param cfg Display configuration (resolution, flush callback, buffers)
|
||||
* @return gfx_disp_t* New display pointer on success, NULL on error
|
||||
*/
|
||||
gfx_disp_t *gfx_disp_add(gfx_handle_t handle, const gfx_disp_config_t *cfg);
|
||||
|
||||
/**
|
||||
* @brief Remove a display from the list and release its resources (child list nodes, event group, buffers).
|
||||
* Does not free the gfx_disp_t; caller must free(disp) after.
|
||||
*
|
||||
* @param disp Display from gfx_disp_add; safe to pass NULL
|
||||
*/
|
||||
void gfx_disp_del(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Invalidate full screen of a display to trigger refresh
|
||||
*
|
||||
* @param disp Display from gfx_disp_add
|
||||
*/
|
||||
void gfx_disp_refresh_all(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Notify that flush is done (e.g. from panel IO callback)
|
||||
*
|
||||
* @param disp Display from gfx_disp_add
|
||||
* @param swap_act_buf Whether to swap the active buffer
|
||||
* @return bool True on success
|
||||
*/
|
||||
bool gfx_disp_flush_ready(gfx_disp_t *disp, bool swap_act_buf);
|
||||
|
||||
/**
|
||||
* @brief Get user data for a display
|
||||
*
|
||||
* @param disp Display from gfx_disp_add
|
||||
* @return void* User data, or NULL
|
||||
*/
|
||||
void *gfx_disp_get_user_data(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Get display horizontal resolution in pixels
|
||||
*
|
||||
* @param disp Display from gfx_disp_add (NULL allowed; returns default width)
|
||||
* @return uint32_t Width in pixels
|
||||
*/
|
||||
uint32_t gfx_disp_get_hor_res(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Get display vertical resolution in pixels
|
||||
*
|
||||
* @param disp Display from gfx_disp_add (NULL allowed; returns default height)
|
||||
* @return uint32_t Height in pixels
|
||||
*/
|
||||
uint32_t gfx_disp_get_ver_res(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Check if display is currently flushing the last block
|
||||
*
|
||||
* @param disp Display from gfx_disp_add
|
||||
* @return true if flushing last block, false otherwise
|
||||
*/
|
||||
bool gfx_disp_is_flushing_last(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Get latest per-display performance statistics
|
||||
*
|
||||
* Stats are updated once a frame is rendered. If no frame has rendered yet,
|
||||
* all fields remain zero.
|
||||
*
|
||||
* @param disp Display handle
|
||||
* @param out_stats Output stats structure
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t gfx_disp_get_perf_stats(gfx_disp_t *disp, gfx_disp_perf_stats_t *out_stats);
|
||||
|
||||
/**
|
||||
* @brief Set default background color for a display
|
||||
*
|
||||
* @param disp Display from gfx_disp_add
|
||||
* @param color Background color (e.g. RGB565)
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t gfx_disp_set_bg_color(gfx_disp_t *disp, gfx_color_t color);
|
||||
|
||||
/**
|
||||
* @brief Enable or disable drawing the background (fill with bg_color before widgets)
|
||||
*
|
||||
* @param disp Display from gfx_disp_add
|
||||
* @param enable true to enable background (default), false to disable background
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t gfx_disp_set_bg_enable(gfx_disp_t *disp, bool enable);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
typedef enum {
|
||||
GFX_LOG_LEVEL_NONE = 0,
|
||||
GFX_LOG_LEVEL_ERROR,
|
||||
GFX_LOG_LEVEL_WARN,
|
||||
GFX_LOG_LEVEL_INFO,
|
||||
GFX_LOG_LEVEL_DEBUG,
|
||||
GFX_LOG_LEVEL_VERBOSE,
|
||||
} gfx_log_level_t;
|
||||
|
||||
typedef enum {
|
||||
GFX_LOG_MODULE_CORE = 0,
|
||||
GFX_LOG_MODULE_DISP,
|
||||
GFX_LOG_MODULE_OBJ,
|
||||
GFX_LOG_MODULE_REFR,
|
||||
GFX_LOG_MODULE_RENDER,
|
||||
GFX_LOG_MODULE_TIMER,
|
||||
GFX_LOG_MODULE_TOUCH,
|
||||
GFX_LOG_MODULE_IMG_DEC,
|
||||
GFX_LOG_MODULE_LABEL,
|
||||
GFX_LOG_MODULE_LABEL_OBJ,
|
||||
GFX_LOG_MODULE_DRAW_LABEL,
|
||||
GFX_LOG_MODULE_FONT_LV,
|
||||
GFX_LOG_MODULE_FONT_FT,
|
||||
GFX_LOG_MODULE_IMG,
|
||||
GFX_LOG_MODULE_QRCODE,
|
||||
GFX_LOG_MODULE_BUTTON,
|
||||
GFX_LOG_MODULE_ANIM,
|
||||
GFX_LOG_MODULE_ANIM_DEC,
|
||||
GFX_LOG_MODULE_MOTION,
|
||||
GFX_LOG_MODULE_EAF_DEC,
|
||||
GFX_LOG_MODULE_QRCODE_LIB,
|
||||
GFX_LOG_MODULE_COUNT,
|
||||
} gfx_log_module_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
void gfx_log_set_level(gfx_log_module_t module, gfx_log_level_t level);
|
||||
gfx_log_level_t gfx_log_get_level(gfx_log_module_t module);
|
||||
void gfx_log_set_level_all(gfx_log_level_t level);
|
||||
bool gfx_log_should_output(gfx_log_module_t module, gfx_log_level_t level);
|
||||
const char *gfx_log_module_name(gfx_log_module_t module);
|
||||
void gfx_log_write(gfx_log_module_t module, gfx_log_level_t level, const char *tag, const char *format, ...);
|
||||
void gfx_log_writev(gfx_log_module_t module, gfx_log_level_t level, const char *tag, const char *format, va_list args);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,200 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "gfx_types.h"
|
||||
#include "core/gfx_disp.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/* Object types */
|
||||
#define GFX_OBJ_TYPE_SCREEN 0x00 /**< Screen type (reserved) */
|
||||
#define GFX_OBJ_TYPE_IMAGE 0x01
|
||||
#define GFX_OBJ_TYPE_LABEL 0x02
|
||||
#define GFX_OBJ_TYPE_ANIMATION 0x03
|
||||
#define GFX_OBJ_TYPE_QRCODE 0x04
|
||||
#define GFX_OBJ_TYPE_BUTTON 0x05
|
||||
#define GFX_OBJ_TYPE_MESH_IMAGE 0x06
|
||||
#define GFX_OBJ_TYPE_LIST 0x07
|
||||
#define GFX_OBJ_TYPE_FACE_EMOTE 0x08
|
||||
/* 0x09 reserved for removed dragon emote */
|
||||
#define GFX_OBJ_TYPE_LOBSTER_EMOTE 0x0A
|
||||
/* 0x0B reserved for removed lobster face emote */
|
||||
#define GFX_OBJ_TYPE_STICKMAN_EMOTE 0x0C
|
||||
|
||||
/* Alignment constants (similar to LVGL) */
|
||||
#define GFX_ALIGN_DEFAULT 0x00
|
||||
#define GFX_ALIGN_TOP_LEFT 0x00
|
||||
#define GFX_ALIGN_TOP_MID 0x01
|
||||
#define GFX_ALIGN_TOP_RIGHT 0x02
|
||||
#define GFX_ALIGN_LEFT_MID 0x03
|
||||
#define GFX_ALIGN_CENTER 0x04
|
||||
#define GFX_ALIGN_RIGHT_MID 0x05
|
||||
#define GFX_ALIGN_BOTTOM_LEFT 0x06
|
||||
#define GFX_ALIGN_BOTTOM_MID 0x07
|
||||
#define GFX_ALIGN_BOTTOM_RIGHT 0x08
|
||||
#define GFX_ALIGN_OUT_TOP_LEFT 0x09
|
||||
#define GFX_ALIGN_OUT_TOP_MID 0x0A
|
||||
#define GFX_ALIGN_OUT_TOP_RIGHT 0x0B
|
||||
#define GFX_ALIGN_OUT_LEFT_TOP 0x0C
|
||||
#define GFX_ALIGN_OUT_LEFT_MID 0x0D
|
||||
#define GFX_ALIGN_OUT_LEFT_BOTTOM 0x0E
|
||||
#define GFX_ALIGN_OUT_RIGHT_TOP 0x0F
|
||||
#define GFX_ALIGN_OUT_RIGHT_MID 0x10
|
||||
#define GFX_ALIGN_OUT_RIGHT_BOTTOM 0x11
|
||||
#define GFX_ALIGN_OUT_BOTTOM_LEFT 0x12
|
||||
#define GFX_ALIGN_OUT_BOTTOM_MID 0x13
|
||||
#define GFX_ALIGN_OUT_BOTTOM_RIGHT 0x14
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/* Opaque object type - actual definition in gfx_obj_priv.h */
|
||||
typedef struct gfx_obj gfx_obj_t;
|
||||
typedef struct gfx_touch_event gfx_touch_event_t;
|
||||
|
||||
/**
|
||||
* @brief Application-level touch callback (register with gfx_obj_set_touch_cb)
|
||||
* @param obj Object that received the touch
|
||||
* @param event Touch event (PRESS / RELEASE / MOVE)
|
||||
* @param user_data User data passed to gfx_obj_set_touch_cb
|
||||
*/
|
||||
typedef void (*gfx_obj_touch_cb_t)(gfx_obj_t *obj, const gfx_touch_event_t *event, void *user_data);
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Set the position of an object
|
||||
* @param obj Pointer to the object
|
||||
* @param x X coordinate
|
||||
* @param y Y coordinate
|
||||
*/
|
||||
esp_err_t gfx_obj_set_pos(gfx_obj_t *obj, gfx_coord_t x, gfx_coord_t y);
|
||||
|
||||
/**
|
||||
* @brief Set the size of an object
|
||||
* @param obj Pointer to the object
|
||||
* @param w Width
|
||||
* @param h Height
|
||||
*/
|
||||
esp_err_t gfx_obj_set_size(gfx_obj_t *obj, uint16_t w, uint16_t h);
|
||||
|
||||
/**
|
||||
* @brief Align an object relative to the screen or another object
|
||||
* @param obj Pointer to the object to align
|
||||
* @param align Alignment type (see GFX_ALIGN_* constants)
|
||||
* @param x_ofs X offset from the alignment position
|
||||
* @param y_ofs Y offset from the alignment position
|
||||
*/
|
||||
esp_err_t gfx_obj_align(gfx_obj_t *obj, uint8_t align, gfx_coord_t x_ofs, gfx_coord_t y_ofs);
|
||||
|
||||
/**
|
||||
* @brief Align an object relative to another object
|
||||
* @param obj Pointer to the object to align
|
||||
* @param base Reference object; NULL means align to the display
|
||||
* @param align Alignment type (see GFX_ALIGN_* constants)
|
||||
* @param x_ofs X offset from the alignment position
|
||||
* @param y_ofs Y offset from the alignment position
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t gfx_obj_align_to(gfx_obj_t *obj, gfx_obj_t *base, uint8_t align, gfx_coord_t x_ofs, gfx_coord_t y_ofs);
|
||||
|
||||
/**
|
||||
* @brief Set object visibility
|
||||
* @param obj Object to set visibility for
|
||||
* @param visible True to make object visible, false to hide
|
||||
*/
|
||||
esp_err_t gfx_obj_set_visible(gfx_obj_t *obj, bool visible);
|
||||
|
||||
/**
|
||||
* @brief Get object visibility
|
||||
* @param obj Object to check visibility for
|
||||
* @return True if object is visible, false if hidden
|
||||
*/
|
||||
bool gfx_obj_get_visible(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Update object's layout (mark for recalculation before rendering)
|
||||
* @param obj Object to update layout
|
||||
* @note This is used when object properties that affect layout have changed,
|
||||
* but the actual position calculation needs to be deferred until rendering
|
||||
*/
|
||||
void gfx_obj_update_layout(gfx_obj_t *obj);
|
||||
|
||||
/* Object getters */
|
||||
|
||||
/**
|
||||
* @brief Get the position of an object
|
||||
* @param obj Pointer to the object
|
||||
* @param x Pointer to store X coordinate
|
||||
* @param y Pointer to store Y coordinate
|
||||
*/
|
||||
esp_err_t gfx_obj_get_pos(gfx_obj_t *obj, gfx_coord_t *x, gfx_coord_t *y);
|
||||
|
||||
/**
|
||||
* @brief Get the size of an object
|
||||
* @param obj Pointer to the object
|
||||
* @param w Pointer to store width
|
||||
* @param h Pointer to store height
|
||||
*/
|
||||
esp_err_t gfx_obj_get_size(gfx_obj_t *obj, uint16_t *w, uint16_t *h);
|
||||
|
||||
/* Object management */
|
||||
|
||||
/**
|
||||
* @brief Delete an object
|
||||
* @param obj Pointer to the object to delete
|
||||
*/
|
||||
esp_err_t gfx_obj_delete(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Register application touch callback for an object
|
||||
*
|
||||
* When this object is the hit target of a touch (PRESS/MOVE/RELEASE), the callback
|
||||
* is invoked. Pass NULL to unregister.
|
||||
*
|
||||
* @param obj Object to listen on
|
||||
* @param cb Callback (NULL to clear)
|
||||
* @param user_data Passed to cb
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t gfx_obj_set_touch_cb(gfx_obj_t *obj, gfx_obj_touch_cb_t cb, void *user_data);
|
||||
|
||||
/**
|
||||
* @brief Get object creation sequence id (monotonic per process lifetime)
|
||||
* @param obj Object pointer
|
||||
* @return uint32_t Sequence id, 0 if obj is NULL
|
||||
*/
|
||||
uint32_t gfx_obj_get_trace_id(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Get object class name (from registered widget class metadata)
|
||||
* @param obj Object pointer
|
||||
* @return const char* Class name string, or NULL
|
||||
*/
|
||||
const char *gfx_obj_get_class_name(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Get object creation tag (creation-site annotation)
|
||||
* @param obj Object pointer
|
||||
* @return const char* Creation tag string, or NULL
|
||||
*/
|
||||
const char *gfx_obj_get_trace_tag(gfx_obj_t *obj);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gfx_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/* Timer callback function type */
|
||||
typedef void (*gfx_timer_cb_t)(void *);
|
||||
|
||||
/* Timer handle type for external use */
|
||||
typedef void *gfx_timer_handle_t;
|
||||
|
||||
/**********************
|
||||
* GLOBAL PROTOTYPES
|
||||
**********************/
|
||||
|
||||
/*=====================
|
||||
* Timer functions
|
||||
*====================*/
|
||||
|
||||
/**
|
||||
* @brief Create a new timer
|
||||
* @param handle Player handle
|
||||
* @param timer_cb Timer callback function
|
||||
* @param period Timer period in milliseconds
|
||||
* @param user_data User data passed to callback
|
||||
* @return Timer handle, NULL on error
|
||||
*/
|
||||
gfx_timer_handle_t gfx_timer_create(void *handle, gfx_timer_cb_t timer_cb, uint32_t period, void *user_data);
|
||||
|
||||
/**
|
||||
* @brief Delete a timer
|
||||
* @param handle Player handle
|
||||
* @param timer Timer handle to delete
|
||||
*/
|
||||
void gfx_timer_delete(void *handle, gfx_timer_handle_t timer);
|
||||
|
||||
/**
|
||||
* @brief Pause a timer
|
||||
* @param timer Timer handle to pause
|
||||
*/
|
||||
void gfx_timer_pause(gfx_timer_handle_t timer);
|
||||
|
||||
/**
|
||||
* @brief Resume a timer
|
||||
* @param timer Timer handle to resume
|
||||
*/
|
||||
void gfx_timer_resume(gfx_timer_handle_t timer);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Check if a timer is running
|
||||
* @param timer_handle Timer handle to check
|
||||
* @return true if timer is running, false otherwise
|
||||
*/
|
||||
bool gfx_timer_is_running(gfx_timer_handle_t timer_handle);
|
||||
|
||||
/**
|
||||
* @brief Set timer repeat count
|
||||
* @param timer Timer handle to modify
|
||||
* @param repeat_count Number of times to repeat (-1 for infinite)
|
||||
*/
|
||||
void gfx_timer_set_repeat_count(gfx_timer_handle_t timer, int32_t repeat_count);
|
||||
|
||||
/**
|
||||
* @brief Set timer period
|
||||
* @param timer Timer handle to modify
|
||||
* @param period New period in milliseconds
|
||||
*/
|
||||
void gfx_timer_set_period(gfx_timer_handle_t timer, uint32_t period);
|
||||
|
||||
/**
|
||||
* @brief Reset a timer
|
||||
* @param timer Timer handle to reset
|
||||
*/
|
||||
void gfx_timer_reset(gfx_timer_handle_t timer);
|
||||
|
||||
/**
|
||||
* @brief Get current system tick
|
||||
* @return Current tick value in milliseconds
|
||||
*/
|
||||
uint32_t gfx_timer_tick_get(void);
|
||||
|
||||
/**
|
||||
* @brief Get actual FPS from timer manager
|
||||
* @param handle Player handle
|
||||
* @return Actual FPS value, 0 if handle is invalid
|
||||
*/
|
||||
uint32_t gfx_timer_get_actual_fps(void *handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_lcd_touch.h"
|
||||
|
||||
#include "core/gfx_disp.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* TYPEDEFS
|
||||
*********************/
|
||||
/** Touch handle: from gfx_touch_add(), pass to event_cb and other touch APIs */
|
||||
typedef struct gfx_touch gfx_touch_t;
|
||||
|
||||
typedef enum {
|
||||
GFX_TOUCH_EVENT_PRESS = 0,
|
||||
GFX_TOUCH_EVENT_RELEASE,
|
||||
GFX_TOUCH_EVENT_MOVE, /**< Finger moved while pressed (slide) */
|
||||
} gfx_touch_event_type_t;
|
||||
|
||||
/** Payload passed to gfx_touch_event_cb_t; hit_obj is set when touch is bound to a disp */
|
||||
typedef struct gfx_touch_event {
|
||||
gfx_touch_event_type_t type;
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
uint16_t strength;
|
||||
uint8_t track_id;
|
||||
uint32_t timestamp_ms;
|
||||
} gfx_touch_event_t;
|
||||
|
||||
typedef void (*gfx_touch_event_cb_t)(gfx_touch_t *touch, const gfx_touch_event_t *event, void *user_data);
|
||||
|
||||
/** Passed to gfx_touch_add(); NULL or no handle disables touch */
|
||||
typedef struct {
|
||||
esp_lcd_touch_handle_t handle; /**< LCD touch driver handle */
|
||||
gfx_touch_event_cb_t event_cb; /**< Event callback */
|
||||
uint32_t poll_ms; /**< Poll interval ms (0 = default) */
|
||||
gfx_disp_t *disp; /**< Display handle */
|
||||
void *user_data; /**< User data for callback */
|
||||
} gfx_touch_config_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Add a touch device (like gfx_disp_add; multiple touch devices supported)
|
||||
*
|
||||
* @param handle Graphics handle from gfx_emote_init
|
||||
* @param cfg Touch configuration (handle, poll_ms, event_cb, etc.); required
|
||||
* @return gfx_touch_t* Touch pointer on success, NULL on error
|
||||
*/
|
||||
gfx_touch_t *gfx_touch_add(gfx_handle_t handle, const gfx_touch_config_t *cfg);
|
||||
|
||||
/**
|
||||
* @brief Bind a display to a touch device
|
||||
*
|
||||
* @param touch Touch pointer returned from gfx_touch_add
|
||||
* @param disp Display to receive touch hit-testing and dispatch
|
||||
* @return ESP_OK on success, ESP_ERR_INVALID_ARG if touch is NULL
|
||||
*/
|
||||
esp_err_t gfx_touch_set_disp(gfx_touch_t *touch, gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Remove a touch device from the list and release resources (stops polling, disables IRQ).
|
||||
* Does not free the gfx_touch_t; caller must free(touch) after.
|
||||
*
|
||||
* @param touch Touch pointer returned from gfx_touch_add; safe to pass NULL
|
||||
*/
|
||||
void gfx_touch_del(gfx_touch_t *touch);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/* Pixel size constants */
|
||||
#define GFX_PIXEL_SIZE_16BPP 2 /**< 16-bit color format: 2 bytes per pixel */
|
||||
#define GFX_PIXEL_SIZE_8BPP 1 /**< 8-bit format: 1 byte per pixel */
|
||||
|
||||
/**
|
||||
* @brief Calculate buffer pointer with offset for 16-bit format (RGB565)
|
||||
* @param buffer Base buffer pointer (any type)
|
||||
* @param y_offset Vertical offset in pixels
|
||||
* @param stride Width of buffer in pixels
|
||||
* @param x_offset Horizontal offset in pixels
|
||||
* @return Calculated gfx_color_t pointer with offset applied
|
||||
*/
|
||||
#define GFX_BUFFER_OFFSET_16BPP(buffer, y_offset, stride, x_offset) \
|
||||
((uint8_t *)((uint8_t *)(buffer) + \
|
||||
(y_offset) * (stride) * GFX_PIXEL_SIZE_16BPP + \
|
||||
(x_offset) * GFX_PIXEL_SIZE_16BPP))
|
||||
|
||||
/**
|
||||
* @brief Calculate buffer pointer with offset for 8-bit format
|
||||
* @param buffer Base buffer pointer (any type)
|
||||
* @param y_offset Vertical offset in pixels
|
||||
* @param stride Width of buffer in pixels
|
||||
* @param x_offset Horizontal offset in pixels
|
||||
* @return Calculated uint8_t pointer with offset applied
|
||||
*/
|
||||
#define GFX_BUFFER_OFFSET_8BPP(buffer, y_offset, stride, x_offset) \
|
||||
((uint8_t *)((uint8_t *)(buffer) + \
|
||||
(y_offset) * (stride) * GFX_PIXEL_SIZE_8BPP + \
|
||||
(x_offset) * GFX_PIXEL_SIZE_8BPP))
|
||||
|
||||
/**
|
||||
* @brief Calculate buffer pointer with offset for 4-bit format (2 pixels per byte)
|
||||
* @param buffer Base buffer pointer (any type)
|
||||
* @param y_offset Vertical offset in pixels
|
||||
* @param stride Width of buffer in pixels (will be divided by 2)
|
||||
* @param x_offset Horizontal offset in pixels (will be divided by 2)
|
||||
* @return Calculated uint8_t pointer with offset applied
|
||||
*/
|
||||
#define GFX_BUFFER_OFFSET_4BPP(buffer, y_offset, stride, x_offset) \
|
||||
((uint8_t *)((uint8_t *)(buffer) + \
|
||||
(y_offset) * ((stride) / 2) + \
|
||||
(x_offset) / 2))
|
||||
|
||||
#define GFX_COLOR_HEX(color) ((gfx_color_t)gfx_color_hex(color))
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/* Basic types */
|
||||
typedef uint8_t gfx_opa_t; /**< Opacity (0-255) */
|
||||
typedef int16_t gfx_coord_t; /**< Coordinate type */
|
||||
|
||||
/** Graphics handle type */
|
||||
typedef void *gfx_handle_t; /**< Graphics handle type */
|
||||
|
||||
/* Color type with full member for compatibility */
|
||||
typedef union {
|
||||
uint16_t full; /**< Full 16-bit color value */
|
||||
} gfx_color_t;
|
||||
|
||||
/* Area structure */
|
||||
typedef struct {
|
||||
gfx_coord_t x1;
|
||||
gfx_coord_t y1;
|
||||
gfx_coord_t x2;
|
||||
gfx_coord_t y2;
|
||||
} gfx_area_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Convert a 32-bit hexadecimal color to gfx_color_t
|
||||
* @param c The 32-bit hexadecimal color to convert
|
||||
* @return Converted color in gfx_color_t type
|
||||
*/
|
||||
gfx_color_t gfx_color_hex(uint32_t c);
|
||||
|
||||
/**
|
||||
* @brief Convert a semantic gfx_color_t to native framebuffer order.
|
||||
*
|
||||
* Use this helper only when writing raw 16-bit pixels directly into a buffer
|
||||
* or calling raw fill helpers that do not accept a separate `swap` argument.
|
||||
*
|
||||
* @param color Semantic RGB565 color value.
|
||||
* @param swap Whether the target buffer expects swapped byte order.
|
||||
* @return Native-order 16-bit pixel value for the target buffer.
|
||||
*/
|
||||
static inline uint16_t gfx_color_to_native_u16(gfx_color_t color, bool swap)
|
||||
{
|
||||
return swap ? (uint16_t)__builtin_bswap16(color.full) : color.full;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/gfx_types.h"
|
||||
#include "core/gfx_core.h"
|
||||
#include "core/gfx_disp.h"
|
||||
#include "core/gfx_log.h"
|
||||
#include "core/gfx_timer.h"
|
||||
#include "core/gfx_touch.h"
|
||||
#include "core/gfx_obj.h"
|
||||
#include "widget/gfx_img.h"
|
||||
#include "widget/gfx_mesh_img.h"
|
||||
#include "widget/gfx_motion.h"
|
||||
#include "widget/gfx_motion_scene.h"
|
||||
#include "widget/gfx_qrcode.h"
|
||||
#include "widget/gfx_label.h"
|
||||
#include "widget/gfx_button.h"
|
||||
#include "widget/gfx_anim.h"
|
||||
#include "widget/gfx_font_lvgl.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "core/gfx_obj.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
typedef enum {
|
||||
GFX_ANIM_SEGMENT_ACTION_CONTINUE = 0,
|
||||
GFX_ANIM_SEGMENT_ACTION_PAUSE,
|
||||
} gfx_anim_segment_action_t;
|
||||
|
||||
/**
|
||||
* @brief Playback description for one animation segment.
|
||||
*
|
||||
* A segment defines:
|
||||
* - frame range
|
||||
* - playback speed
|
||||
* - total repeat count
|
||||
* - what to do when the segment finishes
|
||||
*
|
||||
* Use `gfx_anim_set_segment()` for the simple single-segment case.
|
||||
* Use `gfx_anim_set_segments()` when you need a playback plan.
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t start; /* inclusive start frame */
|
||||
uint32_t end; /* inclusive end frame */
|
||||
uint32_t fps; /* playback fps for this segment */
|
||||
uint32_t play_count; /* total plays for this segment, 0 means forever */
|
||||
gfx_anim_segment_action_t end_action; /* action after the last play finishes */
|
||||
} gfx_anim_segment_t;
|
||||
|
||||
/**
|
||||
* @brief Public animation source type.
|
||||
*
|
||||
* The current implementation supports in-memory animation payloads.
|
||||
* The enum exists so future source types can be added without changing the
|
||||
* source-setting API shape again.
|
||||
*/
|
||||
typedef enum {
|
||||
GFX_ANIM_SRC_TYPE_MEMORY = 0, /**< In-memory animation payload */
|
||||
} gfx_anim_src_type_t;
|
||||
|
||||
/**
|
||||
* @brief Typed animation source descriptor.
|
||||
*
|
||||
* `gfx_anim_set_src_desc()` is the preferred source setter for new code.
|
||||
* `gfx_anim_set_src()` remains as a compatibility wrapper for raw memory
|
||||
* buffers and length pairs.
|
||||
*/
|
||||
typedef struct {
|
||||
gfx_anim_src_type_t type; /**< Source payload type */
|
||||
const void *data; /**< Type-specific payload pointer */
|
||||
size_t data_len; /**< Payload length in bytes */
|
||||
} gfx_anim_src_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Create an animation object on a display
|
||||
* @param disp Display from gfx_emote_add_disp(handle, &disp_cfg)
|
||||
* @return Pointer to the created animation object
|
||||
*/
|
||||
gfx_obj_t *gfx_anim_create(gfx_disp_t *disp);
|
||||
|
||||
/* Animation setters */
|
||||
|
||||
/**
|
||||
* @brief Set the typed source descriptor for an animation object
|
||||
*
|
||||
* This is the preferred source setter for new code.
|
||||
*
|
||||
* @param obj Pointer to the animation object
|
||||
* @param src Pointer to the typed source descriptor
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_set_src_desc(gfx_obj_t *obj, const gfx_anim_src_t *src);
|
||||
|
||||
/**
|
||||
* @brief Set the source data for an animation object
|
||||
*
|
||||
* Compatibility wrapper for in-memory animation payloads.
|
||||
*
|
||||
* @param obj Pointer to the animation object
|
||||
* @param src_data Source data
|
||||
* @param src_len Source data length
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_set_src(gfx_obj_t *obj, const void *src_data, size_t src_len);
|
||||
|
||||
/**
|
||||
* @brief Set the segment for an animation object
|
||||
* @param obj Pointer to the animation object
|
||||
* @param start Start frame index
|
||||
* @param end End frame index
|
||||
* @param fps Frames per second
|
||||
* @param repeat Whether to repeat the animation
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_set_segment(gfx_obj_t *obj, uint32_t start, uint32_t end, uint32_t fps, bool repeat);
|
||||
/**
|
||||
* @brief Set a segment playback plan for an animation object
|
||||
* @param obj Pointer to the animation object
|
||||
* @param segments Segment plan array
|
||||
* @param segment_count Number of segment entries in the array
|
||||
*
|
||||
* Each segment uses `play_count` to describe the total number of plays:
|
||||
* - `play_count = 1`: play once
|
||||
* - `play_count = N`: play N times
|
||||
* - `play_count = 0`: play forever
|
||||
*
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_set_segments(gfx_obj_t *obj, const gfx_anim_segment_t *segments, size_t segment_count);
|
||||
|
||||
/**
|
||||
* @brief Drain the remaining segment plan and block until playback finishes
|
||||
*
|
||||
* This API is intended for segment-plan mode. If playback is currently inside
|
||||
* a loop phase of the active segment, or paused after a segment `end_action`,
|
||||
* calling this API will drain the remaining plan exactly once:
|
||||
* - the active segment continues from its current frame to its end
|
||||
* - the active segment's remaining repeat count is ignored
|
||||
* - all following segments are played once in order
|
||||
* - pause actions in the remaining plan are ignored
|
||||
*
|
||||
* The function blocks until the remaining plan has finished.
|
||||
*
|
||||
* Do not call this API while holding the graphics lock.
|
||||
*
|
||||
* @param obj Pointer to the animation object
|
||||
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if there is no remaining work,
|
||||
* or another ESP_ERR_* code on failure
|
||||
*/
|
||||
esp_err_t gfx_anim_play_left_to_tail(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Start the animation
|
||||
* @param obj Pointer to the animation object
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_start(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Stop the animation
|
||||
* @param obj Pointer to the animation object
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_stop(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Set mirror display for an animation object
|
||||
*
|
||||
* Manual mirror duplicates the rendered image horizontally and inserts the
|
||||
* provided offset between the original and mirrored copy.
|
||||
* For display-width-aware mirroring, use `gfx_anim_set_auto_mirror()`.
|
||||
*
|
||||
* @param obj Pointer to the animation object
|
||||
* @param enabled Whether to enable mirror display
|
||||
* @param offset Mirror offset in pixels
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_set_mirror(gfx_obj_t *obj, bool enabled, int16_t offset);
|
||||
|
||||
/**
|
||||
* @brief Set auto mirror alignment for animation object
|
||||
*
|
||||
* Auto mirror computes the mirror offset from the current display width.
|
||||
* Compared with `gfx_anim_set_mirror()`, this mode is easier to use when the
|
||||
* animation should mirror around the display center without a fixed offset.
|
||||
*
|
||||
* @param obj Animation object
|
||||
* @param enabled Whether to enable auto mirror alignment
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_anim_set_auto_mirror(gfx_obj_t *obj, bool enabled);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "widget/gfx_label.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Create a button object on a display
|
||||
* @param disp Display from gfx_disp_add()
|
||||
* @return Pointer to the created button object
|
||||
*/
|
||||
gfx_obj_t *gfx_button_create(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Set the label text for a button
|
||||
* @param obj Button object
|
||||
* @param text Text string; NULL is treated as an empty string
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_text(gfx_obj_t *obj, const char *text);
|
||||
|
||||
/**
|
||||
* @brief Set the label text for a button using printf-style formatting
|
||||
* @param obj Button object
|
||||
* @param fmt Format string
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_text_fmt(gfx_obj_t *obj, const char *fmt, ...);
|
||||
|
||||
/**
|
||||
* @brief Set the font used by the button label
|
||||
* @param obj Button object
|
||||
* @param font Font handle
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_font(gfx_obj_t *obj, gfx_font_t font);
|
||||
|
||||
/**
|
||||
* @brief Set the label text color for a button
|
||||
* @param obj Button object
|
||||
* @param color Text color
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_text_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
/**
|
||||
* @brief Set the normal background color for a button
|
||||
* @param obj Button object
|
||||
* @param color Background color
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_bg_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
/**
|
||||
* @brief Set the pressed background color for a button
|
||||
* @param obj Button object
|
||||
* @param color Pressed background color
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_bg_color_pressed(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
/**
|
||||
* @brief Set the border color for a button
|
||||
* @param obj Button object
|
||||
* @param color Border color
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_border_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
/**
|
||||
* @brief Set the border width for a button
|
||||
* @param obj Button object
|
||||
* @param width Border width in pixels; 0 disables the border
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_border_width(gfx_obj_t *obj, uint16_t width);
|
||||
|
||||
/**
|
||||
* @brief Set the text alignment for a button label
|
||||
* @param obj Button object
|
||||
* @param align Text alignment
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_button_set_text_align(gfx_obj_t *obj, gfx_text_align_t align);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
/*********************
|
||||
* INCLUDES
|
||||
*********************/
|
||||
#include <stdint.h>
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/*
|
||||
* The following code (gfx_font_lv_load_from_binary and gfx_font_lv_delete)
|
||||
* is derived from 78/xiaozhi-fonts project.
|
||||
* Original source: https://github.com/78/xiaozhi-fonts
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Load an LVGL font from binary data
|
||||
* @param bin_addr Pointer to binary data containing lv_font_t structure
|
||||
* @return Pointer to loaded lv_font_t, or NULL on failure
|
||||
*/
|
||||
lv_font_t *gfx_font_lv_load_from_binary(uint8_t *bin_addr);
|
||||
|
||||
/**
|
||||
* @brief Delete an LVGL font created from binary data
|
||||
* @param font Pointer to lv_font_t to delete
|
||||
*/
|
||||
void gfx_font_lv_delete(lv_font_t *font);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/gfx_obj.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/* Magic numbers for image headers */
|
||||
#define C_ARRAY_HEADER_MAGIC 0x19
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/* Color format enumeration - simplified for public use */
|
||||
typedef enum {
|
||||
GFX_COLOR_FORMAT_RGB565 = 0x04, /**< RGB565 format without alpha channel */
|
||||
GFX_COLOR_FORMAT_RGB565A8 = 0x0A, /**< RGB565 format with separate alpha channel */
|
||||
} gfx_color_format_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic: 8; /**< Magic number. Must be GFX_IMAGE_HEADER_MAGIC */
|
||||
uint32_t cf : 8; /**< Color format: See `gfx_color_format_t` */
|
||||
uint32_t flags: 16; /**< Image flags */
|
||||
uint32_t w: 16; /**< Width of the image */
|
||||
uint32_t h: 16; /**< Height of the image */
|
||||
uint32_t stride: 16; /**< Number of bytes in a row */
|
||||
uint32_t reserved: 16; /**< Reserved for future use */
|
||||
} gfx_image_header_t;
|
||||
|
||||
/* Image descriptor structure - compatible with LVGL */
|
||||
typedef struct {
|
||||
gfx_image_header_t header; /**< A header describing the basics of the image */
|
||||
uint32_t data_size; /**< Size of the image in bytes */
|
||||
const uint8_t *data; /**< Pointer to the data of the image */
|
||||
const void *reserved; /**< Reserved field for future use */
|
||||
const void *reserved_2; /**< Reserved field for future use */
|
||||
} gfx_image_dsc_t;
|
||||
|
||||
/**
|
||||
* @brief Public image source type.
|
||||
*
|
||||
* Use this enum together with `gfx_img_src_t` to describe where an image
|
||||
* payload comes from. The current implementation supports in-memory
|
||||
* `gfx_image_dsc_t` payloads and keeps room for future source types.
|
||||
*/
|
||||
typedef enum {
|
||||
GFX_IMG_SRC_TYPE_IMAGE_DSC = 0, /**< In-memory gfx_image_dsc_t payload */
|
||||
} gfx_img_src_type_t;
|
||||
|
||||
/**
|
||||
* @brief Typed image source descriptor.
|
||||
*
|
||||
* `gfx_img_set_src_desc()` is the preferred API because it makes the source
|
||||
* type explicit. `gfx_img_set_src()` remains as a compatibility wrapper for
|
||||
* direct `gfx_image_dsc_t *` usage.
|
||||
*/
|
||||
typedef struct {
|
||||
gfx_img_src_type_t type; /**< Source payload type */
|
||||
const void *data; /**< Type-specific payload pointer */
|
||||
} gfx_img_src_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Create an image object on a display
|
||||
* @param disp Display from gfx_emote_add_disp(handle, &disp_cfg)
|
||||
* @return Pointer to the created image object, NULL on error
|
||||
*/
|
||||
gfx_obj_t *gfx_img_create(gfx_disp_t *disp);
|
||||
|
||||
/* Image setters */
|
||||
|
||||
/**
|
||||
* @brief Set the typed source descriptor for an image object
|
||||
*
|
||||
* This is the preferred source setter for new code. It keeps the public API
|
||||
* extensible when additional image source types are introduced.
|
||||
*
|
||||
* @param obj Pointer to the image object
|
||||
* @param src Pointer to the typed source descriptor
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_img_set_src_desc(gfx_obj_t *obj, const gfx_img_src_t *src);
|
||||
|
||||
/**
|
||||
* @brief Set the source data for an image object
|
||||
*
|
||||
* Compatibility wrapper for in-memory `gfx_image_dsc_t` payloads.
|
||||
*
|
||||
* @param obj Pointer to the image object
|
||||
* @param src Pointer to the image source data
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_img_set_src(gfx_obj_t *obj, void *src);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include "sdkconfig.h"
|
||||
#include "core/gfx_obj.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/* Font handle type - hides internal FreeType implementation */
|
||||
typedef void *gfx_font_t;
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* Text alignment enumeration (similar to LVGL)
|
||||
*/
|
||||
typedef enum {
|
||||
GFX_TEXT_ALIGN_AUTO, /**< Align text auto */
|
||||
GFX_TEXT_ALIGN_LEFT, /**< Align text to left */
|
||||
GFX_TEXT_ALIGN_CENTER, /**< Align text to center */
|
||||
GFX_TEXT_ALIGN_RIGHT, /**< Align text to right */
|
||||
} gfx_text_align_t;
|
||||
|
||||
/**
|
||||
* Long text mode enumeration (similar to LVGL)
|
||||
*/
|
||||
typedef enum {
|
||||
GFX_LABEL_LONG_WRAP, /**< Break the long lines (word wrap) */
|
||||
GFX_LABEL_LONG_SCROLL, /**< Make the text scrolling horizontally smoothly */
|
||||
GFX_LABEL_LONG_CLIP, /**< Simply clip the parts which don't fit */
|
||||
GFX_LABEL_LONG_SCROLL_SNAP, /**< Jump to next section after interval (horizontal paging) */
|
||||
} gfx_label_long_mode_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name; /**< The name of the font file */
|
||||
const void *mem; /**< The pointer to the font file */
|
||||
size_t mem_size; /**< The size of the memory */
|
||||
uint16_t font_size; /**< The size of the font */
|
||||
} gfx_label_cfg_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Create a label object on a display
|
||||
* @param disp Display from gfx_emote_add_disp(handle, &disp_cfg)
|
||||
* @return Pointer to the created label object
|
||||
*/
|
||||
gfx_obj_t *gfx_label_create(gfx_disp_t *disp);
|
||||
|
||||
#ifdef CONFIG_GFX_FONT_FREETYPE_SUPPORT
|
||||
/* Font management */
|
||||
|
||||
/**
|
||||
* @brief Create a new font
|
||||
* @param cfg Font configuration
|
||||
* @param ret_font Pointer to store the font handle
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_new_font(const gfx_label_cfg_t *cfg, gfx_font_t *ret_font);
|
||||
|
||||
/**
|
||||
* @brief Delete a font and free its resources
|
||||
* @param font Font handle to delete
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_delete_font(gfx_font_t font);
|
||||
#endif
|
||||
|
||||
/* Label setters */
|
||||
|
||||
/**
|
||||
* @brief Set the text for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param text Text string to display
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_text(gfx_obj_t *obj, const char *text);
|
||||
|
||||
/**
|
||||
* @brief Set the text for a label object with format
|
||||
* @param obj Pointer to the label object
|
||||
* @param fmt Format string
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_text_fmt(gfx_obj_t *obj, const char *fmt, ...);
|
||||
|
||||
/**
|
||||
* @brief Set the color for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param color Color value
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
/**
|
||||
* @brief Set the background color for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param bg_color Background color value
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_bg_color(gfx_obj_t *obj, gfx_color_t bg_color);
|
||||
|
||||
/**
|
||||
* @brief Enable or disable background for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param enable True to enable background, false to disable
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_bg_enable(gfx_obj_t *obj, bool enable);
|
||||
|
||||
/**
|
||||
* @brief Set the opacity for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param opa Opacity value (0-255)
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_opa(gfx_obj_t *obj, gfx_opa_t opa);
|
||||
|
||||
/**
|
||||
* @brief Set the font for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param font Font handle
|
||||
*/
|
||||
esp_err_t gfx_label_set_font(gfx_obj_t *obj, gfx_font_t font);
|
||||
|
||||
/**
|
||||
* @brief Set the text alignment for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param align Text alignment value
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_text_align(gfx_obj_t *obj, gfx_text_align_t align);
|
||||
|
||||
/**
|
||||
* @brief Set the long text mode for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param long_mode Long text handling mode (wrap, scroll, or clip)
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_long_mode(gfx_obj_t *obj, gfx_label_long_mode_t long_mode);
|
||||
|
||||
/**
|
||||
* @brief Set the line spacing for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param spacing Line spacing in pixels
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_line_spacing(gfx_obj_t *obj, uint16_t spacing);
|
||||
|
||||
/**
|
||||
* @brief Set the horizontal scrolling speed for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param speed_ms Scrolling speed in milliseconds per pixel
|
||||
* @note Only effective when long_mode is GFX_LABEL_LONG_SCROLL
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_scroll_speed(gfx_obj_t *obj, uint32_t speed_ms);
|
||||
|
||||
/**
|
||||
* @brief Set whether scrolling should loop continuously
|
||||
* @param obj Pointer to the label object
|
||||
* @param loop True to enable continuous looping, false for one-time scroll
|
||||
* @note Only effective when long_mode is GFX_LABEL_LONG_SCROLL
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_scroll_loop(gfx_obj_t *obj, bool loop);
|
||||
|
||||
/**
|
||||
* @brief Set the scroll step size for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param step Scroll step size in pixels per timer tick (default: 1, can be negative)
|
||||
* @note Only effective when long_mode is GFX_LABEL_LONG_SCROLL
|
||||
* @note Step cannot be zero
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_scroll_step(gfx_obj_t *obj, int32_t step);
|
||||
|
||||
/**
|
||||
* @brief Set the snap scroll interval time for a label object
|
||||
* @param obj Pointer to the label object
|
||||
* @param interval_ms Interval time in milliseconds to stay on each section before jumping
|
||||
* @note Only effective when long_mode is GFX_LABEL_LONG_SCROLL_SNAP
|
||||
* @note The jump offset is automatically calculated as the label width
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_snap_interval(gfx_obj_t *obj, uint32_t interval_ms);
|
||||
|
||||
/**
|
||||
* @brief Set whether snap scrolling should loop continuously
|
||||
* @param obj Pointer to the label object
|
||||
* @param loop True to enable continuous looping, false to stop at end
|
||||
* @note Only effective when long_mode is GFX_LABEL_LONG_SCROLL_SNAP
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_label_set_snap_loop(gfx_obj_t *obj, bool loop);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,313 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "core/gfx_obj.h"
|
||||
#include "widget/gfx_img.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Mesh control point in integer pixel coordinates.
|
||||
*
|
||||
* Use this type when callers only need whole-pixel positioning.
|
||||
* For subpixel precision, use `gfx_mesh_img_point_q8_t`.
|
||||
*/
|
||||
typedef struct {
|
||||
gfx_coord_t x;
|
||||
gfx_coord_t y;
|
||||
} gfx_mesh_img_point_t;
|
||||
|
||||
/**
|
||||
* @brief Mesh control point in Q8 fixed-point coordinates.
|
||||
*
|
||||
* This variant is intended for subpixel-accurate deformation and animation.
|
||||
* Compared with `gfx_mesh_img_point_t`, it preserves 1/256 pixel precision.
|
||||
*/
|
||||
typedef struct {
|
||||
int32_t x_q8;
|
||||
int32_t y_q8;
|
||||
} gfx_mesh_img_point_q8_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Create a mesh-image object on a display.
|
||||
*
|
||||
* A mesh-image widget deforms an image through a regular point grid.
|
||||
* Compared with `gfx_img_create()`, this widget supports per-point warp.
|
||||
*
|
||||
* @param disp Display that owns the object
|
||||
* @return Created object, or NULL on failure
|
||||
*/
|
||||
gfx_obj_t *gfx_mesh_img_create(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Set a typed image source descriptor for the mesh.
|
||||
*
|
||||
* This is the preferred source setter for new code. It keeps the source type
|
||||
* explicit and aligned with the image widget API.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param src Typed image source descriptor
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_src_desc(gfx_obj_t *obj, const gfx_img_src_t *src);
|
||||
|
||||
/**
|
||||
* @brief Set the image source for the mesh.
|
||||
*
|
||||
* Compatibility wrapper for direct `gfx_image_dsc_t *` payloads.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param src In-memory image source payload
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_src(gfx_obj_t *obj, void *src);
|
||||
|
||||
/**
|
||||
* @brief Configure mesh grid density.
|
||||
*
|
||||
* `cols` and `rows` describe the number of cells, not points.
|
||||
* The actual point count is `(cols + 1) * (rows + 1)`.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param cols Horizontal cell count
|
||||
* @param rows Vertical cell count
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_grid(gfx_obj_t *obj, uint8_t cols, uint8_t rows);
|
||||
|
||||
/**
|
||||
* @brief Get the current mesh point count.
|
||||
*
|
||||
* This reflects the current grid configuration.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @return Number of points in the current mesh
|
||||
*/
|
||||
size_t gfx_mesh_img_get_point_count(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Get one mesh point in object-local pixel coordinates.
|
||||
*
|
||||
* Compared with `gfx_mesh_img_get_point_screen()`, this returns coordinates
|
||||
* relative to the mesh object itself.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param point_idx Point index in the current grid
|
||||
* @param point Output point
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_get_point(gfx_obj_t *obj, size_t point_idx, gfx_mesh_img_point_t *point);
|
||||
|
||||
/**
|
||||
* @brief Get one mesh point in screen coordinates.
|
||||
*
|
||||
* Compared with `gfx_mesh_img_get_point()`, this includes the current object
|
||||
* position and alignment result.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param point_idx Point index in the current grid
|
||||
* @param x Output screen x
|
||||
* @param y Output screen y
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_get_point_screen(gfx_obj_t *obj, size_t point_idx, gfx_coord_t *x, gfx_coord_t *y);
|
||||
|
||||
/**
|
||||
* @brief Get one mesh point in object-local Q8 coordinates.
|
||||
*
|
||||
* Compared with `gfx_mesh_img_get_point()`, this preserves subpixel precision.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param point_idx Point index in the current grid
|
||||
* @param point Output Q8 point
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_get_point_q8(gfx_obj_t *obj, size_t point_idx, gfx_mesh_img_point_q8_t *point);
|
||||
|
||||
/**
|
||||
* @brief Get one mesh point in screen-space Q8 coordinates.
|
||||
*
|
||||
* This combines the current object position with the point's subpixel value.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param point_idx Point index in the current grid
|
||||
* @param x_q8 Output screen x in Q8
|
||||
* @param y_q8 Output screen y in Q8
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_get_point_screen_q8(gfx_obj_t *obj, size_t point_idx, int32_t *x_q8, int32_t *y_q8);
|
||||
|
||||
/**
|
||||
* @brief Set one mesh point in object-local pixel coordinates.
|
||||
*
|
||||
* For subpixel updates, use `gfx_mesh_img_set_point_q8()`.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param point_idx Point index in the current grid
|
||||
* @param x Local x
|
||||
* @param y Local y
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_point(gfx_obj_t *obj, size_t point_idx, gfx_coord_t x, gfx_coord_t y);
|
||||
|
||||
/**
|
||||
* @brief Set all mesh points in object-local pixel coordinates.
|
||||
*
|
||||
* The caller must provide exactly the current point count.
|
||||
* For subpixel updates, use `gfx_mesh_img_set_points_q8()`.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param points Point array
|
||||
* @param point_count Number of entries in `points`
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_points(gfx_obj_t *obj, const gfx_mesh_img_point_t *points, size_t point_count);
|
||||
|
||||
/**
|
||||
* @brief Set one mesh point in object-local Q8 coordinates.
|
||||
*
|
||||
* Compared with `gfx_mesh_img_set_point()`, this keeps subpixel precision.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param point_idx Point index in the current grid
|
||||
* @param x_q8 Local x in Q8
|
||||
* @param y_q8 Local y in Q8
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_point_q8(gfx_obj_t *obj, size_t point_idx, int32_t x_q8, int32_t y_q8);
|
||||
|
||||
/**
|
||||
* @brief Set all mesh points in object-local Q8 coordinates.
|
||||
*
|
||||
* This is the preferred batch API for smooth deformation animation.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param points Q8 point array
|
||||
* @param point_count Number of entries in `points`
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_points_q8(gfx_obj_t *obj, const gfx_mesh_img_point_q8_t *points, size_t point_count);
|
||||
|
||||
/**
|
||||
* @brief Set the rest pose points in object-local pixel coordinates.
|
||||
*
|
||||
* Rest points define the undeformed reference mesh used for texture sampling
|
||||
* and later reset operations. Compared with `gfx_mesh_img_set_points()`, this
|
||||
* updates the reference pose instead of only the current deformation.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param points Point array
|
||||
* @param point_count Number of entries in `points`
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_rest_points(gfx_obj_t *obj, const gfx_mesh_img_point_t *points, size_t point_count);
|
||||
|
||||
/**
|
||||
* @brief Set the rest pose points in object-local Q8 coordinates.
|
||||
*
|
||||
* Compared with `gfx_mesh_img_set_rest_points()`, this keeps subpixel
|
||||
* precision in the reference pose.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param points Q8 point array
|
||||
* @param point_count Number of entries in `points`
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_rest_points_q8(gfx_obj_t *obj, const gfx_mesh_img_point_q8_t *points, size_t point_count);
|
||||
|
||||
/**
|
||||
* @brief Reset current points back to the stored rest pose.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_reset_points(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Show or hide mesh control points for debugging.
|
||||
*
|
||||
* This affects only debug visualization and does not change the deformation.
|
||||
*
|
||||
* @param obj Mesh-image object
|
||||
* @param visible Whether control points should be drawn
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_ctrl_points_visible(gfx_obj_t *obj, bool visible);
|
||||
|
||||
/**
|
||||
* @brief Set uniform mesh opacity.
|
||||
*
|
||||
* This value is multiplied with any source alpha and anti-aliasing coverage.
|
||||
*
|
||||
* @param obj Mesh image object
|
||||
* @param opa Uniform opacity (0-255)
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_opa(gfx_obj_t *obj, gfx_opa_t opa);
|
||||
|
||||
/**
|
||||
* @brief Enable inward-only edge anti-aliasing.
|
||||
*
|
||||
* When enabled, outer edges of this mesh fade from full opacity to transparent
|
||||
* towards the geometric boundary (inside the triangle) instead of drawing
|
||||
* semi-transparent pixels outside. Prevents visible "bleed" on thin strokes.
|
||||
*
|
||||
* @param obj Mesh image object.
|
||||
* @param inward true = inward AA (no outward bleed); false = default outward AA.
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_aa_inward(gfx_obj_t *obj, bool inward);
|
||||
|
||||
/**
|
||||
* @brief Treat first and last grid columns as adjacent (closed strip).
|
||||
*
|
||||
* When enabled, the left edge of the first column and the right edge of the
|
||||
* last column are marked as internal (shared), so edge AA does not fade them
|
||||
* to transparent. Use for closed stroke paths where the strip endpoints
|
||||
* coincide geometrically.
|
||||
*
|
||||
* Compared with `gfx_mesh_img_set_aa_inward()`, this changes edge topology
|
||||
* interpretation rather than AA direction.
|
||||
*
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_wrap_cols(gfx_obj_t *obj, bool wrap);
|
||||
|
||||
/**
|
||||
* @brief Use scanline polygon fill instead of triangle rasterization.
|
||||
*
|
||||
* When enabled (grid_rows must be 1), the mesh outline is filled as a closed
|
||||
* polygon using a scanline rasterizer with edge AA. No texture mapping —
|
||||
* fills with a solid color. Avoids diagonal-seam artifacts inherent in
|
||||
* per-triangle inward AA.
|
||||
*
|
||||
* Compared with the default textured-triangle mode, this is intended for
|
||||
* stroke-like meshes where a solid-color fill is preferable.
|
||||
*
|
||||
* @param obj Mesh image object
|
||||
* @param enable Whether scanline fill mode should be enabled
|
||||
* @param fill_color Solid fill color (typically white for strokes).
|
||||
* @return ESP_OK on success, ESP_ERR_* otherwise
|
||||
*/
|
||||
esp_err_t gfx_mesh_img_set_scanline_fill(gfx_obj_t *obj, bool enable, gfx_color_t fill_color);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "core/gfx_obj.h"
|
||||
#include "core/gfx_timer.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Lightweight motion driver.
|
||||
*
|
||||
* Goal: let higher-level scene/player code focus on state changes only.
|
||||
* - The driver owns a timer and calls `tick_cb` periodically.
|
||||
* - When `tick_cb` reports changes (or `force_apply`), the driver calls `apply_cb`.
|
||||
*/
|
||||
|
||||
typedef struct gfx_motion_cfg_t {
|
||||
uint16_t timer_period_ms;
|
||||
int16_t damping_div;
|
||||
} gfx_motion_cfg_t;
|
||||
|
||||
typedef struct gfx_motion_t gfx_motion_t;
|
||||
|
||||
typedef bool (*gfx_motion_tick_cb_t)(gfx_motion_t *motion, void *user_data);
|
||||
typedef esp_err_t (*gfx_motion_apply_cb_t)(gfx_motion_t *motion, void *user_data, bool force_apply);
|
||||
|
||||
struct gfx_motion_t {
|
||||
gfx_timer_handle_t timer;
|
||||
gfx_motion_cfg_t cfg;
|
||||
gfx_disp_t *disp;
|
||||
gfx_obj_t *anchor;
|
||||
gfx_motion_tick_cb_t tick_cb;
|
||||
gfx_motion_apply_cb_t apply_cb;
|
||||
void *user_data;
|
||||
};
|
||||
|
||||
void gfx_motion_cfg_init(gfx_motion_cfg_t *cfg, uint16_t timer_period_ms, int16_t damping_div);
|
||||
|
||||
esp_err_t gfx_motion_init(gfx_motion_t *motion,
|
||||
gfx_disp_t *disp,
|
||||
gfx_obj_t *anchor,
|
||||
const gfx_motion_cfg_t *cfg,
|
||||
gfx_motion_tick_cb_t tick_cb,
|
||||
gfx_motion_apply_cb_t apply_cb,
|
||||
void *user_data);
|
||||
|
||||
void gfx_motion_deinit(gfx_motion_t *motion);
|
||||
|
||||
esp_err_t gfx_motion_set_period(gfx_motion_t *motion, uint16_t period_ms);
|
||||
|
||||
/** Run one tick immediately (no wait). */
|
||||
esp_err_t gfx_motion_step(gfx_motion_t *motion, bool force_apply);
|
||||
|
||||
/** Utility: damped step for int16 values (same policy as existing widgets). */
|
||||
int16_t gfx_motion_ease_i16(int16_t cur, int16_t tgt, int16_t div);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,383 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file gfx_motion_scene.h
|
||||
*
|
||||
* Motion Scene Asset — three-layer architecture:
|
||||
*
|
||||
* Layer 1 — GENERATED ASSET (firmware-side):
|
||||
* Named control points grouped into visual parts via segments. The public
|
||||
* field names still use joint_* for ABI compatibility, but semantically they
|
||||
* are generic control points: skeleton endpoints, Bézier controls, or mesh
|
||||
* anchors depending on the segment kind.
|
||||
* All emote types (stickman, face, lobster-style textured) share the same
|
||||
* Motion Scene Asset layout:
|
||||
*
|
||||
* Segment kind Control points Rendered as
|
||||
* ───────────── ─────────────── ────────────────────────────────────
|
||||
* CAPSULE joint_a, joint_b Thick capsule (limb / body segment)
|
||||
* RING joint_a Hollow ring (head)
|
||||
* BEZIER_STRIP joint_a .. +n-1 Open thick Bézier curve (brow)
|
||||
* BEZIER_LOOP joint_a .. +n-1 Closed thick Bézier loop (mouth outline)
|
||||
* BEZIER_FILL joint_a .. +n-1 Closed fill: n=7 eye, n=13 ellipse quad, else any n=3k+1 (hub mesh)
|
||||
*
|
||||
* Stickman: each control point = one skeleton endpoint.
|
||||
* Face: each control point = one cubic Bézier point (n = 3k+1 format).
|
||||
* Textured: any segment can reference a ROM image via segment.resource_idx.
|
||||
* Poses store the *actual* target positions (pre-blended for face expressions).
|
||||
*
|
||||
* Layer 2 — PARSER (gfx_motion_scene.c):
|
||||
* Validates asset, manages runtime pose_cur / pose_tgt interpolation, and
|
||||
* advances action timelines. Zero display calls.
|
||||
*
|
||||
* Layer 3 — RUNTIME (gfx_motion_player.c):
|
||||
* Creates one gfx_mesh_img per segment. On every sync it maps design-space
|
||||
* pose_cur[] to screen pixels and calls the appropriate primitive helper
|
||||
* (capsule / ring / bezier) based on segment kind.
|
||||
* No type flag required — segment kind encodes everything.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "core/gfx_obj.h"
|
||||
#include "widget/gfx_img.h"
|
||||
#include "widget/gfx_motion.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define GFX_MOTION_SCENE_SCHEMA_VERSION 2U
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 0. Resource table (textures / image assets) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* One entry in the asset's resource table.
|
||||
*
|
||||
* A segment with resource_idx > 0 uses resources[resource_idx - 1]
|
||||
* as its mesh_img texture source instead of the runtime solid colour.
|
||||
* This allows texture-mapped segments (e.g. lobster body) to live in
|
||||
* the same unified asset format as solid-colour vector segments.
|
||||
*
|
||||
* resource_idx = 0 → solid colour (default, zero-init compatible)
|
||||
* resource_idx = N → resources[N-1]
|
||||
*/
|
||||
typedef struct {
|
||||
const gfx_image_dsc_t *image; /**< Pointer to the image descriptor (ROM .inc array) */
|
||||
uint16_t uv_x; /**< Source crop origin X (0 = full image) */
|
||||
uint16_t uv_y; /**< Source crop origin Y (0 = full image) */
|
||||
uint16_t uv_w; /**< Source crop width (0 = image width from uv_x) */
|
||||
uint16_t uv_h; /**< Source crop height (0 = image height from uv_y) */
|
||||
} gfx_motion_resource_t;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 1. Segment primitives */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Primitive kind — determines how the renderer draws a segment.
|
||||
*
|
||||
* CAPSULE / RING use control points as endpoint pair / center.
|
||||
* BEZIER_STRIP / BEZIER_LOOP / BEZIER_FILL use a contiguous range of
|
||||
* control points as cubic Bézier controls (n = 3k+1 polygon format).
|
||||
*/
|
||||
typedef enum {
|
||||
GFX_MOTION_SEG_CAPSULE = 0, /**< Thick capsule between joint_a → joint_b */
|
||||
GFX_MOTION_SEG_RING = 1, /**< Hollow ring centred at joint_a */
|
||||
GFX_MOTION_SEG_BEZIER_STRIP = 2, /**< Open thick Bézier curve (e.g. brow) */
|
||||
GFX_MOTION_SEG_BEZIER_LOOP = 3, /**< Closed thick Bézier loop (e.g. mouth outline) */
|
||||
GFX_MOTION_SEG_BEZIER_FILL = 4, /**< Closed filled Bézier shape (e.g. eye sclera) */
|
||||
} gfx_motion_segment_kind_t;
|
||||
|
||||
/** One visual part wiring control points to a rendering primitive. */
|
||||
typedef struct {
|
||||
gfx_motion_segment_kind_t kind;
|
||||
uint16_t joint_a; /**< CAPSULE: start; RING: centre; BEZIER: first ctrl pt */
|
||||
uint16_t joint_b; /**< CAPSULE: end ; unused for RING/BEZIER */
|
||||
uint16_t joint_count; /**< BEZIER_*: number of consecutive control points (n=3k+1) */
|
||||
uint8_t stroke_width; /**< Design-space override; 0 = use layout->stroke_width */
|
||||
uint8_t layer_bit; /**< Visibility layer mask bit (0 = always shown) */
|
||||
int16_t radius_hint; /**< RING: design-space radius */
|
||||
/**
|
||||
* Texture / resource binding.
|
||||
* 0 = solid colour (driven by gfx_motion_player_set_color).
|
||||
* N>0 = use asset->resources[N-1] as the mesh_img image source.
|
||||
*/
|
||||
uint8_t resource_idx;
|
||||
/**
|
||||
* Palette colour index.
|
||||
* 0 = use runtime colour (gfx_motion_player_set_color), not affected by set_color.
|
||||
* N>0 = use asset->color_palette[N-1] (0xRRGGBB) as the fixed segment colour.
|
||||
* set_color() skips palette-coloured segments.
|
||||
*/
|
||||
uint8_t color_idx;
|
||||
/**
|
||||
* Segment opacity 0-255.
|
||||
* 0 is treated as 255 (fully opaque) for zero-init compatibility.
|
||||
*/
|
||||
uint8_t opacity;
|
||||
} gfx_motion_segment_t;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 2. Poses — flat arrays of control point coordinates */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* One pose: flat [x0,y0, x1,y1, …] array, length = joint_count × 2.
|
||||
* joint_count is the ABI field name; conceptually it is the number of
|
||||
* generated control points.
|
||||
* For stickman: x,y = skeleton endpoint position in design space.
|
||||
* For face: x,y = Bézier control point position in design space
|
||||
* (pre-blended from reference shapes + expression weights).
|
||||
*/
|
||||
typedef struct {
|
||||
const int16_t *coords;
|
||||
} gfx_motion_pose_t;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 3. Actions (animation sequences) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** Interpolation style when transitioning into an action step. */
|
||||
typedef enum {
|
||||
GFX_MOTION_INTERP_HOLD = 0, /**< Snap immediately to target pose */
|
||||
GFX_MOTION_INTERP_DAMPED = 1, /**< Exponential ease (damping_div) */
|
||||
} gfx_motion_interp_t;
|
||||
|
||||
/** One step in an action: selects a target pose and how long to hold it. */
|
||||
typedef struct {
|
||||
uint16_t pose_index; /**< Index into gfx_motion_asset_t.poses[] */
|
||||
uint16_t hold_ticks; /**< Timer ticks to hold before advancing */
|
||||
gfx_motion_interp_t interp; /**< Transition style into this step */
|
||||
int8_t facing; /**< 1=right -1=left (mirrors X) */
|
||||
} gfx_motion_action_step_t;
|
||||
|
||||
/** Animation action: a sequence of steps with loop control. */
|
||||
typedef struct {
|
||||
const gfx_motion_action_step_t *steps;
|
||||
uint8_t step_count;
|
||||
bool loop;
|
||||
} gfx_motion_action_t;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 4. Metadata and layout hints */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t version; /**< Must equal GFX_MOTION_SCENE_SCHEMA_VERSION */
|
||||
int32_t viewbox_x;
|
||||
int32_t viewbox_y;
|
||||
int32_t viewbox_w;
|
||||
int32_t viewbox_h;
|
||||
} gfx_motion_meta_t;
|
||||
|
||||
/**
|
||||
* Rendering parameters. Separated from geometry so they can be
|
||||
* overridden without touching the ROM asset.
|
||||
*/
|
||||
typedef struct {
|
||||
int16_t stroke_width; /**< Default capsule / Bézier stroke thickness (design units) */
|
||||
int16_t mirror_x; /**< X axis for facing=-1 horizontal mirroring */
|
||||
int16_t ground_y; /**< Informational floor position */
|
||||
uint16_t timer_period_ms; /**< Action-advance timer period */
|
||||
int16_t damping_div; /**< Divisor for INTERP_DAMPED easing (1 = snap) */
|
||||
} gfx_motion_layout_t;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 5. Top-level asset bundle */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
const gfx_motion_meta_t *meta;
|
||||
|
||||
/** Control point name table (joint_count entries; field name kept for ABI). */
|
||||
const char *const *joint_names;
|
||||
uint16_t joint_count;
|
||||
|
||||
/** Segment wiring (segment_count entries; 0 is valid). */
|
||||
const gfx_motion_segment_t *segments;
|
||||
uint8_t segment_count;
|
||||
|
||||
/** Pose library. */
|
||||
const gfx_motion_pose_t *poses;
|
||||
uint16_t pose_count;
|
||||
|
||||
/** Action library. */
|
||||
const gfx_motion_action_t *actions;
|
||||
uint16_t action_count;
|
||||
|
||||
/** Default playback sequence (action indices). */
|
||||
const uint16_t *sequence;
|
||||
uint16_t sequence_count;
|
||||
|
||||
/** Rendering hints. */
|
||||
const gfx_motion_layout_t *layout;
|
||||
|
||||
/**
|
||||
* Optional texture/image resource table.
|
||||
* Segments reference entries here via segment.resource_idx (1-based).
|
||||
* NULL and resource_count=0 are valid (all segments use solid colour).
|
||||
*/
|
||||
const gfx_motion_resource_t *resources;
|
||||
uint8_t resource_count;
|
||||
|
||||
/**
|
||||
* Optional per-segment colour palette.
|
||||
* Stored as 0xRRGGBB 24-bit values; converted to native pixel at runtime init.
|
||||
* Segments reference entries via segment.color_idx (1-based).
|
||||
* NULL and color_palette_count=0 are valid (all non-resource segments use
|
||||
* the runtime colour set by gfx_motion_player_set_color).
|
||||
*/
|
||||
const uint32_t *color_palette;
|
||||
uint8_t color_palette_count;
|
||||
} gfx_motion_asset_t;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Layer 2 — PARSER runtime state */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Maximum total control points per asset.
|
||||
* Raised beyond 512 so closed-loop rigs can duplicate outline control points
|
||||
* for BEZIER_FILL companions without immediately exhausting the budget.
|
||||
*/
|
||||
#define GFX_MOTION_SCENE_MAX_POINTS 640U
|
||||
|
||||
/**
|
||||
* Maximum control points in a single BEZIER_* segment.
|
||||
* Shared by scene/player code so invalid assets fail early at compile/import time.
|
||||
*/
|
||||
#define GFX_MOTION_SCENE_MAX_SEG_CTRL_POINTS 64U
|
||||
|
||||
typedef struct {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
} gfx_motion_point_t;
|
||||
|
||||
typedef struct {
|
||||
const gfx_motion_asset_t *asset;
|
||||
|
||||
gfx_motion_point_t pose_cur[GFX_MOTION_SCENE_MAX_POINTS]; /**< Current (animated) positions */
|
||||
gfx_motion_point_t pose_tgt[GFX_MOTION_SCENE_MAX_POINTS]; /**< Target positions */
|
||||
|
||||
uint16_t active_action;
|
||||
uint8_t active_step;
|
||||
uint16_t step_ticks;
|
||||
bool action_loop_override_en;
|
||||
bool action_loop_override;
|
||||
bool dirty;
|
||||
} gfx_motion_scene_t;
|
||||
|
||||
esp_err_t gfx_motion_scene_init(gfx_motion_scene_t *scene, const gfx_motion_asset_t *asset);
|
||||
esp_err_t gfx_motion_scene_set_action(gfx_motion_scene_t *scene, uint16_t action_index, bool snap_now);
|
||||
esp_err_t gfx_motion_scene_set_action_loop(gfx_motion_scene_t *scene, bool loop);
|
||||
esp_err_t gfx_motion_scene_clear_action_loop_override(gfx_motion_scene_t *scene);
|
||||
|
||||
/** Ease pose_cur toward pose_tgt one tick. Returns true if any coord changed. */
|
||||
bool gfx_motion_scene_tick(gfx_motion_scene_t *scene);
|
||||
|
||||
/** Advance the action timeline (hold_ticks countdown and step transitions). */
|
||||
void gfx_motion_scene_advance(gfx_motion_scene_t *scene);
|
||||
|
||||
/**
|
||||
* Debug: print active action index, step index, pose index, hold ticks, facing, and interp.
|
||||
* Generated Motion Scene Assets expose action enums in their .inc files; the
|
||||
* parser only sees numeric action/pose indices at runtime.
|
||||
*/
|
||||
void gfx_motion_scene_log_active_step(const gfx_motion_scene_t *scene, const char *reason);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Layer 3 — RUNTIME (unified renderer) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** Maximum mesh_img objects per runtime (one per segment). */
|
||||
#define GFX_MOTION_PLAYER_MAX_SEGMENTS 64U
|
||||
|
||||
/** Maximum colour palette entries (colour_idx 1..GFX_MOTION_PALETTE_MAX). */
|
||||
#define GFX_MOTION_PALETTE_MAX 16U
|
||||
|
||||
/**
|
||||
* Unified animation runtime.
|
||||
*
|
||||
* Owns a gfx_motion_scene_t (scene state) + gfx_motion_t (timer driver) + one gfx_mesh_img
|
||||
* per segment. Dispatches rendering based on segment kind — no separate
|
||||
* "stickman renderer" vs "face renderer".
|
||||
*
|
||||
* Usage:
|
||||
* gfx_motion_player_t player = {0};
|
||||
* gfx_motion_player_init(&player, disp, &my_asset);
|
||||
* gfx_motion_player_set_color(&player, GFX_COLOR_HEX(0xFFFFFF));
|
||||
* gfx_motion_player_set_action(&player, action_index, false);
|
||||
*/
|
||||
typedef struct {
|
||||
gfx_motion_scene_t scene;
|
||||
gfx_motion_t motion;
|
||||
/* ── private ── */
|
||||
gfx_obj_t *seg_objs[GFX_MOTION_PLAYER_MAX_SEGMENTS]; /**< One mesh_img per segment */
|
||||
uint8_t seg_grid_cols[GFX_MOTION_PLAYER_MAX_SEGMENTS];
|
||||
uint8_t seg_grid_rows[GFX_MOTION_PLAYER_MAX_SEGMENTS];
|
||||
uint8_t seg_obj_count;
|
||||
gfx_color_t stroke_color;
|
||||
uint32_t layer_mask;
|
||||
uint16_t solid_pixel;
|
||||
gfx_image_dsc_t solid_img;
|
||||
/** Per-palette-entry native pixels and their 1×1 image descriptors. */
|
||||
uint16_t palette_pixels[GFX_MOTION_PALETTE_MAX];
|
||||
gfx_image_dsc_t palette_imgs[GFX_MOTION_PALETTE_MAX];
|
||||
gfx_coord_t canvas_x;
|
||||
gfx_coord_t canvas_y;
|
||||
uint16_t canvas_w;
|
||||
uint16_t canvas_h;
|
||||
bool mesh_dirty;
|
||||
void *scratch;
|
||||
} gfx_motion_player_t;
|
||||
|
||||
/**
|
||||
* Initialise the player: parse the asset, create mesh objects, and start the motion timer.
|
||||
* Canvas defaults to full display; override with gfx_motion_player_set_canvas().
|
||||
*/
|
||||
esp_err_t gfx_motion_player_init(gfx_motion_player_t *player,
|
||||
gfx_disp_t *disp,
|
||||
const gfx_motion_asset_t *asset);
|
||||
|
||||
/** Destroy all mesh_img objects and stop the motion timer. */
|
||||
void gfx_motion_player_deinit(gfx_motion_player_t *player);
|
||||
|
||||
/** Change the stroke colour for all segments. */
|
||||
esp_err_t gfx_motion_player_set_color(gfx_motion_player_t *player, gfx_color_t color);
|
||||
|
||||
/** Override the canvas region the scene is scaled into. */
|
||||
esp_err_t gfx_motion_player_set_canvas(gfx_motion_player_t *player,
|
||||
gfx_coord_t x, gfx_coord_t y,
|
||||
uint16_t w, uint16_t h);
|
||||
|
||||
/**
|
||||
* Set the visible segment layer mask.
|
||||
*
|
||||
* Segment layer_bit == 0 is always visible. Segment layer_bit N (1..32)
|
||||
* is visible when BIT(N - 1) is set in layer_mask.
|
||||
*/
|
||||
esp_err_t gfx_motion_player_set_layer_mask(gfx_motion_player_t *player, uint32_t layer_mask);
|
||||
|
||||
/** Force the current player state to be applied immediately without advancing time. */
|
||||
esp_err_t gfx_motion_player_sync(gfx_motion_player_t *player);
|
||||
|
||||
/** Switch to an action by index. */
|
||||
esp_err_t gfx_motion_player_set_action(gfx_motion_player_t *player, uint16_t action_idx, bool snap);
|
||||
esp_err_t gfx_motion_player_set_action_loop(gfx_motion_player_t *player, bool loop);
|
||||
esp_err_t gfx_motion_player_clear_action_loop_override(gfx_motion_player_t *player);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/gfx_obj.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* QR Code error correction level
|
||||
*/
|
||||
typedef enum {
|
||||
GFX_QRCODE_ECC_LOW = 0, /**< The QR Code can tolerate about 7% erroneous codewords */
|
||||
GFX_QRCODE_ECC_MEDIUM, /**< The QR Code can tolerate about 15% erroneous codewords */
|
||||
GFX_QRCODE_ECC_QUARTILE, /**< The QR Code can tolerate about 25% erroneous codewords */
|
||||
GFX_QRCODE_ECC_HIGH /**< The QR Code can tolerate about 30% erroneous codewords */
|
||||
} gfx_qrcode_ecc_t;
|
||||
|
||||
/**********************
|
||||
* PUBLIC API
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Create a QR Code object on a display
|
||||
* @param disp Display from gfx_emote_add_disp(handle, &disp_cfg)
|
||||
* @return Pointer to the created QR Code object
|
||||
*/
|
||||
gfx_obj_t *gfx_qrcode_create(gfx_disp_t *disp);
|
||||
|
||||
/* QR code setters */
|
||||
|
||||
/**
|
||||
* @brief Set the data/text for a QR Code object
|
||||
* @param obj Pointer to the QR Code object
|
||||
* @param data Pointer to the null-terminated string to encode
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
* @note The length is automatically calculated using strlen()
|
||||
*/
|
||||
esp_err_t gfx_qrcode_set_data(gfx_obj_t *obj, const char *data);
|
||||
|
||||
/**
|
||||
* @brief Set the size for a QR Code object
|
||||
* @param obj Pointer to the QR Code object
|
||||
* @param size Size in pixels (both width and height)
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_qrcode_set_size(gfx_obj_t *obj, uint16_t size);
|
||||
|
||||
/**
|
||||
* @brief Set the error correction level for a QR Code object
|
||||
* @param obj Pointer to the QR Code object
|
||||
* @param ecc Error correction level
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_qrcode_set_ecc(gfx_obj_t *obj, gfx_qrcode_ecc_t ecc);
|
||||
|
||||
/**
|
||||
* @brief Set the foreground color for a QR Code object
|
||||
* @param obj Pointer to the QR Code object
|
||||
* @param color Foreground color (QR modules color)
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_qrcode_set_color(gfx_obj_t *obj, gfx_color_t color);
|
||||
|
||||
/**
|
||||
* @brief Set the background color for a QR Code object
|
||||
* @param obj Pointer to the QR Code object
|
||||
* @param bg_color Background color
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t gfx_qrcode_set_bg_color(gfx_obj_t *obj, gfx_color_t bg_color);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,222 @@
|
||||
# Image Converter
|
||||
|
||||
将图片转换为 GFX 库支持的格式(RGB565/RGB565A8)的工具脚本。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 支持 RGB565 格式(无透明通道,节省 33% 内存)
|
||||
- ✅ 支持 RGB565A8 格式(带独立 alpha 通道)
|
||||
- ✅ 生成 C 文件或二进制文件
|
||||
- ✅ 支持字节交换(适配不同硬件)
|
||||
- ✅ 批量转换整个目录
|
||||
|
||||
## 格式对比
|
||||
|
||||
| 格式 | 文件大小 (64×64) | 透明支持 | 适用场景 |
|
||||
|-----------|------------------|----------|------------------------|
|
||||
| RGB565 | 8 KB | ❌ | 不透明图标、背景图 |
|
||||
| RGB565A8 | 12 KB | ✅ | 需要透明效果的图标、UI |
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
pip install Pillow
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基础用法
|
||||
|
||||
```bash
|
||||
# 转换为 RGB565A8 格式(默认,带透明通道)
|
||||
python3 image_converter.py image.png
|
||||
|
||||
# 转换为 RGB565 格式(无透明通道,更小)
|
||||
python3 image_converter.py image.png --format rgb565
|
||||
```
|
||||
|
||||
### 高级选项
|
||||
|
||||
```bash
|
||||
# 指定输出目录
|
||||
python3 image_converter.py image.png --output ./output/
|
||||
|
||||
# 生成二进制文件(.bin)而不是 C 文件
|
||||
python3 image_converter.py image.png --bin
|
||||
|
||||
# 启用字节交换(某些硬件需要)
|
||||
python3 image_converter.py image.png --swap16
|
||||
|
||||
# 批量转换目录下所有 PNG 文件
|
||||
python3 image_converter.py ./images/ --output ./converted/
|
||||
|
||||
# 组合使用:RGB565 + 二进制 + 字节交换
|
||||
python3 image_converter.py icon.png --format rgb565 --bin --swap16
|
||||
```
|
||||
|
||||
### 完整参数说明
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|-------------------------|----------------------------------------|-------------|
|
||||
| `input` | 输入文件或目录路径 | 必需 |
|
||||
| `-o, --output` | 输出目录 | 当前目录 |
|
||||
| `-f, --format` | 输出格式:`rgb565` 或 `rgb565a8` | `rgb565a8` |
|
||||
| `--bin` | 生成二进制文件而不是 C 文件 | 关闭 |
|
||||
| `--swap16` | 启用 RGB565 字节交换 | 关闭 |
|
||||
|
||||
## 输出示例
|
||||
|
||||
### C 文件输出 (默认)
|
||||
|
||||
```c
|
||||
#include "gfx.h"
|
||||
|
||||
const uint8_t my_icon_map[] = {
|
||||
0xff, 0xff, 0xff, 0xff, ...
|
||||
};
|
||||
|
||||
const gfx_image_dsc_t my_icon = {
|
||||
.header.cf = GFX_COLOR_FORMAT_RGB565, // 或 GFX_COLOR_FORMAT_RGB565A8
|
||||
.header.magic = C_ARRAY_HEADER_MAGIC,
|
||||
.header.w = 64,
|
||||
.header.h = 64,
|
||||
.data_size = 8192, // RGB565: width*height*2, RGB565A8: width*height*3
|
||||
.data = my_icon_map,
|
||||
};
|
||||
```
|
||||
|
||||
### 二进制文件输出 (--bin)
|
||||
|
||||
```
|
||||
[12 bytes header]
|
||||
[image data]
|
||||
|
||||
Header 结构:
|
||||
- magic (0x19)
|
||||
- cf (0x04=RGB565, 0x0A=RGB565A8)
|
||||
- width, height
|
||||
- stride
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1: 创建不透明图标
|
||||
|
||||
```bash
|
||||
# 转换不需要透明的图标,节省内存
|
||||
python3 image_converter.py logo.png --format rgb565
|
||||
```
|
||||
|
||||
生成的 C 文件可以这样使用:
|
||||
|
||||
```c
|
||||
#include "logo.c"
|
||||
|
||||
gfx_obj_t *img = gfx_img_create(handle);
|
||||
gfx_img_set_src(img, (void *)&logo);
|
||||
gfx_obj_align(img, GFX_ALIGN_CENTER, 0, 0);
|
||||
```
|
||||
|
||||
### 示例 2: 创建带透明效果的 UI 元素
|
||||
|
||||
```bash
|
||||
# 转换需要透明效果的图标
|
||||
python3 image_converter.py button.png --format rgb565a8
|
||||
```
|
||||
|
||||
### 示例 3: 批量转换资源目录
|
||||
|
||||
```bash
|
||||
# 转换 assets 目录下所有 PNG 为 RGB565 格式
|
||||
python3 image_converter.py ./assets/ \
|
||||
--format rgb565 \
|
||||
--output ./src/images/
|
||||
```
|
||||
|
||||
### 示例 4: 为特定硬件生成二进制文件
|
||||
|
||||
```bash
|
||||
# 生成字节交换的二进制文件
|
||||
python3 image_converter.py icon.png \
|
||||
--format rgb565 \
|
||||
--bin \
|
||||
--swap16 \
|
||||
--output ./flash_data/
|
||||
```
|
||||
|
||||
## 数据布局
|
||||
|
||||
### RGB565 格式
|
||||
```
|
||||
[RGB565 pixel data]
|
||||
- Size: width × height × 2 bytes
|
||||
```
|
||||
|
||||
### RGB565A8 格式
|
||||
```
|
||||
[RGB565 pixel data] [Alpha mask data]
|
||||
- RGB565 size: width × height × 2 bytes
|
||||
- Alpha size: width × height × 1 byte
|
||||
- Total: width × height × 3 bytes
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 什么时候用 RGB565,什么时候用 RGB565A8?
|
||||
|
||||
**A:**
|
||||
- **RGB565**: 不需要透明效果的图片(如背景、logo、纯色图标)
|
||||
- **RGB565A8**: 需要透明或半透明效果的图片(如 UI 元素、图标)
|
||||
|
||||
### Q: 什么时候需要 --swap16?
|
||||
|
||||
**A:** 当目标硬件的字节序与生成的不匹配时使用。通常 ESP32 不需要此选项。
|
||||
|
||||
### Q: C 文件和二进制文件的区别?
|
||||
|
||||
**A:**
|
||||
- **C 文件**: 直接编译到程序中,访问速度快,但增加程序大小
|
||||
- **二进制文件**: 存储在外部存储(如 SPIFFS/SD卡),节省程序空间,但需要运行时加载
|
||||
|
||||
### Q: 如何查看生成的文件信息?
|
||||
|
||||
**A:** 运行脚本时会输出详细信息:
|
||||
```
|
||||
Successfully generated output.c
|
||||
Format: RGB565
|
||||
Image size: 64x64
|
||||
Total data size: 8192 bytes
|
||||
RGB565 data: 8192 bytes (4096 pixels)
|
||||
Swap16: disabled
|
||||
```
|
||||
|
||||
## 与现有代码兼容
|
||||
|
||||
该工具生成的文件与现有的 `gfx_img` API 完全兼容:
|
||||
|
||||
```c
|
||||
// 两种格式使用方式完全相同
|
||||
gfx_obj_t *img1 = gfx_img_create(handle);
|
||||
gfx_img_set_src(img1, &rgb565_image); // RGB565 图片
|
||||
|
||||
gfx_obj_t *img2 = gfx_img_create(handle);
|
||||
gfx_img_set_src(img2, &rgb565a8_image); // RGB565A8 图片
|
||||
|
||||
// 库会自动检测格式并正确渲染
|
||||
```
|
||||
|
||||
## 性能对比
|
||||
|
||||
基于 ESP32-S3 测试(64×64 像素图片):
|
||||
|
||||
| 格式 | 内存占用 | 加载时间 | 渲染帧率 |
|
||||
|-----------|----------|----------|----------|
|
||||
| RGB565 | 8 KB | ~5ms | ~60 FPS |
|
||||
| RGB565A8 | 12 KB | ~7ms | ~45 FPS |
|
||||
|
||||
## 许可证
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Copyright 2025 Espressif Systems (Shanghai) CO LTD
|
||||
|
||||
@ -0,0 +1,344 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
PNG to RGB565/RGB565A8 C file converter
|
||||
Converts PNG images to RGB565 or RGB565A8 format with optional byte swapping
|
||||
RGB565: Pure RGB565 format without alpha channel
|
||||
RGB565A8: RGB565 with separate alpha channel
|
||||
Supports both C file and binary output formats
|
||||
Can process single files or batch process all PNG files in a directory
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from PIL import Image
|
||||
import re
|
||||
import struct
|
||||
import glob
|
||||
|
||||
def rgb888_to_rgb565(r, g, b):
|
||||
"""Convert RGB888 to RGB565"""
|
||||
r = (r >> 3) & 0x1F
|
||||
g = (g >> 2) & 0x3F
|
||||
b = (b >> 3) & 0x1F
|
||||
return (r << 11) | (g << 5) | b
|
||||
|
||||
def rgb565_to_bytes(rgb565, swap16=False):
|
||||
"""Convert RGB565 to bytes, optionally swapping byte order"""
|
||||
high_byte = (rgb565 >> 8) & 0xFF
|
||||
low_byte = rgb565 & 0xFF
|
||||
|
||||
if swap16:
|
||||
return [low_byte, high_byte]
|
||||
else:
|
||||
return [high_byte, low_byte]
|
||||
|
||||
def format_array(data, indent=4, per_line=130):
|
||||
"""Format data as C array with proper indentation and line breaks"""
|
||||
lines = []
|
||||
for i in range(0, len(data), per_line):
|
||||
line = ', '.join(f'0x{b:02x}' for b in data[i:i + per_line])
|
||||
lines.append(' ' * indent + line + ',')
|
||||
return '\n'.join(lines)
|
||||
|
||||
def generate_c_file(image_path, output_path, var_name, swap16=False, use_alpha=True):
|
||||
"""Generate C file from PNG image
|
||||
|
||||
Args:
|
||||
image_path: Input PNG file path
|
||||
output_path: Output C file path
|
||||
var_name: Variable name for the C array
|
||||
swap16: Enable byte swapping for RGB565
|
||||
use_alpha: True for RGB565A8, False for RGB565
|
||||
"""
|
||||
|
||||
# Open and convert image
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
except Exception as e:
|
||||
print(f'Error opening image {image_path}: {e}')
|
||||
return False
|
||||
|
||||
width, height = img.size
|
||||
pixels = list(img.getdata())
|
||||
|
||||
# Convert to RGB565 format
|
||||
rgb565_data = []
|
||||
alpha_data = []
|
||||
|
||||
for pixel in pixels:
|
||||
r, g, b, a = pixel
|
||||
|
||||
# Convert RGB to RGB565
|
||||
rgb565 = rgb888_to_rgb565(r, g, b)
|
||||
|
||||
# Add RGB565 bytes (2 bytes) to RGB565 array
|
||||
rgb565_bytes = rgb565_to_bytes(rgb565, swap16)
|
||||
rgb565_data.extend(rgb565_bytes)
|
||||
|
||||
# Add Alpha byte (1 byte) to Alpha array if needed
|
||||
if use_alpha:
|
||||
alpha_data.append(a)
|
||||
|
||||
# Combine data based on format
|
||||
if use_alpha:
|
||||
# RGB565A8: RGB565 first, then Alpha
|
||||
final_data = rgb565_data + alpha_data
|
||||
color_format = 'GFX_COLOR_FORMAT_RGB565A8'
|
||||
format_name = 'RGB565A8'
|
||||
else:
|
||||
# RGB565: Only RGB565 data
|
||||
final_data = rgb565_data
|
||||
color_format = 'GFX_COLOR_FORMAT_RGB565'
|
||||
format_name = 'RGB565'
|
||||
|
||||
# Generate C file content
|
||||
c_content = f"""#include "gfx.h"
|
||||
|
||||
const uint8_t {var_name}_map[] = {{
|
||||
{format_array(final_data)}
|
||||
}};
|
||||
|
||||
const gfx_image_dsc_t {var_name} = {{
|
||||
.header.cf = {color_format},
|
||||
.header.magic = C_ARRAY_HEADER_MAGIC,
|
||||
.header.w = {width},
|
||||
.header.h = {height},
|
||||
.data_size = {len(final_data)},
|
||||
.data = {var_name}_map,
|
||||
}};
|
||||
"""
|
||||
|
||||
# Write to file
|
||||
try:
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(c_content)
|
||||
print(f'Successfully generated {output_path}')
|
||||
print(f'Format: {format_name}')
|
||||
print(f'Image size: {width}x{height}')
|
||||
print(f'Total data size: {len(final_data)} bytes')
|
||||
print(f'RGB565 data: {len(rgb565_data)} bytes ({width * height * 2} bytes)')
|
||||
if use_alpha:
|
||||
print(f'Alpha data: {len(alpha_data)} bytes ({width * height} bytes)')
|
||||
print(f"Swap16: {'enabled' if swap16 else 'disabled'}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'Error writing file {output_path}: {e}')
|
||||
return False
|
||||
|
||||
def generate_bin_file(image_path, output_path, swap16=False, use_alpha=True):
|
||||
"""Generate binary file from PNG image with header compatible with gfx_image_header_t structure
|
||||
|
||||
Args:
|
||||
image_path: Input PNG file path
|
||||
output_path: Output binary file path
|
||||
swap16: Enable byte swapping for RGB565
|
||||
use_alpha: True for RGB565A8, False for RGB565
|
||||
"""
|
||||
|
||||
# Open and convert image
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
except Exception as e:
|
||||
print(f'Error opening image {image_path}: {e}')
|
||||
return False
|
||||
|
||||
width, height = img.size
|
||||
pixels = list(img.getdata())
|
||||
|
||||
# Convert to RGB565 format
|
||||
rgb565_data = []
|
||||
alpha_data = []
|
||||
|
||||
for pixel in pixels:
|
||||
r, g, b, a = pixel
|
||||
|
||||
# Convert RGB to RGB565
|
||||
rgb565 = rgb888_to_rgb565(r, g, b)
|
||||
|
||||
# Add RGB565 bytes (2 bytes) to RGB565 array
|
||||
rgb565_bytes = rgb565_to_bytes(rgb565, swap16)
|
||||
rgb565_data.extend(rgb565_bytes)
|
||||
|
||||
# Add Alpha byte (1 byte) to Alpha array if needed
|
||||
if use_alpha:
|
||||
alpha_data.append(a)
|
||||
|
||||
# Combine data based on format
|
||||
if use_alpha:
|
||||
# RGB565A8: RGB565 first, then Alpha
|
||||
final_data = rgb565_data + alpha_data
|
||||
cf = 0x0A # GFX_COLOR_FORMAT_RGB565A8
|
||||
stride = width * 2 # Stride is only for RGB565 data
|
||||
format_name = 'RGB565A8'
|
||||
else:
|
||||
# RGB565: Only RGB565 data
|
||||
final_data = rgb565_data
|
||||
cf = 0x04 # GFX_COLOR_FORMAT_RGB565
|
||||
stride = width * 2
|
||||
format_name = 'RGB565'
|
||||
|
||||
# Create gfx_image_header_t structure (12 bytes total)
|
||||
magic = 0x19 # C_ARRAY_HEADER_MAGIC
|
||||
flags = 0x0000 # No special flags
|
||||
reserved = 0x0000 # Reserved field
|
||||
|
||||
# Pack gfx_image_header_t as bit fields in 3 uint32_t values
|
||||
# First uint32: magic(8) + cf(8) + flags(16)
|
||||
header_word1 = (magic & 0xFF) | ((cf & 0xFF) << 8) | ((flags & 0xFFFF) << 16)
|
||||
|
||||
# Second uint32: w(16) + h(16)
|
||||
header_word2 = (width & 0xFFFF) | ((height & 0xFFFF) << 16)
|
||||
|
||||
# Third uint32: stride(16) + reserved(16)
|
||||
header_word3 = (stride & 0xFFFF) | ((reserved & 0xFFFF) << 16)
|
||||
|
||||
# Pack header structure - use little-endian for ESP32 compatibility
|
||||
# Layout: header_word1(4) + header_word2(4) + header_word3(4) = 12 bytes total
|
||||
header = struct.pack('<III', header_word1, header_word2, header_word3)
|
||||
|
||||
# Write binary file: header (12 bytes) + image data
|
||||
try:
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(header)
|
||||
f.write(bytes(final_data))
|
||||
print(f'Successfully generated {output_path}')
|
||||
print(f'Format: {format_name}')
|
||||
print(f'Image size: {width}x{height}')
|
||||
print(f'Header size: {len(header)} bytes')
|
||||
print(f'Total data size: {len(final_data)} bytes')
|
||||
print(f'RGB565 data: {len(rgb565_data)} bytes ({width * height * 2} bytes)')
|
||||
if use_alpha:
|
||||
print(f'Alpha data: {len(alpha_data)} bytes ({width * height} bytes)')
|
||||
print(f'Stride: {stride} bytes per row')
|
||||
print(f'Data offset: 12 bytes')
|
||||
print(f'Total file size: {len(header) + len(final_data)} bytes')
|
||||
print(f"Swap16: {'enabled' if swap16 else 'disabled'}")
|
||||
print(f'Header layout: magic=0x{magic:02x}, cf=0x{cf:02x}, flags=0x{flags:04x}')
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'Error writing file {output_path}: {e}')
|
||||
return False
|
||||
|
||||
def process_single_file(input_file, output_dir, bin_format, swap16, use_alpha):
|
||||
"""Process a single PNG file"""
|
||||
# Determine output path and variable name from input filename
|
||||
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
||||
|
||||
if bin_format:
|
||||
# Output binary file
|
||||
output_path = os.path.join(output_dir, f'{base_name}.bin')
|
||||
return generate_bin_file(input_file, output_path, swap16, use_alpha)
|
||||
else:
|
||||
# Output C file
|
||||
output_path = os.path.join(output_dir, f'{base_name}.c')
|
||||
# Convert to valid C identifier
|
||||
var_name = re.sub(r'[^a-zA-Z0-9_]', '_', base_name)
|
||||
if var_name[0].isdigit():
|
||||
var_name = 'img_' + var_name
|
||||
return generate_c_file(input_file, output_path, var_name, swap16, use_alpha)
|
||||
|
||||
def find_png_files(input_path):
|
||||
"""Find all PNG files in the given path"""
|
||||
png_files = []
|
||||
|
||||
if os.path.isfile(input_path):
|
||||
# Single file
|
||||
if input_path.lower().endswith('.png'):
|
||||
png_files.append(input_path)
|
||||
else:
|
||||
print("Warning: Input file doesn't have .png extension")
|
||||
png_files.append(input_path)
|
||||
elif os.path.isdir(input_path):
|
||||
# Directory - find all PNG files
|
||||
png_pattern = os.path.join(input_path, '*.png')
|
||||
png_files = glob.glob(png_pattern)
|
||||
|
||||
# Also search in subdirectories
|
||||
png_pattern_recursive = os.path.join(input_path, '**', '*.png')
|
||||
png_files.extend(glob.glob(png_pattern_recursive, recursive=True))
|
||||
|
||||
# Remove duplicates and sort
|
||||
png_files = sorted(list(set(png_files)))
|
||||
|
||||
return png_files
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Convert PNG to RGB565 or RGB565A8 format',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Convert to RGB565A8 (with alpha) C file
|
||||
%(prog)s image.png
|
||||
|
||||
# Convert to RGB565 (without alpha) C file
|
||||
%(prog)s image.png --format rgb565
|
||||
|
||||
# Convert to binary format with byte swapping
|
||||
%(prog)s image.png --bin --swap16
|
||||
|
||||
# Batch convert all PNG files in directory
|
||||
%(prog)s images/ --output output/
|
||||
"""
|
||||
)
|
||||
parser.add_argument('input', help='Input PNG file path or directory path')
|
||||
parser.add_argument('--output', '-o', help='Output directory (default: current directory)')
|
||||
parser.add_argument('--bin', action='store_true', help='Output binary format instead of C file')
|
||||
parser.add_argument('--swap16', action='store_true', help='Enable byte swapping for RGB565')
|
||||
parser.add_argument('--format', '-f', choices=['rgb565', 'rgb565a8'], default='rgb565a8',
|
||||
help='Output format: rgb565 (no alpha) or rgb565a8 (with alpha, default)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate input path
|
||||
if not os.path.exists(args.input):
|
||||
print(f"Error: Input path '{args.input}' does not exist")
|
||||
return 1
|
||||
|
||||
# Set output directory
|
||||
output_dir = args.output if args.output else '.'
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
# Determine if alpha channel should be included
|
||||
use_alpha = (args.format == 'rgb565a8')
|
||||
|
||||
# Find all PNG files
|
||||
png_files = find_png_files(args.input)
|
||||
|
||||
if not png_files:
|
||||
print(f"No PNG files found in '{args.input}'")
|
||||
return 1
|
||||
|
||||
print(f'Found {len(png_files)} PNG file(s) to process:')
|
||||
for png_file in png_files:
|
||||
print(f' - {png_file}')
|
||||
print(f'Output format: {args.format.upper()}')
|
||||
print(f'Output type: {"Binary" if args.bin else "C file"}')
|
||||
print(f'Byte swap: {"Enabled" if args.swap16 else "Disabled"}')
|
||||
print()
|
||||
|
||||
# Process each PNG file
|
||||
success_count = 0
|
||||
for png_file in png_files:
|
||||
print(f'Processing: {png_file}')
|
||||
if process_single_file(png_file, output_dir, args.bin, args.swap16, use_alpha):
|
||||
success_count += 1
|
||||
print() # Add blank line between files
|
||||
|
||||
print(f'Processing complete: {success_count}/{len(png_files)} files processed successfully')
|
||||
|
||||
if success_count == len(png_files):
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Common macro definitions for renderer modules
|
||||
#ifndef MAX
|
||||
#define MAX(a,b) ((a) > (b) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
#ifndef MIN
|
||||
#define MIN(a,b) ((a) < (b) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
#ifndef CONTAINER_OF
|
||||
#define CONTAINER_OF(ptr, type, member) \
|
||||
((type *)((char *)(ptr) - offsetof(type, member)))
|
||||
#endif
|
||||
|
||||
/* Generic NULL-check utilities */
|
||||
#ifndef GFX_IS_NULL
|
||||
#define GFX_IS_NULL(p) ((p) == NULL)
|
||||
#endif
|
||||
|
||||
#ifndef GFX_NOT_NULL
|
||||
#define GFX_NOT_NULL(p) ((p) != NULL)
|
||||
#endif
|
||||
|
||||
#ifndef GFX_RETURN_IF_NULL
|
||||
#define GFX_RETURN_IF_NULL(p, retval) do { if ((p) == NULL) { return (retval); } } while (0)
|
||||
#endif
|
||||
|
||||
#ifndef GFX_RETURN_IF_NULL_VOID
|
||||
#define GFX_RETURN_IF_NULL_VOID(p) do { if ((p) == NULL) { return; } } while (0)
|
||||
#endif
|
||||
|
||||
/* Generic object type checking macro */
|
||||
#define CHECK_OBJ_TYPE(obj, expected_type, tag) \
|
||||
do { \
|
||||
ESP_RETURN_ON_FALSE(obj, ESP_ERR_INVALID_ARG, tag, "Object is NULL"); \
|
||||
ESP_RETURN_ON_FALSE((obj)->type == (expected_type), ESP_ERR_INVALID_ARG, tag, \
|
||||
"Object type mismatch (expected=%d, actual=%d)", (expected_type), (obj)->type); \
|
||||
} while(0)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include("sdkconfig.h")
|
||||
#include "sdkconfig.h"
|
||||
#define GFX_CONFIG_HAS_SDKCONFIG 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef GFX_CONFIG_HAS_SDKCONFIG
|
||||
#define GFX_CONFIG_HAS_SDKCONFIG 0
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* Software Blend
|
||||
*********************/
|
||||
|
||||
#ifdef CONFIG_GFX_MESH_IMG_SCANLINE_MAX_VERTS
|
||||
#define GFX_MESH_IMG_SCANLINE_MAX_VERTS CONFIG_GFX_MESH_IMG_SCANLINE_MAX_VERTS
|
||||
#else
|
||||
#define GFX_MESH_IMG_SCANLINE_MAX_VERTS 512U
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_BLEND_POLYGON_MAX_INTERSECTIONS
|
||||
#define GFX_BLEND_POLYGON_MAX_INTERSECTIONS CONFIG_GFX_BLEND_POLYGON_MAX_INTERSECTIONS
|
||||
#else
|
||||
#define GFX_BLEND_POLYGON_MAX_INTERSECTIONS 64
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_BLEND_POLYGON_SUB_SAMPLES
|
||||
#define GFX_BLEND_POLYGON_SUB_SAMPLES CONFIG_GFX_BLEND_POLYGON_SUB_SAMPLES
|
||||
#else
|
||||
#define GFX_BLEND_POLYGON_SUB_SAMPLES 8
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_BLEND_POLYGON_COVERAGE_MAX_WIDTH
|
||||
#define GFX_BLEND_POLYGON_COVERAGE_MAX_WIDTH CONFIG_GFX_BLEND_POLYGON_COVERAGE_MAX_WIDTH
|
||||
#else
|
||||
#define GFX_BLEND_POLYGON_COVERAGE_MAX_WIDTH 512
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_BLEND_POLYGON_INWARD_AA
|
||||
#define GFX_BLEND_POLYGON_INWARD_AA 1
|
||||
#elif GFX_CONFIG_HAS_SDKCONFIG
|
||||
#define GFX_BLEND_POLYGON_INWARD_AA 0
|
||||
#else
|
||||
#define GFX_BLEND_POLYGON_INWARD_AA 1
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_BLEND_POLYGON_SOLID_HARD_EDGE
|
||||
#define GFX_BLEND_POLYGON_SOLID_HARD_EDGE 1
|
||||
#elif GFX_CONFIG_HAS_SDKCONFIG
|
||||
#define GFX_BLEND_POLYGON_SOLID_HARD_EDGE 0
|
||||
#else
|
||||
#define GFX_BLEND_POLYGON_SOLID_HARD_EDGE 1
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* Motion Widget
|
||||
*********************/
|
||||
|
||||
#define GFX_MOTION_DEFAULT_TIMER_PERIOD_MS 33U
|
||||
#define GFX_MOTION_DEFAULT_DAMPING_DIV 4
|
||||
#define GFX_MOTION_DEFAULT_STROKE_COLOR 0x1F1F1F
|
||||
#define GFX_MOTION_DEFAULT_SEG_OPACITY 0xFFU
|
||||
|
||||
#define GFX_MOTION_RING_SEGS_MIN 16U
|
||||
#define GFX_MOTION_RING_SEGS_MAX 48U
|
||||
|
||||
#ifdef CONFIG_GFX_MOTION_BEZIER_STROKE_SEGS_PER_SEG
|
||||
#define GFX_MOTION_BEZIER_STROKE_SEGS_PER_SEG CONFIG_GFX_MOTION_BEZIER_STROKE_SEGS_PER_SEG
|
||||
#else
|
||||
#define GFX_MOTION_BEZIER_STROKE_SEGS_PER_SEG 6
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_MOTION_BEZIER_FILL_LOOP_SEGS_PER_SEG
|
||||
#define GFX_MOTION_BEZIER_FILL_LOOP_SEGS_PER_SEG CONFIG_GFX_MOTION_BEZIER_FILL_LOOP_SEGS_PER_SEG
|
||||
#else
|
||||
#define GFX_MOTION_BEZIER_FILL_LOOP_SEGS_PER_SEG 12
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_MOTION_BEZIER_FILL_SEGS
|
||||
#define GFX_MOTION_BEZIER_FILL_SEGS CONFIG_GFX_MOTION_BEZIER_FILL_SEGS
|
||||
#else
|
||||
#define GFX_MOTION_BEZIER_FILL_SEGS 24
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_MOTION_HUB_FILL_MAX_POINTS
|
||||
#define GFX_MOTION_HUB_FILL_MAX_POINTS CONFIG_GFX_MOTION_HUB_FILL_MAX_POINTS
|
||||
#else
|
||||
#define GFX_MOTION_HUB_FILL_MAX_POINTS 512
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_MOTION_BEZIER_FILL_RASTERIZER_TRIANGLE
|
||||
#define GFX_MOTION_BEZIER_FILL_USE_SCANLINE 0
|
||||
#else
|
||||
#define GFX_MOTION_BEZIER_FILL_USE_SCANLINE 1
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* Label Widget
|
||||
*********************/
|
||||
|
||||
#ifdef CONFIG_GFX_LABEL_GLYPH_CACHE_MAX_ENTRIES
|
||||
#define GFX_LABEL_GLYPH_CACHE_MAX_ENTRIES CONFIG_GFX_LABEL_GLYPH_CACHE_MAX_ENTRIES
|
||||
#else
|
||||
#define GFX_LABEL_GLYPH_CACHE_MAX_ENTRIES 64
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_LABEL_GLYPH_CACHE_MAX_BITMAP_BYTES
|
||||
#define GFX_LABEL_GLYPH_CACHE_MAX_BITMAP_BYTES CONFIG_GFX_LABEL_GLYPH_CACHE_MAX_BITMAP_BYTES
|
||||
#else
|
||||
#define GFX_LABEL_GLYPH_CACHE_MAX_BITMAP_BYTES (12 * 1024)
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GFX_LABEL_GLYPH_ATLAS_PAGE_BYTES
|
||||
#define GFX_LABEL_GLYPH_ATLAS_PAGE_BYTES CONFIG_GFX_LABEL_GLYPH_ATLAS_PAGE_BYTES
|
||||
#else
|
||||
#define GFX_LABEL_GLYPH_ATLAS_PAGE_BYTES 1024
|
||||
#endif
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/gfx_log.h"
|
||||
|
||||
#ifndef GFX_LOG_MODULE
|
||||
#error "GFX_LOG_MODULE must be defined before including common/gfx_log_priv.h"
|
||||
#endif
|
||||
|
||||
#define GFX_LOG_WRITE(level, tag, format, ...) \
|
||||
do { \
|
||||
if (gfx_log_should_output(GFX_LOG_MODULE, level)) { \
|
||||
gfx_log_write(GFX_LOG_MODULE, level, tag, format, ##__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define GFX_LOGE(tag, format, ...) GFX_LOG_WRITE(GFX_LOG_LEVEL_ERROR, tag, format, ##__VA_ARGS__)
|
||||
#define GFX_LOGW(tag, format, ...) GFX_LOG_WRITE(GFX_LOG_LEVEL_WARN, tag, format, ##__VA_ARGS__)
|
||||
#define GFX_LOGI(tag, format, ...) GFX_LOG_WRITE(GFX_LOG_LEVEL_INFO, tag, format, ##__VA_ARGS__)
|
||||
#define GFX_LOGD(tag, format, ...) GFX_LOG_WRITE(GFX_LOG_LEVEL_DEBUG, tag, format, ##__VA_ARGS__)
|
||||
#define GFX_LOGV(tag, format, ...) GFX_LOG_WRITE(GFX_LOG_LEVEL_VERBOSE, tag, format, ##__VA_ARGS__)
|
||||
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "core/gfx_types.h"
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* STATIC PROTOTYPES
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* GLOBAL FUNCTIONS
|
||||
**********************/
|
||||
|
||||
gfx_color_t gfx_color_hex(uint32_t c)
|
||||
{
|
||||
gfx_color_t r;
|
||||
r.full = (uint16_t)(((c & 0xF80000) >> 8) | ((c & 0xFC00) >> 5) | ((c & 0xFF) >> 3));
|
||||
return r;
|
||||
}
|
||||
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*********************
|
||||
* INCLUDES
|
||||
*********************/
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "core/gfx_log.h"
|
||||
|
||||
/**********************
|
||||
* STATIC VARIABLES
|
||||
**********************/
|
||||
|
||||
static const char *s_module_names[GFX_LOG_MODULE_COUNT] = {
|
||||
[GFX_LOG_MODULE_CORE] = "core",
|
||||
[GFX_LOG_MODULE_DISP] = "disp",
|
||||
[GFX_LOG_MODULE_OBJ] = "obj",
|
||||
[GFX_LOG_MODULE_REFR] = "refr",
|
||||
[GFX_LOG_MODULE_RENDER] = "render",
|
||||
[GFX_LOG_MODULE_TIMER] = "timer",
|
||||
[GFX_LOG_MODULE_TOUCH] = "touch",
|
||||
[GFX_LOG_MODULE_IMG_DEC] = "img_dec",
|
||||
[GFX_LOG_MODULE_LABEL] = "label",
|
||||
[GFX_LOG_MODULE_LABEL_OBJ] = "label_obj",
|
||||
[GFX_LOG_MODULE_DRAW_LABEL] = "draw_label",
|
||||
[GFX_LOG_MODULE_FONT_LV] = "font_lv",
|
||||
[GFX_LOG_MODULE_FONT_FT] = "font_ft",
|
||||
[GFX_LOG_MODULE_IMG] = "img",
|
||||
[GFX_LOG_MODULE_QRCODE] = "qrcode",
|
||||
[GFX_LOG_MODULE_BUTTON] = "button",
|
||||
[GFX_LOG_MODULE_ANIM] = "anim",
|
||||
[GFX_LOG_MODULE_ANIM_DEC] = "anim_dec",
|
||||
[GFX_LOG_MODULE_MOTION] = "motion",
|
||||
[GFX_LOG_MODULE_EAF_DEC] = "eaf_dec",
|
||||
[GFX_LOG_MODULE_QRCODE_LIB] = "qrcode_lib",
|
||||
};
|
||||
|
||||
static gfx_log_level_t s_module_levels[GFX_LOG_MODULE_COUNT];
|
||||
static bool s_log_levels_initialized;
|
||||
|
||||
/**********************
|
||||
* STATIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
#define GFX_LOG_COLOR_RED "\033[0;31m"
|
||||
#define GFX_LOG_COLOR_YELLOW "\033[0;33m"
|
||||
#define GFX_LOG_COLOR_GREEN "\033[0;32m"
|
||||
#define GFX_LOG_COLOR_CYAN "\033[0;36m"
|
||||
#define GFX_LOG_COLOR_WHITE "\033[0;37m"
|
||||
#define GFX_LOG_COLOR_RESET "\033[0m"
|
||||
|
||||
static char gfx_log_level_to_char(gfx_log_level_t level)
|
||||
{
|
||||
switch (level) {
|
||||
case GFX_LOG_LEVEL_ERROR:
|
||||
return 'E';
|
||||
case GFX_LOG_LEVEL_WARN:
|
||||
return 'W';
|
||||
case GFX_LOG_LEVEL_INFO:
|
||||
return 'I';
|
||||
case GFX_LOG_LEVEL_DEBUG:
|
||||
return 'D';
|
||||
case GFX_LOG_LEVEL_VERBOSE:
|
||||
return 'V';
|
||||
case GFX_LOG_LEVEL_NONE:
|
||||
default:
|
||||
return 'N';
|
||||
}
|
||||
}
|
||||
|
||||
static const char *gfx_log_level_to_color(gfx_log_level_t level)
|
||||
{
|
||||
switch (level) {
|
||||
case GFX_LOG_LEVEL_ERROR:
|
||||
return GFX_LOG_COLOR_RED;
|
||||
case GFX_LOG_LEVEL_WARN:
|
||||
return GFX_LOG_COLOR_YELLOW;
|
||||
case GFX_LOG_LEVEL_INFO:
|
||||
return GFX_LOG_COLOR_GREEN;
|
||||
case GFX_LOG_LEVEL_DEBUG:
|
||||
return GFX_LOG_COLOR_CYAN;
|
||||
case GFX_LOG_LEVEL_VERBOSE:
|
||||
return GFX_LOG_COLOR_WHITE;
|
||||
case GFX_LOG_LEVEL_NONE:
|
||||
default:
|
||||
return GFX_LOG_COLOR_RESET;
|
||||
}
|
||||
}
|
||||
|
||||
static void gfx_log_init_levels(void)
|
||||
{
|
||||
if (s_log_levels_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < GFX_LOG_MODULE_COUNT; i++) {
|
||||
s_module_levels[i] = GFX_LOG_LEVEL_INFO;
|
||||
}
|
||||
|
||||
s_log_levels_initialized = true;
|
||||
}
|
||||
|
||||
/**********************
|
||||
* PUBLIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
void gfx_log_set_level(gfx_log_module_t module, gfx_log_level_t level)
|
||||
{
|
||||
gfx_log_init_levels();
|
||||
|
||||
if (module < 0 || module >= GFX_LOG_MODULE_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_module_levels[module] = level;
|
||||
}
|
||||
|
||||
gfx_log_level_t gfx_log_get_level(gfx_log_module_t module)
|
||||
{
|
||||
gfx_log_init_levels();
|
||||
|
||||
if (module < 0 || module >= GFX_LOG_MODULE_COUNT) {
|
||||
return GFX_LOG_LEVEL_NONE;
|
||||
}
|
||||
|
||||
return s_module_levels[module];
|
||||
}
|
||||
|
||||
void gfx_log_set_level_all(gfx_log_level_t level)
|
||||
{
|
||||
gfx_log_init_levels();
|
||||
|
||||
for (int i = 0; i < GFX_LOG_MODULE_COUNT; i++) {
|
||||
s_module_levels[i] = level;
|
||||
}
|
||||
}
|
||||
|
||||
bool gfx_log_should_output(gfx_log_module_t module, gfx_log_level_t level)
|
||||
{
|
||||
gfx_log_init_levels();
|
||||
|
||||
if (module < 0 || module >= GFX_LOG_MODULE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (level == GFX_LOG_LEVEL_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return level <= s_module_levels[module];
|
||||
}
|
||||
|
||||
const char *gfx_log_module_name(gfx_log_module_t module)
|
||||
{
|
||||
if (module < 0 || module >= GFX_LOG_MODULE_COUNT) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return s_module_names[module];
|
||||
}
|
||||
|
||||
void gfx_log_writev(gfx_log_module_t module, gfx_log_level_t level, const char *tag, const char *format, va_list args)
|
||||
{
|
||||
const char *module_name;
|
||||
const char *color;
|
||||
int64_t ts_us;
|
||||
|
||||
if (!gfx_log_should_output(module, level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
module_name = gfx_log_module_name(module);
|
||||
color = gfx_log_level_to_color(level);
|
||||
ts_us = esp_timer_get_time();
|
||||
|
||||
if (tag != NULL && tag[0] != '\0' && strcmp(tag, module_name) != 0) {
|
||||
printf("%s%c (%" PRIi64 ") %s/%s: ", color, gfx_log_level_to_char(level), ts_us / 1000, module_name, tag);
|
||||
} else {
|
||||
printf("%s%c (%" PRIi64 ") %s: ", color, gfx_log_level_to_char(level), ts_us / 1000, module_name);
|
||||
}
|
||||
|
||||
vprintf(format, args);
|
||||
printf("%s\n", GFX_LOG_COLOR_RESET);
|
||||
}
|
||||
|
||||
void gfx_log_write(gfx_log_module_t module, gfx_log_level_t level, const char *tag, const char *format, ...)
|
||||
{
|
||||
va_list args;
|
||||
|
||||
va_start(args, format);
|
||||
gfx_log_writev(module, level, tag, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
@ -0,0 +1,436 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*********************
|
||||
* INCLUDES
|
||||
*********************/
|
||||
#include <string.h>
|
||||
#define GFX_LOG_MODULE GFX_LOG_MODULE_DISP
|
||||
#include "common/gfx_log_priv.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "soc/soc_caps.h"
|
||||
|
||||
#include "core/display/gfx_disp_priv.h"
|
||||
#include "core/display/gfx_refr_priv.h"
|
||||
#include "core/runtime/gfx_core_priv.h"
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* STATIC VARIABLES
|
||||
**********************/
|
||||
|
||||
static const char *TAG = "disp";
|
||||
|
||||
/**********************
|
||||
* STATIC PROTOTYPES
|
||||
**********************/
|
||||
|
||||
static void gfx_disp_init_default_state(gfx_disp_t *disp);
|
||||
|
||||
/**********************
|
||||
* STATIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
static void gfx_disp_init_default_state(gfx_disp_t *disp)
|
||||
{
|
||||
disp->child_list = NULL;
|
||||
disp->next = NULL;
|
||||
disp->buf.buf_act = disp->buf.buf1;
|
||||
disp->style.bg_color.full = 0x0000;
|
||||
disp->style.bg_enable = true;
|
||||
}
|
||||
|
||||
/**********************
|
||||
* PUBLIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
esp_err_t gfx_disp_buf_free(gfx_disp_t *disp)
|
||||
{
|
||||
if (!disp) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (!disp->buf.ext_bufs) {
|
||||
if (disp->buf.buf1) {
|
||||
heap_caps_free(disp->buf.buf1);
|
||||
disp->buf.buf1 = NULL;
|
||||
}
|
||||
if (disp->buf.buf2) {
|
||||
heap_caps_free(disp->buf.buf2);
|
||||
disp->buf.buf2 = NULL;
|
||||
}
|
||||
}
|
||||
disp->buf.buf_pixels = 0;
|
||||
disp->buf.ext_bufs = false;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t gfx_disp_buf_init(gfx_disp_t *disp, const gfx_disp_config_t *cfg)
|
||||
{
|
||||
if (cfg->buffers.buf1 != NULL) {
|
||||
disp->buf.buf1 = (uint16_t *)cfg->buffers.buf1;
|
||||
disp->buf.buf2 = (uint16_t *)cfg->buffers.buf2;
|
||||
if (cfg->buffers.buf_pixels > 0) {
|
||||
disp->buf.buf_pixels = cfg->buffers.buf_pixels;
|
||||
} else {
|
||||
GFX_LOGW(TAG, "init display buffers: buf_pixels is zero, using screen size");
|
||||
disp->buf.buf_pixels = disp->res.h_res * disp->res.v_res;
|
||||
}
|
||||
disp->buf.ext_bufs = true;
|
||||
} else {
|
||||
#if SOC_PSRAM_DMA_CAPABLE == 0
|
||||
if (cfg->flags.buff_dma && cfg->flags.buff_spiram) {
|
||||
GFX_LOGW(TAG, "init display buffers: dma with spiram is not supported");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
#endif
|
||||
uint32_t buff_caps = 0;
|
||||
if (cfg->flags.buff_dma) {
|
||||
buff_caps |= MALLOC_CAP_DMA;
|
||||
}
|
||||
if (cfg->flags.buff_spiram) {
|
||||
buff_caps |= MALLOC_CAP_SPIRAM;
|
||||
}
|
||||
if (buff_caps == 0) {
|
||||
buff_caps = MALLOC_CAP_DEFAULT;
|
||||
}
|
||||
|
||||
size_t buf_pixels = cfg->buffers.buf_pixels > 0 ? cfg->buffers.buf_pixels : disp->res.h_res * disp->res.v_res;
|
||||
|
||||
disp->buf.buf1 = (uint16_t *)heap_caps_malloc(buf_pixels * sizeof(uint16_t), buff_caps);
|
||||
if (!disp->buf.buf1) {
|
||||
GFX_LOGE(TAG, "init display buffers: allocate frame buffer 1 failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
if (cfg->flags.double_buffer) {
|
||||
disp->buf.buf2 = (uint16_t *)heap_caps_malloc(buf_pixels * sizeof(uint16_t), buff_caps);
|
||||
if (!disp->buf.buf2) {
|
||||
GFX_LOGE(TAG, "init display buffers: allocate frame buffer 2 failed");
|
||||
heap_caps_free(disp->buf.buf1);
|
||||
disp->buf.buf1 = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
} else {
|
||||
disp->buf.buf2 = NULL;
|
||||
}
|
||||
|
||||
disp->buf.buf_pixels = buf_pixels;
|
||||
disp->buf.ext_bufs = false;
|
||||
}
|
||||
disp->buf.buf_act = disp->buf.buf1;
|
||||
disp->style.bg_color.full = 0x0000;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void gfx_disp_del(gfx_disp_t *disp)
|
||||
{
|
||||
if (!disp) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_core_context_t *ctx = (gfx_core_context_t *)disp->ctx;
|
||||
if (ctx != NULL) {
|
||||
if (ctx->disp == disp) {
|
||||
ctx->disp = disp->next;
|
||||
} else {
|
||||
gfx_disp_t *prev = ctx->disp;
|
||||
while (prev != NULL && prev->next != disp) {
|
||||
prev = prev->next;
|
||||
}
|
||||
if (prev != NULL) {
|
||||
prev->next = disp->next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gfx_obj_child_t *child_node = disp->child_list;
|
||||
while (child_node != NULL) {
|
||||
gfx_obj_child_t *next_child = child_node->next;
|
||||
free(child_node);
|
||||
child_node = next_child;
|
||||
}
|
||||
disp->child_list = NULL;
|
||||
|
||||
if (disp->sync.event_group) {
|
||||
vEventGroupDelete(disp->sync.event_group);
|
||||
disp->sync.event_group = NULL;
|
||||
}
|
||||
|
||||
gfx_disp_buf_free(disp);
|
||||
disp->ctx = NULL;
|
||||
disp->next = NULL;
|
||||
}
|
||||
|
||||
gfx_disp_t *gfx_disp_add(gfx_handle_t handle, const gfx_disp_config_t *cfg)
|
||||
{
|
||||
esp_err_t ret;
|
||||
gfx_core_context_t *ctx = (gfx_core_context_t *)handle;
|
||||
if (ctx == NULL || cfg == NULL) {
|
||||
GFX_LOGE(TAG, "create display: handle or config is NULL");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
gfx_disp_t *new_disp = (gfx_disp_t *)malloc(sizeof(gfx_disp_t));
|
||||
if (new_disp == NULL) {
|
||||
GFX_LOGE(TAG, "create display: allocate display state failed");
|
||||
return NULL;
|
||||
}
|
||||
memset(new_disp, 0, sizeof(gfx_disp_t));
|
||||
new_disp->ctx = ctx;
|
||||
new_disp->res.h_res = cfg->h_res;
|
||||
new_disp->res.v_res = cfg->v_res;
|
||||
new_disp->flags.swap = cfg->flags.swap;
|
||||
new_disp->flags.full_frame = cfg->flags.full_frame;
|
||||
new_disp->cb.flush_cb = cfg->flush_cb;
|
||||
new_disp->cb.update_cb = cfg->update_cb;
|
||||
new_disp->cb.user_data = cfg->user_data;
|
||||
gfx_disp_init_default_state(new_disp);
|
||||
|
||||
if (cfg->flags.full_frame && cfg->buffers.buf_pixels > 0) {
|
||||
uint32_t screen_px = new_disp->res.h_res * new_disp->res.v_res;
|
||||
if (cfg->buffers.buf_pixels != screen_px) {
|
||||
GFX_LOGE(TAG, "create display: full_frame requires buf_pixels (%u) == screen size (%u)",
|
||||
(unsigned)cfg->buffers.buf_pixels, (unsigned)screen_px);
|
||||
free(new_disp);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
new_disp->sync.event_group = xEventGroupCreate();
|
||||
if (new_disp->sync.event_group == NULL) {
|
||||
GFX_LOGE(TAG, "create display: create event group failed");
|
||||
free(new_disp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (cfg->buffers.buf1 != NULL) {
|
||||
new_disp->buf.buf1 = (uint16_t *)cfg->buffers.buf1;
|
||||
new_disp->buf.buf2 = (uint16_t *)cfg->buffers.buf2;
|
||||
new_disp->buf.buf_pixels = cfg->buffers.buf_pixels > 0 ? cfg->buffers.buf_pixels : new_disp->res.h_res * new_disp->res.v_res;
|
||||
new_disp->buf.ext_bufs = true;
|
||||
new_disp->buf.buf_act = new_disp->buf.buf1;
|
||||
} else {
|
||||
ret = gfx_disp_buf_init(new_disp, cfg);
|
||||
if (ret != ESP_OK) {
|
||||
vEventGroupDelete(new_disp->sync.event_group);
|
||||
free(new_disp);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx->disp == NULL) {
|
||||
ctx->disp = new_disp;
|
||||
} else {
|
||||
gfx_disp_t *tail = ctx->disp;
|
||||
while (tail->next != NULL) {
|
||||
tail = tail->next;
|
||||
}
|
||||
tail->next = new_disp;
|
||||
}
|
||||
gfx_disp_refresh_all(new_disp);
|
||||
GFX_LOGD(TAG, "create display: object created");
|
||||
return new_disp;
|
||||
}
|
||||
|
||||
esp_err_t gfx_disp_add_child(gfx_disp_t *disp, void *src)
|
||||
{
|
||||
if (disp == NULL || src == NULL) {
|
||||
GFX_LOGE(TAG, "add display child: display or source is NULL");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
gfx_core_context_t *ctx = disp->ctx;
|
||||
if (ctx == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
((gfx_obj_t *)src)->disp = disp;
|
||||
|
||||
gfx_obj_child_t *new_child = (gfx_obj_child_t *)malloc(sizeof(gfx_obj_child_t));
|
||||
if (new_child == NULL) {
|
||||
GFX_LOGE(TAG, "add display child: allocate child node failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
new_child->src = src;
|
||||
new_child->next = NULL;
|
||||
|
||||
if (disp->child_list == NULL) {
|
||||
disp->child_list = new_child;
|
||||
} else {
|
||||
gfx_obj_child_t *current = disp->child_list;
|
||||
while (current->next != NULL) {
|
||||
current = current->next;
|
||||
}
|
||||
current->next = new_child;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t gfx_disp_remove_child(gfx_disp_t *disp, void *src)
|
||||
{
|
||||
if (disp == NULL || src == NULL) {
|
||||
GFX_LOGE(TAG, "remove display child: display or source is NULL");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
gfx_obj_child_t *current = disp->child_list;
|
||||
gfx_obj_child_t *prev = NULL;
|
||||
|
||||
while (current != NULL) {
|
||||
if (current->src == src) {
|
||||
if (prev == NULL) {
|
||||
disp->child_list = current->next;
|
||||
} else {
|
||||
prev->next = current->next;
|
||||
}
|
||||
free(current);
|
||||
return ESP_OK;
|
||||
}
|
||||
prev = current;
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
esp_err_t gfx_disp_delete_children(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
while (disp->child_list != NULL) {
|
||||
gfx_obj_t *obj = (gfx_obj_t *)disp->child_list->src;
|
||||
if (obj == NULL) {
|
||||
gfx_obj_child_t *node = disp->child_list;
|
||||
disp->child_list = node->next;
|
||||
free(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
esp_err_t ret = gfx_obj_delete(obj);
|
||||
if (ret != ESP_OK) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**********************
|
||||
* REFRESH AND FLUSH
|
||||
**********************/
|
||||
|
||||
void gfx_disp_refresh_all(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
GFX_LOGE(TAG, "refresh display: display is NULL");
|
||||
return;
|
||||
}
|
||||
gfx_area_t full_screen;
|
||||
full_screen.x1 = 0;
|
||||
full_screen.y1 = 0;
|
||||
full_screen.x2 = (int)disp->res.h_res - 1;
|
||||
full_screen.y2 = (int)disp->res.v_res - 1;
|
||||
gfx_invalidate_area_disp(disp, &full_screen);
|
||||
}
|
||||
|
||||
bool gfx_disp_flush_ready(gfx_disp_t *disp, bool swap_act_buf)
|
||||
{
|
||||
if (disp == NULL || disp->sync.event_group == NULL) {
|
||||
return false;
|
||||
}
|
||||
disp->render.swap_act_buf = swap_act_buf;
|
||||
if (xPortInIsrContext()) {
|
||||
BaseType_t pxHigherPriorityTaskWoken = pdFALSE;
|
||||
bool result = xEventGroupSetBitsFromISR(disp->sync.event_group, WAIT_FLUSH_DONE, &pxHigherPriorityTaskWoken);
|
||||
if (pxHigherPriorityTaskWoken == pdTRUE) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return xEventGroupSetBits(disp->sync.event_group, WAIT_FLUSH_DONE);
|
||||
}
|
||||
|
||||
/**********************
|
||||
* CONFIG AND STATUS
|
||||
**********************/
|
||||
|
||||
void *gfx_disp_get_user_data(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
GFX_LOGE(TAG, "get display user data: display is NULL");
|
||||
return NULL;
|
||||
}
|
||||
return disp->cb.user_data;
|
||||
}
|
||||
|
||||
uint32_t gfx_disp_get_hor_res(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
return DEFAULT_SCREEN_WIDTH;
|
||||
}
|
||||
return disp->res.h_res;
|
||||
}
|
||||
|
||||
uint32_t gfx_disp_get_ver_res(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
return DEFAULT_SCREEN_HEIGHT;
|
||||
}
|
||||
return disp->res.v_res;
|
||||
}
|
||||
|
||||
esp_err_t gfx_disp_set_bg_color(gfx_disp_t *disp, gfx_color_t color)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
GFX_LOGE(TAG, "set display background color: display is NULL");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
disp->style.bg_color.full = color.full;
|
||||
GFX_LOGD(TAG, "set display background color: 0x%04X", color.full);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t gfx_disp_set_bg_enable(gfx_disp_t *disp, bool enable)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
GFX_LOGE(TAG, "set display background enable: display is NULL");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
disp->style.bg_enable = enable;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool gfx_disp_is_flushing_last(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
return false;
|
||||
}
|
||||
return disp->render.flushing_last;
|
||||
}
|
||||
|
||||
esp_err_t gfx_disp_get_perf_stats(gfx_disp_t *disp, gfx_disp_perf_stats_t *out_stats)
|
||||
{
|
||||
if (disp == NULL || out_stats == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
out_stats->dirty_pixels = disp->render.dirty_pixels;
|
||||
out_stats->frame_time_us = disp->render.frame_time_us;
|
||||
out_stats->render_time_us = disp->render.render_time_us;
|
||||
out_stats->flush_time_us = disp->render.flush_time_us;
|
||||
out_stats->flush_count = disp->render.flush_count;
|
||||
out_stats->blend = disp->render.blend;
|
||||
return ESP_OK;
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "core/gfx_disp.h"
|
||||
#include "core/object/gfx_obj_priv.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* TYPEDEFS
|
||||
*********************/
|
||||
struct gfx_core_context;
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
#ifdef CONFIG_GFX_DISP_INV_BUF_SIZE
|
||||
#define GFX_DISP_INV_BUF_SIZE CONFIG_GFX_DISP_INV_BUF_SIZE
|
||||
#else
|
||||
#define GFX_DISP_INV_BUF_SIZE 64
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* INTERNAL STRUCTS
|
||||
*********************/
|
||||
/** Per-display state; one per screen, linked list for multi-display. Fields grouped by category. */
|
||||
struct gfx_disp {
|
||||
struct gfx_disp *next;
|
||||
struct gfx_core_context *ctx;
|
||||
|
||||
/** Resolution */
|
||||
struct {
|
||||
uint32_t h_res;
|
||||
uint32_t v_res;
|
||||
} res;
|
||||
|
||||
/** Option flags */
|
||||
struct {
|
||||
unsigned char swap : 1;
|
||||
unsigned char full_frame : 1;
|
||||
} flags;
|
||||
|
||||
/** Callbacks and user data */
|
||||
struct {
|
||||
gfx_disp_flush_cb_t flush_cb;
|
||||
gfx_disp_update_cb_t update_cb;
|
||||
void *user_data;
|
||||
} cb;
|
||||
|
||||
/** Sync (event group for flush done) */
|
||||
struct {
|
||||
EventGroupHandle_t event_group;
|
||||
} sync;
|
||||
|
||||
/** Child object list */
|
||||
gfx_obj_child_t *child_list;
|
||||
|
||||
/** Frame buffers */
|
||||
struct {
|
||||
uint16_t *buf1;
|
||||
uint16_t *buf2;
|
||||
uint16_t *buf_act;
|
||||
size_t buf_pixels;
|
||||
bool ext_bufs;
|
||||
} buf;
|
||||
|
||||
/** Display style (e.g. background color) */
|
||||
struct {
|
||||
gfx_color_t bg_color;
|
||||
bool bg_enable; /**< true = fill background before draw; default true */
|
||||
} style;
|
||||
|
||||
/** Render state (flush / swap) */
|
||||
struct {
|
||||
bool flushing_last;
|
||||
bool swap_act_buf;
|
||||
uint32_t dirty_pixels;
|
||||
uint64_t frame_time_us;
|
||||
uint64_t render_time_us;
|
||||
uint64_t flush_time_us;
|
||||
uint32_t flush_count;
|
||||
gfx_blend_perf_stats_t blend;
|
||||
} render;
|
||||
|
||||
/** Dirty / invalidation state */
|
||||
struct {
|
||||
gfx_area_t areas[GFX_DISP_INV_BUF_SIZE];
|
||||
uint8_t merged[GFX_DISP_INV_BUF_SIZE];
|
||||
uint8_t count;
|
||||
} dirty;
|
||||
|
||||
/** Pending sync: dirty areas from previous frame to sync into buf_act at next render start (only non-merged areas, no merged flags) */
|
||||
struct {
|
||||
gfx_area_t areas[GFX_DISP_INV_BUF_SIZE];
|
||||
uint8_t count;
|
||||
} sync_pending;
|
||||
};
|
||||
|
||||
/*********************
|
||||
* INTERNAL API
|
||||
*********************/
|
||||
|
||||
/* Buffer helpers (used by gfx_disp.c and gfx_core.c deinit) */
|
||||
|
||||
/**
|
||||
* @brief Free display frame buffers
|
||||
* @param disp Display whose buffers to free (internal alloc only; ext_bufs are not freed)
|
||||
* @return ESP_OK
|
||||
* @internal Used by gfx_core deinit when tearing down displays.
|
||||
*/
|
||||
esp_err_t gfx_disp_buf_free(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Initialize display buffers from config
|
||||
* @param disp Display to init (h_res, v_res already set)
|
||||
* @param cfg Display config (buffers.buf1/buf2/buf_pixels)
|
||||
* @return ESP_OK on success, ESP_ERR_NO_MEM if internal alloc fails
|
||||
* @internal Used by gfx_disp_add when cfg->buffers.buf1 is NULL.
|
||||
*/
|
||||
esp_err_t gfx_disp_buf_init(gfx_disp_t *disp, const gfx_disp_config_t *cfg);
|
||||
|
||||
/* Object/render helpers (obj/widget/render only, not in public gfx_disp.h) */
|
||||
|
||||
/**
|
||||
* @brief Add a child object to a display
|
||||
* @param disp Display to attach to
|
||||
* @param type Child type (GFX_OBJ_TYPE_IMAGE, GFX_OBJ_TYPE_LABEL, etc.)
|
||||
* @param src Child object pointer (e.g. gfx_obj_t *)
|
||||
* @return ESP_OK on success
|
||||
* @internal Used by gfx_anim_create, gfx_img_create, gfx_label_create, gfx_qrcode_create.
|
||||
*/
|
||||
esp_err_t gfx_disp_add_child(gfx_disp_t *disp, void *src);
|
||||
|
||||
/**
|
||||
* @brief Remove a child object from a display
|
||||
* @param disp Display that owns the child
|
||||
* @param src Child object pointer to remove (e.g. gfx_obj_t *)
|
||||
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if not in list
|
||||
* @internal Used by gfx_obj_delete.
|
||||
*/
|
||||
esp_err_t gfx_disp_remove_child(gfx_disp_t *disp, void *src);
|
||||
|
||||
/**
|
||||
* @brief Delete and detach every child object owned by a display.
|
||||
* @param disp Display that owns the child list
|
||||
* @return ESP_OK on success
|
||||
* @internal Used during display/core teardown to ensure widget destructors run.
|
||||
*/
|
||||
esp_err_t gfx_disp_delete_children(gfx_disp_t *disp);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,300 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*********************
|
||||
* INCLUDES
|
||||
*********************/
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#define GFX_LOG_MODULE GFX_LOG_MODULE_REFR
|
||||
#include "common/gfx_log_priv.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
|
||||
#include "core/display/gfx_refr_priv.h"
|
||||
#include "core/runtime/gfx_core_priv.h"
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* STATIC VARIABLES
|
||||
**********************/
|
||||
|
||||
static const char *TAG = "refr";
|
||||
|
||||
/**********************
|
||||
* STATIC PROTOTYPES
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* STATIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* PUBLIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
/* Area helpers */
|
||||
void gfx_area_copy(gfx_area_t *dest, const gfx_area_t *src)
|
||||
{
|
||||
dest->x1 = src->x1;
|
||||
dest->y1 = src->y1;
|
||||
dest->x2 = src->x2;
|
||||
dest->y2 = src->y2;
|
||||
}
|
||||
|
||||
bool gfx_area_is_in(const gfx_area_t *area_in, const gfx_area_t *area_parent)
|
||||
{
|
||||
if (area_in->x1 >= area_parent->x1 &&
|
||||
area_in->y1 >= area_parent->y1 &&
|
||||
area_in->x2 <= area_parent->x2 &&
|
||||
area_in->y2 <= area_parent->y2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gfx_area_intersect(gfx_area_t *result, const gfx_area_t *a1, const gfx_area_t *a2)
|
||||
{
|
||||
gfx_coord_t x1 = (a1->x1 > a2->x1) ? a1->x1 : a2->x1;
|
||||
gfx_coord_t y1 = (a1->y1 > a2->y1) ? a1->y1 : a2->y1;
|
||||
gfx_coord_t x2 = (a1->x2 < a2->x2) ? a1->x2 : a2->x2;
|
||||
gfx_coord_t y2 = (a1->y2 < a2->y2) ? a1->y2 : a2->y2;
|
||||
|
||||
if (x1 <= x2 && y1 <= y2) {
|
||||
result->x1 = x1;
|
||||
result->y1 = y1;
|
||||
result->x2 = x2;
|
||||
result->y2 = y2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gfx_area_intersect_exclusive(gfx_area_t *result, const gfx_area_t *a1, const gfx_area_t *a2)
|
||||
{
|
||||
gfx_coord_t x1 = (a1->x1 > a2->x1) ? a1->x1 : a2->x1;
|
||||
gfx_coord_t y1 = (a1->y1 > a2->y1) ? a1->y1 : a2->y1;
|
||||
gfx_coord_t x2 = (a1->x2 < a2->x2) ? a1->x2 : a2->x2;
|
||||
gfx_coord_t y2 = (a1->y2 < a2->y2) ? a1->y2 : a2->y2;
|
||||
|
||||
if (x1 < x2 && y1 < y2) {
|
||||
result->x1 = x1;
|
||||
result->y1 = y1;
|
||||
result->x2 = x2;
|
||||
result->y2 = y2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t gfx_area_get_size(const gfx_area_t *area)
|
||||
{
|
||||
uint32_t width = area->x2 - area->x1 + 1;
|
||||
uint32_t height = area->y2 - area->y1 + 1;
|
||||
return width * height;
|
||||
}
|
||||
|
||||
bool gfx_area_is_on(const gfx_area_t *a1, const gfx_area_t *a2)
|
||||
{
|
||||
/* Check if areas are completely separate */
|
||||
if ((a1->x1 > a2->x2) ||
|
||||
(a2->x1 > a1->x2) ||
|
||||
(a1->y1 > a2->y2) ||
|
||||
(a2->y1 > a1->y2)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void gfx_area_join(gfx_area_t *result, const gfx_area_t *a1, const gfx_area_t *a2)
|
||||
{
|
||||
result->x1 = (a1->x1 < a2->x1) ? a1->x1 : a2->x1;
|
||||
result->y1 = (a1->y1 < a2->y1) ? a1->y1 : a2->y1;
|
||||
result->x2 = (a1->x2 > a2->x2) ? a1->x2 : a2->x2;
|
||||
result->y2 = (a1->y2 > a2->y2) ? a1->y2 : a2->y2;
|
||||
}
|
||||
|
||||
void gfx_refr_merge_areas(gfx_disp_t *disp)
|
||||
{
|
||||
uint32_t src_idx;
|
||||
uint32_t dst_idx;
|
||||
gfx_area_t merged_area;
|
||||
|
||||
if (disp == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(disp->dirty.merged, 0, sizeof(disp->dirty.merged));
|
||||
|
||||
for (dst_idx = 0; dst_idx < disp->dirty.count; dst_idx++) {
|
||||
if (disp->dirty.merged[dst_idx] != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (src_idx = 0; src_idx < disp->dirty.count; src_idx++) {
|
||||
if (disp->dirty.merged[src_idx] != 0 || dst_idx == src_idx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!gfx_area_is_on(&disp->dirty.areas[dst_idx], &disp->dirty.areas[src_idx])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
gfx_area_join(&merged_area, &disp->dirty.areas[dst_idx], &disp->dirty.areas[src_idx]);
|
||||
|
||||
uint32_t merged_size = gfx_area_get_size(&merged_area);
|
||||
uint32_t separate_size = gfx_area_get_size(&disp->dirty.areas[dst_idx]) +
|
||||
gfx_area_get_size(&disp->dirty.areas[src_idx]);
|
||||
|
||||
if (merged_size < separate_size) {
|
||||
gfx_area_copy(&disp->dirty.areas[dst_idx], &merged_area);
|
||||
disp->dirty.merged[src_idx] = 1;
|
||||
|
||||
GFX_LOGD(TAG, "merge dirty areas: [%" PRIu32 "] into [%" PRIu32 "], saved %" PRIu32 " pixels",
|
||||
src_idx, dst_idx, separate_size - merged_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_invalidate_area_disp(gfx_disp_t *disp, const gfx_area_t *area_p)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (area_p == NULL) {
|
||||
disp->dirty.count = 0;
|
||||
memset(disp->dirty.merged, 0, sizeof(disp->dirty.merged));
|
||||
GFX_LOGD(TAG, "invalidate area: cleared all dirty areas");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_area_t screen_area;
|
||||
screen_area.x1 = 0;
|
||||
screen_area.y1 = 0;
|
||||
screen_area.x2 = disp->res.h_res - 1;
|
||||
screen_area.y2 = disp->res.v_res - 1;
|
||||
|
||||
gfx_area_t clipped_area;
|
||||
bool success = gfx_area_intersect(&clipped_area, area_p, &screen_area);
|
||||
if (!success) {
|
||||
GFX_LOGD(TAG, "invalidate area: area is out of screen bounds");
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < disp->dirty.count; i++) {
|
||||
if (gfx_area_is_in(&clipped_area, &disp->dirty.areas[i])) {
|
||||
GFX_LOGD(TAG, "invalidate area: area is already covered by dirty area %d", i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (disp->dirty.count < GFX_DISP_INV_BUF_SIZE) {
|
||||
gfx_area_copy(&disp->dirty.areas[disp->dirty.count], &clipped_area);
|
||||
disp->dirty.count++;
|
||||
GFX_LOGD(TAG, "invalidate area: added [%d,%d,%d,%d], total=%d",
|
||||
clipped_area.x1, clipped_area.y1, clipped_area.x2, clipped_area.y2, disp->dirty.count);
|
||||
} else {
|
||||
GFX_LOGW(TAG, "invalidate area: dirty buffer is full[%d], marking full screen", disp->dirty.count);
|
||||
disp->dirty.count = 1;
|
||||
gfx_area_copy(&disp->dirty.areas[0], &screen_area);
|
||||
}
|
||||
|
||||
/* Wake render task so it refreshes without waiting for the next timer tick */
|
||||
gfx_core_context_t *ctx = (gfx_core_context_t *)disp->ctx;
|
||||
if (ctx != NULL && ctx->sync.render_events != NULL) {
|
||||
xEventGroupSetBits(ctx->sync.render_events, GFX_EVENT_INVALIDATE);
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_invalidate_area(gfx_handle_t handle, const gfx_area_t *area_p)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
GFX_LOGE(TAG, "invalidate area: handle is NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_core_context_t *ctx = (gfx_core_context_t *)handle;
|
||||
|
||||
if (area_p == NULL) {
|
||||
for (gfx_disp_t *d = ctx->disp; d != NULL; d = d->next) {
|
||||
gfx_invalidate_area_disp(d, NULL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Invalidate first display (backward compat) */
|
||||
if (ctx->disp != NULL) {
|
||||
gfx_invalidate_area_disp(ctx->disp, area_p);
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_obj_invalidate(gfx_obj_t *obj)
|
||||
{
|
||||
if (obj == NULL) {
|
||||
GFX_LOGE(TAG, "invalidate object: object is NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
if (obj->disp == NULL) {
|
||||
GFX_LOGE(TAG, "invalidate object: object has no display");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_area_t obj_area;
|
||||
obj_area.x1 = obj->geometry.x;
|
||||
obj_area.y1 = obj->geometry.y;
|
||||
obj_area.x2 = obj->geometry.x + obj->geometry.width - 1;
|
||||
obj_area.y2 = obj->geometry.y + obj->geometry.height - 1;
|
||||
|
||||
obj->state.dirty = true;
|
||||
|
||||
gfx_invalidate_area_disp(obj->disp, &obj_area);
|
||||
}
|
||||
|
||||
void gfx_refr_update_layout_dirty(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL || disp->child_list == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_obj_child_t *child_node = disp->child_list;
|
||||
|
||||
while (child_node != NULL) {
|
||||
gfx_obj_t *obj = (gfx_obj_t *)child_node->src;
|
||||
|
||||
if (obj != NULL && obj->state.layout_dirty && obj->align.enabled) {
|
||||
gfx_coord_t old_x = obj->geometry.x;
|
||||
gfx_coord_t old_y = obj->geometry.y;
|
||||
|
||||
gfx_obj_invalidate(obj);
|
||||
gfx_obj_calc_pos_in_parent(obj);
|
||||
|
||||
gfx_obj_invalidate(obj);
|
||||
|
||||
GFX_LOGD(TAG,
|
||||
"layout update: obj=%p (%d,%d) -> (%d,%d)",
|
||||
obj,
|
||||
old_x,
|
||||
old_y,
|
||||
obj->geometry.x,
|
||||
obj->geometry.y);
|
||||
|
||||
obj->state.layout_dirty = false;
|
||||
}
|
||||
|
||||
child_node = child_node->next;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/runtime/gfx_core_priv.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Invalidate an area for a specific display (internal)
|
||||
*/
|
||||
void gfx_invalidate_area_disp(gfx_disp_t *disp, const gfx_area_t *area_p);
|
||||
|
||||
/**
|
||||
* @brief Invalidate an area globally (mark it for redraw) - applies to first display
|
||||
* @param handle Graphics handle
|
||||
* @param area Pointer to the area to invalidate, or NULL to clear all invalid areas
|
||||
*
|
||||
* This function adds an area to the global dirty area list.
|
||||
* - If area is NULL, clears all invalid areas on all displays
|
||||
* - Areas are automatically clipped to screen bounds
|
||||
* - Overlapping/adjacent areas are merged
|
||||
* - If buffer is full, marks entire screen as dirty
|
||||
*/
|
||||
void gfx_invalidate_area(gfx_handle_t handle, const gfx_area_t *area);
|
||||
|
||||
/**
|
||||
* @brief Invalidate an object's area (convenience function)
|
||||
* @param obj Pointer to the object to invalidate
|
||||
*
|
||||
* Marks the entire object bounds as dirty in the global invalidation list.
|
||||
*/
|
||||
void gfx_obj_invalidate(gfx_obj_t *obj);
|
||||
|
||||
/**
|
||||
* @brief Update layout for all objects marked as layout dirty on a display
|
||||
* @param disp Display to update
|
||||
*/
|
||||
void gfx_refr_update_layout_dirty(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Merge overlapping/adjacent dirty areas to minimize redraw regions
|
||||
* @param disp Display containing dirty areas
|
||||
*/
|
||||
void gfx_refr_merge_areas(gfx_disp_t *disp);
|
||||
|
||||
/* Area utility functions (merged from gfx_area.h) */
|
||||
/**
|
||||
* @brief Copy area from src to dest
|
||||
* @param dest Destination area
|
||||
* @param src Source area
|
||||
*/
|
||||
void gfx_area_copy(gfx_area_t *dest, const gfx_area_t *src);
|
||||
|
||||
/**
|
||||
* @brief Check if area_in is fully contained within area_parent
|
||||
* @param area_in Area to check
|
||||
* @param area_parent Parent area
|
||||
* @return true if area_in is completely inside area_parent
|
||||
*/
|
||||
bool gfx_area_is_in(const gfx_area_t *area_in, const gfx_area_t *area_parent);
|
||||
|
||||
/**
|
||||
* @brief Get intersection of two areas
|
||||
* @param result Result area (intersection)
|
||||
* @param a1 First area
|
||||
* @param a2 Second area
|
||||
* @return true if areas intersect, false otherwise
|
||||
*/
|
||||
bool gfx_area_intersect(gfx_area_t *result, const gfx_area_t *a1, const gfx_area_t *a2);
|
||||
|
||||
/**
|
||||
* @brief Get intersection of two half-open areas [x1, x2) x [y1, y2)
|
||||
* @param result Result area (intersection)
|
||||
* @param a1 First area with exclusive x2/y2
|
||||
* @param a2 Second area with exclusive x2/y2
|
||||
* @return true if areas intersect, false otherwise
|
||||
*/
|
||||
bool gfx_area_intersect_exclusive(gfx_area_t *result, const gfx_area_t *a1, const gfx_area_t *a2);
|
||||
|
||||
/**
|
||||
* @brief Get the size (area) of a rectangular region
|
||||
* @param area Area to calculate size for
|
||||
* @return Size in pixels (width * height)
|
||||
*/
|
||||
uint32_t gfx_area_get_size(const gfx_area_t *area);
|
||||
|
||||
/**
|
||||
* @brief Check if two areas are on each other (overlap or touch)
|
||||
* @param a1 First area
|
||||
* @param a2 Second area
|
||||
* @return true if areas overlap or are adjacent (touch)
|
||||
*/
|
||||
bool gfx_area_is_on(const gfx_area_t *a1, const gfx_area_t *a2);
|
||||
|
||||
/**
|
||||
* @brief Join two areas into a larger area (bounding box)
|
||||
* @param result Result area (bounding box of a1 and a2)
|
||||
* @param a1 First area
|
||||
* @param a2 Second area
|
||||
*/
|
||||
void gfx_area_join(gfx_area_t *result, const gfx_area_t *a1, const gfx_area_t *a2);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,384 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*********************
|
||||
* INCLUDES
|
||||
*********************/
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
#define GFX_LOG_MODULE GFX_LOG_MODULE_RENDER
|
||||
#include "common/gfx_log_priv.h"
|
||||
|
||||
#include "core/display/gfx_refr_priv.h"
|
||||
#include "core/display/gfx_render_priv.h"
|
||||
#include "core/draw/gfx_blend_priv.h"
|
||||
#include "core/runtime/gfx_timer_priv.h"
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
/**********************
|
||||
* STATIC VARIABLES
|
||||
**********************/
|
||||
|
||||
static const char *TAG = "render";
|
||||
|
||||
/**********************
|
||||
* STATIC PROTOTYPES
|
||||
**********************/
|
||||
|
||||
static void gfx_render_sync_dirty_areas(gfx_disp_t *disp);
|
||||
|
||||
/**********************
|
||||
* STATIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
static void gfx_render_sync_dirty_areas(gfx_disp_t *disp)
|
||||
{
|
||||
if (!disp->flags.full_frame || disp->buf.buf2 == NULL || disp->sync_pending.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t *dst_screen_buf = disp->buf.buf_act;
|
||||
uint16_t *src_screen_buf = (disp->buf.buf_act == disp->buf.buf1) ? disp->buf.buf2 : disp->buf.buf1;
|
||||
uint32_t stride = disp->res.h_res;
|
||||
const size_t px_size = sizeof(uint16_t);
|
||||
|
||||
for (uint8_t i = 0; i < disp->sync_pending.count; i++) {
|
||||
const gfx_area_t *a = &disp->sync_pending.areas[i];
|
||||
bool covered = false;
|
||||
for (uint8_t j = 0; j < disp->dirty.count && !covered; j++) {
|
||||
if (disp->dirty.merged[j]) {
|
||||
continue;
|
||||
}
|
||||
if (gfx_area_is_in(a, &disp->dirty.areas[j])) {
|
||||
covered = true;
|
||||
}
|
||||
}
|
||||
if (covered) {
|
||||
continue;
|
||||
}
|
||||
uint32_t w = (uint32_t)(a->x2 - a->x1 + 1);
|
||||
uint32_t h = (uint32_t)(a->y2 - a->y1 + 1);
|
||||
for (uint32_t y = 0; y < h; y++) {
|
||||
size_t offset = (size_t)(a->y1 + (gfx_coord_t)y) * stride + (size_t)a->x1;
|
||||
memcpy(dst_screen_buf + offset, src_screen_buf + offset, w * px_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**********************
|
||||
* PUBLIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
void gfx_render_draw_child_objects(gfx_disp_t *disp, const gfx_draw_ctx_t *ctx)
|
||||
{
|
||||
if (disp == NULL || disp->child_list == NULL || ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_obj_child_t *child_node = disp->child_list;
|
||||
|
||||
while (child_node != NULL) {
|
||||
gfx_obj_t *obj = (gfx_obj_t *)child_node->src;
|
||||
|
||||
if (!obj->state.is_visible) {
|
||||
child_node = child_node->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj->vfunc.draw) {
|
||||
obj->vfunc.draw(obj, ctx);
|
||||
}
|
||||
|
||||
child_node = child_node->next;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void gfx_render_update_child_objects(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL || disp->child_list == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_obj_child_t *child_node = disp->child_list;
|
||||
|
||||
while (child_node != NULL) {
|
||||
gfx_obj_t *obj = (gfx_obj_t *)child_node->src;
|
||||
|
||||
if (!obj->state.is_visible) {
|
||||
child_node = child_node->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj->vfunc.update) {
|
||||
obj->vfunc.update(obj);
|
||||
}
|
||||
|
||||
child_node = child_node->next;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t gfx_render_area_summary(gfx_disp_t *disp)
|
||||
{
|
||||
uint32_t total_dirty_pixels = 0;
|
||||
|
||||
if (disp == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < disp->dirty.count; i++) {
|
||||
if (disp->dirty.merged[i]) {
|
||||
continue;
|
||||
}
|
||||
gfx_area_t *area = &disp->dirty.areas[i];
|
||||
uint32_t area_size = gfx_area_get_size(area);
|
||||
total_dirty_pixels += area_size;
|
||||
// GFX_LOGD(TAG, "Draw area [%d]: (%d,%d)->(%d,%d) %dx%d",
|
||||
// i, area->x1, area->y1, area->x2, area->y2,
|
||||
// area->x2 - area->x1 + 1, area->y2 - area->y1 + 1);
|
||||
}
|
||||
|
||||
return total_dirty_pixels;
|
||||
}
|
||||
|
||||
void gfx_render_part_area(gfx_disp_t *disp, gfx_area_t *area, uint8_t area_idx, bool is_last_area)
|
||||
{
|
||||
if (disp == NULL || area == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (area->x2 < area->x1 || area->y2 < area->y1) {
|
||||
GFX_LOGE(TAG, "render area[%d]: invalid bounds (%d,%d)-(%d,%d)", area_idx,
|
||||
area->x1, area->y1, area->x2, area->y2);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t area_w = (uint32_t)(area->x2 - area->x1 + 1);
|
||||
uint32_t row_h = disp->buf.buf_pixels / area_w;
|
||||
if (row_h == 0) {
|
||||
GFX_LOGE(TAG, "render area[%d]: width %" PRIu32 " exceeds buffer, skipping", area_idx, area_w);
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_disp_flush_cb_t flush_cb = disp->cb.flush_cb;
|
||||
if (flush_cb != NULL && disp->sync.event_group == NULL) {
|
||||
GFX_LOGE(TAG, "render area[%d]: flush callback is set but event group is NULL", area_idx);
|
||||
return;
|
||||
}
|
||||
|
||||
disp->render.flushing_last = false;
|
||||
gfx_coord_t cur_y = area->y1;
|
||||
|
||||
while (cur_y <= area->y2) {
|
||||
int64_t render_start_us;
|
||||
int64_t flush_start_us;
|
||||
|
||||
gfx_coord_t chunk_x1 = area->x1;
|
||||
gfx_coord_t chunk_y1 = cur_y;
|
||||
gfx_coord_t chunk_x2 = area->x2 + 1;
|
||||
gfx_coord_t chunk_y2 = cur_y + (gfx_coord_t)row_h;
|
||||
if (chunk_y2 > area->y2 + 1) {
|
||||
chunk_y2 = area->y2 + 1;
|
||||
}
|
||||
|
||||
uint16_t *buf = disp->buf.buf_act;
|
||||
|
||||
int dest_stride = disp->flags.full_frame ? disp->res.h_res : (chunk_x2 - chunk_x1);
|
||||
|
||||
gfx_area_t buf_area;
|
||||
if (disp->flags.full_frame) {
|
||||
buf_area.x1 = 0;
|
||||
buf_area.y1 = 0;
|
||||
buf_area.x2 = (gfx_coord_t)disp->res.h_res;
|
||||
buf_area.y2 = (gfx_coord_t)disp->res.v_res;
|
||||
} else {
|
||||
buf_area.x1 = chunk_x1;
|
||||
buf_area.y1 = chunk_y1;
|
||||
buf_area.x2 = chunk_x2;
|
||||
buf_area.y2 = chunk_y2;
|
||||
}
|
||||
|
||||
gfx_draw_ctx_t draw_ctx = {
|
||||
.buf = buf,
|
||||
.buf_area = buf_area,
|
||||
.clip_area = { chunk_x1, chunk_y1, chunk_x2, chunk_y2 },
|
||||
.stride = dest_stride,
|
||||
.swap = disp->flags.swap,
|
||||
};
|
||||
|
||||
render_start_us = esp_timer_get_time();
|
||||
if (disp->style.bg_enable) {
|
||||
uint16_t bg = gfx_color_to_native_u16(disp->style.bg_color, disp->flags.swap);
|
||||
if (disp->flags.full_frame) {
|
||||
gfx_area_t fill_area = { chunk_x1, chunk_y1, chunk_x2, chunk_y2 }; /* exclusive x2,y2 */
|
||||
gfx_sw_blend_fill_area(buf, (gfx_coord_t)disp->res.h_res, &fill_area, bg);
|
||||
} else {
|
||||
gfx_area_t fill_area = { 0, 0, chunk_x2 - chunk_x1, chunk_y2 - chunk_y1 };
|
||||
gfx_sw_blend_fill_area(buf, chunk_x2 - chunk_x1, &fill_area, bg);
|
||||
}
|
||||
}
|
||||
gfx_render_draw_child_objects(disp, &draw_ctx);
|
||||
disp->render.render_time_us += (uint64_t)(esp_timer_get_time() - render_start_us);
|
||||
|
||||
if (flush_cb != NULL) {
|
||||
xEventGroupClearBits(disp->sync.event_group, WAIT_FLUSH_DONE);
|
||||
|
||||
// uint32_t chunk_px = area_w * (uint32_t)(chunk_y2 - chunk_y1);
|
||||
|
||||
bool is_last_chunk = (chunk_y2 >= area->y2 + 1);
|
||||
disp->render.flushing_last = is_last_chunk && is_last_area;
|
||||
|
||||
// GFX_LOGD(TAG, "Flush: (%d,%d)-(%d,%d) %" PRIu32 " px%s",
|
||||
// chunk_x1, chunk_y1, chunk_x2 - 1, chunk_y2 - 1, chunk_px,
|
||||
// disp->render.flushing_last ? " (last)" : "");
|
||||
|
||||
flush_start_us = esp_timer_get_time();
|
||||
flush_cb(disp, chunk_x1, chunk_y1, chunk_x2, chunk_y2, buf);
|
||||
|
||||
xEventGroupWaitBits(disp->sync.event_group, WAIT_FLUSH_DONE, pdTRUE, pdFALSE, portMAX_DELAY);
|
||||
disp->render.flush_time_us += (uint64_t)(esp_timer_get_time() - flush_start_us);
|
||||
disp->render.flush_count++;
|
||||
|
||||
if (disp->buf.buf2 != NULL && (!disp->flags.full_frame || disp->render.flushing_last)) {
|
||||
disp->buf.buf_act = (disp->buf.buf_act == disp->buf.buf1) ? disp->buf.buf2 : disp->buf.buf1;
|
||||
}
|
||||
}
|
||||
|
||||
cur_y = chunk_y2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Render all dirty areas
|
||||
* @param disp Display
|
||||
*/
|
||||
void gfx_render_dirty_areas(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
disp->render.render_time_us = 0;
|
||||
disp->render.flush_time_us = 0;
|
||||
disp->render.flush_count = 0;
|
||||
gfx_sw_blend_perf_reset(&disp->render.blend);
|
||||
gfx_sw_blend_perf_bind(&disp->render.blend);
|
||||
|
||||
gfx_render_sync_dirty_areas(disp);
|
||||
|
||||
uint8_t last_area_idx = 0;
|
||||
for (uint8_t i = 0; i < disp->dirty.count; i++) {
|
||||
if (!disp->dirty.merged[i]) {
|
||||
last_area_idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t sync_points = 0;
|
||||
for (uint8_t i = 0; i < disp->dirty.count; i++) {
|
||||
if (disp->dirty.merged[i]) {
|
||||
continue;
|
||||
}
|
||||
gfx_area_t *area = &disp->dirty.areas[i];
|
||||
bool is_last_area = (i == last_area_idx);
|
||||
gfx_render_part_area(disp, area, i, is_last_area);
|
||||
sync_points++;
|
||||
gfx_area_copy(&disp->sync_pending.areas[sync_points], area);
|
||||
}
|
||||
gfx_sw_blend_perf_unbind();
|
||||
disp->sync_pending.count = sync_points;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cleanup after rendering - swap buffers and clear dirty flags
|
||||
* @param disp Display
|
||||
*/
|
||||
void gfx_render_cleanup(gfx_disp_t *disp)
|
||||
{
|
||||
if (disp == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disp->dirty.count > 0) {
|
||||
gfx_invalidate_area_disp(disp, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handle rendering of all objects in the scene
|
||||
* @param ctx Player context
|
||||
* @return true if any display was rendered, false otherwise
|
||||
*/
|
||||
bool gfx_render_handler(gfx_core_context_t *ctx)
|
||||
{
|
||||
static const uint32_t fps_sample_window = 100;
|
||||
static uint32_t fps_samples = 0;
|
||||
static uint32_t fps_elapsed_ms = 0;
|
||||
static uint32_t last_tick_ms = 0;
|
||||
|
||||
uint32_t now_ms = gfx_timer_tick_get();
|
||||
if (last_tick_ms == 0) {
|
||||
last_tick_ms = now_ms;
|
||||
} else {
|
||||
uint32_t elapsed_ms = gfx_timer_tick_elaps(last_tick_ms);
|
||||
fps_samples++;
|
||||
fps_elapsed_ms += elapsed_ms;
|
||||
last_tick_ms = now_ms;
|
||||
|
||||
if (fps_samples >= fps_sample_window) {
|
||||
gfx_timer_mgr_t *mgr = &ctx->timer_mgr;
|
||||
mgr->actual_fps = (fps_samples * 1000) / fps_elapsed_ms;
|
||||
fps_samples = 0;
|
||||
fps_elapsed_ms = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool did_render = false;
|
||||
|
||||
for (gfx_disp_t *disp = ctx->disp; disp != NULL; disp = disp->next) {
|
||||
int64_t frame_start_us = esp_timer_get_time();
|
||||
gfx_refr_update_layout_dirty(disp);
|
||||
|
||||
if (disp->dirty.count > 1) {
|
||||
gfx_refr_merge_areas(disp);
|
||||
} else if (disp->dirty.count == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
gfx_render_update_child_objects(disp);
|
||||
|
||||
uint32_t dirty_px = gfx_render_area_summary(disp);
|
||||
gfx_render_dirty_areas(disp);
|
||||
uint64_t frame_time_us = (uint64_t)(esp_timer_get_time() - frame_start_us);
|
||||
disp->render.dirty_pixels = dirty_px;
|
||||
disp->render.frame_time_us = frame_time_us;
|
||||
|
||||
if (dirty_px > 0) {
|
||||
did_render = true;
|
||||
uint32_t screen_px = disp->res.h_res * disp->res.v_res;
|
||||
float dirty_pct = (dirty_px * 100.0f) / (float)screen_px;
|
||||
GFX_LOGD(TAG,
|
||||
"%.1f%% (%" PRIu64 "ms) (%" PRIu64 "|%" PRIu64 ")",
|
||||
dirty_pct,
|
||||
frame_time_us / 1000,
|
||||
disp->render.render_time_us / 1000,
|
||||
disp->render.flush_time_us / 1000);
|
||||
}
|
||||
|
||||
gfx_render_cleanup(disp);
|
||||
}
|
||||
|
||||
return did_render;
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/runtime/gfx_core_priv.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Handle rendering of all objects in the scene (iterates over all displays)
|
||||
* @param ctx Player context
|
||||
* @return true if rendering was performed, false otherwise
|
||||
*/
|
||||
bool gfx_render_handler(gfx_core_context_t *ctx);
|
||||
|
||||
/**
|
||||
* @brief Render all dirty areas for one display
|
||||
*/
|
||||
void gfx_render_dirty_areas(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Render a single dirty area with dynamic height-based blocking
|
||||
* @param is_last_area true if this is the last dirty area in the list (flushing_last = last chunk of this area AND is_last_area)
|
||||
*/
|
||||
void gfx_render_part_area(gfx_disp_t *disp, gfx_area_t *area, uint8_t area_idx, bool is_last_area);
|
||||
|
||||
/**
|
||||
* @brief Cleanup after rendering - swap buffers and clear dirty flags for one display
|
||||
*/
|
||||
void gfx_render_cleanup(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Print summary of dirty areas for one display
|
||||
* @return Total dirty pixels
|
||||
*/
|
||||
uint32_t gfx_render_area_summary(gfx_disp_t *disp);
|
||||
|
||||
/**
|
||||
* @brief Draw child objects for one display using draw context (buf_area + clip_area)
|
||||
* @param ctx Draw context: buf, buf_area, clip_area, stride, swap
|
||||
* buf_area and clip_area use half-open bounds [x1, x2) x [y1, y2)
|
||||
*/
|
||||
void gfx_render_draw_child_objects(gfx_disp_t *disp, const gfx_draw_ctx_t *ctx);
|
||||
|
||||
/**
|
||||
* @brief Update child objects for one display
|
||||
*/
|
||||
void gfx_render_update_child_objects(gfx_disp_t *disp);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "core/gfx_types.h"
|
||||
#include "core/gfx_disp.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*********************
|
||||
* DEFINES
|
||||
*********************/
|
||||
|
||||
/**
|
||||
* Bit flag OR-ed into the `internal_edges` parameter of
|
||||
* gfx_sw_blend_img_triangle_draw to request inward-only edge AA.
|
||||
* When set, the rasterizer fades pixels near non-internal outer edges
|
||||
* from inside the triangle rather than drawing semi-transparent pixels
|
||||
* outside it. This prevents visible "bleed" on thin strokes (eyebrows).
|
||||
*/
|
||||
#define GFX_BLEND_TRI_AA_INWARD 0x80
|
||||
|
||||
#define GFX_BLEND_MAX_EXTRA_AA_EDGES 2
|
||||
|
||||
/**********************
|
||||
* TYPEDEFS
|
||||
**********************/
|
||||
|
||||
typedef struct {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
gfx_coord_t u;
|
||||
gfx_coord_t v;
|
||||
} gfx_sw_blend_img_vertex_t;
|
||||
|
||||
/**
|
||||
* Extra directed edge for cross-triangle inward AA.
|
||||
* Represents edge A→B in mesh subpixel coordinates.
|
||||
*/
|
||||
typedef struct {
|
||||
int32_t a; /**< dy: B.y - A.y */
|
||||
int32_t b; /**< -dx: A.x - B.x */
|
||||
int32_t len; /**< sqrt(a² + b²) */
|
||||
int32_t vx; /**< reference vertex x (A.x, mesh subpixel) */
|
||||
int32_t vy; /**< reference vertex y (A.y, mesh subpixel) */
|
||||
} gfx_sw_blend_aa_edge_t;
|
||||
|
||||
/**********************
|
||||
* PRIVATE FUNCTIONS
|
||||
**********************/
|
||||
|
||||
/**
|
||||
* @brief Fast fill buffer with background color
|
||||
* @param buf Pointer to uint16_t buffer
|
||||
* @param color 16-bit color value
|
||||
* @param pixels Number of pixels to fill
|
||||
*/
|
||||
void gfx_sw_blend_fill(uint16_t *buf, uint16_t color, size_t pixels);
|
||||
|
||||
/**
|
||||
* @brief Fill a rectangle in dest buffer (standard blend form: dest_buf + stride + area)
|
||||
* @param dest_buf Destination buffer (uint16_t)
|
||||
* @param dest_stride Row stride in pixels
|
||||
* @param area Area to fill (x1,y1,x2,y2 exclusive end)
|
||||
* @param color 16-bit color value in native framebuffer byte order
|
||||
*/
|
||||
void gfx_sw_blend_fill_area(uint16_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_area_t *area, uint16_t color);
|
||||
|
||||
/**
|
||||
* @brief Mix two colors with a given mix ratio (internal)
|
||||
* @param c1 First color
|
||||
* @param c2 Second color
|
||||
* @param mix Mix ratio (0-255)
|
||||
* @param swap Whether to swap color format
|
||||
* @return Mixed color
|
||||
*/
|
||||
gfx_color_t gfx_blend_color_mix(gfx_color_t c1, gfx_color_t c2, uint8_t mix, bool swap);
|
||||
|
||||
/**********************
|
||||
* GLOBAL PROTOTYPES
|
||||
**********************/
|
||||
|
||||
/*=====================
|
||||
* Software blending functions
|
||||
*====================*/
|
||||
|
||||
/**
|
||||
* @brief Draw a blended color onto a destination buffer
|
||||
* @param dest_buf Pointer to the destination buffer where the color will be drawn
|
||||
* @param dest_stride Stride (width) of the destination buffer
|
||||
* @param mask Pointer to the mask buffer, if any
|
||||
* @param mask_stride Stride (width) of the mask buffer
|
||||
* @param clip_area Pointer to the clipping area, which limits the area to draw
|
||||
* @param color The color to draw in gfx_color_t type
|
||||
* @param opa The opacity of the color to draw (0-255)
|
||||
* @param swap Whether to swap the color format
|
||||
*/
|
||||
void gfx_sw_blend_draw(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_opa_t *mask, gfx_coord_t mask_stride,
|
||||
gfx_area_t *clip_area, gfx_color_t color, gfx_opa_t opa, bool swap);
|
||||
|
||||
/**
|
||||
* @brief Draw a blended image onto a destination buffer
|
||||
* @param dest_buf Pointer to the destination buffer where the image will be drawn
|
||||
* @param dest_stride Stride (width) of the destination buffer
|
||||
* @param src_buf Pointer to the source image buffer
|
||||
* @param src_stride Stride (width) of the source image buffer
|
||||
* @param mask Pointer to the mask buffer, if any
|
||||
* @param mask_stride Stride (width) of the mask buffer
|
||||
* @param clip_area Pointer to the clipping area, which limits the area to draw
|
||||
* @param swap Whether to swap the color format
|
||||
*/
|
||||
void gfx_sw_blend_img_draw(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_color_t *src_buf, gfx_coord_t src_stride,
|
||||
const gfx_opa_t *mask, gfx_coord_t mask_stride,
|
||||
gfx_area_t *clip_area, bool swap);
|
||||
|
||||
/**
|
||||
* @brief Draw a textured triangle with edge anti-aliasing
|
||||
*
|
||||
* @param internal_edges Bitmask of edges shared with adjacent triangles.
|
||||
* Bit 0 = edge 0 (v1→v2), bit 1 = edge 1 (v2→v0), bit 2 = edge 2 (v0→v1).
|
||||
* AA is suppressed on flagged edges to prevent dark-seam artifacts.
|
||||
* Pass 0 for standalone triangles (full AA on all edges).
|
||||
* @param extra_aa_edges Optional array of extra directed edges for cross-triangle
|
||||
* inward AA distance (NULL when not needed).
|
||||
* @param extra_aa_count Number of entries in extra_aa_edges (0..MAX_EXTRA_AA_EDGES).
|
||||
*/
|
||||
void gfx_sw_blend_img_triangle_draw(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_area_t *buf_area, const gfx_area_t *clip_area,
|
||||
const gfx_color_t *src_buf, gfx_coord_t src_stride, gfx_coord_t src_height,
|
||||
const gfx_opa_t *mask, gfx_coord_t mask_stride,
|
||||
gfx_opa_t opa,
|
||||
const gfx_sw_blend_img_vertex_t *v0,
|
||||
const gfx_sw_blend_img_vertex_t *v1,
|
||||
const gfx_sw_blend_img_vertex_t *v2,
|
||||
uint8_t internal_edges,
|
||||
const gfx_sw_blend_aa_edge_t *extra_aa_edges,
|
||||
uint8_t extra_aa_count,
|
||||
bool swap);
|
||||
|
||||
/**
|
||||
* @brief Scanline polygon fill with edge anti-aliasing.
|
||||
*
|
||||
* Fills a closed polygon defined by vertex arrays with a solid color.
|
||||
* Uses even-odd fill rule. Vertices are in mesh subpixel coordinates
|
||||
* (same as gfx_sw_blend_img_vertex_t x/y).
|
||||
*
|
||||
* Designed for stroke outlines where no texture mapping is needed.
|
||||
*/
|
||||
void gfx_sw_blend_polygon_fill(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_area_t *buf_area, const gfx_area_t *clip_area,
|
||||
gfx_color_t color,
|
||||
gfx_opa_t opa,
|
||||
const int32_t *vx, const int32_t *vy,
|
||||
int vertex_count,
|
||||
bool swap);
|
||||
|
||||
void gfx_sw_blend_perf_reset(gfx_blend_perf_stats_t *stats);
|
||||
void gfx_sw_blend_perf_bind(gfx_blend_perf_stats_t *stats);
|
||||
void gfx_sw_blend_perf_unbind(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*********************
|
||||
* INCLUDES
|
||||
*********************/
|
||||
#include <stddef.h>
|
||||
|
||||
#include "core/draw/gfx_blend_priv.h"
|
||||
#include "core/draw/gfx_sw_draw_priv.h"
|
||||
|
||||
/**********************
|
||||
* STATIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
static bool gfx_sw_draw_get_pixel_ptr(gfx_color_t **pixel,
|
||||
gfx_color_t *dest_buf,
|
||||
gfx_coord_t dest_stride,
|
||||
const gfx_area_t *buf_area,
|
||||
const gfx_area_t *clip_area,
|
||||
gfx_coord_t x,
|
||||
gfx_coord_t y)
|
||||
{
|
||||
if (pixel == NULL || dest_buf == NULL || buf_area == NULL || clip_area == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x < clip_area->x1 || x >= clip_area->x2 || y < clip_area->y1 || y >= clip_area->y2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x < buf_area->x1 || x >= buf_area->x2 || y < buf_area->y1 || y >= buf_area->y2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*pixel = dest_buf + (size_t)(y - buf_area->y1) * dest_stride + (size_t)(x - buf_area->x1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**********************
|
||||
* PUBLIC FUNCTIONS
|
||||
**********************/
|
||||
|
||||
void gfx_sw_draw_point(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_area_t *buf_area, const gfx_area_t *clip_area,
|
||||
gfx_coord_t x, gfx_coord_t y,
|
||||
gfx_color_t color, gfx_opa_t opa, bool swap)
|
||||
{
|
||||
gfx_color_t *pixel = NULL;
|
||||
gfx_color_t draw_color = color;
|
||||
|
||||
if (!gfx_sw_draw_get_pixel_ptr(&pixel, dest_buf, dest_stride, buf_area, clip_area, x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opa >= 0xFF) {
|
||||
draw_color.full = gfx_color_to_native_u16(draw_color, swap);
|
||||
*pixel = draw_color;
|
||||
} else if (opa > 0) {
|
||||
*pixel = gfx_blend_color_mix(color, *pixel, opa, swap);
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_sw_draw_hline(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_area_t *buf_area, const gfx_area_t *clip_area,
|
||||
gfx_coord_t x1, gfx_coord_t x2, gfx_coord_t y,
|
||||
gfx_color_t color, gfx_opa_t opa, bool swap)
|
||||
{
|
||||
gfx_color_t draw_color = color;
|
||||
|
||||
if (dest_buf == NULL || buf_area == NULL || clip_area == NULL || x2 <= x1 || opa == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clip against both clip_area and buf_area in one pass */
|
||||
gfx_coord_t draw_x1 = (x1 > clip_area->x1) ? x1 : clip_area->x1;
|
||||
gfx_coord_t draw_x2 = (x2 < clip_area->x2) ? x2 : clip_area->x2;
|
||||
if (draw_x1 < buf_area->x1) {
|
||||
draw_x1 = buf_area->x1;
|
||||
}
|
||||
if (draw_x2 > buf_area->x2) {
|
||||
draw_x2 = buf_area->x2;
|
||||
}
|
||||
|
||||
if (draw_x2 <= draw_x1) {
|
||||
return;
|
||||
}
|
||||
if (y < clip_area->y1 || y >= clip_area->y2 || y < buf_area->y1 || y >= buf_area->y2) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_color_t *pixel = dest_buf + (size_t)(y - buf_area->y1) * dest_stride
|
||||
+ (size_t)(draw_x1 - buf_area->x1);
|
||||
size_t count = (size_t)(draw_x2 - draw_x1);
|
||||
|
||||
if (opa >= 0xFF) {
|
||||
draw_color.full = gfx_color_to_native_u16(draw_color, swap);
|
||||
gfx_sw_blend_fill((uint16_t *)pixel, draw_color.full, count);
|
||||
} else {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
pixel[i] = gfx_blend_color_mix(color, pixel[i], opa, swap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_sw_draw_vline(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_area_t *buf_area, const gfx_area_t *clip_area,
|
||||
gfx_coord_t x, gfx_coord_t y1, gfx_coord_t y2,
|
||||
gfx_color_t color, gfx_opa_t opa, bool swap)
|
||||
{
|
||||
gfx_color_t draw_color = color;
|
||||
|
||||
if (dest_buf == NULL || buf_area == NULL || clip_area == NULL || y2 <= y1 || opa == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (x < clip_area->x1 || x >= clip_area->x2 || x < buf_area->x1 || x >= buf_area->x2) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clip against both clip_area and buf_area in one pass */
|
||||
gfx_coord_t draw_y1 = (y1 > clip_area->y1) ? y1 : clip_area->y1;
|
||||
gfx_coord_t draw_y2 = (y2 < clip_area->y2) ? y2 : clip_area->y2;
|
||||
if (draw_y1 < buf_area->y1) {
|
||||
draw_y1 = buf_area->y1;
|
||||
}
|
||||
if (draw_y2 > buf_area->y2) {
|
||||
draw_y2 = buf_area->y2;
|
||||
}
|
||||
|
||||
if (draw_y2 <= draw_y1) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_color_t *pixel = dest_buf + (size_t)(draw_y1 - buf_area->y1) * dest_stride
|
||||
+ (size_t)(x - buf_area->x1);
|
||||
|
||||
if (opa >= 0xFF) {
|
||||
draw_color.full = gfx_color_to_native_u16(draw_color, swap);
|
||||
for (gfx_coord_t row = draw_y1; row < draw_y2; ++row) {
|
||||
*pixel = draw_color;
|
||||
pixel += dest_stride;
|
||||
}
|
||||
} else {
|
||||
for (gfx_coord_t row = draw_y1; row < draw_y2; ++row) {
|
||||
*pixel = gfx_blend_color_mix(color, *pixel, opa, swap);
|
||||
pixel += dest_stride;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_sw_draw_rect_stroke(gfx_color_t *dest_buf, gfx_coord_t dest_stride,
|
||||
const gfx_area_t *buf_area, const gfx_area_t *clip_area,
|
||||
const gfx_area_t *rect, uint16_t line_width,
|
||||
gfx_color_t color, gfx_opa_t opa, bool swap)
|
||||
{
|
||||
gfx_coord_t max_line_w;
|
||||
gfx_coord_t max_line_h;
|
||||
gfx_coord_t stroke_w;
|
||||
|
||||
if (dest_buf == NULL || buf_area == NULL || clip_area == NULL || rect == NULL || line_width == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rect->x2 <= rect->x1 || rect->y2 <= rect->y1) {
|
||||
return;
|
||||
}
|
||||
|
||||
max_line_w = (gfx_coord_t)((line_width * 2U <= (uint16_t)(rect->x2 - rect->x1)) ? line_width : ((rect->x2 - rect->x1) / 2));
|
||||
max_line_h = (gfx_coord_t)((line_width * 2U <= (uint16_t)(rect->y2 - rect->y1)) ? line_width : ((rect->y2 - rect->y1) / 2));
|
||||
stroke_w = (max_line_w < max_line_h) ? max_line_w : max_line_h;
|
||||
|
||||
if (stroke_w <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (gfx_coord_t i = 0; i < stroke_w; ++i) {
|
||||
gfx_sw_draw_hline(dest_buf, dest_stride, buf_area, clip_area,
|
||||
rect->x1 + i, rect->x2 - i, rect->y1 + i,
|
||||
color, opa, swap);
|
||||
gfx_sw_draw_hline(dest_buf, dest_stride, buf_area, clip_area,
|
||||
rect->x1 + i, rect->x2 - i, rect->y2 - 1 - i,
|
||||
color, opa, swap);
|
||||
gfx_sw_draw_vline(dest_buf, dest_stride, buf_area, clip_area,
|
||||
rect->x1 + i, rect->y1 + i, rect->y2 - i,
|
||||
color, opa, swap);
|
||||
gfx_sw_draw_vline(dest_buf, dest_stride, buf_area, clip_area,
|
||||
rect->x2 - 1 - i, rect->y1 + i, rect->y2 - i,
|
||||
color, opa, swap);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user