Compare commits

...

6 Commits

Author SHA1 Message Date
4de9f2ba61 feat(assets): 添加 hiyori-assets.bin(Phase 10 EAF 数字人资源包)
ESP Emote GFX Packer 在线工具(emote-gfx-gen-tool-dev.pages.dev)
打包产物,含:
- index.json (286 B): 表情元数据
- hiyori_m06.eaf (505 KB): 默认/积极情绪动画
- hiyori_m07.eaf (450 KB): 思考/疲倦情绪动画
  (m03 待补,sad/angry 暂用 m07 fallback)

sdkconfig 同步: 新 esp_emote_gfx Kconfig 选项的默认值
(HEATSHRINK + JPEG decode + tri-edge AA 等)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:56:33 +08:00
0d0bc33192 chore(deps): managed_components 自动更新
idf.py reconfigure 触发的 idf-component-manager 版本刷新副作用:
- espressif/dl_fft, esp_jpeg, freetype 等组件的小版本更新

不影响项目功能,与 Phase 10 无直接关系。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:56:33 +08:00
86200f5e3a chore(deps): 添加 esp_emote_gfx + esp_mmap_assets 组件(Phase 10 依赖)
Phase 10 数字人模式 LVGL → EAF 切换所需的两个新组件:
- espressif2022/esp_emote_gfx v3.0.5
  轻量软件渲染 UI 框架(gfx_emote_init/gfx_disp_add/gfx_anim/gfx_label)
- espressif/esp_mmap_assets v2.0.0
  资源打包加载(虽然 use_fs 模式 buggy,我们绕过它直接 fopen,
  但保留组件以便后续 mmap partition 模式启用)

gfx_touch.c 含我们的 local shim 兼容 esp_lcd_touch v1.1.2 旧 API
(已在前一个 commit 31982ba 中说明)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:56:32 +08:00
31982ba7b9 feat(ui): Phase 10 - 数字人模式 LVGL → esp_emote_gfx 完整切换
 验证完成:
- 音频卡顿明显改善(用户实测)
- 数字人 hiyori 动画正常显示
- nm 验证:固件中 0 个 lv_*/lvgl_* 函数符号
- kapi.bin: 4.7MB → 2.75MB(-42%)

关键改动:
- main/dzbj/ai_chat_ui_eaf.c (404 行新增):
  完全替代 LVGL 版 ai_chat_ui.c,提供同名 C API(ai_chat_screen_init
  / set_status / set_emotion / set_chat_message / resume_animation)。
  AiChatDisplay C++ 桥接层无需改动。
  内部用 gfx_emote_init + gfx_disp_add + gfx_anim + mmap_assets。
- main/CMakeLists.txt:双轨编译
  CONFIG_BAJI_BADGE_MODE=y → ai_chat_ui.c (LVGL) + bg_gif_demo.c
  CONFIG_BAJI_BADGE_MODE=n → ai_chat_ui_eaf.c (esp_emote_gfx)
- main/dzbj/dzbj_init.c:EAF 模式跳过 lvgl_lcd_init() 调用
- main/dzbj/lcd.c/h:暴露 lcd_io_handle 给 EAF 注册 IO 完成回调

踩坑修复(commit message 留档供后续参考):
1. esp_mmap_assets v2.0.0 在 use_fs=true 模式下 mmap_assets_get_mem()
   返回的是文件内偏移量而非 RAM 指针(fseek bug + offset 没加
   data_section_start),导致 LoadProhibited panic。
   解决:完全绕过 mmap_assets,自己 fopen + 解析 MMAP bin 头
   (layout: 头 16B + 每 entry 28B + data 段每文件 2B magic + 数据)。
2. esp_emote_gfx 期望 esp_lcd_touch v2.x 新 API,项目用 v1.1.2 旧 API。
   在 managed_components 内 gfx_touch.c 加 shim 桥接(local patch,
   reconfigure 后需 reapply)。
3. EAF format magic 是 0x89 'EAF'(gfx_eaf_dec.h),不是 0x5A5A
   (那是 esp_mmap_assets 内部文件分隔符)。
4. SPIFFS 需要在 ai_chat_screen_init 入口自动挂载(不能依赖
   bg_gif_demo 的惰性挂载,那个已被 CONFIG 排除)。

依赖增量:
- espressif2022/esp_emote_gfx: ~3.0.5
- espressif/esp_mmap_assets: * (仅用于声明依赖,运行时被绕过)

数字人模式核心 UI 范围:
- 显示数字人动画  (hiyori_m06/m07, 居中循环)
- 情绪 → GIF 映射  (23 情绪 → 2 EAF,sad/angry 暂用 m07,m03 待补)
- 字幕/状态文字: stub (字体接驳留待后续,需打包 .bin 字体到资源)
- 触摸: 不支持(PoC 阶段不需要)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:53:21 +08:00
3a1111e99d plan(rtc-only): Phase 9 取消 + Phase 10/11/12 规划(LVGL → esp_emote_gfx)
Phase 9 三轮增量优化(jitter buffer / codec init / Core 1 绑定)效果不
明显,用户决策完整切 EAF 验证 GIF 抢资源假设。

