diff --git a/apps/tuya.ai/your_chat_bot/CMakeLists.txt b/apps/tuya.ai/your_chat_bot/CMakeLists.txt index 1b3bc370b..a3a6b3658 100755 --- a/apps/tuya.ai/your_chat_bot/CMakeLists.txt +++ b/apps/tuya.ai/your_chat_bot/CMakeLists.txt @@ -1,6 +1,6 @@ ## # @file CMakeLists.txt -# @brief +# @brief #/ # APP_PATH @@ -9,15 +9,59 @@ set(APP_PATH ${CMAKE_CURRENT_LIST_DIR}) # APP_NAME get_filename_component(APP_NAME ${APP_PATH} NAME) -# APP_SRCS -aux_source_directory(${APP_PATH}/src APP_SRCS) +if (CONFIG_LVGL_PC_SIMULATOR STREQUAL "y") +######################################## +# Simulator mode: display only +######################################## +add_library(${EXAMPLE_LIB}) -set(APP_INC ${APP_PATH}/include) +target_compile_definitions(${EXAMPLE_LIB} + PRIVATE + LVGL_PC_SIMULATOR + ENABLE_CHAT_DISPLAY2=1 +) + +target_include_directories(${EXAMPLE_LIB} + PRIVATE + ${APP_PATH}/include + ${APP_PATH}/../ai_components/assets/include +) ######################################## # Generate language config header ######################################## +# set(LANG_SCRIPT "${APP_PATH}/../ai_components/assets/scripts/gen_lang.py") +# set(LANG_HEADER "${APP_PATH}/../ai_components/assets/include/lang_config.h") + +# if(CONFIG_ENABLE_AI_LANGUAGE_ENGLISH STREQUAL "y") +# set(LANG_JSON "${APP_PATH}/../ai_components/assets/language/en-US/language.json") +# else() +# set(LANG_JSON "${APP_PATH}/../ai_components/assets/language/zh-CN/language.json") +# endif() + +# execute_process( +# COMMAND ${CMAKE_COMMAND} -E env PYTHONIOENCODING=utf-8 PYTHONUNBUFFERED=1 +# ${PYTHON_CMD} ${LANG_SCRIPT} --input ${LANG_JSON} --output ${LANG_HEADER} +# RESULT_VARIABLE LANG_GEN_RESULT +# ) + +# if(NOT LANG_GEN_RESULT EQUAL 0) +# message(FATAL_ERROR "Failed to generate lang_config.h") +# endif() +add_subdirectory(${APP_PATH}/src/display2) + +add_subdirectory(${APP_PATH}/../ai_components) + +else() +######################################## +# Embedded mode: full build (original) +######################################## + +# APP_SRCS +aux_source_directory(${APP_PATH}/src APP_SRCS) + +set(APP_INC ${APP_PATH}/include) ######################################## # Target Configure @@ -43,3 +87,5 @@ if (CONFIG_ENABLE_BATTERY STREQUAL "y") endif() add_subdirectory(${APP_PATH}/../ai_components) + +endif() diff --git a/apps/tuya.ai/your_chat_bot/Kconfig b/apps/tuya.ai/your_chat_bot/Kconfig index 12a8fbeb0..e2e00e92b 100644 --- a/apps/tuya.ai/your_chat_bot/Kconfig +++ b/apps/tuya.ai/your_chat_bot/Kconfig @@ -9,5 +9,6 @@ config ENABLE_BATTERY default n rsource "../ai_components/Kconfig" +rsource "src/display2/Kconfig" endmenu \ No newline at end of file diff --git a/apps/tuya.ai/your_chat_bot/app_default.config b/apps/tuya.ai/your_chat_bot/app_default.config index a201f4824..aff78c58d 100644 --- a/apps/tuya.ai/your_chat_bot/app_default.config +++ b/apps/tuya.ai/your_chat_bot/app_default.config @@ -1,8 +1,8 @@ CONFIG_PROJECT_VERSION="1.0.1" CONFIG_TUYA_PRODUCT_ID="9inb01mvjqh5zhhr" -CONFIG_ENABLE_AI_UI_TEXT_STREAMING=y CONFIG_ENABLE_COMP_AI_AUDIO_CODEC_OPUS=y CONFIG_ENABLE_COMP_AI_VIDEO=y +CONFIG_ENABLE_AI_UI_TEXT_STREAMING=y CONFIG_BUTTON_NAME="ai_chat_button" CONFIG_BOARD_CHOICE_T5AI=y CONFIG_TUYA_T5AI_BOARD_EX_MODULE_35565LCD=y diff --git a/apps/tuya.ai/your_chat_bot/config/TUYA_LINUX_LVGL_SIMULATOR.config b/apps/tuya.ai/your_chat_bot/config/TUYA_LINUX_LVGL_SIMULATOR.config new file mode 100644 index 000000000..decb9b35f --- /dev/null +++ b/apps/tuya.ai/your_chat_bot/config/TUYA_LINUX_LVGL_SIMULATOR.config @@ -0,0 +1,10 @@ +CONFIG_PROJECT_VERSION="1.0.1" +CONFIG_BOARD_CHOICE_LINUX=y +CONFIG_BOARD_CHOICE_UBUNTU=y +CONFIG_ENABLE_LIBLVGL=y +CONFIG_ENABLE_CHAT_DISPLAY2=y +CONFIG_LV_FONT_MONTSERRAT_48=y +CONFIG_LVGL_PC_SIMULATOR=y +CONFIG_SIM_SCREEN_WIDTH=320 +CONFIG_SIM_SCREEN_HEIGHT=480 +CONFIG_ENABLE_GUI2_WECHAT=y \ No newline at end of file diff --git a/apps/tuya.ai/your_chat_bot/src/display2/CMakeLists.txt b/apps/tuya.ai/your_chat_bot/src/display2/CMakeLists.txt index f4f7a9690..7961b2243 100644 --- a/apps/tuya.ai/your_chat_bot/src/display2/CMakeLists.txt +++ b/apps/tuya.ai/your_chat_bot/src/display2/CMakeLists.txt @@ -9,19 +9,24 @@ set(APP_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}) set(APP_MODULE_SRCS) list(APPEND APP_MODULE_SRCS ${APP_MODULE_PATH}/app_ui_helper.c + ) + +if (NOT CONFIG_LVGL_PC_SIMULATOR STREQUAL "y") + list(APPEND APP_MODULE_SRCS ${APP_MODULE_PATH}/app_display.c ${APP_MODULE_PATH}/tuya_lvgl.c ) +endif() -set(APP_MODULE_INC +set(APP_MODULE_INC ${APP_MODULE_PATH} ) # add subdirectory if (CONFIG_ENABLE_GUI2_CHATBOT STREQUAL "y") -add_subdirectory(${APP_MODULE_PATH}/ui_chatbot) + add_subdirectory(${APP_MODULE_PATH}/ui_chatbot) elseif (CONFIG_ENABLE_GUI2_WECHAT STREQUAL "y") -add_subdirectory(${APP_MODULE_PATH}/ui_wechat) + add_subdirectory(${APP_MODULE_PATH}/ui_wechat) endif() ######################################## @@ -30,10 +35,10 @@ endif() target_sources(${EXAMPLE_LIB} PRIVATE ${APP_MODULE_SRCS} - ) +) target_include_directories(${EXAMPLE_LIB} PRIVATE ${APP_MODULE_INC} - ) +) -endif() \ No newline at end of file +endif() diff --git a/apps/tuya.ai/your_chat_bot/src/display2/Kconfig b/apps/tuya.ai/your_chat_bot/src/display2/Kconfig index 19ef752a7..a4376a3a9 100644 --- a/apps/tuya.ai/your_chat_bot/src/display2/Kconfig +++ b/apps/tuya.ai/your_chat_bot/src/display2/Kconfig @@ -1,3 +1,8 @@ +config ENABLE_CHAT_DISPLAY2 + bool "Enable Chat Display2 UI" + depends on ENABLE_LIBLVGL + default n + if(ENABLE_CHAT_DISPLAY2) choice prompt "Select Display UI Style" diff --git a/apps/tuya.ai/your_chat_bot/src/display2/app_ui_helper.c b/apps/tuya.ai/your_chat_bot/src/display2/app_ui_helper.c index 525b359e6..315a92107 100644 --- a/apps/tuya.ai/your_chat_bot/src/display2/app_ui_helper.c +++ b/apps/tuya.ai/your_chat_bot/src/display2/app_ui_helper.c @@ -8,32 +8,20 @@ #include "app_ui_helper.h" #if defined(ENABLE_CHAT_DISPLAY2) && (ENABLE_CHAT_DISPLAY2 == 1) + #include "lang_config.h" +#include "tal_time_service.h" /* POSIX_TM_S, tal_time_get_local_time_custom */ +#include +#include +#ifndef LVGL_PC_SIMULATOR #include "ai_chat_main.h" - -#include "tal_time_service.h" #include "tuya_iot.h" #include "tuya_lvgl.h" - #include "screens/ui_setting.h" #include "ui.h" - #include "tal_api.h" #include "netmgr.h" -#include - -/*********************************************************** -************************macro define************************ -***********************************************************/ - -/*********************************************************** -***********************typedef define*********************** -***********************************************************/ - -/*********************************************************** -********************function declaration******************** -***********************************************************/ /*********************************************************** ***********************variable define********************** @@ -44,14 +32,6 @@ static TIMER_ID s_date_timer_id = NULL; /*********************************************************** ***********************function define********************** ***********************************************************/ - -uint8_t app_ui_get_volume_value(void) -{ - int volume = 0; - ai_audio_player_get_vol(&volume); - return volume; -} - static void __app_ui_set_volume_value_tm_cb(TIMER_ID timer_id, void *arg) { uint8_t value = *(uint8_t *)arg; @@ -68,21 +48,6 @@ static void __app_ui_set_volume_value_tm_cb(TIMER_ID timer_id, void *arg) tuya_lvgl_mutex_unlock(); } -void app_ui_set_volume_value(uint8_t value) -{ - static uint8_t s_volume_value = 0; - - s_volume_value = value; - - if (NULL == s_volume_timer_id) { - tal_sw_timer_create(__app_ui_set_volume_value_tm_cb, &s_volume_value, &s_volume_timer_id); - } - - if (s_volume_timer_id) { - tal_sw_timer_start(s_volume_timer_id, 300, TAL_TIMER_ONCE); - } -} - static void __app_ui_get_date_tm_cb(TIMER_ID timer_id, void *arg) { uint32_t year = 0, month = 0, day = 0; @@ -98,7 +63,6 @@ static void __app_ui_get_date_tm_cb(TIMER_ID timer_id, void *arg) PR_DEBUG("date: %04d-%02d-%02d, time: %02d:%02d", year, month, day, hour, minute); - // update date time display tuya_lvgl_mutex_lock(); ui_setting_date_update(year, month, day); ui_setting_time_update(hour, minute); @@ -110,43 +74,89 @@ static void __app_ui_get_date_tm_cb(TIMER_ID timer_id, void *arg) static uint8_t __app_ui_date_get_next_time(void) { POSIX_TM_S tm = {0}; - tal_time_get_local_time_custom(0, &tm); - return 60 - tm.tm_sec; } +static int __app_ui_time_sync_cb(void *data) +{ + __app_ui_get_date_tm_cb(NULL, NULL); + return 0; +} + +static int __app_ui_network_status_change_cb(void *data) +{ + netmgr_status_e net_status = *(netmgr_status_e *)data; + uint8_t connected = (net_status == NETMGR_LINK_UP) ? 1 : 0; + + tuya_lvgl_mutex_lock(); + ui_setting_wifi_update(connected); + ui_set_system_msg(connected ? SYSTEM_MSG_WIFI_SSID : SYSTEM_MSG_WIFI_DISCONNECTED); + tuya_lvgl_mutex_unlock(); + + return 0; +} +#endif /* !LVGL_PC_SIMULATOR */ + +/*********************************************************** +***********************function define********************** +***********************************************************/ + +uint8_t app_ui_get_volume_value(void) +{ +#ifndef LVGL_PC_SIMULATOR + int volume = 0; + ai_audio_player_get_vol(&volume); + return volume; +#else + return 50; +#endif +} + +void app_ui_set_volume_value(uint8_t value) +{ +#ifndef LVGL_PC_SIMULATOR + static uint8_t s_volume_value = 0; + s_volume_value = value; + + if (NULL == s_volume_timer_id) { + tal_sw_timer_create(__app_ui_set_volume_value_tm_cb, &s_volume_value, &s_volume_timer_id); + } + if (s_volume_timer_id) { + tal_sw_timer_start(s_volume_timer_id, 300, TAL_TIMER_ONCE); + } +#else + (void)value; +#endif +} + void app_ui_get_date_time_loop_start(void) { +#ifndef LVGL_PC_SIMULATOR if (NULL == s_date_timer_id) { tal_sw_timer_create(__app_ui_get_date_tm_cb, NULL, &s_date_timer_id); } - if (s_date_timer_id) { uint8_t next_time = __app_ui_date_get_next_time(); tal_sw_timer_start(s_date_timer_id, next_time * 1000 + 300, TAL_TIMER_ONCE); } +#endif } void app_ui_get_date_time_loop_stop(void) { +#ifndef LVGL_PC_SIMULATOR if (s_date_timer_id) { tal_sw_timer_stop(s_date_timer_id); } -} - -static int __app_ui_time_sync_cb(void *data) -{ - __app_ui_get_date_tm_cb(NULL, NULL); - return 0; +#endif } void app_ui_get_date(uint32_t *year, uint32_t *month, uint32_t *day) { - +#ifndef LVGL_PC_SIMULATOR OPERATE_RET rt = tal_time_check_time_sync(); if (rt != OPRT_OK) { - // subscribe time sync event tal_event_subscribe("app.time.sync", "app_ui_helper", __app_ui_time_sync_cb, SUBSCRIBE_TYPE_ONETIME); return; } @@ -158,24 +168,31 @@ void app_ui_get_date(uint32_t *year, uint32_t *month, uint32_t *day) *day = tm.tm_mday; PR_DEBUG("date: %04d-%02d-%02d", *year, *month, *day); - - return; +#else + if (year) *year = 2025; + if (month) *month = 1; + if (day) *day = 1; +#endif } void app_ui_get_time(uint32_t *hour, uint32_t *minute) { +#ifndef LVGL_PC_SIMULATOR POSIX_TM_S tm = {0}; tal_time_get_local_time_custom(0, &tm); *hour = tm.tm_hour; *minute = tm.tm_min; PR_DEBUG("time: %02d:%02d", *hour, *minute); - - return; +#else + if (hour) *hour = 12; + if (minute) *minute = 0; +#endif } void app_ui_get_wifi_status(uint8_t *status) { +#ifndef LVGL_PC_SIMULATOR netmgr_status_e net_status = NETMGR_LINK_DOWN; netmgr_conn_get(NETCONN_WIFI, NETCONN_CMD_STATUS, &net_status); if (net_status == NETMGR_LINK_UP) { @@ -183,55 +200,49 @@ void app_ui_get_wifi_status(uint8_t *status) } else { *status = 0; } - - return; -} - -static int __app_ui_network_status_change_cb(void *data) -{ - netmgr_status_e net_status = *(netmgr_status_e *)data; - uint8_t connected = (net_status == NETMGR_LINK_UP) ? 1 : 0; - - tuya_lvgl_mutex_lock(); - ui_setting_wifi_update(connected); - ui_set_system_msg(connected ? SYSTEM_MSG_WIFI_SSID : SYSTEM_MSG_WIFI_DISCONNECTED); - tuya_lvgl_mutex_unlock(); - - return 0; +#else + if (status) *status = 1; +#endif } void app_ui_network_status_change_subscribe(void) { - tal_event_subscribe(EVENT_LINK_STATUS_CHG, "app_ui_helper", __app_ui_network_status_change_cb, - SUBSCRIBE_TYPE_NORMAL); +#ifndef LVGL_PC_SIMULATOR + tal_event_subscribe(EVENT_LINK_STATUS_CHG, "app_ui_helper", + __app_ui_network_status_change_cb, SUBSCRIBE_TYPE_NORMAL); +#endif } void app_ui_network_status_change_unsubscribe(void) { - tal_event_unsubscribe(EVENT_LINK_STATUS_CHG, "app_ui_helper", __app_ui_network_status_change_cb); +#ifndef LVGL_PC_SIMULATOR + tal_event_unsubscribe(EVENT_LINK_STATUS_CHG, "app_ui_helper", + __app_ui_network_status_change_cb); +#endif } void app_ui_reset_device(void) { +#ifndef LVGL_PC_SIMULATOR tuya_iot_reset(tuya_iot_client_get()); +#endif } /* waveform power function start */ #include -// Audio power estimation #define AUDIO_POWER_BUFFER_SIZE 160 #define AUDIO_POWER_NORMALIZATION 50000.0f +#ifndef LVGL_PC_SIMULATOR static int16_t sg_audio_power_buffer[AUDIO_POWER_BUFFER_SIZE] = {0}; -static float sg_audio_power = 0.0f; -static MUTEX_HANDLE sg_audio_power_mutex = NULL; +static float sg_audio_power = 0.0f; +static MUTEX_HANDLE sg_audio_power_mutex = NULL; +#endif -/** - * @brief Calculate audio power from PCM samples - */ void app_ui_helper_calculate_audio_power(uint8_t *audio_data, uint32_t data_len) { +#ifndef LVGL_PC_SIMULATOR OPERATE_RET rt = OPRT_OK; if (NULL == sg_audio_power_mutex) { rt = tal_mutex_create_init(&sg_audio_power_mutex); @@ -245,14 +256,13 @@ void app_ui_helper_calculate_audio_power(uint8_t *audio_data, uint32_t data_len) return; } - uint32_t num_samples = data_len / 2; // 16-bit samples = 2 bytes per sample + uint32_t num_samples = data_len / 2; if (num_samples > AUDIO_POWER_BUFFER_SIZE) { num_samples = AUDIO_POWER_BUFFER_SIZE; } int16_t *pcm_samples = (int16_t *)audio_data; - // Update circular buffer if (num_samples >= AUDIO_POWER_BUFFER_SIZE) { memcpy(sg_audio_power_buffer, pcm_samples, AUDIO_POWER_BUFFER_SIZE * sizeof(int16_t)); } else { @@ -262,7 +272,6 @@ void app_ui_helper_calculate_audio_power(uint8_t *audio_data, uint32_t data_len) num_samples * sizeof(int16_t)); } - // Calculate max absolute value float max_value = 0.0f; for (int i = 0; i < AUDIO_POWER_BUFFER_SIZE; i++) { float abs_sample = fabsf((float)sg_audio_power_buffer[i]); @@ -271,29 +280,29 @@ void app_ui_helper_calculate_audio_power(uint8_t *audio_data, uint32_t data_len) } } - // Normalize float normalized_power = max_value / AUDIO_POWER_NORMALIZATION; - if (normalized_power > 1.0f) - normalized_power = 1.0f; - if (normalized_power < 0.0f) - normalized_power = 0.0f; + if (normalized_power > 1.0f) normalized_power = 1.0f; + if (normalized_power < 0.0f) normalized_power = 0.0f; - // Update power with mutex protection if (sg_audio_power_mutex != NULL) { tal_mutex_lock(sg_audio_power_mutex); sg_audio_power = normalized_power; tal_mutex_unlock(sg_audio_power_mutex); } +#else + (void)audio_data; (void)data_len; +#endif } float app_ui_helper_get_audio_power(void) { +#ifndef LVGL_PC_SIMULATOR static float power_max = 0.0f; static float power_min = 0.0f; - static float smoothed_power = 0.0f; // smoothed power value - static float display_value = 0.0f; // final display value (with inertia) - static float adaptive_threshold = 0.01f; // adaptive threshold - static uint32_t frame_count = 0; // frame counter + static float smoothed_power = 0.0f; + static float display_value = 0.0f; + static float adaptive_threshold = 0.01f; + static uint32_t frame_count = 0; float power = 0.0f; if (sg_audio_power_mutex != NULL) { @@ -302,85 +311,48 @@ float app_ui_helper_get_audio_power(void) tal_mutex_unlock(sg_audio_power_mutex); } - // PR_DEBUG("---> audio power: %f", power); - - // update power max and min - if (power > power_max) { - power_max = power; - } - if (power < power_min) { - power_min = power; - } + if (power > power_max) power_max = power; + if (power < power_min) power_min = power; - // update adaptive threshold every 100 frames (about 3 seconds at 33fps) frame_count++; if (frame_count >= 100) { - // use 15% of the maximum value as reference, but at least 0.003 adaptive_threshold = power_max * 0.15f; - if (adaptive_threshold < 0.003f) { - adaptive_threshold = 0.003f; - } - // gradually decay power_max, to avoid long-term impact after one large volume + if (adaptive_threshold < 0.003f) adaptive_threshold = 0.003f; power_max *= 0.9f; frame_count = 0; } - // PR_DEBUG("---> audio power: %f, power_max: %f, threshold: %f", power, power_max, adaptive_threshold); - - // 1. dynamic range adjustment - normalize according to adaptive threshold - // use smaller divisor for more sensitivity float normalized_power = power / (adaptive_threshold * 1.2f); - if (normalized_power > 1.0f) { - normalized_power = 1.0f; - } + if (normalized_power > 1.0f) normalized_power = 1.0f; - // 2. nonlinear mapping - use power curve to amplify changes - // square for values < 0.5 to boost small signals - // direct use for values >= 0.5 to preserve dynamics float enhanced_power; if (normalized_power < 0.5f) { - // amplify small values: x^0.7 (between sqrt and linear) enhanced_power = powf(normalized_power, 0.7f); } else { - // preserve large values with slight boost enhanced_power = 0.5f * powf(0.5f, 0.7f) + (normalized_power - 0.5f) * 1.3f; - if (enhanced_power > 1.0f) { - enhanced_power = 1.0f; - } + if (enhanced_power > 1.0f) enhanced_power = 1.0f; } - // 3. smoothing filter - exponential moving average (EMA) - // alpha = 0.6 means new value weight 60%, history value weight 40% (more responsive) const float alpha = 0.6f; - smoothed_power = alpha * enhanced_power + (1.0f - alpha) * smoothed_power; + smoothed_power = alpha * enhanced_power + (1.0f - alpha) * smoothed_power; - // 4. inertia effect - rise very fast(0.85), fall medium(0.3) float target_value = smoothed_power; if (target_value > display_value) { - // rise very fast for immediate response display_value = 0.85f * target_value + 0.15f * display_value; } else { - // fall at medium speed for natural decay display_value = 0.3f * target_value + 0.7f * display_value; } - // 5. add minimum animation threshold, below which display is 0 - if (display_value < 0.03f) { - display_value *= 0.7f; // faster decay to 0 - } - - // final limit range - if (display_value > 1.0f) { - display_value = 1.0f; - } - if (display_value < 0.0f) { - display_value = 0.0f; - } - - // PR_DEBUG("---> display_value: %f", display_value); + if (display_value < 0.03f) display_value *= 0.7f; + if (display_value > 1.0f) display_value = 1.0f; + if (display_value < 0.0f) display_value = 0.0f; return display_value; +#else + return 0.0f; +#endif } /* waveform power function end */ -#endif \ No newline at end of file + +#endif /* ENABLE_CHAT_DISPLAY2 */ diff --git a/apps/tuya.ai/your_chat_bot/src/display2/ui_wechat/screens/ui_home.c b/apps/tuya.ai/your_chat_bot/src/display2/ui_wechat/screens/ui_home.c index 5423a5efa..e1139c4c2 100644 --- a/apps/tuya.ai/your_chat_bot/src/display2/ui_wechat/screens/ui_home.c +++ b/apps/tuya.ai/your_chat_bot/src/display2/ui_wechat/screens/ui_home.c @@ -8,7 +8,9 @@ #include "lang_config.h" #include "src/core/lv_obj_style_gen.h" +#ifndef LVGL_PC_SIMULATOR #include "tal_api.h" +#endif #include #define UI_CHAT_MSG_MAX_COUNT (20) diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/CMakeLists.txt b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/CMakeLists.txt index 24ca314ea..78dcdcf14 100755 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/CMakeLists.txt +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/CMakeLists.txt @@ -9,6 +9,29 @@ set(APP_PATH ${CMAKE_CURRENT_LIST_DIR}) # APP_NAME get_filename_component(APP_NAME ${APP_PATH} NAME) +if (CONFIG_LVGL_PC_SIMULATOR STREQUAL "y") +######################################## +# Simulator mode: display only +######################################## +add_library(${EXAMPLE_LIB}) + +target_compile_options(${EXAMPLE_LIB} + PRIVATE + "-DLV_LVGL_H_INCLUDE_SIMPLE" +) + +target_include_directories(${EXAMPLE_LIB} + PRIVATE + ${APP_PATH}/include +) + +add_subdirectory(${APP_PATH}/src/display) + +else() +######################################## +# Embedded mode: full build (original) +######################################## + # APP_SRCS aux_source_directory(${APP_PATH}/src APP_SRCS) aux_source_directory(${APP_PATH}/src/media media_alert) @@ -50,3 +73,5 @@ add_subdirectory(${APP_PATH}/src/expand) add_subdirectory(${APP_PATH}/../../tuya.ai/ai_components) target_include_directories(${EXAMPLE_LIB} PRIVATE ${APP_PATH}/../../tuya.ai/ai_components) + +endif() diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/config/TUYA_LINUX_LVGL_SIMULATOR.config b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/config/TUYA_LINUX_LVGL_SIMULATOR.config new file mode 100644 index 000000000..d91085785 --- /dev/null +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/config/TUYA_LINUX_LVGL_SIMULATOR.config @@ -0,0 +1,5 @@ +CONFIG_PROJECT_VERSION="0.0.1" +CONFIG_BOARD_CHOICE_LINUX=y +CONFIG_BOARD_CHOICE_UBUNTU=y +CONFIG_ENABLE_LIBLVGL=y +CONFIG_LVGL_PC_SIMULATOR=y \ No newline at end of file diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/ai_chat_screen.c b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/ai_chat_screen.c index 1af8844d0..e170a5203 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/ai_chat_screen.c +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/ai_chat_screen.c @@ -5,15 +5,14 @@ */ #include "tuya_cloud_types.h" -#include "lv_vendor.h" +#ifdef ENABLE_LVGL_HARDWARE +#include "lv_vendor.h" #include "ai_ui_manage.h" - #include "toast_screen.h" #include "rfid_scan_screen.h" #include "ai_log_screen.h" #include "main_screen.h" - #include "app_display.h" /*********************************************************** @@ -37,7 +36,7 @@ static const CHAT_EMOJI_SHOW_T cCHAT_EMOJI_SHOW_LIST[] = { {EMOJI_ANGRY, "PET: Angry"}, {EMOJI_FEARFUL, "PET: Fearful"}, {EMOJI_SAD, "PET: Sad"}, -}; +}; /*********************************************************** ***********************function define********************** @@ -90,7 +89,6 @@ void __ui_set_custom_msg(uint32_t type, uint8_t *data, int len ) rfid_scan_screen_load(); break; case POCKET_DISP_TP_AI_LOG: - PR_DEBUG("AI LOG: %d", len); if (data && len > 0) { ai_log_screen_update_log((const char *)data, len); } @@ -114,3 +112,9 @@ OPERATE_RET ai_ui_chat_register(void) return ai_ui_register(&intfs); } + +#else /* ENABLE_LVGL_HARDWARE */ + +OPERATE_RET ai_ui_chat_register(void) { return OPRT_OK; } + +#endif /* ENABLE_LVGL_HARDWARE */ diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.c b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.c index 3182a12dd..2a4bea208 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.c +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.c @@ -60,6 +60,9 @@ static lv_obj_t *threshold_label; // Threshold value label static lv_obj_t *status_label; // Camera status label static lv_timer_t *update_timer; // Timer for updating display +// Lifecycle callback (not hardware-specific) +static camera_screen_lifecycle_cb_t sg_lifecycle_callback = NULL; + #ifdef ENABLE_LVGL_HARDWARE static uint8_t *canvas_buffer = NULL; // Canvas buffer for monochrome image static TDL_DISP_FRAME_BUFF_T *sg_p_display_fb = NULL; @@ -87,9 +90,6 @@ static BINARY_CONFIG_T sg_binary_config = { // Calculated threshold for adaptive and otsu methods static uint8_t sg_calculated_threshold = 128; -// Lifecycle callback -static camera_screen_lifecycle_cb_t sg_lifecycle_callback = NULL; - // Photo print callback static camera_photo_print_cb_t sg_print_callback = NULL; #endif @@ -129,18 +129,20 @@ void camera_screen_register_lifecycle_cb(camera_screen_lifecycle_cb_t callback) * @brief Register photo print callback for camera screen * @param callback Callback function, NULL to unregister */ +#ifdef ENABLE_LVGL_HARDWARE void camera_screen_register_print_cb(camera_photo_print_cb_t callback) { sg_print_callback = callback; printf("[Camera] Print callback %s\n", callback ? "registered" : "unregistered"); } +#endif /** * @brief Get method name string */ +#ifdef ENABLE_LVGL_HARDWARE static const char *get_method_name(BINARY_METHOD_E method) { -#ifdef ENABLE_LVGL_HARDWARE switch (method) { case BINARY_METHOD_FIXED: return "Fixed"; @@ -163,10 +165,10 @@ static const char *get_method_name(BINARY_METHOD_E method) default: return "Unknown"; } +} #else - return "N/A"; +static const char *get_method_name(int method) { (void)method; return "N/A"; } #endif -} /** * @brief Update info area display diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.h b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.h index 508f1031a..a1b3112a7 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.h +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/camera_screen.h @@ -25,7 +25,9 @@ extern "C" { #endif #include "screen_manager.h" +#ifdef ENABLE_LVGL_HARDWARE #include "yuv422_to_binary.h" +#endif /** * @brief Camera screen lifecycle callback type * Called when screen is initialized or deinitialized @@ -34,6 +36,7 @@ extern "C" { */ typedef void (*camera_screen_lifecycle_cb_t)(BOOL_T is_init); +#ifdef ENABLE_LVGL_HARDWARE /** * @brief Camera photo print callback type * Called when ENTER key is pressed to print current photo from raw YUV422 data @@ -41,6 +44,7 @@ typedef void (*camera_screen_lifecycle_cb_t)(BOOL_T is_init); * @note The yuv422_data buffer will be freed after callback returns */ typedef void (*camera_photo_print_cb_t)(const YUV422_TO_BINARY_PARAMS_T *params); +#endif extern Screen_t camera_screen; @@ -53,12 +57,14 @@ void camera_screen_deinit(void); */ void camera_screen_register_lifecycle_cb(camera_screen_lifecycle_cb_t callback); +#ifdef ENABLE_LVGL_HARDWARE /** * @brief Register photo print callback for camera screen * Called when ENTER key is pressed with current photo data * @param callback Callback function, NULL to unregister */ void camera_screen_register_print_cb(camera_photo_print_cb_t callback); +#endif #ifdef __cplusplus } /*extern "C"*/ diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/main_screen.c b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/main_screen.c index 79822a7ef..9fa43bf77 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/main_screen.c +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/main_screen.c @@ -31,7 +31,9 @@ #include "ebook_screen.h" #include "rfid_scan_screen.h" #include "camera_screen.h" +#ifdef ENABLE_LVGL_HARDWARE #include "game_pet.h" +#endif #include #include #include @@ -428,7 +430,7 @@ static void keyboard_event_cb(lv_event_t *e) printf("C key pressed - Setting battery to charging\n"); // screen_load(&standby_screen); // screen_load(&ebook_screen); - screen_load(&rfid_scan_screen); + // screen_load(&rfid_scan_screen); break; #endif default: @@ -1056,6 +1058,7 @@ void main_screen_set_pet_animation_state(ai_pet_state_t state) printf("[%s] Pet animation state changing: %d -> %d\n", main_screen.name, current_animation_state, state); +#ifdef ENABLE_LVGL_HARDWARE // Play sound effect based on pet state switch (state) { case AI_PET_STATE_SLEEP: @@ -1086,9 +1089,9 @@ void main_screen_set_pet_animation_state(ai_pet_state_t state) game_pet_play_alert(PET_ALERT_THREE_STAGE_UP_TONE); break; default: - // No sound for AI_PET_STATE_NORMAL break; } +#endif // Simply update the state variable - timer will handle GIF switching current_animation_state = state; diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.c b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.c index 5b090bf1a..4571d08b6 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.c +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.c @@ -22,7 +22,7 @@ /*********************************************************** ************************macro define************************ ***********************************************************/ - +#ifdef ENABLE_LVGL_HARDWARE // Screen dimensions #ifndef AI_PET_SCREEN_WIDTH #define AI_PET_SCREEN_WIDTH 384 @@ -534,4 +534,5 @@ void rfid_scan_screen_load(void) if (screen_get_now_screen() != &rfid_scan_screen) { screen_load(&rfid_scan_screen); } -} \ No newline at end of file +} +#endif // ENABLE_LVGL_HARDWARE \ No newline at end of file diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.h b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.h index 479a0a5ad..d89100601 100755 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.h +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/rfid_scan_screen.h @@ -23,6 +23,7 @@ extern "C" { #endif #include "screen_manager.h" +#ifdef ENABLE_LVGL_HARDWARE #include "rfid_scan.h" extern Screen_t rfid_scan_screen; @@ -46,4 +47,6 @@ void rfid_scan_screen_load(void); } /*extern "C"*/ #endif +#endif // ENABLE_LVGL_HARDWARE + #endif /*RFID_SCAN_SCREEN_H*/ \ No newline at end of file diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.c b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.c index 39b6dde8e..55fb27f9d 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.c +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.c @@ -215,3 +215,14 @@ void screens_init(void) printf("[Error]: startup_screen.screen_obj is NULL or invalid during initialization\n"); } } + +/** + * @brief Initialize the UI + * + * This function is a wrapper for the screens_init function. + * It is used to initialize the UI. + */ +void ui_init(void) +{ + screens_init(); +} \ No newline at end of file diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.h b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.h index 7fa6e71db..e4ac421c8 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.h +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display/ui/screen_manager.h @@ -19,12 +19,19 @@ #ifndef SCREEN_MANAGER_H #define SCREEN_MANAGER_H +#ifdef __cplusplus +extern "C" { +#endif + #include "../lvgl/lvgl.h" /*********************************************************** ************************macro define************************ ***********************************************************/ +#ifndef LVGL_PC_SIMULATOR #define ENABLE_LVGL_HARDWARE +#endif + #ifdef ENABLE_LVGL_HARDWARE #include "tuya_cloud_types.h" #include "tal_log.h" @@ -109,4 +116,16 @@ void screen_load(Screen_t *newScreen); */ void screens_init(void); -#endif // SCREEN_STACK_H +/** + * @brief Initialize the UI + * + * This function is a wrapper for the screens_init function. + * It is used to initialize the UI. + */ +void ui_init(void); + +#ifdef __cplusplus +} /*extern "C"*/ +#endif + +#endif // SCREEN_MANAGER_H diff --git a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/game_pet.c b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/game_pet.c index b3333f8f1..6d740ea8f 100644 --- a/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/game_pet.c +++ b/apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/game_pet.c @@ -409,7 +409,7 @@ static void __game_display_init(void) { lv_vendor_init(DISPLAY_NAME); - screens_init(); + ui_init(); lv_vendor_start(5, 1024*8); } diff --git a/examples/graphics/lvgl_simulator/CMakeLists.txt b/examples/graphics/lvgl_simulator/CMakeLists.txt new file mode 100644 index 000000000..51b031aa6 --- /dev/null +++ b/examples/graphics/lvgl_simulator/CMakeLists.txt @@ -0,0 +1,152 @@ +cmake_minimum_required(VERSION 3.14) +project(LVGLSimulator C) + +# Load app config — edit sim_config.cmake to switch apps +include(${CMAKE_CURRENT_SOURCE_DIR}/sim_config.cmake) + +# Locate TuyaOpen root (simulator lives at examples/graphics/lvgl_simulator/) +get_filename_component(TUYAOPEN_ROOT + "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) + +set(LVGL_DIR "${TUYAOPEN_ROOT}/src/liblvgl/v9") +set(PLATFORM_DIR "${TUYAOPEN_ROOT}/platform/LINUX/tuyaos_adapter") +set(TAL_DIR "${TUYAOPEN_ROOT}/src/tal_system") +set(COMMON_DIR "${TUYAOPEN_ROOT}/src/common") + +# Output executable to dist/ +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/dist) + +# ───────────────────────────────────────────────────────────────────────────── +# Generate main.c (inject entry function name and screen resolution) +# ───────────────────────────────────────────────────────────────────────────── +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/sim_main.c.in + ${CMAKE_BINARY_DIR}/sim_main.c + @ONLY +) + +# ───────────────────────────────────────────────────────────────────────────── +# Source file collection +# ───────────────────────────────────────────────────────────────────────────── + +# LVGL core sources (includes built-in SDL driver; embedded port files excluded) +file(GLOB_RECURSE LVGL_SRCS "${LVGL_DIR}/lvgl/src/*.c") + +# UI source files (SIM_UI_SRC_DIRS entries can be directories or individual .c file paths) +set(UI_SRCS "") +foreach(entry ${SIM_UI_SRC_DIRS}) + if(IS_DIRECTORY "${entry}") + file(GLOB_RECURSE _srcs "${entry}/*.c") + else() + set(_srcs "${entry}") + endif() + list(APPEND UI_SRCS ${_srcs}) +endforeach() + +# platform/LINUX utility library (linked lists, hash tables, etc.) +file(GLOB UTIL_SRCS "${PLATFORM_DIR}/include/utilities/src/*.c") + +# platform/LINUX TKL adapter layer (only the core modules required by TAL) +set(TKL_CORE_SRCS + ${PLATFORM_DIR}/src/tkl_memory.c + ${PLATFORM_DIR}/src/tkl_mutex.c + ${PLATFORM_DIR}/src/tkl_semaphore.c + ${PLATFORM_DIR}/src/tkl_queue.c + ${PLATFORM_DIR}/src/tkl_thread.c + ${PLATFORM_DIR}/src/tkl_system.c + ${PLATFORM_DIR}/src/tkl_output.c + ${PLATFORM_DIR}/src/tkl_fs.c + ${PLATFORM_DIR}/src/tkl_ota.c +) + +# TAL abstraction layer (tal_log.c provides PR_xxx macros; +# tal_event.c and tal_fs.c excluded — they depend on network/UART/KV headers) +set(TAL_SRCS + ${TAL_DIR}/src/tal_api.c + ${TAL_DIR}/src/tal_log.c + ${TAL_DIR}/src/tal_sleep.c + ${TAL_DIR}/src/tal_sw_timer.c + ${TAL_DIR}/src/tal_system.c + ${TAL_DIR}/src/tal_thread.c + ${TAL_DIR}/src/tal_time_serivce.c + ${TAL_DIR}/src/tal_workq_service.c + ${TAL_DIR}/src/tal_workqueue.c +) + +# ───────────────────────────────────────────────────────────────────────────── +# Executable target +# ───────────────────────────────────────────────────────────────────────────── +find_package(SDL2 REQUIRED) + +add_executable(lvgl_sim + ${CMAKE_BINARY_DIR}/sim_main.c + ${LVGL_SRCS} + ${UI_SRCS} + ${UTIL_SRCS} + ${TKL_CORE_SRCS} + ${TAL_SRCS} +) + +# ───────────────────────────────────────────────────────────────────────────── +# Compile-time definitions +# ───────────────────────────────────────────────────────────────────────────── + +# Convert SIM_APP_NAME to a valid C identifier and inject as a preprocessor macro +# (read by tuya_kconfig.h to enable per-app feature macros) +string(MAKE_C_IDENTIFIER "${SIM_APP_NAME}" _sim_app_id) + +target_compile_definitions(lvgl_sim PRIVATE + LVGL_PC_SIMULATOR # Master switch: identifies PC simulator environment + LV_CONF_INCLUDE_SIMPLE # Tell LVGL to locate its config via lv_conf.h + LV_LVGL_H_INCLUDE_SIMPLE # Tell LVGL internals to #include "lvgl.h" + OPERATING_SYSTEM=100 # SYSTEM_LINUX: activates the Linux branch in tuya_cloud_types.h + SIM_APP_${_sim_app_id} # Per-app identifier, read by tuya_kconfig.h +) + +# ───────────────────────────────────────────────────────────────────────────── +# Include paths (order matters: real headers before stub headers) +# ───────────────────────────────────────────────────────────────────────────── +target_include_directories(lvgl_sim PRIVATE + # 1. Real platform/LINUX headers (provide tuya_cloud_types.h, tkl_*.h, tal_*.h) + ${PLATFORM_DIR}/include/utilities/include # tuya_cloud_types.h and utility headers + ${PLATFORM_DIR}/include/system # tkl_*.h + ${TAL_DIR}/include # tal_*.h (including tal_log.h / PR_xxx) + ${COMMON_DIR}/include # tuya_error_code.h, tuya_iot_config.h + + # 2. Stub headers (shadow hardware-only SDK headers such as lv_vendor.h) + ${CMAKE_CURRENT_SOURCE_DIR}/include + + # 3. LVGL driver path trick: resolves "../lvgl/lvgl.h" relative to the driver directory + ${LVGL_DIR}/lvgl + + # 4. LVGL parent directory: resolves "lvgl/lvgl.h" (used by SquareLine Studio–generated code) + ${LVGL_DIR} + + # 5. LVGL config directory (contains lv_conf.h with LVGL_PC_SIMULATOR conditionals) + ${LVGL_DIR}/conf + + # 6. Directory of the generated main.c + ${CMAKE_BINARY_DIR} + + # 7. App-specific headers + ${SIM_UI_INC_DIRS} + + # 8. SDL2 + ${SDL2_INCLUDE_DIRS} +) + +target_link_libraries(lvgl_sim PRIVATE + ${SDL2_LIBRARIES} + m + pthread +) + +# ───────────────────────────────────────────────────────────────────────────── +# Status output +# ───────────────────────────────────────────────────────────────────────────── +message(STATUS "=== LVGL Simulator ===") +message(STATUS "App: ${SIM_APP_NAME}") +message(STATUS "Resolution: ${SIM_SCREEN_W}x${SIM_SCREEN_H}") +message(STATUS "Entry func: ${SIM_ENTRY_FUNC}()") +message(STATUS "UI sources: ${SIM_UI_SRC_DIRS}") +message(STATUS "Output: ${CMAKE_CURRENT_SOURCE_DIR}/dist/lvgl_sim") diff --git a/examples/graphics/lvgl_simulator/README_en.md b/examples/graphics/lvgl_simulator/README_en.md new file mode 100644 index 000000000..20038336b --- /dev/null +++ b/examples/graphics/lvgl_simulator/README_en.md @@ -0,0 +1,230 @@ +# LVGL PC Simulator + +Run any TuyaOpen App's LVGL UI on a desktop—no hardware required. UI source files are compiled directly into a native binary and rendered in an SDL2 window. + +The simulator also compiles the `platform/LINUX` TKL adapter layer and the `src/tal_system` TAL layer, so UI code can call `tal_*` / `tkl_*` interfaces natively and use `PR_xxx` macros for logging. + +--- + +## Directory Layout + +``` +lvgl_simulator/ +├── CMakeLists.txt # Build script (rarely needs editing) +├── sim_config.cmake # App config — the only file you need to edit to switch apps +├── sim_main.c.in # main.c template (generated into .build/ by cmake) +├── include/ # Stub headers (tuya_kconfig.h and other simulator-only stubs) +├── dist/ # Build output: lvgl_sim executable +└── .build/ # CMake intermediate files (safe to delete) +``` + +--- + +## Prerequisites + +### Linux + +```bash +# Ubuntu / Debian +sudo apt install cmake gcc libsdl2-dev + +# Fedora / RHEL +sudo dnf install cmake gcc SDL2-devel + +# Arch Linux +sudo pacman -S cmake gcc sdl2 +``` + +### macOS + +Install [Homebrew](https://brew.sh) if not already installed, then: + +```bash +brew install cmake sdl2 +``` + +--- + +## Build & Run + +### First build (or after changing sim_config.cmake / CMakeLists.txt) + +```bash +cd examples/graphics/lvgl_simulator + +cmake -B .build -DCMAKE_BUILD_TYPE=Debug +cmake --build .build -j$(nproc) + +./dist/lvgl_sim +``` + +### Incremental build (UI source files changed only) + +```bash +cmake --build .build -j$(nproc) +./dist/lvgl_sim +``` + +### Release build + +```bash +cmake -B .build -DCMAKE_BUILD_TYPE=Release +cmake --build .build -j$(nproc) +``` + +### Clean + +```bash +# Remove executable only +rm -f dist/lvgl_sim + +# Full clean — forces a complete rebuild next time +rm -rf .build dist +``` + +--- + +## Configuring sim_config.cmake + +`sim_config.cmake` is the single file you edit to switch between apps. All variables: + +```cmake +# Window title / app identifier (also used to derive the SIM_APP_ macro) +set(SIM_APP_NAME "tuya_t5_pocket_ai") + +# Screen resolution — match the target hardware +set(SIM_SCREEN_W 384) +set(SIM_SCREEN_H 168) + +# UI entry function — called by the simulator's main() +set(SIM_ENTRY_FUNC "ui_init") + +# UI source directories or individual .c file paths — compiled recursively for directories +set(SIM_UI_SRC_DIRS + "${CMAKE_SOURCE_DIR}/../../../apps/your_app/src/display" +) + +# Header search paths (no SDK paths needed; simulator stubs cover them) +set(SIM_UI_INC_DIRS + "${CMAKE_SOURCE_DIR}/../../../apps/your_app/include" + "${CMAKE_SOURCE_DIR}/../../../apps/your_app/src/expand/inc" +) +``` + +After editing `sim_config.cmake`, re-run `cmake -B .build` — an incremental build is not sufficient. + +### Predefined app configurations + +The file ships with four ready-to-use configurations (uncomment the one you want): + +| App name | Resolution | Entry function | Notes | +|----------|-----------|---------------|-------| +| `tuya_t5_pocket_ai` | 384×168 | `ui_init` | Tuya T5 pocket AI device (default active) | +| `your_chat_bot_wechat` | 480×320 | `ui_init` | Chat-bot — WeChat-style UI | +| `your_chat_bot_chatbot` | 480×320 | `ui_init` | Chat-bot — chatbot-style UI | + +--- + +## tuya_kconfig.h — Per-app Feature Macros + +`include/tuya_kconfig.h` is a simulator stub for the Kconfig-generated header that normally lives in the firmware build. It enables the feature macros that each app's UI code depends on. + +CMake converts `SIM_APP_NAME` to a C identifier and injects it as a compile-time macro: + +```cmake +string(MAKE_C_IDENTIFIER "${SIM_APP_NAME}" _sim_app_id) +# e.g. "your_chat_bot_wechat" → SIM_APP_your_chat_bot_wechat +target_compile_definitions(... SIM_APP_${_sim_app_id}) +``` + +`tuya_kconfig.h` can then selectively enable macros per app: + +```c +// Always enable common font sizes used across UIs +#define LV_FONT_MONTSERRAT_16 1 +#define LV_FONT_MONTSERRAT_22 1 +#define LV_FONT_MONTSERRAT_32 1 +#define LV_FONT_MONTSERRAT_48 1 + +``` + +Add new entries here whenever a new app requires feature macros that aren't defined elsewhere. + +--- + +## Macro Reference + +The simulator uses a set of compile-time macros to separate PC and hardware environments. + +### Core macros + +| Macro | When defined | Purpose | +|-------|-------------|---------| +| `LVGL_PC_SIMULATOR` | Injected by CMake during simulator build | Master switch — identifies the PC simulator environment | +| `ENABLE_LVGL_HARDWARE` | Defined by `screen_manager.h` on hardware | Identifies the real hardware environment (pocket project only) | +| `LV_CONF_INCLUDE_SIMPLE` | Injected by CMake | Tells LVGL to locate config via `lv_conf.h` | +| `LV_LVGL_H_INCLUDE_SIMPLE` | Injected by CMake | Tells LVGL internals to `#include "lvgl.h"` | +| `OPERATING_SYSTEM=100` | Injected by CMake | `SYSTEM_LINUX` — activates Linux branch in `tuya_cloud_types.h` | +| `SIM_APP_` | Injected by CMake | Per-app identifier for `tuya_kconfig.h` conditional macros | + +### lv_conf.h options affected by LVGL_PC_SIMULATOR + +| Option | Hardware value | Simulator value | Notes | +|--------|---------------|-----------------|-------| +| `LV_USE_STDLIB_MALLOC` | `LV_STDLIB_CUSTOM` | `LV_STDLIB_CLIB` | Embedded uses tlsf; PC uses system malloc | +| `LV_USE_SDL` | `0` | `1` | SDL2 display/input driver | +| `LV_USE_PNG` | `1` (Tuya custom) | `0` | Hardware PNG decoder (requires PSRAM, unavailable on PC) | +| `LV_USE_LODEPNG` | `0` | `1` | Pure-software PNG decoder used on PC | + +### Writing hardware guards in UI source files + +**Standard pattern — `#ifndef LVGL_PC_SIMULATOR`:** + +```c +// Hardware-only headers +#ifndef LVGL_PC_SIMULATOR +#include "tal_api.h" +#include "lv_vendor.h" +#endif + +// Hardware operation with simulator stub +#ifndef LVGL_PC_SIMULATOR + camera_hw_start(); +#else + printf("[SIM] camera stub\n"); +#endif +``` + +**Function-level stub pattern:** + +```c +OPERATE_RET ai_ui_register_intfs(void) +{ +#ifndef LVGL_PC_SIMULATOR + AI_UI_INTFS_T intfs = {0}; + intfs.disp_emotion = __ui_set_emotion; + intfs.disp_wifi_state = __ui_set_network; + return ai_ui_register(&intfs); +#else + return OPRT_OK; // simulator: no-op +#endif +} +``` + +--- + +## TAL/TKL Interfaces & PR_xxx Logging + +The simulator compiles the full `platform/LINUX` TKL adapter and `src/tal_system` TAL layer. UI code can use these interfaces directly: + +- **`PR_ERR` / `PR_WARN` / `PR_NOTICE` / `PR_INFO` / `PR_DEBUG` / `PR_TRACE`** — TAL log macros, output to stdout +- **`tal_mutex_*` / `tal_semaphore_*`** — thread synchronization backed by Linux pthread +- **`tal_thread_*`** — thread management +- **`tal_system_*`** — system information +- **`tal_log_*`** — logging system, initialized at `TAL_LOG_LEVEL_DEBUG` in `main()` + +--- + +## Keyboard Controls + +SDL keyboard events are mapped to LVGL key codes \ No newline at end of file diff --git a/examples/graphics/lvgl_simulator/README_zh.md b/examples/graphics/lvgl_simulator/README_zh.md new file mode 100644 index 000000000..bf9281ee3 --- /dev/null +++ b/examples/graphics/lvgl_simulator/README_zh.md @@ -0,0 +1,230 @@ +# LVGL PC 模拟器 + +在桌面上运行 TuyaOpen App 的 LVGL UI,无需烧录硬件。UI 源码直接编译为原生可执行文件,通过 SDL2 窗口渲染 LVGL 界面。 + +模拟器同时编译了 `platform/LINUX` 的 TKL 适配层和 `src/tal_system` 的 TAL 层,使得 UI 代码可以直接调用 `tal_*` / `tkl_*` 接口,并使用 `PR_xxx` 宏输出日志。 + +--- + +## 目录结构 + +``` +lvgl_simulator/ +├── CMakeLists.txt # 构建脚本(通常无需修改) +├── sim_config.cmake # App 配置文件(修改此文件切换 App) +├── sim_main.c.in # main.c 模板(cmake 时自动生成到 .build/) +├── include/ # Stub 头文件(tuya_kconfig.h 等模拟器专用桩) +├── dist/ # 编译产物:可执行文件 lvgl_sim +└── .build/ # CMake 中间产物(可安全删除) +``` + +--- + +## 依赖安装 + +### Linux + +```bash +# Ubuntu / Debian +sudo apt install build-essential cmake libsdl2-dev + +# Fedora / RHEL +sudo dnf install @development-tools cmake SDL2-devel + +# Arch Linux +sudo pacman -S base-devel cmake sdl2 +``` + +### macOS + +如未安装 [Homebrew](https://brew.sh),先安装,然后: + +```bash +brew install sdl2 cmake make +``` + +--- + +## 编译与运行 + +### 首次编译(或修改了 sim_config.cmake / CMakeLists.txt 后) + +```bash +cd examples/graphics/lvgl_simulator + +cmake -B .build -DCMAKE_BUILD_TYPE=Debug +cmake --build .build -j$(nproc) + +./dist/lvgl_sim +``` + +### 仅修改了 UI 源码后(增量编译) + +```bash +cmake --build .build -j$(nproc) +./dist/lvgl_sim +``` + +### Release 构建 + +```bash +cmake -B .build -DCMAKE_BUILD_TYPE=Release +cmake --build .build -j$(nproc) +``` + +### 清除编译产物 + +```bash +# 只清除可执行文件 +rm -f dist/lvgl_sim + +# 清除所有中间产物(完全重新编译) +rm -rf .build dist +``` + +--- + +## 修改配置文件 + +`sim_config.cmake` 是切换 App 的唯一入口,所有变量说明如下: + +```cmake +# 窗口标题 / App 标识符(同时用于派生 SIM_APP_ 宏) +set(SIM_APP_NAME "tuya_t5_pocket_ai") + +# 屏幕分辨率(与目标硬件保持一致) +set(SIM_SCREEN_W 384) +set(SIM_SCREEN_H 168) + +# UI 入口函数名(模拟器 main() 会调用此函数) +set(SIM_ENTRY_FUNC "ui_init") + +# UI 源码目录或单个 .c 文件路径;目录会递归搜索所有 .c 文件 +set(SIM_UI_SRC_DIRS + "${CMAKE_SOURCE_DIR}/../../../apps/your_app/src/display" +) + +# UI 头文件搜索路径(不含 SDK 路径,模拟器 stub 已覆盖) +set(SIM_UI_INC_DIRS + "${CMAKE_SOURCE_DIR}/../../../apps/your_app/include" + "${CMAKE_SOURCE_DIR}/../../../apps/your_app/src/expand/inc" +) +``` + +修改 `sim_config.cmake` 后,需要重新运行 `cmake -B .build` 才能生效(增量编译不够)。 + +### 预置 App 配置 + +文件中已内置四套配置(取消注释对应段落即可激活): + +| App 名称 | 分辨率 | 入口函数 | 说明 | +|---------|--------|---------|------| +| `tuya_t5_pocket_ai` | 384×168 | `ui_init` | Tuya T5 口袋 AI 设备(当前默认激活) | +| `your_chat_bot_wechat` | 480×320 | `ui_init` | 聊天机器人 — 微信风格 UI | +| `your_chat_bot_chatbot` | 480×320 | `ui_init` | 聊天机器人 — chatbot 风格 UI | + +--- + +## tuya_kconfig.h — 按 App 开启功能宏 + +`include/tuya_kconfig.h` 是 Kconfig 生成头文件的模拟器桩,用于开启各 App UI 代码所依赖的功能宏。 + +CMake 将 `SIM_APP_NAME` 转换为 C 标识符并注入为编译宏: + +```cmake +string(MAKE_C_IDENTIFIER "${SIM_APP_NAME}" _sim_app_id) +# 例:"your_chat_bot_wechat" → SIM_APP_your_chat_bot_wechat +target_compile_definitions(... SIM_APP_${_sim_app_id}) +``` + +`tuya_kconfig.h` 即可按 App 有条件地开启宏: + +```c +// 各 UI 通用字体大小,无条件开启 +#define LV_FONT_MONTSERRAT_16 1 +#define LV_FONT_MONTSERRAT_22 1 +#define LV_FONT_MONTSERRAT_32 1 +#define LV_FONT_MONTSERRAT_48 1 + +``` + +新增 App 时,若 UI 代码依赖额外的功能宏,在此处添加对应条件定义。 + +--- + +## 宏定义说明 + +模拟器通过一组编译宏区分 PC 模拟环境与真实硬件环境。 + +### 核心宏 + +| 宏 | 定义时机 | 作用 | +|----|---------|------| +| `LVGL_PC_SIMULATOR` | 模拟器编译时由 CMake 注入 | 总开关,标识当前为 PC 模拟器环境 | +| `ENABLE_LVGL_HARDWARE` | 硬件编译时由 `screen_manager.h` 定义 | 标识当前为真实硬件环境(仅 pocket 项目使用) | +| `LV_CONF_INCLUDE_SIMPLE` | 模拟器编译时注入 | 让 LVGL 以 `lv_conf.h` 方式查找配置头 | +| `LV_LVGL_H_INCLUDE_SIMPLE` | 模拟器编译时注入 | 让 LVGL 内部以 `"lvgl.h"` 方式 include | +| `OPERATING_SYSTEM=100` | 模拟器编译时注入 | `SYSTEM_LINUX`,激活 `tuya_cloud_types.h` 的 Linux 分支 | +| `SIM_APP_` | 由 CMake 注入 | 每个 App 的唯一标识,供 `tuya_kconfig.h` 条件开启功能宏 | + +### lv_conf.h 中受 LVGL_PC_SIMULATOR 影响的配置 + +| 配置项 | 硬件值 | 模拟器值 | 说明 | +|--------|--------|---------|------| +| `LV_USE_STDLIB_MALLOC` | `LV_STDLIB_CUSTOM` | `LV_STDLIB_CLIB` | 内存分配器:嵌入式用 tlsf,PC 用系统 malloc | +| `LV_USE_SDL` | `0` | `1` | SDL2 显示/输入驱动 | +| `LV_USE_PNG` | `1`(Tuya 定制) | `0` | 硬件 PNG 解码器(依赖 PSRAM,PC 不可用) | +| `LV_USE_LODEPNG` | `0` | `1` | 纯软件 PNG 解码器(PC 模拟器使用) | + +### 在 UI 源码中使用宏 + +**标准写法 — 使用 `#ifndef LVGL_PC_SIMULATOR`:** + +```c +// 硬件专属头文件 +#ifndef LVGL_PC_SIMULATOR +#include "tal_api.h" +#include "lv_vendor.h" +#endif + +// 硬件操作分支,模拟器提供默认实现 +#ifndef LVGL_PC_SIMULATOR + camera_hw_start(); +#else + printf("[SIM] camera stub\n"); +#endif +``` + +**函数级整体屏蔽写法:** + +```c +OPERATE_RET ai_ui_register_intfs(void) +{ +#ifndef LVGL_PC_SIMULATOR + AI_UI_INTFS_T intfs = {0}; + intfs.disp_emotion = __ui_set_emotion; + intfs.disp_wifi_state = __ui_set_network; + return ai_ui_register(&intfs); +#else + return OPRT_OK; // 模拟器:空实现 +#endif +} +``` + +--- + +## TAL/TKL 接口与 PR_xxx 日志 + +模拟器编译了完整的 `platform/LINUX` TKL 适配层和 `src/tal_system` TAL 层,UI 代码可以直接使用: + +- **`PR_ERR` / `PR_WARN` / `PR_NOTICE` / `PR_INFO` / `PR_DEBUG` / `PR_TRACE`** — TAL 日志宏,输出到 stdout +- **`tal_mutex_*` / `tal_semaphore_*`** — 线程同步,底层调用 Linux pthread +- **`tal_thread_*`** — 线程管理 +- **`tal_system_*`** — 系统信息 +- **`tal_log_*`** — 日志系统,已在 `main()` 中初始化为 `TAL_LOG_LEVEL_DEBUG` + +--- + +## 键盘控制 + +SDL 键盘事件映射到 LVGL 按键 diff --git a/examples/graphics/lvgl_simulator/include/tuya_kconfig.h b/examples/graphics/lvgl_simulator/include/tuya_kconfig.h new file mode 100644 index 000000000..360828fed --- /dev/null +++ b/examples/graphics/lvgl_simulator/include/tuya_kconfig.h @@ -0,0 +1,15 @@ +/* Simulator stub for Kconfig-generated header. + * App-specific feature macros are enabled here based on SIM_APP_ + * injected by CMake (see CMakeLists.txt target_compile_definitions). + */ +#ifndef TUYA_KCONFIG_H +#define TUYA_KCONFIG_H + +/* OPERATING_SYSTEM is injected by CMake as OPERATING_SYSTEM=100 (SYSTEM_LINUX) */ + +#define LV_FONT_MONTSERRAT_16 1 +#define LV_FONT_MONTSERRAT_22 1 +#define LV_FONT_MONTSERRAT_32 1 +#define LV_FONT_MONTSERRAT_48 1 + +#endif /* TUYA_KCONFIG_H */ diff --git a/examples/graphics/lvgl_simulator/sim_config.cmake b/examples/graphics/lvgl_simulator/sim_config.cmake new file mode 100644 index 000000000..657b5b0a9 --- /dev/null +++ b/examples/graphics/lvgl_simulator/sim_config.cmake @@ -0,0 +1,54 @@ +# ───────────────────────────────────────────────────────────────────────────── +# LVGL PC Simulator — App config file +# Modify this file to switch between different apps, other files do not need to be modified. +# After modification, you need to re-run cmake -B .build to take effect (incremental compilation is not enough). +# ───────────────────────────────────────────────────────────────────────────── + +# ── current active config: tuya_t5_pocket_ai ──────────────────────────────────── +# tuya_t5_pocket_ai (384×168): +set(SIM_APP_NAME "tuya_t5_pocket_ai") +set(SIM_SCREEN_W 384) +set(SIM_SCREEN_H 168) +set(SIM_ENTRY_FUNC "ui_init") +set(SIM_UI_SRC_DIRS + "${CMAKE_SOURCE_DIR}/../../../apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/display" +) +set(SIM_UI_INC_DIRS + "${CMAKE_SOURCE_DIR}/../../../apps/tuya_t5_pocket/tuya_t5_pocket_ai/include" + "${CMAKE_SOURCE_DIR}/../../../apps/tuya_t5_pocket/tuya_t5_pocket_ai/src/expand/inc" +) + +# ───────────────────────────────────────────────────────────────────────────── +# +# your_chat_bot / ui_wechat (480×320): +# set(SIM_APP_NAME "your_chat_bot_wechat") +# set(SIM_SCREEN_W 320) +# set(SIM_SCREEN_H 480) +# set(SIM_ENTRY_FUNC "ui_init") +# set(SIM_UI_SRC_DIRS +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2/ui_wechat" +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2/app_ui_helper.c" +# ) +# set(SIM_UI_INC_DIRS +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2" +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2/ui_wechat" +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/ai_components/assets/include" +# ) +# +# ───────────────────────────────────────────────────────────────────────────── +# your_chat_bot / ui_chatbot (480×320): +# set(SIM_APP_NAME "your_chat_bot_chatbot") +# set(SIM_SCREEN_W 480) +# set(SIM_SCREEN_H 320) +# set(SIM_ENTRY_FUNC "ui_init") +# set(SIM_UI_SRC_DIRS +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2/ui_chatbot" +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2/app_ui_helper.c" +# ) +# set(SIM_UI_INC_DIRS +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2" +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/your_chat_bot/src/display2/ui_chatbot" +# "${CMAKE_SOURCE_DIR}/../../../apps/tuya.ai/ai_components/assets/include" +# ) + +# ───────────────────────────────────────────────────────────────────────────── diff --git a/examples/graphics/lvgl_simulator/sim_main.c.in b/examples/graphics/lvgl_simulator/sim_main.c.in new file mode 100644 index 000000000..35e3d64c8 --- /dev/null +++ b/examples/graphics/lvgl_simulator/sim_main.c.in @@ -0,0 +1,39 @@ +/* Auto-generated by CMake — do not edit manually */ +#include "lvgl.h" +#include "src/drivers/sdl/lv_sdl_window.h" +#include "src/drivers/sdl/lv_sdl_mouse.h" +#include "src/drivers/sdl/lv_sdl_keyboard.h" +#include "tal_log.h" +#include "tkl_output.h" +#include +#include + +extern void @SIM_ENTRY_FUNC@(void); + +int main(void) +{ + /* Initialize TAL logging so PR_xxx macros produce output */ + tal_log_init(TAL_LOG_LEVEL_DEBUG, 4096, (TAL_LOG_OUTPUT_CB)tkl_log_output); + + lv_init(); + + lv_display_t *disp = lv_sdl_window_create(@SIM_SCREEN_W@, @SIM_SCREEN_H@); + lv_sdl_window_set_title(disp, "@SIM_APP_NAME@ [@SIM_SCREEN_W@x@SIM_SCREEN_H@]"); + + lv_sdl_mouse_create(); + + lv_indev_t *kb = lv_sdl_keyboard_create(); + lv_group_t *g = lv_group_create(); + lv_group_set_default(g); + lv_indev_set_group(kb, g); + + printf("[sim] Starting UI: @SIM_ENTRY_FUNC@()\n"); + @SIM_ENTRY_FUNC@(); + + while (1) { + uint32_t ms = lv_timer_handler(); + if (ms > 50) ms = 50; + usleep(ms * 1000); + } + return 0; +} diff --git a/src/liblvgl/CMakeLists.txt b/src/liblvgl/CMakeLists.txt index 534c9bfcb..e778fa075 100755 --- a/src/liblvgl/CMakeLists.txt +++ b/src/liblvgl/CMakeLists.txt @@ -8,6 +8,7 @@ if (CONFIG_ENABLE_LIBLVGL STREQUAL "y") add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/v8) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/v9) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/simulator) ######################################## # Layer Configure diff --git a/src/liblvgl/Kconfig b/src/liblvgl/Kconfig index 04969a68e..f60f26494 100755 --- a/src/liblvgl/Kconfig +++ b/src/liblvgl/Kconfig @@ -76,4 +76,7 @@ menuconfig ENABLE_LIBLVGL # Font configuration rsource "Fonts_Kconfig" + + # PC Simulator + rsource "simulator/Kconfig" endif \ No newline at end of file diff --git a/src/liblvgl/simulator/CMakeLists.txt b/src/liblvgl/simulator/CMakeLists.txt new file mode 100644 index 000000000..e13551c7b --- /dev/null +++ b/src/liblvgl/simulator/CMakeLists.txt @@ -0,0 +1,33 @@ +## +# @file CMakeLists.txt +# @brief LVGL PC simulator component +#/ + +if (CONFIG_LVGL_PC_SIMULATOR STREQUAL "y") + +set(MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +set(MODULE_NAME "lvgl_simulator") + +set(LIB_SRCS ${MODULE_PATH}/src/lvgl_sim.c) +set(LIB_PUBLIC_INC ${MODULE_PATH}/include) + +add_library(${MODULE_NAME}) + +target_sources(${MODULE_NAME} PRIVATE ${LIB_SRCS}) +target_include_directories(${MODULE_NAME} PUBLIC ${LIB_PUBLIC_INC}) + +target_compile_definitions(${MODULE_NAME} + PUBLIC + LVGL_PC_SIMULATOR + LVGL_SIM_ENTRY_FUNC=${CONFIG_SIM_ENTRY_FUNC} +) + +######################################## +# Layer Configure +######################################## +list(APPEND COMPONENT_LIBS ${MODULE_NAME}) +set(COMPONENT_LIBS "${COMPONENT_LIBS}" PARENT_SCOPE) +list(APPEND COMPONENT_PUBINC ${LIB_PUBLIC_INC}) +set(COMPONENT_PUBINC "${COMPONENT_PUBINC}" PARENT_SCOPE) + +endif() diff --git a/src/liblvgl/simulator/Kconfig b/src/liblvgl/simulator/Kconfig new file mode 100644 index 000000000..746534ed8 --- /dev/null +++ b/src/liblvgl/simulator/Kconfig @@ -0,0 +1,30 @@ +menuconfig LVGL_PC_SIMULATOR + bool "LVGL PC Simulator (SDL)" + depends on ENABLE_LIBLVGL && PLATFORM_LINUX + default n + help + Build as PC simulator with SDL2 window. + Only display/UI code will be compiled. + +if LVGL_PC_SIMULATOR + + config SIM_SCREEN_WIDTH + int "Screen width" + default 384 + + config SIM_SCREEN_HEIGHT + int "Screen height" + default 168 + + config SIM_WINDOW_TITLE + string "Window title" + default "TuyaOpen LVGL Simulator" + + config SIM_ENTRY_FUNC + string "UI entry function name" + default "ui_init" + help + The function called after LVGL init to create UI. + Must be provided by the app display code. + +endif diff --git a/src/liblvgl/simulator/include/lvgl_sim.h b/src/liblvgl/simulator/include/lvgl_sim.h new file mode 100644 index 000000000..d5ec77d48 --- /dev/null +++ b/src/liblvgl/simulator/include/lvgl_sim.h @@ -0,0 +1,34 @@ +/** + * @file lvgl_sim.h + * @brief LVGL PC simulator interface + * @version 1.0 + * @date 2025-04-30 + * @copyright Copyright (c) Tuya Inc. + */ +#ifndef __LVGL_SIM_H__ +#define __LVGL_SIM_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief UI entry callback type + */ +typedef void (*LVGL_SIM_ENTRY_CB)(void); + +/** + * @brief Start LVGL simulator with SDL2 + * + * Init LVGL, create SDL window/mouse/keyboard, call entry_cb, + * then run lv_timer_handler loop. Does not return. + * + * @param[in] entry_cb UI init function (e.g. ui_init) + * @return none + */ +void lvgl_sim_start(LVGL_SIM_ENTRY_CB entry_cb); + +#ifdef __cplusplus +} +#endif +#endif /* __LVGL_SIM_H__ */ diff --git a/src/liblvgl/simulator/src/lvgl_sim.c b/src/liblvgl/simulator/src/lvgl_sim.c new file mode 100644 index 000000000..40f109966 --- /dev/null +++ b/src/liblvgl/simulator/src/lvgl_sim.c @@ -0,0 +1,69 @@ +/** + * @file lvgl_sim.c + * @brief LVGL PC simulator — SDL2 bootstrap, entry point, main loop + * @version 1.0 + * @date 2025-04-30 + * @copyright Copyright (c) Tuya Inc. + */ +#include "lvgl_sim.h" +#include "lvgl.h" +#include "src/drivers/sdl/lv_sdl_window.h" +#include "src/drivers/sdl/lv_sdl_mouse.h" +#include "src/drivers/sdl/lv_sdl_keyboard.h" +#include + +#ifndef SIM_SCREEN_WIDTH +#define SIM_SCREEN_WIDTH 384 +#endif + +#ifndef SIM_SCREEN_HEIGHT +#define SIM_SCREEN_HEIGHT 168 +#endif + +#ifndef SIM_WINDOW_TITLE +#define SIM_WINDOW_TITLE "TuyaOpen LVGL Simulator" +#endif + +/** + * @brief Start LVGL simulator with SDL2 + * @param[in] entry_cb UI init function + * @return none + */ +void lvgl_sim_start(LVGL_SIM_ENTRY_CB entry_cb) +{ + lv_init(); + + lv_display_t *disp = lv_sdl_window_create(SIM_SCREEN_WIDTH, + SIM_SCREEN_HEIGHT); + lv_sdl_window_set_title(disp, SIM_WINDOW_TITLE); + lv_sdl_mouse_create(); + + lv_indev_t *kb = lv_sdl_keyboard_create(); + lv_group_t *g = lv_group_create(); + lv_group_set_default(g); + lv_indev_set_group(kb, g); + + if (entry_cb) { + entry_cb(); + } + + while (1) { + uint32_t ms = lv_timer_handler(); + if (ms > 50) { + ms = 50; + } + usleep(ms * 1000); + } +} + +/* LVGL_SIM_ENTRY_FUNC is injected via compile definition (e.g. ui_init) */ +extern void LVGL_SIM_ENTRY_FUNC(void); + +/** + * @brief Simulator entry point — provides main() for Linux platform + */ +int main(void) +{ + lvgl_sim_start(LVGL_SIM_ENTRY_FUNC); + return 0; +} diff --git a/src/liblvgl/v9/conf/lv_conf.h b/src/liblvgl/v9/conf/lv_conf.h index d3c09a172..6de5182ad 100755 --- a/src/liblvgl/v9/conf/lv_conf.h +++ b/src/liblvgl/v9/conf/lv_conf.h @@ -49,7 +49,11 @@ * - LV_STDLIB_RTTHREAD: RT-Thread implementation * - LV_STDLIB_CUSTOM: Implement the functions externally */ -#define LV_USE_STDLIB_MALLOC LV_STDLIB_CUSTOM +#ifdef LVGL_PC_SIMULATOR + #define LV_USE_STDLIB_MALLOC LV_STDLIB_CLIB +#else + #define LV_USE_STDLIB_MALLOC LV_STDLIB_CUSTOM +#endif #define LV_USE_STDLIB_STRING LV_STDLIB_BUILTIN #define LV_USE_STDLIB_SPRINTF LV_STDLIB_BUILTIN @@ -96,7 +100,7 @@ * - LV_OS_WINDOWS * - LV_OS_CUSTOM */ #if defined(ENABLE_LVGL_OS_FREERTOS) && (ENABLE_LVGL_OS_FREERTOS == 1) -#define LV_USE_OS LV_OS_FREERTOS +// #define LV_USE_OS LV_OS_FREERTOS #else #define LV_USE_OS LV_OS_NONE #endif @@ -754,15 +758,21 @@ /*PNG decoder library*/ // Modified by TUYA Start +#ifndef LVGL_PC_SIMULATOR #define LV_USE_PNG 1 #if LV_USE_PNG #define LV_PNG_USE_PSRAM 1 /* 0: use default sram memory, 1: use psram memory. */ #endif +#else +#define LV_USE_PNG 0 +#endif // Modified by TUYA End /*LODEPNG decoder library*/ #ifdef ENABLE_LVGL_LODEPNG #define LV_USE_LODEPNG ENABLE_LVGL_LODEPNG +#elif defined(LVGL_PC_SIMULATOR) +#define LV_USE_LODEPNG 1 #else #define LV_USE_LODEPNG 0 #endif @@ -966,7 +976,11 @@ *==================*/ /*Use SDL to open window on PC and handle mouse and keyboard*/ -#define LV_USE_SDL 0 +#ifdef LVGL_PC_SIMULATOR + #define LV_USE_SDL 1 +#else + #define LV_USE_SDL 0 +#endif #if LV_USE_SDL #define LV_SDL_INCLUDE_PATH #define LV_SDL_RENDER_MODE LV_DISPLAY_RENDER_MODE_DIRECT /*LV_DISPLAY_RENDER_MODE_DIRECT is recommended for best performance*/ diff --git a/src/liblvgl/v9/lvgl/src/core/lv_obj_event.c b/src/liblvgl/v9/lvgl/src/core/lv_obj_event.c index f92654b55..1fd336fed 100755 --- a/src/liblvgl/v9/lvgl/src/core/lv_obj_event.c +++ b/src/liblvgl/v9/lvgl/src/core/lv_obj_event.c @@ -98,8 +98,10 @@ lv_event_dsc_t * lv_obj_add_event_cb(lv_obj_t * obj, lv_event_cb_t event_cb, lv_ lv_obj_allocate_spec_attr(obj); // Modified by TUYA Start +#ifndef LVGL_PC_SIMULATOR void lvMsgEventReg(lv_obj_t *obj, lv_event_code_t eventCode); lvMsgEventReg(obj, filter); +#endif // Modified by TUYA End return lv_event_add(&obj->spec_attr->event_list, event_cb, filter, user_data); diff --git a/src/liblvgl/v9/lvgl/src/core/lv_obj_tree.c b/src/liblvgl/v9/lvgl/src/core/lv_obj_tree.c index b8fcfb90c..16f951fcc 100755 --- a/src/liblvgl/v9/lvgl/src/core/lv_obj_tree.c +++ b/src/liblvgl/v9/lvgl/src/core/lv_obj_tree.c @@ -546,8 +546,10 @@ static void obj_delete_core(lv_obj_t * obj) // Modified by TUYA Start //del self event obj +#ifndef LVGL_PC_SIMULATOR void lvMsgEventDel(lv_obj_t *obj); lvMsgEventDel(obj); +#endif // Modified by TUYA End /*All children deleted. Now clean up the object specific data*/ diff --git a/src/liblvgl/v9/lvgl/src/misc/lv_timer.c b/src/liblvgl/v9/lvgl/src/misc/lv_timer.c index a03ac01b1..78dfe8715 100755 --- a/src/liblvgl/v9/lvgl/src/misc/lv_timer.c +++ b/src/liblvgl/v9/lvgl/src/misc/lv_timer.c @@ -65,8 +65,10 @@ LV_ATTRIBUTE_TIMER_HANDLER uint32_t lv_timer_handler(void) LV_TRACE_TIMER("begin"); // Modified by TUYA Start +#ifndef LVGL_PC_SIMULATOR void lvMsgHandle(void); lvMsgHandle(); +#endif // Modified by TUYA End lv_timer_state_t * state_p = &state; diff --git a/src/liblvgl/v9/lvgl/src/stdlib/lv_mem.c b/src/liblvgl/v9/lvgl/src/stdlib/lv_mem.c index ac7939c41..5c0cc91b2 100755 --- a/src/liblvgl/v9/lvgl/src/stdlib/lv_mem.c +++ b/src/liblvgl/v9/lvgl/src/stdlib/lv_mem.c @@ -154,7 +154,7 @@ lv_result_t lv_mem_test(void) return LV_RESULT_INVALID; } -#if LV_USE_STDLIB_MALLOC != LV_STDLIB_CUSTOM +#if LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN if(lv_tlsf_check(tlsf)) { LV_LOG_WARN("failed"); return LV_RESULT_INVALID; @@ -173,7 +173,7 @@ void lv_mem_monitor(lv_mem_monitor_t * mon_p) { lv_memzero(mon_p, sizeof(lv_mem_monitor_t)); -#if LV_USE_STDLIB_MALLOC != LV_STDLIB_CUSTOM +#if LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN lv_mem_monitor_core(mon_p); #endif }