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>
This commit is contained in:
parent
3a1111e99d
commit
31982ba7b9
@ -169,6 +169,20 @@ dependencies:
|
|||||||
registry_url: https://components.espressif.com/
|
registry_url: https://components.espressif.com/
|
||||||
type: service
|
type: service
|
||||||
version: 2.5.0
|
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:
|
espressif/esp_new_jpeg:
|
||||||
component_hash: 98823384f51ca298e2c9bebacd1c629148e528ed0902d18b16556df151519e68
|
component_hash: 98823384f51ca298e2c9bebacd1c629148e528ed0902d18b16556df151519e68
|
||||||
dependencies: []
|
dependencies: []
|
||||||
@ -290,11 +304,12 @@ direct_dependencies:
|
|||||||
- espressif/esp_lcd_touch
|
- espressif/esp_lcd_touch
|
||||||
- espressif/esp_lcd_touch_cst816s
|
- espressif/esp_lcd_touch_cst816s
|
||||||
- espressif/esp_lvgl_port
|
- espressif/esp_lvgl_port
|
||||||
|
- espressif/esp_mmap_assets
|
||||||
- espressif/knob
|
- espressif/knob
|
||||||
- espressif/led_strip
|
- espressif/led_strip
|
||||||
- espressif2022/esp_emote_gfx
|
- espressif2022/esp_emote_gfx
|
||||||
- idf
|
- idf
|
||||||
- lvgl/lvgl
|
- lvgl/lvgl
|
||||||
manifest_hash: 90544e3d787e63c288feeb33cf16100755d3ed90c47270526fd2fb5754eba469
|
manifest_hash: 56465d60ff0a813df7f9be998612a4c2bc61e6d560c2f56fd585445d05b25456
|
||||||
target: esp32s3
|
target: esp32s3
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
|||||||
@ -28,10 +28,10 @@ set(SOURCES "audio_codecs/audio_codec.cc"
|
|||||||
"dzbj/pages_pwm.c"
|
"dzbj/pages_pwm.c"
|
||||||
"dzbj/dzbj_init.c" # 含 dzbj_hw_display_init(公共硬件初始化);dzbj_display_init 函数体内部 #ifdef 包裹
|
"dzbj/dzbj_init.c" # 含 dzbj_hw_display_init(公共硬件初始化);dzbj_display_init 函数体内部 #ifdef 包裹
|
||||||
"dzbj/fatfs.c" # DecodeImg 公共(AI 模式 BG GIF PoC 也用);吧唧专用函数 fatfs_init/list 等无副作用
|
"dzbj/fatfs.c" # DecodeImg 公共(AI 模式 BG GIF PoC 也用);吧唧专用函数 fatfs_init/list 等无副作用
|
||||||
"dzbj/ai_chat_ui.c" # AI 对话模式 LVGL 屏幕
|
# Phase 10: ai_chat_ui 双轨编译
|
||||||
"dzbj/sprite_demo.c" # Sprite Sheet PoC(RGB565 raw 替代 GIF)
|
# CONFIG_BAJI_BADGE_MODE=y → ai_chat_ui.c (LVGL 版)
|
||||||
"dzbj/dual_gif_demo.c" # 双 GIF 循环播放 PoC
|
# CONFIG_BAJI_BADGE_MODE=n → ai_chat_ui_eaf.c (esp_emote_gfx 版)
|
||||||
"dzbj/bg_gif_demo.c" # 背景 + 透明 GIF 叠加(方案 C)
|
# 这两个在下方 if(CONFIG_BAJI_BADGE_MODE) 块中条件编译
|
||||||
"fonts/font_puhui_20_4.c" # 阿里巴巴普惠体 20px 4bpp(GB2312 简体中文)
|
"fonts/font_puhui_20_4.c" # 阿里巴巴普惠体 20px 4bpp(GB2312 简体中文)
|
||||||
# SquareLine Studio UI 公共文件(AI 模式也使用)
|
# SquareLine Studio UI 公共文件(AI 模式也使用)
|
||||||
"ui/ui.c"
|
"ui/ui.c"
|
||||||
@ -221,6 +221,11 @@ if(CONFIG_BAJI_BADGE_MODE)
|
|||||||
"dzbj/dzbj_button.c"
|
"dzbj/dzbj_button.c"
|
||||||
"dzbj/dzbj_battery.c"
|
"dzbj/dzbj_battery.c"
|
||||||
"dzbj/ble_transfer.c"
|
"dzbj/ble_transfer.c"
|
||||||
|
# 吧唧模式 LVGL UI(AI 对话屏幕 + 各种 PoC)
|
||||||
|
"dzbj/ai_chat_ui.c"
|
||||||
|
"dzbj/sprite_demo.c"
|
||||||
|
"dzbj/dual_gif_demo.c"
|
||||||
|
"dzbj/bg_gif_demo.c"
|
||||||
# SquareLine Studio 吧唧专属 UI 屏幕(9 个)
|
# SquareLine Studio 吧唧专属 UI 屏幕(9 个)
|
||||||
"ui/screens/ui_ScreenHome.c"
|
"ui/screens/ui_ScreenHome.c"
|
||||||
"ui/screens/ui_ScreenImg.c"
|
"ui/screens/ui_ScreenImg.c"
|
||||||
@ -232,6 +237,11 @@ if(CONFIG_BAJI_BADGE_MODE)
|
|||||||
"ui/screens/ui_ScreenSharing.c"
|
"ui/screens/ui_ScreenSharing.c"
|
||||||
"ui/screens/ui_ScreenReceiving.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()
|
endif()
|
||||||
|
|
||||||
if(CONFIG_CONNECTION_TYPE_MQTT_UDP)
|
if(CONFIG_CONNECTION_TYPE_MQTT_UDP)
|
||||||
|
|||||||
404
main/dzbj/ai_chat_ui_eaf.c
Normal file
404
main/dzbj/ai_chat_ui_eaf.c
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
/*
|
||||||
|
* Phase 10: 数字人 RTC 模式 EAF UI 实现
|
||||||
|
*
|
||||||
|
* 完全替代 ai_chat_ui.c (LVGL 版),提供相同的 C API 签名让 AiChatDisplay 桥接层无需改动。
|
||||||
|
*
|
||||||
|
* 架构:
|
||||||
|
* esp_emote_gfx (gfx_emote_init + gfx_disp_add + gfx_anim)
|
||||||
|
* ↓
|
||||||
|
* mmap_assets (use_fs 模式,从 /spiflash/hiyori-assets.bin 加载)
|
||||||
|
* ↓
|
||||||
|
* panel_handle (lcd.c 暴露,已由 lcd_init 完成硬件初始化)
|
||||||
|
*
|
||||||
|
* PoC 阶段说明:
|
||||||
|
* - 只显示数字人动画(核心目的:验证显示 + 听感效果)
|
||||||
|
* - 字幕/状态文字: 仅日志输出(字体接驳留待后续,需要打包 .bin 字体到 mmap_assets)
|
||||||
|
* - 资源限制: 当前 hiyori-assets.bin 只含 m06 + m07(用户在线工具未导入 m03)
|
||||||
|
* sad/angry 等负面情绪暂时降级到 m07
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ai_chat_ui.h"
|
||||||
|
#include "lcd.h" // 引用 panel_handle / lcd_io_handle
|
||||||
|
|
||||||
|
#include "gfx.h"
|
||||||
|
#include "esp_mmap_assets.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_lcd_panel_io.h"
|
||||||
|
#include "esp_lcd_panel_ops.h"
|
||||||
|
#include "esp_spiffs.h" // Phase 10: SPIFFS 自动挂载
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// Phase 10 v2 修复:
|
||||||
|
// esp_mmap_assets v2.0.0 在 use_fs=true 模式下,mmap_assets_get_mem() 返回的是文件内偏移量
|
||||||
|
// 而不是 RAM 指针(看 esp_mmap_assets.c line 523 + line 353 的 fseek 用法)。
|
||||||
|
// 把 offset 当指针 dereference 会导致 LoadProhibited panic。
|
||||||
|
// 修复:开机时用 mmap_assets_copy_by_index 把所有 EAF 数据 fread 到 PSRAM buffer,
|
||||||
|
// 运行时直接用 buffer 指针给 gfx_anim 使用。
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *data; // EAF 数据 PSRAM 指针(malloc 出来)
|
||||||
|
size_t size; // EAF 大小
|
||||||
|
char name[40]; // 文件名
|
||||||
|
} eaf_cache_entry_t;
|
||||||
|
|
||||||
|
static eaf_cache_entry_t s_eaf_cache[8]; // 预留 8 个表情槽位
|
||||||
|
static int s_eaf_cache_count = 0;
|
||||||
|
|
||||||
|
static const char *TAG = "AI_CHAT_EAF";
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 配置常量
|
||||||
|
// ==========================================================
|
||||||
|
#define EAF_ASSETS_PATH "/spiflash/hiyori-assets.bin"
|
||||||
|
#define EAF_MAX_FILES 3 // index.json + 2 个 EAF (m06 + m07)
|
||||||
|
#define EAF_DEFAULT_FPS 14 // 与工具配置一致
|
||||||
|
#define LCD_W 360
|
||||||
|
#define LCD_H 360
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 全局 EAF 上下文
|
||||||
|
// ==========================================================
|
||||||
|
static gfx_handle_t s_emote_handle = NULL;
|
||||||
|
static gfx_disp_t *s_disp = NULL;
|
||||||
|
static gfx_obj_t *s_anim_obj = NULL;
|
||||||
|
static mmap_assets_handle_t s_assets = NULL;
|
||||||
|
static int s_current_emotion_idx = -1;
|
||||||
|
static bool s_initialized = false;
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 情绪 → asset 名字 映射表
|
||||||
|
// ==========================================================
|
||||||
|
typedef struct {
|
||||||
|
const char *emotion;
|
||||||
|
const char *asset_name;
|
||||||
|
} eaf_emotion_map_t;
|
||||||
|
|
||||||
|
static const eaf_emotion_map_t s_emotion_map[] = {
|
||||||
|
// 默认/积极 → m06
|
||||||
|
{"neutral", "hiyori_m06.eaf"},
|
||||||
|
{"happy", "hiyori_m06.eaf"},
|
||||||
|
{"laughing", "hiyori_m06.eaf"},
|
||||||
|
{"funny", "hiyori_m06.eaf"},
|
||||||
|
{"loving", "hiyori_m06.eaf"},
|
||||||
|
{"relaxed", "hiyori_m06.eaf"},
|
||||||
|
{"delicious", "hiyori_m06.eaf"},
|
||||||
|
{"kissy", "hiyori_m06.eaf"},
|
||||||
|
{"confident", "hiyori_m06.eaf"},
|
||||||
|
{"silly", "hiyori_m06.eaf"},
|
||||||
|
{"blink", "hiyori_m06.eaf"},
|
||||||
|
{"curious", "hiyori_m06.eaf"},
|
||||||
|
// 思考/疲倦 → m07
|
||||||
|
{"sleepy", "hiyori_m07.eaf"},
|
||||||
|
{"thinking", "hiyori_m07.eaf"},
|
||||||
|
{"confused", "hiyori_m07.eaf"},
|
||||||
|
{"embarrassed", "hiyori_m07.eaf"},
|
||||||
|
{"dizzy", "hiyori_m07.eaf"},
|
||||||
|
// 负面/严肃 → 暂用 m07(m03 未导入)
|
||||||
|
{"sad", "hiyori_m07.eaf"},
|
||||||
|
{"crying", "hiyori_m07.eaf"},
|
||||||
|
{"angry", "hiyori_m07.eaf"},
|
||||||
|
{"surprised", "hiyori_m07.eaf"},
|
||||||
|
{"shocked", "hiyori_m07.eaf"},
|
||||||
|
};
|
||||||
|
#define EMOTION_MAP_SIZE (sizeof(s_emotion_map) / sizeof(s_emotion_map[0]))
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// LCD flush 回调 (gfx → esp_lcd_panel_draw_bitmap)
|
||||||
|
// ==========================================================
|
||||||
|
static void eaf_disp_flush_cb(gfx_disp_t *disp, int x1, int y1, int x2, int y2, const void *data) {
|
||||||
|
esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)gfx_disp_get_user_data(disp);
|
||||||
|
esp_lcd_panel_draw_bitmap(panel, x1, y1, x2, y2, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// panel IO 完成回调,通知 gfx flush 完毕
|
||||||
|
static bool eaf_flush_io_ready(esp_lcd_panel_io_handle_t panel_io,
|
||||||
|
esp_lcd_panel_io_event_data_t *edata,
|
||||||
|
void *user_ctx) {
|
||||||
|
gfx_disp_t *disp = (gfx_disp_t *)user_ctx;
|
||||||
|
if (disp) {
|
||||||
|
gfx_disp_flush_ready(disp, true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 按名字查找 cache 中的 EAF entry
|
||||||
|
// ==========================================================
|
||||||
|
static int find_cache_index_by_name(const char *name) {
|
||||||
|
if (!name) return -1;
|
||||||
|
for (int i = 0; i < s_eaf_cache_count; i++) {
|
||||||
|
if (strcmp(s_eaf_cache[i].name, name) == 0 && s_eaf_cache[i].data) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 切换表情到指定 asset (用 PSRAM 中 cache 的 EAF 数据)
|
||||||
|
// ==========================================================
|
||||||
|
static esp_err_t switch_emotion_by_asset(const char *asset_name) {
|
||||||
|
if (!s_initialized || !s_anim_obj) return ESP_ERR_INVALID_STATE;
|
||||||
|
|
||||||
|
int idx = find_cache_index_by_name(asset_name);
|
||||||
|
if (idx < 0) {
|
||||||
|
ESP_LOGW(TAG, "Asset 未在 cache: %s", asset_name);
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (idx == s_current_emotion_idx) {
|
||||||
|
return ESP_OK; // 已是当前表情
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *eaf_data = s_eaf_cache[idx].data;
|
||||||
|
size_t eaf_size = s_eaf_cache[idx].size;
|
||||||
|
|
||||||
|
esp_err_t ret = gfx_emote_lock(s_emote_handle);
|
||||||
|
if (ret != ESP_OK) return ret;
|
||||||
|
|
||||||
|
gfx_anim_stop(s_anim_obj);
|
||||||
|
|
||||||
|
gfx_anim_src_t src = {
|
||||||
|
.type = GFX_ANIM_SRC_TYPE_MEMORY,
|
||||||
|
.data = eaf_data,
|
||||||
|
.data_len = eaf_size,
|
||||||
|
};
|
||||||
|
gfx_anim_set_src_desc(s_anim_obj, &src);
|
||||||
|
|
||||||
|
// 居中显示,hiyori 209×360 居中放 360×360 屏
|
||||||
|
gfx_obj_align(s_anim_obj, GFX_ALIGN_CENTER, 0, 0);
|
||||||
|
|
||||||
|
// 全部帧 + EAF_DEFAULT_FPS + 永远循环
|
||||||
|
gfx_anim_set_segment(s_anim_obj, 0, 0xFFFFFFFF, EAF_DEFAULT_FPS, true);
|
||||||
|
gfx_anim_start(s_anim_obj);
|
||||||
|
|
||||||
|
gfx_emote_unlock(s_emote_handle);
|
||||||
|
|
||||||
|
s_current_emotion_idx = idx;
|
||||||
|
ESP_LOGI(TAG, "切换表情: %s (idx=%d, size=%d)", asset_name, idx, eaf_size);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 公开 C API(与 ai_chat_ui.c 完全相同的签名)
|
||||||
|
// ==========================================================
|
||||||
|
|
||||||
|
void ai_chat_screen_init(void) {
|
||||||
|
if (s_initialized) {
|
||||||
|
ESP_LOGW(TAG, "已初始化,跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "============================");
|
||||||
|
ESP_LOGI(TAG, "=== EAF 数字人 UI 初始化 ===");
|
||||||
|
ESP_LOGI(TAG, "============================");
|
||||||
|
|
||||||
|
// 0. 确保 SPIFFS 挂载(mmap_assets_new with use_fs=true 需要 vfs 路径可访问)
|
||||||
|
size_t spiffs_total = 0, spiffs_used = 0;
|
||||||
|
esp_err_t mount_ret = esp_spiffs_info("storage", &spiffs_total, &spiffs_used);
|
||||||
|
if (mount_ret != ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "SPIFFS 未挂载,自动挂载到 /spiflash...");
|
||||||
|
esp_vfs_spiffs_conf_t spiffs_cfg = {
|
||||||
|
.base_path = "/spiflash",
|
||||||
|
.partition_label = "storage",
|
||||||
|
.max_files = 5,
|
||||||
|
.format_if_mount_failed = false,
|
||||||
|
};
|
||||||
|
mount_ret = esp_vfs_spiffs_register(&spiffs_cfg);
|
||||||
|
if (mount_ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "SPIFFS 挂载失败: %s", esp_err_to_name(mount_ret));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_spiffs_info("storage", &spiffs_total, &spiffs_used);
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "SPIFFS 已就绪: total=%u KB, used=%u KB",
|
||||||
|
(unsigned)(spiffs_total / 1024), (unsigned)(spiffs_used / 1024));
|
||||||
|
|
||||||
|
// 1. 自己解析 hiyori-assets.bin(绕过 esp_mmap_assets v2.0.0 use_fs 模式的严重 offset bug)
|
||||||
|
//
|
||||||
|
// MMAP bin 实际 layout(hex 反推得出):
|
||||||
|
// [0x00-0x03] "MMAP" magic
|
||||||
|
// [0x04-0x07] version + checksum (2B + 2B)
|
||||||
|
// [0x08-0x0B] header_size = 16
|
||||||
|
// [0x0C-0x0F] file_count
|
||||||
|
// [0x10-0x1F] reserved (16B)
|
||||||
|
// [0x20+] file entry table,每 entry = 28B (16B name + 4B size + 4B offset + 4B pad)
|
||||||
|
// [data] table 后是数据段。每个文件: 2B 0x5A 0x5A magic prefix + size 字节数据。
|
||||||
|
// entry.offset 是相对数据段起点的偏移(指向文件的 magic prefix 起点)
|
||||||
|
ESP_LOGI(TAG, "解析 hiyori-assets.bin:");
|
||||||
|
FILE *f = fopen(EAF_ASSETS_PATH, "rb");
|
||||||
|
if (!f) {
|
||||||
|
ESP_LOGE(TAG, "打开 %s 失败", EAF_ASSETS_PATH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8_t header[16];
|
||||||
|
if (fread(header, 1, 16, f) != 16 || memcmp(header, "MMAP", 4) != 0) {
|
||||||
|
ESP_LOGE(TAG, "MMAP 头解析失败");
|
||||||
|
fclose(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t file_count = header[12] | (header[13] << 8) | (header[14] << 16) | (header[15] << 24);
|
||||||
|
ESP_LOGI(TAG, " MMAP file_count=%u", (unsigned)file_count);
|
||||||
|
// 跳过 reserved 16B 到 entry table 起点 (0x20)
|
||||||
|
fseek(f, 0x20, SEEK_SET);
|
||||||
|
const size_t ENTRY_SIZE = 28; // 16 + 4 + 4 + 4
|
||||||
|
const size_t DATA_START = 0x20 + file_count * ENTRY_SIZE;
|
||||||
|
|
||||||
|
s_eaf_cache_count = 0;
|
||||||
|
for (uint32_t i = 0; i < file_count; i++) {
|
||||||
|
uint8_t entry[28];
|
||||||
|
fseek(f, 0x20 + i * ENTRY_SIZE, SEEK_SET);
|
||||||
|
if (fread(entry, 1, ENTRY_SIZE, f) != ENTRY_SIZE) {
|
||||||
|
ESP_LOGE(TAG, " entry[%u] 读取失败", (unsigned)i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char name[17] = {0};
|
||||||
|
memcpy(name, entry, 16);
|
||||||
|
name[16] = '\0';
|
||||||
|
uint32_t fsize = entry[16] | (entry[17] << 8) | (entry[18] << 16) | (entry[19] << 24);
|
||||||
|
uint32_t foffset = entry[20] | (entry[21] << 8) | (entry[22] << 16) | (entry[23] << 24);
|
||||||
|
|
||||||
|
// 只缓存 .eaf 文件
|
||||||
|
size_t nlen = strlen(name);
|
||||||
|
if (nlen < 4 || strcmp(name + nlen - 4, ".eaf") != 0) {
|
||||||
|
ESP_LOGI(TAG, " 跳过非 EAF: %s (size=%u)", name, (unsigned)fsize);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 真实文件位置 = data_section_start + entry.offset + 2 (跳过 0x5A 0x5A magic prefix)
|
||||||
|
size_t real_offset = DATA_START + foffset + 2;
|
||||||
|
|
||||||
|
uint8_t *buf = heap_caps_malloc(fsize, MALLOC_CAP_SPIRAM);
|
||||||
|
if (!buf) {
|
||||||
|
ESP_LOGE(TAG, " PSRAM malloc 失败: %s (size=%u)", name, (unsigned)fsize);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fseek(f, real_offset, SEEK_SET) != 0 || fread(buf, 1, fsize, f) != fsize) {
|
||||||
|
ESP_LOGE(TAG, " fread 失败: %s @ offset %zu", name, real_offset);
|
||||||
|
heap_caps_free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 EAF format magic
|
||||||
|
if (buf[0] != 0x89 || buf[1] != 'E' || buf[2] != 'A' || buf[3] != 'F') {
|
||||||
|
ESP_LOGE(TAG, " EAF magic 失败: %s (got %02x %02x %02x %02x)",
|
||||||
|
name, buf[0], buf[1], buf[2], buf[3]);
|
||||||
|
heap_caps_free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_eaf_cache_count >= (int)(sizeof(s_eaf_cache)/sizeof(s_eaf_cache[0]))) {
|
||||||
|
ESP_LOGW(TAG, " cache 已满,丢弃: %s", name);
|
||||||
|
heap_caps_free(buf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
s_eaf_cache[s_eaf_cache_count].data = buf;
|
||||||
|
s_eaf_cache[s_eaf_cache_count].size = fsize;
|
||||||
|
strncpy(s_eaf_cache[s_eaf_cache_count].name, name, sizeof(s_eaf_cache[0].name) - 1);
|
||||||
|
s_eaf_cache[s_eaf_cache_count].name[sizeof(s_eaf_cache[0].name) - 1] = '\0';
|
||||||
|
ESP_LOGI(TAG, " ✓ Cached [%d] %s (%u bytes) @ %p (file_offset=%zu)",
|
||||||
|
s_eaf_cache_count, name, (unsigned)fsize, buf, real_offset);
|
||||||
|
s_eaf_cache_count++;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
if (s_eaf_cache_count == 0) {
|
||||||
|
ESP_LOGE(TAG, "没有 EAF 资源被加载,初始化中止");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "EAF 预加载完成,共 %d 个表情可用", s_eaf_cache_count);
|
||||||
|
|
||||||
|
// 2. 初始化 gfx 核心(绑 Core 0,与原 LVGL 一致避免抢音频 Core 1)
|
||||||
|
gfx_core_config_t gfx_cfg = {
|
||||||
|
.fps = 25,
|
||||||
|
.task = GFX_EMOTE_INIT_CONFIG(),
|
||||||
|
};
|
||||||
|
gfx_cfg.task.task_priority = 4;
|
||||||
|
gfx_cfg.task.task_affinity = 0; // Core 0
|
||||||
|
gfx_cfg.task.task_stack = 8 * 1024;
|
||||||
|
s_emote_handle = gfx_emote_init(&gfx_cfg);
|
||||||
|
if (!s_emote_handle) {
|
||||||
|
ESP_LOGE(TAG, "gfx_emote_init 失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 添加 display(接管 panel_handle)
|
||||||
|
gfx_disp_config_t disp_cfg = {
|
||||||
|
.h_res = LCD_W,
|
||||||
|
.v_res = LCD_H,
|
||||||
|
.flush_cb = eaf_disp_flush_cb,
|
||||||
|
.update_cb = NULL,
|
||||||
|
.user_data = (void *)panel_handle,
|
||||||
|
.flags = {
|
||||||
|
.swap = true, // RGB565 字节序(与 LVGL 配置一致)
|
||||||
|
.buff_dma = true,
|
||||||
|
.buff_spiram = false,
|
||||||
|
.double_buffer = true,
|
||||||
|
},
|
||||||
|
.buffers = { .buf1 = NULL, .buf2 = NULL, .buf_pixels = LCD_W * 20 },
|
||||||
|
};
|
||||||
|
s_disp = gfx_disp_add(s_emote_handle, &disp_cfg);
|
||||||
|
if (!s_disp) {
|
||||||
|
ESP_LOGE(TAG, "gfx_disp_add 失败");
|
||||||
|
gfx_emote_deinit(s_emote_handle);
|
||||||
|
s_emote_handle = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册 panel IO 完成回调
|
||||||
|
const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = eaf_flush_io_ready };
|
||||||
|
esp_lcd_panel_io_register_event_callbacks(lcd_io_handle, &cbs, s_disp);
|
||||||
|
|
||||||
|
// 4. 设置背景色 = BG_COLOR (0x000000 黑色,与 LVGL 版一致)
|
||||||
|
gfx_disp_set_bg_color(s_disp, GFX_COLOR_HEX(0x000000));
|
||||||
|
|
||||||
|
// 5. 创建动画对象 + 加载默认表情 m06
|
||||||
|
s_anim_obj = gfx_anim_create(s_disp);
|
||||||
|
if (!s_anim_obj) {
|
||||||
|
ESP_LOGE(TAG, "gfx_anim_create 失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_initialized = true;
|
||||||
|
|
||||||
|
// 默认表情 = neutral → m06
|
||||||
|
switch_emotion_by_asset("hiyori_m06.eaf");
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "=== EAF 数字人 UI 初始化完成 ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_chat_set_status(const char* status) {
|
||||||
|
// PoC 阶段不显示状态文字(gfx_label 需要字体资源接驳,留待后续)
|
||||||
|
if (status) {
|
||||||
|
ESP_LOGI(TAG, "状态: %s(PoC 阶段暂不显示)", status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_chat_set_emotion(const char* emotion) {
|
||||||
|
if (!emotion || !s_initialized) return;
|
||||||
|
|
||||||
|
// 查映射表
|
||||||
|
const char *asset_name = "hiyori_m06.eaf"; // 默认 fallback
|
||||||
|
for (size_t i = 0; i < EMOTION_MAP_SIZE; i++) {
|
||||||
|
if (strcmp(emotion, s_emotion_map[i].emotion) == 0) {
|
||||||
|
asset_name = s_emotion_map[i].asset_name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch_emotion_by_asset(asset_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_chat_set_chat_message(const char* role, const char* content) {
|
||||||
|
(void)role;
|
||||||
|
// PoC 阶段不显示字幕(gfx_label 需要字体资源接驳,留待后续)
|
||||||
|
if (content && content[0]) {
|
||||||
|
ESP_LOGI(TAG, "字幕: %s(PoC 阶段暂不显示)", content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_chat_resume_animation(void) {
|
||||||
|
// EAF 动画由 gfx_anim_start 持续播放,无需手动 resume
|
||||||
|
ESP_LOGD(TAG, "resume_animation(EAF 模式下自动循环,无需操作)");
|
||||||
|
}
|
||||||
@ -13,10 +13,13 @@
|
|||||||
#define TAG "DZBJ"
|
#define TAG "DZBJ"
|
||||||
|
|
||||||
// 仅硬件+LVGL 初始化(不加载 SquareLine UI,不点亮背光)
|
// 仅硬件+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) {
|
void dzbj_hw_display_init(i2c_master_bus_handle_t i2c_bus) {
|
||||||
ESP_LOGI(TAG, "开始初始化显示硬件...");
|
ESP_LOGI(TAG, "开始初始化显示硬件...");
|
||||||
|
|
||||||
// 1. LCD 硬件初始化(QSPI ST77916)
|
// 1. LCD 硬件初始化(QSPI ST77916)—— 共享
|
||||||
lcd_init();
|
lcd_init();
|
||||||
ESP_LOGI(TAG, "LCD 硬件初始化完成");
|
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)");
|
ESP_LOGI(TAG, "屏幕触摸已禁用 (DZBJ_ENABLE_TOUCH=0)");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 4. LVGL 初始化(显示)
|
#ifdef CONFIG_BAJI_BADGE_MODE
|
||||||
|
// 4. LVGL 初始化(仅吧唧模式)
|
||||||
lvgl_lcd_init();
|
lvgl_lcd_init();
|
||||||
ESP_LOGI(TAG, "LVGL 初始化完成");
|
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
|
#ifdef CONFIG_BAJI_BADGE_MODE
|
||||||
|
|||||||
@ -209,7 +209,8 @@ static const st77916_lcd_init_cmd_t lcd_init_cmds[] = {
|
|||||||
|
|
||||||
static lv_disp_t * disp_handle = NULL;
|
static lv_disp_t * disp_handle = NULL;
|
||||||
esp_lcd_panel_handle_t panel_handle = NULL; // 暴露给 sprite_demo 等模块直接 DMA 写 LCD
|
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
|
#if DZBJ_ENABLE_TOUCH
|
||||||
static esp_lcd_touch_handle_t touch_handle = NULL;
|
static esp_lcd_touch_handle_t touch_handle = NULL;
|
||||||
static esp_lcd_panel_io_handle_t tp_io_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.pclk_hz = 80 * 1000 * 1000;
|
||||||
io_config.trans_queue_depth = 64; // 默认 10 太小,sprite 分条传输需要更大队列
|
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));
|
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 = {
|
const st77916_vendor_config_t vendor_config = {
|
||||||
.init_cmds = lcd_init_cmds,
|
.init_cmds = lcd_init_cmds,
|
||||||
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st77916_lcd_init_cmd_t),
|
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st77916_lcd_init_cmd_t),
|
||||||
|
|||||||
@ -6,6 +6,10 @@
|
|||||||
#include "esp_lcd_st77916.h"
|
#include "esp_lcd_st77916.h"
|
||||||
#include <driver/i2c_master.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 lcd_init(void);
|
||||||
void lvgl_lcd_init(void);
|
void lvgl_lcd_init(void);
|
||||||
void lcd_clear_screen_black(void);
|
void lcd_clear_screen_black(void);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ dependencies:
|
|||||||
esp_jpeg: "*"
|
esp_jpeg: "*"
|
||||||
## Phase 10: 数字人模式 UI 框架(替代 LVGL,仅 CONFIG_BAJI_BADGE_MODE=n 时使用)
|
## Phase 10: 数字人模式 UI 框架(替代 LVGL,仅 CONFIG_BAJI_BADGE_MODE=n 时使用)
|
||||||
espressif2022/esp_emote_gfx: "~3.0.5"
|
espressif2022/esp_emote_gfx: "~3.0.5"
|
||||||
|
espressif/esp_mmap_assets: "*"
|
||||||
## Required IDF version
|
## Required IDF version
|
||||||
idf:
|
idf:
|
||||||
version: ">=5.3"
|
version: ">=5.3"
|
||||||
|
|||||||
@ -0,0 +1,463 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********************
|
||||||
|
* INCLUDES
|
||||||
|
*********************/
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "esp_attr.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#define GFX_LOG_MODULE GFX_LOG_MODULE_TOUCH
|
||||||
|
#include "common/gfx_log_priv.h"
|
||||||
|
|
||||||
|
#include "core/object/gfx_obj_priv.h"
|
||||||
|
#include "core/runtime/gfx_core_priv.h"
|
||||||
|
#include "core/runtime/gfx_touch_priv.h"
|
||||||
|
|
||||||
|
/* =========================================================================
|
||||||
|
* Phase 10 LOCAL PATCH: 兼容 esp_lcd_touch v1.1.2(项目使用版本,无 v2 新 API)
|
||||||
|
*
|
||||||
|
* esp_emote_gfx v3.0.5 期望 esp_lcd_touch v2.x 的 esp_lcd_touch_point_data_t
|
||||||
|
* 和 esp_lcd_touch_get_data,本项目用的是 v1.1.2 只有 esp_lcd_touch_get_coordinates。
|
||||||
|
* 加 shim 桥接旧 API → 新 API 类型,让编译通过。
|
||||||
|
*
|
||||||
|
* 注意:reconfigure 或 update esp_emote_gfx 后此补丁会丢失,需要 reapply。
|
||||||
|
* 数字人 PoC 不使用触摸,运行时这段代码不会被调用。
|
||||||
|
* ========================================================================= */
|
||||||
|
#include "esp_lcd_touch.h"
|
||||||
|
typedef struct {
|
||||||
|
uint16_t x;
|
||||||
|
uint16_t y;
|
||||||
|
uint16_t strength;
|
||||||
|
uint16_t track_id;
|
||||||
|
} esp_lcd_touch_point_data_t;
|
||||||
|
static inline esp_err_t esp_lcd_touch_get_data(esp_lcd_touch_handle_t tp,
|
||||||
|
esp_lcd_touch_point_data_t *points,
|
||||||
|
uint8_t *count, uint8_t max) {
|
||||||
|
uint16_t x[4] = {0}, y[4] = {0}, s[4] = {0};
|
||||||
|
uint8_t n = 0;
|
||||||
|
uint8_t mp = max < 4 ? max : 4;
|
||||||
|
bool ok = esp_lcd_touch_get_coordinates(tp, x, y, s, &n, mp);
|
||||||
|
if (!ok) {
|
||||||
|
*count = 0;
|
||||||
|
return ESP_OK; // 旧 API 无触摸时返回 false,shim 转为成功+0 点
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < n && i < max; i++) {
|
||||||
|
points[i].x = x[i];
|
||||||
|
points[i].y = y[i];
|
||||||
|
points[i].strength = s[i];
|
||||||
|
points[i].track_id = 0; // v1.1.2 无 track_id
|
||||||
|
}
|
||||||
|
*count = n;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
/* ====================== END Phase 10 LOCAL PATCH ====================== */
|
||||||
|
|
||||||
|
/*********************
|
||||||
|
* DEFINES
|
||||||
|
*********************/
|
||||||
|
|
||||||
|
/**********************
|
||||||
|
* STATIC VARIABLES
|
||||||
|
**********************/
|
||||||
|
|
||||||
|
static const char *TAG = "touch";
|
||||||
|
static const uint32_t DEFAULT_POLL_MS = 15;
|
||||||
|
static const uint32_t DEFAULT_IRQ_POLL_MS = 5;
|
||||||
|
|
||||||
|
/**********************
|
||||||
|
* TYPEDEFS
|
||||||
|
**********************/
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
gfx_touch_t *touch;
|
||||||
|
void *original_user_data;
|
||||||
|
volatile bool unregistering;
|
||||||
|
} gfx_touch_isr_ctx_t;
|
||||||
|
|
||||||
|
/**********************
|
||||||
|
* STATIC PROTOTYPES
|
||||||
|
**********************/
|
||||||
|
|
||||||
|
static void gfx_touch_poll_cb(void *user_data);
|
||||||
|
|
||||||
|
/**********************
|
||||||
|
* STATIC FUNCTIONS
|
||||||
|
**********************/
|
||||||
|
|
||||||
|
/** Return topmost visible object on disp that contains (x, y), or NULL (same order as render = last in list = front) */
|
||||||
|
static gfx_obj_t *gfx_touch_hit_test(gfx_disp_t *disp, uint16_t x, uint16_t y)
|
||||||
|
{
|
||||||
|
gfx_obj_t *hit = NULL;
|
||||||
|
for (gfx_obj_child_t *n = disp->child_list; n != NULL; n = n->next) {
|
||||||
|
gfx_obj_t *obj = (gfx_obj_t *)n->src;
|
||||||
|
if (!obj->state.is_visible) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (obj->align.enabled || obj->state.layout_dirty) {
|
||||||
|
gfx_obj_calc_pos_in_parent(obj);
|
||||||
|
}
|
||||||
|
int32_t ox = obj->geometry.x;
|
||||||
|
int32_t oy = obj->geometry.y;
|
||||||
|
uint32_t w = obj->geometry.width;
|
||||||
|
uint32_t h = obj->geometry.height;
|
||||||
|
if (w == 0 || h == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((int32_t)x >= ox && (int32_t)x < ox + (int32_t)w && (int32_t)y >= oy && (int32_t)y < oy + (int32_t)h) {
|
||||||
|
hit = obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t gfx_touch_now_ms(void)
|
||||||
|
{
|
||||||
|
return (uint32_t)(esp_timer_get_time() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void gfx_touch_dispatch(gfx_touch_t *touch, gfx_touch_event_type_t type, const esp_lcd_touch_point_data_t *pt)
|
||||||
|
{
|
||||||
|
void *hit_obj = NULL;
|
||||||
|
|
||||||
|
gfx_touch_event_t evt = {
|
||||||
|
.type = type,
|
||||||
|
.x = touch->last_x,
|
||||||
|
.y = touch->last_y,
|
||||||
|
.strength = touch->last_strength,
|
||||||
|
.track_id = touch->last_id,
|
||||||
|
.timestamp_ms = gfx_touch_now_ms(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pt) {
|
||||||
|
evt.x = pt->x;
|
||||||
|
evt.y = pt->y;
|
||||||
|
evt.strength = pt->strength;
|
||||||
|
evt.track_id = pt->track_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch->disp) {
|
||||||
|
if (type == GFX_TOUCH_EVENT_PRESS) {
|
||||||
|
hit_obj = gfx_touch_hit_test(touch->disp, evt.x, evt.y);
|
||||||
|
if (hit_obj != NULL) {
|
||||||
|
touch->pressed_obj = (gfx_obj_t *)hit_obj;
|
||||||
|
touch->pressed_id = evt.track_id;
|
||||||
|
} else {
|
||||||
|
touch->pressed_obj = NULL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* MOVE / RELEASE: keep delivering to the object that got PRESS (drag support) */
|
||||||
|
if (touch->pressed_obj != NULL && evt.track_id == touch->pressed_id) {
|
||||||
|
hit_obj = (gfx_obj_t *)touch->pressed_obj;
|
||||||
|
} else {
|
||||||
|
hit_obj = NULL;
|
||||||
|
}
|
||||||
|
if (type == GFX_TOUCH_EVENT_RELEASE) {
|
||||||
|
touch->pressed_obj = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hit_obj != NULL) {
|
||||||
|
gfx_obj_t *obj = (gfx_obj_t *)hit_obj;
|
||||||
|
if (obj->vfunc.touch_event) {
|
||||||
|
obj->vfunc.touch_event(obj, &evt);
|
||||||
|
}
|
||||||
|
if (obj->user_touch_cb) {
|
||||||
|
obj->user_touch_cb(obj, &evt, obj->user_touch_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch->event_cb) {
|
||||||
|
touch->event_cb((gfx_touch_t *)touch, &evt, touch->user_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void IRAM_ATTR gfx_touch_isr(esp_lcd_touch_handle_t tp)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!tp || !tp->config.user_data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx_touch_isr_ctx_t *isr_ctx = (gfx_touch_isr_ctx_t *)tp->config.user_data;
|
||||||
|
if (!isr_ctx || isr_ctx->unregistering || !isr_ctx->touch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isr_ctx->touch->irq_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t gfx_touch_enable_interrupt(gfx_touch_t *touch)
|
||||||
|
{
|
||||||
|
if (!touch || !touch->handle || touch->int_gpio_num == GPIO_NUM_NC) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx_touch_isr_ctx_t *isr_ctx = calloc(1, sizeof(gfx_touch_isr_ctx_t));
|
||||||
|
if (!isr_ctx) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
isr_ctx->touch = touch;
|
||||||
|
isr_ctx->original_user_data = touch->handle->config.user_data;
|
||||||
|
touch->isr_ctx = isr_ctx;
|
||||||
|
|
||||||
|
esp_err_t ret = esp_lcd_touch_register_interrupt_callback_with_data(touch->handle, gfx_touch_isr, isr_ctx);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
touch->isr_ctx = NULL;
|
||||||
|
free(isr_ctx);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->irq_enabled = true;
|
||||||
|
touch->irq_pending = false;
|
||||||
|
GFX_LOGI(TAG, "init touch: interrupt enabled on gpio %d", touch->int_gpio_num);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void gfx_touch_disable_interrupt(gfx_touch_t *touch)
|
||||||
|
{
|
||||||
|
if (!touch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch->irq_enabled && touch->int_gpio_num != GPIO_NUM_NC && GPIO_IS_VALID_GPIO(touch->int_gpio_num)) {
|
||||||
|
esp_err_t gpio_ret = gpio_intr_disable(touch->int_gpio_num);
|
||||||
|
if (gpio_ret != ESP_OK) {
|
||||||
|
GFX_LOGW(TAG, "delete touch: disable gpio interrupt failed on pin %d (%d)", touch->int_gpio_num, gpio_ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch->isr_ctx) {
|
||||||
|
gfx_touch_isr_ctx_t *isr_ctx = (gfx_touch_isr_ctx_t *)touch->isr_ctx;
|
||||||
|
isr_ctx->unregistering = true;
|
||||||
|
esp_lcd_touch_register_interrupt_callback(touch->handle, NULL);
|
||||||
|
if (touch->handle && touch->handle->config.user_data != isr_ctx->original_user_data) {
|
||||||
|
touch->handle->config.user_data = isr_ctx->original_user_data;
|
||||||
|
}
|
||||||
|
free(isr_ctx);
|
||||||
|
touch->isr_ctx = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->irq_enabled = false;
|
||||||
|
touch->irq_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void gfx_touch_poll_cb(void *user_data)
|
||||||
|
{
|
||||||
|
gfx_touch_t *touch = (gfx_touch_t *)user_data;
|
||||||
|
if (!touch || !touch->handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch->irq_enabled) {
|
||||||
|
if (!touch->irq_pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
touch->irq_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ret = esp_lcd_touch_read_data(touch->handle);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
GFX_LOGW(TAG, "poll touch: read failed (%d)", ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_lcd_touch_point_data_t points[1] = {0};
|
||||||
|
uint8_t count = 0;
|
||||||
|
|
||||||
|
ret = esp_lcd_touch_get_data(touch->handle, points, &count, 1);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
GFX_LOGW(TAG, "poll touch: get data failed (%d)", ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pressed_now = (count > 0);
|
||||||
|
|
||||||
|
if (pressed_now) {
|
||||||
|
uint16_t new_x = points[0].x;
|
||||||
|
uint16_t new_y = points[0].y;
|
||||||
|
|
||||||
|
if (pressed_now && !touch->pressed) {
|
||||||
|
gfx_touch_dispatch(touch, GFX_TOUCH_EVENT_PRESS, &points[0]);
|
||||||
|
} else if (touch->pressed && (new_x != touch->last_x || new_y != touch->last_y)) {
|
||||||
|
gfx_touch_dispatch(touch, GFX_TOUCH_EVENT_MOVE, &points[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->last_x = new_x;
|
||||||
|
touch->last_y = new_y;
|
||||||
|
touch->last_strength = points[0].strength;
|
||||||
|
touch->last_id = points[0].track_id;
|
||||||
|
} else {
|
||||||
|
if (touch->pressed) {
|
||||||
|
gfx_touch_dispatch(touch, GFX_TOUCH_EVENT_RELEASE, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->pressed = pressed_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************
|
||||||
|
* PUBLIC FUNCTIONS
|
||||||
|
**********************/
|
||||||
|
|
||||||
|
esp_err_t gfx_touch_start(gfx_touch_t *touch, const gfx_touch_config_t *cfg)
|
||||||
|
{
|
||||||
|
if (!touch || !touch->ctx || !cfg) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg->handle) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->handle = cfg->handle;
|
||||||
|
touch->disp = cfg->disp;
|
||||||
|
touch->event_cb = cfg->event_cb;
|
||||||
|
touch->user_data = cfg->user_data;
|
||||||
|
touch->int_gpio_num = GPIO_NUM_NC;
|
||||||
|
touch->irq_enabled = false;
|
||||||
|
touch->irq_pending = false;
|
||||||
|
touch->isr_ctx = NULL;
|
||||||
|
|
||||||
|
bool irq_requested = false;
|
||||||
|
gpio_num_t selected_gpio = GPIO_NUM_NC;
|
||||||
|
|
||||||
|
if (touch->handle->config.int_gpio_num != GPIO_NUM_NC &&
|
||||||
|
GPIO_IS_VALID_GPIO(touch->handle->config.int_gpio_num)) {
|
||||||
|
selected_gpio = touch->handle->config.int_gpio_num;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected_gpio != GPIO_NUM_NC) {
|
||||||
|
touch->int_gpio_num = selected_gpio;
|
||||||
|
irq_requested = true;
|
||||||
|
} else {
|
||||||
|
touch->int_gpio_num = GPIO_NUM_NC;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t default_poll = irq_requested ? DEFAULT_IRQ_POLL_MS : DEFAULT_POLL_MS;
|
||||||
|
touch->poll_ms = cfg->poll_ms ? cfg->poll_ms : default_poll;
|
||||||
|
touch->pressed = false;
|
||||||
|
touch->last_x = 0;
|
||||||
|
touch->last_y = 0;
|
||||||
|
touch->last_strength = 0;
|
||||||
|
touch->last_id = 0;
|
||||||
|
touch->pressed_obj = NULL;
|
||||||
|
|
||||||
|
if (irq_requested) {
|
||||||
|
esp_err_t irq_ret = gfx_touch_enable_interrupt(touch);
|
||||||
|
if (irq_ret != ESP_OK) {
|
||||||
|
GFX_LOGW(TAG, "init touch: enable gpio interrupt failed on %d (%d), using polling mode", touch->int_gpio_num, irq_ret);
|
||||||
|
touch->int_gpio_num = GPIO_NUM_NC;
|
||||||
|
touch->irq_enabled = false;
|
||||||
|
touch->irq_pending = false;
|
||||||
|
if (!cfg->poll_ms) {
|
||||||
|
touch->poll_ms = DEFAULT_POLL_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->poll_timer = gfx_timer_create(touch->ctx, gfx_touch_poll_cb, touch->poll_ms, touch);
|
||||||
|
if (!touch->poll_timer) {
|
||||||
|
GFX_LOGE(TAG, "init touch: create polling timer failed");
|
||||||
|
if (touch->irq_enabled || touch->isr_ctx) {
|
||||||
|
gfx_touch_disable_interrupt(touch);
|
||||||
|
}
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
GFX_LOGD(TAG, "init touch: polling started (%"PRIu32" ms)", touch->poll_ms);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gfx_touch_del(gfx_touch_t *touch)
|
||||||
|
{
|
||||||
|
if (!touch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx_core_context_t *ctx = (gfx_core_context_t *)touch->ctx;
|
||||||
|
if (ctx != NULL) {
|
||||||
|
if (ctx->touch == touch) {
|
||||||
|
ctx->touch = touch->next;
|
||||||
|
} else {
|
||||||
|
gfx_touch_t *prev = ctx->touch;
|
||||||
|
while (prev != NULL && prev->next != touch) {
|
||||||
|
prev = prev->next;
|
||||||
|
}
|
||||||
|
if (prev != NULL) {
|
||||||
|
prev->next = touch->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch->irq_enabled || touch->isr_ctx) {
|
||||||
|
gfx_touch_disable_interrupt(touch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touch->poll_timer && touch->ctx) {
|
||||||
|
gfx_timer_delete(touch->ctx, touch->poll_timer);
|
||||||
|
touch->poll_timer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->ctx = NULL;
|
||||||
|
touch->next = NULL;
|
||||||
|
touch->handle = NULL;
|
||||||
|
touch->event_cb = NULL;
|
||||||
|
touch->user_data = NULL;
|
||||||
|
touch->pressed = false;
|
||||||
|
touch->pressed_obj = NULL;
|
||||||
|
touch->int_gpio_num = GPIO_NUM_NC;
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx_touch_t *gfx_touch_add(gfx_handle_t handle, const gfx_touch_config_t *cfg)
|
||||||
|
{
|
||||||
|
if (!handle || !cfg || !cfg->handle) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx_core_context_t *ctx = (gfx_core_context_t *)handle;
|
||||||
|
|
||||||
|
gfx_touch_t *new_touch = (gfx_touch_t *)calloc(1, sizeof(gfx_touch_t));
|
||||||
|
if (!new_touch) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
memset(new_touch, 0, sizeof(gfx_touch_t));
|
||||||
|
new_touch->ctx = ctx;
|
||||||
|
|
||||||
|
if (gfx_touch_start(new_touch, cfg) != ESP_OK) {
|
||||||
|
free(new_touch);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->touch == NULL) {
|
||||||
|
ctx->touch = new_touch;
|
||||||
|
} else {
|
||||||
|
gfx_touch_t *tail = ctx->touch;
|
||||||
|
while (tail->next != NULL) {
|
||||||
|
tail = tail->next;
|
||||||
|
}
|
||||||
|
tail->next = new_touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new_touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gfx_touch_set_disp(gfx_touch_t *touch, gfx_disp_t *disp)
|
||||||
|
{
|
||||||
|
if (!touch || !disp) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch->disp = disp;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user