Phase 9 → CANCELLED:
- v1 jitter buffer device_state 判错(漏 kDeviceStateDialog)
- v1 ES7210 重试破坏 ES8311 init 导致开机播报无声
- v2 修正 device_state 后 jitter 工作但仍卡
- v3 background_task 绑 Core 1 + DIAG-5 未硬件验证
- 所有代码改动 git restore 回滚(无 commit),Phase 8 DIAG 埋点保留
- CANCELLED.md 记录教训

Phase 10 新增(数字人模式 LVGL → esp_emote_gfx 完整切换):
- 添加 espressif2022/esp_emote_gfx ~3.0.5 依赖(已 reconfigure 拉取)
- API 风险扫清:GFX_LABEL_LONG_WRAP 支持中文换行、
  gfx_font_lv_load_from_binary 兼容 LVGL bitmap font
- 双轨编译:CONFIG_BAJI_BADGE_MODE=y 保 LVGL,=n 走 EAF
- PLAN.md 含 10 个子任务从依赖到完整 UI 切换
- 预估 3-5 天

Phase 11 占位:LVGL 释放的 ~40KB DRAM + ~80KB PSRAM 投到 WiFi
缓冲扩容(STATIC_RX 10→16、DYN_RX/TX 32→48、RX_BA_WIN 6→16)+
Opus/RTC SDK jitter buffer 扩容

Phase 12 占位:原 Phase 10 集成测试 + 推送,重编号

ROADMAP 同步更新,依赖关系矫正。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:37:34 +08:00
3dc6cadf49 diag(rtc-only): Phase 8 - 音频卡顿根因诊断埋点 + 数据采集报告
数字人 RTC 模式音频卡顿根因定位。通过 4 类 ESP_LOGW 埋点采集运行时
数据,对照表格判定根因,输出 Phase 9 实施分支决策。

埋点实现(main/application.cc,PHASE8_DIAG_ENABLE 宏开关,关闭后零开销):
- DIAG-1 queue 深度:3 处(出队 + WebSocket 入队 + RTC 入队),50ms 节流
- DIAG-2 codec->OutputData 写入耗时:>15ms 阈值告警
- DIAG-3 WiFi RSSI:OnClockTimer 1Hz
- DIAG-4 heap 快照 + 碎片率:OnClockTimer 1Hz

实测结论(见 DIAG_REPORT.md):用户感知卡顿 = 两个独立根因
- A. 开机播报阶段 ③' codec init 时序缺陷(ES7210 I2C 失败 +
  126 次 write_slow 集中在 2-13s)
- B. RTC 对话阶段 ⑤ Opus/WebSocket 应用层帧到达抖动
  (queue 突发堆积 19 + queue=0 出现 58 次,但 codec 写入 0 次 slow)

完全排除:① CPU 争抢、② PSRAM 带宽、④ WiFi 丢包(RSSI -24~-33dBm
极强)、⑥ 内存碎片(heap 全程稳定)

Phase 9 推荐分支 B'(双线修复,原 A/C 的 EAF 方案不适用):
- 9.1 应用层 jitter buffer(fill-threshold + drain)—— 解 B
- 9.2 开机 codec init 时序修复(ES7210 reset + ready 等待)—— 解 A
- 估时 1 天

ROADMAP 同步:Phase 7 矫正为 battery_psm(实际状态)、Phase 8 新增
诊断、Phase 9 占位待 Phase 8 决策、原"集成测试"挪到 Phase 10。
新增 .planning/STATE.md 记录 roadmap evolution。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:40:42 +08:00
1050 changed files with 547271 additions and 307 deletions

42
.planning/STATE.md Normal file
View 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 PSRAMLVGL 框架本体)
- 投入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 后如何接驳(需确认数字人模式是否需要触摸交互)

View File

@ -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 regressionv2 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 → .eaf4-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 → 16AMPDU 窗口扩大减少重传)
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 1dzbj 模块清理完成)+ Phase 3GIF 资源就位)
- Phase 4 ⊥ Phase 5情绪映射和字幕显示独立可并行
- Phase 6 依赖 Phase 1清理 sleep_mgr 调用点)
- Phase 7 必须最后
- Phase 7 依赖 Phase 6PowerSaveTimer 状态机重写需 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 重编号) |

View File

