From cd23e0f15581b4e5f40a97282fb04099c70dd0c1 Mon Sep 17 00:00:00 2001 From: espressif2022 <111102666+espressif2022@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:07:13 +0800 Subject: [PATCH] feat: add emote_gfx UI for EchoEar (#1022) * feat: add emote_gfx UI for EchoEar * feat: delete local assets --- .gitignore | 3 +- main/CMakeLists.txt | 21 ++ main/boards/echoear/EchoEar.cc | 361 +++++++++++++---------- main/boards/echoear/README.md | 35 +++ main/boards/echoear/emote_display.cc | 419 +++++++++++++++++++++++++++ main/boards/echoear/emote_display.h | 66 +++++ main/idf_component.yml | 1 + partitions/v1/16m_echoear.csv | 9 + 8 files changed, 763 insertions(+), 152 deletions(-) create mode 100644 main/boards/echoear/emote_display.cc create mode 100644 main/boards/echoear/emote_display.h create mode 100644 partitions/v1/16m_echoear.csv diff --git a/.gitignore b/.gitignore index 5cdd7a33..752db531 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ main/mmap_generate_emoji.h .cache main/mmap_generate_emoji.h *.pyc -*.bin \ No newline at end of file +*.bin +mmap_generate_*.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 448eb0df..81aed176 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -335,4 +335,25 @@ spiffs_create_partition_assets( FLASH_IN_PROJECT MMAP_FILE_SUPPORT_FORMAT ".aaf" ) +endif() + +if(CONFIG_BOARD_TYPE_ECHOEAR) + +idf_build_get_property(build_components BUILD_COMPONENTS) +foreach(COMPONENT ${build_components}) + if(COMPONENT MATCHES "esp_emote_gfx" OR COMPONENT MATCHES "espressif2022__esp_emote_gfx") + set(EMOTE_GFX_COMPONENT ${COMPONENT}) + idf_component_get_property(EMOTE_GFX_COMPONENT_PATH ${EMOTE_GFX_COMPONENT} COMPONENT_DIR) + set(SPIFFS_DIR "${EMOTE_GFX_COMPONENT_PATH}/emoji_normal") + break() + endif() +endforeach() + +spiffs_create_partition_assets( + assets_A + ${SPIFFS_DIR} + FLASH_IN_PROJECT + MMAP_FILE_SUPPORT_FORMAT ".aaf, ttf, bin" + IMPORT_INC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE} +) endif() \ No newline at end of file diff --git a/main/boards/echoear/EchoEar.cc b/main/boards/echoear/EchoEar.cc index 784ef0e8..6c417a08 100644 --- a/main/boards/echoear/EchoEar.cc +++ b/main/boards/echoear/EchoEar.cc @@ -5,6 +5,7 @@ #include "button.h" #include "config.h" #include "backlight.h" +#include "emote_display.h" #include #include @@ -25,6 +26,8 @@ #define TAG "EchoEar" +#define USE_LVGL_DEFAULT 0 + LV_FONT_DECLARE(font_puhui_20_4); LV_FONT_DECLARE(font_awesome_20_4); temperature_sensor_handle_t temp_sensor = NULL; @@ -219,26 +222,34 @@ gpio_num_t AUDIO_I2S_GPIO_DIN = AUDIO_I2S_GPIO_DIN_1; gpio_num_t AUDIO_CODEC_PA_PIN = AUDIO_CODEC_PA_PIN_1; gpio_num_t QSPI_PIN_NUM_LCD_RST = QSPI_PIN_NUM_LCD_RST_1; gpio_num_t TOUCH_PAD2 = TOUCH_PAD2_1; -gpio_num_t UART1_TX = UART1_TX_1; -gpio_num_t UART1_RX = UART1_RX_1; +gpio_num_t UART1_TX = UART1_TX_1; +gpio_num_t UART1_RX = UART1_RX_1; class Charge : public I2cDevice { public: - Charge(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + Charge(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) + { read_buffer_ = new uint8_t[8]; } - ~Charge() { + ~Charge() + { delete[] read_buffer_; } - void Printcharge() { - ReadRegs(0x08, read_buffer_, 2); + void Printcharge() + { + ReadRegs(0x08, read_buffer_, 2); ReadRegs(0x0c, read_buffer_ + 2, 2); ESP_ERROR_CHECK(temperature_sensor_get_celsius(temp_sensor, &tsens_value)); - int16_t voltage = (uint16_t)(read_buffer_[1] << 8 | read_buffer_[0]); - int16_t current = (int16_t)(read_buffer_[3] << 8 | read_buffer_[2]); - } - static void TaskFunction(void *pvParameters) { + int16_t voltage = static_cast(read_buffer_[1] << 8 | read_buffer_[0]); + int16_t current = static_cast(read_buffer_[3] << 8 | read_buffer_[2]); + + // Use the variables to avoid warnings (can be removed if actual implementation uses them) + (void)voltage; + (void)current; + } + static void TaskFunction(void *pvParameters) + { Charge* charge = static_cast(pvParameters); while (true) { charge->Printcharge(); @@ -250,7 +261,6 @@ private: uint8_t* read_buffer_ = nullptr; }; - class Cst816s : public I2cDevice { public: struct TouchPoint_t { @@ -258,32 +268,120 @@ public: int x = -1; int y = -1; }; - Cst816s(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + + enum TouchEvent { + TOUCH_NONE, + TOUCH_PRESS, + TOUCH_RELEASE, + TOUCH_HOLD + }; + + Cst816s(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) + { read_buffer_ = new uint8_t[6]; + was_touched_ = false; + press_count_ = 0; + + // Create touch interrupt semaphore + touch_isr_mux_ = xSemaphoreCreateBinary(); + if (touch_isr_mux_ == NULL) { + ESP_LOGE("EchoEar", "Failed to create touch semaphore"); + } } - ~Cst816s() { + ~Cst816s() + { delete[] read_buffer_; + + // Delete semaphore if it exists + if (touch_isr_mux_ != NULL) { + vSemaphoreDelete(touch_isr_mux_); + touch_isr_mux_ = NULL; + } } - void UpdateTouchPoint() { + void UpdateTouchPoint() + { ReadRegs(0x02, read_buffer_, 6); tp_.num = read_buffer_[0] & 0x0F; tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2]; tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4]; } - const TouchPoint_t& GetTouchPoint() { + const TouchPoint_t &GetTouchPoint() + { return tp_; } + TouchEvent CheckTouchEvent() + { + bool is_touched = (tp_.num > 0); + TouchEvent event = TOUCH_NONE; + + if (is_touched && !was_touched_) { + // Press event (transition from not touched to touched) + press_count_++; + event = TOUCH_PRESS; + ESP_LOGI("EchoEar", "TOUCH PRESS - count: %d, x: %d, y: %d", press_count_, tp_.x, tp_.y); + } else if (!is_touched && was_touched_) { + // Release event (transition from touched to not touched) + event = TOUCH_RELEASE; + ESP_LOGI("EchoEar", "TOUCH RELEASE - total presses: %d", press_count_); + } else if (is_touched && was_touched_) { + // Continuous touch (hold) + event = TOUCH_HOLD; + ESP_LOGD("EchoEar", "TOUCH HOLD - x: %d, y: %d", tp_.x, tp_.y); + } + + // Update previous state + was_touched_ = is_touched; + return event; + } + + int GetPressCount() const + { + return press_count_; + } + + void ResetPressCount() + { + press_count_ = 0; + } + + // Semaphore management methods + SemaphoreHandle_t GetTouchSemaphore() + { + return touch_isr_mux_; + } + + bool WaitForTouchEvent(TickType_t timeout = portMAX_DELAY) + { + if (touch_isr_mux_ != NULL) { + return xSemaphoreTake(touch_isr_mux_, timeout) == pdTRUE; + } + return false; + } + + void NotifyTouchEvent() + { + if (touch_isr_mux_ != NULL) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(touch_isr_mux_, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); + } + } + private: uint8_t* read_buffer_ = nullptr; TouchPoint_t tp_; + + // Touch state tracking + bool was_touched_; + int press_count_; + + // Touch interrupt semaphore + SemaphoreHandle_t touch_isr_mux_; }; -static SemaphoreHandle_t touch_isr_mux = NULL; -static bool touch_event_pending = false; -static int64_t touch_event_time = 0; class EspS3Cat : public WifiBoard { private: @@ -291,13 +389,17 @@ private: Cst816s* cst816s_; Charge* charge_; Button boot_button_; +#if USE_LVGL_DEFAULT LcdDisplay* display_; +#else + anim::EmoteDisplay* display_ = nullptr; +#endif PwmBacklight* backlight_ = nullptr; esp_timer_handle_t touchpad_timer_; esp_lcd_touch_handle_t tp; // LCD touch handle - - void InitializeI2c() { + void InitializeI2c() + { i2c_master_bus_config_t i2c_bus_cfg = { .i2c_port = I2C_NUM_0, .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, @@ -315,14 +417,15 @@ private: temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(10, 50); ESP_ERROR_CHECK(temperature_sensor_install(&temp_sensor_config, &temp_sensor)); ESP_ERROR_CHECK(temperature_sensor_enable(temp_sensor)); - + } - uint8_t DetectPcbVersion() { - esp_err_t ret = i2c_master_probe(i2c_bus_, 0x18, 100); + uint8_t DetectPcbVersion() + { + esp_err_t ret = i2c_master_probe(i2c_bus_, 0x18, 100); uint8_t pcb_verison = 0; if (ret == ESP_OK) { ESP_LOGI(TAG, "PCB verison V1.0"); - pcb_verison = 0; + pcb_verison = 0; } else { gpio_config_t gpio_conf = { .pin_bit_mask = (1ULL << GPIO_NUM_48), @@ -334,10 +437,10 @@ private: ESP_ERROR_CHECK(gpio_config(&gpio_conf)); ESP_ERROR_CHECK(gpio_set_level(GPIO_NUM_48, 1)); vTaskDelay(pdMS_TO_TICKS(100)); - ret = i2c_master_probe(i2c_bus_, 0x18, 100); + ret = i2c_master_probe(i2c_bus_, 0x18, 100); if (ret == ESP_OK) { ESP_LOGI(TAG, "PCB verison V1.2"); - pcb_verison = 1; + pcb_verison = 1; AUDIO_I2S_GPIO_DIN = AUDIO_I2S_GPIO_DIN_2; AUDIO_CODEC_PA_PIN = AUDIO_CODEC_PA_PIN_2; QSPI_PIN_NUM_LCD_RST = QSPI_PIN_NUM_LCD_RST_2; @@ -346,141 +449,87 @@ private: UART1_RX = UART1_RX_2; } else { ESP_LOGE(TAG, "PCB version detection error"); - + } - } + } return pcb_verison; } - - static void touchpad_timer_callback(void* arg) { - auto& board = (EspS3Cat&)Board::GetInstance(); - auto touchpad = board.GetTouchpad(); - static bool was_touched = false; - static int64_t touch_start_time = 0; - const int64_t TOUCH_THRESHOLD_MS = 500; - - touchpad->UpdateTouchPoint(); - auto touch_point = touchpad->GetTouchPoint(); - - if (touch_point.num > 0 && !was_touched) { - was_touched = true; - touch_start_time = esp_timer_get_time() / 1000; - } - else if (touch_point.num == 0 && was_touched) { - was_touched = false; - int64_t touch_duration = (esp_timer_get_time() / 1000) - touch_start_time; - - if (touch_duration < TOUCH_THRESHOLD_MS) { - auto& app = Application::GetInstance(); - if (app.GetDeviceState() == kDeviceStateStarting && - !WifiStation::GetInstance().IsConnected()) { - board.ResetWifiConfiguration(); - } - app.ToggleChatState(); - } - } - } - static void touchpad_callback(Cst816s::TouchPoint_t touch_point) { - auto& board = (EspS3Cat&)Board::GetInstance(); - static bool was_touched = false; - static int64_t touch_start_time = 0; - const int64_t TOUCH_THRESHOLD_MS = 500; - - if (touch_point.num > 0 && !was_touched) { - was_touched = true; - touch_start_time = esp_timer_get_time() / 1000; - } - else if (touch_point.num == 0 && was_touched) { - was_touched = false; - int64_t touch_duration = (esp_timer_get_time() / 1000) - touch_start_time; - - if (touch_duration < TOUCH_THRESHOLD_MS) { - auto& app = Application::GetInstance(); - if (app.GetDeviceState() == kDeviceStateStarting && - !WifiStation::GetInstance().IsConnected()) { - board.ResetWifiConfiguration(); - } - app.ToggleChatState(); - } - } - } - - static void lvgl_port_touch_isr_cb(void* arg) + static void touch_isr_callback(void* arg) { - int64_t current_time = esp_timer_get_time() / 1000; - static int64_t last_touch_time = 0; - - if (current_time - last_touch_time >= 300) { - touch_event_pending = true; - touch_event_time = current_time; - last_touch_time = current_time; - - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - if (touch_isr_mux != NULL) { - xSemaphoreGiveFromISR(touch_isr_mux, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } + Cst816s* touchpad = static_cast(arg); + if (touchpad != nullptr) { + touchpad->NotifyTouchEvent(); } } - static void touch_event_task(void* arg) { + static void touch_event_task(void* arg) + { + Cst816s* touchpad = static_cast(arg); + if (touchpad == nullptr) { + ESP_LOGE(TAG, "Invalid touchpad pointer in touch_event_task"); + vTaskDelete(NULL); + return; + } + while (true) { - if (xSemaphoreTake(touch_isr_mux, portMAX_DELAY) == pdTRUE) { - if (touch_event_pending) { - touch_event_pending = false; + if (touchpad->WaitForTouchEvent()) { + auto &app = Application::GetInstance(); + auto &board = (EspS3Cat &)Board::GetInstance(); - auto& board = (EspS3Cat&)Board::GetInstance(); - auto& app = Application::GetInstance(); + ESP_LOGI(TAG, "Touch event, TP_PIN_NUM_INT: %d", gpio_get_level(TP_PIN_NUM_INT)); + touchpad->UpdateTouchPoint(); + auto touch_event = touchpad->CheckTouchEvent(); - if (app.GetDeviceState() == kDeviceStateStarting && - !WifiStation::GetInstance().IsConnected()) { + if (touch_event == Cst816s::TOUCH_RELEASE) { + if (app.GetDeviceState() == kDeviceStateStarting && + !WifiStation::GetInstance().IsConnected()) { board.ResetWifiConfiguration(); + } else { + app.ToggleChatState(); } - app.ToggleChatState(); } } } } - void InitializeCharge() { + void InitializeCharge() + { charge_ = new Charge(i2c_bus_, 0x55); xTaskCreatePinnedToCore(Charge::TaskFunction, "batterydecTask", 3 * 1024, charge_, 6, NULL, 0); } - void InitializeCst816sTouchPad() { + void InitializeCst816sTouchPad() + { cst816s_ = new Cst816s(i2c_bus_, 0x15); - touch_isr_mux = xSemaphoreCreateBinary(); - if (touch_isr_mux == NULL) { - ESP_LOGE(TAG, "Failed to create touch semaphore"); - return; - } + xTaskCreatePinnedToCore(touch_event_task, "touch_task", 4 * 1024, cst816s_, 5, NULL, 1); - xTaskCreatePinnedToCore(touch_event_task, "touch_task", 4 * 1024, NULL, 5, NULL, 1); - const gpio_config_t int_gpio_config = { .pin_bit_mask = (1ULL << TP_PIN_NUM_INT), .mode = GPIO_MODE_INPUT, - .intr_type = GPIO_INTR_NEGEDGE + // .intr_type = GPIO_INTR_NEGEDGE + .intr_type = GPIO_INTR_ANYEDGE }; gpio_config(&int_gpio_config); gpio_install_isr_service(0); gpio_intr_enable(TP_PIN_NUM_INT); - gpio_isr_handler_add(TP_PIN_NUM_INT, EspS3Cat::lvgl_port_touch_isr_cb, NULL); + gpio_isr_handler_add(TP_PIN_NUM_INT, EspS3Cat::touch_isr_callback, cst816s_); } - void InitializeSpi() { + void InitializeSpi() + { const spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK, - QSPI_PIN_NUM_LCD_DATA0, - QSPI_PIN_NUM_LCD_DATA1, - QSPI_PIN_NUM_LCD_DATA2, - QSPI_PIN_NUM_LCD_DATA3, - QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); + QSPI_PIN_NUM_LCD_DATA0, + QSPI_PIN_NUM_LCD_DATA1, + QSPI_PIN_NUM_LCD_DATA2, + QSPI_PIN_NUM_LCD_DATA3, + QSPI_LCD_H_RES * 80 * sizeof(uint16_t)); ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO)); } - void Initializest77916Display(uint8_t pcb_verison) { + void Initializest77916Display(uint8_t pcb_verison) + { esp_lcd_panel_io_handle_t panel_io = nullptr; esp_lcd_panel_handle_t panel = nullptr; @@ -511,73 +560,83 @@ private: esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); +#if USE_LVGL_DEFAULT display_ = new SpiLcdDisplay(panel_io, panel, - DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, - { - .text_font = &font_puhui_20_4, - .icon_font = &font_awesome_20_4, - .emoji_font = font_emoji_64_init(), - }); + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY, { + .text_font = &font_puhui_20_4, + .icon_font = &font_awesome_20_4, + .emoji_font = font_emoji_64_init(), + }); +#else + display_ = new anim::EmoteDisplay(panel, panel_io); +#endif backlight_ = new PwmBacklight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); backlight_->RestoreBrightness(); } - void InitializeButtons() { + void InitializeButtons() + { boot_button_.OnClick([this]() { - auto& app = Application::GetInstance(); + auto &app = Application::GetInstance(); if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ESP_LOGI(TAG, "Boot button pressed, enter WiFi configuration mode"); ResetWifiConfiguration(); } app.ToggleChatState(); }); - gpio_config_t power_gpio_config = { - .pin_bit_mask = (BIT64(POWER_CTRL) ), + gpio_config_t power_gpio_config = { + .pin_bit_mask = (BIT64(POWER_CTRL)), .mode = GPIO_MODE_OUTPUT, - - }; + + }; ESP_ERROR_CHECK(gpio_config(&power_gpio_config)); gpio_set_level(POWER_CTRL, 0); } public: - EspS3Cat() : boot_button_(BOOT_BUTTON_GPIO) { + EspS3Cat() : boot_button_(BOOT_BUTTON_GPIO) + { InitializeI2c(); uint8_t pcb_verison = DetectPcbVersion(); InitializeCharge(); InitializeCst816sTouchPad(); - + InitializeSpi(); Initializest77916Display(pcb_verison); InitializeButtons(); } - virtual AudioCodec* GetAudioCodec() override { + virtual AudioCodec* GetAudioCodec() override + { static BoxAudioCodec audio_codec( - i2c_bus_, - AUDIO_INPUT_SAMPLE_RATE, + i2c_bus_, + AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, - AUDIO_I2S_GPIO_MCLK, - AUDIO_I2S_GPIO_BCLK, - AUDIO_I2S_GPIO_WS, - AUDIO_I2S_GPIO_DOUT, + AUDIO_I2S_GPIO_MCLK, + AUDIO_I2S_GPIO_BCLK, + AUDIO_I2S_GPIO_WS, + AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN, - AUDIO_CODEC_PA_PIN, - AUDIO_CODEC_ES8311_ADDR, - AUDIO_CODEC_ES7210_ADDR, + AUDIO_CODEC_PA_PIN, + AUDIO_CODEC_ES8311_ADDR, + AUDIO_CODEC_ES7210_ADDR, AUDIO_INPUT_REFERENCE); return &audio_codec; } - - virtual Display* GetDisplay() override { + + virtual Display* GetDisplay() override + { return display_; } - Cst816s* GetTouchpad() { + Cst816s* GetTouchpad() + { return cst816s_; } - virtual Backlight* GetBacklight() override { + virtual Backlight* GetBacklight() override + { return backlight_; } }; diff --git a/main/boards/echoear/README.md b/main/boards/echoear/README.md index 4cf3b06f..bf73f1d0 100644 --- a/main/boards/echoear/README.md +++ b/main/boards/echoear/README.md @@ -24,8 +24,43 @@ idf.py menuconfig 分别配置如下选项: +### 基本配置 - `Xiaozhi Assistant` → `Board Type` → 选择 `EchoEar` +### 分区表配置 +- `Partition Table` → `Partition Table` → 选择 `Custom partition table CSV` +- `Partition Table` → `Custom partition CSV file` → 输入 `partitions/v1/16m_echoear.csv` + +### UI风格选择 + +EchoEar 支持两种不同的UI显示风格,通过修改代码中的宏定义来选择: + +#### 自定义表情显示系统 (推荐) +```c +#define USE_LVGL_DEFAULT 0 +``` +- **特点**: 使用自定义的 `EmoteDisplay` 表情显示系统 +- **功能**: 支持丰富的表情动画、眼睛动画、状态图标显示 +- **适用**: 智能助手场景,提供更生动的人机交互体验 +- **类**: `anim::EmoteDisplay` + `anim::EmoteEngine` + +#### LVGL默认显示系统 +```c +#define USE_LVGL_DEFAULT 1 +``` +- **特点**: 使用标准LVGL图形库的显示系统 +- **功能**: 传统的文本和图标显示界面 +- **适用**: 需要标准GUI控件的应用场景 +- **类**: `SpiLcdDisplay` + +#### 如何修改 +1. 打开 `main/boards/echoear/EchoEar.cc` 文件 +2. 找到第29行的宏定义:`#define USE_LVGL_DEFAULT 0` +3. 修改为想要的值(0或1) +4. 重新编译项目 + +> **说明**: EchoEar 使用16MB Flash,需要使用专门的分区表配置来合理分配存储空间给应用程序、OTA更新、资源文件等。 + 按 `S` 保存,按 `Q` 退出。 **编译** diff --git a/main/boards/echoear/emote_display.cc b/main/boards/echoear/emote_display.cc new file mode 100644 index 00000000..f0bbb320 --- /dev/null +++ b/main/boards/echoear/emote_display.cc @@ -0,0 +1,419 @@ +#include "emote_display.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "display/lcd_display.h" +#include "mmap_generate_emoji_normal.h" +#include "config.h" +#include "gfx.h" + +namespace anim { + +static const char* TAG = "emoji"; + +// UI element management +static gfx_obj_t* obj_label_tips = nullptr; +static gfx_obj_t* obj_label_time = nullptr; +static gfx_obj_t* obj_anim_eye = nullptr; +static gfx_obj_t* obj_anim_mic = nullptr; +static gfx_obj_t* obj_img_icon = nullptr; +static gfx_image_dsc_t icon_img_dsc; + +// Track current icon to determine when to show time +static int current_icon_type = MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN; + +enum class UIDisplayMode : uint8_t { + SHOW_ANIM_TOP = 1, // Show obj_anim_mic + SHOW_TIME = 2, // Show obj_label_time + SHOW_TIPS = 3 // Show obj_label_tips +}; + +static void SetUIDisplayMode(UIDisplayMode mode) +{ + gfx_obj_set_visible(obj_anim_mic, false); + gfx_obj_set_visible(obj_label_time, false); + gfx_obj_set_visible(obj_label_tips, false); + + // Show the selected control + switch (mode) { + case UIDisplayMode::SHOW_ANIM_TOP: + gfx_obj_set_visible(obj_anim_mic, true); + break; + case UIDisplayMode::SHOW_TIME: + gfx_obj_set_visible(obj_label_time, true); + break; + case UIDisplayMode::SHOW_TIPS: + gfx_obj_set_visible(obj_label_tips, true); + break; + } +} + +static void clock_tm_callback(void* user_data) +{ + // Only display time when battery icon is shown + if (current_icon_type == MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN) { + time_t now; + struct tm timeinfo; + time(&now); + + setenv("TZ", "GMT+0", 1); + tzset(); + localtime_r(&now, &timeinfo); + + char time_str[6]; + snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min); + + gfx_label_set_text(obj_label_time, time_str); + SetUIDisplayMode(UIDisplayMode::SHOW_TIME); + } +} + +static void InitializeAssets(mmap_assets_handle_t* assets_handle) +{ + const mmap_assets_config_t assets_cfg = { + .partition_label = "assets_A", + .max_files = MMAP_EMOJI_NORMAL_FILES, + .checksum = MMAP_EMOJI_NORMAL_CHECKSUM, + .flags = {.mmap_enable = true, .full_check = true} + }; + + mmap_assets_new(&assets_cfg, assets_handle); +} + +static void InitializeGraphics(esp_lcd_panel_handle_t panel, gfx_handle_t* engine_handle) +{ + gfx_core_config_t gfx_cfg = { + .flush_cb = EmoteEngine::OnFlush, + .user_data = panel, + .flags = { + .swap = true, + .double_buffer = true, + .buff_dma = true, + }, + .h_res = DISPLAY_WIDTH, + .v_res = DISPLAY_HEIGHT, + .fps = 30, + .buffers = { + .buf1 = nullptr, + .buf2 = nullptr, + .buf_pixels = DISPLAY_WIDTH * 16, + }, + .task = GFX_EMOTE_INIT_CONFIG() + }; + + gfx_cfg.task.task_stack_caps = MALLOC_CAP_DEFAULT; + gfx_cfg.task.task_affinity = 0; + gfx_cfg.task.task_priority = 5; + gfx_cfg.task.task_stack = 20 * 1024; + + *engine_handle = gfx_emote_init(&gfx_cfg); +} + +static void InitializeEyeAnimation(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle) +{ + obj_anim_eye = gfx_anim_create(engine_handle); + + const void* anim_data = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_IDLE_ONE_AAF); + size_t anim_size = mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_IDLE_ONE_AAF); + + gfx_anim_set_src(obj_anim_eye, anim_data, anim_size); + + gfx_obj_align(obj_anim_eye, GFX_ALIGN_LEFT_MID, 10, -20); + gfx_anim_set_mirror(obj_anim_eye, true, (DISPLAY_WIDTH - (173 + 10) * 2)); + gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, 20, false); + gfx_anim_start(obj_anim_eye); +} + +static void InitializeFont(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle) +{ + gfx_font_t font; + gfx_label_cfg_t font_cfg = { + .name = "DejaVuSans.ttf", + .mem = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_KAITI_TTF), + .mem_size = static_cast(mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_KAITI_TTF)), + }; + gfx_label_new_font(engine_handle, &font_cfg, &font); + + ESP_LOGI(TAG, "stack: %d", uxTaskGetStackHighWaterMark(nullptr)); +} + +static void InitializeLabels(gfx_handle_t engine_handle) +{ + // Initialize tips label + obj_label_tips = gfx_label_create(engine_handle); + gfx_obj_align(obj_label_tips, GFX_ALIGN_TOP_MID, 0, 45); + gfx_obj_set_size(obj_label_tips, 160, 40); + gfx_label_set_text(obj_label_tips, "启动中..."); + gfx_label_set_font_size(obj_label_tips, 20); + gfx_label_set_color(obj_label_tips, GFX_COLOR_HEX(0xFFFFFF)); + gfx_label_set_text_align(obj_label_tips, GFX_TEXT_ALIGN_LEFT); + gfx_label_set_long_mode(obj_label_tips, GFX_LABEL_LONG_SCROLL); + gfx_label_set_scroll_speed(obj_label_tips, 20); + gfx_label_set_scroll_loop(obj_label_tips, true); + + // Initialize time label + obj_label_time = gfx_label_create(engine_handle); + gfx_obj_align(obj_label_time, GFX_ALIGN_TOP_MID, 0, 30); + gfx_obj_set_size(obj_label_time, 160, 50); + gfx_label_set_text(obj_label_time, "--:--"); + gfx_label_set_font_size(obj_label_time, 40); + gfx_label_set_color(obj_label_time, GFX_COLOR_HEX(0xFFFFFF)); + gfx_label_set_text_align(obj_label_time, GFX_TEXT_ALIGN_CENTER); +} + +static void InitializeMicAnimation(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle) +{ + obj_anim_mic = gfx_anim_create(engine_handle); + gfx_obj_align(obj_anim_mic, GFX_ALIGN_TOP_MID, 0, 25); + + const void* anim_data = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_LISTEN_AAF); + size_t anim_size = mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_LISTEN_AAF); + gfx_anim_set_src(obj_anim_mic, anim_data, anim_size); + gfx_anim_start(obj_anim_mic); + gfx_obj_set_visible(obj_anim_mic, false); +} + +static void InitializeIcon(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle) +{ + obj_img_icon = gfx_img_create(engine_handle); + gfx_obj_align(obj_img_icon, GFX_ALIGN_TOP_MID, -100, 38); + + SetupImageDescriptor(assets_handle, &icon_img_dsc, MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN); + gfx_img_set_src(obj_img_icon, static_cast(&icon_img_dsc)); +} + +static void RegisterCallbacks(esp_lcd_panel_io_handle_t panel_io, gfx_handle_t engine_handle) +{ + const esp_lcd_panel_io_callbacks_t cbs = { + .on_color_trans_done = EmoteEngine::OnFlushIoReady, + }; + esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, engine_handle); +} + +void SetupImageDescriptor(mmap_assets_handle_t assets_handle, + gfx_image_dsc_t* img_dsc, + int asset_id) +{ + const void* img_data = mmap_assets_get_mem(assets_handle, asset_id); + size_t img_size = mmap_assets_get_size(assets_handle, asset_id); + + std::memcpy(&img_dsc->header, img_data, sizeof(gfx_image_header_t)); + img_dsc->data = static_cast(img_data) + sizeof(gfx_image_header_t); + img_dsc->data_size = img_size - sizeof(gfx_image_header_t); +} + +EmoteEngine::EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io) +{ + ESP_LOGI(TAG, "Create EmoteEngine, panel: %p, panel_io: %p", panel, panel_io); + + InitializeAssets(&assets_handle_); + InitializeGraphics(panel, &engine_handle_); + + gfx_emote_lock(engine_handle_); + gfx_emote_set_bg_color(engine_handle_, GFX_COLOR_HEX(0x000000)); + + // Initialize all UI components + InitializeEyeAnimation(engine_handle_, assets_handle_); + InitializeFont(engine_handle_, assets_handle_); + InitializeLabels(engine_handle_); + InitializeMicAnimation(engine_handle_, assets_handle_); + InitializeIcon(engine_handle_, assets_handle_); + + current_icon_type = MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN; + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); + + gfx_timer_create(engine_handle_, clock_tm_callback, 1000, obj_label_tips); + + gfx_emote_unlock(engine_handle_); + + RegisterCallbacks(panel_io, engine_handle_); +} + +EmoteEngine::~EmoteEngine() +{ + if (engine_handle_) { + gfx_emote_deinit(engine_handle_); + engine_handle_ = nullptr; + } + + if (assets_handle_) { + mmap_assets_del(assets_handle_); + assets_handle_ = nullptr; + } +} + +void EmoteEngine::setEyes(int aaf, bool repeat, int fps) +{ + if (!engine_handle_) { + return; + } + + const void* src_data = mmap_assets_get_mem(assets_handle_, aaf); + size_t src_len = mmap_assets_get_size(assets_handle_, aaf); + + Lock(); + gfx_anim_set_src(obj_anim_eye, src_data, src_len); + gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, fps, repeat); + gfx_anim_start(obj_anim_eye); + Unlock(); +} + +void EmoteEngine::stopEyes() +{ + // Implementation if needed +} + +void EmoteEngine::Lock() +{ + if (engine_handle_) { + gfx_emote_lock(engine_handle_); + } +} + +void EmoteEngine::Unlock() +{ + if (engine_handle_) { + gfx_emote_unlock(engine_handle_); + } +} + +void EmoteEngine::SetIcon(int asset_id) +{ + if (!engine_handle_) { + return; + } + + Lock(); + SetupImageDescriptor(assets_handle_, &icon_img_dsc, asset_id); + gfx_img_set_src(obj_img_icon, static_cast(&icon_img_dsc)); + current_icon_type = asset_id; + Unlock(); +} + +bool EmoteEngine::OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, + esp_lcd_panel_io_event_data_t* edata, + void* user_ctx) +{ + return true; +} + +void EmoteEngine::OnFlush(gfx_handle_t handle, int x_start, int y_start, + int x_end, int y_end, const void* color_data) +{ + auto* panel = static_cast(gfx_emote_get_user_data(handle)); + if (panel) { + esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data); + } + gfx_emote_flush_ready(handle, true); +} + +// EmoteDisplay implementation +EmoteDisplay::EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io) +{ + InitializeEngine(panel, panel_io); +} + +EmoteDisplay::~EmoteDisplay() = default; + +void EmoteDisplay::SetEmotion(const char* emotion) +{ + if (!engine_) { + return; + } + + using EmotionParam = std::tuple; + static const std::unordered_map emotion_map = { + {"happy", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"laughing", {MMAP_EMOJI_NORMAL_ENJOY_ONE_AAF, true, 20}}, + {"funny", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"loving", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"embarrassed", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"confident", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"delicious", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"sad", {MMAP_EMOJI_NORMAL_SAD_ONE_AAF, true, 20}}, + {"crying", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"sleepy", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"silly", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"angry", {MMAP_EMOJI_NORMAL_ANGRY_ONE_AAF, true, 20}}, + {"surprised", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"shocked", {MMAP_EMOJI_NORMAL_SHOCKED_ONE_AAF, true, 20}}, + {"thinking", {MMAP_EMOJI_NORMAL_THINKING_ONE_AAF, true, 20}}, + {"winking", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"relaxed", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}}, + {"confused", {MMAP_EMOJI_NORMAL_DIZZY_ONE_AAF, true, 20}}, + {"neutral", {MMAP_EMOJI_NORMAL_IDLE_ONE_AAF, false, 20}}, + {"idle", {MMAP_EMOJI_NORMAL_IDLE_ONE_AAF, false, 20}}, + }; + + auto it = emotion_map.find(emotion); + if (it != emotion_map.end()) { + int aaf = std::get<0>(it->second); + bool repeat = std::get<1>(it->second); + int fps = std::get<2>(it->second); + engine_->setEyes(aaf, repeat, fps); + } +} + +void EmoteDisplay::SetChatMessage(const char* role, const char* content) +{ + engine_->Lock(); + if (content && strlen(content) > 0) { + gfx_label_set_text(obj_label_tips, content); + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); + } + engine_->Unlock(); +} + +void EmoteDisplay::SetStatus(const char* status) +{ + if (!engine_) { + return; + } + + if (std::strcmp(status, "聆听中...") == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_ANIM_TOP); + engine_->setEyes(MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20); + engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_MIC_BIN); + } else if (std::strcmp(status, "待命") == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_TIME); + engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN); + } else if (std::strcmp(status, "说话中...") == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); + engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_SPEAKER_ZZZ_BIN); + } else if (std::strcmp(status, "错误") == 0) { + SetUIDisplayMode(UIDisplayMode::SHOW_TIPS); + engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN); + } + + engine_->Lock(); + if (std::strcmp(status, "连接中...") != 0) { + gfx_label_set_text(obj_label_tips, status); + } + engine_->Unlock(); +} + +void EmoteDisplay::InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io) +{ + engine_ = std::make_unique(panel, panel_io); +} + +bool EmoteDisplay::Lock(int timeout_ms) +{ + return true; +} + +void EmoteDisplay::Unlock() +{ + // Implementation if needed +} + +} // namespace anim diff --git a/main/boards/echoear/emote_display.h b/main/boards/echoear/emote_display.h new file mode 100644 index 00000000..24e8e3b0 --- /dev/null +++ b/main/boards/echoear/emote_display.h @@ -0,0 +1,66 @@ +#pragma once + +#include "display/lcd_display.h" +#include +#include +#include +#include +#include "mmap_generate_emoji_normal.h" +#include "gfx.h" + +namespace anim { + +// Helper function for setting up image descriptors +void SetupImageDescriptor(mmap_assets_handle_t assets_handle, gfx_image_dsc_t* img_dsc, int asset_id); + +class EmoteEngine; + +using FlushIoReadyCallback = std::function; +using FlushCallback = std::function; + +class EmoteEngine { +public: + EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io); + ~EmoteEngine(); + + void setEyes(int aaf, bool repeat, int fps); + void stopEyes(); + + void Lock(); + void Unlock(); + + void SetIcon(int asset_id); + mmap_assets_handle_t GetAssetsHandle() const { return assets_handle_; } + + // Callback functions (public to be accessible from static helper functions) + static bool OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx); + static void OnFlush(gfx_handle_t handle, int x_start, int y_start, int x_end, int y_end, const void *color_data); + +private: + gfx_handle_t engine_handle_; + mmap_assets_handle_t assets_handle_; +}; + +class EmoteDisplay : public Display { +public: + EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io); + virtual ~EmoteDisplay(); + + virtual void SetEmotion(const char* emotion) override; + virtual void SetStatus(const char* status) override; + virtual void SetChatMessage(const char* role, const char* content) override; + + anim::EmoteEngine* GetEngine() + { + return engine_.get(); + } + +private: + void InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io); + virtual bool Lock(int timeout_ms = 0) override; + virtual void Unlock() override; + + std::unique_ptr engine_; +}; + +} // namespace anim diff --git a/main/idf_component.yml b/main/idf_component.yml index 4aea7e76..31a759ab 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -31,6 +31,7 @@ dependencies: esp_lvgl_port: ~2.6.0 espressif/esp_io_expander_tca95xx_16bit: ^2.0.0 espressif2022/image_player: ==1.1.0~1 + espressif2022/esp_emote_gfx: ^1.0.0 espressif/adc_mic: ^0.2.0 espressif/esp_mmap_assets: '>=1.2' txp666/otto-emoji-gif-component: ~1.0.2 diff --git a/partitions/v1/16m_echoear.csv b/partitions/v1/16m_echoear.csv new file mode 100644 index 00000000..543c92ce --- /dev/null +++ b/partitions/v1/16m_echoear.csv @@ -0,0 +1,9 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +model, data, spiffs, 0x10000, 0xF0000, +ota_0, app, ota_0, 0x100000, 5M, +ota_1, app, ota_1, 0x700000, 5M, +assets_A, data, spiffs, , 4000K,