From 5cdbbf127e0027722c77b6d4723a8a9040564612 Mon Sep 17 00:00:00 2001 From: Forairaaaaa Date: Tue, 17 Mar 2026 16:21:05 +0800 Subject: [PATCH] update firmware source code to v0.18 --- firmware/CMakeLists.txt | 4 +- firmware/dependencies.lock | 100 +- firmware/main/CMakeLists.txt | 54 +- firmware/main/Kconfig.projbuild | 75 +- .../main/apps/app_ai_agent/app_ai_agent.cpp | 6 +- firmware/main/apps/app_avatar/app_avatar.cpp | 76 +- firmware/main/apps/app_avatar/app_avatar.h | 11 - .../apps/app_espnow_ctrl/app_espnow_ctrl.cpp | 113 +- .../apps/app_espnow_ctrl/app_espnow_ctrl.h | 4 + .../apps/app_espnow_ctrl/view/page_selector.h | 20 + .../main/apps/app_espnow_ctrl/view/view.h | 92 + .../main/apps/app_launcher/app_launcher.cpp | 36 +- .../main/apps/app_launcher/app_launcher.h | 6 +- firmware/main/apps/app_launcher/view/view.cpp | 191 +- firmware/main/apps/app_setup/app_setup.cpp | 70 +- firmware/main/apps/app_setup/app_setup.h | 5 +- firmware/main/apps/app_setup/view/view.cpp | 7 +- .../main/apps/app_setup/workers/about.cpp | 296 ++ .../apps/app_setup/workers/connectivity.cpp | 132 +- .../main/apps/app_setup/workers/display.cpp | 80 + .../main/apps/app_setup/workers/servo.cpp | 248 +- .../main/apps/app_setup/workers/startup.cpp | 83 + .../main/apps/app_setup/workers/system.cpp | 225 + .../main/apps/app_setup/workers/workers.h | 153 +- firmware/main/apps/common/common.h | 7 +- .../common/home_indicator/home_indicator.cpp | 283 ++ .../common/home_indicator/home_indicator.h | 18 + .../apps/common/loading_page/loading_page.h | 44 + .../main/apps/common/reminder/reminder.cpp | 132 + firmware/main/apps/common/reminder/reminder.h | 27 + .../apps/common/reminder/reminder_view.hpp | 99 + .../apps/common/status_bar/status_bar.cpp | 425 ++ .../main/apps/common/status_bar/status_bar.h | 18 + firmware/main/apps/common/toast/toast.cpp | 229 + firmware/main/apps/common/toast/toast.h | 93 + firmware/main/assets/assets.cpp | 68 + firmware/main/assets/assets.h | 23 + firmware/main/assets/images/icon_app_store.c | 4488 +++++++++++++++++ .../main/assets/images/icon_bat_lightning.c | 68 + firmware/main/assets/images/icon_bell.c | 126 + firmware/main/assets/images/icon_dance.c | 4488 +++++++++++++++++ firmware/main/assets/images/icon_ezdata.c | 4488 +++++++++++++++++ firmware/main/assets/images/icon_home.c | 196 + firmware/main/assets/images/icon_wifi_high.c | 99 + firmware/main/assets/images/icon_wifi_low.c | 99 + .../main/assets/images/icon_wifi_medium.c | 103 + firmware/main/assets/images/icon_wifi_slash.c | 103 + .../images/setup_stackchan_front_view.c | 3311 ++++++++++++ firmware/main/assets/sfx/convert_cmd.sh | 1 + firmware/main/assets/sfx/new_notification.ogg | Bin 0 -> 6935 bytes firmware/main/hal/board/hal_bridge.cc | 11 +- firmware/main/hal/board/hal_bridge.h | 10 +- firmware/main/hal/board/stackchan.cc | 123 +- firmware/main/hal/board/stackchan_camera.cc | 191 +- firmware/main/hal/board/stackchan_display.cc | 43 +- firmware/main/hal/board/stackchan_display.h | 1 + .../drivers/PCF8563_Class/PCF8563_Class.cpp | 271 + .../drivers/PCF8563_Class/PCF8563_Class.hpp | 64 + .../PY32IOExpander_Class.cpp | 5 + .../PY32IOExpander_Class.hpp | 5 + firmware/main/hal/hal.cpp | 139 +- firmware/main/hal/hal.h | 102 +- firmware/main/hal/hal_ble.cpp | 106 +- firmware/main/hal/hal_espnow.cpp | 6 +- firmware/main/hal/hal_head_touch.cpp | 10 +- firmware/main/hal/hal_imu.cpp | 13 +- firmware/main/hal/hal_io_expander.cpp | 2 +- firmware/main/hal/hal_mcp.cpp | 145 +- firmware/main/hal/hal_network.cpp | 131 + firmware/main/hal/hal_rtc.cpp | 126 + firmware/main/hal/hal_servo.cpp | 125 +- firmware/main/hal/hal_ws_avatar.cpp | 168 +- firmware/main/hal/utils/bleprph/bleprph.h | 12 +- firmware/main/hal/utils/bleprph/gatt_svr.c | 59 +- .../utils/motion_detector/motion_detector.h | 77 +- firmware/main/hal/utils/ota/ota.c | 189 + firmware/main/hal/utils/ota/ota.h | 16 + firmware/main/hal/utils/reminder/reminder.cpp | 160 - firmware/main/hal/utils/reminder/reminder.h | 74 - .../hal/utils/secret_logic/secret_logic.cpp | 25 + .../hal/utils/secret_logic/secret_logic.h | 16 + .../simple_audio_player.cpp | 211 - .../simple_audio_player/simple_audio_player.h | 46 - .../hal/utils/wifi_connect/wifi_station.cc | 10 +- .../hal/utils/wifi_connect/wifi_station.h | 13 +- firmware/main/idf_component.yml | 19 +- firmware/main/main.cpp | 14 + .../addons/neon_light/neon_light.cpp | 99 + .../stackchan/addons/neon_light/neon_light.h | 80 + .../main/stackchan/animation/animation.cpp | 4 + firmware/main/stackchan/animation/animation.h | 4 + .../main/stackchan/avatar/avatar/avatar.h | 12 + .../avatar/avatar/elements/element.h | 2 +- .../stackchan/avatar/decorators/angry.cpp | 61 +- .../decorators/assets/decorator_dizzy.c | 233 + .../avatar/decorators/assets/decorator_shy.c | 145 + .../stackchan/avatar/decorators/decorators.h | 127 +- .../stackchan/avatar/decorators/dizzy.cpp | 127 + .../stackchan/avatar/decorators/heart.cpp | 61 +- .../main/stackchan/avatar/decorators/shy.cpp | 100 + .../stackchan/avatar/decorators/sweat.cpp | 77 +- firmware/main/stackchan/json/json_helper.cpp | 41 + firmware/main/stackchan/json/json_helper.h | 5 + firmware/main/stackchan/modifiable.h | 7 + firmware/main/stackchan/modifiers/blink.h | 95 +- firmware/main/stackchan/modifiers/breath.h | 131 +- firmware/main/stackchan/modifiers/head_pet.h | 165 +- .../stackchan/modifiers/idle_expression.h | 93 + .../main/stackchan/modifiers/idle_motion.h | 134 +- firmware/main/stackchan/modifiers/imu.h | 143 +- firmware/main/stackchan/modifiers/modifiers.h | 1 + firmware/main/stackchan/modifiers/speaking.h | 163 +- firmware/main/stackchan/modifiers/timed.h | 62 +- firmware/main/stackchan/motion/motion.cpp | 10 + firmware/main/stackchan/motion/motion.h | 4 + firmware/main/stackchan/motion/servo.cpp | 11 +- firmware/main/stackchan/motion/servo.h | 19 +- firmware/main/stackchan/stackchan.h | 33 +- firmware/main/stackchan/utils/timer.h | 165 - firmware/partitions.csv | 9 + firmware/repos.json | 2 +- firmware/scan_secrets.py | 191 + firmware/sdkconfig.defaults | 6 +- 123 files changed, 24587 insertions(+), 1896 deletions(-) create mode 100644 firmware/main/apps/app_espnow_ctrl/view/view.h create mode 100644 firmware/main/apps/app_setup/workers/about.cpp create mode 100644 firmware/main/apps/app_setup/workers/display.cpp create mode 100644 firmware/main/apps/app_setup/workers/startup.cpp create mode 100644 firmware/main/apps/app_setup/workers/system.cpp create mode 100644 firmware/main/apps/common/home_indicator/home_indicator.cpp create mode 100644 firmware/main/apps/common/home_indicator/home_indicator.h create mode 100644 firmware/main/apps/common/loading_page/loading_page.h create mode 100644 firmware/main/apps/common/reminder/reminder.cpp create mode 100644 firmware/main/apps/common/reminder/reminder.h create mode 100644 firmware/main/apps/common/reminder/reminder_view.hpp create mode 100644 firmware/main/apps/common/status_bar/status_bar.cpp create mode 100644 firmware/main/apps/common/status_bar/status_bar.h create mode 100644 firmware/main/apps/common/toast/toast.cpp create mode 100644 firmware/main/apps/common/toast/toast.h create mode 100644 firmware/main/assets/assets.cpp create mode 100644 firmware/main/assets/images/icon_app_store.c create mode 100644 firmware/main/assets/images/icon_bat_lightning.c create mode 100644 firmware/main/assets/images/icon_bell.c create mode 100644 firmware/main/assets/images/icon_dance.c create mode 100644 firmware/main/assets/images/icon_ezdata.c create mode 100644 firmware/main/assets/images/icon_home.c create mode 100644 firmware/main/assets/images/icon_wifi_high.c create mode 100644 firmware/main/assets/images/icon_wifi_low.c create mode 100644 firmware/main/assets/images/icon_wifi_medium.c create mode 100644 firmware/main/assets/images/icon_wifi_slash.c create mode 100644 firmware/main/assets/images/setup_stackchan_front_view.c create mode 100644 firmware/main/assets/sfx/convert_cmd.sh create mode 100644 firmware/main/assets/sfx/new_notification.ogg create mode 100644 firmware/main/hal/drivers/PCF8563_Class/PCF8563_Class.cpp create mode 100644 firmware/main/hal/drivers/PCF8563_Class/PCF8563_Class.hpp create mode 100644 firmware/main/hal/hal_network.cpp create mode 100644 firmware/main/hal/hal_rtc.cpp create mode 100644 firmware/main/hal/utils/ota/ota.c create mode 100644 firmware/main/hal/utils/ota/ota.h delete mode 100644 firmware/main/hal/utils/reminder/reminder.cpp delete mode 100644 firmware/main/hal/utils/reminder/reminder.h create mode 100644 firmware/main/hal/utils/secret_logic/secret_logic.cpp create mode 100644 firmware/main/hal/utils/secret_logic/secret_logic.h delete mode 100644 firmware/main/hal/utils/simple_audio_player/simple_audio_player.cpp delete mode 100644 firmware/main/hal/utils/simple_audio_player/simple_audio_player.h create mode 100644 firmware/main/stackchan/addons/neon_light/neon_light.cpp create mode 100644 firmware/main/stackchan/addons/neon_light/neon_light.h create mode 100644 firmware/main/stackchan/avatar/decorators/assets/decorator_dizzy.c create mode 100644 firmware/main/stackchan/avatar/decorators/assets/decorator_shy.c create mode 100644 firmware/main/stackchan/avatar/decorators/dizzy.cpp create mode 100644 firmware/main/stackchan/avatar/decorators/shy.cpp create mode 100644 firmware/main/stackchan/modifiers/idle_expression.h delete mode 100644 firmware/main/stackchan/utils/timer.h create mode 100644 firmware/partitions.csv create mode 100644 firmware/scan_secrets.py diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index b9e0ac2..b5041a9 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -2,10 +2,10 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) -set(PROJECT_VER "2.0.4") +set(PROJECT_VER "2.1.0") # Add this line to disable the specific warning add_compile_options(-Wno-missing-field-initializers) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(StackChan) +project(stack-chan) diff --git a/firmware/dependencies.lock b/firmware/dependencies.lock index 2b502ab..0acf4c4 100644 --- a/firmware/dependencies.lock +++ b/firmware/dependencies.lock @@ -1,6 +1,6 @@ dependencies: 78/esp-ml307: - component_hash: 57c8cc3309fb3da9195ec861cba3726b5cf24c362def803e7174c0b1d72e60e3 + component_hash: d4de9738baaaa5e6655c686a6083e699f3605de54dc83233ff3328cf55a4f543 dependencies: - name: idf require: private @@ -8,7 +8,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 3.3.7 + version: 3.5.3 78/esp-opus: component_hash: 8182b733f071d7bfe1e837f4c9f8649a63e4c937177f089e65772880c02f2e17 dependencies: @@ -34,7 +34,7 @@ dependencies: type: service version: 2.4.1 78/esp-wifi-connect: - component_hash: 2613d9970df9f2203c2c549991e3bad74847b0a7facffe6698f4341e0bf73589 + component_hash: bf996fc603af1ff529668c788c7b38a8d94b1d43455af56d116d27ebb04b742a dependencies: - name: idf require: private @@ -42,7 +42,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 2.6.1 + version: 3.0.2 78/esp_lcd_nv3023: component_hash: fa88abfc19a312eb5e6f2ffa187e0a9faf67e01e758bfb979d3f9d92561a494f dependencies: @@ -58,7 +58,7 @@ dependencies: type: service version: 1.0.0 78/xiaozhi-fonts: - component_hash: 51f67bcf9dec623de90d9deb72de50dfbc3634fb948b7abf27df65b386258b94 + component_hash: e5526ea3c290742963ef6ab64023669260b72aefdc37bb218c34c618962ef2a8 dependencies: - name: idf require: private @@ -66,7 +66,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 1.5.4 + version: 1.5.5 espressif/adc_battery_estimation: component_hash: b915167c87ed5a84b13d680bd011c2ed9a15121f1e247c6903141b9f138f3606 dependencies: @@ -99,8 +99,26 @@ dependencies: registry_url: https://components.espressif.com/ type: service version: 0.2.1 + espressif/bmi270_sensor: + component_hash: fac23a5ecd2effdac0fb0705988d853745b8ffb99a384f87a307b3b25d15d432 + dependencies: + - name: espressif/cmake_utilities + registry_url: https://components.espressif.com + require: private + version: 0.* + - name: espressif/i2c_bus + registry_url: https://components.espressif.com + require: public + version: '*' + - name: idf + require: private + version: '>=5.3' + source: + registry_url: https://components.espressif.com/ + type: service + version: 0.1.1 espressif/button: - component_hash: 4ed25fcf354b48aa5e5680b9e7549e38d7738f9b613c94a2e39119c100db9c95 + component_hash: fccb18c37f1cfe0797b74a53a44d3f400f5fd01f4993b40052dfb7f401915089 dependencies: - name: espressif/cmake_utilities registry_url: https://components.espressif.com @@ -112,7 +130,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 4.1.4 + version: 4.1.5 espressif/cmake_utilities: component_hash: 351350613ceafba240b761b4ea991e0f231ac7a9f59a9ee901f751bddc0bb18f dependencies: @@ -365,19 +383,19 @@ dependencies: type: service version: 1.2.1 espressif/esp_lcd_touch_cst816s: - component_hash: 51dfa9cee38d8a20f4b625d7f89120dff08c8557e613e09dfb31f53d48648235 + component_hash: 6b11c489952c121396d4eaf5804b45e3f1f8dcd2c548dc6f2cbedf68622b013d dependencies: - name: espressif/esp_lcd_touch registry_url: https://components.espressif.com require: public - version: ^1.0.4 + version: ^1.2.0 - name: idf require: private version: '>=4.4.2' source: registry_url: https://components.espressif.com/ type: service - version: 1.1.0 + version: 1.1.1 espressif/esp_lcd_touch_ft5x06: component_hash: abaec05f46a793549b60afdda9eff86e3c8e87782c8c169007911232388d2858 dependencies: @@ -393,39 +411,36 @@ dependencies: type: service version: 1.0.7 espressif/esp_lcd_touch_gt1151: - component_hash: 69dd86cd5fad2442e3bf15c8c4ddddbe9e8739bdb26cd5f2c5249601b4c3cc38 + component_hash: acd0fb87e8b58dc90cebda8438ee31c068975c15c040a62723843ca0420d38b6 dependencies: - - name: idf - require: private - version: '>=4.4.2' - name: espressif/esp_lcd_touch registry_url: https://components.espressif.com require: public - version: ^1.0.4 + version: ^1.2.0 + - name: idf + require: private + version: '>=4.4.2' source: registry_url: https://components.espressif.com/ type: service - version: 1.0.5~2 + version: 1.1.0 espressif/esp_lcd_touch_gt911: - component_hash: acc1c184358aa29ef72506f618c9c76a8cc2bf12af38a2bff3d44d84f3a08857 + component_hash: be02e243d18b9a661bc13b0d22c0a5cfa3f708cf04d6eb059772276c8c8a4d76 dependencies: - name: espressif/esp_lcd_touch registry_url: https://components.espressif.com require: public - version: ^1.1.0 + version: ^1.2.0 - name: idf require: private version: '>=4.4.2' source: registry_url: https://components.espressif.com/ type: service - version: 1.1.3 + version: 1.2.0~1 espressif/esp_lcd_touch_st7123: - component_hash: 3592f0dac4b3baf34c40679922026c1e22e9b925b9446e981cd1c23306ecfe65 + component_hash: 717080a55368a5ca3e3704081a75ddab3e0b8884e335e73b7094beb8ae55f195 dependencies: - - name: idf - require: private - version: '>=4.4.2' - name: espressif/cmake_utilities registry_url: https://components.espressif.com require: private @@ -434,12 +449,15 @@ dependencies: registry_url: https://components.espressif.com require: public version: ^1 + - name: idf + require: private + version: '>=4.4.2' source: registry_url: https://components.espressif.com/ type: service - version: 1.0.0 + version: 1.0.1 espressif/esp_lvgl_port: - component_hash: bfb2778c063b05a6c47c1a8de6d166ee4d911bc86fe0b3ff9e6bab06d2812033 + component_hash: 3264cc85d86a7c32b67d26a1353a57c67e7986bb293ebb1731801936b056e632 dependencies: - name: idf require: private @@ -451,7 +469,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 2.6.2 + version: 2.6.3 espressif/esp_mmap_assets: component_hash: 91d785326b03db15e2f7f1314d8c976d38f21aa5759b570dcbbc89bcf247fd27 dependencies: @@ -563,11 +581,11 @@ dependencies: require: private version: '>=4.0' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 1.5.0 espressif/knob: - component_hash: 23bb6b0f62d4a2bbfe8b65e182397541339b8e48d54cef15909f5d4f5bd43d29 + component_hash: 138ed090b4c9090a0a678f695b9d1884b368130c729b7333ed35bd7f4e8da80e dependencies: - name: espressif/cmake_utilities registry_url: https://components.espressif.com @@ -579,9 +597,9 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 1.0.1 + version: 1.0.2 espressif/led_strip: - component_hash: 223998f10cae6d81f2ad2dd3c1103c2221be298c708e37917482b0153f3ec64e + component_hash: f0d1b7f93eb3ee57bcfd220656176b0b5616e1947f89ed44364555cf910c4840 dependencies: - name: idf require: private @@ -589,7 +607,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 3.0.1~1 + version: 3.0.2 espressif/usb_host_uvc: component_hash: b0cb9a3a2eaa09dc6e5cc3e47e04d0a73e47c2d3dcfb3afce959224a1fe5fb5e dependencies: @@ -614,7 +632,7 @@ dependencies: - linux version: 2.3.1 espressif2022/esp_emote_gfx: - component_hash: 7478561e4e8bd0efd4307647ec7221da2519de4ff1cd0cb08dccfbd0161cadaa + component_hash: 67986ead33c11de895eb7ba9a069ea89949ec911ce7cf0989478cc9da2e54735 dependencies: - name: espressif/cmake_utilities registry_url: https://components.espressif.com @@ -622,7 +640,7 @@ dependencies: version: 0.* - name: espressif/esp_new_jpeg registry_url: https://components.espressif.com - require: private + require: public version: 0.6.* - name: espressif/freetype registry_url: https://components.espressif.com @@ -631,10 +649,14 @@ dependencies: - name: idf require: private version: '>=5.0' + - name: lvgl/lvgl + registry_url: https://components.espressif.com + require: public + version: '*' source: registry_url: https://components.espressif.com/ type: service - version: 1.2.0~1 + version: 2.0.0 espressif2022/image_player: component_hash: 0e42ed1c9665debd15f2f3e7e56519100e75e446410962226cb5e5402da3fa43 dependencies: @@ -693,7 +715,7 @@ dependencies: type: service version: 1.0.2 waveshare/esp_lcd_touch_cst9217: - component_hash: c5da844e73ab7b7e02938901e71f38deed94b26976045fb4517e7fc1e8dc4db5 + component_hash: 27a00845832b7987cacf4c4125cbed5415d940987583065b01870ac080930856 dependencies: - name: espressif/esp_lcd_touch registry_url: https://components.espressif.com @@ -705,7 +727,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 1.0.3 + version: 1.0.4 wvirgil123/sscma_client: component_hash: bf7d4a1f157303f4850d0b9b465c733ce38e8689ff40ac4743a44520524c41d5 dependencies: @@ -728,6 +750,7 @@ direct_dependencies: - 78/xiaozhi-fonts - espressif/adc_battery_estimation - espressif/adc_mic +- espressif/bmi270_sensor - espressif/button - espressif/cmake_utilities - espressif/dl_fft @@ -753,7 +776,6 @@ direct_dependencies: - espressif/esp_mmap_assets - espressif/esp_new_jpeg - espressif/esp_video -- espressif/i2c_bus - espressif/knob - espressif/led_strip - espressif2022/esp_emote_gfx @@ -765,6 +787,6 @@ direct_dependencies: - waveshare/esp_lcd_sh8601 - waveshare/esp_lcd_touch_cst9217 - wvirgil123/sscma_client -manifest_hash: 8b8d8d3a405eda302202a654ce1dfdbc17f486994ac285a18867b38f07a065c0 +manifest_hash: 58298729f89f9edb041dac03ee9a91c4f6f97f277634f6349f91981c9b4b715f target: esp32s3 version: 2.0.0 diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index c56336f..43c51aa 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -1,7 +1,6 @@ # Define xiaozhi source directory set(XIAOZHI_MAIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../xiaozhi-esp32/main") - file(GLOB_RECURSE STACK_CHAN_SOURCES "apps/*.c" "apps/*.cc" @@ -47,6 +46,7 @@ set(SOURCES "audio/audio_codec.cc" "display/lvgl_display/gif/lvgl_gif.cc" "display/lvgl_display/gif/gifdec.c" "display/lvgl_display/jpg/image_to_jpeg.cpp" + "display/lvgl_display/jpg/jpeg_to_image.c" "protocols/protocol.cc" "protocols/mqtt_protocol.cc" "protocols/websocket_protocol.cc" @@ -55,8 +55,9 @@ set(SOURCES "audio/audio_codec.cc" "application.cc" "ota.cc" "settings.cc" - "device_state_event.cc" + "device_state_machine.cc" "assets.cc" + # "main.cc" ) # Transform relative paths to absolute paths from xiaozhi-esp32/main @@ -65,7 +66,6 @@ list(TRANSFORM SOURCES PREPEND "${XIAOZHI_MAIN_DIR}/") set(INCLUDE_DIRS "." "display" "display/lvgl_display" "display/lvgl_display/jpg" "audio" "protocols") # Transform include dirs to absolute paths from xiaozhi-esp32/main list(TRANSFORM INCLUDE_DIRS PREPEND "${XIAOZHI_MAIN_DIR}/") -list(APPEND INCLUDE_DIRS) # Add board common files file(GLOB BOARD_COMMON_SOURCES ${XIAOZHI_MAIN_DIR}/boards/common/*.cc) @@ -99,25 +99,28 @@ if(CONFIG_BOARD_TYPE_M5STACK_STACK_CHAN) endif() file(GLOB BOARD_SOURCES + ${XIAOZHI_MAIN_DIR}/boards/${BOARD_TYPE}/*.cc + ${XIAOZHI_MAIN_DIR}/boards/${BOARD_TYPE}/*.c ) list(APPEND SOURCES ${BOARD_SOURCES}) # Select audio processor according to Kconfig if(CONFIG_USE_AUDIO_PROCESSOR) - set(AUDIO_PROCESSOR_SRC "audio/processors/afe_audio_processor.cc") + list(APPEND SOURCES "${XIAOZHI_MAIN_DIR}/audio/processors/afe_audio_processor.cc") else() - set(AUDIO_PROCESSOR_SRC "audio/processors/no_audio_processor.cc") + list(APPEND SOURCES "${XIAOZHI_MAIN_DIR}/audio/processors/no_audio_processor.cc") endif() -list(APPEND SOURCES "${XIAOZHI_MAIN_DIR}/${AUDIO_PROCESSOR_SRC}") - if(CONFIG_IDF_TARGET_ESP32S3 OR CONFIG_IDF_TARGET_ESP32P4) - set(WAKE_WORD_SRCS "audio/wake_words/afe_wake_word.cc" "audio/wake_words/custom_wake_word.cc") + list(APPEND SOURCES "${XIAOZHI_MAIN_DIR}/audio/wake_words/afe_wake_word.cc") + list(APPEND SOURCES "${XIAOZHI_MAIN_DIR}/audio/wake_words/custom_wake_word.cc") else() - set(WAKE_WORD_SRCS "audio/wake_words/esp_wake_word.cc") + list(APPEND SOURCES "${XIAOZHI_MAIN_DIR}/audio/wake_words/esp_wake_word.cc") endif() -list(TRANSFORM WAKE_WORD_SRCS PREPEND "${XIAOZHI_MAIN_DIR}/") -list(APPEND SOURCES ${WAKE_WORD_SRCS}) +# Auto Select Additional Sources +if (CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING) + list(APPEND SOURCES "${XIAOZHI_MAIN_DIR}/boards/common/blufi.cpp") +endif () # Select language directory according to Kconfig if(CONFIG_LANGUAGE_ZH_CN) set(LANG_DIR "zh-CN") @@ -233,16 +236,14 @@ file(GLOB COMMON_SOUNDS # If target chip is ESP32, exclude specific files to avoid build errors if(CONFIG_IDF_TARGET_ESP32) - set(ESP32_EXCLUDE_FILES - "audio/codecs/box_audio_codec.cc" - "audio/codecs/es8388_audio_codec.cc" - "audio/codecs/es8389_audio_codec.cc" - "led/gpio_led.cc" - "boards/common/esp32_camera.cc" - "display/lvgl_display/jpg/image_to_jpeg.cpp" - ) - list(TRANSFORM ESP32_EXCLUDE_FILES PREPEND "${XIAOZHI_MAIN_DIR}/") - list(REMOVE_ITEM SOURCES ${ESP32_EXCLUDE_FILES}) + list(REMOVE_ITEM SOURCES "audio/codecs/box_audio_codec.cc" + "audio/codecs/es8388_audio_codec.cc" + "audio/codecs/es8389_audio_codec.cc" + "led/gpio_led.cc" + "${XIAOZHI_MAIN_DIR}/boards/common/esp32_camera.cc" + "display/lvgl_display/jpg/image_to_jpeg.cpp" + "display/lvgl_display/jpg/jpeg_to_image.c" + ) endif() idf_component_register(SRCS ${SOURCES} @@ -264,12 +265,12 @@ target_compile_definitions(${COMPONENT_LIB} # Add generation rules add_custom_command( OUTPUT ${LANG_HEADER} - COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/../xiaozhi-esp32/scripts/gen_lang.py + COMMAND python ${PROJECT_DIR}/xiaozhi-esp32/scripts/gen_lang.py --language "${LANG_DIR}" --output "${LANG_HEADER}" DEPENDS ${LANG_JSON} - ${CMAKE_CURRENT_SOURCE_DIR}/../xiaozhi-esp32/scripts/gen_lang.py + ${PROJECT_DIR}/xiaozhi-esp32/scripts/gen_lang.py COMMENT "Generating ${LANG_DIR} language config" ) @@ -327,6 +328,7 @@ endforeach() endif() +set(DEFAULT_ASSETS_EXTRA_FILES "${CMAKE_CURRENT_SOURCE_DIR}/assets/assets_bin") # Function to build default assets based on configuration function(build_default_assets_bin) @@ -360,10 +362,10 @@ function(build_default_assets_bin) # Create custom command to build assets add_custom_command( OUTPUT ${GENERATED_ASSETS_BIN} - COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/../xiaozhi-esp32/scripts/build_default_assets.py ${BUILD_ARGS} + COMMAND python ${PROJECT_DIR}/xiaozhi-esp32/scripts/build_default_assets.py ${BUILD_ARGS} DEPENDS ${SDKCONFIG} - ${CMAKE_CURRENT_SOURCE_DIR}/../xiaozhi-esp32/scripts/build_default_assets.py + ${PROJECT_DIR}/xiaozhi-esp32/scripts/build_default_assets.py COMMENT "Building default assets.bin based on configuration" VERBATIM ) @@ -421,7 +423,7 @@ function(get_assets_local_file assets_source assets_local_file_var) if(IS_ABSOLUTE "${assets_source}") set(ASSETS_LOCAL_FILE "${assets_source}") else() - set(ASSETS_LOCAL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${assets_source}") + set(ASSETS_LOCAL_FILE "${XIAOZHI_MAIN_DIR}/${assets_source}") endif() # Check if local file exists diff --git a/firmware/main/Kconfig.projbuild b/firmware/main/Kconfig.projbuild index e1a3742..6f503f3 100644 --- a/firmware/main/Kconfig.projbuild +++ b/firmware/main/Kconfig.projbuild @@ -153,6 +153,9 @@ choice BOARD_TYPE config BOARD_TYPE_ESP_SPOT_S3 bool "Espressif Spot-S3" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_ESP_SPOT_C5 + bool "Espressif Spot-C5" + depends on IDF_TARGET_ESP32C5 config BOARD_TYPE_ESP_HI bool "Espressif ESP-HI" depends on IDF_TARGET_ESP32C3 @@ -249,6 +252,9 @@ choice BOARD_TYPE config BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_2_06 bool "Waveshare ESP32-S3-Touch-AMOLED-2.06" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_2_06 + bool "Waveshare ESP32-C6-Touch-AMOLED-2.06" + depends on IDF_TARGET_ESP32C6 config BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_75 bool "Waveshare ESP32-S3-Touch-AMOLED-1.75" depends on IDF_TARGET_ESP32S3 @@ -270,6 +276,9 @@ choice BOARD_TYPE config BOARD_TYPE_WAVESHARE_C6_LCD_1_69 bool "Waveshare ESP32-C6-LCD-1.69" depends on IDF_TARGET_ESP32C6 + config BOARD_TYPE_WAVESHARE_C6_TOUCH_LCD_1_83 + bool "Waveshare ESP32-C6-Touch-LCD-1.83" + depends on IDF_TARGET_ESP32C6 config BOARD_TYPE_WAVESHARE_C6_TOUCH_AMOLED_1_43 bool "Waveshare ESP32-C6-Touch-AMOLOED-1.43" depends on IDF_TARGET_ESP32C6 @@ -285,6 +294,9 @@ choice BOARD_TYPE config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_5 bool "Waveshare ESP32-S3-Touch-LCD-3.5" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_WAVESHARE_S3_ePaper_1_54 + bool "Waveshare ESP32-S3-ePaper-1.54" + depends on IDF_TARGET_ESP32S3 config BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_5B bool "Waveshare ESP32-S3-Touch-LCD-3.5B" depends on IDF_TARGET_ESP32S3 @@ -324,6 +336,9 @@ choice BOARD_TYPE config BOARD_TYPE_MOVECALL_MOJI_ESP32S3 bool "Movecall Moji 小智AI衍生版" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_MOVECALL_MOJI2_ESP32C5 + bool "Movecall Moji2.0 小智AI衍生版" + depends on IDF_TARGET_ESP32C5 config BOARD_TYPE_MOVECALL_CUICAN_ESP32S3 bool "Movecall CuiCan 璀璨·AI吊坠" depends on IDF_TARGET_ESP32S3 @@ -372,6 +387,9 @@ choice BOARD_TYPE config BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307 bool "无名科技星智1.54(ML307)" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_XINGZHI_METAL_1_54_WIFI + bool "无名科技星智1.54 METAL(wifi)" + depends on IDF_TARGET_ESP32S3 config BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER bool "Seeed Studio SenseCAP Watcher" depends on IDF_TARGET_ESP32S3 @@ -432,6 +450,9 @@ choice BOARD_TYPE config BOARD_TYPE_AIPI_LITE bool "AIPI-Lite" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_HU_087 + bool "HU-087" + depends on IDF_TARGET_ESP32S3 endchoice choice @@ -465,7 +486,7 @@ choice ESP_S3_LCD_EV_Board_Version_TYPE endchoice choice DISPLAY_OLED_TYPE - depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32 + depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32 || BOARD_TYPE_HU_087 prompt "OLED Type" default OLED_SSD1306_128X32 help @@ -659,6 +680,29 @@ config USE_AUDIO_DEBUGGER help Enable audio debugger, send audio data through UDP to the host machine +menu "WiFi Configuration Method" + help + WiFi Configuration Method Selection + config USE_HOTSPOT_WIFI_PROVISIONING + bool "Hotspot" + default y + help + Use WiFi Hotspot to transmit WiFi configuration data + config USE_ACOUSTIC_WIFI_PROVISIONING + bool "Acoustic" + help + Use audio signal to transmit WiFi configuration data + + config USE_ESP_BLUFI_WIFI_PROVISIONING + bool "Esp Blufi" + help + Use esp blufi protocol to transmit WiFi configuration data + select BT_ENABLED + select BT_BLE_42_FEATURES_SUPPORTED + select BT_BLE_BLUFI_ENABLE + select MBEDTLS_DHM_C +endmenu + config AUDIO_DEBUG_UDP_SERVER string "Audio Debug UDP Server Address" default "192.168.2.100:8000" @@ -666,12 +710,6 @@ config AUDIO_DEBUG_UDP_SERVER help UDP server address, format: IP:PORT, used to receive audio debugging data -config USE_ACOUSTIC_WIFI_PROVISIONING - bool "Enable Acoustic WiFi Provisioning" - default n - help - Enable acoustic WiFi provisioning, use audio signal to transmit WiFi configuration data - config RECEIVE_CUSTOM_MESSAGE bool "Enable Custom Message Reception" default n @@ -683,6 +721,16 @@ menu "Camera Configuration" comment "Warning: Please read the help text before modifying these settings." + config XIAOZHI_CAMERA_ALLOW_JPEG_INPUT + bool "Allow JPEG Input" + default n + help + Allow JPEG Input format for the camera. + + This option may need to be enabled when using a USB camera. + + Not currently supported when used simultaneously with XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE. + config XIAOZHI_ENABLE_HARDWARE_JPEG_ENCODER bool "Enable Hardware JPEG Encoder" default y @@ -691,6 +739,14 @@ menu "Camera Configuration" Use hardware JPEG encoder on ESP32-P4 to encode image to JPEG. See https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/api-reference/peripherals/jpeg.html for more details. + config XIAOZHI_ENABLE_HARDWARE_JPEG_DECODER + bool "Enable Hardware JPEG Decoder" + default n + depends on SOC_JPEG_DECODE_SUPPORTED && XIAOZHI_CAMERA_ALLOW_JPEG_INPUT + help + Use hardware JPEG decoder on ESP32-P4 to decode JPEG to image. + See https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/api-reference/peripherals/jpeg.html for more details. + config XIAOZHI_ENABLE_CAMERA_DEBUG_MODE bool "Enable Camera Debug Mode" default n @@ -699,7 +755,7 @@ menu "Camera Configuration" Only works on boards that support camera. config XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP - bool "Enable software camera buffer endianness swapping (USE WITH CAUTION)" + bool "Enable software camera buffer endianness swapping" default n depends on !CAMERA_SENSOR_SWAP_PIXEL_BYTE_ORDER help @@ -714,12 +770,15 @@ menu "Camera Configuration" menuconfig XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE bool "Enable Camera Image Rotation" default n + depends on !XIAOZHI_CAMERA_ALLOW_JPEG_INPUT help Enable camera image rotation, rotate the camera image to the correct orientation. - On ESP32-P4, rotation is handled by PPA hardware. - On other chips, rotation is done in software with performance cost. - For 180° rotation, use HFlip + VFlip instead of this option. + Not currently supported when used simultaneously with XIAOZHI_CAMERA_ALLOW_JPEG_INPUT. + if XIAOZHI_ENABLE_ROTATE_CAMERA_IMAGE choice XIAOZHI_CAMERA_IMAGE_ROTATION_ANGLE prompt "Camera Image Rotation Angle (clockwise)" diff --git a/firmware/main/apps/app_ai_agent/app_ai_agent.cpp b/firmware/main/apps/app_ai_agent/app_ai_agent.cpp index 0caf95e..ada6662 100644 --- a/firmware/main/apps/app_ai_agent/app_ai_agent.cpp +++ b/firmware/main/apps/app_ai_agent/app_ai_agent.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include using namespace mooncake; using namespace smooth_ui_toolkit::lvgl_cpp; @@ -36,7 +38,9 @@ void AppAiAgent::onOpen() { mclog::tagInfo(getAppInfo().name, "on open"); - GetHAL().startXiaozhi(); + // Request to start Xiaozhi service + // All apps will be uninstall in next mooncake update + GetHAL().requestXiaozhiStart(); } // Called repeatedly while the App is running diff --git a/firmware/main/apps/app_avatar/app_avatar.cpp b/firmware/main/apps/app_avatar/app_avatar.cpp index fc66c19..74d2f5c 100644 --- a/firmware/main/apps/app_avatar/app_avatar.cpp +++ b/firmware/main/apps/app_avatar/app_avatar.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -62,18 +63,32 @@ void AppAvatar::onOpen() { mclog::tagInfo(getAppInfo().name, "on open"); + // Create loading page + std::unique_ptr loading_page; + { + LvglLockGuard lock; + loading_page = std::make_unique(0xFF6699, 0x431525); + } + + // Start avatar service + GetHAL().startWebSocketAvatarService([&](std::string_view msg) { + LvglLockGuard lock; + loading_page->setMessage(msg); + }); // GetHAL().startBleServer(); - GetHAL().startWebSocketAvatar(); LvglLockGuard lock; + // Destroy loading page + loading_page.reset(); + // Create default avatar auto avatar = std::make_unique(); avatar->init(lv_screen_active()); GetStackChan().attachAvatar(std::move(avatar)); /* ------------------------------- BLE events ------------------------------- */ - _ble_avatar_data.callback_id = GetHAL().onBleAvatarData.connect([&](const char* data) { + GetHAL().onBleAvatarData.connect([&](const char* data) { std::lock_guard lock(_mutex); if (_ble_avatar_data.update_flag) { return; @@ -82,7 +97,7 @@ void AppAvatar::onOpen() _ble_avatar_data.data_ptr = (char*)data; }); - _ble_motion_data.callback_id = GetHAL().onBleMotionData.connect([&](const char* data) { + GetHAL().onBleMotionData.connect([&](const char* data) { std::lock_guard lock(_mutex); if (_ble_motion_data.update_flag) { return; @@ -93,20 +108,20 @@ void AppAvatar::onOpen() /* ---------------------------- Websocket events ---------------------------- */ // Avatar control - _ws_callback_ids.avatar_id = GetHAL().onWsAvatarData.connect([&](std::string_view data) { + GetHAL().onWsAvatarData.connect([&](std::string_view data) { LvglLockGuard lvgl_lock; GetStackChan().updateAvatarFromJson(data.data()); }); // Motion control - _ws_callback_ids.motion_id = GetHAL().onWsMotionData.connect([&](std::string_view data) { + GetHAL().onWsMotionData.connect([&](std::string_view data) { LvglLockGuard lvgl_lock; check_auto_angle_sync_mode(); GetStackChan().updateMotionFromJson(data.data()); }); // Phone call handling - _ws_callback_ids.call_req_id = GetHAL().onWsCallRequest.connect([&](std::string caller) { + GetHAL().onWsCallRequest.connect([&](std::string caller) { if (_ws_call_view_id >= 0) { mclog::tagWarn(getAppInfo().name, "ws call view already exists"); return; @@ -145,7 +160,7 @@ void AppAvatar::onOpen() _ws_call_view_id = avatar.addDecorator(std::move(view)); }); - _ws_callback_ids.call_end_id = GetHAL().onWsCallEnd.connect([&](WsSignalSource source) { + GetHAL().onWsCallEnd.connect([&](WsSignalSource source) { if (source != WsSignalSource::Remote) { return; } @@ -168,11 +183,10 @@ void AppAvatar::onOpen() }); // Text message handling - _ws_callback_ids.text_msg_id = GetHAL().onWsTextMessage.connect([&](const WsTextMessage_t& message) { + GetHAL().onWsTextMessage.connect([&](const WsTextMessage_t& message) { LvglLockGuard lvgl_lock; auto& stackchan = GetStackChan(); - auto& avatar = stackchan.avatar(); stackchan.addModifier( std::make_unique(fmt::format("{} says: {}", message.name, message.content), 6000)); @@ -186,7 +200,7 @@ void AppAvatar::onOpen() } }); - _ws_callback_ids.dance_data_id = GetHAL().onWsDanceData.connect([&](std::string_view data) { + GetHAL().onWsDanceData.connect([&](std::string_view data) { LvglLockGuard lvgl_lock; auto sequence = stackchan::animation::parse_sequence_from_json(data.data()); if (!sequence.empty()) { @@ -194,8 +208,18 @@ void AppAvatar::onOpen() } }); + GetHAL().onWsLog.connect([&](CommonLogLevel level, std::string_view msg) { + auto type = static_cast(level); + uint32_t duration = type == view::ToastType::Error ? 12000 : 1600; + view::pop_a_toast(msg, type, duration); + }); + /* ------------------------------ Video window ------------------------------ */ _video_window = std::make_unique(lv_screen_active()); + + /* ----------------------------- Common widgets ----------------------------- */ + view::create_home_indicator([&]() { close(); }, 0xFF9ABC, 0x431525); + view::create_status_bar(0xFF9ABC, 0x431525); } void AppAvatar::onRunning() @@ -218,26 +242,36 @@ void AppAvatar::onRunning() } GetStackChan().update(); + + view::update_home_indicator(); + view::update_status_bar(); } void AppAvatar::onClose() { mclog::tagInfo(getAppInfo().name, "on close"); - LvglLockGuard lock; + { + LvglLockGuard lock; - GetStackChan().resetAvatar(); - _video_window.reset(); + GetStackChan().resetAvatar(); + _video_window.reset(); - GetHAL().onBleAvatarData.disconnect(_ble_avatar_data.callback_id); - GetHAL().onBleMotionData.disconnect(_ble_motion_data.callback_id); + GetHAL().onBleAvatarData.clear(); + GetHAL().onBleMotionData.clear(); + + GetHAL().onWsAvatarData.clear(); + GetHAL().onWsMotionData.clear(); + GetHAL().onWsCallRequest.clear(); + GetHAL().onWsCallEnd.clear(); + GetHAL().onWsTextMessage.clear(); + GetHAL().onWsDanceData.clear(); + + view::destroy_home_indicator(); + view::destroy_status_bar(); + } - GetHAL().onWsAvatarData.disconnect(_ws_callback_ids.avatar_id); - GetHAL().onWsMotionData.disconnect(_ws_callback_ids.motion_id); - GetHAL().onWsCallRequest.disconnect(_ws_callback_ids.call_req_id); - GetHAL().onWsCallEnd.disconnect(_ws_callback_ids.call_end_id); - GetHAL().onWsTextMessage.disconnect(_ws_callback_ids.text_msg_id); - GetHAL().onWsDanceData.disconnect(_ws_callback_ids.dance_data_id); + GetHAL().requestWarmReboot(1); } void AppAvatar::check_auto_angle_sync_mode() diff --git a/firmware/main/apps/app_avatar/app_avatar.h b/firmware/main/apps/app_avatar/app_avatar.h index ffd125d..218d03d 100644 --- a/firmware/main/apps/app_avatar/app_avatar.h +++ b/firmware/main/apps/app_avatar/app_avatar.h @@ -31,21 +31,10 @@ class AppAvatar : public mooncake::AppAbility { struct BleHandlerData_t { bool update_flag = false; char* data_ptr = nullptr; - int callback_id = -1; }; BleHandlerData_t _ble_avatar_data; BleHandlerData_t _ble_motion_data; - struct WsCallbackIds_t { - int avatar_id = -1; - int motion_id = -1; - int call_req_id = -1; - int call_end_id = -1; - int text_msg_id = -1; - int dance_data_id = -1; - }; - WsCallbackIds_t _ws_callback_ids; - int _ws_call_view_id = -1; uint32_t _last_motion_cmd_tick = 0; diff --git a/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.cpp b/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.cpp index 1db8540..3752a23 100644 --- a/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.cpp +++ b/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.cpp @@ -5,12 +5,14 @@ */ #include "app_espnow_ctrl.h" #include "view/page_selector.h" +#include "view/view.h" #include #include #include #include #include #include +#include #include #include #include @@ -26,7 +28,7 @@ AppEspnowControl::AppEspnowControl() // 配置 App 图标 setAppInfo().icon = (void*)&icon_controller; // 配置 App 主题颜色 - static uint32_t theme_color = 0xCCCC33; + static uint32_t theme_color = 0x7ACE74; setAppInfo().userData = (void*)&theme_color; } @@ -38,36 +40,27 @@ void AppEspnowControl::onCreate() static std::mutex _mutex; static std::vector _received_data; static int _receiver_id = 0; +static int _wifi_channel = 0; static bool _is_receiver = false; void AppEspnowControl::onOpen() { mclog::tagInfo(getAppInfo().name, "on open"); - // Get role - std::vector role_options = {"Receiver", "Sender"}; - int role_selection = view::create_page_selector_and_wait("Select Role", role_options); - _is_receiver = (role_selection == 0); - mclog::tagInfo(getAppInfo().name, "selected role: {}", _is_receiver ? "Receiver" : "Sender"); + // Default setup + _receiver_id = 1; + _wifi_channel = 1; - // Get channel - std::vector channel_options; - for (int i = 0; i < 13; i++) { - channel_options.push_back(std::to_string(i + 1)); - } - auto wifi_channel = view::create_page_selector_and_wait("Select WiFi Channel", channel_options) + 1; - mclog::tagInfo(getAppInfo().name, "selected wifi channel: {}", wifi_channel); - - // Get id - std::vector id_options; - for (int i = 0; i < 255; i++) { - id_options.push_back(std::to_string(i)); + // Start setup page + bool is_advanced = start_startup_page(); + if (is_advanced) { + start_advanced_page(); } - _receiver_id = view::create_page_selector_and_wait("Select ID", id_options); - mclog::tagInfo(getAppInfo().name, "selected id: {}", _receiver_id); + mclog::tagInfo(getAppInfo().name, "get setup: role {}, id {}, channel {}", _is_receiver ? "Receiver" : "Sender", + _receiver_id, _wifi_channel); // Start espnow - GetHAL().startEspNow(wifi_channel); + GetHAL().startEspNow(_wifi_channel); GetHAL().onEspNowData.connect([](const std::vector& data) { std::lock_guard lock(_mutex); _received_data = data; @@ -82,6 +75,7 @@ void AppEspnowControl::onOpen() avatar->init(lv_screen_active()); stackchan.attachAvatar(std::move(avatar)); + stackchan.clearModifiers(); stackchan.addModifier(std::make_unique()); stackchan.addModifier(std::make_unique()); @@ -89,6 +83,75 @@ void AppEspnowControl::onOpen() stackchan.motion().setAutoAngleSyncEnabled(false); GetHAL().setLaserEnabled(false); + + view::create_home_indicator([&]() { close(); }, 0xA0D99C, 0x154311); + view::create_status_bar(0xA0D99C, 0x154311); +} + +bool AppEspnowControl::start_startup_page() +{ + GetHAL().lvglLock(); + auto page = std::make_unique(); + GetHAL().lvglUnlock(); + while (1) { + GetHAL().delay(50); + LvglLockGuard lock; + if (page->isSelected()) { + break; + } + } + + bool is_advanced = false; + GetHAL().lvglLock(); + if (page->selectedIndex() == 0) { + _is_receiver = true; + } else if (page->selectedIndex() == 1) { + _is_receiver = false; + } else { + is_advanced = true; + } + page.reset(); + GetHAL().lvglUnlock(); + + return is_advanced; +} + +void AppEspnowControl::start_advanced_page() +{ + // Get role + std::vector role_options = {"Receiver", "Sender"}; + int role_selection = view::create_page_selector_and_wait("Select Role", role_options); + _is_receiver = (role_selection == 0); + mclog::tagInfo(getAppInfo().name, "selected role: {}", _is_receiver ? "Receiver" : "Sender"); + + // Get wifi channel + std::vector channel_options; + for (int i = 0; i < 13; i++) { + channel_options.push_back(std::to_string(i + 1)); + } + _wifi_channel = view::create_page_selector_and_wait("Select WiFi Channel", channel_options) + 1; + mclog::tagInfo(getAppInfo().name, "selected wifi channel: {}", _wifi_channel); + + // Get id + if (_is_receiver) { + std::vector id_options; + for (int i = 1; i < 255; i++) { + id_options.push_back(std::to_string(i)); + } + _receiver_id = view::create_page_selector_and_wait("Select Receiver ID", id_options) + 1; + mclog::tagInfo(getAppInfo().name, "selected receiver id: {}", _receiver_id); + } else { + std::vector id_options; + for (int i = 0; i < 255; i++) { + if (i == 0) { + id_options.push_back("0 (Broadcast)"); + continue; + } + id_options.push_back(std::to_string(i)); + } + _receiver_id = view::create_page_selector_and_wait("Select Receiver ID", id_options); + mclog::tagInfo(getAppInfo().name, "selected target id: {}", _receiver_id); + } } void handle_received_data() @@ -178,6 +241,9 @@ void AppEspnowControl::onRunning() } GetStackChan().update(); + + view::update_home_indicator(); + view::update_status_bar(); } void AppEspnowControl::onClose() @@ -185,4 +251,9 @@ void AppEspnowControl::onClose() mclog::tagInfo(getAppInfo().name, "on close"); LvglLockGuard lock; + + view::destroy_home_indicator(); + view::destroy_status_bar(); + + GetHAL().requestWarmReboot(2); } diff --git a/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.h b/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.h index 6eebf72..89d365e 100644 --- a/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.h +++ b/firmware/main/apps/app_espnow_ctrl/app_espnow_ctrl.h @@ -19,4 +19,8 @@ class AppEspnowControl : public mooncake::AppAbility { void onOpen() override; void onRunning() override; void onClose() override; + +private: + bool start_startup_page(); + void start_advanced_page(); }; diff --git a/firmware/main/apps/app_espnow_ctrl/view/page_selector.h b/firmware/main/apps/app_espnow_ctrl/view/page_selector.h index 5a78a51..330114c 100644 --- a/firmware/main/apps/app_espnow_ctrl/view/page_selector.h +++ b/firmware/main/apps/app_espnow_ctrl/view/page_selector.h @@ -20,11 +20,16 @@ class PageSelector { { _panel = std::make_unique(lv_screen_active()); _panel->setPadding(0, 0, 0, 0); + _panel->setBgColor(lv_color_hex(0xF6F6F6)); + _panel->align(LV_ALIGN_CENTER, 0, 0); + _panel->setBorderWidth(0); _panel->setSize(320, 240); + _panel->setRadius(0); _label = std::make_unique(_panel->get()); _label->setText(label); _label->setTextFont(&lv_font_montserrat_24); + _label->setTextColor(lv_color_hex(0x26206A)); _label->align(LV_ALIGN_CENTER, 0, -80); _roller = std::make_unique(_panel->get()); @@ -33,12 +38,20 @@ class PageSelector { _roller->align(LV_ALIGN_CENTER, -45, 35); _roller->onValueChanged().connect([&](uint32_t index) { _selected_index = index; }); _roller->setTextFont(&lv_font_montserrat_24); + _roller->setTextColor(lv_color_hex(0x26206A)); + _roller->setBgColor(lv_color_hex(0xDDEAFF)); + _roller->setRadius(18); + _roller->setShadowWidth(0); + _roller->setBorderWidth(0); _btn_confirm = std::make_unique(_panel->get()); _btn_confirm->label().setText("ok"); + _btn_confirm->label().setTextFont(&lv_font_montserrat_24); _btn_confirm->setSize(70, 110); _btn_confirm->align(LV_ALIGN_CENTER, 110, 40); _btn_confirm->onClick().connect([&]() { _is_selected = true; }); + _btn_confirm->setRadius(18); + _btn_confirm->setShadowWidth(0); } bool update() @@ -60,6 +73,13 @@ class PageSelector { int _selected_index = 0; }; +/** + * @brief Create a page selector and wait object + * + * @param label + * @param options + * @return int Selected option index + */ static inline int create_page_selector_and_wait(std::string_view label, const std::vector options) { GetHAL().lvglLock(); diff --git a/firmware/main/apps/app_espnow_ctrl/view/view.h b/firmware/main/apps/app_espnow_ctrl/view/view.h new file mode 100644 index 0000000..317d457 --- /dev/null +++ b/firmware/main/apps/app_espnow_ctrl/view/view.h @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace view { + +class EspnowRoleSelectorPage { +public: + EspnowRoleSelectorPage() + { + _panel = std::make_unique(lv_screen_active()); + _panel->setSize(320, 240); + _panel->setAlign(LV_ALIGN_CENTER); + _panel->setBgColor(lv_color_hex(0xE7FFE0)); + _panel->setPadding(0, 0, 0, 0); + _panel->setPadRow(18); + _panel->setRadius(0); + _panel->setBorderWidth(0); + + _title = std::make_unique(*_panel); + _title->setText("Select Role:"); + _title->setTextFont(&lv_font_montserrat_24); + _title->setTextColor(lv_color_hex(0x154311)); + _title->align(LV_ALIGN_TOP_MID, 0, 12); + + _btn_receiver = std::make_unique(*_panel); + _btn_receiver->align(LV_ALIGN_TOP_MID, 0, 58); + _btn_receiver->setSize(282, 48); + _btn_receiver->setRadius(18); + _btn_receiver->setBgColor(lv_color_hex(0xA0D99C)); + _btn_receiver->setBorderWidth(0); + _btn_receiver->setShadowWidth(0); + _btn_receiver->label().setText("Receiver"); + _btn_receiver->label().setTextFont(&lv_font_montserrat_24); + _btn_receiver->label().setTextColor(lv_color_hex(0x154311)); + _btn_receiver->onClick().connect([this]() { _selected_index = 0; }); + + _btn_sender = std::make_unique(*_panel); + _btn_sender->align(LV_ALIGN_TOP_MID, 0, 126); + _btn_sender->setSize(282, 48); + _btn_sender->setRadius(18); + _btn_sender->setBgColor(lv_color_hex(0xA0D99C)); + _btn_sender->setBorderWidth(0); + _btn_sender->setShadowWidth(0); + _btn_sender->label().setText("Sender"); + _btn_sender->label().setTextFont(&lv_font_montserrat_24); + _btn_sender->label().setTextColor(lv_color_hex(0x154311)); + _btn_sender->onClick().connect([this]() { _selected_index = 1; }); + + _btn_advanced = std::make_unique(*_panel); + _btn_advanced->align(LV_ALIGN_TOP_MID, 0, 194); + _btn_advanced->setSize(170, 30); + _btn_advanced->setRadius(18); + _btn_advanced->setBgColor(lv_color_hex(0xB9E6B4)); + _btn_advanced->setBorderWidth(0); + _btn_advanced->setShadowWidth(0); + _btn_advanced->label().setText("Advanced"); + _btn_advanced->label().setTextFont(&lv_font_montserrat_16); + _btn_advanced->label().setTextColor(lv_color_hex(0x5F8559)); + _btn_advanced->onClick().connect([this]() { _selected_index = 2; }); + } + + bool isSelected() + { + return _selected_index != -1; + } + + int selectedIndex() + { + return _selected_index; + } + +private: + std::unique_ptr _panel; + std::unique_ptr _title; + std::unique_ptr _btn_receiver; + std::unique_ptr _btn_sender; + std::unique_ptr _btn_advanced; + int _selected_index = -1; +}; + +} // namespace view diff --git a/firmware/main/apps/app_launcher/app_launcher.cpp b/firmware/main/apps/app_launcher/app_launcher.cpp index a655077..e1e3d20 100644 --- a/firmware/main/apps/app_launcher/app_launcher.cpp +++ b/firmware/main/apps/app_launcher/app_launcher.cpp @@ -26,21 +26,29 @@ void AppLauncher::onLauncherOpen() LvglLockGuard lock; - _view = std::make_unique(); - _view->init(getAppProps()); - _view->onAppClicked = [&](int appID) { - mclog::tagInfo(getAppInfo().name, "handle open app, app id: {}", appID); - openApp(appID); - }; + if (!_startup_checked && !GetHAL().isAppConfiged()) { + mclog::tagInfo(getAppInfo().name, "app not configured, start startup worker"); + _startup_worker = std::make_unique(); + } else { + create_launcher_view(); + } } void AppLauncher::onLauncherRunning() { LvglLockGuard lock; - _view->update(); - - screensaver_update(); + if (_startup_worker) { + _startup_worker->update(); + if (_startup_worker->isDone()) { + _startup_worker.reset(); + _startup_checked = true; + create_launcher_view(); + } + } else { + _view->update(); + screensaver_update(); + } GetStackChan().update(); } @@ -59,6 +67,16 @@ void AppLauncher::onLauncherDestroy() mclog::tagInfo(getAppInfo().name, "on close"); } +void AppLauncher::create_launcher_view() +{ + _view = std::make_unique(); + _view->init(getAppProps()); + _view->onAppClicked = [&](int appID) { + mclog::tagInfo(getAppInfo().name, "handle open app, app id: {}", appID); + openApp(appID); + }; +} + void AppLauncher::screensaver_update() { const uint32_t SCREENSAVER_TIMEOUT_MS = 30000; diff --git a/firmware/main/apps/app_launcher/app_launcher.h b/firmware/main/apps/app_launcher/app_launcher.h index 8dcd6eb..c62cca7 100644 --- a/firmware/main/apps/app_launcher/app_launcher.h +++ b/firmware/main/apps/app_launcher/app_launcher.h @@ -5,6 +5,7 @@ */ #pragma once #include "view/view.h" +#include #include #include #include @@ -21,7 +22,10 @@ class AppLauncher : public mooncake::templates::AppLauncherBase { private: std::unique_ptr _view; std::unique_ptr _screensaver; - + std::unique_ptr _startup_worker; uint32_t _screensaver_timecount = 0; + bool _startup_checked = false; + + void create_launcher_view(); void screensaver_update(); }; diff --git a/firmware/main/apps/app_launcher/view/view.cpp b/firmware/main/apps/app_launcher/view/view.cpp index 9c16218..cf0c939 100644 --- a/firmware/main/apps/app_launcher/view/view.cpp +++ b/firmware/main/apps/app_launcher/view/view.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -134,12 +135,13 @@ class PageIndicator { { _last_index = _current_index; - _current_index = (scrollValue + _page_gap / 2) / _page_gap; + // Calculate absolute index + int abs_index = (scrollValue + _page_gap / 2) / _page_gap; + + // Map to 0 ~ N-1 + _current_index = abs_index % _page_num; if (_current_index < 0) { - _current_index = 0; - } - if (_current_index >= _page_num) { - _current_index = _page_num - 1; + _current_index += _page_num; } if (_last_index != _current_index) { @@ -282,6 +284,9 @@ class DynamicIconLabel { static std::string _tag = "LauncherView"; static constexpr int _icon_gap = 320; +// Create 5 copies: [0:Backup] [1:Buffer] [2:Main] [3:Buffer] [4:Backup] +static constexpr int _loop_copies = 5; +static constexpr int _center_copy_index = 2; static int _last_clicked_icon_pos_x = -1; static std::unique_ptr _dynamic_bg_color; @@ -323,54 +328,55 @@ void LauncherView::init(std::vector appPorps) int icon_x = 0; int icon_y = 0; std::vector icon_label_texts; - for (const auto& props : appPorps) { - // mclog::tagInfo(_tag, "name: {}, id: {}", props.info.name, props.appID); - - // Icon panel - _icon_panels.push_back(std::make_unique(_panel->get())); - _icon_panels.back()->setAlign(LV_ALIGN_CENTER); - _icon_panels.back()->setSize(190, 160); - _icon_panels.back()->setPos(icon_x, icon_y); - _icon_panels.back()->setBorderWidth(0); - _icon_panels.back()->removeFlag(LV_OBJ_FLAG_SCROLLABLE); - _icon_panels.back()->setBgOpa(0); - - // Icon click callback - auto app_id = props.appID; - auto pos_x = icon_x; - _icon_panels.back()->onClick().connect([&, app_id, pos_x]() { - _clicked_app_id = app_id; - _last_clicked_icon_pos_x = pos_x; - }); - - // Collect icon label texts - icon_label_texts.push_back(props.info.name); - - // Icon image - if (props.info.icon != nullptr) { - _icon_images.push_back(std::make_unique(_icon_panels.back()->get())); - _icon_images.back()->setSrc(props.info.icon); - _icon_images.back()->setAlign(LV_ALIGN_CENTER); - } + std::vector step_colors; + + // Loop multiple times to create fake infinite scroll + for (int loop = 0; loop < _loop_copies; loop++) { + for (const auto& props : appPorps) { + // Icon panel + _icon_panels.push_back(std::make_unique(_panel->get())); + _icon_panels.back()->setAlign(LV_ALIGN_CENTER); + _icon_panels.back()->setSize(190, 160); + _icon_panels.back()->setPos(icon_x, icon_y); + _icon_panels.back()->setBorderWidth(0); + _icon_panels.back()->removeFlag(LV_OBJ_FLAG_SCROLLABLE); + _icon_panels.back()->setBgOpa(0); + + // Icon click callback + auto app_id = props.appID; + auto pos_x = icon_x; + _icon_panels.back()->onClick().connect([&, app_id, pos_x]() { + _clicked_app_id = app_id; + _last_clicked_icon_pos_x = pos_x; + }); + + // Keep track of data for helpers + icon_label_texts.push_back(props.info.name); + + uint32_t color = 0xDADADA; + if (props.info.userData != nullptr) { + color = *(uint32_t*)props.info.userData; + } + step_colors.push_back(color); - icon_x += _icon_gap; + // Icon image + if (props.info.icon != nullptr) { + _icon_images.push_back(std::make_unique(_icon_panels.back()->get())); + _icon_images.back()->setSrc(props.info.icon); + _icon_images.back()->setAlign(LV_ALIGN_CENTER); + } + + icon_x += _icon_gap; + } } /* ------------------------------ LR indicators ----------------------------- */ - // Scroll to nearby icon handler with wrap-around - int total_icons = appPorps.size(); - auto scroll_to_nearby_icon = [&, total_icons](int direction) { + // Scroll to nearby icon handler + auto scroll_to_nearby_icon = [&](int direction) { auto current_scroll_x = _panel->getScrollX(); int current_index = (current_scroll_x + _icon_gap / 2) / _icon_gap; int target_index = current_index + direction; - // Wrap around at boundaries - if (target_index < 0) { - target_index = total_icons - 1; - } else if (target_index >= total_icons) { - target_index = 0; - } - int target_x = target_index * _icon_gap; int scroll_distance = target_x - current_scroll_x; _panel->scrollBy(-scroll_distance, 0, LV_ANIM_ON); @@ -409,16 +415,6 @@ void LauncherView::init(std::vector appPorps) /* ---------------------------- Dynamic bg color ---------------------------- */ _dynamic_bg_color = std::make_unique(); - std::vector step_colors; - step_colors.resize(appPorps.size()); - for (size_t i = 0; i < appPorps.size(); i++) { - uint32_t color = 0xDADADA; - if (appPorps[i].info.userData != nullptr) { - color = *(uint32_t*)appPorps[i].info.userData; - } - step_colors[i] = color; - } - _dynamic_bg_color->onBgColorChanged = [&](const uint32_t& bgColor) { // mclog::tagInfo(_tag, "bg color changed to {:06X}", bgColor); _panel->setBgColor(lv_color_hex(bgColor)); @@ -428,6 +424,7 @@ void LauncherView::init(std::vector appPorps) /* ------------------------------ Page indicator ---------------------------- */ _page_indicator = std::make_unique(); + // Page indicator only needs to know the real app count (N), not N * copies _page_indicator->init(appPorps.size(), _icon_gap, _panel->get(), 0, 103); /* --------------------------- Dynamic icon label --------------------------- */ @@ -435,20 +432,51 @@ void LauncherView::init(std::vector appPorps) _dynamic_icon_label->init(icon_label_texts, _icon_gap, _panel->get()); /* ----------------------------- History restore ---------------------------- */ + bool need_restore = false; + int restore_icon_pos_x = -1; + + // Normal start pos (Center of the repeated sets) + int base_offset_rounds = _center_copy_index * appPorps.size(); + int default_start_x = base_offset_rounds * _icon_gap; + + // If warm boot was requested + if (GetHAL().getWarmRebootTarget() >= 0) { + auto app_index = GetHAL().getWarmRebootTarget(); + mclog::tagInfo(_tag, "warm boot was requested, app index: {}", app_index); + app_index = uitk::clamp(app_index, 0, static_cast(appPorps.size()) - 1); + + // Restore to center set + restore_icon_pos_x = (base_offset_rounds + app_index) * _icon_gap; + need_restore = true; + GetHAL().clearWarmRebootRequest(); + } + if (_last_clicked_icon_pos_x != -1) { + // Just restore where they left off, it should be in a valid range // mclog::tagInfo(_tag, "navigate to last clicked icon, pos x: {}", _last_clicked_icon_pos_x); - _panel->scrollBy(-_last_clicked_icon_pos_x, 0, LV_ANIM_OFF); + restore_icon_pos_x = _last_clicked_icon_pos_x; + need_restore = true; + _last_clicked_icon_pos_x = -1; + } - _dynamic_bg_color->jumpTo(_last_clicked_icon_pos_x / _icon_gap); - _page_indicator->jumpTo(_last_clicked_icon_pos_x / _icon_gap); - _dynamic_icon_label->jumpTo(_last_clicked_icon_pos_x / _icon_gap); + if (need_restore) { + _panel->scrollBy(-restore_icon_pos_x, 0, LV_ANIM_OFF); - _last_clicked_icon_pos_x = -1; - _state = STATE_NORMAL; + _dynamic_bg_color->jumpTo(restore_icon_pos_x / _icon_gap); + _page_indicator->jumpTo(restore_icon_pos_x / _icon_gap); + _dynamic_icon_label->jumpTo(restore_icon_pos_x / _icon_gap); + + _state = STATE_NORMAL; } // If first create else { + // Init at Center Set + _panel->scrollBy(-default_start_x, 0, LV_ANIM_OFF); + _dynamic_bg_color->jumpTo(default_start_x / _icon_gap); + _page_indicator->jumpTo(default_start_x / _icon_gap); + _dynamic_icon_label->jumpTo(default_start_x / _icon_gap); + // Setup startup animation // x for pos_y, y for radius _startup_anim = std::make_unique(); @@ -461,11 +489,13 @@ void LauncherView::init(std::vector appPorps) _startup_anim->teleport(240, 120); _panel->setY(_startup_anim->directValue().x); _panel->setRadius(_startup_anim->directValue().y); - _startup_anim->move(0, 0); _state = STATE_STARTUP; } + + // Destory boot logo label + GetHAL().bootLogo.reset(); } void LauncherView::update() @@ -504,6 +534,43 @@ void LauncherView::handle_state_normal() _clicked_app_id = -1; } + // We get total size from underlying icons count / copies + int total_icons = _icon_panels.size(); + int icons_per_set = total_icons / _loop_copies; + int set_width_px = icons_per_set * _icon_gap; + + // Check boundaries + // If we are mostly in Copy 1, jump to Copy 2 + // If we are mostly in Copy 3, jump to Copy 2 + // Copy Index: 0 1 [2] 3 4 + + int current_scroll_x = _panel->getScrollX(); + + // Define safe zone (Copy 2) + int center_set_start_x = _center_copy_index * set_width_px; + + // Thresholds: midpoint of Wrap sets + int left_trigger_limit = 1 * set_width_px + (set_width_px / 2); // Middle of Set 1 + int right_trigger_limit = 3 * set_width_px + (set_width_px / 2); // Middle of Set 3 + + // Wrap-around Logic + // Only perform teleport if we are NOT in an automated scroll animation + // (To avoid interrupting the snap/scroll-to animation which would leave us stuck between icons) + // However, if the user is manually dragging (PRESSED), we MUST teleport to allow infinite drag. + bool is_auto_scrolling = lv_obj_is_scrolling(_panel->get()) && !lv_obj_has_state(_panel->get(), LV_STATE_PRESSED); + + if (!is_auto_scrolling) { + if (current_scroll_x < left_trigger_limit) { + // Too far left (Set 1), warp right to Set 2 + // scrollBy(-val) increases scroll_x + _panel->scrollBy(-set_width_px, 0, LV_ANIM_OFF); + } else if (current_scroll_x > right_trigger_limit) { + // Too far right (Set 3), warp left to Set 2 + // scrollBy(+val) decreases scroll_x + _panel->scrollBy(set_width_px, 0, LV_ANIM_OFF); + } + } + int scroll_x = _panel->getScrollX(); // mclog::tagInfo(_tag, "scroll x: {}", scroll_x); diff --git a/firmware/main/apps/app_setup/app_setup.cpp b/firmware/main/apps/app_setup/app_setup.cpp index f8800c5..822ea72 100644 --- a/firmware/main/apps/app_setup/app_setup.cpp +++ b/firmware/main/apps/app_setup/app_setup.cpp @@ -36,25 +36,23 @@ void AppSetup::onOpen() { mclog::tagInfo(getAppInfo().name, "on open"); - LvglLockGuard lock; + // Reset state + _destroy_menu = false; + _need_warm_reset = false; + _magic_count = 0; _menu_sections = {{ "Connectivity", - { - // {"Set Up Wi-Fi", - // [&]() { - // _destroy_menu = true; - // _worker = std::make_unique(); - // }}, - {"App Bind Code", + {{"Set Up Wi-Fi", [&]() { - _destroy_menu = true; - _worker = std::make_unique(); + _destroy_menu = true; + _need_warm_reset = true; + _worker = std::make_unique(); }}}, }, { "Servo", - {{"Zero Calibration", + {{"Calibration", [&]() { _destroy_menu = true; _worker = std::make_unique(); @@ -66,15 +64,45 @@ void AppSetup::onOpen() }}}, }, { - "About", - {{fmt::format("FW Version: {}", common::FirmwareVersion), nullptr}}, + "Display", + {{"Brightness", + [&]() { + _destroy_menu = true; + _worker = std::make_unique(); + }}}, }, { - "End", - {{"Quit", [&]() { close(); }}}, + "System", + {{"Timezone", + [&]() { + _destroy_menu = true; + _worker = std::make_unique(); + }}}, + }, + { + "About", + {{fmt::format("FW Version: {}", common::FirmwareVersion), + [&]() { + _magic_count++; + if (_magic_count >= 10) { + _magic_count = 0; + _destroy_menu = true; + _worker = std::make_unique(); + } + }}, + {"Factory Reset", + [&]() { + _destroy_menu = true; + _worker = std::make_unique(); + }}}, }}; + LvglLockGuard lock; + _menu_page = std::make_unique(_menu_sections); + + view::create_home_indicator([&]() { close(); }); + view::create_status_bar(); } void AppSetup::onRunning() @@ -99,6 +127,9 @@ void AppSetup::onRunning() } GetStackChan().update(); + + view::update_home_indicator(); + view::update_status_bar(); } void AppSetup::onClose() @@ -107,5 +138,14 @@ void AppSetup::onClose() LvglLockGuard lock; + _menu_sections.clear(); _menu_page.reset(); + _worker.reset(); + + view::destroy_home_indicator(); + view::destroy_status_bar(); + + if (_need_warm_reset) { + GetHAL().requestWarmReboot(3); + } } diff --git a/firmware/main/apps/app_setup/app_setup.h b/firmware/main/apps/app_setup/app_setup.h index 4eb05dc..13360bb 100644 --- a/firmware/main/apps/app_setup/app_setup.h +++ b/firmware/main/apps/app_setup/app_setup.h @@ -28,5 +28,8 @@ class AppSetup : public mooncake::AppAbility { std::vector _menu_sections; std::unique_ptr _menu_page; std::unique_ptr _worker; - bool _destroy_menu = false; + + bool _destroy_menu = false; + bool _need_warm_reset = false; + int _magic_count = 0; }; diff --git a/firmware/main/apps/app_setup/view/view.cpp b/firmware/main/apps/app_setup/view/view.cpp index 1b5368c..f59d498 100644 --- a/firmware/main/apps/app_setup/view/view.cpp +++ b/firmware/main/apps/app_setup/view/view.cpp @@ -12,11 +12,12 @@ SelectMenuPage::SelectMenuPage(std::vector sections) : _sections(st { _pannel = std::make_unique(lv_screen_active()); _pannel->setSize(320, 240); - _pannel->setBgColor(lv_color_hex(0xffffff)); - _pannel->setPadding(10, 24, 0, 0); + _pannel->setBgColor(lv_color_hex(0xEDF4FF)); + _pannel->setPadding(30, 72, 0, 0); _pannel->setBorderWidth(0); _pannel->setRadius(0); _pannel->setScrollDir(LV_DIR_VER); + _pannel->setScrollbarMode(LV_SCROLLBAR_MODE_ACTIVE); int cursor_y = 10; @@ -70,7 +71,7 @@ void SelectMenuPage::create_item_button(int y, const MenuItem& item, int section btn->setBgColor(lv_color_hex(0xB8D3FD)); btn->setBorderWidth(0); btn->setShadowWidth(0); - btn->setRadius(12); + btn->setRadius(18); btn->label().setText(item.label); btn->label().setTextFont(&lv_font_montserrat_24); diff --git a/firmware/main/apps/app_setup/workers/about.cpp b/firmware/main/apps/app_setup/workers/about.cpp new file mode 100644 index 0000000..6eb7a3b --- /dev/null +++ b/firmware/main/apps/app_setup/workers/about.cpp @@ -0,0 +1,296 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +// Ref: https://dribbble.com/shots/21953371-WeStud-Creative-Log-In-For-The-Educational-Platform +// Idea by cxbbb +#include "workers.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace uitk; +using namespace uitk::lvgl_cpp; +using namespace setup_workers; + +static std::string _tag = "Setup-About"; + +class Egg { +public: + class Face { + public: + Face(lv_obj_t* parent, const Vector2i& position, float stiffness, float damping, int bodyHeight) + { + const Vector2 logo_size = {64, 48}; + const uint32_t color = 0x36064D; + + _position = position; + + _logo = std::make_unique(parent); + _logo->setSize(logo_size.width, logo_size.height); + _logo->align(LV_ALIGN_TOP_LEFT, position.x, position.y); + _logo->setBgOpa(LV_OPA_TRANSP); + _logo->removeFlag(LV_OBJ_FLAG_SCROLLABLE); + _logo->setPaddingAll(0); + _logo->setBorderWidth(0); + _logo->setRadius(0); + + _left_eye = std::make_unique(_logo->get()); + _left_eye->align(LV_ALIGN_CENTER, -15, -3); + _left_eye->setBgColor(lv_color_hex(color)); + _left_eye->removeFlag(LV_OBJ_FLAG_SCROLLABLE); + _left_eye->setRadius(LV_RADIUS_CIRCLE); + _left_eye->setBorderWidth(0); + _left_eye->setSize(6, 6); + + _right_eye = std::make_unique(_logo->get()); + _right_eye->align(LV_ALIGN_CENTER, 15, -3); + _right_eye->setBgColor(lv_color_hex(color)); + _right_eye->removeFlag(LV_OBJ_FLAG_SCROLLABLE); + _right_eye->setRadius(LV_RADIUS_CIRCLE); + _right_eye->setBorderWidth(0); + _right_eye->setSize(6, 6); + + _mouth = std::make_unique(_logo->get()); + _mouth->align(LV_ALIGN_CENTER, 0, 5); + _mouth->setBgColor(lv_color_hex(color)); + _mouth->removeFlag(LV_OBJ_FLAG_SCROLLABLE); + _mouth->setBorderWidth(0); + _mouth->setSize(18, 2); + _mouth->setRadius(0); + + _look_at_anim.x.springOptions().stiffness = stiffness; + _look_at_anim.x.springOptions().damping = damping; + _look_at_anim.y.springOptions() = _look_at_anim.x.springOptions(); + + _look_at_anim.teleport(_position.x, _position.y + bodyHeight); + update(); + } + + void lookAt(float x, float y, bool instant = false) + { + // x, y are expected to be in range [-1, 1] + const float max_x_offset = 17.0f; + const float max_y_offset = 13.0f; + + float target_x = _position.x + x * max_x_offset; + float target_y = _position.y + y * max_y_offset; + + if (instant) { + _logo->setPos(target_x, target_y); + _look_at_anim.teleport(target_x, target_y); + } else { + _look_at_anim.move(target_x, target_y); + } + } + + void update() + { + _look_at_anim.update(); + if (!_look_at_anim.done()) { + _logo->setPos(_look_at_anim.directValue().x, _look_at_anim.directValue().y); + } + + // Update blink + uint32_t now = GetHAL().millis(); + if (_next_blink_time == 0) { + _next_blink_time = now + Random::getInstance().getInt(100, 3000); + } + + if (now > _next_blink_time) { + if (_is_blinking) { + _is_blinking = false; + _left_eye->setHidden(false); + _right_eye->setHidden(false); + _next_blink_time = now + Random::getInstance().getInt(2000, 6000); + } else { + _is_blinking = true; + _left_eye->setHidden(true); + _right_eye->setHidden(true); + _next_blink_time = now + Random::getInstance().getInt(60, 200); + } + } + } + + private: + std::unique_ptr _logo; + std::unique_ptr _left_eye; + std::unique_ptr _right_eye; + std::unique_ptr _mouth; + Vector2i _position; + AnimateVector2 _look_at_anim; + uint32_t _next_blink_time = 0; + bool _is_blinking = false; + }; + + class Cube { + public: + struct MetaData_t { + Vector2i position; + Vector2i size; + uint32_t color = 0; + Vector2i facePosition; + float stiffness = 200.0f; + float damping = 20.0f; + }; + + Cube(lv_obj_t* parent, const MetaData_t& metaData) + { + _position = metaData.position; + _face_position = metaData.facePosition; + + _body = std::make_unique(parent); + _body->setBgColor(lv_color_hex(metaData.color)); + _body->align(LV_ALIGN_TOP_LEFT, metaData.position.x, metaData.position.y); + _body->setBorderWidth(0); + _body->setSize(metaData.size.x, metaData.size.y); + _body->setRadius(0); + _body->setPaddingAll(0); + + _face = std::make_unique(parent, metaData.facePosition, metaData.stiffness, metaData.damping, + metaData.size.y); + + _body_anim.x.springOptions().stiffness = metaData.stiffness; + _body_anim.x.springOptions().damping = metaData.damping; + _body_anim.y.springOptions() = _body_anim.x.springOptions(); + + _body_anim.teleport(_position.x, _position.y + metaData.size.y); + moveTo(1, 0); + } + + void update(int tpX, int tpY) + { + auto target = getTiltTarget(tpX, tpY); + + // Update face anim + _face->lookAt(target.x, target.y); + _face->update(); + + // Update body anim + moveTo(target.x, target.y); + _body_anim.update(); + if (!_body_anim.done()) { + _body->setPos(_body_anim.directValue().x, _body_anim.directValue().y); + } + } + + Vector2 getTiltTarget(int tpX, int tpY) + { + Vector2 position; + + if (tpX < 0 || tpY < 0) { + position.x = 1.0f; + position.y = 0.0f; + } else { + // Face center + float cx = _face_position.x + 32.0f; + float cy = _face_position.y + 24.0f; + + float x = (tpX - cx) / 160.0f; + float y = (tpY - cy) / 120.0f; + + position.x = uitk::clamp(x, -1.0f, 1.0f); + position.y = uitk::clamp(y, -1.0f, 1.0f); + } + + return position; + } + + void moveTo(float x, float y, bool instant = false) + { + // x, y are expected to be in range [-1, 1] + const float max_x_offset = 5.0f; + const float max_y_offset = 10.0f; + + float target_x = _position.x + x * max_x_offset; + float target_y = _position.y + y * max_y_offset; + + if (instant) { + _body->setPos(target_x, target_y); + _body_anim.teleport(target_x, target_y); + } else { + _body_anim.move(target_x, target_y); + } + } + + private: + std::unique_ptr _body; + std::unique_ptr _face; + Vector2i _position; + Vector2i _face_position; + AnimateVector2 _body_anim; + }; + + inline static const std::vector CubeDefines = { + {{54, 55}, {121, 185 + 10}, 0xBE70E5, {83, 55}, 50.0f, 14.0f}, // + {{107, 131}, {103, 109 + 10}, 0xDA4848, {133, 131}, 100.0f, 16.0f}, // + {{25, 166}, {89, 74 + 10}, 0x76D2DB, {37, 164}, 200.0f, 28.0f}, // + {{136, 199}, {121, 42 + 10}, 0xF5AA79, {178, 196}, 25.0f, 10.0f}, // + }; + + void init() + { + _panel = std::make_unique(lv_screen_active()); + _panel->setBgColor(lv_color_hex(0xF7F6E5)); + _panel->align(LV_ALIGN_CENTER, 0, 0); + _panel->setBorderWidth(0); + _panel->setSize(320, 240); + _panel->setRadius(0); + _panel->setPaddingAll(0); + _panel->removeFlag(LV_OBJ_FLAG_SCROLLABLE); + + for (const auto& metaData : CubeDefines) { + _cubes.push_back(std::make_unique(_panel->get(), metaData)); + } + } + + void update() + { + int tpX = -1; + int tpY = -1; + + lv_indev_t* indev = GetHAL().lvTouchpad; + if (indev) { + lv_indev_state_t state = lv_indev_get_state(indev); + if (state == LV_INDEV_STATE_PR) { + lv_point_t curr_point; + lv_indev_get_point(indev, &curr_point); + tpX = curr_point.x; + tpY = curr_point.y; + } + } + + for (auto& cube : _cubes) { + cube->update(tpX, tpY); + } + } + +private: + std::unique_ptr _panel; + std::vector> _cubes; +}; +std::unique_ptr _egg; + +FwVersionWorker::FwVersionWorker() +{ + _egg = std::make_unique(); + _egg->init(); +} + +FwVersionWorker::~FwVersionWorker() +{ + _egg.reset(); +} + +void FwVersionWorker::update() +{ + if (GetHAL().millis() - _last_tick > 16) { + _last_tick = GetHAL().millis(); + _egg->update(); + } +} diff --git a/firmware/main/apps/app_setup/workers/connectivity.cpp b/firmware/main/apps/app_setup/workers/connectivity.cpp index 226d673..a282768 100644 --- a/firmware/main/apps/app_setup/workers/connectivity.cpp +++ b/firmware/main/apps/app_setup/workers/connectivity.cpp @@ -4,6 +4,8 @@ * SPDX-License-Identifier: MIT */ #include "workers.h" +#include +#include #include #include #include @@ -25,6 +27,9 @@ WifiSetupWorker::WifiSetupWorker() // Create default avatar auto avatar = std::make_unique(); avatar->init(lv_screen_active(), &lv_font_montserrat_24); + avatar->leftEye().setVisible(false); + avatar->rightEye().setVisible(false); + avatar->mouth().setVisible(false); GetStackChan().attachAvatar(std::move(avatar)); _app_config_signal_id = @@ -52,31 +57,45 @@ void WifiSetupWorker::update_state() if (_is_first_in) { _is_first_in = false; - auto& avatar = GetStackChan().avatar(); - avatar.leftEye().setVisible(false); - avatar.rightEye().setVisible(false); - avatar.mouth().setVisible(false); - avatar.setSpeech("Scan the QR code to download the \"StackChan\" app."); - auto& data = _state_app_download_data; - std::string qrcode_text = "todotodotodotodotodotodotodlotodotodotodoto"; + data.panel = std::make_unique(lv_screen_active()); + data.panel->setBgColor(lv_color_hex(0xEDF4FF)); + data.panel->align(LV_ALIGN_CENTER, 0, 0); + data.panel->setBorderWidth(0); + data.panel->setSize(320, 240); + data.panel->setRadius(0); + + data.title = std::make_unique