@ -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 | 火山 RTCVolcEngineRTCLite 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_slowcodec 写入正常)
```
### 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_slowcodec 时间预算充足 |
| ② 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_gfxCPU 完全够用,原 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) 顶部)

View File

@ -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))`(入队点 2RTC 下行)
- 第 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
```
入队点 2line 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` ≥ 5005 分钟 × 多类日志)
- 日志中至少有 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 依赖
- ❌ 不调整 sdkconfigWiFi 缓冲、PSM、jitter buffer 都不动)
- ❌ 不优化 lv_gif / LZW 解码(那是 Phase 9 的事)
- ❌ 不写 audio_codecs/ 层埋点(写在 application.cc 端覆盖所有板子足够)
- ❌ 不删除已有日志DIAG 是新增 tag不冲突

View File

@ -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 依赖
- 不调整 sdkconfigWiFi 缓冲、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集成测试 + 推送

View File

@ -0,0 +1,71 @@
# Phase 9 — 已取消
**取消日期**2026-05-15
**取消决策方**:用户
**取消原因**增量修补策略jitter buffer + codec init 调整)效果不明显,用户改为方案 C 完整切 EAFPhase 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
### v2hotfix
**改动**
- 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 模板 / 多版本日志)保留供回溯

View File

@ -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 buffer3 处代码 + N 行
- 9.2 codec init2 处代码 + 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` 次数 < 20vs baseline 126
- [ ] 对话期 `queue=0` 次数 < 15vs baseline 58
- [ ] 用户主观:开机"卡卡在呢"不抖 + 对话 AI 声音连贯
- [ ] IMPL_REPORT.md 填实数据
- [ ] commit 推送到 Rtc_AIavatar
---
## 6. Phase 9 不做的事
- ❌ 不引入 esp_emote_gfxPhase 8 数据否决了 CPU 假设)
- ❌ 不动 `CONFIG_ESP_WIFI_*_BUFFER_NUM`(物理层 OK
- ❌ 不深挖 ES7210 寄存器级 init 问题(重试 + 降级足够,深层修复留待后续)
- ❌ 不关闭 PHASE8_DIAG_ENABLE保留埋点便于 Phase 9 验证)
- ❌ 不动 LVGL/GIF 相关代码

View File

@ -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 fontEAF 文档说支持 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.gifPoC 阶段)
**前置**: 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吧唧模式专属

View File

@ -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

View File

@ -28,10 +28,10 @@ set(SOURCES "audio_codecs/audio_codec.cc"
"dzbj/pages_pwm.c"
"dzbj/dzbj_init.c" # dzbj_hw_display_initdzbj_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 PoCRGB565 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 4bppGB2312
# 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 UIAI + 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)

View File

@ -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 RSSI1Hzclock_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
View 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"},
// 负面/严肃 → 暂用 m07m03 未导入)
{"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 实际 layouthex 反推得出):
// [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, "状态: %sPoC 阶段暂不显示)", 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, "字幕: %sPoC 阶段暂不显示)", content);
}
}
void ai_chat_resume_animation(void) {
// EAF 动画由 gfx_anim_start 持续播放,无需手动 resume
ESP_LOGD(TAG, "resume_animationEAF 模式下自动循环,无需操作)");
}

View File

@ -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

View File

@ -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),

View File

@ -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);

View File

@ -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"

View File

@ -0,0 +1 @@
a06a58c74f7deb4186460f27f5e6db52fda4c254d7e03c3e05e7987aaf73de1a

View 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]

View File

@ -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]

View 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

View File

@ -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
)

View 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

View 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.

View 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).

View File

@ -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:

View 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/

View File

@ -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)

View 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;
}
}

View 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 &amp; 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

View 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');
}
});
})();

View 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 %}

View File

@ -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 (120) */
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

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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

View 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')

View File

@ -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.

View File

@ -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`

View File

@ -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)": "RGB56516 位色)",
"RGB565A8 format (16-bit color with 8-bit alpha)": "RGB565A816 位色 + 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": "EAFESP 动画格式)文件",
"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)": "FreeTypeTTF/OTF 字体)",
"ESP New JPEG (for JPEG decoding)": "ESP New JPEGJPEG 解码)",
"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,
}

View File

@ -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.

View File

@ -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 colorsource 是一个 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 flagspose 和 canvas 未变化时避免 apply mesh。
* Cached segment grids热路径避免重复 realloc mesh points。
* Bezier sampling densitystroke 和 fill 分别调采样密度。
* Resource UV updates只有 grid 或 resource crop 改变时才重算 rest points。
* Mesh bounds保留 clamp warning因为过大 bounds 往往暴露坐标 bug。
* Scanline fillsolid 大面积 fill 优先走 scanlinetextured 或 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避免静默绘制空白。

View File

@ -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.

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1,4 @@
esp-docs>=2.1.5
breathe>=4.35.0
Babel>=2.12.0

View File

@ -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()

View File

@ -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}/"

View File

@ -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())

View File

@ -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

View File

@ -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

View File

@ -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 (120) */
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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

View File

@ -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

View File

@ -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__)

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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 AB 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 (v1v2), bit 1 = edge 1 (v2v0), bit 2 = edge 2 (v0v1).
* 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

View File

@ -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