diff --git a/.gitignore b/.gitignore index b21fbde..ff59a9c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ managed_components/ # Cache / Compiled .cache *.pyc -*.bin # MCP / Memov .mcp.json diff --git a/application/basic_demo/assets_local/284_240/config.json b/application/basic_demo/assets_local/284_240/config.json new file mode 100644 index 0000000..5c57194 --- /dev/null +++ b/application/basic_demo/assets_local/284_240/config.json @@ -0,0 +1,3 @@ +{ + "emoji_collection": "emoji_large" +} diff --git a/application/basic_demo/assets_local/284_240/emote.json b/application/basic_demo/assets_local/284_240/emote.json new file mode 100644 index 0000000..bd9466a --- /dev/null +++ b/application/basic_demo/assets_local/284_240/emote.json @@ -0,0 +1,4 @@ +[ + {"emote": "swim", "src": "swim.eaf", "loop": true, "fps": 20}, + {"emote": "offline", "src": "offline.eaf", "loop": true, "fps": 20} +] diff --git a/application/basic_demo/assets_local/284_240/layout.json b/application/basic_demo/assets_local/284_240/layout.json new file mode 100644 index 0000000..b77d15c --- /dev/null +++ b/application/basic_demo/assets_local/284_240/layout.json @@ -0,0 +1,47 @@ +[ + { + "type": "anim", + "name": "eye_anim", + "align": "GFX_ALIGN_CENTER", + "x": 0, + "y": 20, + "anim": { + "mirror": "false" + } + }, + { + "type": "anim", + "name": "emerg_dlg", + "align": "GFX_ALIGN_CENTER", + "x": 0, + "y": 20, + "anim": { + "mirror": "false" + } + }, + { + "type": "image", + "name": "status_icon", + "align": "GFX_ALIGN_TOP_LEFT", + "x": 5, + "y": 18 + }, + { + "type": "label", + "name": "toast_label", + "align": "GFX_ALIGN_TOP_MID", + "x": 0, + "y": 20, + "width": 240, + "height": 40, + "label": { + "color": 16777215, + "text_align": "GFX_TEXT_ALIGN_CENTER", + "long_mode": { + "type": "GFX_LABEL_LONG_SCROLL", + "loop": true, + "speed": 10 + } + } + } +] diff --git a/application/basic_demo/assets_local/emoji_large/icon_tips.bin b/application/basic_demo/assets_local/emoji_large/icon_tips.bin new file mode 100644 index 0000000..457a279 Binary files /dev/null and b/application/basic_demo/assets_local/emoji_large/icon_tips.bin differ diff --git a/application/basic_demo/assets_local/emoji_large/offline.eaf b/application/basic_demo/assets_local/emoji_large/offline.eaf new file mode 100644 index 0000000..33d335e Binary files /dev/null and b/application/basic_demo/assets_local/emoji_large/offline.eaf differ diff --git a/application/basic_demo/assets_local/emoji_large/swim.eaf b/application/basic_demo/assets_local/emoji_large/swim.eaf new file mode 100644 index 0000000..09c852b Binary files /dev/null and b/application/basic_demo/assets_local/emoji_large/swim.eaf differ diff --git a/application/basic_demo/components/display_arbiter/CMakeLists.txt b/application/basic_demo/components/display_arbiter/CMakeLists.txt new file mode 100644 index 0000000..472e7dc --- /dev/null +++ b/application/basic_demo/components/display_arbiter/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_component_register( + SRCS + "display_arbiter.c" + INCLUDE_DIRS + "include" + REQUIRES + esp_timer +) diff --git a/application/basic_demo/components/display_arbiter/display_arbiter.c b/application/basic_demo/components/display_arbiter/display_arbiter.c new file mode 100644 index 0000000..ca9fd9b --- /dev/null +++ b/application/basic_demo/components/display_arbiter/display_arbiter.c @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "display_arbiter.h" + +#include +#include + +#include "esp_check.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +static const char *TAG = "display_arbiter"; + +typedef struct { + SemaphoreHandle_t lock; + display_arbiter_owner_t owner; + uint32_t lua_depth; + display_arbiter_owner_changed_cb_t callback; + void *callback_user_ctx; +} display_arbiter_state_t; + +static display_arbiter_state_t s_state = { + .owner = DISPLAY_ARBITER_OWNER_EMOTE, +}; + +static esp_err_t display_arbiter_lock(void) +{ + if (!s_state.lock) { + s_state.lock = xSemaphoreCreateMutex(); + } + ESP_RETURN_ON_FALSE(s_state.lock != NULL, ESP_ERR_NO_MEM, TAG, "create mutex failed"); + ESP_RETURN_ON_FALSE(xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(1000)) == pdTRUE, ESP_ERR_TIMEOUT, TAG, "mutex timeout"); + return ESP_OK; +} + +static void display_arbiter_unlock(void) +{ + if (s_state.lock) { + xSemaphoreGive(s_state.lock); + } +} + +static const char *display_arbiter_owner_to_str(display_arbiter_owner_t owner) +{ + switch (owner) { + case DISPLAY_ARBITER_OWNER_LUA: + return "lua"; + case DISPLAY_ARBITER_OWNER_EMOTE: + return "emote"; + default: + return "unknown"; + } + return "unknown"; +} + +static esp_err_t display_arbiter_change_owner_locked(display_arbiter_owner_t owner) +{ + s_state.owner = owner; + ESP_LOGI(TAG, "display owner changed to %s", display_arbiter_owner_to_str(owner)); + return ESP_OK; +} + +static void display_arbiter_notify_owner_changed(display_arbiter_owner_t owner) +{ + if (s_state.callback) { + s_state.callback(owner, s_state.callback_user_ctx); + } +} + +esp_err_t display_arbiter_acquire(display_arbiter_owner_t owner) +{ + esp_err_t ret = display_arbiter_lock(); + display_arbiter_owner_t notify_owner = DISPLAY_ARBITER_OWNER_NONE; + bool owner_changed = false; + + if (ret != ESP_OK) { + return ret; + } + + ESP_GOTO_ON_FALSE(owner == DISPLAY_ARBITER_OWNER_LUA || owner == DISPLAY_ARBITER_OWNER_EMOTE, ESP_ERR_INVALID_ARG, + fail, TAG, "invalid owner"); + + if (owner == DISPLAY_ARBITER_OWNER_LUA) { + s_state.lua_depth++; + if (s_state.owner != DISPLAY_ARBITER_OWNER_LUA) { + ESP_GOTO_ON_ERROR(display_arbiter_change_owner_locked(DISPLAY_ARBITER_OWNER_LUA), fail, TAG, + "switch to lua owner failed"); + notify_owner = DISPLAY_ARBITER_OWNER_LUA; + owner_changed = true; + } + } else if (s_state.owner != DISPLAY_ARBITER_OWNER_EMOTE) { + ESP_GOTO_ON_ERROR(display_arbiter_change_owner_locked(DISPLAY_ARBITER_OWNER_EMOTE), fail, TAG, + "switch to emote owner failed"); + notify_owner = DISPLAY_ARBITER_OWNER_EMOTE; + owner_changed = true; + } + +fail: + display_arbiter_unlock(); + if (ret == ESP_OK && owner_changed) { + display_arbiter_notify_owner_changed(notify_owner); + } + return ret; +} + +esp_err_t display_arbiter_release(display_arbiter_owner_t owner) +{ + esp_err_t ret = display_arbiter_lock(); + display_arbiter_owner_t notify_owner = DISPLAY_ARBITER_OWNER_NONE; + bool owner_changed = false; + + if (ret != ESP_OK) { + return ret; + } + + ESP_GOTO_ON_FALSE(owner == DISPLAY_ARBITER_OWNER_LUA || owner == DISPLAY_ARBITER_OWNER_EMOTE, ESP_ERR_INVALID_ARG, + fail, TAG, "invalid owner"); + + if (owner == DISPLAY_ARBITER_OWNER_LUA) { + ESP_GOTO_ON_FALSE(s_state.lua_depth > 0, ESP_ERR_INVALID_STATE, fail, TAG, "lua owner is not active"); + s_state.lua_depth--; + if (s_state.lua_depth == 0 && s_state.owner == DISPLAY_ARBITER_OWNER_LUA) { + ESP_GOTO_ON_ERROR(display_arbiter_change_owner_locked(DISPLAY_ARBITER_OWNER_EMOTE), fail, TAG, + "restore emote owner failed"); + notify_owner = DISPLAY_ARBITER_OWNER_EMOTE; + owner_changed = true; + } + } else if (s_state.owner == DISPLAY_ARBITER_OWNER_EMOTE) { + ESP_GOTO_ON_ERROR(display_arbiter_change_owner_locked(DISPLAY_ARBITER_OWNER_NONE), fail, TAG, + "clear emote owner failed"); + notify_owner = DISPLAY_ARBITER_OWNER_NONE; + owner_changed = true; + } + +fail: + display_arbiter_unlock(); + if (ret == ESP_OK && owner_changed) { + display_arbiter_notify_owner_changed(notify_owner); + } + return ret; +} + +display_arbiter_owner_t display_arbiter_get_owner(void) +{ + return s_state.owner; +} + +bool display_arbiter_is_owner(display_arbiter_owner_t owner) +{ + return display_arbiter_get_owner() == owner; +} + +esp_err_t display_arbiter_set_owner_changed_callback(display_arbiter_owner_changed_cb_t callback, void *user_ctx) +{ + esp_err_t ret = display_arbiter_lock(); + + if (ret != ESP_OK) { + return ret; + } + + s_state.callback = callback; + s_state.callback_user_ctx = user_ctx; + display_arbiter_unlock(); + return ESP_OK; +} diff --git a/application/basic_demo/components/display_arbiter/include/display_arbiter.h b/application/basic_demo/components/display_arbiter/include/display_arbiter.h new file mode 100644 index 0000000..92e6666 --- /dev/null +++ b/application/basic_demo/components/display_arbiter/include/display_arbiter.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + DISPLAY_ARBITER_OWNER_NONE = 0, + DISPLAY_ARBITER_OWNER_LUA, + DISPLAY_ARBITER_OWNER_EMOTE, +} display_arbiter_owner_t; + +typedef void (*display_arbiter_owner_changed_cb_t)(display_arbiter_owner_t owner, void *user_ctx); + +esp_err_t display_arbiter_acquire(display_arbiter_owner_t owner); +esp_err_t display_arbiter_release(display_arbiter_owner_t owner); +display_arbiter_owner_t display_arbiter_get_owner(void); +bool display_arbiter_is_owner(display_arbiter_owner_t owner); +esp_err_t display_arbiter_set_owner_changed_callback(display_arbiter_owner_changed_cb_t callback, void *user_ctx); + +#ifdef __cplusplus +} +#endif diff --git a/application/basic_demo/main/CMakeLists.txt b/application/basic_demo/main/CMakeLists.txt index 27d640a..596677a 100644 --- a/application/basic_demo/main/CMakeLists.txt +++ b/application/basic_demo/main/CMakeLists.txt @@ -8,3 +8,11 @@ idf_component_register( "web/styles.css" "web/app.js" ) + +if(CONFIG_BASIC_DEMO_ENABLE_EMOTE) + set(EMOTE_ASSETS_EXTERNAL_PATH "${CMAKE_SOURCE_DIR}/assets_local") + set(EMOTE_ASSETS_FILE "${CMAKE_BINARY_DIR}/emote_assets.bin") + build_speaker_assets_bin("emote" "284_240" ${EMOTE_ASSETS_FILE} ${CONFIG_MMAP_FILE_NAME_LENGTH} ${EMOTE_ASSETS_EXTERNAL_PATH}) + message(STATUS "Generated emote assets: ${EMOTE_ASSETS_FILE} -> emote partition") + esptool_py_flash_to_partition(flash "emote" "${EMOTE_ASSETS_FILE}") +endif() diff --git a/application/basic_demo/main/Kconfig.projbuild b/application/basic_demo/main/Kconfig.projbuild index f6aff39..354cc27 100644 --- a/application/basic_demo/main/Kconfig.projbuild +++ b/application/basic_demo/main/Kconfig.projbuild @@ -20,6 +20,13 @@ config BASIC_DEMO_MEMORY_MODE_LIGHTWEIGHT endchoice +config BASIC_DEMO_ENABLE_EMOTE + bool "Enable emote" + depends on ESP_BOARD_DEV_DISPLAY_LCD_SUPPORT + default y + help + Enable expression emote startup and emote asset packaging for boards with LCD display support. + config BASIC_DEMO_WIFI_SSID string "Default Wi-Fi SSID" default "" diff --git a/application/basic_demo/main/app_expression_emote.c b/application/basic_demo/main/app_expression_emote.c new file mode 100644 index 0000000..1f58811 --- /dev/null +++ b/application/basic_demo/main/app_expression_emote.c @@ -0,0 +1,292 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "esp_check.h" +#include "esp_heap_caps.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_ops.h" +#include "esp_log.h" +#include "esp_board_manager_includes.h" +#include "app_expression_emote.h" +#include "expression_emote.h" +#include "freertos/FreeRTOS.h" +#include "gfx.h" +#include "display_arbiter.h" + +#if defined(CONFIG_BASIC_DEMO_ENABLE_EMOTE) + +static const char *TAG = "app_emote"; + +#define EMOTE_ASSETS_PARTITION "emote" + +static esp_lcd_panel_io_handle_t s_io_handle; +static esp_lcd_panel_handle_t s_panel_handle; +static int s_lcd_width; +static int s_lcd_height; +static emote_handle_t s_emote_handle; + +static bool app_emote_should_swap_color(const dev_display_lcd_config_t *lcd_cfg) +{ + if (lcd_cfg == NULL || lcd_cfg->sub_type == NULL) { + return true; + } + + if (strcmp(lcd_cfg->sub_type, "dsi") == 0 || strcmp(lcd_cfg->sub_type, "mipi_dsi") == 0 || strcmp(lcd_cfg->sub_type, "rgb") == 0) { + return false; + } + + return true; +} + +static void app_emote_on_owner_changed(display_arbiter_owner_t owner, void *user_ctx) +{ + (void)user_ctx; + + if (owner != DISPLAY_ARBITER_OWNER_EMOTE || !s_emote_handle) { + return; + } + + esp_err_t err = emote_notify_all_refresh(s_emote_handle); + if (err != ESP_OK) { + ESP_LOGW(TAG, "refresh after owner switch failed: %s", esp_err_to_name(err)); + } +} + +static void app_emote_flush_callback(int x_start, int y_start, int x_end, int y_end, + const void *data, emote_handle_t handle) +{ + esp_err_t err = ESP_OK; + + if (!s_panel_handle) { + if (handle) { + emote_notify_flush_finished(handle); + } + return; + } + + if (!display_arbiter_is_owner(DISPLAY_ARBITER_OWNER_EMOTE)) { + if (handle) { + emote_notify_flush_finished(handle); + } + return; + } + + err = esp_lcd_panel_draw_bitmap(s_panel_handle, x_start, y_start, x_end, y_end, data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); + if (handle) { + emote_notify_flush_finished(handle); + } + return; + } + + if (handle) { + emote_notify_flush_finished(handle); + } +} + +static void app_emote_update_callback(gfx_disp_event_t event, const void *obj, + emote_handle_t handle) +{ + gfx_obj_t *wait_obj = NULL; + + if (!handle) { + return; + } + + wait_obj = emote_get_obj_by_name(handle, EMT_DEF_ELEM_EMERG_DLG); + if (wait_obj == obj && event == GFX_DISP_EVENT_ALL_FRAME_DONE) { + ESP_LOGI(TAG, "Emergency dialog finished"); + } +} + +static esp_err_t app_emote_load_board_display(void) +{ +#if !CONFIG_ESP_BOARD_DEV_DISPLAY_LCD_SUPPORT + return ESP_ERR_NOT_SUPPORTED; +#else + void *lcd_handle = NULL; + void *lcd_config = NULL; + esp_err_t err = esp_board_manager_get_device_handle(ESP_BOARD_DEVICE_NAME_DISPLAY_LCD, + &lcd_handle); + if (err != ESP_OK) { + return err; + } + + err = esp_board_manager_get_device_config(ESP_BOARD_DEVICE_NAME_DISPLAY_LCD, &lcd_config); + if (err != ESP_OK) { + return err; + } + + ESP_RETURN_ON_FALSE(lcd_handle != NULL && lcd_config != NULL, + ESP_ERR_INVALID_STATE, TAG, + "display_lcd handle/config is NULL"); + + dev_display_lcd_handles_t *lcd_handles = (dev_display_lcd_handles_t *)lcd_handle; + dev_display_lcd_config_t *lcd_cfg = (dev_display_lcd_config_t *)lcd_config; + + ESP_RETURN_ON_FALSE(lcd_handles->panel_handle != NULL, + ESP_ERR_INVALID_STATE, TAG, + "display_lcd panel_handle is NULL"); + + s_panel_handle = lcd_handles->panel_handle; + s_io_handle = lcd_handles->io_handle; + s_lcd_width = lcd_cfg->lcd_width; + s_lcd_height = lcd_cfg->lcd_height; + + ESP_LOGI(TAG, "Using board_manager display: panel=%p io=%p size=%dx%d subtype=%s", + s_panel_handle, s_io_handle, s_lcd_width, s_lcd_height, + lcd_cfg->sub_type ? lcd_cfg->sub_type : "(null)"); + return ESP_OK; +#endif +} + +static emote_config_t app_emote_get_default_config(void) +{ + void *lcd_config = NULL; + bool swap = true; + if (esp_board_manager_get_device_config(ESP_BOARD_DEVICE_NAME_DISPLAY_LCD, &lcd_config) == ESP_OK) { + swap = app_emote_should_swap_color((const dev_display_lcd_config_t *)lcd_config); + } + + emote_config_t config = { + .flags = { + .swap = swap, + .double_buffer = true, + .buff_dma = true, + }, + .gfx_emote = { + .h_res = s_lcd_width, + .v_res = s_lcd_height, + .fps = 10, + }, + .buffers = { + .buf_pixels = (size_t)s_lcd_width * 16, + }, + .task = { + .task_priority = 3, + .task_stack = 12 * 1024, + .task_affinity = -1, +#ifdef CONFIG_SPIRAM_XIP_FROM_PSRAM + .task_stack_in_ext = true, +#else + .task_stack_in_ext = false, +#endif + }, + .flush_cb = app_emote_flush_callback, + .update_cb = app_emote_update_callback, + }; + + return config; +} + +static esp_err_t app_emote_show_idle_msg(const char *idle, const char *msg) +{ + ESP_RETURN_ON_FALSE(s_emote_handle != NULL, ESP_ERR_INVALID_STATE, + TAG, "emote handle is NULL"); + + ESP_RETURN_ON_ERROR(emote_set_event_msg(s_emote_handle, EMOTE_MGR_EVT_SYS, msg), + TAG, "set emote message failed"); + ESP_RETURN_ON_ERROR(emote_set_anim_emoji(s_emote_handle, idle), + TAG, "set emote idle animation failed"); + if (display_arbiter_is_owner(DISPLAY_ARBITER_OWNER_EMOTE)) { + ESP_RETURN_ON_ERROR(emote_notify_all_refresh(s_emote_handle), + TAG, "refresh emote display failed"); + } + + return ESP_OK; +} + +esp_err_t app_expression_emote_set_network_state(bool connected) +{ + const char *idle = connected ? "swim" : "offline"; + const char *msg = connected ? "Wi-Fi connected" : "Wi-Fi offline"; + + ESP_RETURN_ON_FALSE(s_emote_handle != NULL, ESP_ERR_INVALID_STATE, + TAG, "emote handle is NULL"); + + ESP_LOGI(TAG, "Update network emote: %s", idle); + return app_emote_show_idle_msg(idle, msg); +} + +static void app_emote_cleanup(void) +{ + if (s_emote_handle) { + emote_deinit(s_emote_handle); + s_emote_handle = NULL; + } + display_arbiter_set_owner_changed_callback(NULL, NULL); +} + +static esp_err_t app_expression_emote_init(void) +{ + emote_data_t data = { + .type = EMOTE_SOURCE_PARTITION, + .source = { + .partition_label = EMOTE_ASSETS_PARTITION, + }, + .flags = { +#ifdef CONFIG_SPIRAM_XIP_FROM_PSRAM + .mmap_enable = false, +#else + .mmap_enable = true, +#endif + }, + }; + + if (s_emote_handle) { + ESP_LOGI(TAG, "Expression emote app already initialized"); + return ESP_OK; + } + + esp_err_t err = app_emote_load_board_display(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get board display handles: %s", esp_err_to_name(err)); + return err; + } + + emote_config_t config = app_emote_get_default_config(); + ESP_RETURN_ON_ERROR(display_arbiter_set_owner_changed_callback(app_emote_on_owner_changed, NULL), + TAG, "register display owner callback failed"); + s_emote_handle = emote_init(&config); + if (!s_emote_handle || !emote_is_initialized(s_emote_handle)) { + ESP_LOGE(TAG, "emote_init failed"); + app_emote_cleanup(); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Loading emote assets from partition '%s'", EMOTE_ASSETS_PARTITION); + err = emote_mount_and_load_assets(s_emote_handle, &data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "emote_mount_and_load_assets failed: %s", esp_err_to_name(err)); + app_emote_cleanup(); + return err; + } + + err = app_expression_emote_set_network_state(false); + if (err != ESP_OK) { + ESP_LOGE(TAG, "show idle message failed: %s", esp_err_to_name(err)); + app_emote_cleanup(); + return err; + } + + ESP_LOGI(TAG, "Expression emote app initialized"); + return ESP_OK; + +} + +esp_err_t app_expression_emote_start(void) +{ + esp_err_t err = app_expression_emote_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Expression emote init failed"); + app_emote_cleanup(); + } + return err; +} + +#endif diff --git a/application/basic_demo/main/app_expression_emote.h b/application/basic_demo/main/app_expression_emote.h new file mode 100644 index 0000000..d60e527 --- /dev/null +++ b/application/basic_demo/main/app_expression_emote.h @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t app_expression_emote_start(void); +esp_err_t app_expression_emote_set_network_state(bool connected); + +#ifdef __cplusplus +} +#endif diff --git a/application/basic_demo/main/assets/README.md b/application/basic_demo/main/assets/README.md new file mode 100644 index 0000000..b430473 --- /dev/null +++ b/application/basic_demo/main/assets/README.md @@ -0,0 +1,14 @@ +Project-local emote assets override directory. + +`build_speaker_assets_bin(... --external_path)` searches this directory first, +then falls back to the packaged `esp_emote_assets` component defaults. + +If you want to override assets locally, mirror the structure expected by +`esp_emote_assets`, for example: + +- `360_360/config.json` +- `360_360/layout.json` +- `360_360/emote.json` +- `emoji_large/...` +- `emoji_small/...` +- `font/...` diff --git a/application/basic_demo/main/basic_demo_wifi.c b/application/basic_demo/main/basic_demo_wifi.c index e535568..cad6cc4 100644 --- a/application/basic_demo/main/basic_demo_wifi.c +++ b/application/basic_demo/main/basic_demo_wifi.c @@ -25,6 +25,20 @@ static EventGroupHandle_t s_wifi_event_group; static int s_retry_count; static bool s_connected; static char s_ip_addr[16] = "0.0.0.0"; +static basic_demo_wifi_state_cb_t s_state_cb; +static void *s_state_cb_user_ctx; + +static void notify_wifi_state(bool connected) +{ + if (s_connected == connected) { + return; + } + + s_connected = connected; + if (s_state_cb) { + s_state_cb(connected, s_state_cb_user_ctx); + } +} static void wifi_event_handler(void *arg, esp_event_base_t event_base, @@ -39,7 +53,7 @@ static void wifi_event_handler(void *arg, } if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { - s_connected = false; + notify_wifi_state(false); strlcpy(s_ip_addr, "0.0.0.0", sizeof(s_ip_addr)); if (s_retry_count < WIFI_MAX_RETRY) { s_retry_count++; @@ -55,7 +69,7 @@ static void wifi_event_handler(void *arg, if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; snprintf(s_ip_addr, sizeof(s_ip_addr), IPSTR, IP2STR(&event->ip_info.ip)); - s_connected = true; + notify_wifi_state(true); s_retry_count = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); ESP_LOGI(TAG, "Connected, IP=%s", s_ip_addr); @@ -122,6 +136,18 @@ esp_err_t basic_demo_wifi_wait_connected(uint32_t timeout_ms) return ESP_ERR_TIMEOUT; } +esp_err_t basic_demo_wifi_register_state_callback(basic_demo_wifi_state_cb_t cb, void *user_ctx) +{ + s_state_cb = cb; + s_state_cb_user_ctx = user_ctx; + + if (s_state_cb) { + s_state_cb(s_connected, s_state_cb_user_ctx); + } + + return ESP_OK; +} + bool basic_demo_wifi_is_connected(void) { return s_connected; diff --git a/application/basic_demo/main/basic_demo_wifi.h b/application/basic_demo/main/basic_demo_wifi.h index 9dec413..d102aaf 100644 --- a/application/basic_demo/main/basic_demo_wifi.h +++ b/application/basic_demo/main/basic_demo_wifi.h @@ -10,8 +10,11 @@ #include "esp_err.h" +typedef void (*basic_demo_wifi_state_cb_t)(bool connected, void *user_ctx); + esp_err_t basic_demo_wifi_init(void); esp_err_t basic_demo_wifi_start(const char *ssid, const char *password); esp_err_t basic_demo_wifi_wait_connected(uint32_t timeout_ms); +esp_err_t basic_demo_wifi_register_state_callback(basic_demo_wifi_state_cb_t cb, void *user_ctx); bool basic_demo_wifi_is_connected(void); const char *basic_demo_wifi_get_ip(void); diff --git a/application/basic_demo/main/idf_component.yml b/application/basic_demo/main/idf_component.yml index 0a0f348..5ca32ba 100644 --- a/application/basic_demo/main/idf_component.yml +++ b/application/basic_demo/main/idf_component.yml @@ -12,6 +12,9 @@ dependencies: rules: - if: "target in [esp32p4]" + espressif2022/esp_emote_expression: + version: "^1.0.0" + cap_cli: path: ../../../components/claw_capabilities/cap_cli diff --git a/application/basic_demo/main/main.c b/application/basic_demo/main/main.c index 0267d8c..d8fece1 100644 --- a/application/basic_demo/main/main.c +++ b/application/basic_demo/main/main.c @@ -4,6 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ #include "app_claw.h" +#if defined(CONFIG_BASIC_DEMO_ENABLE_EMOTE) +#include "app_expression_emote.h" +#endif #include "basic_demo_settings.h" #include "basic_demo_wifi.h" #include "config_http_server.h" @@ -24,6 +27,20 @@ const char *basic_demo_fatfs_base_path = "/fatfs"; static wl_handle_t s_wl_handle = WL_INVALID_HANDLE; +static void on_wifi_state_changed(bool connected, void *user_ctx) +{ + (void)user_ctx; + +#if defined(CONFIG_BASIC_DEMO_ENABLE_EMOTE) + esp_err_t err = app_expression_emote_set_network_state(connected); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to update network emote: %s", esp_err_to_name(err)); + } +#else + (void)connected; +#endif +} + static esp_err_t init_nvs(void) { esp_err_t err = nvs_flash_init(); @@ -113,9 +130,13 @@ void app_main(void) ESP_ERROR_CHECK(basic_demo_settings_init()); ESP_ERROR_CHECK(basic_demo_settings_load(&s_settings)); ESP_ERROR_CHECK(esp_board_manager_init()); +#if defined(CONFIG_BASIC_DEMO_ENABLE_EMOTE) + ESP_ERROR_CHECK(app_expression_emote_start()); +#endif ESP_ERROR_CHECK(init_fatfs()); ESP_ERROR_CHECK(basic_demo_wifi_init()); ESP_ERROR_CHECK(config_http_server_init(basic_demo_fatfs_base_path)); + ESP_ERROR_CHECK(basic_demo_wifi_register_state_callback(on_wifi_state_changed, NULL)); if (basic_demo_wifi_start(s_settings.wifi_ssid, s_settings.wifi_password) == ESP_OK) { ESP_ERROR_CHECK(config_http_server_start()); diff --git a/application/basic_demo/partitions_16MB.csv b/application/basic_demo/partitions_16MB.csv index cc5ec83..b7c3d3a 100644 --- a/application/basic_demo/partitions_16MB.csv +++ b/application/basic_demo/partitions_16MB.csv @@ -4,4 +4,5 @@ otadata, data, ota, 0xF000, 0x2000 phy_init, data, phy, 0x11000, 0x1000 ota_0, app, ota_0, , 4M ota_1, app, ota_1, , 4M -storage, data, fat, , 7M +emote, data, spiffs, , 3M +storage, data, fat, , 4M diff --git a/application/basic_demo/sdkconfig.defaults b/application/basic_demo/sdkconfig.defaults index a3c5db7..9a76089 100644 --- a/application/basic_demo/sdkconfig.defaults +++ b/application/basic_demo/sdkconfig.defaults @@ -4,7 +4,7 @@ CONFIG_HTTPD_WS_SUPPORT=y CONFIG_SPIRAM_MEMTEST=n -CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=2048 +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0 CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=98304 CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y @@ -24,3 +24,6 @@ CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y CONFIG_MBEDTLS_HARDWARE_AES=n CONFIG_IDF_EXPERIMENTAL_FEATURES=y +CONFIG_MMAP_FILE_NAME_LENGTH=32 +CONFIG_SPIRAM_XIP_FROM_PSRAM=y +CONFIG_EMOTE_DEF_BG_COLOR=0x171617 diff --git a/components/lua_modules/lua_module_display/CMakeLists.txt b/components/lua_modules/lua_module_display/CMakeLists.txt index b595833..43c35f1 100644 --- a/components/lua_modules/lua_module_display/CMakeLists.txt +++ b/components/lua_modules/lua_module_display/CMakeLists.txt @@ -6,6 +6,7 @@ idf_component_register( "include" REQUIRES cap_lua + display_arbiter esp_lcd esp_painter libpng diff --git a/components/lua_modules/lua_module_display/skills/lua_module_display.md b/components/lua_modules/lua_module_display/skills/lua_module_display.md index bc8de2c..26142e5 100644 --- a/components/lua_modules/lua_module_display/skills/lua_module_display.md +++ b/components/lua_modules/lua_module_display/skills/lua_module_display.md @@ -27,6 +27,7 @@ After `display.init(...)` succeeds: - `display.width()` returns the current screen width - `display.height()` returns the current screen height - Most drawing APIs can be used +- The display arbiter automatically grants Lua foreground ownership for the lifetime of the display session When finished: diff --git a/components/lua_modules/lua_module_display/src/display_hal.c b/components/lua_modules/lua_module_display/src/display_hal.c index b0845ae..37059cc 100644 --- a/components/lua_modules/lua_module_display/src/display_hal.c +++ b/components/lua_modules/lua_module_display/src/display_hal.c @@ -8,6 +8,7 @@ #include #include #include +#include "display_arbiter.h" #include "esp_attr.h" #include "esp_check.h" #include "esp_heap_caps.h" @@ -716,6 +717,15 @@ static esp_err_t display_hal_submit_bitmap_locked(int x_start, int y_start, } } + if (!display_arbiter_is_owner(DISPLAY_ARBITER_OWNER_LUA)) { + s_state.flush_in_flight = false; + if (pending_framebuffer_index >= 0) { + s_state.visible_framebuffer_index = (uint8_t)pending_framebuffer_index; + s_state.pending_framebuffer_index = -1; + } + return ESP_OK; + } + if (display_hal_panel_requires_swap()) { pixel_count = (size_t)(x_end - x_start) * (size_t)(y_end - y_start); ESP_RETURN_ON_FALSE(s_state.submit_swap_buffer != NULL, ESP_ERR_INVALID_STATE, TAG, diff --git a/components/lua_modules/lua_module_display/src/lua_module_display.c b/components/lua_modules/lua_module_display/src/lua_module_display.c index d266ec9..0f86959 100644 --- a/components/lua_modules/lua_module_display/src/lua_module_display.c +++ b/components/lua_modules/lua_module_display/src/lua_module_display.c @@ -13,6 +13,7 @@ #include "lua_module_display.h" #include "cap_lua.h" +#include "display_arbiter.h" #include "display_hal.h" #include "esp_err.h" #include "lauxlib.h" @@ -146,8 +147,14 @@ static int lua_display_init(lua_State *L) int lcd_height = lua_display_check_integer_arg(L, 4, "lcd_height"); display_hal_panel_if_t panel_if = lua_display_parse_panel_if(L, 5); - esp_err_t err = display_hal_create(panel_handle, io_handle, panel_if, lcd_width, lcd_height); + esp_err_t err = display_arbiter_acquire(DISPLAY_ARBITER_OWNER_LUA); if (err != ESP_OK) { + return luaL_error(L, "display init acquire failed: %s", esp_err_to_name(err)); + } + + err = display_hal_create(panel_handle, io_handle, panel_if, lcd_width, lcd_height); + if (err != ESP_OK) { + display_arbiter_release(DISPLAY_ARBITER_OWNER_LUA); return luaL_error(L, "display init failed: %s", esp_err_to_name(err)); } @@ -163,6 +170,11 @@ static int lua_display_deinit(lua_State *L) return luaL_error(L, "display deinit failed: %s", esp_err_to_name(err)); } + err = display_arbiter_release(DISPLAY_ARBITER_OWNER_LUA); + if (err != ESP_OK) { + return luaL_error(L, "display release failed: %s", esp_err_to_name(err)); + } + lua_pushboolean(L, 1); return 1; } @@ -1353,7 +1365,6 @@ int luaopen_display(lua_State *L) lua_setfield(L, -2, "init"); lua_pushcfunction(L, lua_display_deinit); lua_setfield(L, -2, "deinit"); - lua_pushcfunction(L, lua_display_width); lua_setfield(L, -2, "width"); lua_pushcfunction(L, lua_display_height);